more fancy features
This commit is contained in:
@@ -46,13 +46,13 @@ func totalReservedKey(sku SKU, locationID LocationID) string {
|
||||
|
||||
func (s *RedisCartReservationService) ReserveForCart(ctx context.Context, req CartReserveRequest) error {
|
||||
keys := []string{
|
||||
getInventoryKey(req.Sku, req.LocationID),
|
||||
cartQtyKey(req.Sku, req.LocationID),
|
||||
cartExpKey(req.Sku, req.LocationID),
|
||||
totalReservedKey(req.Sku, req.LocationID),
|
||||
getInventoryKey(req.SKU, req.LocationID),
|
||||
cartQtyKey(req.SKU, req.LocationID),
|
||||
cartExpKey(req.SKU, req.LocationID),
|
||||
totalReservedKey(req.SKU, req.LocationID),
|
||||
}
|
||||
args := []interface{}{
|
||||
string(req.CartId),
|
||||
string(req.CartID),
|
||||
req.Quantity,
|
||||
int64(req.TTL.Seconds()),
|
||||
}
|
||||
@@ -112,6 +112,39 @@ func (s *RedisCartReservationService) GetReservationStatus(ctx context.Context,
|
||||
}, 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) {
|
||||
keys := []string{
|
||||
getInventoryKey(sku, locationID),
|
||||
|
||||
@@ -49,8 +49,8 @@ func TestReserveAndRelease(t *testing.T) {
|
||||
|
||||
// Reserve
|
||||
req := CartReserveRequest{
|
||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
||||
CartId: cartID,
|
||||
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||
CartID: cartID,
|
||||
Quantity: 2,
|
||||
TTL: 5 * time.Minute,
|
||||
}
|
||||
@@ -132,8 +132,8 @@ func TestConcurrentReserves(t *testing.T) {
|
||||
go func(cartNum int) {
|
||||
defer wg.Done()
|
||||
req := CartReserveRequest{
|
||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
||||
CartId: CartID(fmt.Sprintf("cart%d", cartNum)),
|
||||
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||
CartID: CartID(fmt.Sprintf("cart%d", cartNum)),
|
||||
Quantity: 1,
|
||||
TTL: 5 * time.Minute,
|
||||
}
|
||||
@@ -195,8 +195,8 @@ func TestExpiry(t *testing.T) {
|
||||
|
||||
// Reserve with short TTL
|
||||
req := CartReserveRequest{
|
||||
InventoryReference: &InventoryReference{Sku: sku, LocationID: locationID},
|
||||
CartId: cartID,
|
||||
InventoryReference: &InventoryReference{SKU: sku, LocationID: locationID},
|
||||
CartID: cartID,
|
||||
Quantity: 2,
|
||||
TTL: 10 * time.Second,
|
||||
}
|
||||
@@ -239,3 +239,73 @@ func TestExpiry(t *testing.T) {
|
||||
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()
|
||||
cmds := make([]*redis.StringCmd, len(refs))
|
||||
for i, ref := range refs {
|
||||
key := getInventoryKey(ref.Sku, ref.LocationID)
|
||||
key := getInventoryKey(ref.SKU, ref.LocationID)
|
||||
cmds[i] = pipe.Get(ctx, key)
|
||||
}
|
||||
_, err := pipe.Exec(ctx)
|
||||
@@ -139,7 +139,7 @@ func makeKeysAndArgs(req ...ReserveRequest) ([]string, []string) {
|
||||
if r.Quantity <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
keys[i] = getInventoryKey(r.Sku, r.LocationID)
|
||||
keys[i] = getInventoryKey(r.SKU, r.LocationID)
|
||||
args[i] = strconv.Itoa(int(r.Quantity))
|
||||
}
|
||||
return keys, args
|
||||
|
||||
@@ -16,7 +16,7 @@ type InventoryItem struct {
|
||||
}
|
||||
|
||||
type InventoryReference struct {
|
||||
Sku SKU
|
||||
SKU SKU
|
||||
LocationID LocationID
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ type CartID string
|
||||
|
||||
type CartReserveRequest struct {
|
||||
*InventoryReference
|
||||
CartId CartID
|
||||
CartID CartID
|
||||
Quantity uint32
|
||||
TTL time.Duration
|
||||
}
|
||||
@@ -58,9 +58,16 @@ type CartReservationService interface {
|
||||
GetAvailableInventory(ctx context.Context, sku SKU, locationID LocationID) (int64, 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)
|
||||
GetReservationSummary(ctx context.Context, sku SKU, locationID LocationID) (*ReservationSummary, error)
|
||||
}
|
||||
|
||||
type ReservationStatus struct {
|
||||
Active bool
|
||||
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