# ADMIN BULK UPDATE — SPEC.md

## 1. Concept & Vision

A safe, auditable bulk update module for MercadoDeVida Admin that lets operators change prices and categories across multiple products at once. Every bulk operation requires a **preview** before execution, confirms before committing, and logs every change to the audit trail.

**Rule: PREVIEW != WRITE. SELECT → PREVIEW → CONFIRM → EXECUTE. NEVER SILENT MUTATION.**

## 2. Initial Capabilities

- **Bulk Price Update**: SET, INCREASE BY %, DECREASE BY %, INCREASE BY FIXED, DECREASE BY FIXED
- **Bulk Category Update**: ADD CATEGORY, REMOVE CATEGORY, REPLACE CATEGORIES

The architecture must allow adding future operations (stock, visibility, brands, etc.) without redesign.

## 3. Why Not PATCH in a Loop?

Making N individual `PATCH /products/:id` calls from the browser for bulk operations causes:
- N network requests
- N separate audit entries (not grouped)
- Partial failures with no atomicity
- Race conditions if prices change between calls
- Poor observability

A single backend bulk operation groups these concerns: atomic or near-atomic execution, single audit entry, transactional semantics.

## 4. Bulk Operation Types

### 4.1 Price Operations

```
SET_PRICE          → Set netUnitAmountCents to exact value
INCREASE_PERCENT   → netUnitAmountCents = current * (1 + pct/100), round to nearest cent
DECREASE_PERCENT   → netUnitAmountCents = current * (1 - pct/100), round to nearest cent
INCREASE_FIXED     → netUnitAmountCents = current + fixedCents
DECREASE_FIXED     → netUnitAmountCents = current - fixedCents
```

### 4.2 Category Operations

```
ADD_CATEGORY     → Append categoryId to product's categoryIds (deduplicated)
REMOVE_CATEGORY  → Remove categoryId from product's categoryIds
REPLACE_CATEGORIES → Replace product's categoryIds entirely with provided list
```

**Critical**: "UPDATE CATEGORY" does not exist. The three explicit operations above have very different semantics.

## 5. Selection Strategy

Admin can select products through:
- **Checkbox row selection**: tick individual products in the list
- **Search/filter**: apply filters to the list, implicitly selecting the filtered set

The UI must be explicit about what "Selected products" means:
- "18 products selected" = 18 explicitly checked rows
- "All 42 filtered products" = entire filtered result set (requires separate confirmation)

Do NOT mix these semantics.

## 6. Preview

Preview is a **dry-run**. No product is modified.

Preview is powered by a backend endpoint that returns proposed changes without applying them:

```
POST /admin/bulk/preview
{
  "productIds": ["uuid1", "uuid2", ...],
  "operation": "INCREASE_PERCENT",
  "parameters": { "percent": 5 }
}
→ 200
{
  "items": [
    {
      "productId": "uuid1",
      "currentPriceCents": 350,
      "proposedPriceCents": 368,   ← calculated by backend
      "currentCategoryIds": ["cat-a"],
      "proposedCategoryIds": null,  ← null if not a category op
      "status": "valid",
      "error": null
    },
    {
      "productId": "uuid2",
      "currentPriceCents": 290,
      "proposedPriceCents": null,
      "status": "failed",
      "error": "PRICE_NOT_FOUND"   ← variant has no price
    }
  ],
  "summary": {
    "total": 20,
    "valid": 18,
    "failed": 2
  }
}
```

The preview response is **not stored**. It is regenerated on each request. Backend calculates prices using existing pricing rules (VAT, rounding, etc.) — NOT frontend JavaScript.

## 7. Confirmation

After preview, admin sees:
- What will change (product, old value, new value)
- What will fail (product, reason)
- Summary: "18 products will be updated, 2 skipped"

Admin must explicitly confirm:
```
Are you sure you want to apply these 18 changes?
This action cannot be undone.
[Cancel] [Apply Changes]
```

## 8. Execution

Confirmed preview is executed via:

```
POST /admin/bulk/execute
{
  "productIds": ["uuid1", "uuid2", ...],
  "operation": "INCREASE_PERCENT",
  "parameters": { "percent": 5 }
}
→ 200
{
  "operationId": "bulk-uuid",
  "status": "COMPLETED",
  "results": {
    "total": 20,
    "successful": 18,
    "failed": 2,
    "errors": [
      { "productId": "uuid2", "error": "PRICE_NOT_FOUND" }
    ]
  }
}
```

Backend applies changes in a single transaction per operation. If the operation fails partway, the transaction rolls back (no partial mutations on error).

## 9. Audit

Every bulk execution logs ONE audit entry via existing AuditLogger:

```json
{
  "actorId": "admin-uuid",
  "action": "bulk.execute",
  "target": "bulk:price.increase_percent",
  "metadata": {
    "operationId": "bulk-uuid",
    "operation": "INCREASE_PERCENT",
    "parameters": { "percent": 5 },
    "productIds": ["uuid1", "uuid2"],
    "results": {
      "total": 20,
      "successful": 18,
      "failed": 2,
      "failures": [{ "productId": "uuid2", "error": "PRICE_NOT_FOUND" }]
    }
  }
}
```

Each individual product mutation is NOT logged separately for bulk operations (that would create N audit entries for one admin action).

## 10. RBAC

Bulk operations require dedicated permission:

```
products.bulk_update
```

In the current `permissions.ts`, `admin` role gets all permissions via `can(role, _)`. Future granular permissions can add `products.bulk_update` to the `Permission` type.

## 11. Extension Points

The BulkUpdateService uses a handler registry:

```typescript
interface BulkOperationHandler {
  readonly type: string;          // e.g., 'INCREASE_PERCENT'
  readonly domain: string;         // e.g., 'price', 'category'

  validate(params: unknown): ValidationResult;
  buildPreview(product: Product, params: unknown): PreviewItem;
  apply(product: Product, params: unknown): ProductPatch;
}

const HANDLERS: ReadonlyMap<string, BulkOperationHandler> = new Map([
  ['INCREASE_PERCENT', new IncreasePercentHandler()],
  ['ADD_CATEGORY', new AddCategoryHandler()],
  // Future: ['STOCK_SET', new StockSetHandler()]
]);
```

Adding a new operation = adding one new handler class + registering it. No changes to routing or execution engine.

## 12. Out of Scope

- Stock bulk update (future)
- Visibility bulk update (future)
- Brand bulk update (future)
- Asynchronous background jobs (not needed at expected data volume)
- Operation cancellation (not needed at expected data volume)
- Bulk export (different feature)

## 13. Feature Flag

```
admin_bulk_update
  default: false
```

Flip to true after MVP is tested.

## 14. Acceptance Criteria

See `TESTS.md`.
