From e9e9700a0964f9f767fe251e9490d7501df54653 Mon Sep 17 00:00:00 2001 From: matst80 Date: Sun, 28 Jun 2026 18:47:02 +0200 Subject: [PATCH] more --- cmd/cart/pool-server.go | 28 +++++++--- cmd/cart/projection_overlay.go | 96 ++++++++++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/cmd/cart/pool-server.go b/cmd/cart/pool-server.go index ec12f4d..998157d 100644 --- a/cmd/cart/pool-server.go +++ b/cmd/cart/pool-server.go @@ -75,6 +75,7 @@ func (s *PoolServer) GetCartHandler(w http.ResponseWriter, r *http.Request, id c func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error { sku := r.PathValue("sku") + country := getCountryFromHost(r.Host) if s.idx != nil && s.idx.IsDeleted(sku) { // Bus-deleted SKU — skip the HTTP fetch and reject the add. Mirrors the // product-fetcher's "product service returned %d for sku %s" shape so @@ -86,13 +87,28 @@ func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request, http.Error(w, fmt.Sprintf("product service returned %d for sku %s (bus-deleted)", 404, sku), http.StatusNotFound) return nil } - msg, err := GetItemAddMessage(r.Context(), sku, 1, getCountryFromHost(r.Host), nil) - if err != nil { - return err - } + + // Cache-only fast path: AddSkuToCartHandler is a SINGLE top-level add + // (qty=1, no children). When the projection carries full authoritative + // fields, we skip PRODUCT_BASE_URL entirely and build the AddItem + // directly from the cache. This eliminates one HTTP round-trip per + // add-to-cart under steady-state bus-fed conditions. + var msg *messages.AddItem if s.idx != nil { - if p, ok := s.idx.Get(sku); ok { - ApplyProjectionOverlay(msg, p) + if p, ok := s.idx.Get(sku); ok && HasRequiredFields(p) { + msg = BuildAddItemFromCache(sku, 1, country, nil, p) + } + } + if msg == nil { + var err error + msg, err = GetItemAddMessage(r.Context(), sku, 1, country, nil) + if err != nil { + return err + } + if s.idx != nil { + if p, ok := s.idx.Get(sku); ok { + ApplyProjectionOverlay(msg, p) + } } } diff --git a/cmd/cart/projection_overlay.go b/cmd/cart/projection_overlay.go index b723ceb..f13aded 100644 --- a/cmd/cart/projection_overlay.go +++ b/cmd/cart/projection_overlay.go @@ -16,11 +16,11 @@ import ( // 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 + "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 @@ -34,6 +34,82 @@ func resolveTaxBp(class string) int { return 2500 } +// HasRequiredFields asserts the catalog Projection carries enough authoritative +// fields for the cart to build a complete AddItem WITHOUT an HTTP fallback to +// PRODUCT_BASE_URL. Required: +// +// PriceIncVat > 0 (treats zero as "unbus-fed", not "free"; positive-only +// matches every other cache-authoritative field) +// TaxClass != "" (string identifier; numeric rate resolved via +// resolveTaxBp at the same scale as the HTTP path) +// ItemID != 0 (uint32 dedup key; zero means the producer didn't +// publish one, so we don't claim we know the id) +// +// Display fields (Title, Image) and fulfillment flags (InventoryTracked, +// DropShip) are optional — when present they populate the line, when absent +// the proto-zero value is acceptable. +func HasRequiredFields(p catalog.Projection) bool { + return p.PriceIncVat.Int64() > 0 && + p.TaxClass != "" && + p.ItemID != 0 +} + +// BuildAddItemFromCache constructs a complete AddItem message directly from a +// cache hit that satisfies HasRequiredFields. The caller has already confirmed +// the projection is well-formed; we map every cache-visible carrier onto the +// proto fields the existing HTTP path would have constructed. +// +// Fields mapped: +// +// Sku ← input +// ItemId ← p.ItemID (uint32 dedup key) +// Quantity ← input +// Country ← input +// StoreId ← input +// Price ← p.PriceIncVat.Int64() +// Tax ← int32(resolveTaxBp(p.TaxClass)) +// Name ← p.Title (may be empty) +// Image ← p.Image (may be empty) +// InventoryTracked ← p.InventoryTracked +// DropShip ← p.DropShip +// +// Fields intentionally left blank (cache-only gap; the schema doesn't yet +// carry them and the audience for those fields is downstream of the cart line +// — they are NOT required for line dedup or pricing): +// +// SellerId / SellerName — marketplace split; not on Projection yet. +// Stock — queried synchronously from inventory at +// decision points (per docs/inventory-shape.md). +// OrgPrice / Discount — pre-discount price; a future Projection +// extension when promotion previews need it. +// ExtraJson — dynamic product data (configurator +// width/height etc.); a future schema extension +// for cache-only-skip-HTTP for groups with +// configurator children. +// +// IMPORTANT: this helper is for TOP-LEVEL lines only. Configurator children +// re-price based on parent dimensions (ToItemAddMessage's parent != nil +// path); since the Projection does not yet carry width/height, callers must +// keep the existing HTTP-fetch flow for groups with children. +// +// Tombstoned SKUs never reach this helper; callers check idx.IsDeleted(sku) +// first and short-circuit. +func BuildAddItemFromCache(sku string, qty int, country string, storeId *string, p catalog.Projection) *messages.AddItem { + return &messages.AddItem{ + Sku: sku, + ItemId: p.ItemID, + Quantity: int32(qty), + Country: country, + StoreId: storeId, + Price: p.PriceIncVat.Int64(), + Tax: int32(resolveTaxBp(p.TaxClass)), + Name: p.Title, + Image: p.Image, + InventoryTracked: p.InventoryTracked, + DropShip: p.DropShip, + } +} + // ApplyProjectionOverlay overlays the cache's authoritative catalog facts onto // an AddItem message that was just produced by a PRODUCT_BASE_URL HTTP fetch. // @@ -61,10 +137,12 @@ func resolveTaxBp(class string) int { // ExtraJson product data (configurator options — width/height used by // accessory child-pricing). // -// Now that ItemID round-trips from the bus, a future "cache-only" build path -// (skipping PRODUCT_BASE_URL entirely for known SKUs) is unblocked — the -// only fields left HTTP-only in that future path are Sellers, Stock, OrgPrice -// and configurator extras, none of which affect line dedup. +// SKUs that satisfy HasRequiredFields AND have no configurator children +// route through BuildAddItemFromCache instead — see the cache-only fast path +// in buildItemGroups / AddSkuToCartHandler. ApplyProjectionOverlay remains +// for the cold-grace case (unbus-fed cache entry) AND for groups with +// children (the parent's HTTP fetch is required for child dimension-driven +// pricing; Overlay is applied after the fetch). // // Tombstoned SKUs (catalog.projection_published with deleted=true) → the // caller checks idx.IsDeleted(sku) before this helper and short-circuits.