Files
go-cart-actor/pkg/cart/cart-mutation-helper.go
T
mats 9f1eca07e9
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
parent item mutations
2026-07-08 11:39:42 +02:00

166 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
)
type CartMutationContext struct {
reservationService inventory.ReservationPolicy
}
func NewCartMutationContext(reservationService inventory.ReservationPolicy) *CartMutationContext {
return &CartMutationContext{
reservationService: reservationService,
}
}
func (c *CartMutationContext) ReserveItem(ctx context.Context, cartId CartId, sku string, locationId *string, quantity uint16) (*time.Time, error) {
if quantity <= 0 || c.reservationService == nil {
return nil, nil
}
l := inventory.LocationID("se")
if locationId != nil {
l = inventory.LocationID(*locationId)
}
ttl := time.Minute * 15
endTime := time.Now().Add(ttl)
err := c.reservationService.Reserve(ctx, inventory.CartReserveRequest{
CartID: inventory.CartID(cartId.String()),
InventoryReference: &inventory.InventoryReference{
SKU: inventory.SKU(sku),
LocationID: l,
},
TTL: ttl,
Quantity: uint32(quantity),
})
if err != nil {
return nil, err
}
return &endTime, nil
}
func (c *CartMutationContext) UseReservations(item *CartItem) bool {
if item.ReservationEndTime != nil {
return true
}
if item.DropShip {
return false
}
if item.InventoryTracked {
return true
}
return false
}
func (c *CartMutationContext) ReleaseItem(ctx context.Context, cartId CartId, sku string, locationId *string) error {
if c.reservationService == nil {
return nil
}
l := inventory.LocationID("se")
if locationId != nil {
l = inventory.LocationID(*locationId)
}
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()
reg.RegisterMutations(
actor.NewMutation(context.AddItem),
actor.NewMutation(context.ChangeQuantity),
actor.NewMutation(context.RemoveItem),
actor.NewMutation(ClearCart),
actor.NewMutation(AddVoucher),
actor.NewMutation(RemoveVoucher),
actor.NewMutation(UpsertSubscriptionDetails),
actor.NewMutation(SetUserId),
actor.NewMutation(LineItemMarking),
actor.NewMutation(RemoveLineItemMarking),
actor.NewMutation(SetLineItemCustomFields),
actor.NewMutation(SubscriptionAdded),
actor.NewMutation(context.SetCartType),
actor.NewMutation(SetRecoveryContact),
// actor.NewMutation(SubscriptionRemoved),
)
return reg
}