package cart import ( "context" "errors" "fmt" "log" "time" cart_messages "git.k6n.net/go-cart-actor/proto/cart" "google.golang.org/protobuf/types/known/timestamppb" ) // mutation_add_item.go // // Registers the AddItem cart mutation in the generic mutation registry. // This replaces the legacy switch-based logic previously found in CartGrain.Apply. // // Behavior: // - Validates quantity > 0 // - If an item with same SKU exists -> increases quantity // - Else creates a new CartItem with computed tax amounts // - Totals recalculated automatically via WithTotals() // // NOTE: Any future field additions in messages.AddItem that affect pricing / tax // must keep this handler in sync. var ErrPaymentInProgress = errors.New("payment in progress") func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) error { ctx := context.Background() if m == nil { return fmt.Errorf("AddItem: nil payload") } if m.Quantity < 1 { return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity) } // Merge with any existing item having same SKU and matching StoreId (including both nil). for _, existing := range g.Items { if existing.Sku != m.Sku { continue } sameStore := (existing.StoreId == nil && m.StoreId == nil) || (existing.StoreId != nil && m.StoreId != nil && *existing.StoreId == *m.StoreId) if !sameStore { continue } if c.UseReservations(existing) { if err := c.ReleaseItem(ctx, g.Id, existing.Sku, existing.StoreId); err != nil { log.Printf("failed to release item %d: %v", existing.Id, err) } endTime, err := c.ReserveItem(ctx, g.Id, existing.Sku, existing.StoreId, existing.Quantity+uint16(m.Quantity)) if err != nil { return err } existing.ReservationEndTime = endTime } existing.Quantity += uint16(m.Quantity) existing.Stock = uint16(m.Stock) // If existing had nil store but new has one, adopt it. if existing.StoreId == nil && m.StoreId != nil { existing.StoreId = m.StoreId } return nil } g.mu.Lock() defer g.mu.Unlock() g.lastItemId++ taxRate := float32(25.0) if m.Tax > 0 { taxRate = float32(int(m.Tax) / 100) } pricePerItem := NewPriceFromIncVat(m.Price, taxRate) needsReservation := true if m.ReservationEndTime != nil { needsReservation = m.ReservationEndTime.AsTime().Before(time.Now()) } cartItem := &CartItem{ Id: g.lastItemId, ItemId: uint32(m.ItemId), Quantity: uint16(m.Quantity), Sku: m.Sku, Tax: int(taxRate * 100), Meta: &ItemMeta{ Name: m.Name, Image: m.Image, Brand: m.Brand, Category: m.Category, Category2: m.Category2, Category3: m.Category3, Category4: m.Category4, Category5: m.Category5, Outlet: m.Outlet, SellerName: m.SellerName, }, SellerId: m.SellerId, Cgm: m.Cgm, SaleStatus: m.SaleStatus, ParentId: m.ParentId, Price: *pricePerItem, TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)), Stock: uint16(m.Stock), Disclaimer: m.Disclaimer, OrgPrice: getOrgPrice(m.OrgPrice, taxRate), ArticleType: m.ArticleType, StoreId: m.StoreId, } if needsReservation && c.UseReservations(cartItem) { endTime, err := c.ReserveItem(ctx, g.Id, m.Sku, m.StoreId, uint16(m.Quantity)) if err != nil { return err } if endTime != nil { m.ReservationEndTime = timestamppb.New(*endTime) t := m.ReservationEndTime.AsTime() cartItem.ReservationEndTime = &t } } g.Items = append(g.Items, cartItem) g.UpdateTotals() return nil } func getOrgPrice(orgPrice int64, taxRate float32) *Price { if orgPrice <= 0 { return nil } return NewPriceFromIncVat(orgPrice, taxRate) }