Files
go-cart-actor/pkg/cart/cart-grain.go

277 lines
7.8 KiB
Go

package cart
import (
"encoding/json"
"sync"
"time"
"git.k6n.net/go-cart-actor/pkg/voucher"
"github.com/matst80/go-redis-inventory/pkg/inventory"
)
// 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"`
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"`
SellerId string `json:"sellerId,omitempty"`
OrgPrice *Price `json:"orgPrice,omitempty"`
Cgm string `json:"cgm,omitempty"`
Tax int
Stock uint16 `json:"stock"`
Quantity uint16 `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"`
SaleStatus string `json:"saleStatus"`
Marking *Marking `json:"marking,omitempty"`
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
OrderReference string `json:"orderReference,omitempty"`
IsSubscribed bool `json:"isSubscribed,omitempty"`
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
}
type CartNotification struct {
LinkedId int `json:"id"`
Provider string `json:"provider"`
Title string `json:"title"`
Content string `json:"content"`
}
type SubscriptionDetails struct {
Id string `json:"id,omitempty"`
Version uint16 `json:"version"`
OfferingCode string `json:"offeringCode,omitempty"`
SigningType string `json:"signingType,omitempty"`
Meta json.RawMessage `json:"data,omitempty"`
}
type Notice struct {
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
Code *string `json:"code,omitempty"`
}
type CartPaymentStatus string
const (
CartPaymentStatusPending CartPaymentStatus = "pending"
CartPaymentStatusFailed CartPaymentStatus = "failed"
CartPaymentStatusSuccess CartPaymentStatus = "success"
CartPaymentStatusCancelled CartPaymentStatus = "partial"
)
type Marking struct {
Type uint32 `json:"type"`
Text string `json:"text"`
}
type CartGrain struct {
mu sync.RWMutex
lastItemId uint32
lastVoucherId uint32
lastAccess time.Time
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
userId string
Currency string `json:"currency"`
Language string `json:"language"`
Version uint `json:"version"`
InventoryReserved bool `json:"inventoryReserved"`
Id CartId `json:"id"`
Items []*CartItem `json:"items"`
TotalPrice *Price `json:"totalPrice"`
TotalDiscount *Price `json:"totalDiscount"`
Processing bool `json:"processing"`
//PaymentInProgress uint16 `json:"paymentInProgress"`
OrderReference string `json:"orderReference,omitempty"`
Vouchers []*Voucher `json:"vouchers,omitempty"`
Notifications []CartNotification `json:"cartNotification,omitempty"`
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
CheckoutStatus *CartPaymentStatus `json:"checkoutStatus,omitempty"`
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
}
type Voucher struct {
Code string `json:"code"`
Applied bool `json:"applied"`
Rules []string `json:"rules"`
Description string `json:"description,omitempty"`
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 _, expr := range v.Rules {
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 NewCartGrain(id uint64, ts time.Time) *CartGrain {
return &CartGrain{
lastItemId: 0,
lastVoucherId: 0,
lastAccess: ts,
lastChange: ts,
TotalDiscount: NewPrice(),
Vouchers: []*Voucher{},
Id: CartId(id),
Items: []*CartItem{},
TotalPrice: NewPrice(),
SubscriptionDetails: make(map[string]*SubscriptionDetails),
}
}
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 (c *CartGrain) HandleInventoryChange(change inventory.InventoryChange) {
for _, item := range c.Items {
l := "se"
if item.StoreId != nil {
l = *item.StoreId
}
if item.Sku == change.SKU && change.StockLocationID == l {
item.Stock = uint16(change.Value)
break
}
}
}
func (c *CartGrain) GetState() ([]byte, error) {
return json.Marshal(c)
}
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) UpdateTotals() {
c.TotalPrice = NewPrice()
c.TotalDiscount = NewPrice()
for _, item := range c.Items {
rowTotal := MultiplyPrice(item.Price, int64(item.Quantity))
if item.OrgPrice != nil {
diff := NewPrice()
diff.Add(*item.OrgPrice)
diff.Subtract(item.Price)
diff.Multiply(int64(item.Quantity))
//rowTotal.Subtract(*diff)
item.Discount = diff
if diff.IncVat > 0 {
c.TotalDiscount.Add(*diff)
}
}
item.TotalPrice = *rowTotal
c.TotalPrice.Add(*rowTotal)
}
for _, voucher := range c.Vouchers {
_, ok := voucher.AppliesTo(c)
voucher.Applied = false
if ok {
value := NewPriceFromIncVat(voucher.Value, 25)
if c.TotalPrice.IncVat <= value.IncVat {
// don't apply discounts to more than the total price
continue
}
voucher.Applied = true
c.TotalDiscount.Add(*value)
c.TotalPrice.Subtract(*value)
}
}
}