Data Model: Pay-Ins
Status: Draft | Owner: Platform Team | Last Updated: 2026-04-03
Overview
Pay-In domain model for Simpaisa's mobile wallet and bank collection products across PK, BD, NP, and IQ. Handles 270M+ transactions annually via operator-specific OTP and redirect flows. Extends the canonical Payment entity with operator-specific fields.
Entity: PayInTransaction
Extends Payment from the canonical model.
| Field |
Type |
Description |
id |
string |
ULID (inherited) |
paymentId |
string |
FK → Payment (canonical) |
operatorId |
string |
Mobile operator / wallet provider ID |
transactionType |
PayInTxType |
Named enum (see below) |
msisdn |
string |
Subscriber mobile number (E.164) |
otpStatus |
OTPStatus |
OTP verification state |
simpaisaToken |
string |
Simpaisa-issued payment token |
operatorToken |
string |
Token returned by operator |
redirectUrl |
string |
Merchant return URL after auth |
callbackPayload |
bytes |
Raw operator callback (encrypted) |
channelConfig |
string |
FK → ChannelConfig |
amount |
decimal |
Inherited, precision 4 |
currency |
Currency |
Inherited |
status |
PayInStatus |
Product-specific lifecycle state |
PayInTxType Enum
| Code |
Name |
Description |
0 |
PAYMENT |
Standard one-time payment |
1 |
AUTHORISATION |
Pre-authorisation hold |
8 |
SUBSCRIPTION |
Recurring subscription charge |
9 |
REFUND |
Full or partial refund |
Entity: OTPRequest
| Field |
Type |
Description |
id |
string |
ULID |
payInId |
string |
FK → PayInTransaction |
msisdn |
string |
Target mobile number |
operatorId |
string |
Operator that sends the OTP |
status |
OTPStatus |
PENDING, SENT, VERIFIED, EXPIRED, FAILED |
attempts |
int |
Verification attempts (max 3) |
requestedAt |
datetime |
OTP request timestamp |
verifiedAt |
datetime |
Successful verification timestamp |
expiresAt |
datetime |
OTP validity window end |
Entity: PaymentToken
| Field |
Type |
Description |
id |
string |
ULID |
payInId |
string |
FK → PayInTransaction |
simpaisaToken |
string |
Simpaisa-generated token (opaque) |
operatorToken |
string |
Operator's reference token |
status |
TokenStatus |
ACTIVE, USED, EXPIRED, REVOKED |
expiryDate |
datetime |
Token validity end |
createdAt |
datetime |
Issuance timestamp |
usedAt |
datetime |
Redemption timestamp (nullable) |
Entity: RecurringSubscription
| Field |
Type |
Description |
id |
string |
ULID |
merchantId |
string |
FK → Merchant |
msisdn |
string |
Subscriber number |
operatorId |
string |
Wallet provider |
amount |
decimal |
Recurring charge amount |
currency |
Currency |
Charge currency |
frequency |
Frequency |
DAILY, WEEKLY, MONTHLY, QUARTERLY |
status |
SubStatus |
ACTIVE, PAUSED, CANCELLED, EXPIRED |
nextChargeAt |
datetime |
Next scheduled execution |
startDate |
date |
Subscription commencement |
endDate |
date |
Subscription termination (nullable) |
retryPolicy |
string |
FK → retry config |
Entity: ChannelConfig
| Field |
Type |
Description |
id |
string |
ULID |
channelId |
string |
FK → Channel (canonical) |
operatorId |
string |
Operator identifier |
country |
string |
ISO 3166-1 alpha-2 |
apiBaseUrl |
string |
Operator API endpoint |
authMethod |
AuthMethod |
HMAC, OAUTH2, BASIC, CERTIFICATE |
timeout |
duration |
Request timeout |
retryMax |
int |
Maximum retry attempts |
rateLimitRps |
int |
Requests per second cap |
otpRequired |
bool |
Whether OTP flow is mandatory |
status |
ChannelStatus |
ACTIVE, DEGRADED, MAINTENANCE, DOWN |
Pay-In State Machine
┌──────────┐
│ CREATED │
└────┬─────┘
│ initiate
▼
┌──────────┐
┌─────│ PENDING │─────┐
│ └────┬─────┘ │
│ │ │ timeout
│ │ OTP │
│ ▼ ▼
│ ┌──────────┐ ┌─────────┐
│ │OTP_SENT │ │ EXPIRED │
│ └────┬─────┘ └─────────┘
│ │ verify
│ ▼
│ ┌───────────┐
│ │AUTHORISED │
│ └─────┬─────┘
│ │ execute
│ ▼
│ ┌────────────┐
│ │ PROCESSING │
│ └──┬─────┬───┘
│ │ │
│ success failure
│ │ │
│ ▼ ▼
│ ┌──────────┐ ┌────────┐
│ │COMPLETED │ │ FAILED │
│ └────┬─────┘ └────────┘
│ │ refund
│ ▼
│ ┌──────────┐
└─▶│ REVERSED │
└──────────┘
Entity Relationships (ERD)
┌───────────┐1 N┌──────────────────┐
│ Merchant │────────│ PayInTransaction │
└───────────┘ └───────┬──────────┘
1│ │1 │1
│ │ │
N│ N│ 0..1
┌─────┴┐ ┌─┴──────┴──────┐
│OTPReq│ │ PaymentToken │
└──────┘ └───────────────-┘
│
┌───────────────────┐ 0..1 │
│RecurringSubscript.│◄──────────────┘
└───────────────────┘
│N
│
1
┌───────────────┐
│ ChannelConfig │
└───────────────┘
SurrealDB Table Mapping
DEFINE TABLE pay_in_transaction SCHEMAFULL;
DEFINE FIELD paymentId ON pay_in_transaction TYPE record<payment>;
DEFINE FIELD operatorId ON pay_in_transaction TYPE string;
DEFINE FIELD txType ON pay_in_transaction TYPE string
ASSERT $value IN ['PAYMENT','AUTHORISATION','SUBSCRIPTION','REFUND'];
DEFINE FIELD msisdn ON pay_in_transaction TYPE string;
DEFINE FIELD otpStatus ON pay_in_transaction TYPE string;
DEFINE FIELD status ON pay_in_transaction TYPE string;
DEFINE FIELD amount ON pay_in_transaction TYPE decimal;
DEFINE FIELD currency ON pay_in_transaction TYPE string;
DEFINE FIELD createdAt ON pay_in_transaction TYPE datetime DEFAULT time::now();
DEFINE FIELD updatedAt ON pay_in_transaction TYPE datetime DEFAULT time::now();
DEFINE INDEX idx_payin_merchant ON pay_in_transaction FIELDS merchantId;
DEFINE INDEX idx_payin_msisdn ON pay_in_transaction FIELDS msisdn;
DEFINE INDEX idx_payin_status ON pay_in_transaction FIELDS status;
DEFINE INDEX idx_payin_created ON pay_in_transaction FIELDS createdAt;
DEFINE TABLE otp_request SCHEMAFULL;
DEFINE FIELD payInId ON otp_request TYPE record<pay_in_transaction>;
DEFINE FIELD msisdn ON otp_request TYPE string;
DEFINE FIELD status ON otp_request TYPE string;
DEFINE FIELD attempts ON otp_request TYPE int DEFAULT 0;
DEFINE FIELD expiresAt ON otp_request TYPE datetime;
DEFINE TABLE payment_token SCHEMAFULL;
DEFINE FIELD payInId ON payment_token TYPE record<pay_in_transaction>;
DEFINE FIELD simpaisaToken ON payment_token TYPE string;
DEFINE FIELD operatorToken ON payment_token TYPE string;
DEFINE FIELD status ON payment_token TYPE string;
DEFINE FIELD expiryDate ON payment_token TYPE datetime;
DEFINE TABLE recurring_subscription SCHEMAFULL;
DEFINE FIELD merchantId ON recurring_subscription TYPE record<merchant>;
DEFINE FIELD msisdn ON recurring_subscription TYPE string;
DEFINE FIELD amount ON recurring_subscription TYPE decimal;
DEFINE FIELD frequency ON recurring_subscription TYPE string;
DEFINE FIELD status ON recurring_subscription TYPE string;
DEFINE FIELD nextChargeAt ON recurring_subscription TYPE datetime;
DEFINE TABLE channel_config SCHEMAFULL;
DEFINE FIELD channelId ON channel_config TYPE record<channel>;
DEFINE FIELD operatorId ON channel_config TYPE string;
DEFINE FIELD country ON channel_config TYPE string;
DEFINE FIELD apiBaseUrl ON channel_config TYPE string;
DEFINE FIELD authMethod ON channel_config TYPE string;
DEFINE FIELD otpRequired ON channel_config TYPE bool DEFAULT true;
DEFINE FIELD status ON channel_config TYPE string;