more stuff
This commit is contained in:
1 parent
de2c822657
commit
540aa48c44
2 files changed
+139
-99
No files matched your search
@@ -0,0 +1,139 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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. Lives in a regular .go file (not _test.go)
|
||||
// so other packages can use it as a test adapter without spinning up Redis —
|
||||
// this is the "in-memory map as the test adapter" that the inventory-shape.md
|
||||
// doc promises kills the cart-tests-need-Redis problem.
|
||||
//
|
||||
// Construct with NewInMemoryReservationPolicy(seed...) — seed is a flat list of
|
||||
// (SKU, LocationID, qty) triples. Quantity starts at qty for that SKU+location.
|
||||
// Reserves decrement *available* (held stock counts against availability) and
|
||||
// Release removes a held reservation. Available returns qty minus the sum of all
|
||||
// active held reservations, clamped to >= 0.
|
||||
//
|
||||
// Reserve is idempotent for the same cart (re-reserving a SKU+location adds to
|
||||
// the existing hold rather than double-charging availability).
|
||||
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 triple 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
|
||||
}
|
||||
|
||||
// SetStock is a test-helper for seeding or replenishing a SKU+location's
|
||||
// available quantity. Production callers should never need this — only the
|
||||
// in-memory adapter exposes it.
|
||||
func (p *InMemoryReservationPolicy) SetStock(sku SKU, loc LocationID, qty int64) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.qty[key(sku, loc)] = qty
|
||||
}
|
||||
|
||||
// Reset clears all reservations and leaves available quantities as-is. Test
|
||||
// helper only — useful between subtests that share a fixture.
|
||||
func (p *InMemoryReservationPolicy) Reset() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.held = make(map[string]int64)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Reserve implements ReservationPolicy.
|
||||
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, so we don't double-charge
|
||||
// availability against the same cart.
|
||||
avail += p.held[hk]
|
||||
}
|
||||
if avail < int64(req.Quantity) {
|
||||
return ErrInsufficientStock
|
||||
}
|
||||
avail -= int64(req.Quantity)
|
||||
p.held[hk] += int64(req.Quantity)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release implements ReservationPolicy. Idempotent: releasing an
|
||||
// already-released or never-held cart is a no-op.
|
||||
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
|
||||
}
|
||||
|
||||
// Available implements ReservationPolicy.
|
||||
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.
|
||||
prefix := k + "|"
|
||||
for hk, held := range p.held {
|
||||
if len(hk) > len(prefix) && hk[:len(prefix)] == prefix {
|
||||
avail -= held
|
||||
}
|
||||
}
|
||||
if avail < 0 {
|
||||
avail = 0
|
||||
}
|
||||
return avail, nil
|
||||
}
|
||||
|
||||
// ErrInsufficientStock is returned by Reserve when not enough stock is available.
|
||||
// Distinct from go-redis-inventory's ErrInsufficientInventory by package design —
|
||||
// the ReservationPolicy interface returns generic error; consumers that need to
|
||||
// branch on this should errors.Is against the adapter's own error.
|
||||
var ErrInsufficientStock = &reservationError{"insufficient stock"}
|
||||
|
||||
type reservationError struct{ msg string }
|
||||
|
||||
func (e *reservationError) Error() string { return e.msg }
|
||||
@@ -2,109 +2,10 @@ 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) {
|
||||
|
||||
Reference in new issue
Block a user