Finance
Back-office finance operations — invoices and their cancellation, payments recorded or declared and their verification, allocations, credit accounts with exposure and limit history, and the V2 overdue and accounting exports.
These operations follow the financial commitments of resellers (cahier §4.7): invoices generated from orders, payments by cash on delivery, transfer, CCP, BaridiMob or credit, partial payments and their allocation to invoices, the credit ceiling with used and available credit, and blocking (FIN-01 to FIN-05). Screens: invoices (BO-44), payments and declarations (BO-45), credit accounts (BO-46), the finance dashboard (BO-08) and, in V2, overdue reminders and accounting exports (BO-47, BO-48). Invoice and payment statuses are defined on Lifecycles › Invoice and Payment.
Credit notes are V1 since the 17 Oct 2026 decisions (FIN-08): they are issued automatically by a return reception (admin.returns.receive) and by an after-sales resolution CREDIT_NOTE (admin.tickets.resolve); they appear on invoices and in credit exposure here. Cancelling an invoice remains a cancellation (number kept, never reused).
Who uses it. The Responsable finance (cahier §3: « Paiements, crédit, factures, échéances et rapports financiers »); the Super administrateur; the Direction reads. Access to financial data is strictly limited (cahier §4.7, ACL-04): nothing on this page is returned without finance.view.
| Permission | Unlocks on this page |
|---|---|
finance.view | Invoices, payments, credit accounts (read); V2 overdue list |
finance.create | Recording a payment |
finance.update | Changing the allocations of a confirmed payment |
finance.validate | Verifying a reseller declaration; cancelling an invoice |
finance.administer | Credit limit, payment terms, blocking and policy |
finance.export | V2 accounting exports (list exports of invoices and payments use admin.exports.create) |
Settings used (settings keys): billing.invoiceOn (default SHIPPED), credit.overLimitPolicy (default BLOCK), credit.defaultLimit (0), credit.defaultTermDays (30), payments.enabledMethods.
Credit exposure (used by checkout, order controls, the finance dashboard and this page):
| Term | Formula |
|---|---|
used (exposure, BR-33) | Σ balance of the reseller's UNPAID, PARTIALLY_PAID and OVERDUE invoices (whatever the payment method) + Σ totalTtc of their CREDIT orders not yet invoiced (SUBMITTED, PENDING, VALIDATED, PREPARING, READY, and later statuses while uninvoiced) − confirmed payments and credit notes not yet allocated. Declarations PENDING_VERIFICATION do not reduce it |
available | max(0, limit − used) |
| Control | Applies only to orders paid by CREDIT: orderTotal > available → refused at checkout (BLOCK) or order PENDING with CREDIT_OVER_LIMIT (REQUIRE_APPROVAL); a blocked account refuses every CREDIT order |
Audit. UPDATE/STATUS_CHANGE on invoices (cancellation with reason), CREATE on payments, VALIDATE/REJECT on verifications, UPDATE on allocations (before/after), UPDATE on credit accounts with a CreditLimitChange row for every changed field.
Conventions: lists return Page<T> = { items, total, page, pageSize } (responses); Totals, FileRef, ResellerSummary, UserSummary are shared schemas; money in centimes; dates YYYY-MM-DD in Africa/Algiers; editable records carry version; a missing reason answers 422 REASON_REQUIRED; an Idempotency-Key is recommended on admin.payments.record. 401, 429 and 500 apply everywhere and are not repeated.
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /admin/invoices | admin.invoices.listInvoices with status, reseller, dates filters | finance.view | BO | V1 |
| GET | /admin/invoices/{invoiceId} | admin.invoices.getInvoice with allocations | finance.view | BO | V1 |
| POST | /admin/invoices/{invoiceId}/cancel | admin.invoices.cancelCancel an invoice (V2: issues a credit note) | finance.validate | BO | V1 |
| GET | /admin/payments | admin.payments.listPayments and declarations to verify | finance.view | BO | V1 |
| POST | /admin/payments | admin.payments.recordRecord a payment (cash on delivery, transfer…) with allocations | finance.create | BO | V1 |
| POST | /admin/payments/{paymentId}/verification | admin.payments.verifyConfirm or reject a reseller declaration | finance.validate | BO | V1 |
| PUT | /admin/payments/{paymentId}/allocations | admin.payments.allocateAllocate a payment across invoices | finance.update | BO | V1 |
| GET | /admin/credit-accounts | admin.credit.listCredit accounts with live exposure (filters: policy, blocked, over limit, overdue, wilaya, q) | finance.view | BO | V1 |
| GET | /admin/credit-accounts/{resellerId} | admin.credit.getCredit account with exposure | finance.view | BO | V1 |
| PATCH | /admin/credit-accounts/{resellerId} | admin.credit.updateSet limit, terms, block/unblock (history kept) | finance.administer | BO | V1 |
| GET | /admin/finance/overdue | admin.finance.overdueOverdue invoices and reminder history (V2) | finance.view | BO | V2 |
| POST | /admin/finance/exports | admin.finance.exportAccounting export (V2) | finance.export | BO | V2 |
Schemas
Invoice
interface Invoice {
id: Uuid;
number: string; // FAC-2026-000123
status: InvoiceStatus; // UNPAID, PARTIALLY_PAID, PAID, OVERDUE, CANCELLED
order: { id: Uuid; number: string; paymentMethod: PaymentMethod };
reseller: ResellerSummary & { customerType: CustomerType };
issuedAt: Iso;
dueDate: string; // YYYY-MM-DD = issue date + the account's termDays (CREDIT orders) or billing.dueDaysDefault (other methods, default 7) — BR-36
totals: Totals;
vatBreakdown: { vatRate: BasisPoints; baseHt: Money; vat: Money }[];
paidAmount: Money; // Σ confirmed allocations
creditedAmount: Money; // Σ credit notes allocated to this invoice
balance: Money; // totalTtc − paidAmount − creditedAmount
daysOverdue: number; // 0 unless OVERDUE
lines: { sku: string; label: string; quantity: number; unitPriceHt: Money; discountHt: Money; vatRate: BasisPoints; totalHt: Money; totalTtc: Money }[];
allocations: { paymentId: Uuid; paymentNumber: string; method: PaymentMethod; amount: Money; allocatedAt: Iso }[];
creditNotes: { id: Uuid; number: string; amountTtc: Money; allocatedAmount: Money; issuedAt: Iso; origin: "RETURN" | "AFTER_SALES"; file?: FileRef }[];
legal: { sellerName: string; rc: string; nif: string; nis: string; ai: string; address: string };
pdf?: FileRef; // signed URL, 5 min
cancelledAt?: Iso;
cancelReason?: string;
version: number;
}Payment
interface Payment {
id: Uuid;
number: string; // PAY-2026-000077
status: PaymentStatus; // PENDING_VERIFICATION, CONFIRMED, REJECTED
source: "BACK_OFFICE" | "RESELLER_DECLARATION" | "DELIVERY_COLLECTION"; // PaymentSource
reseller: ResellerSummary;
method: PaymentMethod;
amount: Money;
paidOn: string; // YYYY-MM-DD, date of the transfer / deposit / collection
reference?: string; // transfer reference, CCP receipt number…
proof?: FileRef; // PAYMENT_PROOF
order?: { id: Uuid; number: string }; // collections on delivery
declaredAt?: Iso; declaredBy?: UserSummary;
recordedBy?: UserSummary;
verifiedAt?: Iso; verifiedBy?: UserSummary;
rejection?: { reasonCode: string; reason?: string };
allocations: { invoiceId: Uuid; invoiceNumber: string; amount: Money }[];
unallocated: Money; // amount − Σ allocations (an advance once CONFIRMED)
note?: string;
createdAt: Iso;
version: number;
}CreditAccount
interface CreditAccount {
resellerId: Uuid;
reseller: ResellerSummary & { customerType: CustomerType };
limit: Money;
termsDays: number; // payment terms → invoice due date
blocked: boolean;
blockedReason?: string;
policy: "INHERIT" | "BLOCK" | "REQUIRE_APPROVAL"; // CreditPolicy; INHERIT = credit.overLimitPolicy
effectivePolicy: "BLOCK" | "REQUIRE_APPROVAL";
used: Money;
available: Money;
exposure: { openInvoices: Money; uninvoicedCreditOrders: { count: number; amount: Money }; unallocatedCredit: Money }; // used = openInvoices + uninvoicedCreditOrders.amount − unallocatedCredit
overdue: { amount: Money; invoices: number; oldestDaysOverdue: number };
history: { field: "LIMIT" | "TERMS_DAYS" | "BLOCKED" | "POLICY"; before: number | boolean | string; after: number | boolean | string; reason: string; changedBy: UserSummary; changedAt: Iso }[]; // CreditLimitChange, last 20
updatedAt: Iso;
version: number;
}Invoices
GET /admin/invoices
admin.invoices.list · Permission finance.view · Apps BO · Release V1 · Requirements FIN-01
Purpose. Invoices with status, reseller and date filters.
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | InvoiceStatus[] | repeatable | |
resellerId | Uuid | Reseller 360° tab | |
orderId | Uuid | ||
wilayaCode / delegateId | string[] / Uuid[] | repeatable | Reseller's wilaya / assigned delegate |
paymentMethod | PaymentMethod[] | repeatable | Of the order |
dateField | string | issuedAt (default) | dueDate | |
from / to | string (date) | ||
minBalance | Money | ||
q | string | min 2 chars | Invoice or order number, store name, reseller number |
sort | string | issuedAt (default), dueDate, balance, totalTtc, number | |
order / page / pageSize | desc · 1 · 25 (max 200) |
Response 200 OK
{
"items": [
{
"id": "01930c2d-7e8f-7a9b-8c0d-1e2f3a4b5c70", "number": "FAC-2026-000123", "status": "PARTIALLY_PAID",
"order": { "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158", "paymentMethod": "CREDIT" },
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "customerType": "DEMI_GROS" },
"issuedAt": "2026-11-05T09:30:00Z", "dueDate": "2026-12-05",
"totalTtc": 3074960, "paidAmount": 2000000, "creditedAmount": 0, "balance": 1074960, "daysOverdue": 0
}
],
"total": 1184,
"page": 1,
"pageSize": 25,
"totals": { "totalTtc": 3074960, "balance": 1074960 }
}totals sums the filtered set (not only the page).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown status or sort field |
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
Rules & side effects
- Read-only. Invoices are created by the order state machine at
billing.invoiceOn(defaultSHIPPED); this page never creates one.
GET /admin/invoices/{invoiceId}
admin.invoices.get · Permission finance.view · Apps BO · Release V1 · Requirements FIN-01
Purpose. An invoice with its lines, VAT breakdown, legal mentions, allocations, credit notes and PDF.
Response 200 OK — Invoice
{
"id": "01930c2d-7e8f-7a9b-8c0d-1e2f3a4b5c70",
"number": "FAC-2026-000123",
"status": "PARTIALLY_PAID",
"order": { "id": "0193016e-4b2a-7c11-9d8e-7f6a5b4c3d2e", "number": "CMD-2026-000158", "paymentMethod": "CREDIT" },
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "customerType": "DEMI_GROS" },
"issuedAt": "2026-11-05T09:30:00Z",
"dueDate": "2026-12-05",
"totals": { "subtotalHt": 2710000, "discountTotal": 126000, "deliveryFee": 0, "vatTotal": 490960, "totalTtc": 3074960, "currency": "DZD" },
"vatBreakdown": [{ "vatRate": 1900, "baseHt": 2584000, "vat": 490960 }],
"paidAmount": 2000000,
"creditedAmount": 0,
"balance": 1074960,
"daysOverdue": 0,
"lines": [
{ "sku": "HC-X14-CC-1M-BK", "label": "HOCO Câble USB-C vers USB-C 60 W 1 m — Noir · 1 m", "quantity": 20, "unitPriceHt": 35000, "discountHt": 0, "vatRate": 1900, "totalHt": 700000, "totalTtc": 833000 },
{ "sku": "AF-A29-50W-WH", "label": "ACEFAST Chargeur GaN 50 W 2 ports — Blanc", "quantity": 6, "unitPriceHt": 210000, "discountHt": 126000, "vatRate": 1900, "totalHt": 1134000, "totalTtc": 1349460 },
{ "sku": "BF-BH20-BK", "label": "BOROFONE Support voiture magnétique — Noir", "quantity": 15, "unitPriceHt": 50000, "discountHt": 0, "vatRate": 1900, "totalHt": 750000, "totalTtc": 892500 }
],
"allocations": [{ "paymentId": "01930d3e-8f90-7a1b-9c2d-3e4f5a6b7c81", "paymentNumber": "PAY-2026-000077", "method": "BANK_TRANSFER", "amount": 2000000, "allocatedAt": "2026-11-12T10:04:00Z" }],
"creditNotes": [],
"legal": { "sellerName": "HOCO Algérie SARL", "rc": "16/00-0987654 B 18", "nif": "001816098765432", "nis": "098716010098765", "ai": "16012098765", "address": "Zone d'activités, Rouiba, Alger" },
"pdf": { "id": "01930d60-2c3d-7e4f-8a5b-6c7d8e9f0a1b", "url": "https://files.hoco.dz/s/fac-2026-000123.pdf?sig=…", "mimeType": "application/pdf", "size": 61207, "purpose": "GENERATED_DOCUMENT" },
"version": 3
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
| 404 | NOT_FOUND | Unknown invoice |
Rules & side effects
- Read-only. Seller legal identifiers come from the company settings; the values above are placeholders.
POST /admin/invoices/{invoiceId}/cancel
admin.invoices.cancel · Permission finance.validate · Apps BO · Release V1 · Requirements FIN-01 · FIN-08
Purpose. Cancels an invoice that must not stand (wrong data, cancelled order). The number is kept and never reused; a credit note for the full amount is issued in V1 (BR-39).
Request
interface CancelInvoiceBody { reason: string; reissue?: boolean; version: number } // reissue: issue a corrected invoice at once when the order is still invoiceable{ "reason": "Adresse de facturation erronée — facture réémise.", "reissue": true, "version": 3 }Response 200 OK — the cancelled Invoice (status CANCELLED, cancelledAt, cancelReason) plus "creditNote": { "id": "01931c50-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "number": "AV-2026-000006", "amountTtc": 3074960 } and, when reissue is true, "reissued": { "id": "01930e20-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "number": "FAC-2026-000131" }.
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks finance.validate |
| 404 | NOT_FOUND | Unknown invoice |
| 409 | INVOICE_HAS_PAYMENTS | Confirmed allocations or allocated credit notes exist — deallocate first |
| 409 | INVOICE_ALREADY_CANCELLED | Status CANCELLED |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | REASON_REQUIRED | Empty reason |
Rules & side effects
UNPAID/OVERDUE→CANCELLED; aCreditNote(AV-number, full amount, originINVOICE_CANCELLATION) is issued and linked to the invoice; the credit exposure moves back to the order if it is still invoiceable (or disappears when the order is cancelled or returned).reissueissues a newFAC-invoice from the order's current snapshot.- The PDF is regenerated with the mention « ANNULÉE ». Audit
STATUS_CHANGEwith the reason. - Notifications: the reseller is notified through the credit note (
invoice.issuedtemplate family; no dedicated event in the notifications matrix); the reissued invoice sendsinvoice.issued.
Used by BO-44
Payments
GET /admin/payments
admin.payments.list · Permission finance.view · Apps BO · Release V1 · Requirements FIN-03
Purpose. Payments and reseller declarations; the default tab is « À vérifier » (PENDING_VERIFICATION).
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
status | PaymentStatus[] | repeatable | |
source | PaymentSource[] | BACK_OFFICE, RESELLER_DECLARATION, DELIVERY_COLLECTION | |
method | PaymentMethod[] | repeatable | |
resellerId | Uuid | ||
unallocated | boolean | Confirmed payments with an unallocated remainder | |
from / to | string (date) | On paidOn | |
q | string | min 2 chars | Payment number, reference, store name, invoice number |
sort | string | createdAt (default), paidOn, amount | |
order / page / pageSize | desc · 1 · 25 (max 200) |
Response 200 OK
{
"items": [
{
"id": "01930d4f-9a01-7b2c-8d3e-4f5a6b7c8d92", "number": "PAY-2026-000079", "status": "PENDING_VERIFICATION", "source": "RESELLER_DECLARATION",
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE" },
"method": "CCP", "amount": 1074960, "paidOn": "2026-11-20", "reference": "CCP 0012345678 clé 45 — reçu n° 88231",
"proof": { "id": "01930d4f-0b12-7c3d-9e4f-5a6b7c8d9e03", "url": "https://files.hoco.dz/s/pay79-proof.jpg?sig=…", "thumbnailUrl": "https://files.hoco.dz/s/pay79-proof-thumb.jpg?sig=…", "mimeType": "image/jpeg", "size": 734221, "purpose": "PAYMENT_PROOF" },
"declaredAt": "2026-11-20T15:41:00Z", "declaredBy": { "id": "0192f4b8-7001-7a02-8b03-0c0d0e0f1011", "displayName": "Karim Benali" },
"allocations": [], "unallocated": 1074960, "createdAt": "2026-11-20T15:41:00Z", "version": 1
}
],
"total": 6,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown enum value |
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
Rules & side effects
- Read-only. New declarations and cash collections arrive live with
payment.declared.
POST /admin/payments
admin.payments.record · Permission finance.create · Apps BO · Release V1 · Requirements FIN-03 · FIN-04
Purpose. Records a payment finance has seen (transfer on the bank statement, cash brought to the depot…), confirmed at once, with its allocation to invoices.
Request
interface RecordPaymentBody {
resellerId: Uuid;
method: PaymentMethod; // must be enabled (payments.enabledMethods); CREDIT is not a payment method here
amount: Money; // > 0
paidOn: string; // YYYY-MM-DD, not in the future
reference?: string;
proofFileId?: Uuid; // PAYMENT_PROOF
allocations?: { invoiceId: Uuid; amount: Money }[]; // omitted + autoAllocate → oldest due first
autoAllocate?: boolean; // default true when allocations is omitted
note?: string;
}{
"resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a",
"method": "BANK_TRANSFER",
"amount": 2000000,
"paidOn": "2026-11-12",
"reference": "VIR BNA 2026-11-12 / 004512",
"allocations": [{ "invoiceId": "01930c2d-7e8f-7a9b-8c0d-1e2f3a4b5c70", "amount": 2000000 }]
}Response 201 Created — the Payment: number PAY-2026-000077, status CONFIRMED, source BACK_OFFICE, recordedBy the caller, allocations as sent, unallocated 0, version 1.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Amount ≤ 0, future paidOn, CREDIT method, duplicate invoice in allocations |
| 403 | PERMISSION_DENIED | Caller lacks finance.create |
| 404 | NOT_FOUND | Unknown reseller, invoice or file |
| 422 | ALLOCATION_EXCEEDS_PAYMENT | Σ allocations > amount |
| 422 | ALLOCATION_EXCEEDS_BALANCE | An allocation exceeds the invoice balance — details: { invoiceId, balance } |
| 422 | ALLOCATION_RESELLER_MISMATCH | An invoice belongs to another reseller |
| 422 | INVOICE_CANCELLED | Allocation to a cancelled invoice |
| 422 | PAYMENT_METHOD_DISABLED | Method not in payments.enabledMethods |
Rules & side effects
PAY-number; statusCONFIRMED; allocations applied in the same transaction; invoice statuses recomputed (hereUNPAID→PARTIALLY_PAID, balance 1 074 960); an unallocated remainder is an advance that lowers credit exposure.- Credit exposure recomputed; orders
PENDINGforCREDIT_OVER_LIMITare re-checked and move toSUBMITTEDwhen the credit now suffices (they still wait for confirmation). - Audit
CREATEonPaymentwith allocations. TimelineInteractionPAYMENTon the reseller. - Notifications:
payment.confirmed→ reseller (push + in-app) with the amount and the invoices settled.
POST /admin/payments/{paymentId}/verification
admin.payments.verify · Permission finance.validate · Apps BO · Release V1 · Requirements FIN-03
Purpose. Confirms or rejects a reseller's declaration (transfer, CCP, BaridiMob with proof) or a cash collection recorded at delivery.
Request
interface VerifyPaymentBody {
decision: "CONFIRM" | "REJECT";
amount?: Money; // CONFIRM: the amount actually received if it differs from the declaration (≤ declared)
allocations?: { invoiceId: Uuid; amount: Money }[]; // CONFIRM; default: oldest due invoices first
reasonCode?: string; // REJECT: PAYMENT_REJECT_REASON (PROOF_UNREADABLE, AMOUNT_MISMATCH, NOT_RECEIVED, DUPLICATE, OTHER…)
reason?: string; // REJECT: required for OTHER; shown to the reseller
note?: string;
version: number;
}{ "decision": "CONFIRM", "allocations": [{ "invoiceId": "01930c2d-7e8f-7a9b-8c0d-1e2f3a4b5c70", "amount": 1074960 }], "version": 1 }Response 200 OK — the Payment with status CONFIRMED, verifiedAt, verifiedBy, allocations; invoice FAC-2026-000123 becomes PAID (balance 0).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | reasonCode on a confirmation, amount above the declared amount |
| 403 | PERMISSION_DENIED | Caller lacks finance.validate |
| 404 | NOT_FOUND | Unknown payment or invoice |
| 409 | PAYMENT_NOT_PENDING | Status is not PENDING_VERIFICATION |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | REASON_REQUIRED | Rejection without reasonCode, or OTHER without reason |
| 422 | ALLOCATION_EXCEEDS_PAYMENT · ALLOCATION_EXCEEDS_BALANCE · ALLOCATION_RESELLER_MISMATCH · INVOICE_CANCELLED | As for recording |
Rules & side effects
PENDING_VERIFICATION→CONFIRMED(allocations applied, invoices recomputed, exposure lowered — the declaration did not change available credit before) or →REJECTED(proof kept).- Audit
VALIDATEorREJECT; timelinePAYMENTentry on confirmation. - Notifications:
payment.confirmedorpayment.rejected(with the reason) → reseller (push + in-app).
Used by BO-45
PUT /admin/payments/{paymentId}/allocations
admin.payments.allocate · Permission finance.update · Apps BO · Release V1 · Requirements FIN-04
Purpose. Replaces the allocation of a confirmed payment across invoices (reconciliation, correcting a mistake — cahier §4.7 « Paiements partiels et rapprochement »).
Request
interface AllocatePaymentBody {
allocations: { invoiceId: Uuid; amount: Money }[]; // the full new set (empty = everything unallocated)
reason: string; // required: a confirmed payment is never edited silently
version: number;
}{
"allocations": [
{ "invoiceId": "01930c2e-1a2b-7c3d-9e4f-5a6b7c8d9e01", "amount": 1023400 },
{ "invoiceId": "01930c2d-7e8f-7a9b-8c0d-1e2f3a4b5c70", "amount": 976600 }
],
"reason": "Le client précise que le virement couvre d'abord FAC-2026-000122.",
"version": 1
}Response 200 OK — the Payment with its new allocations and unallocated, plus affectedInvoices: [{ id, number, status, balance }].
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks finance.update |
| 404 | NOT_FOUND | Unknown payment or invoice |
| 409 | PAYMENT_NOT_CONFIRMED | Payment PENDING_VERIFICATION or REJECTED |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | REASON_REQUIRED | Empty reason |
| 422 | ALLOCATION_EXCEEDS_PAYMENT · ALLOCATION_EXCEEDS_BALANCE · ALLOCATION_RESELLER_MISMATCH · INVOICE_CANCELLED | Balance is checked after releasing this payment's current allocations |
Rules & side effects
- All-or-nothing; invoices statuses recomputed (a
PAIDinvoice can go back toPARTIALLY_PAID,UNPAIDorOVERDUE); credit exposure recomputed. - Audit
UPDATEonPaymentwith the before/after allocations and the reason. No notification to the reseller.
Credit accounts
GET /admin/credit-accounts
admin.credit.list · Permission finance.view · Apps BO · Release V1 · Requirements FIN-05 · Added in contract 1.1.0 (SRV-41)
Purpose. The table of BO-46: every credit account with its live exposure — the resellers with an account row (limit set by finance) and every approved customer (default limit credit.defaultLimit, version: 0 until a first change).
Request
| Query | Type | Values / default | Notes |
|---|---|---|---|
page, pageSize | int | 1, 25 (max 200) | Offset pagination |
q | string | ≥ 2 characters | Reseller number, store name, owner name or phone |
policy | CreditPolicy[] | repeatable: INHERIT | BLOCK | REQUIRE_APPROVAL | The account's own policy (INHERIT includes resellers without an account row) |
blocked | boolean | Credit blocked by finance | |
overLimit | boolean | used above limit | |
hasOverdue | boolean | At least one OVERDUE invoice | |
wilayaCode | string[] | repeatable | Store address |
sort | string | storeName (default) | limit | used | available | overdue | updatedAt | overdue sorts by the overdue amount |
order | asc | desc | asc |
Response 200 OK — { items, total, page, pageSize }; each item is a CreditAccount without history, plus overLimit:
{
"items": [
{
"resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a",
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "customerType": "DEMI_GROS" },
"limit": 5000000, "termsDays": 30, "blocked": false, "policy": "INHERIT", "effectivePolicy": "BLOCK",
"used": 6074600, "available": 0, "overLimit": true,
"exposure": { "openInvoices": 2100000, "uninvoicedCreditOrders": { "count": 1, "amount": 3974600 }, "unallocatedCredit": 0 },
"overdue": { "amount": 0, "invoices": 0, "oldestDaysOverdue": 0 },
"updatedAt": "2026-10-02T09:00:00Z", "version": 4
}
],
"total": 1, "page": 1, "pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown sort, q shorter than 2 characters |
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
Rules & side effects
- Read-only. The exposure is computed live (formula above), never stored: a filter on
overLimit/hasOverdueor a sort other thanstoreNamecomputes it for every matching account before paginating.
Used by BO-46
GET /admin/credit-accounts/{resellerId}
admin.credit.get · Permission finance.view · Apps BO · Release V1 · Requirements FIN-05
Purpose. A reseller's credit account with its exposure, overdue amounts and limit history.
Request
| Path | Type | Notes |
|---|---|---|
resellerId | Uuid | Every reseller has an account (limit credit.defaultLimit until set) |
Response 200 OK — CreditAccount
{
"resellerId": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a",
"reseller": { "id": "0192f4b8-61c2-7a3e-9b10-1f2e3d4c5b6a", "number": "REV-000123", "storeName": "Phone Store El Bahia", "ownerName": "Karim Benali", "phone": "+213555123456", "wilayaCode": "31", "communeName": "Bir El Djir", "status": "ACTIVE", "customerType": "DEMI_GROS" },
"limit": 5000000,
"termsDays": 30,
"blocked": false,
"policy": "INHERIT",
"effectivePolicy": "BLOCK",
"used": 6074600,
"available": 0,
"exposure": { "openInvoices": 2100000, "uninvoicedCreditOrders": { "count": 1, "amount": 3974600 }, "unallocatedCredit": 0 },
"overdue": { "amount": 0, "invoices": 0, "oldestDaysOverdue": 0 },
"history": [
{ "field": "LIMIT", "before": 3000000, "after": 5000000, "reason": "Historique de paiement régulier sur 4 mois.", "changedBy": { "id": "01929eaa-7b8c-7d9e-8fa0-1b2c3d4e5f60", "displayName": "Rachid Ouali" }, "changedAt": "2026-10-02T09:00:00Z" }
],
"updatedAt": "2026-10-02T09:00:00Z",
"version": 4
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
| 404 | NOT_FOUND | Unknown reseller |
Rules & side effects
- Read-only; exposure is computed live with the formula above.
PATCH /admin/credit-accounts/{resellerId}
admin.credit.update · Permission finance.administer · Apps BO · Release V1 · Requirements FIN-05 · FIN-06
Purpose. Sets the credit limit, payment terms, policy, or blocks / unblocks the account — every change historised with its reason.
Request
interface UpdateCreditAccountBody {
limit?: Money; // ≥ 0
termsDays?: number; // 0–180; applies to invoices issued from now on
blocked?: boolean;
policy?: "INHERIT" | "BLOCK" | "REQUIRE_APPROVAL";
reason: string; // required, 3–500
version: number;
}{ "limit": 7000000, "reason": "Plafond relevé pour la commande CMD-2026-000158 (accord direction).", "version": 4 }Response 200 OK — the updated CreditAccount (limit 7 000 000, used 6 074 600, available 925 400 — and the credit control of CMD-2026-000158, which excludes the order itself, becomes OK: 7 000 000 − 2 100 000 = 4 900 000 ≥ 3 974 600; a new history entry; version 5) plus warnings — e.g. [{ "code": "LIMIT_BELOW_USED", "details": { "used": 6074600 } }] when a new limit is below used (allowed: CREDIT orders are then refused).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Negative limit, terms out of range, empty body |
| 403 | PERMISSION_DENIED | Caller lacks finance.administer |
| 404 | NOT_FOUND | Unknown reseller |
| 409 | VERSION_CONFLICT | Stale version |
| 422 | REASON_REQUIRED | Empty reason |
Rules & side effects
- One
CreditLimitChangerow per changed field (before, after, reason, actor); auditUPDATE. Detailed credit (automatic blocking on overdue, approval workflows) is V2 (FIN-06); in V1 blocking is manual. blocked: truerefuses everyCREDITcheckout and makes order controlsBLOCKED; ordersPENDINGforCREDIT_OVER_LIMITare re-checked after a limit increase.- Notifications:
credit.limit_changed→ reseller (in-app) on a limit change;credit.blocked→ reseller (push + in-app) and the assigned delegate when blocked. The reseller app's finance screen reflects the new available credit at once.
V2 — overdue and accounting
GET /admin/finance/overdue
admin.finance.overdue · Permission finance.view · Apps BO · Release V2 · Requirements FIN-07
V2
admin.invoices.list ?status=OVERDUE.Purpose. Overdue invoices grouped by reseller with aging buckets and the history of automatic reminders (PaymentReminder).
Request — filters agingBucket (1-30, 31-60, 61-90, 90+), resellerId, delegateId, wilayaCode, minAmount, reminderStatus; sort overdueAmount (default) | oldestDaysOverdue; page / pageSize.
Response 200 OK
{
"items": [
{
"reseller": { "id": "0192f501-6b7c-7d8e-9f0a-1b2c3d4e5f60", "number": "REV-000188", "storeName": "Mobile Center", "ownerName": "Samir Haddad", "phone": "+213770998877", "wilayaCode": "16", "communeName": "Bir Mourad Raïs", "status": "SUSPENDED" },
"overdueAmount": 1890000,
"invoices": [{ "id": "01930a01-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "number": "FAC-2026-000088", "dueDate": "2026-09-30", "balance": 1890000, "daysOverdue": 36 }],
"aging": { "1-30": 0, "31-60": 1890000, "61-90": 0, "90+": 0 },
"reminders": [{ "at": "2026-10-07T08:00:00Z", "channel": "SMS", "level": 1 }, { "at": "2026-10-21T08:00:00Z", "channel": "WHATSAPP", "level": 2 }]
}
],
"total": 17,
"page": 1,
"pageSize": 25
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks finance.view |
Rules & side effects
- Read-only. V2 reminders are sent by a daily job per reminder rules (FIN-07).
Used by BO-47
POST /admin/finance/exports
admin.finance.export · Permission finance.export · Apps BO · Release V2 · Requirements FIN-09
V2
Purpose. Accounting exports for the client's accounting tool: sales journal, payments journal, credit notes and aged balance.
Request
interface FinanceExportBody {
kind: "SALES_JOURNAL" | "PAYMENTS_JOURNAL" | "CREDIT_NOTES" | "AGED_BALANCE";
from: string; to: string; // AGED_BALANCE: `to` is the balance date
format: "XLSX" | "CSV";
layout?: "GENERIC"; // accounting-tool layouts added when the ERP is known (INT-06)
}{ "kind": "SALES_JOURNAL", "from": "2027-01-01", "to": "2027-01-31", "format": "CSV" }Response 202 Accepted — an ExportJob with "source": { "kind": "REPORT", "key": "finance.sales_journal" }.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown kind, to before from |
| 403 | PERMISSION_DENIED | Caller lacks finance.export |
| 422 | REPORT_RANGE_TOO_LARGE | More than 366 days |
Rules & side effects
- Audit
EXPORT; notification and SSEexport.readyto the requester.
Used by BO-48
Orders & delivery
Back-office order processing — queue of orders to confirm, controls, confirmation with overrides, refusal, controlled modification, transitions, cancellation, documents, returns, the delivery board, proof of delivery and carriers.
Promotions & loyalty
Back-office promotion operations — promotions with scope, targeting, schedule, caps and stacking, their status and simulation, banners and daily offers, and the V2 packs and loyalty programme.