Pular para o conteúdo principal

Wallets

Sponsor Build Status Opensource ByJG GitHub source GitHub license GitHub release

A robust PHP library for managing digital wallets and financial transactions with full audit trails, transaction chain integrity, and support for reserved funds.

Features

  • Multiple wallets per user - Each user can have multiple wallets for different currencies
  • Multiple currencies - Support for any currency type (fiat, crypto, points, etc.)
  • Reserved funds - Pre-authorize and block funds for pending transactions
  • Transaction chain integrity - Immutable transaction history with UUID linking and checksums
  • Atomic operations - All balance updates are atomic and transactional
  • Flexible scale - Support for different decimal places (cents, satoshis, whole units)
  • Overdraft support - Optional negative balances with configurable minimum values
  • Audit trail - Complete transaction history with balance snapshots
  • Idempotent operations - UUID-based transaction deduplication
  • Extensible - Easily extend wallets and transactions with custom fields

Installation

composer require byjg/wallets

Quick Start

use ByJG\Wallets\Service\WalletService;
use ByJG\Wallets\Service\WalletTypeService;
use ByJG\Wallets\Service\TransactionService;
use ByJG\Wallets\Entity\WalletTypeEntity;
use ByJG\Wallets\Repository\WalletRepository;
use ByJG\Wallets\Repository\WalletTypeRepository;
use ByJG\Wallets\Repository\TransactionRepository;
use ByJG\Wallets\DTO\TransactionDTO;
use ByJG\AnyDataset\Db\Factory;

// Initialize database connection
$dbDriver = Factory::getDbInstance('mysql://user:pass@localhost/dbname');

// Initialize repositories
$walletTypeRepo = new WalletTypeRepository($dbDriver);
$transactionRepo = new TransactionRepository($dbDriver);
$walletRepo = new WalletRepository($dbDriver);

// Initialize services
$walletTypeService = new WalletTypeService($walletTypeRepo);
$transactionService = new TransactionService($transactionRepo, $walletRepo);
$walletService = new WalletService($walletRepo, $walletTypeService, $transactionService);

// Create a wallet type
$walletType = new WalletTypeEntity();
$walletType->setWalletTypeId('USD');
$walletType->setName('US Dollar');
$walletTypeService->update($walletType);

// Create a wallet with $100.00 initial balance (10000 cents)
$walletId = $walletService->createWallet('USD', 'user-123', 10000, 2);

// Add funds: $50.00
$transaction = $transactionService->addFunds(
TransactionDTO::create($walletId, 5000)
->setDescription('Deposit from bank account')
);

// Withdraw funds: $30.00
$transaction = $transactionService->withdrawFunds(
TransactionDTO::create($walletId, 3000)
->setDescription('Purchase payment')
);

// Reserve funds for pending withdrawal: $20.00
$reserve = $transactionService->reserveFundsForWithdraw(
TransactionDTO::create($walletId, 2000)
->setDescription('Pre-authorization')
);

// Accept the reservation
$transactionService->acceptFundsById($reserve->getTransactionId());

// Get wallet balance
$wallet = $walletService->getById($walletId);
echo "Available: " . ($wallet->getAvailable() / 100) . " USD\n";

Documentation

Full documentation is available at https://opensource.byjg.com/docs/php/wallets

Core Concepts

Integer Storage with Scale

All monetary amounts are stored as integers (BIGINT) representing the smallest currency unit:

// USD with scale=2 (cents)
$walletId = $walletService->createWallet('USD', 'user-123', 10000, 2);
// 10000 represents $100.00

// Bitcoin with scale=8 (satoshis)
$btcWalletId = $walletService->createWallet('BTC', 'user-123', 100000000, 8);
// 100000000 represents 1.00000000 BTC

// Loyalty points with scale=0 (whole units)
$pointsWalletId = $walletService->createWallet('POINTS', 'user-123', 1000, 0);
// 1000 represents 1000 points

Balance Components

Each wallet maintains three balance components:

  • balance - Total funds (reserved + available)
  • reserved - Funds held for pending transactions
  • available - Funds available for immediate use
$wallet = $walletService->getById($walletId);
echo "Balance: " . $wallet->getBalance(); // 10000 (total)
echo "Reserved: " . $wallet->getReserved(); // 2000 (blocked)
echo "Available: " . $wallet->getAvailable(); // 8000 (usable)

Transaction Types

