Skip to main content

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;
    }