Files
mats 9495c654fa
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 5s
link user
2026-06-30 18:45:58 +02:00

265 lines
8.2 KiB
Go

package promotions
import (
"testing"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
"git.k6n.net/mats/platform/money"
)
func timeZero() time.Time { return time.Date(2024, 6, 10, 12, 0, 0, 0, time.UTC) }
// volymrabattRule mirrors the seeded data/promotions.json rule so the test
// exercises the same tier configuration shipped to the cart service.
func volymrabattRule() PromotionRule {
return PromotionRule{
ID: "volymrabatt",
Name: "Volymrabatt",
Status: StatusActive,
Priority: 100,
Conditions: Conditions{
BaseCondition{ID: "min", Type: CondCartTotal, Operator: OpGreaterOrEqual, Value: cvNum(2000000)},
},
Actions: []Action{
{
ID: "tiers",
Type: ActionTieredDiscount,
Config: map[string]interface{}{
"tiers": []interface{}{
map[string]interface{}{"minTotal": 2000000.0, "maxTotal": 4000000.0, "percent": 5.0},
map[string]interface{}{"minTotal": 4000000.0, "maxTotal": 6000000.0, "percent": 9.0},
map[string]interface{}{"minTotal": 6000000.0, "maxTotal": 9000000.0, "percent": 14.0},
},
},
},
},
}
}
// cartWithTotal builds a grain whose single line item produces the requested
// gross (incl. 25% VAT) total in öre, then runs UpdateTotals.
func cartWithTotal(incVat int64) *cart.CartGrain {
g := cart.NewCartGrain(1, timeZero())
g.Items = []*cart.CartItem{
{Id: 1, Sku: "SKU", Quantity: 1, Price: *cart.NewPriceFromIncVat(incVat, 25)},
}
g.UpdateTotals()
return g
}
func TestVolymrabattTiers(t *testing.T) {
rules := []PromotionRule{volymrabattRule()}
cases := []struct {
name string
total int64
wantDiscount money.Cents
wantNet money.Cents
}{
{"below threshold (19 999 kr)", 1999900, 0, 1999900},
{"low tier 5% (20 000 kr)", 2000000, 100000, 1900000},
{"low tier boundary just under 40 000", 3999900, 199995, 3799905},
{"mid tier 9% at 40 000 boundary", 4000000, 360000, 3640000},
{"mid tier 9% (50 000 kr)", 5000000, 450000, 4550000},
{"high tier 14% at 60 000 boundary", 6000000, 840000, 5160000},
{"high tier 14% (90 000 kr)", 9000000, 1260000, 7740000},
{"above top tier (100 000 kr) -> no discount", 10000000, 0, 10000000},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
svc := NewPromotionService(nil)
g := cartWithTotal(c.total)
results, _ := svc.EvaluateAll(rules, NewContextFromCart(g, WithNow(timeZero())))
ctx := NewContextFromCart(g, WithNow(timeZero()))
svc.ApplyResults(g, results, ctx)
if got := g.TotalDiscount.IncVat; got != c.wantDiscount {
t.Errorf("discount = %d, want %d", got, c.wantDiscount)
}
if got := g.TotalPrice.IncVat; got != c.wantNet {
t.Errorf("net total = %d, want %d", got, c.wantNet)
}
})
}
}
// findApplied returns the first appliedPromotions entry for ruleID.
func findApplied(g *cart.CartGrain, ruleID string) (cart.AppliedPromotion, bool) {
for _, ap := range g.AppliedPromotions {
if ap.PromotionId == ruleID {
return ap, true
}
}
return cart.AppliedPromotion{}, false
}
// TestApplyResultsRecordsEffects covers the applied/pending breakdown the result
// now exposes: an applied effect attributed to its rule, a pending nudge with
// progress when below the threshold, and progress-toward-the-next-tier on an
// already-applied tiered discount.
func TestApplyResultsRecordsEffects(t *testing.T) {
rules := []PromotionRule{volymrabattRule()}
apply := func(total int64) *cart.CartGrain {
svc := NewPromotionService(nil)
g := cartWithTotal(total)
results, _ := svc.EvaluateAll(rules, NewContextFromCart(g, WithNow(timeZero())))
svc.ApplyResults(g, results, NewContextFromCart(g, WithNow(timeZero())))
return g
}
t.Run("applied effect records discount + next-tier progress", func(t *testing.T) {
g := apply(5000000) // qualifies at the 9% tier
ap, ok := findApplied(g, "volymrabatt")
if !ok {
t.Fatal("no volymrabatt entry recorded")
}
if ap.Pending {
t.Error("applied effect should not be pending")
}
if ap.Discount == nil || ap.Discount.IncVat != 450000 {
t.Errorf("discount = %v, want IncVat 450000", ap.Discount)
}
// 50 000 kr sits in the 9% tier; the next tier (14%) starts at 60 000 kr.
if got := ap.Progress["remaining"]; got != int64(1000000) {
t.Errorf("progress remaining = %v, want 1000000", got)
}
if got := ap.Progress["nextPercent"]; got != 14.0 {
t.Errorf("progress nextPercent = %v, want 14", got)
}
})
t.Run("pending nudge below threshold", func(t *testing.T) {
g := apply(1500000) // below the 20 000 kr entry tier
if g.TotalDiscount.IncVat != 0 {
t.Errorf("nothing should be applied, discount = %d", g.TotalDiscount.IncVat)
}
ap, ok := findApplied(g, "volymrabatt")
if !ok {
t.Fatal("expected a pending volymrabatt nudge")
}
if !ap.Pending || ap.Discount != nil {
t.Errorf("expected pending with no discount, got pending=%v discount=%v", ap.Pending, ap.Discount)
}
if got := ap.Progress["remaining"]; got != int64(500000) {
t.Errorf("progress remaining = %v, want 500000", got)
}
})
}
func TestBuyXGetYEffect(t *testing.T) {
rule := PromotionRule{
ID: "buy2get1",
Name: "Buy 2 Get 1 Free",
Status: StatusActive,
Actions: []Action{
{
ID: "buy2get1-action",
Type: ActionBuyXGetY,
Config: map[string]interface{}{
"buy": 2.0,
"get": 1.0,
"discount": 100.0,
},
},
},
}
svc := NewPromotionService(nil)
// Cart with 3 items of price 1000 each. Buy 2 get 1 free -> 1 is free -> 1000 discount.
g := cart.NewCartGrain(1, timeZero())
g.Items = []*cart.CartItem{
{Id: 1, Sku: "SKU1", Quantity: 3, Price: *cart.NewPriceFromIncVat(1000, 25)},
}
g.UpdateTotals()
results, _ := svc.EvaluateAll([]PromotionRule{rule}, NewContextFromCart(g, WithNow(timeZero())))
svc.ApplyResults(g, results, NewContextFromCart(g, WithNow(timeZero())))
if got := g.TotalDiscount.IncVat; got != 1000 {
t.Errorf("discount = %d, want 1000", got)
}
if got := g.TotalPrice.IncVat; got != 2000 {
t.Errorf("total price = %d, want 2000", got)
}
if len(g.AppliedPromotions) != 1 {
t.Fatalf("expected 1 applied promotion, got %d", len(g.AppliedPromotions))
}
itemIds := g.AppliedPromotions[0].ItemIds
if len(itemIds) != 1 || itemIds[0] != 1 {
t.Errorf("expected affected itemIds [1], got %v", itemIds)
}
}
func TestBundleDiscountEffect(t *testing.T) {
rule := PromotionRule{
ID: "bundle-deal",
Name: "Bundle Deal",
Status: StatusActive,
Actions: []Action{
{
ID: "bundle-action",
Type: ActionBundleDiscount,
BundleConfig: &BundleConfig{
Containers: []BundleContainer{
{
ID: "shoes",
Name: "Shoes",
Quantity: 1,
SelectionType: "any",
QualifyingRules: BundleQualifyingRules{
Type: "category",
Value: "shoes",
},
},
{
ID: "socks",
Name: "Socks",
Quantity: 2,
SelectionType: "any",
QualifyingRules: BundleQualifyingRules{
Type: "category",
Value: "socks",
},
},
},
Pricing: BundlePricing{
Type: "fixed_price",
Value: 40.0, // 40 kr -> 4000 ore
},
RequireAllContainers: true,
},
},
},
}
svc := NewPromotionService(nil)
// Cart with 1 shoe (5000) and 2 socks (1000 each). Total original = 7000. Bundle fixed price is 4000. Discount = 3000.
g := cart.NewCartGrain(1, timeZero())
g.Items = []*cart.CartItem{
{Id: 1, Sku: "shoe-1", Quantity: 1, Price: *cart.NewPriceFromIncVat(5000, 25), Meta: &cart.ItemMeta{Category: "shoes"}},
{Id: 2, Sku: "socks-1", Quantity: 2, Price: *cart.NewPriceFromIncVat(1000, 25), Meta: &cart.ItemMeta{Category: "socks"}},
}
g.UpdateTotals()
results, _ := svc.EvaluateAll([]PromotionRule{rule}, NewContextFromCart(g, WithNow(timeZero())))
svc.ApplyResults(g, results, NewContextFromCart(g, WithNow(timeZero())))
if got := g.TotalDiscount.IncVat; got != 3000 {
t.Errorf("discount = %d, want 3000", got)
}
if got := g.TotalPrice.IncVat; got != 4000 {
t.Errorf("total price = %d, want 4000", got)
}
if len(g.AppliedPromotions) != 1 {
t.Fatalf("expected 1 applied promotion, got %d", len(g.AppliedPromotions))
}
itemIds := g.AppliedPromotions[0].ItemIds
if len(itemIds) != 2 || itemIds[0] != 1 || itemIds[1] != 2 {
t.Errorf("expected affected itemIds [1, 2], got %v", itemIds)
}
}