78 lines
3.2 KiB
Go
78 lines
3.2 KiB
Go
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
|
|
}
|