better results
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s

This commit is contained in:
2026-07-08 00:55:05 +02:00
parent 781c1bd171
commit e20793a6b3
4 changed files with 545 additions and 91 deletions
+122 -11
View File
@@ -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