package main import ( "encoding/json" "fmt" "slices" "sync" "time" messages "git.tornberg.me/go-cart-actor/pkg/messages" "git.tornberg.me/go-cart-actor/pkg/voucher" ) // Legacy padded [16]byte CartId and its helper methods removed. // Unified CartId (uint64 with base62 string form) now defined in cart_id.go. type StockStatus int type ItemMeta struct { Name string `json:"name"` Brand string `json:"brand,omitempty"` Category string `json:"category,omitempty"` Category2 string `json:"category2,omitempty"` Category3 string `json:"category3,omitempty"` Category4 string `json:"category4,omitempty"` Category5 string `json:"category5,omitempty"` SellerId string `json:"sellerId,omitempty"` SellerName string `json:"sellerName,omitempty"` Image string `json:"image,omitempty"` Outlet *string `json:"outlet,omitempty"` } type CartItem struct { Id uint32 `json:"id"` ItemId uint32 `json:"itemId,omitempty"` ParentId uint32 `json:"parentId,omitempty"` Sku string `json:"sku"` Price Price `json:"price"` TotalPrice Price `json:"totalPrice"` OrgPrice *Price `json:"orgPrice,omitempty"` Stock StockStatus `json:"stock"` Quantity int `json:"qty"` Discount *Price `json:"discount,omitempty"` Disclaimer string `json:"disclaimer,omitempty"` ArticleType string `json:"type,omitempty"` StoreId *string `json:"storeId,omitempty"` Meta *ItemMeta `json:"meta,omitempty"` } type CartDelivery struct { Id uint32 `json:"id"` Provider string `json:"provider"` Price Price `json:"price"` Items []uint32 `json:"items"` PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"` } type CartNotification struct { LinkedId int `json:"id"` Provider string `json:"provider"` Title string `json:"title"` Content string `json:"content"` } type CartGrain struct { mu sync.RWMutex lastItemId uint32 lastDeliveryId uint32 lastVoucherId uint32 lastAccess time.Time lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts) userId string Id CartId `json:"id"` Items []*CartItem `json:"items"` TotalPrice *Price `json:"totalPrice"` TotalDiscount *Price `json:"totalDiscount"` Deliveries []*CartDelivery `json:"deliveries,omitempty"` Processing bool `json:"processing"` PaymentInProgress bool `json:"paymentInProgress"` OrderReference string `json:"orderReference,omitempty"` PaymentStatus string `json:"paymentStatus,omitempty"` Vouchers []*Voucher `json:"vouchers,omitempty"` Notifications []CartNotification `json:"cartNotification,omitempty"` } type Voucher struct { Code string `json:"code"` Rules []*messages.VoucherRule `json:"rules"` Id uint32 `json:"id"` Value int64 `json:"value"` } func (v *Voucher) AppliesTo(cart *CartGrain) ([]*CartItem, bool) { // No rules -> applies to entire cart if len(v.Rules) == 0 { return cart.Items, true } // Build evaluation context once ctx := voucher.EvalContext{ Items: make([]voucher.Item, 0, len(cart.Items)), CartTotalInc: 0, } if cart.TotalPrice != nil { ctx.CartTotalInc = cart.TotalPrice.IncVat } for _, it := range cart.Items { category := "" if it.Meta != nil { category = it.Meta.Category } ctx.Items = append(ctx.Items, voucher.Item{ Sku: it.Sku, Category: category, UnitPrice: it.Price.IncVat, }) } // All voucher rules must pass (logical AND) for _, rule := range v.Rules { expr := rule.GetCondition() if expr == "" { // Empty condition treated as pass (acts like a comment / placeholder) continue } rs, err := voucher.ParseRules(expr) if err != nil { // Fail closed on parse error return nil, false } if !rs.Applies(ctx) { return nil, false } } return cart.Items, true } func (c *CartGrain) GetId() uint64 { return uint64(c.Id) } func (c *CartGrain) GetLastChange() time.Time { return c.lastChange } func (c *CartGrain) GetLastAccess() time.Time { return c.lastAccess } func (c *CartGrain) GetCurrentState() (*CartGrain, error) { c.lastAccess = time.Now() return c, nil } func getInt(data float64, ok bool) (int, error) { if !ok { return 0, fmt.Errorf("invalid type") } return int(data), nil } // func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*CartGrain, error) { // cartItem, err := getItemData(sku, qty, country) // if err != nil { // return nil, err // } // cartItem.StoreId = storeId // return c.Apply(cartItem, false) // } func (c *CartGrain) GetState() ([]byte, error) { return json.Marshal(c) } func (c *CartGrain) ItemsWithDelivery() []uint32 { ret := make([]uint32, 0, len(c.Items)) for _, item := range c.Items { for _, delivery := range c.Deliveries { for _, id := range delivery.Items { if item.Id == id { ret = append(ret, id) } } } } return ret } func (c *CartGrain) ItemsWithoutDelivery() []uint32 { ret := make([]uint32, 0, len(c.Items)) hasDelivery := c.ItemsWithDelivery() for _, item := range c.Items { found := slices.Contains(hasDelivery, item.Id) if !found { ret = append(ret, item.Id) } } return ret } func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) { c.mu.RLock() defer c.mu.RUnlock() for _, item := range c.Items { if item.Sku == sku { return item, true } } return nil, false } // func (c *CartGrain) Apply(content proto.Message, isReplay bool) (*CartGrain, error) { // updated, err := ApplyRegistered(c, content) // if err != nil { // if err == ErrMutationNotRegistered { // return nil, fmt.Errorf("unsupported mutation type %T (not registered)", content) // } // return nil, err // } // // Sliding TTL: update lastChange only for non-replay successful mutations. // if updated != nil && !isReplay { // c.lastChange = time.Now() // c.lastAccess = time.Now() // go AppendCartEvent(c.Id, content) // } // return updated, nil // } func (c *CartGrain) UpdateTotals() { c.TotalPrice = NewPrice() c.TotalDiscount = NewPrice() for _, item := range c.Items { rowTotal := MultiplyPrice(item.Price, int64(item.Quantity)) item.TotalPrice = *rowTotal c.TotalPrice.Add(*rowTotal) if item.OrgPrice != nil { diff := NewPrice() diff.Add(*item.OrgPrice) diff.Subtract(item.Price) if diff.IncVat > 0 { c.TotalDiscount.Add(*diff) } } } for _, delivery := range c.Deliveries { c.TotalPrice.Add(delivery.Price) } for _, voucher := range c.Vouchers { if _, ok := voucher.AppliesTo(c); ok { value := NewPriceFromIncVat(voucher.Value, 25) c.TotalDiscount.Add(*value) c.TotalPrice.Subtract(*value) } } }