Files
go-cart-actor/pkg/promotions/evaluate.go
T
mats 2443fd8b49
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled
promotions
2026-06-18 11:32:19 +02:00

120 lines
4.0 KiB
Go

package promotions
import (
"time"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
)
// ----------------------------
// Stateless evaluation
// ----------------------------
//
// Evaluate runs the promotion engine against a caller-supplied context instead
// of a real cart, so a storefront can ask "what would these items get?" without
// 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.
// 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
// does the best it can with whatever the caller provides.
type EvalItem struct {
Sku string `json:"sku,omitempty"`
Category string `json:"category,omitempty"`
Quantity uint16 `json:"qty,omitempty"`
PriceIncVat int64 `json:"priceIncVat,omitempty"`
VatRate float32 `json:"vatRate,omitempty"`
}
// EvaluateRequest is a stateless promotion-evaluation context. Every field is
// optional. Items drive the cart total and the item-scoped conditions; when no
// items are given, CartTotalIncVat synthesises a single line so total-only rules
// (volume discounts, free-shipping thresholds) can still be previewed.
type EvaluateRequest struct {
Items []EvalItem `json:"items,omitempty"`
CartTotalIncVat int64 `json:"cartTotalIncVat,omitempty"`
CustomerSegment string `json:"customerSegment,omitempty"`
CustomerLifetimeValue float64 `json:"customerLifetimeValue,omitempty"`
OrderCount int `json:"orderCount,omitempty"`
Now *time.Time `json:"now,omitempty"`
}
// EvaluateResponse is the outcome of a stateless evaluation: the totals after
// promotions and the per-promotion breakdown (applied discounts plus pending
// "spend X more for ..." nudges).
type EvaluateResponse struct {
TotalPrice *cart.Price `json:"totalPrice"`
TotalDiscount *cart.Price `json:"totalDiscount"`
AppliedPromotions []cart.AppliedPromotion `json:"appliedPromotions,omitempty"`
}
// Evaluate runs rules against a synthetic cart built from req and returns the
// resulting totals and effects without touching any real cart state.
func (s *PromotionService) Evaluate(rules []PromotionRule, req EvaluateRequest) EvaluateResponse {
g := req.toCart()
g.UpdateTotals()
ctx := NewContextFromCart(g, req.contextOptions()...)
results, _ := s.EvaluateAll(rules, ctx)
s.ApplyResults(g, results, ctx)
return EvaluateResponse{
TotalPrice: g.TotalPrice,
TotalDiscount: g.TotalDiscount,
AppliedPromotions: g.AppliedPromotions,
}
}
// toCart materialises the request into a throwaway CartGrain.
func (req EvaluateRequest) toCart() *cart.CartGrain {
now := time.Now()
if req.Now != nil {
now = *req.Now
}
g := cart.NewCartGrain(0, now)
for _, it := range req.Items {
qty := it.Quantity
if qty == 0 {
qty = 1
}
vat := it.VatRate
if vat == 0 {
vat = 25
}
item := &cart.CartItem{
Sku: it.Sku,
Quantity: qty,
Price: *cart.NewPriceFromIncVat(it.PriceIncVat, vat),
}
if it.Category != "" {
item.Meta = &cart.ItemMeta{Category: it.Category}
}
g.Items = append(g.Items, item)
}
if len(g.Items) == 0 && req.CartTotalIncVat > 0 {
g.Items = append(g.Items, &cart.CartItem{
Sku: "synthetic",
Quantity: 1,
Price: *cart.NewPriceFromIncVat(req.CartTotalIncVat, 25),
})
}
return g
}
// contextOptions maps the optional customer/time fields onto context options.
func (req EvaluateRequest) contextOptions() []ContextOption {
var opts []ContextOption
if req.Now != nil {
opts = append(opts, WithNow(*req.Now))
}
if req.CustomerSegment != "" {
opts = append(opts, WithCustomerSegment(req.CustomerSegment))
}
if req.CustomerLifetimeValue != 0 {
opts = append(opts, WithCustomerLifetimeValue(req.CustomerLifetimeValue))
}
if req.OrderCount != 0 {
opts = append(opts, WithOrderCount(req.OrderCount))
}
return opts
}