HOCO B2B
SRSDatabase design

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 (default SHIPPED), 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 amountPaid and amountCredited and 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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
numberString @db.VarChar(20)——Unique. FAC-YYYY-NNNNNN from NumberSequence INVOICE, allocated in the issuing transaction so the sequence has no gaps.
orderIdString @db.Uuid——Unique. FK → Order · Restrict. One invoice per order in V1.
resellerIdString @db.Uuid——FK → Reseller · Restrict.
statusInvoiceStatus—UNPAID—
issuedAtDateTime———
dueDateDateTime @db.Date——Issue date + CreditAccount.paymentTermsDays for CREDIT; issue date otherwise.
currencyString @db.Char(3)—"DZD"—
paymentMethodPaymentMethod——Snapshot from the order.
subtotalHtInt——Copied from the order at issue.
discountTotalInt——HT.
deliveryFeeInt——HT.
vatTotalInt———
totalTtcInt——subtotalHt − discountTotal + deliveryFee + vatTotal.
stampDutyInt—0Assumption — Algerian stamp duty (droit de timbre) on cash-settled invoices, computed at issue from setting billing.stampDuty; rates to confirm with HOCO's accountant.
amountPaidInt—0Cache: Σ PaymentAllocation of CONFIRMED payments, maintained in the allocating transaction.
amountCreditedInt—0Cache: Σ PaymentAllocation of credit notes applied to this invoice.
paidAtDateTimeyes—When amountPaid reached the amount due.
cancelledAtDateTimeyes——
cancelledByIdString @db.Uuidyes—FK → User · Restrict.
cancelReasonStringyes——
pdfFileIdString @db.Uuidyes—FK → File · Restrict. Generated PDF (GENERATED_DOCUMENT); regenerated on demand, the data above is the source of truth.
sellerLegalNameString——Seller snapshot from setting billing.sellerIdentity.
sellerAddressString———
sellerNifString @db.VarChar(20)———
sellerNisString @db.VarChar(20)———
sellerRcString @db.VarChar(30)———
sellerAiString @db.VarChar(20)———
sellerRibString @db.VarChar(30)yes—Bank account printed for transfers.
buyerNameString——Buyer snapshot: Reseller.legalName or storeName.
buyerAddressString———
buyerNifString @db.VarChar(20)yes——
buyerNisString @db.VarChar(20)yes——
buyerRcString @db.VarChar(30)yes——
buyerAiString @db.VarChar(20)yes——
createdAtDateTime—now()—
updatedAtDateTime—@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: number unique · orderId unique · @@index([resellerId, status, dueDate]) · @@index([status, dueDate]) · @@index([issuedAt]).
  • Enums: InvoiceStatus, PaymentMethod.
  • Lifecycle: UNPAID → PARTIALLY_PAID → PAID; the daily job sets OVERDUE when dueDate has passed with a balance (back to PARTIALLY_PAID/PAID as payments arrive); CANCELLED by admin.invoices.cancel while 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 amountPaid is 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 recomputes status.

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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
invoiceIdString @db.Uuid——FK → Invoice · Cascade.
orderItemIdString @db.Uuidyes—FK → OrderItem · Restrict.
skuString @db.VarChar(40)———
descriptionString——Brand, product and variant as printed.
quantityInt———
unitPriceHtInt———
discountHtInt—0—
vatRateInt——Basis points.
lineTotalHtInt———
vatAmountInt———
lineTotalTtcInt———
sortOrderInt—0—
createdAtDateTime—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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
numberString @db.VarChar(20)——Unique. PAY-YYYY-NNNNNN from NumberSequence PAYMENT.
resellerIdString @db.Uuid——FK → Reseller · Restrict.
methodPaymentMethod——Never CREDIT (credit is deferral, not a payment) — CHECK.
statusPaymentStatus——Declarations start PENDING_VERIFICATION; payments recorded by finance start CONFIRMED.
amountInt———
currencyString @db.Char(3)—"DZD"—
orderIdString @db.Uuidyes—FK → Order · Restrict. Set for cash collected on delivery (admin.deliveries.proof) and for declarations made from an order.
paidAtDateTime——When the money was paid, as declared or recorded.
referenceString @db.VarChar(60)yes—Transfer/CCP/BaridiMob reference; indexed to flag duplicate declarations.
proofFileIdString @db.Uuidyes—FK → File · Restrict. Purpose PAYMENT_PROOF; required for declarations.
isDeclarationBoolean—falsetrue when declared by the reseller (RA-31).
requestedInvoiceIdsString[] @db.Uuid—[]The invoices the reseller says a declaration pays (payments.declare → invoiceIds); finance allocates them first at verification.
recordedByIdString @db.Uuid——FK → User · Restrict. Reseller user (declaration) or finance user.
verifiedByIdString @db.Uuidyes—FK → User · Restrict.
verifiedAtDateTimeyes——
rejectReasonCodeString @db.VarChar(40)yes—ReferenceItem PAYMENT_REJECT_REASON; required for REJECTED.
rejectCommentStringyes——
noteStringyes——
createdAtDateTime—now()—
updatedAtDateTime—@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: number unique · @@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 = createdAt of 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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
paymentIdString @db.Uuidyes—FK → Payment · Cascade. Set for a payment allocation.
creditNoteIdString @db.Uuidyes—FK → CreditNote · Cascade. Set instead of paymentId when a credit note settles part of an invoice.
invoiceIdString @db.Uuid——FK → Invoice · Restrict.
amountInt———
allocatedByIdString @db.Uuidyes—FK → User · Restrict. null when proposed by the reseller in a declaration.
createdAtDateTime—now()—
updatedAtDateTime—@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 + amountCredited of an invoice ≤ its amount due. Only CONFIRMED payments count in Invoice.amountPaid.
  • PUT …/allocations replaces 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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
