ref for projection snapshots

This commit is contained in:
mats committed 2026-06-28 18:41:31 +02:00
1 parent 21bcb63440
commit 5a3ffcb6bc
4 files changed
+91 -5

No files matched your search

Executable
BIN
View File
Binary file not shown.
+58
View File
@@ -116,6 +116,7 @@ func Apply(s Schema, source map[string]any) (Projection, error) {
}
p := Projection{
ItemID: uint32Of(source, s.ItemIdField),
ID: id,
SKU: sku,
Title: stringOf(source, s.TitleField),
@@ -180,6 +181,63 @@ func stringOf(m map[string]any, key string) string {
return fmt.Sprintf("%v", v)
}
// uint32Of returns the value at key as a uint32. Best-effort coercion:
// json.Number / int / int64 / float (integral check) / truthy numeric
// string. Returns 0 for any missing / non-uint32-representable value —
// Projection.ItemID = 0 is the documented "no id" signal that the cart's
// overlay path uses to fall back to PRODUCT_BASE_URL.
func uint32Of(m map[string]any, key string) uint32 {
if key == "" {
return 0
}
v, ok := m[key]
if !ok || v == nil {
return 0
}
switch x := v.(type) {
case uint32:
return x
case uint64:
if x > 0xFFFFFFFF {
return 0 // overflow – treat as unset, downstream falls back
}
return uint32(x)
case int:
if x < 0 || x > 0xFFFFFFFF {
return 0
}
return uint32(x)
case int64:
if x < 0 || x > 0xFFFFFFFF {
return 0
}
return uint32(x)
case float64:
if x != float64(uint32(x)) || x < 0 || x > 0xFFFFFFFF {
return 0 // fractional or out-of-range
}
return uint32(x)
case json.Number:
s := x.String()
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0
}
return uint32(n)
case string:
s := strings.TrimSpace(x)
if s == "" {
return 0
}
n, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0
}
return uint32(n)
}
return 0
}
// boolOf returns the value at key coerced through truthy. nil/missing keys
// are false; strings lower-cased and matched against truthy; bools passed
// through; numerics "!= 0"; json.Number goes through Float64.
+21 -5
View File
@@ -9,6 +9,7 @@
// Mapping source JSON → Projection is per-tenant and lives in Schema.
// The consensus mapping for DefaultSchema() is:
//
// item_id ← int coercion of source[ItemIdField] (omitempty; 0 = unset)
// id ← IDPrefix + SKU (e.g. "sku:<SKU>")
// sku ← source[SKUField]
// title ← source[TitleField] (omitempty, optional)
@@ -48,8 +49,17 @@ import (
//
// Field rationale:
//
// ID — opaque published identity; SKU alone is not a stable
// cross-module id (different scopes reuse it).
// ItemID — opaque numeric catalog item id from the producer's
// source (e.g. product service `/api/get/{sku}` integer
// `id` field, or the bygglagret raw row `item_id`).
// Carried here as uint32 to mirror go-cart-actor's
// proto messages.AddItem.ItemId which dedupes cart lines
// on equality. Zero means "producer didn't publish" — a
// legitimate value when the source has no integer id
// (the cart fall-back path uses HTTP PRODUCT_BASE_URL
// to recover the id in that case).
// ID — opaque published string 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
@@ -66,10 +76,16 @@ import (
// tracking for non-stock items.
// Category / Brand / Image — display projection only.
// UpdatedAt — version stamp for snapshot + revalidate.
//
// WIRE-COMPAT: adding a field is forward+backward-safe. Old snapshot
// readers ignore unknown JSON keys; new readers populate the field from
// the JSON token. The on-disk snapshot binary header/record layout is
// unchanged \u2014 only the per-blob JSON payload gains a key.
type Projection struct {
ID string `json:"id"`
SKU string `json:"sku"`
Title string `json:"title,omitempty"`
ItemID uint32 `json:"item_id,omitempty"`
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).
+12
View File
@@ -61,6 +61,12 @@ type Schema struct {
CategoryField string `json:"category_field,omitempty"`
BrandField string `json:"brand_field,omitempty"`
ImageField string `json:"image_field,omitempty"`
// ItemIdField is the source-side key carrying the producer's catalog
// item id (uint32-equivalent). DefaultSchema points at "id" (matching
// the product service's /api/get/{sku} shape). Producers that omit
// an integer id field simply leave ItemIdField empty — consumers see
// Projection.ItemID=0 and fall back to their HTTP lookup.
ItemIdField string `json:"item_id_field,omitempty"`
// --- fixed / default values used when a source field is absent ---.
FixedCurrency string `json:"fixed_currency,omitempty"`
@@ -118,6 +124,8 @@ func DefaultSchema() Schema {
CategoryField: "category",
BrandField: "brand",
ImageField: "image",
// Standard product service carries the integer catalog id under "id".
ItemIdField: "id",
UpdatedAtField: "updated_at",
FixedCurrency: "", // read from source if present
@@ -139,6 +147,9 @@ func DefaultSchema() Schema {
// 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.
//
// ItemIdField: bygglagret rows carry the integer item id under "item_id"
// (matches the per-row log shape we see from the upstream product import).
func BygglagretSchema() Schema {
return Schema{
Tenant: "bygglagret",
@@ -152,6 +163,7 @@ func BygglagretSchema() Schema {
CategoryField: "CategoryPath",
BrandField: "Brand",
ImageField: "PrimaryImage",
ItemIdField: "item_id",
UpdatedAtField: "LastModifiedMs",
FixedCurrency: "SEK",