more fancy features
This commit is contained in:
@@ -55,11 +55,9 @@ err = cartService.ReserveForCart(ctx, inventory.CartReserveRequest{
|
|||||||
// Get available inventory (accounting for reservations)
|
// Get available inventory (accounting for reservations)
|
||||||
available, err := cartService.GetAvailableInventory(ctx, "item1", "warehouse1")
|
available, err := cartService.GetAvailableInventory(ctx, "item1", "warehouse1")
|
||||||
|
|
||||||
// Get reservation status and remaining time
|
// Get reservation summary for an item
|
||||||
active, remaining, err := cartService.GetReservationStatus(ctx, "item1", "warehouse1", "cart123")
|
summary, err := cartService.GetReservationSummary(ctx, "item1", "warehouse1")
|
||||||
if active {
|
fmt.Printf("Reservations: %d, Next release: %v, Remaining: %d\n", summary.ReservationCount, summary.EarliestRelease, summary.RemainingItems)
|
||||||
fmt.Printf("Reservation active, %v remaining\n", remaining)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ func totalReservedKey(sku SKU, locationID LocationID) string {
|
|||||||
|
|
||||||
func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error {
|
func (s *RedisCartReservationService) ReserveForCart(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),
|
||||||
cartExpKey(req.Sku, req.LocationID),
|
cartExpKey(req.SKU, req.LocationID),
|
||||||
totalReservedKey(req.Sku, req.LocationID),
|
totalReservedKey(req.SKU, req.LocationID),
|
||||||
}
|
}
|
||||||
args := []interface{}{
|
args := []interface{}{
|
||||||
string(req.CartId),
|
string(req.CartID),
|
||||||
req.Quantity,
|
req.Quantity,
|
||||||
int64(req.TTL.Seconds()),
|
int64(req.TTL.Seconds()),
|
||||||
}
|
}
|
||||||
@@ -112,6 +112,39 @@ func (s *RedisCartReservationService) GetReservationStatus(ctx context.Context,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error) {
|
||||||
|
qtyKey := cartQtyKey(sku, locationID)
|
||||||
|
expKey := cartExpKey(sku, locationID)
|
||||||
|
|
||||||
|
// Get reservation count
|
||||||
|
reservationCount, err := s.client.HLen(ctx, qtyKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get earliest release (min score in sorted set)
|
||||||
|
minScore, err := s.client.ZRangeWithScores(ctx, expKey, 0, 0).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var earliestRelease time.Time
|
||||||
|
if len(minScore) > 0 {
|
||||||
|
earliestRelease = time.Unix(int64(minScore[0].Score), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get remaining items
|
||||||
|
remainingItems, err := s.GetAvailableInventory(ctx, sku, locationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReservationSummary{
|
||||||
|
ReservationCount: reservationCount,
|
||||||
|
EarliestRelease: earliestRelease,
|
||||||
|
RemainingItems: remainingItems,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) {
|
func (s *RedisCartReservationService) GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error) {
|
||||||
keys := []string{
|
keys := []string{
|
||||||
getInventoryKey(sku, locationID),
|
getInventoryKey(sku, locationID),
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ func TestReserveAndRelease(t *testing.T) {
|
|||||||
|
|
||||||
// Reserve
|
// Reserve
|
||||||
req := CartReserveRequest{
|
req := CartReserveRequest{
|
||||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||||
CartId: cartID,
|
CartID: cartID,
|
||||||
Quantity: 2,
|
Quantity: 2,
|
||||||
TTL: 5 * time.Minute,
|
TTL: 5 * time.Minute,
|
||||||
}
|
}
|
||||||
@@ -132,8 +132,8 @@ func TestConcurrentReserves(t *testing.T) {
|
|||||||
go func(cartNum int) {
|
go func(cartNum int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
req := CartReserveRequest{
|
req := CartReserveRequest{
|
||||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||||
CartId: CartID(fmt.Sprintf("cart%d", cartNum)),
|
CartID: CartID(fmt.Sprintf("cart%d", cartNum)),
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
TTL: 5 * time.Minute,
|
TTL: 5 * time.Minute,
|
||||||
}
|
}
|
||||||
@@ -195,8 +195,8 @@ func TestExpiry(t *testing.T) {
|
|||||||
|
|
||||||
// Reserve with short TTL
|
// Reserve with short TTL
|
||||||
req := CartReserveRequest{
|
req := CartReserveRequest{
|
||||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||||
CartId: cartID,
|
CartID: cartID,
|
||||||
Quantity: 2,
|
Quantity: 2,
|
||||||
TTL: 10 * time.Second,
|
TTL: 10 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -239,3 +239,73 @@ func TestExpiry(t *testing.T) {
|
|||||||
t.Errorf("expected available 8, got %d", available)
|
t.Errorf("expected available 8, got %d", available)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetReservationSummary(t *testing.T) {
|
||||||
|
client, mr, err := setupTestRedis()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer mr.Close()
|
||||||
|
|
||||||
|
service, err := NewRedisCartReservationService(client)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sku := SKU("test-item")
|
||||||
|
locationID := LocationID("warehouse1")
|
||||||
|
|
||||||
|
// Set initial inventory
|
||||||
|
err = client.Set(ctx, getInventoryKey(sku, locationID), 10, 0).Err()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve two carts
|
||||||
|
req1 := CartReserveRequest{
|
||||||
|
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||||
|
CartID: "cart1",
|
||||||
|
Quantity: 2,
|
||||||
|
TTL: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
err = service.ReserveForCart(ctx, req1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req2 := CartReserveRequest{
|
||||||
|
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||||
|
CartID: "cart2",
|
||||||
|
Quantity: 1,
|
||||||
|
TTL: 10 * time.Minute,
|
||||||
|
}
|
||||||
|
err = service.ReserveForCart(ctx, req2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get summary
|
||||||
|
summary, err := service.GetReservationSummary(ctx, sku, locationID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary.ReservationCount != 2 {
|
||||||
|
t.Errorf("expected 2 reservations, got %d", summary.ReservationCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary.RemainingItems != 7 {
|
||||||
|
t.Errorf("expected remaining 7, got %d", summary.RemainingItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
if summary.EarliestRelease.IsZero() {
|
||||||
|
t.Error("expected earliest release time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Earliest should be cart1's expiry (5 min)
|
||||||
|
expectedEarliest := time.Now().Add(5 * time.Minute)
|
||||||
|
if summary.EarliestRelease.After(expectedEarliest.Add(1*time.Second)) || summary.EarliestRelease.Before(expectedEarliest.Add(-1*time.Second)) {
|
||||||
|
t.Errorf("earliest release time incorrect: %v", summary.EarliestRelease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (s *RedisInventoryService) GetInventoryBatch(ctx context.Context, refs ...*
|
|||||||
pipe := s.client.Pipeline()
|
pipe := s.client.Pipeline()
|
||||||
cmds := make([]*redis.StringCmd, len(refs))
|
cmds := make([]*redis.StringCmd, len(refs))
|
||||||
for i, ref := range refs {
|
for i, ref := range refs {
|
||||||
key := getInventoryKey(ref.Sku, ref.LocationID)
|
key := getInventoryKey(ref.SKU, ref.LocationID)
|
||||||
cmds[i] = pipe.Get(ctx, key)
|
cmds[i] = pipe.Get(ctx, key)
|
||||||
}
|
}
|
||||||
_, err := pipe.Exec(ctx)
|
_, err := pipe.Exec(ctx)
|
||||||
@@ -139,7 +139,7 @@ func makeKeysAndArgs(req ...ReserveRequest) ([]string, []string) {
|
|||||||
if r.Quantity <= 0 {
|
if r.Quantity <= 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
keys[i] = getInventoryKey(r.Sku, r.LocationID)
|
keys[i] = getInventoryKey(r.SKU, r.LocationID)
|
||||||
args[i] = strconv.Itoa(int(r.Quantity))
|
args[i] = strconv.Itoa(int(r.Quantity))
|
||||||
}
|
}
|
||||||
return keys, args
|
return keys, args
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type InventoryItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InventoryReference struct {
|
type InventoryReference struct {
|
||||||
Sku SKU
|
SKU SKU
|
||||||
LocationID LocationID
|
LocationID LocationID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ type CartID string
|
|||||||
|
|
||||||
type CartReserveRequest struct {
|
type CartReserveRequest struct {
|
||||||
*InventoryReference
|
*InventoryReference
|
||||||
CartId CartID
|
CartID CartID
|
||||||
Quantity uint32
|
Quantity uint32
|
||||||
TTL time.Duration
|
TTL time.Duration
|
||||||
}
|
}
|
||||||
@@ -58,9 +58,16 @@ type CartReservationService interface {
|
|||||||
GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error)
|
GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, error)
|
||||||
GetReservationExpiry(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (time.Time, error)
|
GetReservationExpiry(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (time.Time, error)
|
||||||
GetReservationStatus(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (*ReservationStatus, error)
|
GetReservationStatus(ctx context.Context, sku SKU, locationID LocationID, cartID CartID) (*ReservationStatus, error)
|
||||||
|
GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReservationStatus struct {
|
type ReservationStatus struct {
|
||||||
Active bool
|
Active bool
|
||||||
RemainingTime time.Duration
|
RemainingTime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReservationSummary struct {
|
||||||
|
ReservationCount int64 `json:"reservation_count"`
|
||||||
|
EarliestRelease time.Time `json:"earliest_release"`
|
||||||
|
RemainingItems int64 `json:"remaining_items"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user