v2

Payments Flow Guide

Purpose

This wiki explains the current Payments behavior in Loreax, with flowcharts for the implemented withdrawal path and the planned top-up path. It intentionally distinguishes between what is already live in-app and what is still deferred.

Current Status

  • Withdrawal method creation is implemented
  • Withdrawal request creation is implemented
  • Dual-approval admin mock settlement is implemented
  • Real provider B2C payout execution is deferred
  • Provider contract and C2B DTOs exist
  • End-user top-up HTTP flow is still planned, not implemented

Payment Surface Today

  • POST /api/v1/payments/withdrawal-methods
  • GET /api/v1/payments/withdrawal-methods
  • POST /api/v1/payments/withdrawals
  • GET /api/v1/payments/withdrawals
  • GET /api/v1/payments/withdrawals/{withdrawal}
  • POST /api/v1/admin/payments/withdrawals/{withdrawal}/mock-settlement-requests
  • POST /api/v1/admin/payments/approval-requests/{approvalRequest}/mock-settlement-approvals

Withdrawal Method Registration Flow

flowchart TD
    A["Client POST /api/v1/payments/withdrawal-methods"] --> B["CreateWithdrawalMethodRequest validates payload"]
    B --> C["CreateWithdrawalMethodAction starts DB transaction"]
    C --> D{"Marked as primary?"}
    D -- "Yes" --> E["Clear previous primary methods for user"]
    D -- "No" --> F{"User already has methods?"}
    F -- "No" --> G["Promote first method to primary automatically"]
    F -- "Yes" --> H["Keep existing primary"]
    E --> I["Create withdrawal_methods row"]
    G --> I
    H --> I
    I --> J["Mask phone number for safe display"]
    J --> K["Return 201 with WithdrawalMethodResource"]

Withdrawal Request Flow

This is the main implemented money-out path today.

flowchart TD
    A["Client POST /api/v1/payments/withdrawals"] --> B["RequestWithdrawalRequest validates payload and Idempotency-Key"]
    B --> C["RequestWithdrawalAction checks prior payment_intent by idempotency key"]
    C --> D{"Replay of same request?"}
    D -- "Yes" --> E["Return existing withdrawal"]
    D -- "No" --> F["Start DB transaction"]
    F --> G["Lock wallet row"]
    G --> H["Load owned withdrawal method"]
    H --> I["Check min/max/day limits from platform settings"]
    I --> J["Check available balance minus reserved withdrawals"]
    J --> K{"All checks pass?"}
    K -- "No" --> L["Throw business exception and return 430"]
    K -- "Yes" --> M["Create payment_intents row purpose=withdrawal status=pending"]
    M --> N["Create withdrawals row status=pending_approval"]
    N --> O["Link payment intent to withdrawal"]
    O --> P["Return 201 with withdrawal + payment intent"]

Admin Mock Settlement Flow

Real B2C is still deferred, so withdrawals settle through a controlled admin approval path.

flowchart TD
    A["Admin POST mock-settlement-request endpoint"] --> B["RequestWithdrawalApprovalAction"]
    B --> C{"Withdrawal already terminal?"}
    C -- "Yes" --> D["Return 430"]
    C -- "No" --> E["Open approval_requests row for withdrawal"]
    E --> F["Return 202 with approval request"]
    F --> G["Second admin POST mock-settlement-approval endpoint"]
    G --> H["ApproveWithdrawalMockPayoutAction"]
    H --> I["ApproveRequestAction enforces dual approval rules"]
    I --> J["Lock withdrawal and ensure not terminal"]
    J --> K["LedgerService::post() creates withdrawal ledger transaction"]
    K --> L["payment_intent -> succeeded, provider_method=mock_b2c"]
    L --> M["withdrawal -> succeeded"]
    M --> N["withdrawal method -> verified=true"]
    N --> O["Return settled withdrawal resource"]

Withdrawal Decision Path

flowchart TD
    A["Withdrawal request arrives"] --> B{"Idempotency replay?"}
    B -- "Yes" --> C["Return same stored result"]
    B -- "No" --> D{"Method belongs to user?"}
    D -- "No" --> E["Return 404"]
    D -- "Yes" --> F{"Within min/max/day limits?"}
    F -- "No" --> G["Return 430 limit error"]
    F -- "Yes" --> H{"Spendable balance sufficient?"}
    H -- "No" --> I["Return 430 insufficient funds"]
    H -- "Yes" --> J["Create pending withdrawal"]
    J --> K["Await dual approval"]
    K --> L["Mock settlement posts ledger movement"]

Planned Top-Up Flow

This is the intended C2B flow from the technical design. The provider contract and DTOs exist, but the user-facing top-up endpoints are not implemented yet.

flowchart TD
    A["Client POST /api/v1/payments/top-ups"] --> B["Validate amount, phone number, idempotency key"]
    B --> C["Create payment_intent purpose=top_up status=pending"]
    C --> D["IPaymentProvider::initiateC2bStk()"]
    D --> E["User receives MPESA/Kashier prompt"]
    E --> F{"User approves?"}
    F -- "No" --> G["Provider reports failure or timeout"]
    G --> H["payment_intent -> failed"]
    F -- "Yes" --> I["Provider callback confirms collection"]
    I --> J["LedgerService::post(top_up rule)"]
    J --> K["payment_intent -> succeeded"]
    K --> L["Wallet balance increases"]

What Is Implemented Versus Planned

Implemented now

  • Withdrawal method creation and listing
  • Withdrawal request creation with idempotency and limit checks
  • Spendable-balance reservation behavior
  • Dual-approval mock settlement
  • Ledger posting on successful mock settlement

Planned or deferred

  • Real provider B2C execution
  • Top-up initiation endpoint
  • Top-up callback handler
  • Automated failure reversal path for B2C failures

Implemented Files Behind The Current Flow

Important Notes

  • Idempotency-Key is required for the withdrawal mutation endpoint.
  • The current withdrawal path is intentionally honest about using mock_b2c settlement after dual approval.
  • The top-up flow should be read as design direction plus provider-contract groundwork, not as a finished HTTP surface in this repo today.