parent item mutations
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s

This commit is contained in:
2026-07-08 11:39:42 +02:00
parent e20793a6b3
commit 9f1eca07e9
14 changed files with 784 additions and 103 deletions
+69
View File
@@ -2,9 +2,11 @@ package cart
import (
"context"
"log"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart"
"git.k6n.net/mats/platform/inventory"
)
@@ -71,6 +73,73 @@ func (c *CartMutationContext) ReleaseItem(ctx context.Context, cartId CartId, sk
return c.reservationService.Release(ctx, inventory.SKU(sku), l, inventory.CartID(cartId.String()))
}
// CascadeChildQuantities scales every direct child's quantity
// proportionally when a parent's quantity is bumped (ChangeQuantity or
// the AddItem-merge path).
//
// - Ratio: newChildQty = oldChildQty × newParentQty / oldParentQty.
// - Floor: scale-down via integer division can yield 0 (e.g. parent
// 2 → 1, child qty 1 → (1×1)/2 = 0). We clamp to 1 so a scale-down
// never silently deletes an accessory. The mismatch is acceptable —
// shipping one extra child is better than dropping one.
// - Reservations: each child's reservation is released then re-acquired
// at the new quantity if UseReservations is true for that child AND a
// prior reservation existed. A failing reservation is logged, not
// raised, so a single bad child doesn't abort the whole cascade
// (matches the existing removal pattern in mutation_items.go).
// - Recursion: descends into grandchildren using the child's own
// before/after quantities as the next ratio's input, so deeply
// nested accessory trees (parent → child → grandchild) stay in
// ratio with the top-level change.
//
// Caller is responsible for locking any grain mutex and for re-running
// UpdateTotals after the cascade has settled.
func (c *CartMutationContext) CascadeChildQuantities(
ctx context.Context,
g *CartGrain,
parentLineId uint32,
oldParentQty uint16,
newParentQty uint16,
) {
if oldParentQty == 0 || oldParentQty == newParentQty {
return
}
for _, it := range g.Items {
if it == nil {
continue
}
if it.ParentId == nil || *it.ParentId != parentLineId {
continue
}
oldChildQty := it.Quantity
ratio := uint32(newParentQty) * uint32(oldChildQty) / uint32(oldParentQty)
newChildQty := uint16(ratio)
if newChildQty == 0 {
newChildQty = 1
}
if newChildQty == oldChildQty {
continue
}
if g.Type == cart_messages.CartType_REGULAR && c.UseReservations(it) && it.ReservationEndTime != nil {
if err := c.ReleaseItem(ctx, g.Id, it.Sku, it.StoreId); err != nil {
log.Printf("CascadeChildQuantities: failed to release %s: %v", it.Sku, err)
}
endTime, err := c.ReserveItem(ctx, g.Id, it.Sku, it.StoreId, newChildQty)
if err != nil {
log.Printf("CascadeChildQuantities: failed to reserve %s at qty %d: %v", it.Sku, newChildQty, err)
} else if endTime != nil {
it.ReservationEndTime = endTime
}
}
it.Quantity = newChildQty
// Descend so grandchildren track this child's ratio.
c.CascadeChildQuantities(ctx, g, it.Id, oldChildQty, newChildQty)
}
}
func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegistry {
reg := actor.NewMutationRegistry()