package inventory import ( "context" "sync" "testing" "time" ) // InMemoryReservationPolicy is a thread-safe in-memory implementation of // ReservationPolicy for tests — no Redis needed. Backed by a map from // SKU+location to available quantity. type InMemoryReservationPolicy struct { mu sync.Mutex qty map[string]int64 // key: "sku|location" → available quantity held map[string]int64 // key: "sku|location|cartID" → reserved quantity } // NewInMemoryReservationPolicy creates a policy seeded with the given // sku+location → available quantity entries. Each entry is a pair of // (sku, location, qty); pass in threes. func NewInMemoryReservationPolicy(seed ...any) *InMemoryReservationPolicy { p := &InMemoryReservationPolicy{ qty: make(map[string]int64), held: make(map[string]int64), } for i := 0; i+2 < len(seed); i += 3 { sku, ok := seed[i].(SKU) if !ok { continue } loc, ok := seed[i+1].(LocationID) if !ok { continue } qty, ok := seed[i+2].(int64) if !ok { continue } p.qty[key(sku, loc)] = qty } return p } func key(sku SKU, loc LocationID) string { return string(sku) + "|" + string(loc) } func heldKey(sku SKU, loc LocationID, c CartID) string { return string(sku) + "|" + string(loc) + "|" + string(c) } func (p *InMemoryReservationPolicy) Reserve(_ context.Context, req CartReserveRequest) error { p.mu.Lock() defer p.mu.Unlock() k := key(req.SKU, req.LocationID) avail := p.qty[k] hk := heldKey(req.SKU, req.LocationID, req.CartID) if _, exists := p.held[hk]; exists { // Already held — add to existing reservation avail -= p.held[hk] } if avail < int64(req.Quantity) { return ErrInsufficientStock } p.held[hk] += int64(req.Quantity) return nil } func (p *InMemoryReservationPolicy) Release(_ context.Context, sku SKU, locationID LocationID, cartID CartID) error { p.mu.Lock() defer p.mu.Unlock() delete(p.held, heldKey(sku, locationID, cartID)) return nil } func (p *InMemoryReservationPolicy) Available(_ context.Context, sku SKU, locationID LocationID) (int64, error) { p.mu.Lock() defer p.mu.Unlock() k := key(sku, locationID) avail := p.qty[k] // Subtract all held quantities for this sku+location for hk, held := range p.held { if len(hk) > len(k) && hk[:len(k)] == k && hk[len(k)] == '|' { avail -= held } } if avail < 0 { avail = 0 } return avail, nil } // Reset clears all reservations and resets available quantities. func (p *InMemoryReservationPolicy) Reset() { p.mu.Lock() defer p.mu.Unlock() p.held = make(map[string]int64) } // ErrInsufficientStock is returned by Reserve when not enough stock is available. var ErrInsufficientStock = &reservationError{"insufficient stock"} type reservationError struct{ msg string } func (e *reservationError) Error() string { return e.msg } // ── Tests ──────────────────────────────────────────────────────────────── func TestInMemoryReserveAndRelease(t *testing.T) { ctx := context.Background() p := NewInMemoryReservationPolicy( SKU("ABC-1"), LocationID("se"), int64(10), SKU("ABC-1"), LocationID("no"), int64(5), ) // Reserve available stock err := p.Reserve(ctx, CartReserveRequest{ InventoryReference: &InventoryReference{SKU: "ABC-1", LocationID: "se"}, CartID: "cart-1", Quantity: 3, TTL: 15 * time.Minute, }) if err != nil { t.Fatalf("Reserve: %v", err) } avail, _ := p.Available(ctx, "ABC-1", "se") if avail != 7 { t.Fatalf("after reserve 3 of 10, expected 7 available, got %d", avail) } // Release if err := p.Release(ctx, "ABC-1", "se", "cart-1"); err != nil { t.Fatalf("Release: %v", err) } avail, _ = p.Available(ctx, "ABC-1", "se") if avail != 10 { t.Fatalf("after release, expected 10 available, got %d", avail) } } func TestInMemoryInsufficientStock(t *testing.T) { ctx := context.Background() p := NewInMemoryReservationPolicy( SKU("ABC-1"), LocationID("se"), int64(2), ) err := p.Reserve(ctx, CartReserveRequest{ InventoryReference: &InventoryReference{SKU: "ABC-1", LocationID: "se"}, CartID: "cart-1", Quantity: 5, TTL: 15 * time.Minute, }) if err == nil { t.Fatal("expected insufficient stock error, got nil") } } func TestInMemoryConcurrentReserves(t *testing.T) { ctx := context.Background() p := NewInMemoryReservationPolicy( SKU("ABC-1"), LocationID("se"), int64(100), ) errs := make(chan error, 10) for i := 0; i < 10; i++ { go func(n int) { errs <- p.Reserve(ctx, CartReserveRequest{ InventoryReference: &InventoryReference{SKU: "ABC-1", LocationID: "se"}, CartID: CartID(string(rune('0' + n))), Quantity: 10, TTL: 15 * time.Minute, }) }(i) } for i := 0; i < 10; i++ { if err := <-errs; err != nil { t.Errorf("concurrent reserve %d: %v", i, err) } } avail, _ := p.Available(ctx, "ABC-1", "se") if avail != 0 { t.Fatalf("after 10×10 reserves from 100, expected 0 available, got %d", avail) } }