// 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"` // Properties carries per-line domain attributes a reactor needs that don't // fit the fixed fields — e.g. booking_hold_id / booking_start / booking_end // for the scheduling service's order.created commit reactor (wire-safe // addition, same precedent as Created.CheckoutID). Optional; reactors must // tolerate absent properties on every line. Properties map[string]string `json:"properties,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"` // CheckoutID links the originating checkout grain (wire-safe addition). CheckoutID string `json:"checkout_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 } // OfferLine is one line of an accepted offer — the quotation row the ledger // contract carries. UnitPriceIncVat is öre; TaxRateBp is basis points // (2500 = 25 %). type OfferLine struct { Sku string `json:"sku,omitempty"` Name string `json:"name"` Quantity int `json:"quantity"` UnitPriceIncVat int64 `json:"unit_price_inc_vat,omitempty"` TaxRateBp int `json:"tax_rate_bp,omitempty"` } // OfferAccepted is the payload of an event.OfferAccepted envelope: a B2B // offer (offert) was accepted and becomes the agreement (avtal). The payload // carries the offer identity, the buyer company, and the offer's line items // so the consumer — the ledger's offer reactor — can create/activate the // contract exactly as offered. Lines may be empty defensively; reactors must // still record the acceptance as an agreement covering the referenced offer. type OfferAccepted struct { OrderID string `json:"order_id"` OfferNumber string `json:"offer_number"` CompanyID string `json:"company_id,omitempty"` CompanyName string `json:"company_name,omitempty"` OrgNumber string `json:"org_number,omitempty"` CustomerEmail string `json:"customer_email,omitempty"` ValidUntil string `json:"valid_until,omitempty"` AcceptedAtMs int64 `json:"accepted_at_ms"` Notes string `json:"notes,omitempty"` Lines []OfferLine `json:"lines,omitempty"` } func (o OfferAccepted) Encode() ([]byte, error) { return json.Marshal(o) } func DecodeOfferAccepted(raw []byte) (OfferAccepted, error) { var o OfferAccepted err := json.Unmarshal(raw, &o) return o, 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 }