From d74c8a05920df004af60b72e5fa2ff510458661c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Sat, 15 Aug 2026 20:54:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(ledger):=20deploy=20the=20bookkeeping=20se?= =?UTF-8?q?rvice=20=E2=80=94=20compose/k8s,=20ingress,=20off-site=20backup?= =?UTF-8?q?=20volume,=20submodule=20bumps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy: ledger service in docker-compose (ledger profile), k8s 27-ledger.yaml (StatefulSet, PVCs, OFFSITE_DIR backup volume), ingress route, nginx /api/ledgers proxy, ucp-profile with the ledger grain - platform/event + platform/order: invoice.issued/paid/credited payload contracts (VatBuckets per rate, OCR reference on payments, credit-note reversal payload) - prep-dev-data creates data/commerce/ledger - docs/status.md: ledger service row + Phase-2 export/import summary; wayfinder: ledger-accounting map + tickets 07-16, b2b-invoices map notes - bumps: go-cart-actor 5c9036d (ledger domain), backoffice f09202d (Ledger dashboard + proxy) Generated with Codebuff 🤖 Co-Authored-By: Codebuff --- event/event.go | 10 +++++++-- order/order.go | 57 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/event/event.go b/event/event.go index 35bf269..4165b59 100644 --- a/event/event.go +++ b/event/event.go @@ -64,9 +64,15 @@ const ( // InvoiceIssued is emitted when an order is invoiced (pay-later placement or // manual invoice issuance). Payload is an order.InvoiceIssued (platform/order). InvoiceIssued Type = "invoice.issued" - // InvoicePaid is emitted when an invoiced order payment status crosses to paid. - // Payload is an order.InvoicePaid (platform/order). + // InvoicePaid is emitted for EVERY registered payment on an invoiced order + // (each event = one payment fact with its own amount/paidAt; the ledger + // books 1930→1510 per event). Payload is an order.InvoicePaid (platform/order). InvoicePaid Type = "invoice.paid" + // InvoiceCredited is emitted when a credit note is issued against an issued + // invoice. Payload is an order.InvoiceCredited (platform/order): the credited + // amount + per-rate VAT split, which the ledger books as the reversal + // voucher (1510 C / 304x D / 261x D per rate). + InvoiceCredited Type = "invoice.credited" // Analytics (tracking). Replaces the integer enum 0–6 that finder and diff --git a/order/order.go b/order/order.go index d171e76..1d93de7 100644 --- a/order/order.go +++ b/order/order.go @@ -59,16 +59,28 @@ func Decode(raw []byte) (Created, error) { return c, err } +// VatBucket is one VAT rate's split on an issued invoice: the net basis and +// the VAT amount, both in minor units. RateBp is basis points (2500 = 25 %). +// Summing ExVat+Vat across buckets equals AmountIncVat — the ledger reactor +// books the invoice from these (revenue accounts per rate, e.g. 3041/3042/3043 +// and 2611/2621/2631 in BAS), so it needs the split, not just the total. +type VatBucket struct { + RateBp int `json:"rate_bp"` + ExVat int64 `json:"ex_vat"` + Vat int64 `json:"vat"` +} + // InvoiceIssued is the payload of an event.InvoiceIssued envelope. type InvoiceIssued struct { - OrderID string `json:"order_id"` - InvoiceNumber string `json:"invoice_number"` - CompanyID string `json:"company_id,omitempty"` - CustomerEmail string `json:"customer_email,omitempty"` - AmountIncVat int64 `json:"amount_inc_vat"` - DueDate string `json:"due_date"` - OCRReference string `json:"ocr_reference"` - IssuedAtMs int64 `json:"issued_at_ms"` + OrderID string `json:"order_id"` + InvoiceNumber string `json:"invoice_number"` + CompanyID string `json:"company_id,omitempty"` + CustomerEmail string `json:"customer_email,omitempty"` + AmountIncVat int64 `json:"amount_inc_vat"` + VatBuckets []VatBucket `json:"vat_buckets,omitempty"` + DueDate string `json:"due_date"` + OCRReference string `json:"ocr_reference"` + IssuedAtMs int64 `json:"issued_at_ms"` } func (i InvoiceIssued) Encode() ([]byte, error) { return json.Marshal(i) } @@ -86,6 +98,10 @@ type InvoicePaid struct { CompanyID string `json:"company_id,omitempty"` AmountPaid int64 `json:"amount_paid"` PaidAtMs int64 `json:"paid_at_ms"` + // OcrReference lets the ledger settle the exact receivable (the payment's + // 1510 credit references the invoice's OCR) without depending on the + // invoice.issued event having been booked first. + OcrReference string `json:"ocr_reference,omitempty"` } func (i InvoicePaid) Encode() ([]byte, error) { return json.Marshal(i) } @@ -96,3 +112,28 @@ func DecodeInvoicePaid(raw []byte) (InvoicePaid, error) { return i, err } +// InvoiceCredited is the payload of an event.InvoiceCredited envelope: a +// credit note was issued against an issued invoice. The credited amount and +// per-rate VAT split mirror the credited lines (line total × qty / line +// quantity, VAT proportionally — the same math as the order grain's +// InvoiceCreditedAmount). The ledger books the reversal voucher +// (1510 C / 304x D / 261x D per rate), idempotent per credit number. +type InvoiceCredited struct { + OrderID string `json:"order_id"` + InvoiceNumber string `json:"invoice_number"` // the original invoice + CreditNumber string `json:"credit_number"` // from the same gap-free series + CompanyID string `json:"company_id,omitempty"` + OcrReference string `json:"ocr_reference,omitempty"` + AmountIncVat int64 `json:"amount_inc_vat"` + VatBuckets []VatBucket `json:"vat_buckets,omitempty"` + CreditedAtMs int64 `json:"credited_at_ms"` +} + +func (c InvoiceCredited) Encode() ([]byte, error) { return json.Marshal(c) } + +func DecodeInvoiceCredited(raw []byte) (InvoiceCredited, error) { + var c InvoiceCredited + err := json.Unmarshal(raw, &c) + return c, err +} +