# ADMIN BULK UPDATE — TESTS.md ## Unit Tests ### Price Handlers ``` BULK-UT-001: INCREASE_PERCENT 5% on 350 cents → proposed = 368 cents BULK-UT-002: INCREASE_PERCENT 100% on 200 cents → proposed = 400 cents BULK-UT-003: DECREASE_PERCENT 5% on 350 cents → proposed = 333 cents BULK-UT-004: DECREASE_PERCENT 50% on 200 cents → proposed = 100 cents BULK-UT-005: DECREASE_FIXED 50 on 350 cents → proposed = 300 cents BULK-UT-006: INCREASE_FIXED 50 on 350 cents → proposed = 400 cents BULK-UT-007: DECREASE_FIXED 500 on 350 cents → proposed = 0 (floor at 0) BULK-UT-008: SET_PRICE 500 → proposed = 500 BULK-UT-009: INCREASE_PERCENT with percent=0 → ValidationResult.valid=false BULK-UT-010: INCREASE_PERCENT with percent=-5 → ValidationResult.valid=false BULK-UT-011: INCREASE_PERCENT with percent=1001 → ValidationResult.valid=false BULK-UT-012: INCREASE_PERCENT without vatRate → ValidationResult.valid=false BULK-UT-013: DECREASE_PERCENT with vatRate=general → validation passes BULK-UT-014: preview() for product without price → status=failed, error=PRICE_NOT_FOUND BULK-UT-015: preview() for product with price → status=valid, proposedPriceCents calculated BULK-UT-016: buildPatch() returns correct { pricing: { netUnitAmountCents, vatRate } } ``` ### Category Handlers ``` BULK-UT-020: ADD_CATEGORY to product with [A, B] → proposedCategoryIds = [A, B, C] (C added) BULK-UT-021: ADD_CATEGORY where C already present → no duplicate (deduplicated) BULK-UT-022: REMOVE_CATEGORY from product with [A, B, C] → [A, B] BULK-UT-023: REMOVE_CATEGORY where not present → no change BULK-UT-024: REPLACE_CATEGORIES with [X, Y] → proposedCategoryIds = [X, Y] BULK-UT-025: REPLACE_CATEGORIES with empty array → proposedCategoryIds = [] BULK-UT-026: ADD_CATEGORY without categoryId → ValidationResult.valid=false BULK-UT-027: REMOVE_CATEGORY without categoryId → ValidationResult.valid=false BULK-UT-028: buildPatch() returns correct { categoryIds: [...] } ``` ### BulkService ``` BULK-UT-030: preview() returns items for all provided productIds BULK-UT-031: preview() returns summary with correct valid/failed counts BULK-UT-032: preview() throws AppError(400) for unknown operation type BULK-UT-033: preview() throws AppError(422) for invalid parameters BULK-UT-034: execute() returns operationId BULK-UT-035: execute() returns COMPLETED when all succeed BULK-UT-036: execute() returns COMPLETED_WITH_ERRORS when some fail BULK-UT-037: execute() calls audit.log() with correct metadata BULK-UT-038: execute() rollback on error → no product modified (verify by checking DB) ``` ## Integration Tests ### Database ``` BULK-IT-001: catalog_products.updated_at updated after bulk price change BULK-IT-002: pricing_variants.net_unit_amount_cents updated after SET_PRICE BULK-IT-003: catalog_product_categories updated correctly for ADD_CATEGORY BULK-IT-004: catalog_product_categories updated correctly for REMOVE_CATEGORY BULK-IT-005: catalog_product_categories updated correctly for REPLACE_CATEGORIES BULK-IT-006: Concurrent bulk operations on overlapping product sets → no race condition BULK-IT-007: Price changed by bulk update → subsequent checkout uses new price ``` ### API Routes ``` BULK-IT-010: POST /admin/bulk/preview with valid INCREASE_PERCENT → 200 + PreviewResult BULK-IT-011: POST /admin/bulk/preview with DECREASE_PERCENT → 200 + PreviewResult BULK-IT-012: POST /admin/bulk/preview with ADD_CATEGORY → 200 + PreviewResult BULK-IT-013: POST /admin/bulk/preview with REMOVE_CATEGORY → 200 + PreviewResult BULK-IT-014: POST /admin/bulk/preview with REPLACE_CATEGORIES → 200 + PreviewResult BULK-IT-015: POST /admin/bulk/preview with invalid percent → 422 + error message BULK-IT-016: POST /admin/bulk/preview without session cookie → 401 BULK-IT-017: POST /admin/bulk/preview with customer role → 403 BULK-IT-020: POST /admin/bulk/execute with valid SET_PRICE → 200 + ExecuteResult + DB updated BULK-IT-021: POST /admin/bulk/execute with valid INCREASE_PERCENT → 200 + DB updated BULK-IT-022: POST /admin/bulk/execute with valid ADD_CATEGORY → 200 + DB updated BULK-IT-023: POST /admin/bulk/execute → security_audit_log row created BULK-IT-024: POST /admin/bulk/execute without auth → 401 BULK-IT-025: POST /admin/bulk/execute with customer role → 403 BULK-IT-026: POST /admin/bulk/execute with partial failures → 200 + COMPLETED_WITH_ERRORS ``` ## Admin UI Tests ``` BULK-ADM-UT-001: Page loads with product table and checkboxes BULK-ADM-UT-002: Selecting 3 products updates "3 products selected" counter BULK-ADM-UT-003: Deselecting all shows "0 products selected" BULK-ADM-UT-004: Search filter narrows product list BULK-ADM-UT-005: "Continue" button disabled when 0 products selected BULK-ADM-UT-010: Clicking Price card shows SET, +%, -%, +FIXED, -FIXED options BULK-ADM-UT-011: Clicking Categories card shows ADD, REMOVE, REPLACE options BULK-ADM-UT-012: INCREASE_PERCENT selected → percentage input appears BULK-ADM-UT-013: INCREASE_PERCENT with invalid input → error state on field BULK-ADM-UT-014: ADD_CATEGORY selected → category picker appears BULK-ADM-UT-020: Preview shows product rows with current → proposed values BULK-ADM-UT-021: Failed rows highlighted in red with error reason BULK-ADM-UT-022: Summary bar shows "X valid, Y failed" BULK-ADM-UT-023: "Back to Configure" navigates back BULK-ADM-UT-024: Preview does NOT show [Apply Changes] button BULK-ADM-UT-030: Confirmation dialog shows warning text with correct count BULK-ADM-UT-031: "Cancel" returns to preview without calling execute BULK-ADM-UT-032: "Apply Changes" calls execute API once BULK-ADM-UT-033: Result shows success count and failure count BULK-ADM-UT-034: "New Bulk Update" button resets wizard to step 1 ``` ## Given/When/Then Acceptance Criteria ``` GIVEN 20 selected products with valid prices WHEN operator previews a 5% price increase THEN no product is modified AND all 20 proposed prices are calculated by the backend AND the preview shows current and proposed values GIVEN a product without a price variant WHEN operator previews any price operation on that product THEN the preview item has status=failed and error=PRICE_NOT_FOUND AND the summary shows failed=1 GIVEN 20 selected products WHEN operator clicks "Apply Changes" on the confirmation step THEN the backend applies the changes in a single transaction AND the security_audit_log receives one entry for the bulk operation AND each affected product's updated_at is updated AND the result shows successful=20, failed=0 GIVEN a bulk execute with 2 products invalid and 18 valid WHEN the operation runs THEN the 18 valid products are updated AND the 2 invalid products are not modified AND the result shows COMPLETED_WITH_ERRORS with failure details AND the audit log entry captures the partial failure GIVEN a non-admin user WHEN they call POST /admin/bulk/execute THEN the backend returns 403 Forbidden AND no audit entry is created AND no product is modified GIVEN a preview shows 18 valid changes WHEN a second operator applies a price change to one of those products before execute THEN the execute still applies the original preview's proposed price AND no optimistic concurrency check is silently bypassed (document this behavior) ``` --- ## Preview Staleness Policy Preview data may be stale when execute runs (another admin changed a price). Current design: execute uses the same calculation on current DB state, which is correct behavior for bulk updates (we want to apply the percentage to whatever the current price is, not the stale preview price). If stricter concurrency control is needed (e.g., "apply only if price hasn't changed since preview"), this requires: - `expected_version` or `updated_at` in preview response - Compare at execute time - This is a future enhancement, not MVP scope.