# 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 ```