193 lines
6.1 KiB
Go
193 lines
6.1 KiB
Go
package cart
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
"time"
|
||
|
||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||
cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||
"git.k6n.net/mats/platform/inventory"
|
||
)
|
||
|
||
type CartMutationContext struct {
|
||
reservationService inventory.ReservationPolicy
|
||
}
|
||
|
||
func NewCartMutationContext(reservationService inventory.ReservationPolicy) *CartMutationContext {
|
||
return &CartMutationContext{
|
||
reservationService: reservationService,
|
||
}
|
||
}
|
||
|
||
func (c *CartMutationContext) ReserveItem(ctx context.Context, cartId CartId, sku string, locationId *string, quantity uint16) (*time.Time, error) {
|
||
|
||
if quantity <= 0 || c.reservationService == nil {
|
||
return nil, nil
|
||
}
|
||
|
||
l := inventory.LocationID("se")
|
||
if locationId != nil {
|
||
l = inventory.LocationID(*locationId)
|
||
}
|
||
ttl := time.Minute * 15
|
||
endTime := time.Now().Add(ttl)
|
||
err := c.reservationService.Reserve(ctx, inventory.CartReserveRequest{
|
||
CartID: inventory.CartID(cartId.String()),
|
||
InventoryReference: &inventory.InventoryReference{
|
||
SKU: inventory.SKU(sku),
|
||
LocationID: l,
|
||
},
|
||
TTL: ttl,
|
||
Quantity: uint32(quantity),
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &endTime, nil
|
||
|
||
}
|
||
|
||
func (c *CartMutationContext) UseReservations(item *CartItem) bool {
|
||
if item.ReservationEndTime != nil {
|
||
return true
|
||
}
|
||
if item.DropShip {
|
||
return false
|
||
}
|
||
if item.InventoryTracked {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (c *CartMutationContext) ReleaseItem(ctx context.Context, cartId CartId, sku string, locationId *string) error {
|
||
if c.reservationService == nil {
|
||
return nil
|
||
}
|
||
l := inventory.LocationID("se")
|
||
if locationId != nil {
|
||
l = inventory.LocationID(*locationId)
|
||
}
|
||
return c.reservationService.Release(ctx, inventory.SKU(sku), l, inventory.CartID(cartId.String()))
|
||
}
|
||
|
||
// equalOptional is the canonical nullable-equality primitive used by the
|
||
// cart mutations when deciding whether two identifier fields (parent/child
|
||
// links, store ids, anything else nullable) refer to the same thing.
|
||
//
|
||
// Returns true when both pointers are nil, when both alias the same
|
||
// address, or when both are non-nil and dereference to equal values. The
|
||
// same-address short-circuit is a no-op for correctness but lets a caller
|
||
// who passes the same variable get back true without traversing the
|
||
// generic's comparison path.
|
||
//
|
||
// One tested primitive covers every *T the cart currently compares —
|
||
// `*uint32` for ParentId, `*string` for StoreId — and any future
|
||
// comparable pointer type. Hand-rolled "both nil OR both non-nil and
|
||
// equal" expressions were duplicated at AddItem's merge site and were a
|
||
// footgun whenever a new field was added (which axis of the predicate
|
||
// was the relevant one?). Keeping a single helper makes future merges
|
||
// safer to copy.
|
||
func equalOptional[T comparable](a, b *T) bool {
|
||
if a == b { // both nil OR same address
|
||
return true
|
||
}
|
||
if a == nil || b == nil {
|
||
return false
|
||
}
|
||
return *a == *b
|
||
}
|
||
|
||
// CascadeChildQuantities scales every direct child's quantity
|
||
// proportionally when a parent's quantity is bumped (ChangeQuantity or
|
||
// the AddItem-merge path).
|
||
//
|
||
// - Ratio: newChildQty = oldChildQty × newParentQty / oldParentQty.
|
||
// - Floor: scale-down via integer division can yield 0 (e.g. parent
|
||
// 2 → 1, child qty 1 → (1×1)/2 = 0). We clamp to 1 so a scale-down
|
||
// never silently deletes an accessory. The mismatch is acceptable —
|
||
// shipping one extra child is better than dropping one.
|
||
// - Reservations: each child's reservation is released then re-acquired
|
||
// at the new quantity if UseReservations is true for that child AND a
|
||
// prior reservation existed. A failing reservation is logged, not
|
||
// raised, so a single bad child doesn't abort the whole cascade
|
||
// (matches the existing removal pattern in mutation_items.go).
|
||
// - Recursion: descends into grandchildren using the child's own
|
||
// before/after quantities as the next ratio's input, so deeply
|
||
// nested accessory trees (parent → child → grandchild) stay in
|
||
// ratio with the top-level change.
|
||
//
|
||
// Caller is responsible for locking any grain mutex and for re-running
|
||
// UpdateTotals after the cascade has settled.
|
||
func (c *CartMutationContext) CascadeChildQuantities(
|
||
ctx context.Context,
|
||
g *CartGrain,
|
||
parentLineId uint32,
|
||
oldParentQty uint16,
|
||
newParentQty uint16,
|
||
) {
|
||
if oldParentQty == 0 || oldParentQty == newParentQty {
|
||
return
|
||
}
|
||
|
||
for _, it := range g.Items {
|
||
if it == nil {
|
||
continue
|
||
}
|
||
if it.ParentId == nil || *it.ParentId != parentLineId {
|
||
continue
|
||
}
|
||
oldChildQty := it.Quantity
|
||
ratio := uint32(newParentQty) * uint32(oldChildQty) / uint32(oldParentQty)
|
||
newChildQty := uint16(ratio)
|
||
if newChildQty == 0 {
|
||
newChildQty = 1
|
||
}
|
||
if newChildQty == oldChildQty {
|
||
continue
|
||
}
|
||
|
||
if g.Type == cart_messages.CartType_REGULAR && c.UseReservations(it) && it.ReservationEndTime != nil {
|
||
if err := c.ReleaseItem(ctx, g.Id, it.Sku, it.StoreId); err != nil {
|
||
log.Printf("CascadeChildQuantities: failed to release %s: %v", it.Sku, err)
|
||
}
|
||
endTime, err := c.ReserveItem(ctx, g.Id, it.Sku, it.StoreId, newChildQty)
|
||
if err != nil {
|
||
log.Printf("CascadeChildQuantities: failed to reserve %s at qty %d: %v", it.Sku, newChildQty, err)
|
||
} else if endTime != nil {
|
||
it.ReservationEndTime = endTime
|
||
}
|
||
}
|
||
|
||
it.Quantity = newChildQty
|
||
// Descend so grandchildren track this child's ratio.
|
||
c.CascadeChildQuantities(ctx, g, it.Id, oldChildQty, newChildQty)
|
||
}
|
||
}
|
||
|
||
func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegistry {
|
||
|
||
reg := actor.NewMutationRegistry()
|
||
reg.RegisterMutations(
|
||
actor.NewMutation(context.AddItem),
|
||
actor.NewMutation(context.ChangeQuantity),
|
||
actor.NewMutation(context.RemoveItem),
|
||
actor.NewMutation(ClearCart),
|
||
actor.NewMutation(AddVoucher),
|
||
actor.NewMutation(RemoveVoucher),
|
||
actor.NewMutation(UpsertSubscriptionDetails),
|
||
actor.NewMutation(SetUserId),
|
||
actor.NewMutation(LineItemMarking),
|
||
actor.NewMutation(RemoveLineItemMarking),
|
||
actor.NewMutation(SetLineItemCustomFields),
|
||
actor.NewMutation(SubscriptionAdded),
|
||
actor.NewMutation(context.SetCartType),
|
||
actor.NewMutation(SetRecoveryContact),
|
||
// actor.NewMutation(SubscriptionRemoved),
|
||
)
|
||
return reg
|
||
|
||
}
|