package inventory import ( "context" "errors" platforminv "git.k6n.net/mats/platform/inventory" ) // ReservationPolicyAdapter wraps a RedisCartReservationService to satisfy the // platform/inventory.ReservationPolicy interface. It translates between the // platform's value types (SKU, LocationID, CartID, CartReserveRequest, // InventoryReference) and the Redis-level types used internally by // go-redis-inventory, so that callers (cart, checkout, backoffice, the // inventory service itself) can depend on the seam and remain agnostic to the // storage substrate — see docs/inventory-shape.md §"Next: inventory service as // single writer". // // The error translation below means callers can `errors.Is(err, // platforminv.ErrInsufficientStock)` regardless of whether the underlying // storage is Redis Lua (today), an actor grain (the deep rewrite), or // something else (the test adapter in platform/inventory/memory.go). type ReservationPolicyAdapter struct { inner *RedisCartReservationService } // NewReservationPolicyAdapter creates an adapter that satisfies // platform/inventory.ReservationPolicy by delegating to the Redis-backed // reservation service. func NewReservationPolicyAdapter(inner *RedisCartReservationService) *ReservationPolicyAdapter { return &ReservationPolicyAdapter{inner: inner} } // Compile-time assertion that ReservationPolicyAdapter satisfies the // platform/inventory.ReservationPolicy interface. If a method is added or // renamed on either side, this stops compiling. var _ platforminv.ReservationPolicy = (*ReservationPolicyAdapter)(nil) func (a *ReservationPolicyAdapter) Reserve(ctx context.Context, req platforminv.CartReserveRequest) error { return translateReservationError(a.inner.Reserve(ctx, CartReserveRequest{ InventoryReference: &InventoryReference{ SKU: SKU(req.SKU), LocationID: LocationID(req.LocationID), }, CartID: CartID(req.CartID), Quantity: req.Quantity, TTL: req.TTL, })) } func (a *ReservationPolicyAdapter) Release(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID, cartID platforminv.CartID) error { return a.inner.Release(ctx, SKU(sku), LocationID(locationID), CartID(cartID)) } func (a *ReservationPolicyAdapter) Available(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID) (int64, error) { return a.inner.Available(ctx, SKU(sku), LocationID(locationID)) } // translateReservationError maps the Redis-storage-specific sentinel to the // platform-level sentinel so consumers can errors.Is against one set of errors // regardless of the underlying ReservationPolicy implementation. Pass-through // for unknown errors — those carry enough context on their own. // // Trade-off: the original Redis sentinel is replaced, not wrapped, so callers // that need to discriminate "Redis ran out of stock" from "Lua script timed // out" must look inside the seam rather than outside. Per the platform seam // contract that's the right cut: storage internals stay encapsulated; callers // branch on the cross-module vocabulary. func translateReservationError(err error) error { if err == nil { return nil } if errors.Is(err, ErrInsufficientInventory) { return platforminv.ErrInsufficientStock } return err }