Files
platform/order/order.go
T
mats 7c2a34cc7a feat(shop-sales): ledger contracts + docs for consumer PSP sales tracking
platform/order + platform/event gain the sale.paid and refund.issued wire
contracts (per-rate VatBuckets, PSP provider, timestamps) that drive the
ledger's SHOP-/REFUND- vouchers (go-cart-actor 2b819bd). docs/status.md
records the shop-sales + PSP settlement stack (tickets 17 + 18); the
wayfinder ledger tracker gains the two closed tickets and their Decisions
entries. Bumps go-cart-actor + backoffice (PSP float stat).

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-08-17 20:01:58 +02:00

188 lines
7.8 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
}
// SalePaid is the payload of an event.SalePaid envelope: a consumer (PSP)
// order was captured. It is the accounting fact for shop sales — the ledger
// aggregates captured sales into the daily SHOP- voucher (clearing D /
// 304x C per rate / 261x C per rate). Invoice orders never emit it (they book
// via invoice.issued).
//
// PaymentMethod is the PSP provider name (e.g. "stripe", "klarna"), carried
// from the order's payment record. CapturedAtMs is the capture moment — the
// ledger keys the daily voucher on its UTC date (fakturametoden: capture date
// decides the VAT period).
type SalePaid struct {
OrderID string `json:"order_id"`
PaymentMethod string `json:"payment_method,omitempty"`
AmountIncVat int64 `json:"amount_inc_vat"`
VatBuckets []VatBucket `json:"vat_buckets,omitempty"`
CapturedAtMs int64 `json:"captured_at_ms"`
}
func (s SalePaid) Encode() ([]byte, error) { return json.Marshal(s) }
func DecodeSalePaid(raw []byte) (SalePaid, error) {
var s SalePaid
err := json.Unmarshal(raw, &s)
return s, err
}
// RefundIssued is the payload of an event.RefundIssued envelope: a refund was
// issued against a captured PSP order. The amount and per-rate VAT split are
// pro-rated from the order's captured sale (line-level return mapping is out
// of scope). The ledger books the reversal voucher (clearing C / 304x D /
// 261x D per rate), idempotent per RefundID (the provider refund reference,
// unique per refund). Invoice orders reverse via invoice.credited instead.
type RefundIssued struct {
OrderID string `json:"order_id"`
RefundID string `json:"refund_id"`
AmountIncVat int64 `json:"amount_inc_vat"`
VatBuckets []VatBucket `json:"vat_buckets,omitempty"`
RefundedAtMs int64 `json:"refunded_at_ms"`
}
func (r RefundIssued) Encode() ([]byte, error) { return json.Marshal(r) }
func DecodeRefundIssued(raw []byte) (RefundIssued, error) {
var r RefundIssued
err := json.Unmarshal(raw, &r)
return r, err
}