better results
This commit is contained in:
+122
-11
@@ -285,24 +285,53 @@ func applyTieredDiscount(g *cart.CartGrain, a Action) *cart.Price {
|
||||
return applyPercentageDiscount(g, pct)
|
||||
}
|
||||
|
||||
// applyPercentageDiscount removes pct% of the current cart total, preserving the
|
||||
// VAT-rate breakdown proportionally, recording it in TotalDiscount and returning
|
||||
// the discount applied (nil if nothing was taken off).
|
||||
// applyPercentageDiscount removes pct% of each line's row total, sums
|
||||
// the per-line amounts into the cart's TotalDiscount, and returns the
|
||||
// total (nil if nothing was taken off). Per-line first then sum
|
||||
// (instead of total first then split) avoids penny-level rounding
|
||||
// gaps where the per-line amounts no longer sum to the reported
|
||||
// total. The per-line amount is also added to each line's Discount
|
||||
// field on top of the OrgPrice-based discount that UpdateTotals
|
||||
// already set, so consumers (the /promotions/evaluate-with-cart
|
||||
// preview, the checkout payload mapper) can see the effective unit
|
||||
// price per item.
|
||||
func applyPercentageDiscount(g *cart.CartGrain, pct float64) *cart.Price {
|
||||
if pct <= 0 || g.TotalPrice.IncVat <= 0 {
|
||||
return nil
|
||||
}
|
||||
discount := scalePrice(g.TotalPrice, pct)
|
||||
if discount.IncVat <= 0 {
|
||||
totalDiscount := cart.NewPrice()
|
||||
for _, item := range g.Items {
|
||||
if item == nil || item.TotalPrice.IncVat <= 0 {
|
||||
continue
|
||||
}
|
||||
itemDisc := scalePrice(&item.TotalPrice, pct)
|
||||
if itemDisc.IncVat <= 0 {
|
||||
continue
|
||||
}
|
||||
// Clamp to this line's row total so a single item can't go
|
||||
// negative when its per-line discount (post-rounding) ends
|
||||
// up larger than its row. itemDisc is *cart.Price; assign
|
||||
// through the pointer to overwrite the rounded value with
|
||||
// the raw row total when needed.
|
||||
if itemDisc.IncVat > item.TotalPrice.IncVat {
|
||||
*itemDisc = item.TotalPrice
|
||||
}
|
||||
if item.Discount == nil {
|
||||
item.Discount = cart.NewPrice()
|
||||
}
|
||||
item.Discount.Add(*itemDisc)
|
||||
totalDiscount.Add(*itemDisc)
|
||||
}
|
||||
if totalDiscount.IncVat <= 0 {
|
||||
return nil
|
||||
}
|
||||
// Never discount below zero.
|
||||
if discount.IncVat > g.TotalPrice.IncVat {
|
||||
discount = g.TotalPrice
|
||||
// Safety net for any aggregate rounding overshoot.
|
||||
if totalDiscount.IncVat > g.TotalPrice.IncVat {
|
||||
totalDiscount = g.TotalPrice
|
||||
}
|
||||
g.TotalDiscount.Add(*discount)
|
||||
g.TotalPrice.Subtract(*discount)
|
||||
return discount
|
||||
g.TotalDiscount.Add(*totalDiscount)
|
||||
g.TotalPrice.Subtract(*totalDiscount)
|
||||
return totalDiscount
|
||||
}
|
||||
|
||||
// selectTier returns the discount percent for the band the total falls into.
|
||||
@@ -437,6 +466,17 @@ func (buyXGetYEffect) Apply(g *cart.CartGrain, rule PromotionRule, a Action) (*c
|
||||
unitDiscount := scalePrice(&units[i].item.Price, discount)
|
||||
totalDiscount.Add(*unitDiscount)
|
||||
affectedMap[units[i].item.Id] = true
|
||||
// Per-line distribution: add the per-unit discount to the
|
||||
// item's Discount field on top of the OrgPrice-based
|
||||
// discount that UpdateTotals already set. If the same item
|
||||
// is the free unit multiple times (e.g. qty 3 with
|
||||
// buy-2-get-1 and a second buy-2-get-1), Add is called
|
||||
// per free unit so the cumulative amount lands on the
|
||||
// right line.
|
||||
if units[i].item.Discount == nil {
|
||||
units[i].item.Discount = cart.NewPrice()
|
||||
}
|
||||
units[i].item.Discount.Add(*unitDiscount)
|
||||
}
|
||||
|
||||
if totalDiscount.IncVat <= 0 {
|
||||
@@ -590,6 +630,15 @@ func (bundleDiscountEffect) Apply(g *cart.CartGrain, _ PromotionRule, a Action)
|
||||
|
||||
if bundleDiscount != nil && bundleDiscount.IncVat > 0 {
|
||||
totalDiscount.Add(*bundleDiscount)
|
||||
// Per-line distribution: split the bundle discount
|
||||
// across the entries in `bundle` proportionally to
|
||||
// each entry's per-unit price (same item can appear
|
||||
// multiple times if it's in the bundle multiple
|
||||
// times). distributeBundleDiscount walks the entries
|
||||
// and adds the share to each item's Discount field,
|
||||
// with the last entry absorbing any rounding remainder
|
||||
// so the per-line sum matches bundleDiscount exactly.
|
||||
distributeBundleDiscount(bundle, bundleDiscount)
|
||||
for _, item := range bundle {
|
||||
affectedMap[item.Id] = true
|
||||
}
|
||||
@@ -623,6 +672,68 @@ func (bundleDiscountEffect) Progress(s *PromotionService, rule PromotionRule, a
|
||||
// Helpers
|
||||
// ----------------------------
|
||||
|
||||
// distributeBundleDiscount splits bundleDiscount across the entries
|
||||
// in bundleItems proportionally to each entry's per-unit price, and
|
||||
// adds the share to each item's Discount field. The same item may
|
||||
// appear multiple times in bundleItems (when it's in the bundle
|
||||
// multiple times); each entry gets its own share, and Add on the
|
||||
// item's Discount field is cumulative. The last entry absorbs any
|
||||
// rounding remainder on IncVat (and on each VAT rate) so the
|
||||
// per-line sum matches bundleDiscount exactly. No-op when
|
||||
// bundleDiscount is nil/zero or bundleItems is empty.
|
||||
func distributeBundleDiscount(bundleItems []*cart.CartItem, bundleDiscount *cart.Price) {
|
||||
if bundleDiscount == nil || bundleDiscount.IncVat <= 0 || len(bundleItems) == 0 {
|
||||
return
|
||||
}
|
||||
total := int64(0)
|
||||
prices := make([]int64, len(bundleItems))
|
||||
for i, item := range bundleItems {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
prices[i] = item.Price.IncVat.Int64()
|
||||
total += prices[i]
|
||||
}
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
var allocated int64
|
||||
for i, item := range bundleItems {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
var shareIncVat int64
|
||||
if i == len(bundleItems)-1 {
|
||||
shareIncVat = bundleDiscount.IncVat.Int64() - allocated
|
||||
} else {
|
||||
shareIncVat = bundleDiscount.IncVat.Int64() * prices[i] / total
|
||||
}
|
||||
if shareIncVat <= 0 {
|
||||
continue
|
||||
}
|
||||
sharePrice := &cart.Price{
|
||||
IncVat: money.Cents(shareIncVat),
|
||||
VatRates: make(map[int]money.Cents, len(bundleDiscount.VatRates)),
|
||||
}
|
||||
// Pro-rate each VAT rate by the same share fraction so the
|
||||
// VAT breakdown on the per-line discount matches the
|
||||
// proportional split. Guard against the (degenerate) case
|
||||
// where bundleDiscount.IncVat is somehow 0 here — the
|
||||
// outer guard at the top should prevent this, but be
|
||||
// safe.
|
||||
if bundleDiscount.IncVat.Int64() > 0 {
|
||||
for rate, amount := range bundleDiscount.VatRates {
|
||||
sharePrice.VatRates[rate] = money.Cents(amount.Int64() * shareIncVat / bundleDiscount.IncVat.Int64())
|
||||
}
|
||||
}
|
||||
if item.Discount == nil {
|
||||
item.Discount = cart.NewPrice()
|
||||
}
|
||||
item.Discount.Add(*sharePrice)
|
||||
allocated += shareIncVat
|
||||
}
|
||||
}
|
||||
|
||||
func eligibleItems(rule PromotionRule, g *cart.CartGrain) []*cart.CartItem {
|
||||
var categories []string
|
||||
var productIDs []string
|
||||
|
||||
Reference in New Issue
Block a user