some inventory

This commit is contained in:
mats committed 2026-07-25 18:12:26 +02:00
1 parent 9f0d7dc7d8
commit 4dc1d7bc1c
3 files changed
+64 -20

No files matched your search

+41 -6
View File
@@ -2,14 +2,24 @@ package inventory
import ( import (
"context" "context"
"errors"
platforminv "git.k6n.net/mats/platform/inventory" platforminv "git.k6n.net/mats/platform/inventory"
) )
// ReservationPolicyAdapter wraps a RedisCartReservationService to satisfy the // ReservationPolicyAdapter wraps a RedisCartReservationService to satisfy the
// platform/inventory.ReservationPolicy interface. It translates between the // platform/inventory.ReservationPolicy interface. It translates between the
// platform's value types (SKU, LocationID, CartID) and the Redis-level types // platform's value types (SKU, LocationID, CartID, CartReserveRequest,
// used internally by go-redis-inventory. // 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 { type ReservationPolicyAdapter struct {
inner *RedisCartReservationService inner *RedisCartReservationService
} }
@@ -21,8 +31,13 @@ func NewReservationPolicyAdapter(inner *RedisCartReservationService) *Reservatio
return &ReservationPolicyAdapter{inner: inner} 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 { func (a *ReservationPolicyAdapter) Reserve(ctx context.Context, req platforminv.CartReserveRequest) error {
return a.inner.ReserveForCart(ctx, CartReserveRequest{ return translateReservationError(a.inner.Reserve(ctx, CartReserveRequest{
InventoryReference: &InventoryReference{ InventoryReference: &InventoryReference{
SKU: SKU(req.SKU), SKU: SKU(req.SKU),
LocationID: LocationID(req.LocationID), LocationID: LocationID(req.LocationID),
@@ -30,13 +45,33 @@ func (a *ReservationPolicyAdapter) Reserve(ctx context.Context, req platforminv.
CartID: CartID(req.CartID), CartID: CartID(req.CartID),
Quantity: req.Quantity, Quantity: req.Quantity,
TTL: req.TTL, TTL: req.TTL,
}) }))
} }
func (a *ReservationPolicyAdapter) Release(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID, cartID platforminv.CartID) error { func (a *ReservationPolicyAdapter) Release(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID, cartID platforminv.CartID) error {
return a.inner.ReleaseForCart(ctx, SKU(sku), LocationID(locationID), CartID(cartID)) 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) { func (a *ReservationPolicyAdapter) Available(ctx context.Context, sku platforminv.SKU, locationID platforminv.LocationID) (int64, error) {
return a.inner.GetAvailableInventory(ctx, SKU(sku), LocationID(locationID)) 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
} }
+13 -4
View File
@@ -44,7 +44,10 @@ func totalReservedKey(sku SKU, locationID LocationID) string {
return fmt.Sprintf("total_reserved:%s:%s", sku, locationID) return fmt.Sprintf("total_reserved:%s:%s", sku, locationID)
} }
func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error { // Reserve satisfies inventory.ReservationPolicy (platform/inventory).
// Method renamed from ReserveForCart as part of cut-30.x: land the ReservationPolicy
// interface seam — see docs/inventory-shape.md §Shallow — `ReservationPolicy` interface.
func (s *RedisCartReservationService) Reserve(ctx context.Context, req CartReserveRequest) error {
keys := []string{ keys := []string{
getInventoryKey(req.SKU, req.LocationID), getInventoryKey(req.SKU, req.LocationID),
cartQtyKey(req.SKU, req.LocationID), cartQtyKey(req.SKU, req.LocationID),
@@ -69,7 +72,10 @@ func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req Ca
return nil return nil
} }
func (s *RedisCartReservationService) ReleaseForCart(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) error { // Release satisfies inventory.ReservationPolicy (platform/inventory).
// Method renamed from ReleaseForCart as part of cut-30.x: land the ReservationPolicy
// interface seam — see docs/inventory-shape.md §Shallow — `ReservationPolicy` interface.
func (s *RedisCartReservationService) Release(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) error {
keys := []string{ keys := []string{
cartQtyKey(sku, locationID), cartQtyKey(sku, locationID),
cartExpKey(sku, locationID), cartExpKey(sku, locationID),
@@ -133,7 +139,7 @@ func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context,
} }
// Get remaining items // Get remaining items
remainingItems, err := s.GetAvailableInventory(ctx, sku, locationID) remainingItems, err := s.Available(ctx, sku, locationID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -145,7 +151,10 @@ func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context,
}, nil }, nil
} }
func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) { // Available satisfies inventory.ReservationPolicy (platform/inventory).
// Method renamed from GetAvailableInventory as part of cut-30.x: land the
// ReservationPolicy interface seam — see docs/inventory-shape.md.
func (s *RedisCartReservationService) Available(ctx context.Context, sku SKU, locationID LocationID) (int64, error) {
keys := []string{ keys := []string{
getInventoryKey(sku, locationID), getInventoryKey(sku, locationID),
cartQtyKey(sku, locationID), cartQtyKey(sku, locationID),
+10 -10
View File
@@ -54,13 +54,13 @@ func TestReserveAndRelease(t *testing.T) {
Quantity: 2, Quantity: 2,
TTL: 5 * time.Minute, TTL: 5 * time.Minute,
} }
err = service.ReserveForCart(ctx, req) err = service.Reserve(ctx, req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Check available // Check available
available, err := service.GetAvailableInventory(ctx, sku, locationID) available, err := service.Available(ctx, sku, locationID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -81,13 +81,13 @@ func TestReserveAndRelease(t *testing.T) {
} }
// Release // Release
err = service.ReleaseForCart(ctx, sku, locationID, cartID) err = service.Release(ctx, sku, locationID, cartID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Check available after release // Check available after release
available, err = service.GetAvailableInventory(ctx, sku, locationID) available, err = service.Available(ctx, sku, locationID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -137,7 +137,7 @@ func TestConcurrentReserves(t *testing.T) {
Quantity: 1, Quantity: 1,
TTL: 5 * time.Minute, TTL: 5 * time.Minute,
} }
err := service.ReserveForCart(ctx, req) err := service.Reserve(ctx, req)
results <- err results <- err
}(i) }(i)
} }
@@ -161,7 +161,7 @@ func TestConcurrentReserves(t *testing.T) {
} }
// Check final available // Check final available
available, err := service.GetAvailableInventory(ctx, sku, locationID) available, err := service.Available(ctx, sku, locationID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -200,7 +200,7 @@ func TestExpiry(t *testing.T) {
Quantity: 2, Quantity: 2,
TTL: 10 * time.Second, TTL: 10 * time.Second,
} }
err = service.ReserveForCart(ctx, req) err = service.Reserve(ctx, req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -231,7 +231,7 @@ func TestExpiry(t *testing.T) {
// To test expiry, we can't easily, so skip the expiry part for now // To test expiry, we can't easily, so skip the expiry part for now
// Check available // Check available
available, err := service.GetAvailableInventory(ctx, sku, locationID) available, err := service.Available(ctx, sku, locationID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -269,7 +269,7 @@ func TestGetReservationSummary(t *testing.T) {
Quantity: 2, Quantity: 2,
TTL: 5 * time.Minute, TTL: 5 * time.Minute,
} }
err = service.ReserveForCart(ctx, req1) err = service.Reserve(ctx, req1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -280,7 +280,7 @@ func TestGetReservationSummary(t *testing.T) {
Quantity: 1, Quantity: 1,
TTL: 10 * time.Minute, TTL: 10 * time.Minute,
} }
err = service.ReserveForCart(ctx, req2) err = service.Reserve(ctx, req2)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }