Files
go-cart-actor/pkg/cart/mutation_change_quantity.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

91 lines
2.9 KiB
Go

package cart
import (
"context"
"fmt"
"log"
cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart"
)
// mutation_change_quantity.go
//
// Registers the ChangeQuantity mutation.
//
// Behavior:
// - Locates an item by its cart-local line item Id (not source item_id).
// - If requested quantity <= 0 the line AND any descendants are
// removed via the shared RemoveItem cascade (transitive parent→child).
// - Otherwise the line's Quantity is updated and direct children are
// rescaled proportionally so a "2 of this + 2 of each accessory"
// bundle stays internally consistent when the parent becomes 3.
// - Totals are recalculated (WithTotals).
//
// Error handling:
// - Returns an error if the item Id is not found.
// - Returns an error if payload is nil (defensive).
//
// Concurrency:
// - Uses the grain's RW-safe mutation pattern: we mutate in place under
// the grain's implicit expectation that higher layers control access.
// (If strict locking is required around every mutation, wrap logic in
// an explicit g.mu.Lock()/Unlock(), but current model mirrors prior code.)
func (c *CartMutationContext) ChangeQuantity(g *CartGrain, m *cart_messages.ChangeQuantity) error {
if m == nil {
return fmt.Errorf("ChangeQuantity: nil payload")
}
ctx := context.Background()
foundIndex := -1
for i, it := range g.Items {
if it.Id == uint32(m.Id) {
foundIndex = i
break
}
}
if foundIndex == -1 {
return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
}
if m.Quantity <= 0 {
// Setting a parent line to 0 means "remove it AND its accessories".
// Delegate to RemoveItem so the cascade logic in one place stays
// the source of truth for descendant cleanup + reservation
// release — otherwise we'd duplicate the transitive cascade here.
return c.RemoveItem(g, &cart_messages.RemoveItem{Id: m.Id})
}
item := g.Items[foundIndex]
if item == nil {
return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
}
oldQty := item.Quantity
if g.Type == cart_messages.CartType_REGULAR && c.UseReservations(item) {
if item.ReservationEndTime != nil {
err := c.ReleaseItem(ctx, g.Id, item.Sku, item.StoreId)
if err != nil {
log.Printf("unable to release reservation for %s in location: %v", item.Sku, item.StoreId)
}
}
endTime, err := c.ReserveItem(ctx, g.Id, item.Sku, item.StoreId, uint16(m.Quantity))
if err != nil {
return err
}
item.ReservationEndTime = endTime
}
item.Quantity = uint16(m.Quantity)
// Rescale any direct child lines proportionally so accessories stay
// in sync with their parent. Descends recursively into grandchildren
// via CascadeChildQuantities itself. Skipped when the target itself
// is a child line — direct qty edits on a child are treated as
// explicit user intent and don't propagate further.
if item.ParentId == nil {
c.CascadeChildQuantities(ctx, g, item.Id, oldQty, item.Quantity)
}
g.UpdateTotals()
return nil
}