Data Model: Pay-Outs
Status: Draft | Owner: Platform Team | Last Updated: 2026-04-03
Overview
Pay-Out (disbursement) domain model for Simpaisa's multi-market payment platform. Supports bank transfers, mobile wallet credits, and batch disbursements across PK, BD, NP, and IQ. Extends the canonical Payment entity with beneficiary resolution, balance reservation, and settlement tracking.
Entity: Disbursement
Extends Payment from the canonical model.
| Field |
Type |
Description |
id |
string |
ULID (inherited) |
paymentId |
string |
FK → Payment (canonical) |
merchantId |
string |
FK → Merchant |
beneficiaryId |
string |
FK → BeneficiaryAccount |
bankCode |
string |
SWIFT/BIC or local bank code |
accountNumber |
string |
Destination account (IBAN/MSISDN) |
accountType |
AccountType |
IBAN, MSISDN, WALLET |
batchId |
string |
FK → DisbursementBatch (nullable) |
amount |
decimal |
Disbursement amount (precision 4) |
currency |
Currency |
ISO 4217 |
status |
DisbursementStatus |
6-state lifecycle |
reservationId |
string |
FK → BalanceReservation |
channelId |
string |
FK → Channel |
channelRef |
string |
Provider's transaction reference |
reason |
string |
Disbursement purpose/narration |
failureCode |
string |
Provider error code (nullable) |
failureMessage |
string |
Human-readable failure reason |
scheduledAt |
datetime |
Deferred execution time (nullable) |
executedAt |
datetime |
Actual execution timestamp |
settledAt |
datetime |
Settlement confirmation timestamp |
Entity: DisbursementBatch
| Field |
Type |
Description |
id |
string |
ULID |
merchantId |
string |
FK → Merchant |
name |
string |
Batch label (e.g. "April Salaries") |
totalCount |
int |
Number of disbursements in batch |
totalAmount |
decimal |
Sum of all disbursement amounts |
currency |
Currency |
Batch currency (single currency only) |
status |
BatchStatus |
DRAFT, SUBMITTED, PROCESSING, COMPLETED, PARTIAL, FAILED |
successCount |
int |
Completed disbursements |
failureCount |
int |
Failed disbursements |
submittedAt |
datetime |
Batch submission timestamp |
completedAt |
datetime |
All items resolved timestamp |
createdBy |
string |
User who created the batch |
approvedBy |
string |
User who approved (4-eyes principle) |
Entity: BeneficiaryAccount
| Field |
Type |
Description |
id |
string |
ULID |
merchantId |
string |
FK → Merchant (scoped) |
name |
string |
Beneficiary full name |
accountNumber |
string |
IBAN, MSISDN, or wallet ID |
accountType |
AccountType |
IBAN, MSISDN, WALLET |
bankCode |
string |
SWIFT/BIC or local code |
bankName |
string |
Institution name |
country |
string |
ISO 3166-1 alpha-2 |
currency |
Currency |
Default currency |
status |
BeneficiaryStatus |
ACTIVE, SUSPENDED, BLOCKED |
verifiedAt |
datetime |
Account verification timestamp |
createdAt |
datetime |
Registration timestamp |
Entity: BalanceReservation
| Field |
Type |
Description |
id |
string |
ULID |
merchantId |
string |
FK → Merchant |
amount |
decimal |
Reserved amount |
currency |
Currency |
ISO 4217 |
status |
ReservationStatus |
HELD, CAPTURED, RELEASED, EXPIRED |
disbursementId |
string |
FK → Disbursement |
expiresAt |
datetime |
Auto-release deadline |
createdAt |
datetime |
Reservation timestamp |
releasedAt |
datetime |
Release/capture timestamp |
Entity: SettlementRecord
| Field |
Type |
Description |
id |
string |
ULID |
disbursementId |
string |
FK → Disbursement |
channelId |
string |
FK → Channel |
settlementRef |
string |
Provider settlement reference |
amount |
decimal |
Settled amount |
fee |
decimal |
Channel fee deducted |
netAmount |
decimal |
amount - fee |
settledAt |
datetime |
Settlement timestamp |
reconciled |
bool |
Matched with internal ledger |
reconciledAt |
datetime |
Reconciliation timestamp |
Disbursement 6-State Machine
┌──────────┐
│ CREATED │
└────┬─────┘
│ reserve balance
▼
┌──────────┐
│ RESERVED │
└────┬─────┘
│ submit to channel
▼
┌────────────┐
│ PROCESSING │
└──┬──────┬──┘
│ │
success failure
│ │
▼ ▼
┌──────────┐ ┌────────┐
│COMPLETED │ │ FAILED │
└──────────┘ └───┬────┘
│ balance released
▼
┌──────────┐
│ REVERSED │
└──────────┘
Transition Rules:
- CREATED → RESERVED: Balance check passes, funds held
- RESERVED → PROCESSING: Submitted to channel adapter
- PROCESSING → COMPLETED: Provider confirms credit
- PROCESSING → FAILED: Provider rejects or times out
- FAILED → REVERSED: Balance reservation released back to merchant
- No direct COMPLETED → REVERSED (use separate refund flow)
Channel Adapter Interface
// ChannelAdapter defines the contract for pay-out providers.
type ChannelAdapter interface {
// ValidateAccount checks beneficiary account is reachable.
ValidateAccount(ctx context.Context, req AccountValidationRequest) (*AccountValidationResponse, error)
// Execute initiates the disbursement with the provider.
Execute(ctx context.Context, req DisbursementRequest) (*DisbursementResponse, error)
// QueryStatus polls the provider for transaction status.
QueryStatus(ctx context.Context, channelRef string) (*StatusResponse, error)
// HealthCheck returns the channel's current availability.
HealthCheck(ctx context.Context) (*HealthResponse, error)
}
Entity Relationships (ERD)
┌───────────┐1 N┌──────────────┐N 1┌───────────────────┐
│ Merchant │────────│ Disbursement │────────│ BeneficiaryAccount│
└───────────┘ └──────┬───────┘ └───────────────────┘
│1 1│ │1
│ │ │
N 1│ │1
┌────────────────┐ ┌─────┴──────────┐ ┌────────────────┐
│Disbursement │ │Balance │ │Settlement │
│Batch │ │Reservation │ │Record │
└────────────────┘ └────────────────┘ └────────────────┘
SurrealDB Table Mapping
DEFINE TABLE disbursement SCHEMAFULL;
DEFINE FIELD paymentId ON disbursement TYPE record<payment>;
DEFINE FIELD merchantId ON disbursement TYPE record<merchant>;
DEFINE FIELD beneficiaryId ON disbursement TYPE record<beneficiary_account>;
DEFINE FIELD bankCode ON disbursement TYPE string;
DEFINE FIELD accountNumber ON disbursement TYPE string;
DEFINE FIELD accountType ON disbursement TYPE string
ASSERT $value IN ['IBAN','MSISDN','WALLET'];
DEFINE FIELD batchId ON disbursement TYPE option<record<disbursement_batch>>;
DEFINE FIELD amount ON disbursement TYPE decimal;
DEFINE FIELD currency ON disbursement TYPE string;
DEFINE FIELD status ON disbursement TYPE string
ASSERT $value IN ['CREATED','RESERVED','PROCESSING','COMPLETED','FAILED','REVERSED'];
DEFINE FIELD reservationId ON disbursement TYPE record<balance_reservation>;
DEFINE FIELD channelRef ON disbursement TYPE option<string>;
DEFINE FIELD executedAt ON disbursement TYPE option<datetime>;
DEFINE FIELD settledAt ON disbursement TYPE option<datetime>;
DEFINE FIELD createdAt ON disbursement TYPE datetime DEFAULT time::now();
DEFINE INDEX idx_disb_merchant ON disbursement FIELDS merchantId;
DEFINE INDEX idx_disb_batch ON disbursement FIELDS batchId;
DEFINE INDEX idx_disb_status ON disbursement FIELDS status;
DEFINE INDEX idx_disb_created ON disbursement FIELDS createdAt;
DEFINE TABLE disbursement_batch SCHEMAFULL;
DEFINE FIELD merchantId ON disbursement_batch TYPE record<merchant>;
DEFINE FIELD name ON disbursement_batch TYPE string;
DEFINE FIELD totalCount ON disbursement_batch TYPE int;
DEFINE FIELD totalAmount ON disbursement_batch TYPE decimal;
DEFINE FIELD currency ON disbursement_batch TYPE string;
DEFINE FIELD status ON disbursement_batch TYPE string;
DEFINE FIELD createdBy ON disbursement_batch TYPE string;
DEFINE FIELD approvedBy ON disbursement_batch TYPE option<string>;
DEFINE TABLE beneficiary_account SCHEMAFULL;
DEFINE FIELD merchantId ON beneficiary_account TYPE record<merchant>;
DEFINE FIELD name ON beneficiary_account TYPE string;
DEFINE FIELD accountNumber ON beneficiary_account TYPE string;
DEFINE FIELD accountType ON beneficiary_account TYPE string;
DEFINE FIELD bankCode ON beneficiary_account TYPE string;
DEFINE FIELD country ON beneficiary_account TYPE string;
DEFINE FIELD status ON beneficiary_account TYPE string;
DEFINE TABLE balance_reservation SCHEMAFULL;
DEFINE FIELD merchantId ON balance_reservation TYPE record<merchant>;
DEFINE FIELD amount ON balance_reservation TYPE decimal;
DEFINE FIELD currency ON balance_reservation TYPE string;
DEFINE FIELD status ON balance_reservation TYPE string
ASSERT $value IN ['HELD','CAPTURED','RELEASED','EXPIRED'];
DEFINE FIELD disbursementId ON balance_reservation TYPE record<disbursement>;
DEFINE FIELD expiresAt ON balance_reservation TYPE datetime;
DEFINE TABLE settlement_record SCHEMAFULL;
DEFINE FIELD disbursementId ON settlement_record TYPE record<disbursement>;
DEFINE FIELD channelId ON settlement_record TYPE record<channel>;
DEFINE FIELD amount ON settlement_record TYPE decimal;
DEFINE FIELD fee ON settlement_record TYPE decimal;
DEFINE FIELD netAmount ON settlement_record TYPE decimal;
DEFINE FIELD settledAt ON settlement_record TYPE datetime;
DEFINE FIELD reconciled ON settlement_record TYPE bool DEFAULT false;