# ADMIN BULK UPDATE — TASKS.md

## Backend

### BULK-BE-001
**ID**: BULK-BE-001
**Title**: BulkOperation domain types
**Goal**: Define all types for bulk operations
**Why**: Shared contracts for handlers, orchestrator, and API
**Dependencies**: None
**Applications**: Backend
**Modules**: bulk/domain/bulk-operation.ts
**Database impact**: None
**API contracts**: Shapes of request/response objects
**Permissions**: N/A
**Implementation**: Define BulkOperationType enum, BulkOperationParams union, PreviewItem, PreviewResult, ExecuteResult interfaces; ValidationResult
**Tests**: Type tests (TypeScript compilation)
**Expected blast radius**: Low
**Definition of Done**: All types defined; TypeScript compiles without errors

### BULK-BE-002
**ID**: BULK-BE-002
**Title**: BulkOperationHandler port + handler registry
**Goal**: Define the extension interface for bulk operations
**Why**: Allows adding future operations without touching core orchestrator
**Dependencies**: BULK-BE-001
**Applications**: Backend
**Modules**: bulk/domain/ports.ts
**Database impact**: None
**API contracts**: BulkOperationHandler interface
**Permissions**: N/A
**Implementation**: Define `BulkOperationHandler` interface; create `HANDLERS` registry map; export from bulk/index.ts
**Tests**: Handler lookup by type works; unknown type returns undefined
**Expected blast radius**: Low
**Definition of Done**: Interface exists; registry is extensible; adding a handler requires only adding to registry

### BULK-BE-003
**ID**: BULK-BE-003
**Title**: Price bulk operation handlers
**Goal**: Implement SET, INCREASE_PERCENT, DECREASE_PERCENT, INCREASE_FIXED, DECREASE_FIXED
**Why**: Initial price operations
**Dependencies**: BULK-BE-002
**Applications**: Backend
**Modules**: bulk/application/handlers/price-handlers.ts
**Database impact**: None
**API contracts**: None (domain)
**Permissions**: N/A
**Implementation**: Five handler classes; each implements validate(), preview(), buildPatch(); rounding uses Math.round(); VAT is stored as net price
**Tests**:
- BULK-BE-UT-001: INCREASE_PERCENT 5% on 350 cents → 368 cents
- BULK-BE-UT-002: DECREASE_PERCENT 5% on 350 cents → 333 cents
- BULK-BE-UT-003: INCREASE_FIXED 50 on 350 cents → 400 cents
- BULK-BE-UT-004: DECREASE_FIXED 50 on 350 cents → 300 cents
- BULK-BE-UT-005: DECREASE_FIXED below 0 → floor at 0
- BULK-BE-UT-006: SET_PRICE 500 → 500
- BULK-BE-UT-007: invalid percent (0, negative, >1000) → ValidationResult.valid=false
- BULK-BE-UT-008: missing VAT rate → ValidationResult.valid=false
- BULK-BE-UT-009: product without price → PreviewItem.status=failed, error=PRICE_NOT_FOUND
**Expected blast radius**: Low
**Definition of Done**: All five handlers produce correct price calculations; invalid params rejected at validate()

### BULK-BE-004
**ID**: BULK-BE-004
**Title**: Category bulk operation handlers
**Goal**: Implement ADD_CATEGORY, REMOVE_CATEGORY, REPLACE_CATEGORIES
**Why**: Initial category operations
**Dependencies**: BULK-BE-002
**Applications**: Backend
**Modules**: bulk/application/handlers/category-handlers.ts
**Database impact**: None
**API contracts**: None (domain)
**Permissions**: N/A
**Implementation**: Three handler classes; ADD deduplicates; REMOVE filters; REPLACE replaces entirely; validates categoryId exists (optional — can be deferred)
**Tests**:
- BULK-BE-UT-010: ADD_CATEGORY to product with 2 categories → 3 categories
- BULK-BE-UT-011: ADD_CATEGORY with duplicate → no-op (deduplicated)
- BULK-BE-UT-012: REMOVE_CATEGORY from product with 3 categories → 2 categories
- BULK-BE-UT-013: REMOVE_CATEGORY not present → no change
- BULK-BE-UT-014: REPLACE_CATEGORIES with 2 new categories → exactly those 2
- BULK-BE-UT-015: ADD without categoryId → ValidationResult.valid=false
**Expected blast radius**: Low
**Definition of Done**: All three handlers produce correct category patches; validation rejects invalid input

