Files
platform/order/order.go
T
mats d74c8a0592 feat(ledger): deploy the bookkeeping service — compose/k8s, ingress, off-site backup volume, submodule bumps
- 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 <noreply@codebuff.com>
2026-08-15 20:54:23 +02:00

140 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package order is the platform's shared order seam: the exported wire shape of
// the `order.created` domain event (B4.1). It is the contract between the order
// service (producer) and every reactor — inventory commit, fulfillment
// allocation, loyalty, ERP sync — none of which the order service knows about.
//
// Like platform/catalog and platform/inventory, it holds only the wire value
// types + encode/decode, and imports only the stdlib.
//
// `order.created` is a FACT, not a request: by the time it is emitted the order
// is placed and paid. Reactors must be idempotent (the transport is
// at-least-once) and must not assume they can refuse it — the sale already
// happened. Oversell on commit is reconciled downstream (backorder), not
// prevented here.
package order
import "encoding/json"
// Line is one ordered line item, reduced to what reactors need: the SKU, how
// many units sold, the inventory location to commit against, and whether the
// item is drop-ship (fulfilled by the supplier, never decremented from local
// stock). Pricing/tax live in the order grain, not on this seam.
type Line struct {
SKU string `json:"sku"`
Quantity int `json:"quantity"`
// Location is the inventory location / store id to decrement. Empty means
// "use the order's Country" (central stock) — see Created.Country.
Location string `json:"location,omitempty"`
// DropShip marks items fulfilled directly by the supplier. The inventory
// reactor must skip the stock decrement for these lines.
DropShip bool `json:"drop_ship,omitempty"`
}
// Created is the payload of an event.OrderCreated envelope.
//
// Country is the default inventory location for the commit (central stock). A
// Line may override it with its own Location (store id); when a line's Location
// is empty the commit falls back to Country. See docs/inventory-shape.md.
type Created struct {
OrderID string `json:"order_id"`
OrderReference string `json:"order_reference,omitempty"`
CartID string `json:"cart_id,omitempty"`
Country string `json:"country"`
Currency string `json:"currency,omitempty"`
CustomerEmail string `json:"customer_email,omitempty"`
// CustomerID is the customer profile ID for identity linking (B0.4). Reactors
// (e.g. profile service) use this to link the order to the customer profile.
CustomerID string `json:"customer_id,omitempty"`
Lines []Line `json:"lines"`
PlacedAtMs int64 `json:"placed_at_ms,omitempty"`
}
// Encode serialises the payload for event.Event.Payload.
func (c Created) Encode() ([]byte, error) { return json.Marshal(c) }
// Decode parses an order.created payload — the consumer side of Encode.
func Decode(raw []byte) (Created, error) {
var c Created
err := json.Unmarshal(raw, &c)
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"`
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) }
func DecodeInvoiceIssued(raw []byte) (InvoiceIssued, error) {
var i InvoiceIssued
err := json.Unmarshal(raw, &i)
return i, err
}
// InvoicePaid is the payload of an event.InvoicePaid envelope.
type InvoicePaid struct {
OrderID string `json:"order_id"`
InvoiceNumber string `json:"invoice_number"`
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) }
func DecodeInvoicePaid(raw []byte) (InvoicePaid, error) {
var i InvoicePaid
err := json.Unmarshal(raw, &i)
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
}