π° NextGate Payment & Financial System Architecture
π Table of Contents
- System Overview
- Core Components
- Payment Flow
- Money Movement Architecture
- Ledger System (Double-Entry Bookkeeping)
- Escrow Mechanism
- Domain-Specific Handling
- Transaction History
- Key Design Patterns
π― System Overview
NextGate uses a universal payment orchestration system that handles payments across multiple domains (Products, Events, etc.) through a unified interface while maintaining domain-specific business logic.
High-Level Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT LAYER β
β (Web/Mobile Apps making API calls to checkout endpoints) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONTROLLER LAYER β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ β
β β ProductCheckout β β EventCheckout β β Wallet β β
β β Controller β β Controller β β Controller β β
β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CHECKOUT SERVICE LAYER β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ProductCheckoutService β EventCheckoutService β β
β β - Create checkout β - Create checkout β β
β β - Validate items β - Validate tickets β β
β β - Hold inventory β - Hold tickets β β
β β - Calculate pricing β - Calculate pricing β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β UNIVERSAL PAYMENT ORCHESTRATION β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PaymentOrchestrator β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β’ Receives: (sessionId, domain: PRODUCT/EVENT) β β β
β β β β’ Validates session status & expiration β β β
β β β β’ Routes to appropriate payment processor β β β
β β β β’ Handles: SUCCESS | FAILED | PENDING β β β
β β β β’ Publishes events for async processing β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UniversalCheckoutSessionService β β
β β (Fetches session from correct domain repository) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PAYMENT PROCESSOR LAYER β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β WalletPayment β β ExternalPayment β β
β β Processor β β Processor β β
β β ββββββββββββββββββββ β β ββββββββββββββββββββ β β
β β β β’ Check balance β β β β β’ M-Pesa β β β
β β β β’ Extract payee β β β β β’ Tigo Pesa β β β
β β β β’ Call escrow β β β β β’ Airtel Money β β β
β β ββββββββββββββββββββ β β β β’ Cards β β β
β ββββββββββββββββββββββββ β ββββββββββββββββββββ β β
β β (Not yet implemented)β β
β ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STRATEGY PATTERN - SESSION METADATA β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SessionMetadataExtractor Registry β β
β β ββββββββββββββββββββββ ββββββββββββββββββββββ β β
β β β ProductSession β β EventSession β β β
β β β MetadataExtractor β β MetadataExtractor β β β
β β β ββββββββββββββββββ β β ββββββββββββββββββ β β β
β β β β Extract payee: β β β β Extract payee: β β β β
β β β β Shop Owner β β β β Event Organizerβ β β β
β β β ββββββββββββββββββ β β ββββββββββββββββββ β β β
β β ββββββββββββββββββββββ ββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FINANCIAL CORE LAYER β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β EscrowService β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β holdMoney(session, payer, payee, amount) β β β
β β β β’ Generate escrow number β β β
β β β β’ Calculate fees (5% platform fee) β β β
β β β β’ Create escrow entity β β β
β β β β’ Move money via LedgerService β β β
β β β β’ Create transaction history β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β LedgerService β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Double-Entry Bookkeeping System β β β
β β β createEntry(debitAccount, creditAccount, amount) β β β
β β β β’ Validate accounts β β β
β β β β’ Generate entry number β β β
β β β β’ DEBIT one account β β β
β β β β’ CREDIT another account β β β
β β β β’ Update balances atomically β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β TransactionHistoryService β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β β’ Records user-facing transaction history β β β
β β β β’ Links to ledger entries β β β
β β β β’ Tracks DEBIT/CREDIT/NEUTRAL directions β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DATABASE LAYER β
β ββββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββββ β
β β Checkout β β Escrow β β Ledger Accounts β β
β β Sessions β β Accounts β β & Entries β β
β β (JSONB) β β (Relational) β β (Relational) β β
β ββββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββββ β
β ββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ β
β β Wallets β β Transaction History β β
β β (Metadata) β β (User-facing records) β β
β ββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EVENT & CALLBACK LAYER (Async) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β PaymentCompletedEvent β β
β β (Spring ApplicationEvent) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
β β ProductPayment β β EventPayment β β
β β CompletedListener β β CompletedListener β β
β β ββββββββββββββββββββ β β ββββββββββββββββββββ β β
β β β β’ Create orders β β β β β’ Create booking β β β
β β β β’ Handle group β β β β β’ Reserve ticketsβ β β
β β β β’ Handle install β β β β β’ Generate QR β β β
β β β β’ Clear cart β β β β β’ Send emails β β β
β β ββββββββββββββββββββ β β ββββββββββββββββββββ β β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π§ Core Components
1. PayableCheckoutSession (Contract Interface)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PayableCheckoutSession Interface β
β (Universal contract for all checkout sessions) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Required Methods: β
β β’ getSessionId() : UUID β
β β’ getSessionDomain() : CheckoutSessionsDomains β
β β’ getPayer() : AccountEntity β
β β’ getTotalAmount() : BigDecimal β
β β’ getCurrency() : String β
β β’ getStatus() : CheckoutSessionStatus β
β β’ isExpired() : boolean β
β β’ canRetryPayment() : boolean β
β β’ getPaymentAttemptCount() : int β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β β
ββββββββββββββββ ββββββββββββββββ
β β
βββββββββββββββββββββ ββββββββββββββββββββββ
β ProductCheckout β β EventCheckout β
β SessionEntity β β SessionEntity β
β (implements β β (implements β
β interface) β β interface) β
βββββββββββββββββββββ ββββββββββββββββββββββ
2. Payment Orchestrator
The central coordinator that:
- β Validates checkout session
- β Routes to correct payment processor
- β Handles callbacks
- β Publishes events
- β Returns unified response
3. Strategy Pattern Components
SessionMetadataExtractor Strategy
Purpose: Extract domain-specific payee information
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SessionMetadataExtractorRegistry β
β (Routes to correct extractor based on domain) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
ββββββββββββββββββββ ββββββββββββββββββββ
β Product Domain β β Event Domain β
ββββββββββββββββββββ€ ββββββββββββββββββββ€
β extractPayee(): β β extractPayee(): β
β β’ Get shopId β β β’ Get eventId β
β β’ Fetch shop β β β’ Fetch event β
β β’ Return owner β β β’ Return organizerβ
ββββββββββββββββββββ ββββββββββββββββββββ
PostPaymentHandler Strategy
Purpose: Handle domain-specific post-payment logic
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostPaymentHandlerRegistry β
β (Routes to correct handler based on domain) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
ββββββββββββββββββββ ββββββββββββββββββββ
β Product Domain β β Event Domain β
ββββββββββββββββββββ€ ββββββββββββββββββββ€
β β’ Handle group β β β’ Update session β
β β’ Handle install β β β’ Async booking β
β β’ Clear cart β β via listener β
β β’ Update session β ββββββββββββββββββββ
ββββββββββββββββββββ
πΈ Payment Flow - Step by Step
Step 1: User Initiates Payment
User clicks "Pay Now"
β
POST /api/v1/e-commerce/checkout-sessions/{sessionId}/process-payment
OR
POST /api/v1/e-events/checkout/{sessionId}/payment
Step 2: Controller β Service
Controller receives request
β
Calls: checkoutService.processPayment(sessionId)
β
Service delegates to: paymentOrchestrator.processPayment(sessionId, domain)
Step 3: Payment Orchestrator Validates
βββββββββββββββββββββββββββββββββββββββββββββββ
β PaymentOrchestrator.processPayment() β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Fetch session via UniversalCheckout β
β SessionService β
β β β
β 2. Validate session.status == β
β PENDING_PAYMENT β
β β β
β 3. Check if session.isExpired() β
β β β
β 4. Determine payment method (WALLET) β
β β β
β 5. Route to WalletPaymentProcessor β
βββββββββββββββββββββββββββββββββββββββββββββββ
Step 4: Wallet Payment Processing
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WalletPaymentProcessor.processPayment() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Extract payer = session.getPayer() β
β β β
β 2. Extract payee via SessionMetadataExtractor β
β β’ PRODUCT β Shop Owner β
β β’ EVENT β Event Organizer β
β β β
β 3. Get wallet balance via WalletService β
β β β
β 4. Validate: balance >= totalAmount β
β β β
β 5. Call: escrowService.holdMoney( β
β session, payer, payee, amount) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 5: Escrow Creation
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EscrowService.holdMoney() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Generate escrow number: "ESC-2025-000123" β
β β β
β 2. Calculate fees: β
β platformFee = amount Γ 5% β
β sellerAmount = amount - platformFee β
β β β
β 3. Create EscrowAccountEntity β
β - buyer: payer β
β - seller: payee β
β - totalAmount: amount β
β - status: HELD β
β β β
β 4. Create ledger account for escrow β
β β β
β 5. Move money via LedgerService: β
β createEntry( β
β debit: payerWalletAccount, β
β credit: escrowAccount, β
β amount: totalAmount β
β ) β
β β β
β 6. Create transaction history records β
β - PRODUCT_PURCHASE (DEBIT) for buyer β
β - ESCROW_HOLD (DEBIT) for admin tracking β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 6: Ledger Entry (Double-Entry Bookkeeping)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LedgerService.createEntry() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Input: β
β β’ debitAccount: Payer's Wallet Ledger Account β
β β’ creditAccount: Escrow Ledger Account β
β β’ amount: 50,000 TZS β
β β’ type: PURCHASE β
β β
β Process: β
β 1. Validate accounts are different β
β 2. Validate amount > 0 β
β 3. Check: debitAccount.balance >= amount β
β 4. Generate entry number: "LE-2025-000456" β
β β β
β 5. Create LedgerEntryEntity β
β β β
β 6. Update balances ATOMICALLY: β
β debitAccount.balance -= 50,000 β
β creditAccount.balance += 50,000 β
β β β
β 7. Save entry and accounts in transaction β
β β
β Result: Money moved from payer to escrow β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 7: Success Handling
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PaymentOrchestrator.handleSuccessfulPayment() β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Update session status: β
β PAYMENT_COMPLETED β
β β β
β 2. Set: session.escrowId = escrow.getId() β
β β β
β 3. Publish: PaymentCompletedEvent β
β (Async - handled by listeners) β
β β β
β 4. Call: paymentCallback.onPaymentSuccess() β
β (Routes to PostPaymentHandler) β
β β β
β 5. Return PaymentResponse to user β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 8: Async Processing (Event Listeners)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PaymentCompletedEvent Published β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
ββββββββββββββββββββββ ββββββββββββββββββββββ
β ProductPayment β β EventPayment β
β CompletedListener β β CompletedListener β
ββββββββββββββββββββββ€ ββββββββββββββββββββββ€
β @Async β β @Async β
β @Transactional β β @Transactional β
β β β β
β Switch by type: β β Actions: β
β β β β
β REGULAR_DIRECTLY: β β 1. Create booking β
β β’ Create order β β 2. Reserve tickets β
β β β 3. Generate QR β
β REGULAR_CART: β β 4. Send emails β
β β’ Create order β β 5. Update status: β
β β’ Clear cart β β COMPLETED β
β β ββββββββββββββββββββββ
β GROUP_PURCHASE: β
β β’ Join/create groupβ
β β
β INSTALLMENT: β
β β’ Create agreement β
β β’ Schedule paymentsβ
ββββββββββββββββββββββ
π΅ Money Movement Architecture
Account Types in the System
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LEDGER ACCOUNT TYPES β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. USER_WALLET β
β β’ One per user β
β β’ Stores user's balance β
β β’ Linked to WalletEntity β
β β
β 2. ESCROW β
β β’ One per transaction β
β β’ Temporary holding account β
β β’ Released to seller after delivery/completion β
β β
β 3. PLATFORM_REVENUE β
β β’ Single account for entire platform β
β β’ Collects all platform fees (5%) β
β β’ Never debited (only credited) β
β β
β 4. PLATFORM_RESERVE β
β β’ Emergency fund β
β β’ For refunds, disputes, etc. β
β β
β 5. EXTERNAL_MONEY_IN β
β β’ Virtual account β
β β’ Represents money coming from outside β
β β’ Source for wallet top-ups β
β β
β 6. EXTERNAL_MONEY_OUT β
β β’ Virtual account β
β β’ Represents money leaving platform β
β β’ Destination for withdrawals β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Money Flow: Purchase Transaction
STEP 1: PAYMENT (Buyer β Escrow)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββ βββββββββββββββ
β Buyer's β 50,000 TZS β Escrow β
β Wallet β ββββββββββββββ β Account β
β (DEBIT) β β (CREDIT) β
βββββββββββββββ βββββββββββββββ
β
Balance: 100,000 β 50,000 Balance: 0 β 50,000
Ledger Entry: LE-2025-000456
β’ DEBIT: Buyer Wallet -50,000 TZS
β’ CREDIT: Escrow Account +50,000 TZS
STEP 2: ESCROW RELEASE (Escrow β Seller + Platform)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββ
β Escrow β
β Account β 47,500 TZS (95%)
β (DEBIT) β ββββββββββββββ βββββββββββββββ
β β β Seller's β
βββββββββββββββ β Wallet β
β β (CREDIT) β
β βββββββββββββββ
β
β 2,500 TZS (5%)
βββββββββββββββ ββββββββββββββββββββ
β Platform β
β Revenue Account β
β (CREDIT) β
ββββββββββββββββββββ
Ledger Entries (Split):
Entry 1: LE-2025-000457
β’ DEBIT: Escrow Account -47,500 TZS
β’ CREDIT: Seller Wallet +47,500 TZS
Entry 2: LE-2025-000458
β’ DEBIT: Escrow Account -2,500 TZS
β’ CREDIT: Platform Revenue +2,500 TZS
Final Escrow Balance: 50,000 β 0 TZS
Money Flow: Wallet Top-Up
WALLET TOP-UP (External β User Wallet)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββ βββββββββββββββ
β External β 100,000 TZS β User's β
β Money In β βββββββββββββ β Wallet β
β (Virtual) β β (CREDIT) β
β (DEBIT) β βββββββββββββββ
βββββββββββββββββββ
Ledger Entry: LE-2025-000459
β’ DEBIT: External Money In -100,000 TZS (virtual)
β’ CREDIT: User Wallet +100,000 TZS
Transaction History:
β’ Type: WALLET_TOPUP
β’ Direction: CREDIT
β’ Amount: 100,000 TZS
Money Flow: Wallet Withdrawal
WALLET WITHDRAWAL (User Wallet β External)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββ βββββββββββββββββββ
β User's β 50,000 TZS β External β
β Wallet β βββββββββββββ β Money Out β
β (DEBIT) β β (Virtual) β
βββββββββββββββ β (CREDIT) β
βββββββββββββββββββ
Ledger Entry: LE-2025-000460
β’ DEBIT: User Wallet -50,000 TZS
β’ CREDIT: External Money Out +50,000 TZS (virtual)
Transaction History:
β’ Type: WALLET_WITHDRAWAL
β’ Direction: DEBIT
β’ Amount: 50,000 TZS
π Ledger System (Double-Entry Bookkeeping)
Core Principle
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FUNDAMENTAL ACCOUNTING RULE β
β β
β For every transaction: β
β TOTAL DEBITS = TOTAL CREDITS β
β β
β Money never appears or disappears. β
β It only moves between accounts. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Ledger Accounts
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LedgerAccountEntity β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ id: UUID β
β β’ accountNumber: "WALLET-johndoe" β
β β’ accountType: USER_WALLET | ESCROW | PLATFORM_... β
β β’ owner: AccountEntity (nullable for platform) β
β β’ currentBalance: BigDecimal (cached) β
β β’ currency: "TZS" β
β β’ isActive: Boolean β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Ledger Entries
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LedgerEntryEntity β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ id: UUID β
β β’ entryNumber: "LE-2025-000123" β
β β’ debitAccount: LedgerAccountEntity β
β β’ creditAccount: LedgerAccountEntity β
β β’ amount: BigDecimal β
β β’ entryType: PURCHASE | ESCROW_RELEASE | REFUND β
β β’ referenceType: "PRODUCT_CHECKOUT" β
β β’ referenceId: UUID (checkout session ID) β
β β’ description: "Product purchase payment" β
β β’ createdAt: LocalDateTime β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Example Ledger Flow
SCENARIO: User buys product for 10,000 TZS
BEFORE TRANSACTION:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Buyer Wallet: 100,000 TZS β
β Escrow Account: 0 TZS β
β Seller Wallet: 50,000 TZS β
β Platform Revenue: 5,000 TZS β
βββββββββββββββββββββββββββββββββββββββββββββββ
STEP 1: Create Ledger Entry (Payment)
βββββββββββββββββββββββββββββββββββββββββββββββ
β Entry: LE-2025-000123 β
β β’ DEBIT: Buyer Wallet -10,000 TZS β
β β’ CREDIT: Escrow +10,000 TZS β
βββββββββββββββββββββββββββββββββββββββββββββββ
AFTER PAYMENT:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Buyer Wallet: 90,000 TZS (-10,000) β
β Escrow Account: 10,000 TZS (+10,000) β
β Seller Wallet: 50,000 TZS (no change) β
β Platform Revenue: 5,000 TZS (no change) β
βββββββββββββββββββββββββββββββββββββββββββββββ
STEP 2: Create Ledger Entries (Escrow Release)
βββββββββββββββββββββββββββββββββββββββββββββββ
β Entry: LE-2025-000124 β
β β’ DEBIT: Escrow -9,500 TZS β
β β’ CREDIT: Seller Wallet +9,500 TZS β
β β
β Entry: LE-2025-000125 β
β β’ DEBIT: Escrow -500 TZS β
β β’ CREDIT: Platform Revenue +500 TZS β
βββββββββββββββββββββββββββββββββββββββββββββββ
AFTER ESCROW RELEASE:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Buyer Wallet: 90,000 TZS (no change) β
β Escrow Account: 0 TZS (-10,000) β
β Seller Wallet: 59,500 TZS (+9,500) β
β Platform Revenue: 5,500 TZS (+500) β
βββββββββββββββββββββββββββββββββββββββββββββββ
VERIFICATION (Double-Entry Rule):
Total Debits = 10,000 + 9,500 + 500 = 20,000 TZS β
Total Credits = 10,000 + 9,500 + 500 = 20,000 TZS β
π Escrow Mechanism
Escrow Lifecycle
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ESCROW ACCOUNT LIFECYCLE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CREATION HELD RELEASED
β β β
β β β
ββββββββββββ ββββββββββββ ββββββββββββ
β PAYMENT β β WAITING β β DELIVERY β
β PROCESSEDβ βββββββ β FOR β βββββββ β CONFIRMEDβ
β β β DELIVERY β β β
ββββββββββββ ββββββββββββ ββββββββββββ
β
β (Cancel/Dispute)
β
ββββββββββββ
β REFUNDED β
β OR β
β DISPUTED β
ββββββββββββ
Escrow Account Structure
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EscrowAccountEntity β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ escrowNumber: "ESC-2025-000123" β
β β’ checkoutSessionId: UUID β
β β’ sessionDomain: PRODUCT | EVENT β
β β’ buyer: AccountEntity β
β β’ seller: AccountEntity β
β β’ totalAmount: 50,000 TZS β
β β’ platformFeePercentage: 5.00% β
β β’ platformFeeAmount: 2,500 TZS β
β β’ sellerAmount: 47,500 TZS β
β β’ status: HELD | RELEASED | REFUNDED | DISPUTED β
β β’ ledgerAccountId: UUID β
β β’ createdAt: 2025-02-16 10:30:00 β
β β’ releasedAt: null β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Fee Calculation
ββββββββββββββββββββββββββββββββββββββββββββββ
β Escrow Fee Calculation β
ββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Total Amount: 50,000 TZS β
β Platform Fee (5%): 2,500 TZS β
β ββββββββββββββββββββββββββββββ β
β Seller Receives: 47,500 TZS β
β β
β Formula: β
β platformFee = total Γ (5 / 100) β
β sellerAmount = total - platformFee β
ββββββββββββββββββββββββββββββββββββββββββββββ
Escrow Operations
1. Hold Money (Payment)
escrowService.holdMoney(session, payer, payee, amount)
β
1. Create escrow entity
2. Calculate fees
3. Create escrow ledger account
4. Move money: payer wallet β escrow
5. Create transaction history
β
Result: Money held in escrow
2. Release Money (Delivery Confirmed)
escrowService.releaseMoney(escrowId)
β
1. Validate escrow.status = HELD
2. Get seller wallet ledger account
3. Get platform revenue account
4. Split money:
β’ 95% β seller wallet
β’ 5% β platform revenue
5. Update escrow.status = RELEASED
6. Create transaction history
β
Result: Money distributed
3. Refund Money (Order Cancelled)
escrowService.refundMoney(escrowId)
β
1. Validate escrow.status = HELD or DISPUTED
2. Get buyer wallet ledger account
3. Move money: escrow β buyer wallet
4. Update escrow.status = REFUNDED
5. Create transaction history
β
Result: Money returned to buyer
π Domain-Specific Handling
Product Domain
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PRODUCT CHECKOUT SESSION TYPES β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. REGULAR_DIRECTLY
ββββββββββββββββ
β’ Single product, direct purchase
β’ "Buy Now" button
β’ No cart involved
Flow:
User β Product Page β Buy Now β Checkout β Payment
β
Create Order Immediately
2. REGULAR_CART
ββββββββββββββββ
β’ Multiple products from cart
β’ Cart-based purchase
Flow:
User β Add to Cart β View Cart β Checkout β Payment
β
Create Order + Clear Cart
3. GROUP_PURCHASE
ββββββββββββββββ
β’ Group buying deal
β’ Wait for minimum participants
β’ Uses groupPrice (discounted)
Flow:
User β Join/Create Group β Payment (wallet only)
β
Group Instance Created/Joined
β
Wait for group completion β Create Orders
4. INSTALLMENT
ββββββββββββββββ
β’ Buy now, pay in installments
β’ Down payment required
β’ Monthly payments scheduled
Flow:
User β Select Plan β Pay Down Payment (wallet only)
β
Installment Agreement Created
β
Schedule Future Payments β Create Order (maybe)
Event Domain
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EVENT CHECKOUT SESSION β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
TICKET TYPES:
βββββββββββββ
1. PAID - Regular paid ticket
2. FREE - Free entry ticket
3. DONATION - Pay-what-you-want ticket
Flow:
User β Select Tickets β Add Attendee Info β Payment
β
ββββββββββββββββ΄βββββββββββββββ
β β
PAID TICKET FREE TICKET
β β
Create Escrow Skip Escrow (amount = 0)
Move Money β
β β
βββββββββ΄ββββββββββββββββββββββββββββ
β
Publish PaymentCompletedEvent
β
EventPaymentCompletedListener
β
ββββββββββββββββββββββββββ
β 1. Create Booking β
β 2. Reserve Tickets β
β 3. Generate QR Codes β
β 4. Send to Attendees β
ββββββββββββββββββββββββββ
π Transaction History
Transaction History provides a user-friendly view of all financial activities.
Structure
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TransactionHistory Entity β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ transactionRef: "#2025T000123" β
β β’ account: AccountEntity (user) β
β β’ type: PRODUCT_PURCHASE | WALLET_TOPUP | ... β
β β’ direction: DEBIT | CREDIT | NEUTRAL β
β β’ amount: 50,000 TZS β
β β’ title: "Product Purchase" β
β β’ description: "Payment for order #..." β
β β’ ledgerEntryId: UUID (link to ledger) β
β β’ referenceType: "PRODUCT_CHECKOUT" β
β β’ referenceId: UUID β
β β’ status: COMPLETED | PENDING | FAILED β
β β’ createdAt: 2025-02-16 10:30:00 β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Transaction Types
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSACTION TYPES β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β WALLET OPERATIONS: β
β β’ WALLET_TOPUP (CREDIT) β
β β’ WALLET_WITHDRAWAL (DEBIT) β
β β
β PRODUCT PURCHASES: β
β β’ PRODUCT_PURCHASE (DEBIT) - paid β
β β’ FREE_PRODUCT (NEUTRAL) - free β
β β’ CASH_PRODUCT_PAYMENT (NEUTRAL) - cash β
β β’ PRODUCT_REFUND (CREDIT) - refunded β
β β
β EVENT PURCHASES: β
β β’ EVENT_TICKET_PURCHASE (DEBIT) - paid ticket β
β β’ FREE_EVENT_TICKET (NEUTRAL) - free ticket β
β β’ CASH_EVENT_TICKET (NEUTRAL) - cash ticket β
β β’ EVENT_TICKET_REFUND (CREDIT) - refunded β
β β
β SELLER OPERATIONS: β
β β’ SALE (CREDIT) - money earned β
β β’ SALE_REFUND (DEBIT) - refund issuedβ
β β
β PLATFORM OPERATIONS: β
β β’ PLATFORM_FEE_COLLECTED (CREDIT) - platform fee β
β β
β ESCROW OPERATIONS: β
β β’ ESCROW_HOLD (DEBIT) - for tracking β
β β’ ESCROW_RELEASE (CREDIT) - for tracking β
β β’ ESCROW_REFUND (CREDIT) - for tracking β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Direction Types
ββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSACTION DIRECTIONS β
ββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β DEBIT (β) β
β β’ Money leaving your wallet β
β β’ Displayed as negative amount β
β β’ Examples: Purchases, Withdrawals β
β β
β CREDIT (+) β
β β’ Money entering your wallet β
β β’ Displayed as positive amount β
β β’ Examples: Top-ups, Refunds, Sales β
β β
β NEUTRAL (β) β
β β’ No wallet impact β
β β’ Used for: Free items, Cash payments β
β β’ Amount shown but doesn't affect balance β
ββββββββββββββββββββββββββββββββββββββββββββββ
π¨ Key Design Patterns
1. Strategy Pattern
Purpose: Handle domain-specific logic
Components:
β’ SessionMetadataExtractor - Extract payee
β’ PostPaymentHandler - Handle post-payment
Benefit: Easy to add new domains (Subscriptions, etc.)
2. Contract Interface (PayableCheckoutSession)
Purpose: Universal payment interface
Benefit:
β’ Payment system domain-agnostic
β’ Add new checkout types easily
β’ Type-safe polymorphism
3. Observer Pattern (Events)
Purpose: Async processing after payment
Components:
β’ PaymentCompletedEvent
β’ ProductPaymentCompletedListener
β’ EventPaymentCompletedListener
Benefit:
β’ Non-blocking payment response
β’ Decoupled order/booking creation
β’ Easy to add new listeners
4. Service Layer Pattern
Purpose: Separation of concerns
Layers:
Controller β Service β Orchestrator β Processor β Core
Benefit:
β’ Clear responsibilities
β’ Easy testing
β’ Maintainable code
5. Repository Pattern
Purpose: Data access abstraction
Components:
β’ ProductCheckoutSessionRepo
β’ EventCheckoutSessionRepo
β’ EscrowAccountRepo
β’ LedgerAccountRepo
Benefit:
β’ Database-agnostic
β’ Easy to mock for testing
π Security & Validation
Payment Validation Checklist
β Session status = PENDING_PAYMENT
β Session not expired
β Session belongs to authenticated user
β Wallet is active
β Sufficient balance
β Amount > 0
β No duplicate payment (no existing escrow)
β Payment method allowed for session type
β Inventory/tickets available
Transaction Safety
β All money movements in transactions
β Double-entry bookkeeping verified
β Atomic balance updates
β Idempotent operations
β Retry mechanism for failed operations
β Audit trail via transaction history
π³ Payment Scenarios & Special Cases
Overview of Payment Types
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PAYMENT SCENARIO MATRIX β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β PAID (Regular Payment) β
β β’ Amount > 0 β
β β’ Wallet payment (current) β
β β’ External payment (future: M-Pesa, cards, etc.) β
β β’ Creates escrow β
β β’ Platform fee collected (5%) β
β β
β FREE (No Payment) β
β β’ Amount = 0 β
β β’ No wallet deduction β
β β’ No escrow created β
β β’ No platform fee β
β β’ Direct order/booking creation β
β β
β CASH (Physical Payment) β
β β’ Amount > 0 β
β β’ Payment collected physically β
β β’ No wallet involvement β
β β’ No escrow created β
β β’ No ledger entry β
β β’ Tracked in transaction history only β
β β
β DONATION (Pay-What-You-Want) β
β β’ Amount >= 0 (user decides) β
β β’ Uses wallet payment β
β β’ Creates escrow if amount > 0 β
β β’ Platform fee collected if amount > 0 β
β β’ Limited to 1 per user β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Scenario 1: PAID TICKET/PRODUCT (Standard Flow)
USER JOURNEY:
βββββββββββββ
User selects paid ticket/product β Checkout β Pay with Wallet
PAYMENT FLOW:
βββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 1: Checkout Session Created β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ sessionType: REGULAR_DIRECTLY / EVENT_TICKET_PURCHASE β
β β’ pricing.total: 50,000 TZS β
β β’ status: PENDING_PAYMENT β
β β’ paymentIntent.provider: "WALLET" β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 2: User Initiates Payment β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β POST /checkout/{sessionId}/process-payment β
β β β
β PaymentOrchestrator.processPayment() β
β β β
β Validates: session.getTotalAmount() > 0 β β
β Routes to: WalletPaymentProcessor β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 3: Wallet Payment Processing β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β WalletPaymentProcessor: β
β 1. Check wallet balance: 100,000 TZS β β
β 2. Extract payee (shop owner/event organizer) β
β 3. Call escrowService.holdMoney() β
β β β
β Escrow Created: β
β β’ escrowNumber: "ESC-2025-000123" β
β β’ totalAmount: 50,000 TZS β
β β’ platformFee: 2,500 TZS (5%) β
β β’ sellerAmount: 47,500 TZS β
β β’ status: HELD β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 4: Ledger Entry (Double-Entry Bookkeeping) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Entry: LE-2025-000456 β
β β’ DEBIT: Buyer Wallet Account -50,000 TZS β
β β’ CREDIT: Escrow Account +50,000 TZS β
β β β
β Account Balances Updated: β
β β’ Buyer Wallet: 100,000 β 50,000 TZS β
β β’ Escrow: 0 β 50,000 TZS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 5: Transaction History Created β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Transaction 1 (Buyer's View): β
β β’ ref: "#2025T000789" β
β β’ type: PRODUCT_PURCHASE / EVENT_TICKET_PURCHASE β
β β’ direction: DEBIT β
β β’ amount: 50,000 TZS β
β β’ title: "Product Purchase" / "Ticket Purchase" β
β β’ status: COMPLETED β
β β β
β Transaction 2 (Admin Tracking): β
β β’ type: ESCROW_HOLD β
β β’ direction: DEBIT β
β β’ amount: 50,000 TZS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 6: Session Updated & Event Published β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ session.status β PAYMENT_COMPLETED β
β β’ session.escrowId β escrow.id β
β β’ session.completedAt β now() β
β β β
β PaymentCompletedEvent published β
β β β
β Async Listeners: β
β β’ Create order/booking β
β β’ Send notifications β
β β’ Clear cart (if applicable) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 7: Response to User β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentResponse: β
β β’ success: true β
β β’ status: SUCCESS β
β β’ message: "Payment processed successfully" β
β β’ escrowNumber: "ESC-2025-000123" β
β β’ amountPaid: 50,000 TZS β
β β’ platformFee: 2,500 TZS β
β β’ sellerAmount: 47,500 TZS β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT:
βββββββ
β Money moved from buyer to escrow
β Ledger balanced
β Transaction history recorded
β Order/booking created (async)
β User receives confirmation
Scenario 2: FREE TICKET/PRODUCT (Zero Payment)
USER JOURNEY:
βββββββββββββ
User selects free ticket/product β Checkout β Submit (no payment)
SPECIAL CHARACTERISTICS:
ββββββββββββββββββββββββ
β’ pricing.total = 0 TZS
β’ No wallet deduction
β’ No escrow created
β’ No platform fee
β’ Direct completion
PAYMENT FLOW:
βββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 1: Checkout Session Created β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ ticketPricingType: FREE β
β β’ pricing.total: 0 TZS β
β β’ status: PENDING_PAYMENT (initial) β
β β’ paymentIntent: null (no payment needed) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 2: Automatic Processing (No User Action) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β EventCheckoutServiceImpl.createCheckoutSession(): β
β β β
β Detects: ticket.ticketPricingType == FREE β
β β β
β Sets: session.status β PAYMENT_COMPLETED (immediately) β
β β β
β Calls: processFreeTicketCheckout(session) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 3: Free Checkout Processing β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β processFreeTicketCheckout(): β
β 1. session.markAsCompleted() β
β 2. session.completedAt β now() β
β 3. Save session β
β β β
β NO Escrow Creation β β
β NO Wallet Deduction β β
β NO Ledger Entry β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 4: Transaction History (Optional Tracking) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β trackFreeTransaction(): β
β β’ ref: "#2025T000790" β
β β’ type: FREE_PRODUCT / FREE_EVENT_TICKET β
β β’ direction: NEUTRAL (no balance impact) β
β β’ amount: 0 TZS β
β β’ title: "Free Ticket Booking" β
β β’ ledgerEntryId: null (no ledger) β
β β’ status: COMPLETED β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 5: Event Published β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentCompletedEvent( β
β sessionId, β
β domain: EVENT, β
β session, β
β escrow: null β NO ESCROW FOR FREE β
β ) β
β β β
β EventPaymentCompletedListener: β
β β’ Detects: escrow == null (free ticket) β
β β’ Creates booking β
β β’ Reserves tickets β
β β’ Generates QR codes β
β β’ Sends confirmation emails β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 6: Response to User β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentResponse: β
β β’ success: true β
β β’ status: SUCCESS β
β β’ message: "Free ticket booking confirmed!" β
β β’ amountPaid: 0 TZS β
β β’ escrowId: null β
β β’ escrowNumber: null β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
COMPARISON: FREE vs PAID
βββββββββββββββββββββββββ
ββββββββββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ
β Operation β PAID β FREE β
ββββββββββββββββββββββΌββββββββββββββΌββββββββββββββ€
β Wallet Check β YES β NO β
β Escrow Created β YES β NO β
β Ledger Entry β YES β NO β
β Platform Fee β YES β NO β
β Transaction Historyβ YES β OPTIONAL β
β Order/Booking β ASYNC β ASYNC β
β User Balance Impactβ YES β NO β
ββββββββββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ
RESULT:
βββββββ
β No money moved
β No escrow created
β Session marked completed immediately
β Booking created (async)
β User receives free ticket
Scenario 3: CASH PAYMENT (Physical Money)
USER JOURNEY:
βββββββββββββ
User pays cash at door/venue β Staff creates checkout β Marks as cash
SPECIAL CHARACTERISTICS:
ββββββββββββββββββββββββ
β’ Amount > 0 (but not from wallet)
β’ Payment collected physically (offline)
β’ No wallet deduction
β’ No escrow (money never in system)
β’ No ledger entry (money outside system)
β’ Tracked in transaction history for records
β’ Used for: Door sales, in-person events
IMPLEMENTATION:
βββββββββββββββ
Option 1: Payment Method Detection (Current)
ββββββββββββββββββββββββββββββββββββββββββββ
PaymentOrchestrator detects paymentMethod == CASH
Option 2: Pricing Detection (Alternative)
ββββββββββββββββββββββββββββββββββββββββββ
Session has special cashPaymentIndicator flag
PAYMENT FLOW:
βββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 1: Checkout Session Created (Staff/User) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ sessionType: REGULAR_DIRECTLY / EVENT_TICKET_PURCHASE β
β β’ pricing.total: 20,000 TZS β
β β’ paymentIntent.provider: "CASH" (or detected later) β
β β’ status: PENDING_PAYMENT β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 2: Payment Processing β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β POST /checkout/{sessionId}/process-payment β
β β β
β PaymentOrchestrator.processPayment(): β
β β β
β Detects: paymentMethod == CASH β
β β β
β Routes to: handleCashCheckout(session) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 3: Cash Checkout Processing β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β handleCashCheckout(): β
β 1. session.status β COMPLETED (immediate) β
β 2. session.completedAt β now() β
β 3. Save session β
β β β
β NO Wallet Check β β
β NO Escrow Creation β β
β NO Ledger Entry β β
β NO Balance Deduction β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 4: Transaction History (Record Keeping) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β trackCashTransaction(): β
β β’ ref: "#2025T000791" β
β β’ type: CASH_PRODUCT_PAYMENT / CASH_EVENT_TICKET β
β β’ direction: NEUTRAL (no wallet impact) β
β β’ amount: 20,000 TZS β
β β’ title: "Cash Product Payment" β
β β’ description: "Cash payment for product (Session: ...)" β
β β’ ledgerEntryId: null (no ledger entry) β
β β’ status: COMPLETED β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 5: Event Published β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentCompletedEvent( β
β sessionId, β
β domain: PRODUCT/EVENT, β
β session, β
β escrow: null β NO ESCROW FOR CASH β
β ) β
β β β
β Listener processes: β
β β’ Creates order/booking β
β β’ NO escrow release needed β
β β’ NO platform fee collected β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 6: Response to User β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentResponse: β
β β’ success: true β
β β’ status: SUCCESS β
β β’ message: "Cash payment confirmed!" β
β β’ paymentMethod: CASH β
β β’ amountPaid: 20,000 TZS β
β β’ escrowId: null β
β β’ escrowNumber: null β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
USE CASES:
ββββββββββ
1. Event Door Sales
β’ User arrives at venue
β’ Pays cash at entrance
β’ Staff creates checkout & marks as cash
β’ User receives ticket immediately
2. In-Store Purchase
β’ Customer buys product in physical store
β’ Pays with physical cash
β’ Staff records sale in system
β’ Order created for tracking
3. COD (Cash on Delivery)
β’ User orders online
β’ Chooses cash payment
β’ Courier collects cash on delivery
β’ System updated after confirmation
COMPARISON: CASH vs WALLET
βββββββββββββββββββββββββββ
ββββββββββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ
β Operation β WALLET β CASH β
ββββββββββββββββββββββΌββββββββββββββΌββββββββββββββ€
β Money in System β YES β NO β
β Wallet Deduction β YES β NO β
β Escrow Created β YES β NO β
β Ledger Entry β YES β NO β
β Platform Fee β YES β NO* β
β Transaction Historyβ YES β YES β
β Balance Impact β YES β NO β
β Use Case β Online β Physical β
ββββββββββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ
* Platform fee for cash could be handled separately
in business reconciliation, outside the system
RESULT:
βββββββ
β No money in digital system
β No wallet/ledger impact
β Transaction recorded for tracking
β Order/booking created
β Physical cash collected separately
Scenario 4: DONATION TICKET (Pay-What-You-Want)
USER JOURNEY:
βββββββββββββ
User sees donation event β Decides amount β Pays via wallet
SPECIAL CHARACTERISTICS:
ββββββββββββββββββββββββ
β’ Amount >= 0 (user decides)
β’ Can be 0 (free) or any amount
β’ Only available for EVENT domain (not products)
β’ Wallet payment required (no external payments)
β’ Limited to 1 ticket per person
β’ No tickets for other attendees
β’ Online purchase only (no door sales)
TICKET TYPE:
ββββββββββββ
ticketPricingType: DONATION
PAYMENT FLOW:
βββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 1: User Selects Donation Amount β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Donation UI: β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
β β How much would you like to contribute? β β
β β β β
β β Suggested: 5,000 TZS β β
β β β β
β β ββββββββββ ββββββββββ ββββββββββ β β
β β β 5,000 β β 10,000 β β 20,000 β β β
β β ββββββββββ ββββββββββ ββββββββββ β β
β β β β
β β Or enter custom amount: [________] TZS β β
β β β β
β β [ ] I'll just attend for free (0 TZS) β β
β ββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 2: Checkout Session Created β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β CreateEventCheckoutRequest: β
β β’ ticketTypeId: <donation-ticket-id> β
β β’ ticketsForMe: 1 (must be exactly 1) β
β β’ donationAmount: 15,000 TZS (user chose) β
β β’ otherAttendees: [] (must be empty) β
β β β
β Validation: β
β β ticketPricingType == DONATION β
β β totalQuantity == 1 (donation tickets limited) β
β β No other attendees β
β β SalesChannel == ONLINE_ONLY β
β β β
β Session Created: β
β β’ ticketDetails.unitPrice: 0 TZS (ticket is free) β
β β’ ticketDetails.donationAmount: 15,000 TZS β
β β’ pricing.total: 15,000 TZS β
β β’ paymentIntent.provider: "WALLET" (forced) β
β β’ status: PENDING_PAYMENT β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Step 3: Payment Branch (Based on Amount) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββ΄βββββββββββββββββββ
β β
ββββββββββββββββββββ ββββββββββββββββββββ
β Amount = 0 β β Amount > 0 β
β (Free Donation) β β (Paid Donation) β
ββββββββββββββββββββ ββββββββββββββββββββ
β β
[SAME AS FREE [SAME AS PAID
TICKET FLOW] TICKET FLOW]
β β
No Escrow Escrow Created
No Wallet Wallet Deducted
Direct Complete Platform Fee (5%)
SCENARIO A: USER DONATES 15,000 TZS
ββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Payment Processing (Paid Donation) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentOrchestrator: β
β β’ Detects: session.getTotalAmount() = 15,000 TZS > 0 β
β β’ Routes to: WalletPaymentProcessor β
β β β
β WalletPaymentProcessor: β
β β’ Check balance: sufficient β β
β β’ Extract payee: event organizer β
β β’ Create escrow: 15,000 TZS β
β β β
β Escrow Created: β
β β’ totalAmount: 15,000 TZS β
β β’ platformFee: 750 TZS (5%) β
β β’ sellerAmount: 14,250 TZS β
β β β
β Ledger Entry: β
β β’ DEBIT: Buyer Wallet -15,000 TZS β
β β’ CREDIT: Escrow +15,000 TZS β
β β β
β Transaction History: β
β β’ type: DONATION_EVENT_TICKET β
β β’ direction: DEBIT β
β β’ amount: 15,000 TZS β
β β’ title: "Donation Ticket" β
β β’ metadata: { donationAmount: 15000, suggested: 5000 } β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SCENARIO B: USER CHOOSES FREE (0 TZS)
βββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Payment Processing (Free Donation) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PaymentOrchestrator: β
β β’ Detects: session.getTotalAmount() = 0 TZS β
β β’ Routes to: handleFreeCheckout() β
β β β
β Free Processing: β
β β’ NO wallet check β
β β’ NO escrow β
β β’ NO ledger entry β
β β’ Direct session completion β
β β β
β Transaction History (Optional): β
β β’ type: FREE_EVENT_TICKET (or DONATION_EVENT_TICKET) β
β β’ direction: NEUTRAL β
β β’ amount: 0 TZS β
β β’ title: "Free Donation Ticket" β
β β’ metadata: { donationAmount: 0, suggested: 5000 } β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
VALIDATION RULES FOR DONATION:
βββββββββββββββββββββββββββββββ
β Ticket type must be DONATION
β Quantity must be exactly 1
β No other attendees allowed
β Sales channel must be ONLINE_ONLY
β Payment method forced to WALLET (no cash, no external)
β maxQuantityPerUser enforced (1 per person)
Checked in: EventCheckoutValidations.validateDonationTicket()
COMPARISON: DONATION vs REGULAR PAID
βββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ
β Feature β REGULAR β DONATION β
ββββββββββββββββββββββββΌββββββββββββββΌββββββββββββββ€
β Fixed Price β YES β NO β
β User Sets Amount β NO β YES β
β Can be Free (0) β NO β YES β
β Quantity Limit β Variable β 1 Only β
β Other Attendees β Allowed β Blocked β
β Sales Channel β Any β Online Only β
β Payment Method β Any β Wallet Only β
β Escrow (if > 0) β YES β YES β
β Platform Fee (if >0) β YES β YES β
ββββββββββββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ
USE CASES:
ββββββββββ
1. Charity Event
β’ Event organizer hosts fundraiser
β’ Tickets free, but donations welcomed
β’ Users choose contribution amount
β’ Platform collects fee only on donations > 0
2. Community Event
β’ Local meetup or workshop
β’ Entry is free
β’ Optional donation to cover costs
β’ Organizer receives donations minus platform fee
3. Awareness Campaign
β’ NGO hosts event
β’ Free entry for all
β’ Donations support the cause
β’ Transparent tracking via transaction history
RESULT:
βββββββ
β Flexible payment (free or any amount)
β User decides contribution
β Platform fee only on donations > 0
β Limited to 1 per person
β Wallet payment only
β Same escrow/ledger logic if amount > 0
π― Payment Scenario Decision Tree
User Initiates Payment
β
ββββββ΄βββββ
β β
Amount > 0? Amount = 0?
β β
YES [FREE FLOW]
β β’ No escrow
β β’ No wallet
β β’ Complete immediately
β β’ Transaction: NEUTRAL
Payment
Method?
β
ββ WALLET βββββ [PAID FLOW]
β β’ Check balance
β β’ Create escrow
β β’ Ledger entry
β β’ Platform fee (5%)
β β’ Transaction: DEBIT
β
ββ CASH βββββββ [CASH FLOW]
β β’ No wallet
β β’ No escrow
β β’ No ledger
β β’ Complete immediately
β β’ Transaction: NEUTRAL (tracking)
β
ββ EXTERNAL βββ [EXTERNAL FLOW]
β’ (Not yet implemented)
β’ M-Pesa / Cards / etc.
β’ Would follow PAID flow
π° Money Movement Summary by Scenario
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MONEY FLOW BY PAYMENT TYPE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PAID (Wallet):
βββββββββββββββ
Buyer Wallet ββ[amount]βββ Escrow Account
β
βββββββββββββββββ΄ββββββββββββββββ
β β
Seller Wallet Platform Revenue
[95% of amount] [5% of amount]
FREE:
βββββ
(No money movement)
User β Direct Order/Booking
CASH:
βββββ
(Physical money, outside system)
User β [Physical Cash] β Seller/Organizer
System: Records transaction for tracking only
DONATION (Amount > 0):
ββββββββββββββββββββββ
[Same as PAID flow]
Buyer Wallet ββ[user-chosen amount]βββ Escrow Account
β
βββββββββββββββββ΄ββββββββββββββββ
β β
Organizer Wallet Platform Revenue
[95% of donation] [5% of donation]
DONATION (Amount = 0):
ββββββββββββββββββββββ
[Same as FREE flow]
(No money movement)
User β Direct Booking
π Transaction History by Scenario
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TRANSACTION HISTORY ENTRIES BY SCENARIO β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PAID Purchase (Product):
ββββββββββββββββββββββββ
Buyer sees:
β’ Type: PRODUCT_PURCHASE
β’ Direction: DEBIT (-)
β’ Amount: 50,000 TZS
β’ Balance impact: YES
Seller sees (after escrow release):
β’ Type: SALE
β’ Direction: CREDIT (+)
β’ Amount: 47,500 TZS
β’ Balance impact: YES
Platform sees:
β’ Type: PLATFORM_FEE_COLLECTED
β’ Direction: CREDIT (+)
β’ Amount: 2,500 TZS
PAID Ticket (Event):
ββββββββββββββββββββ
Buyer sees:
β’ Type: EVENT_TICKET_PURCHASE
β’ Direction: DEBIT (-)
β’ Amount: 30,000 TZS
β’ Balance impact: YES
Organizer sees (after escrow release):
β’ Type: SALE
β’ Direction: CREDIT (+)
β’ Amount: 28,500 TZS
β’ Balance impact: YES
Platform sees:
β’ Type: PLATFORM_FEE_COLLECTED
β’ Direction: CREDIT (+)
β’ Amount: 1,500 TZS
FREE Product:
βββββββββββββ
Buyer sees:
β’ Type: FREE_PRODUCT
β’ Direction: NEUTRAL (β)
β’ Amount: 0 TZS
β’ Balance impact: NO
FREE Ticket:
ββββββββββββ
Buyer sees:
β’ Type: FREE_EVENT_TICKET
β’ Direction: NEUTRAL (β)
β’ Amount: 0 TZS
β’ Balance impact: NO
CASH Product:
βββββββββββββ
Buyer sees:
β’ Type: CASH_PRODUCT_PAYMENT
β’ Direction: NEUTRAL (β)
β’ Amount: 20,000 TZS
β’ Balance impact: NO
β’ Note: "Physical cash payment"
CASH Ticket:
ββββββββββββ
Buyer sees:
β’ Type: CASH_EVENT_TICKET
β’ Direction: NEUTRAL (β)
β’ Amount: 15,000 TZS
β’ Balance impact: NO
β’ Note: "Cash payment at door"
DONATION (Paid):
ββββββββββββββββ
Buyer sees:
β’ Type: DONATION_EVENT_TICKET
β’ Direction: DEBIT (-)
β’ Amount: 10,000 TZS (user chose)
β’ Balance impact: YES
β’ Metadata: { suggested: 5000, donated: 10000 }
Organizer sees (after escrow release):
β’ Type: SALE
β’ Direction: CREDIT (+)
β’ Amount: 9,500 TZS
β’ Balance impact: YES
DONATION (Free):
ββββββββββββββββ
Buyer sees:
β’ Type: FREE_EVENT_TICKET (or DONATION_EVENT_TICKET)
β’ Direction: NEUTRAL (β)
β’ Amount: 0 TZS
β’ Balance impact: NO
β’ Metadata: { suggested: 5000, donated: 0 }
π Code Implementation Points
PaymentOrchestrator Detection Logic
@Override
@Transactional
public PaymentResponse processPayment(PaymentRequest request)
throws ItemNotFoundException, RandomExceptions, BadRequestException {
PayableCheckoutSession session = checkoutSessionService.findCheckoutSession(
request.getCheckoutSessionId(),
request.getSessionDomain()
);
// ========================================
// SCENARIO DETECTION
// ========================================
// FREE Detection
if (session.getTotalAmount().compareTo(BigDecimal.ZERO) == 0) {
return handleFreeCheckout(session);
}
// Payment Method Detection
PaymentMethod paymentMethod = determinePaymentMethod(session, request);
// CASH Detection
if (paymentMethod == PaymentMethod.CASH) {
return handleCashCheckout(session);
}
// PAID (Wallet/External) Detection
return routeToProcessor(session, paymentMethod);
}
Event Ticket Type Validation
// EventCheckoutValidations.validateTicketTypeAndPrice()
switch (ticketPricingType) {
case FREE -> validateFreeTicket(ticket, request);
case PAID -> validatePaidTicket(ticket, request);
case DONATION -> validateDonationTicket(ticket, request);
}
private void validateDonationTicket(...) {
// Total quantity must be exactly 1
if (totalQuantity > 1) {
throw new BadRequestException(
"Donation tickets are limited to 1 per person");
}
// No other attendees
if (request.getOtherAttendees() != null
&& !request.getOtherAttendees().isEmpty()) {
throw new BadRequestException(
"Donation tickets cannot be purchased for other attendees");
}
// Online only
if (ticket.getSalesChannel() != SalesChannel.ONLINE_ONLY) {
throw new BadRequestException(
"Donation tickets can only be purchased online");
}
}
π Data Consistency
ACID Properties Maintained
Atomicity:
β’ Ledger entries in transactions
β’ Balance updates atomic
β’ No partial money movements
Consistency:
β’ Double-entry rule enforced
β’ Total debits = Total credits
β’ Balance integrity checks
Isolation:
β’ Transaction-level isolation
β’ No concurrent balance corruption
Durability:
β’ All operations persisted
β’ Audit trail maintained
π Scalability Considerations
Async Processing
Payment β Return immediately β Process in background
Benefits:
β’ Fast API response
β’ Better user experience
β’ Handles spike loads
Event-Driven Architecture
Payment success β Publish event β Multiple listeners
Benefits:
β’ Loosely coupled
β’ Easy to add features
β’ Horizontal scaling
Separate Read/Write Models
Write: Ledger entries (normalized)
Read: Transaction history (denormalized)
Benefit:
β’ Fast queries
β’ Optimized for each use case
π― Summary
Key Strengths of the Architecture
-
Universal Payment System
- Handles multiple domains (Products, Events)
- Easy to extend to new domains
-
Financial Integrity
- Double-entry bookkeeping
- Escrow protection
- Audit trail
-
Scalability
- Async processing
- Event-driven
- Loosely coupled
-
Flexibility
- Strategy pattern for domain logic
- Multiple payment methods support
- Multiple checkout types
-
Security
- Transaction-level safety
- Validation at every step
- Balance integrity
π Key Classes Reference
CHECKOUT:
β’ ProductCheckoutSessionEntity
β’ EventCheckoutSessionEntity
β’ PayableCheckoutSession (interface)
PAYMENT:
β’ PaymentOrchestrator
β’ WalletPaymentProcessor
β’ ExternalPaymentProcessor
STRATEGY:
β’ SessionMetadataExtractor
β’ PostPaymentHandler
FINANCIAL:
β’ EscrowService
β’ LedgerService
β’ WalletService
β’ TransactionHistoryService
ENTITIES:
β’ EscrowAccountEntity
β’ LedgerAccountEntity
β’ LedgerEntryEntity
β’ WalletEntity
β’ TransactionHistory
EVENTS:
β’ PaymentCompletedEvent
β’ ProductPaymentCompletedListener
β’ EventPaymentCompletedListener
End of Documentation
This architecture provides a solid foundation for a multi-domain payment system with financial integrity, scalability, and extensibility built in from the ground up.
No comments to display
No comments to display