From 4dc1d7bc1c506eb31e8570b4687f6c5223357205 Mon Sep 17 00:00:00 2001 From: matst80 Date: Sat, 25 Jul 2026 18:12:26 +0200 Subject: [PATCH] some inventory --- pkg/inventory/adapter.go | 47 ++++++++++++++++--- pkg/inventory/redis_reservation_service.go | 17 +++++-- .../redis_reservation_service_test.go | 20 ++++---- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/pkg/inventory/adapter.go b/pkg/inventory/adapter.go index 086e69e..4949bd0 100644 --- a/pkg/inventory/adapter.go +++ b/pkg/inventory/adapter.go @@ -2,14 +2,24 @@ 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) and the Redis-level types -// used internally by go-redis-inventory. +// 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 } @@ -21,8 +31,13 @@ func NewReservationPolicyAdapter(inner *RedisCartReservationService) *Reservatio 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 a.inner.ReserveForCart(ctx, CartReserveRequest{ + return translateReservationError(a.inner.Reserve(ctx, CartReserveRequest{ InventoryReference: &InventoryReference{ SKU: SKU(req.SKU), LocationID: LocationID(req.LocationID), @@ -30,13 +45,33 @@ func (a *ReservationPolicyAdapter) Reserve(ctx context.Context, req platforminv. 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.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) { - 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 } diff --git a/pkg/inventory/redis_reservation_service.go b/pkg/inventory/redis_reservation_service.go index ef1aec1..8011007 100644 --- a/pkg/inventory/redis_reservation_service.go +++ b/pkg/inventory/redis_reservation_service.go @@ -44,7 +44,10 @@ func totalReservedKey(sku SKU, locationID LocationID) string { 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{ getInventoryKey(req.SKU, req.LocationID), cartQtyKey(req.SKU, req.LocationID), @@ -69,7 +72,10 @@ func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req Ca 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{ cartQtyKey(sku, locationID), cartExpKey(sku, locationID), @@ -133,7 +139,7 @@ func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context, } // Get remaining items - remainingItems, err := s.GetAvailableInventory(ctx, sku, locationID) + remainingItems, err := s.Available(ctx, sku, locationID) if err != nil { return nil, err } @@ -145,7 +151,10 @@ func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context, }, 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{ getInventoryKey(sku, locationID), cartQtyKey(sku, locationID), diff --git a/pkg/inventory/redis_reservation_service_test.go b/pkg/inventory/redis_reservation_service_test.go index 145b59c..a2cf042 100644 --- a/pkg/inventory/redis_reservation_service_test.go +++ b/pkg/inventory/redis_reservation_service_test.go @@ -54,13 +54,13 @@ func TestReserveAndRelease(t *testing.T) { Quantity: 2, TTL: 5 * time.Minute, } - err = service.ReserveForCart(ctx, req) + err = service.Reserve(ctx, req) if err != nil { t.Fatal(err) } // Check available - available, err := service.GetAvailableInventory(ctx, sku, locationID) + available, err := service.Available(ctx, sku, locationID) if err != nil { t.Fatal(err) } @@ -81,13 +81,13 @@ func TestReserveAndRelease(t *testing.T) { } // Release - err = service.ReleaseForCart(ctx, sku, locationID, cartID) + err = service.Release(ctx, sku, locationID, cartID) if err != nil { t.Fatal(err) } // Check available after release - available, err = service.GetAvailableInventory(ctx, sku, locationID) + available, err = service.Available(ctx, sku, locationID) if err != nil { t.Fatal(err) } @@ -137,7 +137,7 @@ func TestConcurrentReserves(t *testing.T) { Quantity: 1, TTL: 5 * time.Minute, } - err := service.ReserveForCart(ctx, req) + err := service.Reserve(ctx, req) results <- err }(i) } @@ -161,7 +161,7 @@ func TestConcurrentReserves(t *testing.T) { } // Check final available - available, err := service.GetAvailableInventory(ctx, sku, locationID) + available, err := service.Available(ctx, sku, locationID) if err != nil { t.Fatal(err) } @@ -200,7 +200,7 @@ func TestExpiry(t *testing.T) { Quantity: 2, TTL: 10 * time.Second, } - err = service.ReserveForCart(ctx, req) + err = service.Reserve(ctx, req) if err != nil { 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 // Check available - available, err := service.GetAvailableInventory(ctx, sku, locationID) + available, err := service.Available(ctx, sku, locationID) if err != nil { t.Fatal(err) } @@ -269,7 +269,7 @@ func TestGetReservationSummary(t *testing.T) { Quantity: 2, TTL: 5 * time.Minute, } - err = service.ReserveForCart(ctx, req1) + err = service.Reserve(ctx, req1) if err != nil { t.Fatal(err) } @@ -280,7 +280,7 @@ func TestGetReservationSummary(t *testing.T) { Quantity: 1, TTL: 10 * time.Minute, } - err = service.ReserveForCart(ctx, req2) + err = service.Reserve(ctx, req2) if err != nil { t.Fatal(err) }