package main import ( messages "git.k6n.net/mats/go-cart-actor/proto/cart" "git.k6n.net/mats/platform/catalog" ) // taxClassToBp maps the canonical TaxClass string identifiers published on // catalog.Projection to basis-points-of-a-percent (rate × 100) — the single // platform-wide rate scale used by AddItem.Tax (int32 basis points), the // mutation registry, and the tax.Provider Compute formula. // // Values reflect the standard tariff names used by the Nordic configuration. // Per-country / per-tenant numeric resolution (e.g. Finland 25.5%, Ireland // 13.5%) is delegated to platform/tax and is a follow-up: this static map // gets the common case right (2500 / 1200 / 600 / 0) and lets the cache path // ship while the platform/tax lookup is wired into the cart pool server. var taxClassToBp = map[string]int{ "standard": 2500, "reduced": 1200, // common reduced rate (e.g. SE/NO food) "lowered": 600, // super-reduced "zero": 0, // zero-rated (export, healthcare, ...) "exempt": 0, // similarly untaxed, separate nominal class } // resolveTaxBp returns the basis-point rate for a TaxClass string, falling // back to platform "standard" (25%) when the class is unrecognised. The // default matches the grain's existing behaviour (AddItem falls back to 2500 // when m.Tax == 0 — see pkg/cart/mutation_add_item.go). func resolveTaxBp(class string) int { if bp, ok := taxClassToBp[class]; ok { return bp } return 2500 } // ApplyProjectionOverlay overlays the cache's authoritative catalog facts onto // an AddItem message that was just produced by a PRODUCT_BASE_URL HTTP fetch. // // Authoritative-from-cache fields (these WIN over the HTTP-fetched value when // both are present, because the bus is the live stream): // // Price (bus has post-class-overrides; product service has static) // Tax (TaxClass → bp via resolveTaxBp; only when TaxClass is set — // an empty TaxClass means the producer didn't classify, // so we leave the HTTP-fetched rate intact rather than // quietly swapping in a default 2500 and miscategorising) // Title (canonical, post-trim) // Image (canonical) // InventoryTracked (only flips to true; false is the zero value in proto // and may be unset on the wire, so we don't second-guess) // DropShip (only flips to true; same reasoning) // // The HTTP-fetched values remain authoritative for fields the cache does not // carry: SellerId / SellerName (marketplace split), Stock (per docs/inventory- // shape.md stock is queried synchronously from inventory, not cached), // OrgPrice / Discount (not yet on the projection schema), the dynamic // ExtraJson product data (configurator options — width/height used by // accessory child-pricing), and the catalog uint32 ItemId (the product // service's integer id which messages.AddItem.ItemId dedupes on — Projection // only carries the opaque string ID, so a cache-only build path is blocked // and the HTTP fetch is preserved). // // Tombstoned SKUs (catalog.projection_published with deleted=true) → the // caller checks idx.IsDeleted(sku) before this helper and short-circuits. func ApplyProjectionOverlay(msg *messages.AddItem, p catalog.Projection) { if msg == nil { return } // Price: positive bus values override HTTP. A zero PriceIncVat is treated // as "no value" rather than "free". if p.PriceIncVat.Int64() > 0 { msg.Price = p.PriceIncVat.Int64() } // Tax: only override when the bus has classified the SKU. An empty TaxClass // means the producer had nothing to say about VAT; the HTTP rate is more // trustworthy in that case than a guessed 2500. if p.TaxClass != "" { msg.Tax = int32(resolveTaxBp(p.TaxClass)) } // Display fields: only override if non-empty so the HTTP-fanned value // remains in place when the cache hasn't populated them yet. if p.Title != "" { msg.Name = p.Title } if p.Image != "" { msg.Image = p.Image } // Flags: only flip to true (positive boolean signals). A false is the // proto-zero value and not worth overriding. if p.InventoryTracked { msg.InventoryTracked = true } if p.DropShip { msg.DropShip = true } }