Prisma schema blueprint
Status: Implementation reference, not yet a complete executable schema. Model names and constraints must be checked against chosen provider semantics.
Conventions
- PostgreSQL and Prisma migrations; application root without a
srcdirectory where applicable. - UUID primary keys,
createdAt,updatedAt, and explicit actor/audit references. BigIntinteger minor-unit amounts for all financial values; ISO 4217 currency (AUD-only in v1 unless agreed otherwise).Decimalfor pricing rates, never binary floating-point.- All merchant-owned objects include
merchantId; compound uniqueness for external identifiers. - Do not persist raw card PAN, CVV, bank passwords or unencrypted provider secrets.
- Use dedicated append-only ledger tables; prohibit update/delete through application permissions.
Suggested Prisma excerpt
enum BillStatus {
RECEIVED
VALIDATED
SCHEDULED
COLLECTING
COLLECTED
FAILED
CANCELLED
EXCEPTION
}
enum AttemptStatus {
CREATED
SUBMITTED
PENDING
SUCCEEDED
DECLINED
FAILED
UNKNOWN
}
model Merchant {
id String @id @default(uuid()) @db.Uuid
legalName String
abn String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bills Bill[]
}
model Bill {
id String @id @default(uuid()) @db.Uuid
merchantId String @db.Uuid
externalBillId String
amountMinor BigInt
currency String @default("AUD") @db.VarChar(3)
dueAt DateTime
version Int @default(1)
status BillStatus @default(RECEIVED)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
merchant Merchant @relation(fields: [merchantId], references: [id])
attempts PaymentAttempt[]
@@unique([merchantId, externalBillId])
@@index([merchantId, status, dueAt])
}
model PaymentAttempt {
id String @id @default(uuid()) @db.Uuid
billId String @db.Uuid
attemptNumber Int
provider String
providerIdempotencyKey String @unique
providerPaymentId String?
status AttemptStatus @default(CREATED)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bill Bill @relation(fields: [billId], references: [id])
@@unique([billId, attemptNumber])
@@index([provider, providerPaymentId])
}
model LedgerTransaction {
id String @id @default(uuid()) @db.Uuid
eventKey String @unique
currency String @db.VarChar(3)
postedAt DateTime @default(now())
entries LedgerEntry[]
}
model LedgerEntry {
id String @id @default(uuid()) @db.Uuid
transactionId String @db.Uuid
accountId String @db.Uuid
amountMinor BigInt
// Signed amount convention: debit positive, credit negative.
transaction LedgerTransaction @relation(fields: [transactionId], references: [id])
@@index([accountId, transactionId])
}
This excerpt deliberately omits the full LedgerAccount model and relations, consent, customer linking, collection and payout tables. It must not be pasted directly into production without completing those dependencies.
Database-level invariants
- CHECK amount > 0 for bills and payout instructions, except signed journal entries.
- Transactional assertion that journal entry signed amounts sum to zero per transaction and currency; Prisma schema alone cannot enforce this.
- Unique
(provider, providerEventId)for webhook inbox. - Unique
(merchantId, externalBillId); bill version history stored separately. - Unique
(merchantId, operation, idempotencyKey)for request replay. - Foreign keys on payout allocations and collection IDs.
- Reserve allocations atomically using locks or unique allocation constraints.
Migration order
Identity/merchant → consumer and consent → billing → provider inbox/outbox → payment attempts → ledger → settlement/collections → payout allocation → merchant pricing/invoices → refunds/disputes. Run migration verification and rollback rehearsals before live payments.