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-methodsGET /api/v1/payments/withdrawal-methodsPOST /api/v1/payments/withdrawalsGET /api/v1/payments/withdrawalsGET /api/v1/payments/withdrawals/{withdrawal}POST /api/v1/admin/payments/withdrawals/{withdrawal}/mock-settlement-requestsPOST /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
- CreateWithdrawalMethodAction.php
- RequestWithdrawalAction.php
- RequestWithdrawalApprovalAction.php
- ApproveWithdrawalMockPayoutAction.php
- WithdrawalMethodController.php
- WithdrawalController.php
- AdminWithdrawalController.php
- WithdrawalControllerTest.php
- WithdrawalMethodControllerTest.php
Important Notes
Idempotency-Keyis required for the withdrawal mutation endpoint.- The current withdrawal path is intentionally honest about using
mock_b2csettlement 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.