parent item mutations
This commit is contained in:
+21
-79
@@ -17,6 +17,11 @@ import (
|
||||
// creating or mutating a cart. It builds a synthetic CartGrain from the request,
|
||||
// runs the same EvaluateAll + ApplyResults path the live cart uses, and returns
|
||||
// the resulting totals plus the applied/pending effect breakdown.
|
||||
//
|
||||
// The per-line breakdown (cart.EvaluatedItem) is computed via the canonical
|
||||
// cart.MapEvaluatedItems so the preview and post-add responses share shape
|
||||
// and rounding rules — see pkg/cart/evaluated_item.go for the type and the
|
||||
// map function.
|
||||
|
||||
// EvalItem is one — possibly partial — cart line for a stateless evaluation.
|
||||
// Missing fields fall back to sensible defaults (qty 1, 25% VAT) so the engine
|
||||
@@ -46,88 +51,15 @@ type EvaluateRequest struct {
|
||||
// promotions, the per-promotion breakdown (applied discounts plus pending
|
||||
// "spend X more for ..." nudges), and the per-line breakdown so the
|
||||
// storefront can render the real (post-discount) unit price for each item.
|
||||
//
|
||||
// Items is the same []cart.EvaluatedItem shape the live cart exposes on
|
||||
// CartGrain.EvaluatedItems — referenced from cart so the preview and the
|
||||
// live response match byte-for-byte on the wire.
|
||||
type EvaluateResponse struct {
|
||||
TotalPrice *cart.Price `json:"totalPrice"`
|
||||
TotalDiscount *cart.Price `json:"totalDiscount"`
|
||||
AppliedPromotions []cart.AppliedPromotion `json:"appliedPromotions,omitempty"`
|
||||
Items []EvaluatedItem `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// EvaluatedItem is one line in the EvaluateResponse.Items list. It mirrors
|
||||
// the line that was evaluated (sku, quantity, unit list price, optional
|
||||
// OrgPrice for the strikethrough) and adds the per-line discount the
|
||||
// engine applied (orgPrice-based + promotion-based, additive) plus the
|
||||
// resulting effective per-unit and per-line totals. Distributing the
|
||||
// total discount down to the line level lets the storefront render
|
||||
// "Item X: 100 kr → 80 kr" without re-doing the math on the client.
|
||||
type EvaluatedItem struct {
|
||||
Sku string `json:"sku"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Quantity uint16 `json:"qty"`
|
||||
PriceIncVat int64 `json:"priceIncVat"` // per-unit list price
|
||||
OrgPriceIncVat int64 `json:"orgPriceIncVat,omitempty"` // per-unit pre-discount list price (for strikethrough)
|
||||
DiscountIncVat int64 `json:"discountIncVat"` // per-line TOTAL discount (orgPrice + promotion), incVat in öre
|
||||
EffectivePriceIncVat int64 `json:"effectivePriceIncVat"` // per-unit, after discount, incVat in öre (rounded)
|
||||
EffectiveTotalIncVat int64 `json:"effectiveTotalIncVat"` // per-line, after discount, incVat in öre (exact)
|
||||
}
|
||||
|
||||
// MapEvaluatedItems walks the grain's items after ApplyResults and
|
||||
// produces the per-line EvaluatedItem list. The grain's item.Discount
|
||||
// carries the TOTAL per-line discount (orgPrice + promotion — additive,
|
||||
// set by UpdateTotals and the effects' per-line distribution). The
|
||||
// effective totals are derived as (price * qty - discount), clamped to
|
||||
// 0 (a promotion that over-discounted a line shows as free, not
|
||||
// negative). The effective per-unit price is the per-line total
|
||||
// divided by quantity, rounded to the nearest öre so the per-unit
|
||||
// display matches the per-line total when multiplied back.
|
||||
// Exported so the /promotions/evaluate-with-cart preview handler in
|
||||
// cmd/cart can produce the same per-line shape the stateless
|
||||
// Evaluate path produces.
|
||||
func MapEvaluatedItems(g *cart.CartGrain) []EvaluatedItem {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]EvaluatedItem, 0, len(g.Items))
|
||||
for _, it := range g.Items {
|
||||
if it == nil {
|
||||
continue
|
||||
}
|
||||
var name string
|
||||
if it.Meta != nil {
|
||||
name = it.Meta.Name
|
||||
}
|
||||
var orgPrice int64
|
||||
if it.OrgPrice != nil {
|
||||
orgPrice = it.OrgPrice.IncVat.Int64()
|
||||
}
|
||||
var discount int64
|
||||
if it.Discount != nil {
|
||||
discount = it.Discount.IncVat.Int64()
|
||||
}
|
||||
priceRow := it.Price.IncVat.Int64() * int64(it.Quantity)
|
||||
effTotal := priceRow - discount
|
||||
if effTotal < 0 {
|
||||
effTotal = 0
|
||||
}
|
||||
var effUnit int64
|
||||
if it.Quantity > 0 {
|
||||
// Nearest-öre rounding so per-unit * qty matches the
|
||||
// per-line total (within 1 öre either way). Half-up:
|
||||
// (effTotal + qty/2) / qty.
|
||||
effUnit = (effTotal + int64(it.Quantity)/2) / int64(it.Quantity)
|
||||
}
|
||||
out = append(out, EvaluatedItem{
|
||||
Sku: it.Sku,
|
||||
Name: name,
|
||||
Quantity: it.Quantity,
|
||||
PriceIncVat: it.Price.IncVat.Int64(),
|
||||
OrgPriceIncVat: orgPrice,
|
||||
DiscountIncVat: discount,
|
||||
EffectivePriceIncVat: effUnit,
|
||||
EffectiveTotalIncVat: effTotal,
|
||||
})
|
||||
}
|
||||
return out
|
||||
Items []cart.EvaluatedItem `json:"items,omitempty"`
|
||||
}
|
||||
|
||||
// Evaluate runs rules against a synthetic cart built from req and returns the
|
||||
@@ -139,7 +71,7 @@ func (s *PromotionService) Evaluate(rules []PromotionRule, req EvaluateRequest)
|
||||
TotalPrice: g.TotalPrice,
|
||||
TotalDiscount: g.TotalDiscount,
|
||||
AppliedPromotions: g.AppliedPromotions,
|
||||
Items: MapEvaluatedItems(g),
|
||||
Items: cart.MapEvaluatedItems(g),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +129,16 @@ func (s *PromotionService) EvaluateAndApply(rules []PromotionRule, g *cart.CartG
|
||||
results, _ = s.EvaluateAll(rules, ctx)
|
||||
}
|
||||
s.ApplyResults(g, results, ctx)
|
||||
// Populate the per-line breakdown now that ApplyResults is done —
|
||||
// every effect has distributed per-line Discounts onto the grain's
|
||||
// items, so MapEvaluatedItems reads consistent state. Sits on the
|
||||
// canonical pipeline so callers (live-cart processor, the cart-aware
|
||||
// preview handler, internal/admin mutations) never have to remember
|
||||
// to refresh it themselves; downstream readers (UCP cart response,
|
||||
// legacy /cart HTTP handler, cart-mcp, AMQP mutation feed) see a
|
||||
// grain that already carries the per-line breakdown on its own
|
||||
// EvaluatedItems field.
|
||||
g.EvaluatedItems = cart.MapEvaluatedItems(g)
|
||||
}
|
||||
|
||||
// markCouponsBypassed scans the evaluation results for any applicable
|
||||
|
||||
Reference in New Issue
Block a user