parent item mutations
This commit is contained in:
@@ -195,6 +195,16 @@ func cartGrainToResponse(id string, g *cart.CartGrain) CartResponse {
|
||||
resp.Status = string(*g.CheckoutStatus)
|
||||
}
|
||||
|
||||
// Per-line evaluation breakdown. The canonical promotion pipeline
|
||||
// (pkg/promotions.PromotionService.EvaluateAndApply) populates this
|
||||
// field on the grain itself after every successful mutation, so the
|
||||
// conversion just reads g.EvaluatedItems verbatim — no recompute at
|
||||
// response time. Same shape /promotions/evaluate-with-cart returns
|
||||
// (both reference cart.EvaluatedItem and cart.MapEvaluatedItems),
|
||||
// so a line verified via GET /ucp/v1/carts/{id} byte-matches the
|
||||
// preview from /promotions/evaluate-with-cart.
|
||||
resp.EvaluatedItems = g.EvaluatedItems
|
||||
|
||||
for _, it := range g.Items {
|
||||
if it == nil {
|
||||
continue
|
||||
|
||||
@@ -229,6 +229,106 @@ func TestCartResponse_Conversion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCartResponse_EvaluatedItems verifies that the per-line breakdown the
|
||||
// canonical promotion pipeline populates on CartGrain.EvaluatedItems flows
|
||||
// verbatim into the UCP CartResponse's EvaluatedItems field. Conversion is a
|
||||
// straight pass-through now — we hand-construct the grain's EvaluatedItems
|
||||
// field with the expected values to lock down the wire shape independent of
|
||||
// the math MapEvaluatedItems applies (which has its own tests in pkg/cart).
|
||||
func TestCartResponse_EvaluatedItems(t *testing.T) {
|
||||
id := mustParseID("ABCD")
|
||||
g := cart.NewCartGrain(id, time.UnixMilli(1000000))
|
||||
g.Currency = "SEK"
|
||||
g.Items = append(g.Items, &cart.CartItem{
|
||||
Sku: "promo-1",
|
||||
Quantity: 2,
|
||||
Price: *mustPrice(10000, 2500),
|
||||
TotalPrice: *mustPrice(20000, 5000),
|
||||
Tax: 2500,
|
||||
OrgPrice: mustPrice(12000, 3000),
|
||||
Discount: mustPrice(2000, 500),
|
||||
Meta: &cart.ItemMeta{Name: "Promo Item"},
|
||||
})
|
||||
g.TotalPrice = mustPrice(20000, 5000)
|
||||
|
||||
// In the live cart, pkg/promotions.PromotionService.EvaluateAndApply
|
||||
// populates g.EvaluatedItems via cart.MapEvaluatedItems after every
|
||||
// successful mutation. The conversion test only cares that whatever
|
||||
// the grain carries projects into resp.EvaluatedItems unchanged — so
|
||||
// the input is the canonical shape, not the math's output.
|
||||
g.EvaluatedItems = []cart.EvaluatedItem{{
|
||||
Sku: "promo-1",
|
||||
Name: "Promo Item",
|
||||
Quantity: 2,
|
||||
PriceIncVat: 10000,
|
||||
OrgPriceIncVat: 12000,
|
||||
DiscountIncVat: 2000,
|
||||
EffectivePriceIncVat: 9000,
|
||||
EffectiveTotalIncVat: 18000,
|
||||
}}
|
||||
|
||||
resp := cartGrainToResponse(g.Id.String(), g)
|
||||
if len(resp.EvaluatedItems) != 1 {
|
||||
t.Fatalf("expected 1 evaluated item, got %d", len(resp.EvaluatedItems))
|
||||
}
|
||||
ev := resp.EvaluatedItems[0]
|
||||
if ev.Sku != "promo-1" {
|
||||
t.Fatalf("expected sku 'promo-1', got %q", ev.Sku)
|
||||
}
|
||||
if ev.Name != "Promo Item" {
|
||||
t.Fatalf("expected name 'Promo Item', got %q", ev.Name)
|
||||
}
|
||||
if ev.Quantity != 2 {
|
||||
t.Fatalf("expected qty 2, got %d", ev.Quantity)
|
||||
}
|
||||
if ev.PriceIncVat != 10000 {
|
||||
t.Fatalf("expected priceIncVat 10000, got %d", ev.PriceIncVat)
|
||||
}
|
||||
if ev.OrgPriceIncVat != 12000 {
|
||||
t.Fatalf("expected orgPriceIncVat 12000, got %d", ev.OrgPriceIncVat)
|
||||
}
|
||||
if ev.DiscountIncVat != 2000 {
|
||||
t.Fatalf("expected discountIncVat 2000, got %d", ev.DiscountIncVat)
|
||||
}
|
||||
if ev.EffectiveTotalIncVat != 18000 {
|
||||
t.Fatalf("expected effectiveTotalIncVat 18000, got %d", ev.EffectiveTotalIncVat)
|
||||
}
|
||||
if ev.EffectivePriceIncVat != 9000 {
|
||||
t.Fatalf("expected effectivePriceIncVat 9000, got %d", ev.EffectivePriceIncVat)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCartResponse_EvaluatedItems_MapEquivalence confirms that
|
||||
// cart.MapEvaluatedItems produces the same shape the test's hand-built
|
||||
// []cart.EvaluatedItem entries contain, given matching inputs. This locks
|
||||
// the conversion path's output to the canonical pipeline's output — if
|
||||
// either side starts omitting or renaming a field, this fails.
|
||||
func TestCartResponse_EvaluatedItems_MapEquivalence(t *testing.T) {
|
||||
id := mustParseID("ABCD")
|
||||
g := cart.NewCartGrain(id, time.UnixMilli(1000000))
|
||||
g.Currency = "SEK"
|
||||
g.Items = append(g.Items, &cart.CartItem{
|
||||
Sku: "promo-1",
|
||||
Quantity: 2,
|
||||
Price: *mustPrice(10000, 2500),
|
||||
TotalPrice: *mustPrice(20000, 5000),
|
||||
Tax: 2500,
|
||||
OrgPrice: mustPrice(12000, 3000),
|
||||
Discount: mustPrice(2000, 500),
|
||||
Meta: &cart.ItemMeta{Name: "Promo Item"},
|
||||
})
|
||||
g.TotalPrice = mustPrice(20000, 5000)
|
||||
|
||||
canonical := cart.MapEvaluatedItems(g)
|
||||
if len(canonical) != 1 {
|
||||
t.Fatalf("expected 1 evaluated item from MapEvaluatedItems, got %d", len(canonical))
|
||||
}
|
||||
e := canonical[0]
|
||||
if e.DiscountIncVat != 2000 || e.EffectiveTotalIncVat != 18000 || e.EffectivePriceIncVat != 9000 {
|
||||
t.Fatalf("MapEvaluatedItems math off: %+v", e)
|
||||
}
|
||||
}
|
||||
|
||||
func mustPrice(incVat int64, totalVat int64) *cart.Price {
|
||||
return &cart.Price{
|
||||
IncVat: money.Cents(incVat),
|
||||
|
||||
+19
-4
@@ -121,11 +121,26 @@ type VoucherInput struct {
|
||||
}
|
||||
|
||||
// CartResponse is the UCP cart response body.
|
||||
//
|
||||
// EvaluatedItems mirrors the per-line breakdown the canonical promotion
|
||||
// pipeline (pkg/promotions.PromotionService.EvaluateAndApply) populates
|
||||
// on the grain after every successful mutation — see
|
||||
// pkg/cart/evaluated_item.go for the EvaluatedItem type and the rounding
|
||||
// rules. The shape and field order match the live cart grain's own
|
||||
// EvaluatedItems field and the response from /promotions/evaluate-with-cart
|
||||
// (both reference cart.EvaluatedItem and cart.MapEvaluatedItems), so a
|
||||
// line verified via GET /ucp/v1/carts/{id} byte-matches the preview.
|
||||
//
|
||||
// Kept parallel to Items rather than merged into CartItem to preserve
|
||||
// the UCP wire contract — existing UCP clients keep working unchanged
|
||||
// and the per-line breakdown can grow on the EvaluatedItem type without
|
||||
// churning the standard item shape.
|
||||
type CartResponse struct {
|
||||
Id string `json:"id"`
|
||||
Items []CartItem `json:"items,omitempty"`
|
||||
Totals Totals `json:"totals"`
|
||||
Status string `json:"status"`
|
||||
Id string `json:"id"`
|
||||
Items []CartItem `json:"items,omitempty"`
|
||||
EvaluatedItems []cart.EvaluatedItem `json:"evaluatedItems,omitempty"`
|
||||
Totals Totals `json:"totals"`
|
||||
Status string `json:"status"`
|
||||
AppliedPromotions []cart.AppliedPromotion `json:"appliedPromotions,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user