Skip to content

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;