i
This commit is contained in:
1 parent
4dc1d7bc1c
commit
78a7d35570
2 files changed
+88
No files matched your search
@@ -3,6 +3,7 @@ package inventory
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -151,6 +152,75 @@ func (s *RedisCartReservationService) GetReservationSummary(ctx context.Context,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReservationDetail returns the full per-cart reservation detail for a
|
||||||
|
// SKU+location. Unlike GetReservationSummary (which returns only aggregates),
|
||||||
|
// this method fetches every active (non-expired) row from Redis and returns
|
||||||
|
// a ReservationDetailResponse containing the adjusted summary + per-cart lines.
|
||||||
|
// Expired rows are filtered out so the UI only sees live holds. The two Redis
|
||||||
|
// fetches (HGetAll + ZRangeWithScores) run sequentially — for an infrequent
|
||||||
|
// admin endpoint the goroutine+channel overhead is not worth the latency win.
|
||||||
|
func (s *RedisCartReservationService) GetReservationDetail(ctx context.Context, sku SKU, locationID LocationID) (*ReservationDetailResponse, error) {
|
||||||
|
qtyKey := cartQtyKey(sku, locationID)
|
||||||
|
expKey := cartExpKey(sku, locationID)
|
||||||
|
|
||||||
|
// Fetch all cart→qty pairs.
|
||||||
|
qtyMap, err := s.client.HGetAll(ctx, qtyKey).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all cart→expiry pairs.
|
||||||
|
expPairs, err := s.client.ZRangeWithScores(ctx, expKey, 0, -1).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index expiry by cartId.
|
||||||
|
expMap := make(map[string]time.Time, len(expPairs))
|
||||||
|
for _, z := range expPairs {
|
||||||
|
key, ok := z.Member.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expMap[key] = time.Unix(int64(z.Score), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the line list, skipping expired entries.
|
||||||
|
now := time.Now()
|
||||||
|
var lines []ReservationDetailLine
|
||||||
|
var earliestRelease time.Time
|
||||||
|
for cartID, qtyStr := range qtyMap {
|
||||||
|
expiresAt, hasExp := expMap[cartID]
|
||||||
|
if !hasExp || expiresAt.Before(now) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
qty, _ := strconv.ParseUint(qtyStr, 10, 32)
|
||||||
|
|
||||||
|
lines = append(lines, ReservationDetailLine{
|
||||||
|
CartID: cartID,
|
||||||
|
Quantity: uint32(qty),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
})
|
||||||
|
if earliestRelease.IsZero() || expiresAt.Before(earliestRelease) {
|
||||||
|
earliestRelease = expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingItems, err := s.Available(ctx, sku, locationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ReservationDetailResponse{
|
||||||
|
Summary: &ReservationSummary{
|
||||||
|
ReservationCount: int64(len(lines)),
|
||||||
|
EarliestRelease: earliestRelease,
|
||||||
|
RemainingItems: remainingItems,
|
||||||
|
},
|
||||||
|
Lines: lines,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Available satisfies inventory.ReservationPolicy (platform/inventory).
|
// Available satisfies inventory.ReservationPolicy (platform/inventory).
|
||||||
// Method renamed from GetAvailableInventory as part of cut-30.x: land the
|
// Method renamed from GetAvailableInventory as part of cut-30.x: land the
|
||||||
// ReservationPolicy interface seam — see docs/inventory-shape.md.
|
// ReservationPolicy interface seam — see docs/inventory-shape.md.
|
||||||
|
|||||||
@@ -35,3 +35,21 @@ type ReservationSummary struct {
|
|||||||
EarliestRelease time.Time `json:"earliest_release"`
|
EarliestRelease time.Time `json:"earliest_release"`
|
||||||
RemainingItems int64 `json:"remaining_items"`
|
RemainingItems int64 `json:"remaining_items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReservationDetailLine is one row of the per-cart reservation detail,
|
||||||
|
// returned by GetReservationDetail when the HTTP caller sets ?detail=true.
|
||||||
|
// Expired reservations (expires_at in the past) are filtered out — the
|
||||||
|
// list shows only active holds.
|
||||||
|
type ReservationDetailLine struct {
|
||||||
|
CartID string `json:"cart_id"`
|
||||||
|
Quantity uint32 `json:"quantity"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationDetailResponse is the wire shape when ?detail=true on
|
||||||
|
// GET /reservations/{sku}/{locationId}. Carries the aggregate summary
|
||||||
|
// alongside the per-cart lines so the UI can show both in one fetch.
|
||||||
|
type ReservationDetailResponse struct {
|
||||||
|
Summary *ReservationSummary `json:"summary"`
|
||||||
|
Lines []ReservationDetailLine `json:"lines"`
|
||||||
|
}
|
||||||
Reference in new issue
Block a user