resellerIdString @db.Uuid——Unique. FK → Reseller · Restrict.
creditLimitInt—0Ceiling in centimes; 0 = no credit (method CREDIT hidden at checkout).
paymentTermsDaysInt—30Days from invoice issue to due date.
overLimitPolicyCreditOverLimitPolicyyes—null = INHERIT: the setting credit.overLimitPolicy applies (API policy).
isBlockedBoolean—falseBlocks new CREDIT orders (manual, or automatic on overdue in V2).
blockedReasonStringyes——
blockedAtDateTimeyes——
blockedByIdString @db.Uuidyes—FK → User · Restrict.
versionInt—1Optimistic lock: every write does version = version + 1 WHERE version = :expected; 0 rows → 409 VERSION_CONFLICT.
createdAtDateTime—now()—
updatedAtDateTime—@updatedAt—
  • Relations: reseller → Reseller (resellerId, onDelete: Restrict) · blockedBy → User (blockedById, onDelete: Restrict).
  • Referenced by: CreditLimitChange.creditAccountId.
  • Keys & indexes: resellerId unique.
  • 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) with paymentMethod = CREDIT + Σ totalTtc of its CREDIT orders from VALIDATED to DELIVERED that 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 UPDATE on this row, so two simultaneous CREDIT checkouts cannot both consume the last available credit; version protects 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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
creditAccountIdString @db.Uuid——FK → CreditAccount · Restrict.
previousLimitIntyes——
newLimitIntyes——
previousTermsDaysIntyes——
newTermsDaysIntyes——
previousPolicyCreditOverLimitPolicyyes——
newPolicyCreditOverLimitPolicyyes——
previousBlockedBooleanyes——
newBlockedBooleanyes——
reasonStringyes——
changedByIdString @db.Uuidyes—FK → User · Restrict.
createdAtDateTime—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).

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
numberString @db.VarChar(20)——Unique. Proposed format AVO-YYYY-NNNNNN (NumberSequence CREDIT_NOTE, gapless like invoices) — not yet in AUTHORING's list of business numbers.
invoiceIdString @db.Uuid——FK → Invoice · Restrict.
resellerIdString @db.Uuid——FK → Reseller · Restrict.
returnRequestIdString @db.Uuidyes—Unique. FK → ReturnRequest · Restrict. Set when issued by a return reception; an after-sales CREDIT_NOTE resolution links from Ticket.creditNoteId instead.
reasonString———
subtotalHtInt———
vatTotalInt———
totalTtcInt———
amountAppliedInt—0Cache: Σ PaymentAllocation of the credit note.
issuedAtDateTime———
cancelledAtDateTimeyes——
pdfFileIdString @db.Uuidyes—FK → File · Restrict.
issuedByIdString @db.Uuid——FK → User · Restrict.
createdAtDateTime—now()—
updatedAtDateTime—@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: number unique · returnRequestId unique · @@index([resellerId, issuedAt]) · @@index([invoiceId]).
  • Immutable once issued except amountApplied and cancelledAt. 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.

FieldPrisma typeNullDefaultConstraints / notes
idString @db.Uuid—uuid(7)PK.
invoiceIdString @db.Uuid——FK → Invoice · Restrict.
resellerIdString @db.Uuid——FK → Reseller · Restrict.
levelInt——1, 2, 3 — escalation step from the reminder policy setting.
channelNotificationChannel———
scheduledAtDateTime———
sentAtDateTimeyes——
cancelledAtDateTimeyes—Invoice paid before the send.
notificationIdString @db.Uuidyes—FK → Notification · SetNull.
createdByIdString @db.Uuidyes—FK → User · Restrict. null for automatic reminders.
createdAtDateTime—now()—
updatedAtDateTime—@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"
}

On this page