Data Model: Cards
Status: Draft | Owner: Platform Team | Last Updated: 2026-04-03
Overview
Card payment domain model for Simpaisa's multi-market payment gateway. Supports Visa, Mastercard, and local schemes across PK, BD, NP, and IQ. Handles the full auth→capture→refund lifecycle, 3D Secure authentication, PCI DSS-compliant tokenisation, and dispute management. Extends the canonical Payment entity.
Entity: CardPayment
Extends Payment from the canonical model.
| Field |
Type |
Description |
PCI CDE |
id |
string |
ULID (inherited) |
No |
paymentId |
string |
FK → Payment (canonical) |
No |
maskedPan |
string |
First 6 + last 4 (e.g. 411111****1111) |
No |
cardBrand |
CardBrand |
VISA, MASTERCARD, LOCAL |
No |
cardType |
CardType |
CREDIT, DEBIT, PREPAID |
No |
authCode |
string |
Acquirer authorisation code |
No |
threeDSStatus |
ThreeDSStatus |
3DS authentication result |
No |
threeDSVersion |
string |
3DS protocol version (2.1, 2.2) |
No |
eci |
string |
Electronic Commerce Indicator |
No |
authAmount |
decimal |
Authorised amount |
No |
capturedAmount |
decimal |
Total captured so far |
No |
refundedAmount |
decimal |
Total refunded so far |
No |
currency |
Currency |
ISO 4217 |
No |
status |
CardPaymentStatus |
Lifecycle state |
No |
acquirerId |
string |
Acquirer/processor ID |
No |
acquirerRef |
string |
Acquirer's transaction reference |
No |
merchantRef |
string |
Merchant order reference |
No |
tokenId |
string |
FK → CardToken (nullable) |
No |
captureMode |
CaptureMode |
AUTO, MANUAL |
No |
Entity: CardAuthorisation
| Field |
Type |
Description |
PCI CDE |
id |
string |
ULID |
No |
cardPaymentId |
string |
FK → CardPayment |
No |
amount |
decimal |
Authorised amount |
No |
currency |
Currency |
ISO 4217 |
No |
responseCode |
string |
ISO 8583 response code |
No |
authCode |
string |
Issuer authorisation code |
No |
avsResult |
string |
Address Verification result |
No |
cvvResult |
string |
CVV verification result |
No |
declineReason |
string |
Human-readable decline reason |
No |
authorisedAt |
datetime |
Authorisation timestamp |
No |
expiresAt |
datetime |
Auth hold expiry (typically 7–30 days) |
No |
Entity: CardCapture
| Field |
Type |
Description |
id |
string |
ULID |
cardPaymentId |
string |
FK → CardPayment |
authId |
string |
FK → CardAuthorisation |
amount |
decimal |
Capture amount (≤ auth amount) |
currency |
Currency |
ISO 4217 |
status |
CaptureStatus |
PENDING, COMPLETED, FAILED |
capturedAt |
datetime |
Capture confirmation timestamp |
settlementDate |
date |
Expected settlement date |
Entity: CardRefund
| Field |
Type |
Description |
id |
string |
ULID |
cardPaymentId |
string |
FK → CardPayment |
captureId |
string |
FK → CardCapture |
amount |
decimal |
Refund amount (≤ captured amount) |
currency |
Currency |
ISO 4217 |
reason |
string |
Refund reason |
status |
RefundStatus |
PENDING, COMPLETED, FAILED |
acquirerRef |
string |
Acquirer refund reference |
refundedAt |
datetime |
Refund confirmation timestamp |
Entity: CardVoid
| Field |
Type |
Description |
id |
string |
ULID |
cardPaymentId |
string |
FK → CardPayment |
authId |
string |
FK → CardAuthorisation |
reason |
string |
Void reason |
status |
VoidStatus |
PENDING, COMPLETED, FAILED |
voidedAt |
datetime |
Void confirmation timestamp |
Entity: CardToken
| Field |
Type |
Description |
PCI CDE |
id |
string |
ULID |
No |
merchantId |
string |
FK → Merchant (scoped) |
No |
tokenisedPan |
string |
Opaque token replacing real PAN |
No |
maskedPan |
string |
Display-safe masked PAN |
No |
cardBrand |
CardBrand |
VISA, MASTERCARD, LOCAL |
No |
expiryMonth |
int |
Card expiry month |
Yes |
expiryYear |
int |
Card expiry year |
Yes |
cardholderName |
string |
Name on card |
No |
status |
TokenStatus |
ACTIVE, SUSPENDED, REVOKED, EXPIRED |
No |
createdAt |
datetime |
Tokenisation timestamp |
No |
lastUsedAt |
datetime |
Last transaction timestamp |
No |
PCI DSS Note: The real PAN is stored ONLY in the isolated Card Data Environment (CDE) vault. tokenisedPan is a non-reversible reference. expiryMonth/expiryYear are CDE-adjacent — stored encrypted at rest with separate key management.
Entity: DisputeCase
| Field |
Type |
Description |
id |
string |
ULID |
cardPaymentId |
string |
FK → CardPayment |
merchantId |
string |
FK → Merchant |
disputeType |
DisputeType |
CHARGEBACK, PRE_ARB, ARBITRATION |
reasonCode |
string |
Scheme reason code |
amount |
decimal |
Disputed amount |
currency |
Currency |
ISO 4217 |
status |
DisputeStatus |
OPENED, MERCHANT_RESPONSE, WON, LOST, EXPIRED |
evidence |
[]string |
Evidence document references |
deadline |
date |
Response deadline |
openedAt |
datetime |
Dispute opened timestamp |
resolvedAt |
datetime |
Resolution timestamp (nullable) |
Card Payment State Machine (Auth → Capture → Refund)
┌──────────┐
│ CREATED │
└────┬─────┘
│ 3DS check
▼
┌────────────┐
│3DS_PENDING │
└──┬──────┬──┘
│ │
pass fail
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│AUTHORISED│ │3DS_FAILED │
└────┬─────┘ └──────────────┘
│
├────────────────┐
│ (auto/manual) │ void
▼ ▼
┌──────────┐ ┌────────┐
│ CAPTURED │ │ VOIDED │
└────┬─────┘ └────────┘
│
├─────────────┐
│ partial │ full
▼ ▼
┌────────────────┐ ┌──────────┐
│PARTIAL_REFUNDED│ │ REFUNDED │
└────────────────┘ └──────────┘
Key Rules:
- AUTHORISED → CAPTURED: Auto-capture or manual capture within auth window
- AUTHORISED → VOIDED: Cancel before capture (releases hold)
- CAPTURED → PARTIAL_REFUNDED: Refund < captured amount
- CAPTURED → REFUNDED: Refund = captured amount
- Auth hold expires after 7–30 days (scheme-dependent)
PCI DSS Field Classification
| Field |
CDE Scope |
Storage Location |
Encryption |
| Full PAN |
In-scope |
CDE vault only |
AES-256-GCM |
| CVV/CVC |
Never stored |
— |
— |
| Expiry date |
In-scope |
CDE vault |
AES-256-GCM |
| Cardholder name |
Out-of-scope |
Application DB |
At-rest (TDE) |
| Masked PAN |
Out-of-scope |
Application DB |
At-rest (TDE) |
| Tokenised PAN |
Out-of-scope |
Application DB |
At-rest (TDE) |
| Auth code |
Out-of-scope |
Application DB |
At-rest (TDE) |
| 3DS data |
Out-of-scope |
Application DB |
At-rest (TDE) |
Entity Relationships (ERD)
┌───────────┐1 N┌─────────────┐1 N┌──────────────────┐
│ Merchant │────────│ CardPayment │──────│CardAuthorisation │
└───────────┘ └──────┬──────┘ └──────────────────┘
│1 │1 │1 │1
│ │ │ │
N N│ N│ 0..N
┌──────────┐ ┌─────┴┐ ┌─┴─────┐ ┌───────────┐
│CardToken │ │Capture│ │Refund │ │DisputeCase│
└──────────┘ └──────┘ └───────┘ └───────────┘
│
┌──┴───┐
│ Void │
└──────┘
SurrealDB Table Mapping
DEFINE TABLE card_payment SCHEMAFULL;
DEFINE FIELD paymentId ON card_payment TYPE record<payment>;
DEFINE FIELD maskedPan ON card_payment TYPE string;
DEFINE FIELD cardBrand ON card_payment TYPE string
ASSERT $value IN ['VISA','MASTERCARD','LOCAL'];
DEFINE FIELD authCode ON card_payment TYPE option<string>;
DEFINE FIELD threeDSStatus ON card_payment TYPE string;
DEFINE FIELD authAmount ON card_payment TYPE decimal;
DEFINE FIELD capturedAmount ON card_payment TYPE decimal DEFAULT 0;
DEFINE FIELD refundedAmount ON card_payment TYPE decimal DEFAULT 0;
DEFINE FIELD status ON card_payment TYPE string
ASSERT $value IN ['CREATED','3DS_PENDING','3DS_FAILED','AUTHORISED','CAPTURED','VOIDED','PARTIAL_REFUNDED','REFUNDED'];
DEFINE FIELD tokenId ON card_payment TYPE option<record<card_token>>;
DEFINE FIELD createdAt ON card_payment TYPE datetime DEFAULT time::now();
DEFINE INDEX idx_card_merchant ON card_payment FIELDS merchantId;
DEFINE INDEX idx_card_status ON card_payment FIELDS status;
DEFINE INDEX idx_card_brand ON card_payment FIELDS cardBrand;
DEFINE TABLE card_authorisation SCHEMAFULL;
DEFINE FIELD cardPaymentId ON card_authorisation TYPE record<card_payment>;
DEFINE FIELD amount ON card_authorisation TYPE decimal;
DEFINE FIELD responseCode ON card_authorisation TYPE string;
DEFINE FIELD authCode ON card_authorisation TYPE string;
DEFINE FIELD authorisedAt ON card_authorisation TYPE datetime;
DEFINE FIELD expiresAt ON card_authorisation TYPE datetime;
DEFINE TABLE card_capture SCHEMAFULL;
DEFINE FIELD cardPaymentId ON card_capture TYPE record<card_payment>;
DEFINE FIELD authId ON card_capture TYPE record<card_authorisation>;
DEFINE FIELD amount ON card_capture TYPE decimal;
DEFINE FIELD status ON card_capture TYPE string;
DEFINE FIELD capturedAt ON card_capture TYPE option<datetime>;
DEFINE TABLE card_refund SCHEMAFULL;
DEFINE FIELD cardPaymentId ON card_refund TYPE record<card_payment>;
DEFINE FIELD captureId ON card_refund TYPE record<card_capture>;
DEFINE FIELD amount ON card_refund TYPE decimal;
DEFINE FIELD reason ON card_refund TYPE string;
DEFINE FIELD status ON card_refund TYPE string;
DEFINE FIELD refundedAt ON card_refund TYPE option<datetime>;
DEFINE TABLE card_void SCHEMAFULL;
DEFINE FIELD cardPaymentId ON card_void TYPE record<card_payment>;
DEFINE FIELD authId ON card_void TYPE record<card_authorisation>;
DEFINE FIELD reason ON card_void TYPE string;
DEFINE FIELD status ON card_void TYPE string;
-- CardToken: stored in CDE-segmented SurrealDB instance
DEFINE TABLE card_token SCHEMAFULL;
DEFINE FIELD merchantId ON card_token TYPE record<merchant>;
DEFINE FIELD tokenisedPan ON card_token TYPE string;
DEFINE FIELD maskedPan ON card_token TYPE string;
DEFINE FIELD cardBrand ON card_token TYPE string;
DEFINE FIELD status ON card_token TYPE string
ASSERT $value IN ['ACTIVE','SUSPENDED','REVOKED','EXPIRED'];
DEFINE FIELD createdAt ON card_token TYPE datetime DEFAULT time::now();
-- expiryMonth/expiryYear encrypted at application layer before storage
DEFINE TABLE dispute_case SCHEMAFULL;
DEFINE FIELD cardPaymentId ON dispute_case TYPE record<card_payment>;
DEFINE FIELD merchantId ON dispute_case TYPE record<merchant>;
DEFINE FIELD disputeType ON dispute_case TYPE string;
DEFINE FIELD reasonCode ON dispute_case TYPE string;
DEFINE FIELD amount ON dispute_case TYPE decimal;
DEFINE FIELD status ON dispute_case TYPE string;
DEFINE FIELD deadline ON dispute_case TYPE datetime;
DEFINE FIELD openedAt ON dispute_case TYPE datetime;