TypeCodeDescription
BalanceBInitial balance or reset
DepositDAdd funds immediately
WithdrawWRemove funds immediately
Deposit BlockedDBReserve for incoming funds
Withdraw BlockedWBReserve funds for withdrawal
RejectRReverse a reserved transaction

Transaction Chain Integrity

Every transaction links to the previous via uuid and previousuuid, creating an immutable audit trail:

$tx1 = $transactionService->addFunds($dto1);  // previousuuid = null
$tx2 = $transactionService->addFunds($dto2); // previousuuid = $tx1->uuid
$tx3 = $transactionService->withdrawFunds($dto3); // previousuuid = $tx2->uuid

Each transaction includes a SHA-256 checksum for data integrity verification.

Use Cases

E-commerce Platform

// Reserve funds when order is placed
$reserve = $transactionService->reserveFundsForWithdraw(
TransactionDTO::create($walletId, 9999)
->setDescription('Order #12345')
->setReferenceSource('ecommerce')
->setReferenceId('order-12345')
);

// Capture payment when order ships
$transactionService->acceptFundsById($reserve->getTransactionId());

// Or cancel and release funds if order is cancelled
// $transactionService->rejectFundsById($reserve->getTransactionId());

Multi-Currency Wallets

// Create multiple wallets for same user
$usdWallet = $walletService->createWallet('USD', 'user-123', 100000, 2);
$eurWallet = $walletService->createWallet('EUR', 'user-123', 50000, 2);
$btcWallet = $walletService->createWallet('BTC', 'user-123', 5000000, 8);

// Transfer between wallets (with exchange rate logic in your app)
$walletService->transferFunds($usdWallet, $eurWallet, 10000);

Gaming/Betting Platform

// Reserve bet amount
$betReserve = $transactionService->reserveFundsForWithdraw(
TransactionDTO::create($walletId, 5000)
->setDescription('Bet on Game #789')
);

// User wins - reject withdrawal and add winnings
$transactionService->rejectFundsById($betReserve->getTransactionId());
$transactionService->addFunds(
TransactionDTO::create($walletId, 10000)
->setDescription('Bet winnings')
);

// User loses - accept withdrawal
// $transactionService->acceptFundsById($betReserve->getTransactionId());

Testing

# Start MySQL container
docker run --name mysql-container --rm \
-e MYSQL_ROOT_PASSWORD=password \
-p 3306:3306 -d mysql:8.0

# Run migrations
vendor/bin/migrate up mysql://root:password@localhost/test -path=db

# Run tests
vendor/bin/phpunit

API Reference

WalletService

  • createWallet(string $walletTypeId, string $userId, int $balance, int $scale = 2, int $minValue = 0, ?string $extra = null): int
  • getById(int $walletId): WalletEntity
  • getByUserId(string $userId, string $walletType = ""): array
  • getByWalletTypeId(string $walletTypeId): array
  • overrideBalance(int $walletId, int $newBalance, int $newScale = 2, int $newMinValue = 0, string $description = "Reset Balance"): ?int
  • partialBalance(int $walletId, int $balance, string $description = "Partial Balance"): TransactionEntity
  • closeWallet(int $walletId): ?int
  • transferFunds(int $walletSource, int $walletTarget, int $amount): array

TransactionService

  • addFunds(TransactionDTO $dto): TransactionEntity
  • withdrawFunds(TransactionDTO $dto): TransactionEntity
  • reserveFundsForWithdraw(TransactionDTO $dto): TransactionEntity
  • reserveFundsForDeposit(TransactionDTO $dto): TransactionEntity
  • acceptFundsById(int $transactionId): int
  • acceptFundsByUuid(string $uuid): int
  • rejectFundsById(int $transactionId): int
  • rejectFundsByUuid(string $uuid): int
  • acceptPartialFundsById(int $transactionId, TransactionDTO $transactionDTO, TransactionDTO $transactionRefundDTO): TransactionEntity
  • getById(int $transactionId): TransactionEntity
  • getByWallet(int $walletId, int $limit = null, int $offset = null): array
  • getByDate(int $walletId, string $startDate, string $endDate, int $limit = null, int $offset = null): array
  • getByReference(string $referenceSource, string $referenceId): array
  • getByUuid(string $uuid): ?TransactionEntity
  • existsTransactionByUuid(string $uuid): bool
  • getReservedTransactions(int $walletId): array

Dependencies


Open source ByJG