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 }