### BULK-BE-005
**ID**: BULK-BE-005
**Title**: BulkService orchestrator
**Goal**: Orchestrate preview + execute; integrate with audit
**Why**: Core business logic tying handlers, repository, and audit together
**Dependencies**: BULK-BE-001, BULK-BE-002, BULK-BE-003, BULK-BE-004
**Applications**: Backend
**Modules**: bulk/application/bulk-service.ts
**Database impact**: None
**API contracts**: None (service)
**Permissions**: N/A
**Implementation**: BulkService with preview() and execute(); execute() uses transaction; logs audit entry on execution; rollback on error; generate operationId
**Tests**:
- BULK-BE-UT-020: preview() calls handler.validate() and handler.preview() for each product
- BULK-BE-UT-021: execute() calls handler.buildPatch() for each valid product
- BULK-BE-UT-022: execute() logs one audit entry per bulk operation
- BULK-BE-UT-023: execute() rollback on error → no product modified
- BULK-BE-UT-024: unknown operation type → AppError(400)
**Expected blast radius**: Medium — changes persistence
**Definition of Done**: BulkService correctly orchestrates all operations; audit entry is complete; transaction is atomic

### BULK-BE-006
**ID**: BULK-BE-006
**Title**: BulkProductRepository — read products with pricing in batch
**Goal**: Efficiently load product+price data for bulk operations
**Why**: Avoids N queries for N products
**Dependencies**: BULK-BE-001
**Applications**: Backend
**Modules**: bulk/infrastructure/pg-bulk-repository.ts
**Database impact**: None
**API contracts**: None
**Permissions**: N/A
**Implementation**: Single SQL query to JOIN catalog_products + pricing_variants; returns ProductWithPrice[] (internal type); handles products with no price gracefully
**Tests**: BULK-BE-IT-001: batch load of 20 products returns correct data; BULK-BE-IT-002: product without price returns null price field
**Expected blast radius**: Low
**Definition of Done**: Products loaded in one query; missing price handled gracefully

### BULK-BE-007
**ID**: BULK-BE-007
**Title**: Bulk API routes — preview and execute
**Goal**: Expose bulk operations via REST API
**Why**: Admin UI needs these endpoints
**Dependencies**: BULK-BE-005, BULK-BE-006
**Applications**: Backend
**Modules**: bulk/api/bulk.routes.ts
**Database impact**: None (read for preview; write for execute)
**API contracts**: POST /admin/bulk/preview, POST /admin/bulk/execute
**Permissions**: admin role required on both
**Implementation**: Register routes in build-app.ts; parse request body; call BulkService; serialize response; handle errors with AppError
**Tests**:
- BULK-BE-IT-010: POST /admin/bulk/preview with valid INCREASE_PERCENT → 200 + PreviewResult
- BULK-BE-IT-011: POST /admin/bulk/preview with invalid params → 422
- BULK-BE-IT-012: POST /admin/bulk/preview without auth → 401
- BULK-BE-IT-013: POST /admin/bulk/execute with valid params → 200 + ExecuteResult
- BULK-BE-IT-014: POST /admin/bulk/execute → applies changes to DB; products updated
- BULK-BE-IT-015: audit log entry created after execute
**Expected blast radius**: Low — new routes
**Definition of Done**: Both routes respond correctly; auth enforced; changes persisted

