# EXPIRATION TRACKING — TESTS.md

## Unit Tests

### LotService

```
EXP-UT-001: createLot with expiry product and valid date → succeeds
EXP-UT-002: createLot with expiry product and missing date → throws MissingExpirationDateError (422)
EXP-UT-003: createLot with past date → throws PastExpirationDateError (422)
EXP-UT-004: createLot with non-expiry product and no date → succeeds (null expiry allowed)
EXP-UT-005: allocateLots FEFO order — earliest expiry first
EXP-UT-006: allocateLots partial — consumes only available quantity from earliest
EXP-UT-007: allocateLots insufficient → throws InsufficientStockError
EXP-UT-008: allocateLots across multiple lots (quantity exceeds first lot)
EXP-UT-009: getAvailableStock excludes expired lots
EXP-UT-010: getAvailableStock excludes lots with expiration_date = today (near_expiry status)
EXP-UT-011: updateLot quantity to 0 → still exists (not auto-deleted)
EXP-UT-012: deleteLot removes lot and logs audit
```

### InventoryService (extended)

```
EXP-UT-020: checkAvailability non-expiry product → existing variant-level sum
EXP-UT-021: checkAvailability expiry product with valid lots → sum of non-expired quantities
EXP-UT-022: checkAvailability expiry product with only expired lots → 0 available
EXP-UT-023: reserve expiry product → FEFO allocation from non-expired lots
EXP-UT-024: reserve non-expiry product → existing behavior unchanged
EXP-UT-025: release returns reserved quantity to FEFO lots
EXP-UT-026: feature flag OFF → delegates to existing variant-level path
```

### Product (expiration field)

```
EXP-UT-030: product.create sets expiration_tracking_enabled to false by default
EXP-UT-031: product.patch can update expiration_tracking_enabled
EXP-UT-032: product.findById returns expiration_tracking_enabled
```

## Integration Tests

### Database

```
EXP-IT-001: inventory_lots table enforces CHECK expiration_date >= CURRENT_DATE
EXP-IT-002: inventory_lots FK to catalog_variants ON DELETE CASCADE
EXP-IT-003: inventory_movements allows new operation types
EXP-IT-004: catalog_products expiration_tracking_enabled defaults to false
```

### API Routes

```
EXP-IT-010: POST /inventory/lots with valid body → 201 + lot object
EXP-IT-011: POST /inventory/lots missing expiration_date on expiry product → 422
EXP-IT-012: POST /inventory/lots with past expiration_date → 422
EXP-IT-013: PATCH /inventory/lots/:id updates quantity
EXP-IT-014: DELETE /inventory/lots/:id → 200 + lot removed
EXP-IT-015: GET /inventory/lots?variant_id=X → returns lots for variant
EXP-IT-016: GET /inventory/lots?filter=expiring → returns only near-expiry lots
EXP-IT-017: GET /inventory/lots?filter=expired → returns expired lots
EXP-IT-018: GET /inventory/lots?filter=no-expiry → returns null-expiry lots
EXP-IT-019: All lot endpoints require admin auth → 401 without session
```

### Full Flow

```
EXP-IT-030: Enable expiry on product → create lots → check availability → available
EXP-IT-031: Enable expiry on product → no lots created → availability = 0
EXP-IT-032: Reserve from FEFO lots → earliest expires first → correct lot decremented
EXP-IT-033: Expired lot never contributes to availability
EXP-IT-034: After feature flag OFF → reverts to variant-level behavior
```

## Admin Tests

```
EXP-ADM-UT-001: ProductEditor — enabling expiry toggle shows lot management UI
EXP-ADM-UT-002: ProductEditor — disabling expiry hides lot management UI
EXP-ADM-UT-003: Lot table — correct VALID/NEAR_EXPIRY/EXPIRED status badges
EXP-ADM-UT-004: Create lot form — expiry date required for tracking products
EXP-ADM-UT-005: Create lot form — expiry date optional for non-tracking products
EXP-ADM-UT-006: Inventory filter tabs — each shows correct subset
```

## Checkout Regression Tests

```
EXP-E2E-001: Checkout — normal non-expiry product → completes successfully
EXP-E2E-002: Checkout — expiry product with no valid lots → unavailable
EXP-E2E-003: Checkout — expiry product with valid lots → reserves correctly
EXP-E2E-004: Checkout — FEFO: oldest expiry lot decremented first
EXP-E2E-005: Checkout — cart with mixed expiry/non-expiry products → both work
EXP-E2E-006: Checkout — reserve then release → lots restored to correct quantities
```

## Given/When/Then Acceptance Criteria

```
GIVEN a product with expiration_tracking_enabled = false
WHEN inventory is received for a variant of that product
THEN expiration_date is NOT required
AND the variant uses existing variant-level stock
AND checkout works as before

GIVEN a product with expiration_tracking_enabled = true
WHEN inventory is received without an expiration date
THEN the backend returns 422 MISSING_EXPIRATION_DATE

GIVEN a product with expiration_tracking_enabled = true
WHEN a lot is received with an expiration date in the past
THEN the backend returns 422 PAST_EXPIRATION_DATE

GIVEN an inventory lot whose expiration_date is in the past
WHEN sellable inventory is calculated
THEN that lot contributes zero sellable units
AND the lot remains visible in admin
AND the lot can be manually adjusted/deleted by admin

GIVEN a product with expiration_tracking_enabled = true and multiple lots
WHEN a customer reserves units
THEN lots are allocated in FEFO order (earliest expiration first)
AND when the earliest lot is exhausted, allocation continues to the next

GIVEN the feature flag expiration_tracking = false
WHEN any existing checkout flow runs
THEN behavior is byte-for-byte identical to pre-migration
```
