|
|
|
@@ -31,7 +31,7 @@ type Effect interface {
|
|
|
|
|
// Apply mutates the cart for a qualifying action and returns the discount it
|
|
|
|
|
// took (nil for non-monetary effects such as free shipping) plus whether
|
|
|
|
|
// anything took effect worth recording.
|
|
|
|
|
Apply(g *cart.CartGrain, rule PromotionRule, a Action) (discount *cart.Price, applied bool)
|
|
|
|
|
Apply(g *cart.CartGrain, rule PromotionRule, a Action) (discount *cart.Price, itemIds []uint32, applied bool)
|
|
|
|
|
// Progress reports how far the cart is from (further) unlocking this action,
|
|
|
|
|
// as an open key/value payload. qualified says whether the owning rule
|
|
|
|
|
// currently applies, letting the effect distinguish "remaining to unlock"
|
|
|
|
@@ -77,8 +77,9 @@ func (s *PromotionService) ApplyResults(g *cart.CartGrain, results []EvaluationR
|
|
|
|
|
}
|
|
|
|
|
recorded := false
|
|
|
|
|
if res.Applicable {
|
|
|
|
|
if discount, applied := eff.Apply(g, res.Rule, a); applied {
|
|
|
|
|
if discount, itemIds, applied := eff.Apply(g, res.Rule, a); applied {
|
|
|
|
|
entry.Discount = discount
|
|
|
|
|
entry.ItemIds = itemIds
|
|
|
|
|
recorded = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -111,9 +112,9 @@ type percentageDiscountEffect struct{}
|
|
|
|
|
|
|
|
|
|
func (percentageDiscountEffect) Type() ActionType { return ActionPercentageDiscount }
|
|
|
|
|
|
|
|
|
|
func (percentageDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
|
|
|
|
func (percentageDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
|
|
|
|
d := applyPercentageDiscount(g, percentFromValue(a.Value))
|
|
|
|
|
return d, d != nil
|
|
|
|
|
return d, nil, d != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (percentageDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
|
|
|
@@ -129,8 +130,8 @@ type freeShippingEffect struct{}
|
|
|
|
|
|
|
|
|
|
func (freeShippingEffect) Type() ActionType { return ActionFreeShipping }
|
|
|
|
|
|
|
|
|
|
func (freeShippingEffect) Apply(_ *cart.CartGrain, _ PromotionRule, _ Action) (*cart.Price, bool) {
|
|
|
|
|
return nil, true
|
|
|
|
|
func (freeShippingEffect) Apply(_ *cart.CartGrain, _ PromotionRule, _ Action) (*cart.Price, []uint32, bool) {
|
|
|
|
|
return nil, nil, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (freeShippingEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
|
|
|
@@ -148,9 +149,9 @@ type tieredDiscountEffect struct{}
|
|
|
|
|
|
|
|
|
|
func (tieredDiscountEffect) Type() ActionType { return ActionTieredDiscount }
|
|
|
|
|
|
|
|
|
|
func (tieredDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
|
|
|
|
func (tieredDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
|
|
|
|
d := applyTieredDiscount(g, a)
|
|
|
|
|
return d, d != nil
|
|
|
|
|
return d, nil, d != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (tieredDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
|
|
|
@@ -387,7 +388,7 @@ type unitItem struct {
|
|
|
|
|
price money.Cents
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*cart.Price, bool) {
|
|
|
|
|
func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
|
|
|
|
config := a.Config
|
|
|
|
|
buy := int(firstNum(config, "buy"))
|
|
|
|
|
if buy <= 0 {
|
|
|
|
@@ -416,7 +417,7 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|
|
|
|
n := len(units)
|
|
|
|
|
numFree := (n / (buy + get)) * get
|
|
|
|
|
if numFree <= 0 {
|
|
|
|
|
return nil, false
|
|
|
|
|
return nil, nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort units by price ascending so we discount the cheapest ones
|
|
|
|
@@ -431,13 +432,15 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
totalDiscount := cart.NewPrice()
|
|
|
|
|
affectedMap := make(map[uint32]bool)
|
|
|
|
|
for i := 0; i < numFree; i++ {
|
|
|
|
|
unitDiscount := scalePrice(&units[i].item.Price, discount)
|
|
|
|
|
totalDiscount.Add(*unitDiscount)
|
|
|
|
|
affectedMap[units[i].item.Id] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if totalDiscount.IncVat <= 0 {
|
|
|
|
|
return nil, false
|
|
|
|
|
return nil, nil, false
|
|
|
|
|
}
|
|
|
|
|
// Never discount below zero.
|
|
|
|
|
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
|
|
|
@@ -445,7 +448,14 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
|
|
|
|
}
|
|
|
|
|
g.TotalDiscount.Add(*totalDiscount)
|
|
|
|
|
g.TotalPrice.Subtract(*totalDiscount)
|
|
|
|
|
return totalDiscount, true
|
|
|
|
|
|
|
|
|
|
var itemIds []uint32
|
|
|
|
|
for id := range affectedMap {
|
|
|
|
|
itemIds = append(itemIds, id)
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(itemIds)
|
|
|
|
|
|
|
|
|
|
return totalDiscount, itemIds, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (buyXGetYEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
|
|
|
@@ -460,9 +470,9 @@ type bundleDiscountEffect struct{}
|
|
|
|
|
|
|
|
|
|
func (bundleDiscountEffect) Type() ActionType { return ActionBundleDiscount }
|
|
|
|
|
|
|
|
|
|
func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, bool) {
|
|
|
|
|
func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action) (*cart.Price, []uint32, bool) {
|
|
|
|
|
if a.BundleConfig == nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
return nil, nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1. Represent all cart items as individual units
|
|
|
|
@@ -545,10 +555,11 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(formedBundles) == 0 {
|
|
|
|
|
return nil, false
|
|
|
|
|
return nil, nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalDiscount := cart.NewPrice()
|
|
|
|
|
affectedMap := make(map[uint32]bool)
|
|
|
|
|
for _, bundle := range formedBundles {
|
|
|
|
|
// Calculate the original bundle price
|
|
|
|
|
bundlePrice := cart.NewPrice()
|
|
|
|
@@ -579,11 +590,14 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|
|
|
|
|
|
|
|
|
if bundleDiscount != nil && bundleDiscount.IncVat > 0 {
|
|
|
|
|
totalDiscount.Add(*bundleDiscount)
|
|
|
|
|
for _, item := range bundle {
|
|
|
|
|
affectedMap[item.Id] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if totalDiscount.IncVat <= 0 {
|
|
|
|
|
return nil, false
|
|
|
|
|
return nil, nil, false
|
|
|
|
|
}
|
|
|
|
|
// Never discount below zero.
|
|
|
|
|
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
|
|
|
@@ -591,7 +605,14 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
|
|
|
|
}
|
|
|
|
|
g.TotalDiscount.Add(*totalDiscount)
|
|
|
|
|
g.TotalPrice.Subtract(*totalDiscount)
|
|
|
|
|
return totalDiscount, true
|
|
|
|
|
|
|
|
|
|
var itemIds []uint32
|
|
|
|
|
for id := range affectedMap {
|
|
|
|
|
itemIds = append(itemIds, id)
|
|
|
|
|
}
|
|
|
|
|
slices.Sort(itemIds)
|
|
|
|
|
|
|
|
|
|
return totalDiscount, itemIds, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (bundleDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a Action, ctx *PromotionEvalContext, qualified bool) (map[string]any, bool) {
|
|
|
|
|