### BULK-BE-008
**ID**: BULK-BE-008
**Title**: Wire bulk module into build-app.ts
**Goal**: Register bulk routes with Fastify
**Why**: Module must be mounted in the application
**Dependencies**: BULK-BE-007
**Applications**: Backend
**Modules**: app/build-app.ts
**Database impact**: None
**API contracts**: None
**Permissions**: N/A
**Implementation**: Import registerBulkRoutes; call in build-app.ts alongside other modules; pass pool, authenticate, audit
**Tests**: Integration test that /admin/bulk/preview and /admin/bulk/execute respond
**Expected blast radius**: Low
**Definition of Done**: Bulk routes accessible at /admin/bulk/*

## Admin

### BULK-ADM-001
**ID**: BULK-ADM-001
**Title**: Bulk Update page shell + product selection
**Goal**: Product selection step of the wizard
**Why**: Admin needs to pick which products to update
**Dependencies**: BULK-BE-007
**Applications**: Admin
**Modules**: apps/admin/src/app/(dashboard)/bulk-update/page.tsx
**Database impact**: None
**API contracts**: GET /catalog/products (existing)
**Permissions**: products.bulk_update
**Implementation**: Full-width page with product table; checkbox column; search/filter bar; selected count badge; "Continue to Configure" button
**Tests**: Products load; checkboxes select/deselect; selected count accurate; filter narrows selection
**Expected blast radius**: Low
**Definition of Done**: Admin can select products and see selection count

### BULK-ADM-002
**ID**: BULK-ADM-002
**Title**: Operation selector + configuration step
**Goal**: Choose and configure the bulk operation type
**Why**: Second step of the wizard
**Dependencies**: BULK-ADM-001
**Applications**: Admin
**Modules**: apps/admin/src/app/(dashboard)/bulk-update/page.tsx
**Database impact**: None
**API contracts**: None yet
**Permissions**: products.bulk_update
**Implementation**: Two cards: Price / Categories; selecting Price shows sub-options (SET, +%, -%, +FIXED, -FIXED) with parameter inputs; selecting Categories shows (ADD, REMOVE, REPLACE) with category picker; Back button returns to selection
**Tests**: Selecting each operation shows correct sub-options; invalid input shows error state
**Expected blast radius**: Low
**Definition of Done**: Admin can select operation type and enter parameters

### BULK-ADM-003
**ID**: BULK-ADM-003
**Title**: Preview step
**Goal**: Show proposed changes before committing
**Why**: Critical safety step — see what will change before writing
**Dependencies**: BULK-BE-007, BULK-ADM-002
**Applications**: Admin
**Modules**: apps/admin/src/app/(dashboard)/bulk-update/page.tsx
**Database impact**: None (dry-run)
**API contracts**: POST /admin/bulk/preview
**Permissions**: products.bulk_update
**Implementation**: Call preview API with selected productIds, operation, parameters; display table (Product | Current | Proposed | Status); show summary bar ("18 valid, 2 failed"); Back button to Configure
**Tests**: Preview loads; shows correct current→proposed values; failed rows highlighted with error reason
**Expected blast radius**: Low
**Definition of Done**: Preview accurately reflects what execute() would do; no product modified

### BULK-ADM-004
**ID**: BULK-ADM-004
**Title**: Confirmation + execution step
**Goal**: Require explicit confirmation before executing bulk operation
**Why**: Safety barrier — prevent accidental bulk changes
**Dependencies**: BULK-ADM-003, BULK-BE-007
**Applications**: Admin
**Modules**: apps/admin/src/app/(dashboard)/bulk-update/page.tsx
**Database impact**: MODIFIES PRODUCTS
**API contracts**: POST /admin/bulk/execute
**Permissions**: products.bulk_update
**Implementation**: Show warning message ("X changes will be applied. This cannot be undone."); [Cancel] [Apply Changes] buttons; on Apply: call execute API; on success: show result summary; on error: show error + retry option
**Tests**: Confirmation dialog shows before any write; cancel returns to preview; apply calls execute; result shows success/failure counts
**Expected blast radius**: HIGH — modifies products
**Definition of Done**: Confirmation required; execute called only after explicit confirmation; result displayed accurately

### BULK-ADM-005
**ID**: BULK-ADM-005
**Title**: Navigation sidebar — add Bulk Update link
**Goal**: Make bulk update accessible from admin navigation
**Why**: Discoverability
**Dependencies**: BULK-ADM-001
**Applications**: Admin
**Modules**: apps/admin/src/lib/permissions.ts
**Database impact**: None
**API contracts**: None
**Permissions**: products.bulk_update
**Implementation**: Add to NAV_ITEMS: { href: '/bulk-update', label: 'Actualización masiva', icon: '📦', permission: 'products.bulk_update' }
**Tests**: Bulk Update appears in sidebar for admin users
**Expected blast radius**: Low
**Definition of Done**: Nav item visible in admin sidebar

## QA

### BULK-QA-001
**ID**: BULK-QA-001
**Title**: Bulk update E2E tests
**Goal**: End-to-end validation of complete bulk update flow
**Why**: Critical safety feature — full flow must work
**Dependencies**: BULK-BE-007, BULK-ADM-004
**Applications**: QA
**Modules**: E2E tests
**Tests**:
- BULK-QA-UT-001: Select 5 products → preview shows correct proposed prices
- BULK-QA-UT-002: Preview does NOT modify any product
- BULK-QA-UT-003: Confirm → products actually updated in DB
- BULK-QA-UT-004: Audit log entry exists for bulk operation
- BULK-QA-UT-005: 5% increase on products with VAT general → correct net price applied
- BULK-QA-UT-006: ADD_CATEGORY → categories added without removing existing
- BULK-QA-UT-007: REMOVE_CATEGORY → category removed, others preserved
- BULK-QA-UT-008: REPLACE_CATEGORIES → only new categories remain
- BULK-QA-UT-009: Product without price → failed in preview and in execute (not silently skipped)
- BULK-QA-UT-010: Non-admin user → 401 on /admin/bulk/execute
**Expected blast radius**: N/A
**Definition of Done**: All tests pass; QA sign-off obtained

---

## Task Summary Table

| Task | Layer | Feature | Depends On | Risk | Parallel |
|------|-------|---------|-----------|------|---------|
| BULK-BE-001 | Backend | Domain types | — | Low | * |
| BULK-BE-002 | Backend | Handler interface | BULK-BE-001 | Low | * |
| BULK-BE-003 | Backend | Price handlers | BULK-BE-002 | Low | * |
| BULK-BE-004 | Backend | Category handlers | BULK-BE-002 | Low | * |
| BULK-BE-005 | Backend | BulkService orchestrator | BULK-BE-001..004 | Medium | * |
| BULK-BE-006 | Backend | BulkProductRepository | BULK-BE-001 | Low | * |
| BULK-BE-007 | Backend | API routes | BULK-BE-005, BULK-BE-006 | Medium | * |
| BULK-BE-008 | Backend | Wire into build-app | BULK-BE-007 | Low | * |
| BULK-ADM-001 | Admin | Product selection UI | BULK-BE-007 | Low | * |
| BULK-ADM-002 | Admin | Operation config step | BULK-ADM-001 | Low | * |
| BULK-ADM-003 | Admin | Preview step | BULK-ADM-002, BULK-BE-007 | Low | * |
| BULK-ADM-004 | Admin | Confirm + execute step | BULK-ADM-003, BULK-BE-007 | Medium | * |
| BULK-ADM-005 | Admin | Sidebar nav link | BULK-ADM-001 | Low | * |
| BULK-QA-001 | QA | E2E tests | BULK-BE-007, BULK-ADM-004 | Medium | After all |

**Parallel group**: BULK-BE-001 and BULK-BE-002 are independent; BULK-BE-003 and BULK-BE-004 are parallel (both depend on BE-002); BULK-BE-005 depends on all four handlers; BULK-BE-006 independent; BULK-BE-007 depends on BE-005 + BE-006; Admin tasks BULK-ADM-001 through BULK-ADM-005 are sequential (wizard flow).

**Recommended order**:
1. BULK-BE-001 + BULK-BE-002 + BULK-BE-006 (parallel, no deps)
2. BULK-BE-003 + BULK-BE-004 (parallel, both depend on BE-002)
3. BULK-BE-005 (depends on BE-001..004)
4. BULK-BE-007 (depends on BE-005, BE-006)
5. BULK-BE-008 (depends on BE-007)
6. BULK-ADM-001 (depends on BE-007)
7. BULK-ADM-002 through BULK-ADM-004 (sequential wizard)
8. BULK-ADM-005 (parallel with any ADM task)
9. BULK-QA-001 (after everything)

**High-risk tasks**:
- BULK-BE-005: Transaction logic must be correct — wrong rollback = data corruption
- BULK-ADM-004: Executes actual DB writes — requires confirmation UI; must not auto-submit
- BULK-BE-007: New API endpoints with admin auth — verify auth enforced

**MVP boundary**: BULK-BE-001 through BULK-BE-008 (backend) + BULK-ADM-001 through BULK-ADM-005. BULK-QA-001 is required before go-live.

**Migration risks**: None — bulk update is purely additive; no existing data migration required.
