Pular para o conteúdo principal

Transaction Operations

Transaction DTO​

All transaction operations use TransactionDTO to pass data:

use ByJG\Wallets\DTO\TransactionDTO;

// Create with wallet ID and amount
$dto = TransactionDTO::create($walletId, 5000);

// Create empty (for partial operations)
$dto = TransactionDTO::createEmpty();

// Set optional properties
$dto->setDescription('Purchase payment')
->setCode('PMT')
->setReferenceId('order-12345')
->setReferenceSource('ecommerce')
->setUuid($customUuid); // Optional: auto-generated if not set

TransactionDTO Properties​

PropertyTypeDescription
walletIdintTarget wallet ID
amountintTransaction amount in smallest unit
descriptionstringHuman-readable description
codestringTransaction code for categorization (max 10 chars)
referenceIdstringExternal reference ID
referenceSourcestringSource system name
uuidstringUnique transaction identifier (auto-generated)

Add Funds (Deposit)​

Add funds immediately to a wallet:

$transaction = $transactionService->addFunds(
TransactionDTO::create($walletId, 10000)
->setDescription('Bank deposit')
->setCode('DEP')
->setReferenceId('bank-tx-456')
->setReferenceSource('bank-api')
);

echo "New balance: " . ($transaction->getBalance() / 100);
echo "Transaction ID: " . $transaction->getTransactionId();

Creates a Deposit (D) transaction that:

  • Increases balance by amount
  • Increases available by amount
  • Does not affect reserved

Withdraw Funds​

Remove funds immediately from a wallet:

$transaction = $transactionService->withdrawFunds(
TransactionDTO::create($walletId, 5000)
->setDescription('ATM withdrawal')
->setCode('ATM')
);

Creates a Withdraw (W) transaction that:

  • Decreases balance by amount
  • Decreases available by amount
  • Validates that available >= amount
  • Validates that available - amount >= minValue
Insufficient Funds

Throws AmountException if insufficient available funds or would violate minValue.

Transaction Types​

Immediate Transactions​

TypeCodeOperationDescription
BalanceBResetSets wallet to a specific balance, ignoring history
DepositDAddAdds funds immediately
WithdrawWSubtractRemoves funds immediately
RejectRReverseReverses a reserved transaction

Reserved Transactions​

TypeCodeOperationDescription
Deposit BlockedDBReserve for depositReserves space for incoming funds
Withdraw BlockedWBReserve for withdrawalBlocks funds for pending withdrawal

See Reserved Funds for details on pending transactions.

Retrieving Transactions​

Get Transaction by ID​

$transaction = $transactionService->getById($transactionId);

echo "Amount: " . ($transaction->getAmount() / 100);
echo "Type: " . $transaction->getTypeId();
echo "Balance after: " . ($transaction->getBalance() / 100);

Get Transactions by Wallet​

// Get all transactions for a wallet
$transactions = $transactionService->getByWallet($walletId);

// Get with limit and offset
$transactions = $transactionService->getByWallet(
walletId: $walletId,
limit: 50,
offset: 0
);

Get Transactions by Date Range​

$transactions = $transactionService->getByDate(
walletId: $walletId,
startDate: '2024-01-01',
endDate: '2024-01-31',
limit: 100,
offset: 0
);

Get Transactions by Reference​

$transactions = $transactionService->getByReference(
referenceSource: 'ecommerce',
referenceId: 'order-12345'
);

Get Reserved Transactions​

// Get all pending reserved transactions for a wallet
$reserved = $transactionService->getReservedTransactions($walletId);

foreach ($reserved as $tx) {
echo $tx->getTypeId(); // 'DB' or 'WB'
echo $tx->getAmount();
}

Get Transaction by UUID​

$transaction = $transactionService->getByUuid($uuid);

Check for Duplicate Transactions​

// Check if transaction with UUID already exists
$exists = $transactionService->existsTransactionByUuid($uuid);

if ($exists) {
// Handle duplicate - maybe return existing transaction
$transaction = $transactionService->getByUuid($uuid);
}

Transaction Entity Properties​

Core Properties​

PropertyTypeDescription
transactionIdintUnique transaction identifier
walletIdintWallet this transaction belongs to
walletTypeIdstringWallet type (for denormalization)
typeIdstringTransaction type (B/D/W/DB/WB/R)
amountintTransaction amount (always positive)
scaleintDecimal scale at time of transaction
datedatetimeTransaction timestamp

Balance Snapshots​

These represent wallet state after this transaction:

PropertyTypeDescription
balanceintTotal balance after transaction
reservedintReserved amount after transaction
availableintAvailable amount after transaction

Metadata​

PropertyTypeDescription
codestringTransaction code (e.g., 'DEP', 'PMT')
descriptionstringHuman-readable description
transactionParentIdint|nullParent transaction for accept/reject operations
referenceIdstring|nullExternal reference identifier
referenceSourcestring|nullExternal system name

