# EXPIRATION TRACKING — TASKS.md

## Backend

### EXP-BE-001
**ID**: EXP-BE-001
**Title**: Product expiration_tracking_enabled column
**Goal**: Add boolean column to catalog_products
**Why**: Products need to declare whether they require expiration tracking
**Dependencies**: None
**Applications**: Backend
**Modules**: catalog_products table, Product domain, ProductRepository
**Database impact**: ALTER TABLE catalog_products ADD COLUMN expiration_tracking_enabled boolean NOT NULL DEFAULT false
**API contracts**: GET /products/:id returns field; PATCH /products/:id accepts field
**Permissions**: admin
**Implementation**: Add to NewProduct interface, ProductPatch type, pg-product-repository
**Tests**: Unit — default false; Integration — column exists with correct default
**Migration**: See MIGRATION.md Phase 1
**Expected blast radius**: Low — only affects new product queries
**Definition of Done**: Column exists, defaults to false, persists on create/update

### EXP-BE-002
**ID**: EXP-BE-002
**Title**: InventoryLot domain and repository
**Goal**: New InventoryLot entity and InventoryLotRepository port + PgInventoryLotRepository
**Why**: Core data model for lot-level stock
**Dependencies**: EXP-BE-001
**Applications**: Backend
**Modules**: inventory/domain, inventory/infrastructure
**Database impact**: CREATE TABLE inventory_lots (see DESIGN.md)
**API contracts**: None yet (repository only)
**Permissions**: N/A
**Implementation**: InventoryLot interface, CreateLotCommand, InventoryLotRepository port, PgInventoryLotRepository
**Tests**: Unit — CRUD operations; Integration — table constraints, FK
**Expected blast radius**: Low — new table, no existing data touched
**Definition of Done**: Lot CRUD works, FK to catalog_variants enforced, CHECK constraint on expiration_date

### EXP-BE-003
**ID**: EXP-BE-003
**Title**: LotService — create, update, delete, FEFO allocation
**Goal**: Domain service handling lot business rules
**Why**: Encapsulates expiration validation and FEFO logic
**Dependencies**: EXP-BE-002
**Applications**: Backend
**Modules**: inventory/application
**API contracts**: None (domain service)
**Permissions**: N/A
**Implementation**: LotService class with createLot (validates expiry required), allocateLots (FEFO), adjustQuantity
**Tests**: Unit — missing expiry rejected; past expiry rejected; FEFO order; partial lot allocation
**Expected blast radius**: Low
**Definition of Done**: LotService methods have correct business rules; FEFO allocates from earliest expiry first

### EXP-BE-004
**ID**: EXP-BE-004
**Title**: InventoryLots API routes
**Goal**: CRUD endpoints for lots + availability query with filter
**Why**: Admin needs to manage lots; frontend needs to display them
**Dependencies**: EXP-BE-003
**Applications**: Backend
**Modules**: inventory/api
**API contracts**: GET /inventory/lots, POST /inventory/lots, PATCH /inventory/lots/:id, DELETE /inventory/lots/:id (all admin)
**Permissions**: admin role required
**Implementation**: New route registrations in inventory module; serialize LotService results
**Tests**: Integration — CRUD round-trip; filter=expiring|expired|all
**Expected blast radius**: Low — new routes
**Definition of Done**: All 5 routes respond correctly; filter parameters work; auth enforced

### EXP-BE-005
**ID**: EXP-BE-005
**Title**: InventoryService — integrate LotService for expiration products
**Goal**: Extend existing InventoryService to delegate to LotService when expiry is enabled
**Why**: Preserve existing InventoryServicePort contract while adding expiration support
**Dependencies**: EXP-BE-001, EXP-BE-003
**Applications**: Backend, Checkout
**Modules**: inventory/application, checkout
**API contracts**: Existing InventoryServicePort contract unchanged
**Permissions**: N/A
**Implementation**: In InventoryService, check product.expiration_tracking_enabled; if true, use LotService.getAvailableStock and LotService.allocateLots; feature flag gates behavior
**Tests**: Unit — delegation to LotService for expiry products; existing path for non-expiry products
**Expected blast radius**: Checkout uses InventoryServicePort — must not break
**Definition of Done**: Checkout reserve/confirm still works for both expiry and non-expiry products; FEFO used for expiry products

