172 lines
7.8 KiB
Go
172 lines
7.8 KiB
Go
// Schema declarations and the DefaultSchema consensus shape.
|
|
//
|
|
// A Schema maps a producer's private source JSON to the platform-wide
|
|
// Projection. Field names point at keys in the source map; Value coercions
|
|
// happen in the Builder. A Schema is the unit of configuration per tenant;
|
|
// the Registry (registry.go) carries the per-tenant map.
|
|
package catalog
|
|
|
|
// UpdatedAtSource controls where the Projection's UpdatedAt comes from.
|
|
//
|
|
// - StampAtPublish — Builder sets UpdatedAt to time.Now().UnixMilli() at the
|
|
// moment Build() runs. RESERVED as a deliberate-fallback for sources
|
|
// that genuinely have no other version stamp. NOT a content-version
|
|
// address: a full snapshot rebuild re-stamps every projection and
|
|
// invalidates every consumer cache even when nothing changed. Use only
|
|
// when the producer side has NO source mtime / hash to set.
|
|
//
|
|
// - UseSourceField — the production-correct choice. Builder parses
|
|
// source[Schema.UpdatedAtField] as a Unix-millisecond integer (or
|
|
// decimal-fraction-of-Unix-second if the source uses seconds — Apply
|
|
// accepts that and converts). This is the content-version value;
|
|
// rebuilds produce identical UpdatedAt when content is identical.
|
|
//
|
|
// Critical contract: UpdatedAt is the CONTENT VERSION, not the publish
|
|
// timestamp. Consumers use it for snapshot + revalidate. If your schema
|
|
// sets UpdatedAtSource=StampAtPublish, every snapshot rebuild churns the
|
|
// version. Choose UseSourceField for any consumer-cache scenario.
|
|
type UpdatedAtSource int
|
|
|
|
const (
|
|
// StampAtPublish sets UpdatedAt at Build() time. DefaultSchema
|
|
// deliberately does NOT use this; it's reserved for callers who know
|
|
// they want publish-time semantics.
|
|
StampAtPublish UpdatedAtSource = iota
|
|
// UseSourceField reads the version from Schema.UpdatedAtField. This is
|
|
// the contract for content-versioned snapshots.
|
|
UseSourceField
|
|
)
|
|
|
|
// Schema is the declarative per-tenant mapping from a producer's source JSON
|
|
// to the published Projection. Zero value is NOT valid — always start from
|
|
// DefaultSchema() and override what differs, so unset-but-required fields
|
|
// surface immediately rather than silently emitting empty Projections.
|
|
//
|
|
// All "Field" strings refer to keys in the source map[string]any that the
|
|
// producer hands the Builder. Empty Field means "this Projection carrier is
|
|
// supplied by Fixed* / Default* / Build-time, not read from source".
|
|
type Schema struct {
|
|
// Tenant is the registry key this schema is registered under. Empty
|
|
// means "default" — see Registry.
|
|
Tenant string `json:"tenant,omitempty"`
|
|
|
|
// --- source field references (per carrier) ---
|
|
SKUField string `json:"sku_field"`
|
|
TitleField string `json:"title_field,omitempty"`
|
|
PriceField string `json:"price_field"`
|
|
CurrencyField string `json:"currency_field,omitempty"`
|
|
TaxClassField string `json:"tax_class_field,omitempty"`
|
|
InventoryTrackedF string `json:"inventory_tracked_field,omitempty"`
|
|
DropShipF string `json:"drop_ship_field,omitempty"`
|
|
CategoryField string `json:"category_field,omitempty"`
|
|
BrandField string `json:"brand_field,omitempty"`
|
|
ImageField string `json:"image_field,omitempty"`
|
|
|
|
// --- fixed / default values used when a source field is absent ---.
|
|
FixedCurrency string `json:"fixed_currency,omitempty"`
|
|
// DefaultTaxClass is what the Builder writes when the source has no
|
|
// TaxClass field (e.g. producer doesn't classify). Empty means "leave
|
|
// empty" — consumer's tax.Provider gets called with empty string.
|
|
DefaultTaxClass string `json:"default_tax_class,omitempty"`
|
|
|
|
// --- derived-value controls ---
|
|
// PriceScale: source value interpreted as MINOR units if ≤ 1 (pass
|
|
// through; floats round half-away-from-zero, ints pass through exact),
|
|
// MAJOR units if 100 (multiply by 100 to get øre), etc. Negative =
|
|
// rejected. The Builder rounds half-away-from-zero on fractional
|
|
// results so the integer Projection never silently truncates.
|
|
PriceScale int `json:"price_scale"`
|
|
// IDPrefix is prepended to the normalised ID. Use "sku:" for the
|
|
// consensus convention; an empty prefix means raw SKU.
|
|
IDPrefix string `json:"id_prefix,omitempty"`
|
|
// UpdatedAtSource — see type doc. DefaultSchema uses UseSourceField and
|
|
// pins UpdatedAtField="updated_at". Producers MUST supply an updated_at
|
|
// value (Unix-ms int); Apply returns ErrProjectionBuild (wrapped) when
|
|
// it's missing rather than silently stamp-now'ing and churning every
|
|
// rebuild. StampAtPublish is RESERVED for sources with no other stamp.
|
|
UpdatedAtSource UpdatedAtSource `json:"updated_at_source"`
|
|
// UpdatedAtField is the SOURCE-side key (when UpdatedAtSource ==
|
|
// UseSourceField) the Builder reads the version from.
|
|
UpdatedAtField string `json:"updated_at_field,omitempty"`
|
|
// TrueValues is the set of source values that map to InventoryTracked
|
|
// / DropShip = true. Comparison is lower-cased. Defaults to
|
|
// DefaultTrueValues below if left nil — never add "false" / "0" etc,
|
|
// the Builder treats anything not in TrueValues as false.
|
|
TrueValues []string `json:"true_values,omitempty"`
|
|
}
|
|
|
|
// DefaultSchema is the consensus schema: SKU under "sku", price under
|
|
// "price_inc_vat" already in øre, currency from source "currency" or empty,
|
|
// tax_class from "tax_class", inventory_tracked / drop_ship from
|
|
// "inventory_tracked" / "drop_ship" bool fields. updated_at MUST come from
|
|
// the source under the field name "updated_at" (Unix-ms) — Apply rejects
|
|
// missing values loudly rather than silent stamp-now churn.
|
|
//
|
|
// Use this when a tenant has no override file; it is the safe default for
|
|
// producers that already emit platform-shaped JSON.
|
|
func DefaultSchema() Schema {
|
|
return Schema{
|
|
Tenant: "default",
|
|
// source-side field names
|
|
SKUField: "sku",
|
|
TitleField: "title",
|
|
PriceField: "price_inc_vat",
|
|
CurrencyField: "currency",
|
|
TaxClassField: "tax_class",
|
|
InventoryTrackedF: "inventory_tracked",
|
|
DropShipF: "drop_ship",
|
|
CategoryField: "category",
|
|
BrandField: "brand",
|
|
ImageField: "image",
|
|
UpdatedAtField: "updated_at",
|
|
|
|
FixedCurrency: "", // read from source if present
|
|
DefaultTaxClass: "standard",
|
|
|
|
PriceScale: 1, // price_inc_vat already in minor units
|
|
IDPrefix: "sku:",
|
|
UpdatedAtSource: UseSourceField, // change to StampAtPublish only with intent
|
|
TrueValues: DefaultTrueValues(),
|
|
}
|
|
}
|
|
|
|
// BygglagretSchema is an explicit preset for the Swedish building-supplies
|
|
// tenant that ships inc-VAT float prices in a "PriceInclVAT" string field.
|
|
//
|
|
// CRITICAL: TaxClassField (VATClass) is a STRING CLASS IDENTIFIER, not a raw
|
|
// percent. The producer MUST pre-classify numeric VAT codes ("25" →
|
|
// "standard", "12" → "reduced-12", "6" → "reduced-6") before publishing.
|
|
// The wire contract is class identifiers; platform/tax resolves them to
|
|
// numeric rates out of band. Shipping "25" as the class would break that
|
|
// resolution path on the consumer side.
|
|
func BygglagretSchema() Schema {
|
|
return Schema{
|
|
Tenant: "bygglagret",
|
|
SKUField: "ArticleNumber",
|
|
TitleField: "Name",
|
|
PriceField: "PriceInclVAT",
|
|
CurrencyField: "", // fixed below
|
|
TaxClassField: "VATClass",
|
|
InventoryTrackedF: "StockTracked",
|
|
DropShipF: "DropshipFlag",
|
|
CategoryField: "CategoryPath",
|
|
BrandField: "Brand",
|
|
ImageField: "PrimaryImage",
|
|
UpdatedAtField: "LastModifiedMs",
|
|
|
|
FixedCurrency: "SEK",
|
|
DefaultTaxClass: "standard",
|
|
|
|
PriceScale: 100, // PriceInclVAT is in SEK; multiply for øre
|
|
IDPrefix: "sku:",
|
|
UpdatedAtSource: UseSourceField,
|
|
TrueValues: DefaultTrueValues(),
|
|
}
|
|
}
|
|
|
|
// DefaultTrueValues is the consensus truthy-string set for bool-typed
|
|
// source fields. Lower-cased comparison in Builder.
|
|
func DefaultTrueValues() []string {
|
|
return []string{"true", "yes", "1", "on"}
|
|
}
|