more
This commit is contained in:
+17
-1
@@ -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 {
|
func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
||||||
sku := r.PathValue("sku")
|
sku := r.PathValue("sku")
|
||||||
|
country := getCountryFromHost(r.Host)
|
||||||
if s.idx != nil && s.idx.IsDeleted(sku) {
|
if s.idx != nil && s.idx.IsDeleted(sku) {
|
||||||
// Bus-deleted SKU — skip the HTTP fetch and reject the add. Mirrors the
|
// 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
|
// product-fetcher's "product service returned %d for sku %s" shape so
|
||||||
@@ -86,7 +87,21 @@ 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)
|
http.Error(w, fmt.Sprintf("product service returned %d for sku %s (bus-deleted)", 404, sku), http.StatusNotFound)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
msg, err := GetItemAddMessage(r.Context(), sku, 1, getCountryFromHost(r.Host), nil)
|
|
||||||
|
// 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 && 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -95,6 +110,7 @@ func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request,
|
|||||||
ApplyProjectionOverlay(msg, p)
|
ApplyProjectionOverlay(msg, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, err := s.ApplyLocal(r.Context(), id, msg)
|
data, err := s.ApplyLocal(r.Context(), id, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -34,6 +34,82 @@ func resolveTaxBp(class string) int {
|
|||||||
return 2500
|
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
|
// ApplyProjectionOverlay overlays the cache's authoritative catalog facts onto
|
||||||
// an AddItem message that was just produced by a PRODUCT_BASE_URL HTTP fetch.
|
// 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
|
// ExtraJson product data (configurator options — width/height used by
|
||||||
// accessory child-pricing).
|
// accessory child-pricing).
|
||||||
//
|
//
|
||||||
// Now that ItemID round-trips from the bus, a future "cache-only" build path
|
// SKUs that satisfy HasRequiredFields AND have no configurator children
|
||||||
// (skipping PRODUCT_BASE_URL entirely for known SKUs) is unblocked — the
|
// route through BuildAddItemFromCache instead — see the cache-only fast path
|
||||||
// only fields left HTTP-only in that future path are Sellers, Stock, OrgPrice
|
// in buildItemGroups / AddSkuToCartHandler. ApplyProjectionOverlay remains
|
||||||
// and configurator extras, none of which affect line dedup.
|
// 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
|
// Tombstoned SKUs (catalog.projection_published with deleted=true) → the
|
||||||
// caller checks idx.IsDeleted(sku) before this helper and short-circuits.
|
// caller checks idx.IsDeleted(sku) before this helper and short-circuits.
|
||||||
|
|||||||
Reference in New Issue
Block a user