### EXP-BE-006
**ID**: EXP-BE-006
**Title**: Extend inventory_movements with lot operation types
**Goal**: Track lot_create, lot_adjust, lot_delete in movement audit log
**Why**: Full auditability of lot changes
**Dependencies**: EXP-BE-002
**Applications**: Backend
**Modules**: inventory/infrastructure, security
**Database impact**: ALTER TABLE inventory_movements — new operation types + optional lot_id FK
**API contracts**: Movement audit reflects lot operations
**Permissions**: N/A
**Implementation**: Add operation types in pg-inventory-repository insertMovement calls
**Tests**: Integration — movements logged with correct operation type
**Expected blast radius**: Low
**Definition of Done**: Lot mutations produce audit entries

## Database

### EXP-DB-001
**ID**: EXP-DB-001
**Title**: Run expiration tracking migrations
**Goal**: Apply all DB changes from MIGRATION.md Phase 1
**Why**: Infrastructure for lot model
**Dependencies**: EXP-BE-001 (column on catalog_products), EXP-BE-002 (lots table)
**Applications**: Database
**Modules**: N/A
**Database impact**: See MIGRATION.md Phase 1
**API contracts**: N/A
**Permissions**: DBA
**Implementation**: Add migration file or run raw SQL against dev DB; apply via docker-compose migration pipeline
**Tests**: Verify schema after migration
**Expected blast radius**: Low — additive changes
**Definition of Done**: Migration runs without error; new columns/tables exist with correct constraints

## Admin

### EXP-ADM-001
**ID**: EXP-ADM-001
**Title**: Product Editor — expiration tracking toggle
**Goal**: Add checkbox to General tab: "Track expiration dates"
**Why**: Admin configures per-product policy
**Dependencies**: EXP-BE-001
**Applications**: Admin
**Modules**: ProductEditor, GeneralSection
**Database impact**: None (uses EXP-BE-001)
**API contracts**: PATCH /catalog/products/:id
**Permissions**: products.write
**Implementation**: Add toggle to GeneralSection; saves { expiration_tracking_enabled: boolean } on save
**Tests**: Toggle saves correctly; shows/hides expiration UI based on state
**Expected blast radius**: Low
**Definition of Done**: Admin can enable/disable expiry tracking per product; toggle persists

### EXP-ADM-002
**ID**: EXP-ADM-002
**Title**: Inventory — lot-level stock view
**Goal**: Show lots table for expiry-tracking products in inventory page
**Why**: Operational visibility into expiration state
**Dependencies**: EXP-BE-004
**Applications**: Admin
**Modules**: InventorySection, inventory page
**Database impact**: None
**API contracts**: GET /inventory/lots?variant_id=X
**Permissions**: inventory.read
**Implementation**: Extend InventorySection to show lots when product has expiry enabled; compute status (VALID/NEAR_EXPIRY/EXPIRED) client-side from FLAG_EXPIRY_WARNING_DAYS
**Tests**: Lot table renders correctly; status computed from dates
**Expected blast radius**: Low
**Definition of Done**: Lots displayed with correct quantity, date, and status badge

### EXP-ADM-003
**ID**: EXP-ADM-003
**Title**: Inventory — lot create/edit/delete
**Goal**: Inline lot management in inventory section
**Why**: Admin must be able to add/update/remove lots
**Dependencies**: EXP-BE-004
**Applications**: Admin
**Modules**: InventorySection
**Database impact**: None
**API contracts**: POST/PATCH/DELETE /inventory/lots/:id
**Permissions**: inventory.write
**Implementation**: Add lot form (quantity, expiration date); inline edit on lot row; delete confirmation
**Tests**: Create lot with required expiry date; edit quantity; delete lot; 422 shown for missing expiry
**Expected blast radius**: Low
**Definition of Done**: Admin can fully manage lots; validation errors shown correctly

