Files
go-cart-actor/pkg/cart/mutation_add_item.go
T
mats 1c97a4bdb0
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 5s
wishlist and dashboard
2026-07-01 13:04:55 +02:00

178 lines
4.9 KiB
Go

package cart
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"time"
cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart"
"google.golang.org/protobuf/types/known/timestamppb"
)
// decodeExtra unpacks the dynamic product data carried as raw JSON. Invalid
// payloads are logged and dropped rather than failing the mutation.
func decodeExtra(b []byte) map[string]json.RawMessage {
if len(b) == 0 {
return nil
}
m := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &m); err != nil {
log.Printf("AddItem: invalid extra_json: %v", err)
return nil
}
return m
}
// 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 the same item id (ItemId) exists -> increases quantity
// - Else creates a new CartItem with computed tax amounts
// - Totals recalculated automatically via WithTotals()
//
// Item identity is the catalog item id (ItemId), not the SKU: the product
// service is looked up by id and the returned SKU is reference-only.
//
// 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 the same item id and matching StoreId
// (including both nil). Identity is the id; SKU is reference-only.
for _, existing := range g.Items {
if existing.ItemId != m.ItemId {
continue
}
sameStore := (existing.StoreId == nil && m.StoreId == nil) ||
(existing.StoreId != nil && m.StoreId != nil && *existing.StoreId == *m.StoreId)
if !sameStore {
continue
}
if g.Type == cart_messages.CartType_REGULAR && 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)
existing.InventoryTracked = m.InventoryTracked
existing.DropShip = m.DropShip
// If existing had nil store but new has one, adopt it.
if existing.StoreId == nil && m.StoreId != nil {
existing.StoreId = m.StoreId
}
// Refresh dynamic product data with the latest payload.
if extra := decodeExtra(m.ExtraJson); extra != nil {
existing.Extra = extra
}
// Replace custom fields when provided on the re-add.
if len(m.CustomFields) > 0 {
existing.CustomFields = m.CustomFields
}
return nil
}
g.mu.Lock()
defer g.mu.Unlock()
g.lastItemId++
// m.Tax is the rate in basis points (2500 = 25%, 1250 = 12.5%) — the single
// platform scale; flows straight through, no conversion.
rateBp := 2500
if m.Tax > 0 {
rateBp = int(m.Tax)
}
pricePerItem := NewPriceFromIncVat(m.Price, rateBp)
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: rateBp,
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, rateBp),
ArticleType: m.ArticleType,
StoreId: m.StoreId,
Extra: decodeExtra(m.ExtraJson),
CustomFields: m.CustomFields,
InventoryTracked: m.InventoryTracked,
DropShip: m.DropShip,
}
if g.Type == cart_messages.CartType_REGULAR && 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, rateBp int) *Price {
if orgPrice <= 0 {
return nil
}
return NewPriceFromIncVat(orgPrice, rateBp)
}