135 lines
6.2 KiB
Go
135 lines
6.2 KiB
Go
// Package catalog — see doc.go for the package summary.
|
||
//
|
||
// This file holds the published Projection struct: the wire shape that
|
||
// commerce modules receive. Every producer adapter (writeradmin, future CMS
|
||
// catalog adapter, ...) emits this exact shape; every consumer (cart,
|
||
// checkout, order, UCP add-to-cart) reads from this exact shape. Per-tenant
|
||
// field remapping happens in Schema + Builder (schema.go, builder.go).
|
||
//
|
||
// Mapping source JSON → Projection is per-tenant and lives in Schema.
|
||
// The consensus mapping for DefaultSchema() is:
|
||
//
|
||
// id ← IDPrefix + SKU (e.g. "sku:<SKU>")
|
||
// sku ← source[SKUField]
|
||
// title ← source[TitleField] (omitempty, optional)
|
||
// price_inc_vat ← coercive parse of source[PriceField] × PriceScale
|
||
// currency ← source[CurrencyField], or FixedCurrency
|
||
// tax_class ← source[TaxClassField] (string identifier)
|
||
// inventory_tracked ← bool coercion of source[InventoryTrackedField]
|
||
// drop_ship ← bool coercion of source[DropShipField]
|
||
// category ← source[CategoryField] (omitempty)
|
||
// brand ← source[BrandField] (omitempty)
|
||
// image ← source[ImageField] (omitempty)
|
||
// updated_at ← Unix-ms from source[UpdatedAtField] OR stamped at
|
||
// publish time when UpdatedAtSource == StampAtPublish
|
||
//
|
||
// SCALE (rate-bearing fields): any field that carries a tax rate uses the
|
||
// platform-wide basis-points-of-a-percent convention (rate × 100; 25% = 2500,
|
||
// 12.5% = 1250, 8.1% = 810). Never float. Producer-side fields stay in
|
||
// minor currency units (öre / cents) for amounts — already int64. TaxClass
|
||
// on the wire is a STRING identifier; numeric rate resolution is a platform/tax
|
||
// concern (out of package scope by design).
|
||
package catalog
|
||
|
||
import (
|
||
"encoding/json"
|
||
|
||
"git.k6n.net/mats/platform/money"
|
||
)
|
||
|
||
// Projection is the published, versioned catalog entry that commerce modules
|
||
// consume. Finder owns updating it; cart / checkout / order read it.
|
||
//
|
||
// Versioning: UpdatedAt is a monotonic Unix-millisecond timestamp stamped by
|
||
// the producer at the time the AMQP event is emitted (or read from a
|
||
// designated source field, see Schema.UpdatedAtSource). Consumers, when
|
||
// revalidating, treat a strictly greater UpdatedAt as the authoritative
|
||
// replacement and refresh the local copy.
|
||
//
|
||
// Field rationale:
|
||
//
|
||
// ID — opaque published identity; SKU alone is not a stable
|
||
// cross-module id (different scopes reuse it).
|
||
// SKU — domain identifier; the join key cart already uses.
|
||
// PriceIncVat — display + checkout projection; NOT transactional truth.
|
||
// System of record for the total is settled via the
|
||
// checkout/order payload — see platform-uniforming-plan.md.
|
||
// Currency — optional but recommended; absent ⇒ caller supplies.
|
||
// TaxClass — class identifier (string). Numeric rate resolution is
|
||
// a platform/tax concern (see platform/tax.Provider). This
|
||
// struct never carries a numeric rate; that keeps the
|
||
// rate-scale convention in one place and keeps the wire
|
||
// free of consumer-policy values.
|
||
// InventoryTracked / DropShip — fulfillment-routing flags. The C1 contract
|
||
// asks for these so the cart can pre-mark drop-ship
|
||
// items before checkout and inventory can opt-out of
|
||
// tracking for non-stock items.
|
||
// Category / Brand / Image — display projection only.
|
||
// UpdatedAt — version stamp for snapshot + revalidate.
|
||
type Projection struct {
|
||
ID string `json:"id"`
|
||
SKU string `json:"sku"`
|
||
Title string `json:"title,omitempty"`
|
||
PriceIncVat money.Cents `json:"price_inc_vat"`
|
||
Currency string `json:"currency,omitempty"`
|
||
// TaxClass uses a string identifier (see platform/tax class constants).
|
||
// Empty string means "unclassified"; the consumer's tax.Provider is what
|
||
// resolves the identifier to a numeric rate.
|
||
TaxClass string `json:"tax_class"`
|
||
InventoryTracked bool `json:"inventory_tracked"`
|
||
DropShip bool `json:"drop_ship"`
|
||
Category string `json:"category,omitempty"`
|
||
Brand string `json:"brand,omitempty"`
|
||
Image string `json:"image,omitempty"`
|
||
UpdatedAt int64 `json:"updated_at"`
|
||
}
|
||
|
||
// PayloadKey is the envelope key producers use when publishing a slice of
|
||
// projections over the bus as an `event.Event.Payload` body. Consumers parse
|
||
// this exact key — never a per-producer ad-hoc name — so the wire shape is
|
||
// stable.
|
||
const PayloadKey = "catalog_projection"
|
||
|
||
// Encode serialises a Projection to JSON suitable for the event.Payload.
|
||
// Kept as a package-level helper so producers and tests share the exact wire
|
||
// shape; any future wire renaming lands here, not in every caller.
|
||
func (p Projection) Encode() ([]byte, error) {
|
||
return json.Marshal(p)
|
||
}
|
||
|
||
// EncodeBatch serialises a slice to a JSON array.
|
||
func EncodeBatch(batch []Projection) ([]byte, error) {
|
||
return json.Marshal(batch)
|
||
}
|
||
|
||
// ProjectionUpdate is one entry in a CatalogProjectionPublished batch. An
|
||
// upsert carries a fully-built Projection. A delete sets Deleted=true; only SKU
|
||
// (and ID) are then meaningful and the consumer drops that SKU from its
|
||
// snapshot / cache. Modelling delete as a flag (not a nil payload) keeps a
|
||
// change-set's upserts and deletes together in one ordered event.
|
||
type ProjectionUpdate struct {
|
||
Projection
|
||
Deleted bool `json:"deleted,omitempty"`
|
||
}
|
||
|
||
// EncodeUpdates serialises a batch for the event.Event.Payload body.
|
||
func EncodeUpdates(batch []ProjectionUpdate) ([]byte, error) {
|
||
return json.Marshal(batch)
|
||
}
|
||
|
||
// DecodeUpdates parses a CatalogProjectionPublished payload — the consumer side
|
||
// of EncodeUpdates.
|
||
func DecodeUpdates(raw []byte) ([]ProjectionUpdate, error) {
|
||
var out []ProjectionUpdate
|
||
err := json.Unmarshal(raw, &out)
|
||
return out, err
|
||
}
|
||
|
||
// Decode parses a Projection from JSON. Symmetric with Encode so consumer
|
||
// tests can round-trip a fixture without depending on the Builder.
|
||
func Decode(raw []byte) (Projection, error) {
|
||
var p Projection
|
||
err := json.Unmarshal(raw, &p)
|
||
return p, err
|
||
}
|