233 lines
10 KiB
Go
233 lines
10 KiB
Go
// 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
|
||
}
|