Finance
Invoices with their legal snapshot, payments and declarations, allocations, credit accounts and their history, credit notes; V2 payment reminders.
Invoices, payments, credit and credit notes (cahier §4.7 — FIN). Prisma file: finance.prisma. Financial data is restricted (ACL-04): resellers read only their own rows, delegates none of these tables.
- Invoices are frozen documents. Issued at the order milestone of
billing.invoiceOn(defaultSHIPPED), numbered without gaps (FAC-YYYY-NNNNNN), with the seller and buyer legal identifiers (NIF, NIS, RC, AI) copied at issue. They are never edited: a correction is a cancellation (nothing paid yet) or a CreditNote (V1). - Settlement goes through PaymentAllocation: a confirmed payment or a credit note is allocated to one or more invoices; the invoice caches
amountPaidandamountCreditedand its status follows the balance. - Credit (FIN-05): CreditAccount holds the limit, terms and blocking; exposure and available credit are computed at checkout and confirmation under a row lock.
Balance of an invoice = totalTtc + stampDuty − amountPaid − amountCredited, computed, never stored.
Invoices
Invoice
A sales invoice generated from an order, with the legal mentions frozen at issue (FIN-01). Gapless numbering per year. Table invoices · written by billing (issued on the order milestone of setting billing.invoiceOn, default SHIPPED; status job for OVERDUE).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
number | String @db.VarChar(20) | — | — | Unique. FAC-YYYY-NNNNNN from NumberSequence INVOICE, allocated in the issuing transaction so the sequence has no gaps. |
orderId | String @db.Uuid | — | — | Unique. FK → Order · Restrict. One invoice per order in V1. |
resellerId | String @db.Uuid | — | — | FK → Reseller · Restrict. |
status | InvoiceStatus | — | UNPAID | — |
issuedAt | DateTime | — | — | — |
dueDate | DateTime @db.Date | — | — | Issue date + CreditAccount.paymentTermsDays for CREDIT; issue date otherwise. |
currency | String @db.Char(3) | — | "DZD" | — |
paymentMethod | PaymentMethod | — | — | Snapshot from the order. |
subtotalHt | Int | — | — | Copied from the order at issue. |
discountTotal | Int | — | — | HT. |
deliveryFee | Int | — | — | HT. |
vatTotal | Int | — | — | — |
totalTtc | Int | — | — | subtotalHt − discountTotal + deliveryFee + vatTotal. |
stampDuty | Int | — | 0 | Assumption — Algerian stamp duty (droit de timbre) on cash-settled invoices, computed at issue from setting billing.stampDuty; rates to confirm with HOCO's accountant. |
amountPaid | Int | — | 0 | Cache: Σ PaymentAllocation of CONFIRMED payments, maintained in the allocating transaction. |
amountCredited | Int | — | 0 | Cache: Σ PaymentAllocation of credit notes applied to this invoice. |
paidAt | DateTime | yes | — | When amountPaid reached the amount due. |
cancelledAt | DateTime | yes | — | — |
cancelledById | String @db.Uuid | yes | — | FK → User · Restrict. |
cancelReason | String | yes | — | — |
pdfFileId | String @db.Uuid | yes | — | FK → File · Restrict. Generated PDF (GENERATED_DOCUMENT); regenerated on demand, the data above is the source of truth. |
sellerLegalName | String | — | — | Seller snapshot from setting billing.sellerIdentity. |
sellerAddress | String | — | — | — |
sellerNif | String @db.VarChar(20) | — | — | — |
sellerNis | String @db.VarChar(20) | — | — | — |
sellerRc | String @db.VarChar(30) | — | — | — |
sellerAi | String @db.VarChar(20) | — | — | — |
sellerRib | String @db.VarChar(30) | yes | — | Bank account printed for transfers. |
buyerName | String | — | — | Buyer snapshot: Reseller.legalName or storeName. |
buyerAddress | String | — | — | — |
buyerNif | String @db.VarChar(20) | yes | — | — |
buyerNis | String @db.VarChar(20) | yes | — | — |
buyerRc | String @db.VarChar(30) | yes | — | — |
buyerAi | String @db.VarChar(20) | yes | — | — |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
order→ Order (orderId,onDelete: Restrict) ·reseller→ Reseller (resellerId,onDelete: Restrict) ·cancelledBy→ User (cancelledById,onDelete: Restrict) ·pdfFile→ File (pdfFileId,onDelete: Restrict). - Referenced by: InvoiceLine.
invoiceId, PaymentAllocation.invoiceId, CreditNote.invoiceId, PaymentReminder.invoiceId. - Keys & indexes:
numberunique ·orderIdunique ·@@index([resellerId, status, dueDate])·@@index([status, dueDate])·@@index([issuedAt]). - Enums: InvoiceStatus, PaymentMethod.
- Lifecycle:
UNPAID → PARTIALLY_PAID → PAID; the daily job setsOVERDUEwhendueDatehas passed with a balance (back toPARTIALLY_PAID/PAIDas payments arrive);CANCELLEDbyadmin.invoices.cancelwhile nothing is allocated; once payments exist the correction is a CreditNote (V1). Issued invoices are never edited. - Derived, not stored: balance due =
totalTtc + stampDuty − amountPaid − amountCredited— computed in SQL and in the API, not stored (two inputs, one writer: a third column could only drift); per-rate VAT summary and amount in words on the PDF — from the lines at render time. - Why
amountPaidis stored although it is a sum: invoice lists sort and filter on balance and status for every reseller; it is written only by the allocation transaction, which also recomputesstatus.
Constraints and indexes Prisma cannot express (raw SQL in the migration):
ALTER TABLE invoices ADD CONSTRAINT invoices_totals CHECK (
total_ttc = subtotal_ht - discount_total + delivery_fee + vat_total
AND stamp_duty >= 0 AND amount_paid >= 0 AND amount_credited >= 0
AND amount_paid + amount_credited <= total_ttc + stamp_duty);Example row:
{
"id": "01a1b128-bf00-73a8-9f2f-e1208bdc0d4a",
"number": "FAC-2026-000123",
"orderId": "01a1ac4b-a100-74df-8237-614783cfeacd",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"status": "PARTIALLY_PAID",
"issuedAt": "2026-11-06T14:00:00.000Z",
"dueDate": "2026-12-06",
"currency": "DZD",
"paymentMethod": "CREDIT",
"subtotalHt": 700000,
"discountTotal": 70000,
"deliveryFee": 0,
"vatTotal": 119700,
"totalTtc": 749700,
"stampDuty": 0,
"amountPaid": 400000,
"amountCredited": 74970,
"paidAt": null,
"cancelledAt": null,
"cancelledById": null,
"cancelReason": null,
"pdfFileId": "01a19ae5-4869-7249-81a2-ab72b47b57e9",
"sellerLegalName": "HOCO DISTRIBUTION ALGÉRIE SARL",
"sellerAddress": "Zone d'activité, Bab Ezzouar, Alger",
"sellerNif": "000216012345678",
"sellerNis": "000216019876543",
"sellerRc": "16/00-0987654B21",
"sellerAi": "16098765432",
"sellerRib": "00799999000123456789",
"buyerName": "BENALI AMINE",
"buyerAddress": "12 rue Didouche Mourad, Alger Centre, Alger",
"buyerNif": "198716010123456",
"buyerNis": "198716010654321",
"buyerRc": "16/00-1234567A19",
"buyerAi": "16012345678",
"createdAt": "2026-11-06T14:00:00.000Z",
"updatedAt": "2026-11-21T09:30:00.000Z"
}InvoiceLine
A product line of an invoice, copied from the order line at issue. Table invoice_lines · written by billing (written once at issue).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
invoiceId | String @db.Uuid | — | — | FK → Invoice · Cascade. |
orderItemId | String @db.Uuid | yes | — | FK → OrderItem · Restrict. |
sku | String @db.VarChar(40) | — | — | — |
description | String | — | — | Brand, product and variant as printed. |
quantity | Int | — | — | — |
unitPriceHt | Int | — | — | — |
discountHt | Int | — | 0 | — |
vatRate | Int | — | — | Basis points. |
lineTotalHt | Int | — | — | — |
vatAmount | Int | — | — | — |
lineTotalTtc | Int | — | — | — |
sortOrder | Int | — | 0 | — |
createdAt | DateTime | — | now() | — |
- Relations:
invoice→ Invoice (invoiceId,onDelete: Cascade) ·orderItem→ OrderItem (orderItemId,onDelete: Restrict). - Keys & indexes:
@@index([invoiceId, sortOrder]). - Immutable (no
updatedAt). The delivery fee is a header amount, not a line.
Example row:
{
"id": "01a1b128-bf00-793a-a51e-2953fc0fbe09",
"invoiceId": "01a1b128-bf00-73a8-9f2f-e1208bdc0d4a",
"orderItemId": "01a1ac4b-a100-7f7e-bf8e-c3ea35df01d8",
"sku": "HOCO-X88-C-BK-1M",
"description": "HOCO X88 Câble USB-A vers USB-C 3 A — Noir · 1 m",
"quantity": 20,
"unitPriceHt": 35000,
"discountHt": 70000,
"vatRate": 1900,
"lineTotalHt": 630000,
"vatAmount": 119700,
"lineTotalTtc": 749700,
"sortOrder": 0,
"createdAt": "2026-11-06T14:00:00.000Z"
}Payments
Payment
Money received or declared: cash on delivery, bank transfer, CCP, BaridiMob (FIN-02, FIN-03). Table payments · written by billing (payments.declare by resellers; admin.payments.* by finance).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
number | String @db.VarChar(20) | — | — | Unique. PAY-YYYY-NNNNNN from NumberSequence PAYMENT. |
resellerId | String @db.Uuid | — | — | FK → Reseller · Restrict. |
method | PaymentMethod | — | — | Never CREDIT (credit is deferral, not a payment) — CHECK. |
status | PaymentStatus | — | — | Declarations start PENDING_VERIFICATION; payments recorded by finance start CONFIRMED. |
amount | Int | — | — | — |
currency | String @db.Char(3) | — | "DZD" | — |
orderId | String @db.Uuid | yes | — | FK → Order · Restrict. Set for cash collected on delivery (admin.deliveries.proof) and for declarations made from an order. |
paidAt | DateTime | — | — | When the money was paid, as declared or recorded. |
reference | String @db.VarChar(60) | yes | — | Transfer/CCP/BaridiMob reference; indexed to flag duplicate declarations. |
proofFileId | String @db.Uuid | yes | — | FK → File · Restrict. Purpose PAYMENT_PROOF; required for declarations. |
isDeclaration | Boolean | — | false | true when declared by the reseller (RA-31). |
requestedInvoiceIds | String[] @db.Uuid | — | [] | The invoices the reseller says a declaration pays (payments.declare → invoiceIds); finance allocates them first at verification. |
recordedById | String @db.Uuid | — | — | FK → User · Restrict. Reseller user (declaration) or finance user. |
verifiedById | String @db.Uuid | yes | — | FK → User · Restrict. |
verifiedAt | DateTime | yes | — | — |
rejectReasonCode | String @db.VarChar(40) | yes | — | ReferenceItem PAYMENT_REJECT_REASON; required for REJECTED. |
rejectComment | String | yes | — | — |
note | String | yes | — | — |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
reseller→ Reseller (resellerId,onDelete: Restrict) ·order→ Order (orderId,onDelete: Restrict) ·proofFile→ File (proofFileId,onDelete: Restrict) ·recordedBy→ User (recordedById,onDelete: Restrict) ·verifiedBy→ User (verifiedById,onDelete: Restrict). - Referenced by: PaymentAllocation.
paymentId. - Keys & indexes:
numberunique ·@@index([resellerId, createdAt(sort: Desc)])·@@index([status, createdAt])·@@index([reference])·@@index([orderId]). - Enums: PaymentMethod, PaymentStatus.
- Derived, not stored: unallocated amount =
amount − Σ PaymentAllocation.amount— an advance that lowers credit exposure until allocated; declaredAt of the lifecycles page =createdAtof a declaration.
Constraints and indexes Prisma cannot express (raw SQL in the migration):
ALTER TABLE payments ADD CONSTRAINT payments_values CHECK (amount > 0 AND method <> 'CREDIT');
ALTER TABLE payments ADD CONSTRAINT payments_declaration_proof CHECK (NOT is_declaration OR proof_file_id IS NOT NULL);Example row:
{
"id": "01a1f86a-a0e0-75da-be88-ad7ef245fbe7",
"number": "PAY-2026-000077",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"method": "BANK_TRANSFER",
"status": "CONFIRMED",
"amount": 400000,
"currency": "DZD",
"orderId": null,
"paidAt": "2026-11-20T09:30:00.000Z",
"reference": "VIR-20261120-88412",
"proofFileId": "01a19ab0-cfda-7b1b-a2d2-3db6bcfe3627",
"isDeclaration": true,
"recordedById": "01a1a077-1080-7aed-87e0-11ae957ae11e",
"verifiedById": "01a19fd5-411e-7a82-8647-950d7b692ffe",
"verifiedAt": "2026-11-21T09:30:00.000Z",
"rejectReasonCode": null,
"rejectComment": null,
"note": null,
"createdAt": "2026-11-20T10:05:00.000Z",
"updatedAt": "2026-11-21T09:30:00.000Z"
}PaymentAllocation
The part of a payment — or of a credit note — applied to one invoice (FIN-04 partial payments and reconciliation, FIN-08). Table payment_allocations · written by billing (admin.payments.allocate, admin.payments.record).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
paymentId | String @db.Uuid | yes | — | FK → Payment · Cascade. Set for a payment allocation. |
creditNoteId | String @db.Uuid | yes | — | FK → CreditNote · Cascade. Set instead of paymentId when a credit note settles part of an invoice. |
invoiceId | String @db.Uuid | — | — | FK → Invoice · Restrict. |
amount | Int | — | — | — |
allocatedById | String @db.Uuid | yes | — | FK → User · Restrict. null when proposed by the reseller in a declaration. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
payment→ Payment (paymentId,onDelete: Cascade) ·creditNote→ CreditNote (creditNoteId,onDelete: Cascade) ·invoice→ Invoice (invoiceId,onDelete: Restrict) ·allocatedBy→ User (allocatedById,onDelete: Restrict). - Keys & indexes:
@@unique([paymentId, invoiceId])·@@unique([creditNoteId, invoiceId])·@@index([invoiceId]). - Rules enforced in the allocating transaction (payment/credit note and invoices locked): Σ allocations of a payment ≤
Payment.amount; Σ allocations of a credit note ≤CreditNote.totalTtc;amountPaid + amountCreditedof an invoice ≤ its amount due. OnlyCONFIRMEDpayments count inInvoice.amountPaid. PUT …/allocationsreplaces the set of a payment (link rows; AuditLog keeps the diff). A credit note issued for an invoice is allocated to it automatically up to its balance; the rest stays available for later invoices.
Constraints and indexes Prisma cannot express (raw SQL in the migration):
ALTER TABLE payment_allocations ADD CONSTRAINT payment_allocations_amount CHECK (amount > 0);
ALTER TABLE payment_allocations ADD CONSTRAINT payment_allocations_one_source
CHECK (num_nonnulls(payment_id, credit_note_id) = 1);Example row:
{
"id": "01a1fd70-f1c0-798d-879f-354405ac8e83",
"paymentId": "01a1f86a-a0e0-75da-be88-ad7ef245fbe7",
"creditNoteId": null,
"invoiceId": "01a1b128-bf00-73a8-9f2f-e1208bdc0d4a",
"amount": 400000,
"allocatedById": "01a19fd5-411e-7a82-8647-950d7b692ffe",
"createdAt": "2026-11-21T09:30:00.000Z",
"updatedAt": "2026-11-21T09:30:00.000Z"
}Credit
CreditAccount
Credit terms of a reseller (FIN-05; detailed terms FIN-06 in V2). Table credit_accounts · written by billing (created when the reseller is validated, with the setting defaults; admin.credit.update).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
resellerId | String @db.Uuid | — | — | Unique. FK → Reseller · Restrict. |
creditLimit | Int | — | 0 | Ceiling in centimes; 0 = no credit (method CREDIT hidden at checkout). |
paymentTermsDays | Int | — | 30 | Days from invoice issue to due date. |
overLimitPolicy | CreditOverLimitPolicy | yes | — | null = INHERIT: the setting credit.overLimitPolicy applies (API policy). |
isBlocked | Boolean | — | false | Blocks new CREDIT orders (manual, or automatic on overdue in V2). |
blockedReason | String | yes | — | — |
blockedAt | DateTime | yes | — | — |
blockedById | String @db.Uuid | yes | — | FK → User · Restrict. |
version | Int | — | 1 | Optimistic lock: every write does version = version + 1 WHERE version = :expected; 0 rows → 409 VERSION_CONFLICT. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
reseller→ Reseller (resellerId,onDelete: Restrict) ·blockedBy→ User (blockedById,onDelete: Restrict). - Referenced by: CreditLimitChange.
creditAccountId. - Keys & indexes:
resellerIdunique. - Enums: CreditOverLimitPolicy (proposed).
- History: Every change of limit, terms, policy or blocking → CreditLimitChange.
- Derived, not stored: credit used (exposure) = Σ balance due of the reseller's open invoices (
UNPAID,PARTIALLY_PAID,OVERDUE) withpaymentMethod = CREDIT+ ΣtotalTtcof itsCREDITorders fromVALIDATEDtoDELIVEREDthat have no invoice yet. available =creditLimit − exposure. Computed by one indexed query at checkout and validation; storing it would need updates from three modules. - Concurrency: checkout and validation read the exposure after
SELECT … FOR UPDATEon this row, so two simultaneousCREDITcheckouts cannot both consume the last available credit;versionprotects back-office edits.
Example row:
{
"id": "01a1a0fe-bee0-7107-8238-c961e9557022",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"creditLimit": 2000000,
"paymentTermsDays": 30,
"overLimitPolicy": null,
"isBlocked": false,
"blockedReason": null,
"blockedAt": null,
"blockedById": null,
"version": 2,
"createdAt": "2026-11-03T10:40:12.000Z",
"updatedAt": "2026-11-03T10:40:12.000Z"
}CreditLimitChange
Append-only history of credit terms (cahier §4.7, historique conservé). Table credit_limit_changes · written by billing (only writer).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
creditAccountId | String @db.Uuid | — | — | FK → CreditAccount · Restrict. |
previousLimit | Int | yes | — | — |
newLimit | Int | yes | — | — |
previousTermsDays | Int | yes | — | — |
newTermsDays | Int | yes | — | — |
previousPolicy | CreditOverLimitPolicy | yes | — | — |
newPolicy | CreditOverLimitPolicy | yes | — | — |
previousBlocked | Boolean | yes | — | — |
newBlocked | Boolean | yes | — | — |
reason | String | yes | — | — |
changedById | String @db.Uuid | yes | — | FK → User · Restrict. |
createdAt | DateTime | — | now() | — |
- Relations:
creditAccount→ CreditAccount (creditAccountId,onDelete: Restrict) ·changedBy→ User (changedById,onDelete: Restrict). - Keys & indexes:
@@index([creditAccountId, createdAt]). - Enums: CreditOverLimitPolicy (proposed).
- Only the attributes that changed are filled; unchanged pairs stay
null.
Example row:
{
"id": "01a1a0ff-7a60-7453-9077-bbf3adcf8039",
"creditAccountId": "01a1a0fe-bee0-7107-8238-c961e9557022",
"previousLimit": 0,
"newLimit": 2000000,
"previousTermsDays": null,
"newTermsDays": null,
"previousPolicy": null,
"newPolicy": null,
"previousBlocked": null,
"newBlocked": null,
"reason": "Plafond accordé après première commande",
"changedById": "01a19fd5-411e-7a82-8647-950d7b692ffe",
"createdAt": "2026-11-03T10:41:00.000Z"
}Credit notes
CreditNote
A credit note (avoir) reducing what a reseller owes — after a return, an after-sales claim, or an invoice correction (FIN-08, V1). Table credit_notes · written by billing (issued by return reception, after-sales resolution CREDIT_NOTE, or by finance).
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
number | String @db.VarChar(20) | — | — | Unique. Proposed format AVO-YYYY-NNNNNN (NumberSequence CREDIT_NOTE, gapless like invoices) — not yet in AUTHORING's list of business numbers. |
invoiceId | String @db.Uuid | — | — | FK → Invoice · Restrict. |
resellerId | String @db.Uuid | — | — | FK → Reseller · Restrict. |
returnRequestId | String @db.Uuid | yes | — | Unique. FK → ReturnRequest · Restrict. Set when issued by a return reception; an after-sales CREDIT_NOTE resolution links from Ticket.creditNoteId instead. |
reason | String | — | — | — |
subtotalHt | Int | — | — | — |
vatTotal | Int | — | — | — |
totalTtc | Int | — | — | — |
amountApplied | Int | — | 0 | Cache: Σ PaymentAllocation of the credit note. |
issuedAt | DateTime | — | — | — |
cancelledAt | DateTime | yes | — | — |
pdfFileId | String @db.Uuid | yes | — | FK → File · Restrict. |
issuedById | String @db.Uuid | — | — | FK → User · Restrict. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
invoice→ Invoice (invoiceId,onDelete: Restrict) ·reseller→ Reseller (resellerId,onDelete: Restrict) ·returnRequest→ ReturnRequest (returnRequestId,onDelete: Restrict) ·pdfFile→ File (pdfFileId,onDelete: Restrict) ·issuedBy→ User (issuedById,onDelete: Restrict). - Referenced by: PaymentAllocation.
creditNoteId, Ticket.creditNoteId. - Keys & indexes:
numberunique ·returnRequestIdunique ·@@index([resellerId, issuedAt])·@@index([invoiceId]). - Immutable once issued except
amountAppliedandcancelledAt. The PDF is generated like invoices (GENERATED_DOCUMENT). Refunding an unapplied balance in cash is outside the platform in V1.
Constraints and indexes Prisma cannot express (raw SQL in the migration):
ALTER TABLE credit_notes ADD CONSTRAINT credit_notes_totals CHECK (total_ttc = subtotal_ht + vat_total AND amount_applied BETWEEN 0 AND total_ttc);Example row:
{
"id": "01a1d46b-d880-7abc-b1f2-e6f4830962f1",
"number": "AVO-2026-000004",
"invoiceId": "01a1b128-bf00-73a8-9f2f-e1208bdc0d4a",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"returnRequestId": "01a1c4bd-4220-7f7e-bf49-85cc6160afd3",
"reason": "Retour de 2 câbles (RET-2026-000012)",
"subtotalHt": 63000,
"vatTotal": 11970,
"totalTtc": 74970,
"amountApplied": 74970,
"issuedAt": "2026-11-13T10:20:00.000Z",
"cancelledAt": null,
"pdfFileId": null,
"issuedById": "01a19fd5-411e-7a82-8647-950d7b692ffe",
"createdAt": "2026-11-13T10:20:00.000Z",
"updatedAt": "2026-11-13T10:20:00.000Z"
}V2 — reminders
PaymentReminder
A scheduled or sent payment reminder (relance d'impayé) for an overdue invoice. Table payment_reminders · written by billing (reminder job, FIN-07) · V2.
| Field | Prisma type | Null | Default | Constraints / notes |
|---|---|---|---|---|
id | String @db.Uuid | — | uuid(7) | PK. |
invoiceId | String @db.Uuid | — | — | FK → Invoice · Restrict. |
resellerId | String @db.Uuid | — | — | FK → Reseller · Restrict. |
level | Int | — | — | 1, 2, 3 — escalation step from the reminder policy setting. |
channel | NotificationChannel | — | — | — |
scheduledAt | DateTime | — | — | — |
sentAt | DateTime | yes | — | — |
cancelledAt | DateTime | yes | — | Invoice paid before the send. |
notificationId | String @db.Uuid | yes | — | FK → Notification · SetNull. |
createdById | String @db.Uuid | yes | — | FK → User · Restrict. null for automatic reminders. |
createdAt | DateTime | — | now() | — |
updatedAt | DateTime | — | @updatedAt | — |
- Relations:
invoice→ Invoice (invoiceId,onDelete: Restrict) ·reseller→ Reseller (resellerId,onDelete: Restrict) ·notification→ Notification (notificationId,onDelete: SetNull) ·createdBy→ User (createdById,onDelete: Restrict). - Keys & indexes:
@@unique([invoiceId, level])·@@index([scheduledAt]). - Enums: NotificationChannel.
Example row:
{
"id": "01a24f84-4c00-7b90-8e27-2d39e4b96af6",
"invoiceId": "01a1b128-bf00-73a8-9f2f-e1208bdc0d4a",
"resellerId": "01a19ba2-2fe0-7300-a148-73d1a4ef38c0",
"level": 1,
"channel": "SMS",
"scheduledAt": "2026-12-07T08:00:00.000Z",
"sentAt": "2026-12-07T08:00:04.000Z",
"cancelledAt": null,
"notificationId": null,
"createdById": null,
"createdAt": "2026-12-07T08:00:00.000Z",
"updatedAt": "2026-12-07T08:00:00.000Z"
}