Integrity Fields​

PropertyTypeDescription
uuidbinary(16)Unique transaction identifier for idempotency
previousUuidbinary(16)|nullUUID of previous transaction (chain integrity)
checksumstring(64)SHA-256 hash of transaction data

Helper Methods​

Float Conversions​

$transaction = $transactionService->getById($transactionId);

// Get values as floats based on scale
$amountFloat = $transaction->getAmountFloat(); // 50.00
$balanceFloat = $transaction->getBalanceFloat(); // 150.75
$reservedFloat = $transaction->getReservedFloat(); // 25.50
$availableFloat = $transaction->getAvailableFloat(); // 125.25

Checksum Validation​

// Calculate checksum for a transaction
$checksum = TransactionEntity::calculateChecksum($transaction);

// Validate checksum
$isValid = TransactionEntity::validateChecksum($transaction, $checksum);

if (!$isValid) {
throw new Exception('Transaction data integrity compromised!');
}

The checksum is calculated from:

SHA256(amount|balance|reserved|available|uuid|previousuuid)

Transaction Chain Integrity​

Every transaction links to the previous transaction via previousUuid, creating an immutable chain:

$transaction1 = $transactionService->addFunds(...); // previousUuid = null
$transaction2 = $transactionService->addFunds(...); // previousUuid = $transaction1->uuid
$transaction3 = $transactionService->withdrawFunds(...); // previousUuid = $transaction2->uuid

This ensures:

  1. Chronological ordering of transactions
  2. Tamper detection - any modification breaks the chain
  3. Auditability - can verify entire transaction history

Idempotency​

Use UUIDs to prevent duplicate transactions:

use ByJG\MicroOrm\Literal\HexUuidLiteral;

// Generate a UUID for this operation
$uuid = HexUuidLiteral::uuid();

$dto = TransactionDTO::create($walletId, 5000)
->setUuid($uuid)
->setDescription('Payment');

// First attempt - succeeds
$transaction = $transactionService->addFunds($dto);

// Retry with same UUID - will detect duplicate
if ($transactionService->existsTransactionByUuid($uuid)) {
// Return existing transaction instead of creating duplicate
$transaction = $transactionService->getByUuid($uuid);
}

Error Handling​

Common Exceptions​

use ByJG\Wallets\Exception\AmountException;
use ByJG\Wallets\Exception\TransactionException;
use ByJG\Wallets\Exception\WalletException;

try {
$transaction = $transactionService->withdrawFunds(
TransactionDTO::create($walletId, 100000)
);
} catch (AmountException $e) {
// Insufficient funds or invalid amount
echo "Amount error: " . $e->getMessage();
} catch (WalletException $e) {
// Wallet not found or invalid
echo "Wallet error: " . $e->getMessage();
} catch (TransactionException $e) {
// Transaction operation failed
echo "Transaction error: " . $e->getMessage();
}

Amount Validation​

// Amount must be positive
TransactionDTO::create($walletId, -100); // Throws AmountException

// Must respect minValue
$walletService->createWallet('USD', $userId, 1000, 2, 0);
$transactionService->withdrawFunds(
TransactionDTO::create($walletId, 2000) // Throws AmountException
);

Best Practices​

  1. Always use TransactionDTO

    // Good
    $dto = TransactionDTO::create($walletId, 5000)
    ->setDescription('Purchase')
    ->setCode('PMT');
    $transactionService->addFunds($dto);

    // Bad - don't create TransactionEntity directly
  2. Set meaningful descriptions and codes

    $dto->setDescription('Monthly subscription payment')
    ->setCode('SUB')
    ->setReferenceId('subscription-789')
    ->setReferenceSource('billing-system');
  3. Use reference fields for linking

    // Link to external order
    $dto->setReferenceSource('ecommerce')
    ->setReferenceId('order-12345');

    // Later, find all transactions for an order
    $txs = $transactionService->getByReference('ecommerce', 'order-12345');
  4. Handle idempotency for external operations

    $externalId = 'payment-provider-tx-123';

    // Check if already processed
    $existing = $transactionService->getByReference('payment-provider', $externalId);
    if (!empty($existing)) {
    return $existing[0]; // Already processed
    }

    // Process new transaction
    $dto = TransactionDTO::create($walletId, $amount)
    ->setReferenceSource('payment-provider')
    ->setReferenceId($externalId);
    return $transactionService->addFunds($dto);
  5. Use database transactions for complex operations

    $dbExecutor = $transactionService->getRepository()->getExecutor();

    $dbExecutor->beginTransaction();
    try {
    $tx1 = $transactionService->withdrawFunds($dto1);
    $tx2 = $transactionService->addFunds($dto2);
    $dbExecutor->commitTransaction();
    } catch (Exception $e) {
    $dbExecutor->rollbackTransaction();
    throw $e;
    }