Files
go-cart-actor/pkg/cart/evaluated_item.go
T
mats 9f1eca07e9
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
parent item mutations
2026-07-08 11:39:42 +02:00

96 lines
4.1 KiB
Go

package cart
// EvaluatedItem is one line in the per-line promotion breakdown exposed
// alongside a cart. 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.
//
// Lives in pkg/cart rather than pkg/promotions so CartGrain can carry the
// breakdown as a derived field (populated by the canonical promotion
// pipeline that already manages TotalDiscount/AppliedPromotions), and so
// every consumer of the grain — UCP cart response, legacy /cart HTTP
// handler, cart-mcp, AMQP mutation feed — naturally sees the same shape
// without ad-hoc wrappers at each call site. JSON tags are the canonical
// EvaluatedItem shape that /promotions/evaluate-with-cart has returned
// since the breakdown feature was introduced.
//
// Distributing the total cart discount down to the line level lets the
// storefront render "Item X: 100 kr → 80 kr" without re-doing the math
// on the client and lets verifiers confirm which promotion hit which
// line (per-line DiscountIncVat is the engine's authoritative answer).
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 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.
//
// Returns nil for a nil grain (callers that always pass a non-nil grain
// — the typical case — get a non-nil empty slice for an empty cart, which
// is the desired behavior for JSON omitempty at the consumer).
//
// Exported so the canonical promotion pipeline (pkg/promotions.EvaluateAndApply)
// and the manual /promotions/evaluate-with-cart preview handler can produce
// the same per-line shape.
func MapEvaluatedItems(g *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
}