### EXP-ADM-004
**ID**: EXP-ADM-004
**Title**: Inventory filters — expiring, expired, all, no-expiry
**Goal**: Filter inventory page by expiration status
**Why**: Operational efficiency for stock management
**Dependencies**: EXP-BE-004
**Applications**: Admin
**Modules**: inventory page
**Database impact**: None
**API contracts**: GET /inventory/lots?filter=expiring|expired|all|no-expiry
**Permissions**: inventory.read
**Implementation**: Add filter tabs/dropdown to inventory page; calls API with filter param
**Tests**: Each filter returns correct lot subset
**Expected blast radius**: Low
**Definition of Done**: Filters work; filter state reflected in URL or UI

## QA

### EXP-QA-001
**ID**: EXP-QA-001
**Title**: Expiration tracking regression tests
**Goal**: Ensure existing checkout flow is unbroken
**Why**: No regressions on existing products
**Dependencies**: EXP-BE-005, EXP-DB-001
**Applications**: QA
**Modules**: E2E tests
**Tests**:
- Normal product (non-expiry) still checks out correctly
- Expiry product with no lots: unavailable
- Expiry product with valid lot: available and reservable
- FEFO: earliest expiry lot consumed first
- Expired lot: contributes zero sellable units
**Expected blast radius**: N/A
**Definition of Done**: All regression tests pass

### EXP-QA-002
**ID**: EXP-QA-002
**Title**: Expiration tracking unit/integration tests
**Goal**: Comprehensive test coverage for all new domain code
**Why**: Business rules must be correct
**Dependencies**: EXP-BE-003, EXP-BE-004, EXP-BE-005
**Applications**: QA
**Modules**: Backend test suite
**Tests**: See TESTS.md
**Expected blast radius**: N/A
**Definition of Done**: 100% pass rate on expiration-specific tests

---

## Task Summary Table

| Task | Layer | Feature | Depends On | Risk | Parallel |
|------|-------|---------|-----------|------|---------|
| EXP-BE-001 | Backend | Product expiry column | — | Low | * |
| EXP-BE-002 | Backend | Lot model + repository | — | Low | * |
| EXP-BE-003 | Backend | LotService domain | EXP-BE-002 | Low | * |
| EXP-BE-004 | Backend | Lot API routes | EXP-BE-003 | Low | * |
| EXP-BE-005 | Backend | InventoryService + LotService | EXP-BE-001, EXP-BE-003 | Medium | * |
| EXP-BE-006 | Backend | Movement audit for lots | EXP-BE-002 | Low | EXP-BE-004 |
| EXP-DB-001 | DB | Run migrations | EXP-BE-001, EXP-BE-002 | Low | * |
| EXP-ADM-001 | Admin | Product expiry toggle | EXP-BE-001 | Low | * |
| EXP-ADM-002 | Admin | Lot stock view | EXP-BE-004 | Low | * |
| EXP-ADM-003 | Admin | Lot CRUD | EXP-BE-004 | Low | * |
| EXP-ADM-004 | Admin | Expiry filters | EXP-BE-004 | Low | * |
| EXP-QA-001 | QA | Checkout regression | EXP-BE-005, EXP-DB-001 | Medium | After backend |
| EXP-QA-002 | QA | Domain unit tests | All BE tasks | Low | With backend |

**Parallel group**: EXP-BE-001 and EXP-BE-002 can run in parallel. BE-003 depends on BE-002. BE-004 and BE-006 depend on BE-003. BE-005 depends on BE-001 + BE-003.

**Recommended order**:
1. EXP-BE-001 + EXP-BE-002 (parallel, no dependencies)
2. EXP-BE-003 (depends on BE-002)
3. EXP-BE-004 + EXP-BE-006 (depend on BE-003, parallel)
4. EXP-DB-001 (run after BE-001 + BE-002 code is deployed)
5. EXP-BE-005 (depends on BE-001 + BE-003)
6. EXP-ADM-001 (depends on BE-001)
7. EXP-ADM-002 + EXP-ADM-003 + EXP-ADM-004 (depend on BE-004, parallel)
8. EXP-QA-001 + EXP-QA-002 (after all backend + admin)

**High-risk tasks**: EXP-BE-005 (changes InventoryService contract internal behavior, affects checkout — thorough regression testing required).

**MVP boundary**: EXP-BE-001 through EXP-BE-005 + EXP-DB-001 + EXP-ADM-001 through EXP-ADM-004. EXP-BE-006 (audit) is low priority for MVP. EXP-QA-002 is bundled with implementation. EXP-QA-001 is blocking go-live.
