# Product Dashboard Contracts

Status: **CONFIRMED WITH EXPLICIT LEGACY GAPS**  
Scope: authenticated admin dashboard only. This document is not a Product API contract.

## 1. Route inventory

| Action | Method and path | Middleware order | Handler | UI source | Status |
|---|---|---|---|---|---|
| Catalogue filter | `GET /dashboard/products/filter` | authentication → AJAX authorization/permission → list validator → errors | `filterProducts` | catalogue filter/search forms | CONFIRMED / VIEW_ONLY |
| Catalogue | `GET /dashboard/products` | CSRF token → authentication → authorization/permission → list validator → errors | `products` | sidebar/catalogue | CONFIRMED / VIEW_ONLY |
| Details | `GET /dashboard/products/show/:id` | CSRF token → authentication → authorization/permission → id validator → errors | `showProduct` | row/details action | CONFIRMED / VIEW_ONLY |
| Create page | `GET /dashboard/products/create` | CSRF token → authentication → authorization/permission | `createProductPage` | `create.ejs` | CONFIRMED |
| Create submit | `POST /dashboard/products/create` | authentication → AJAX authorization/permission → multipart parser → CSRF → create validator → errors | `createProduct` | `form.ejs` create mode | CONFIRMED |
| Edit page | `GET /dashboard/products/edit/:id` | CSRF token → authentication → authorization/permission → id validator → errors | `editProductPage` | row/details action | CONFIRMED / VIEW_ONLY |
| Edit submit | `PUT /dashboard/products/edit/:id` | authentication → AJAX authorization/permission → multipart parser → CSRF → edit validator → errors | `editProduct` | `form.ejs` edit mode | CONFIRMED |
| Soft delete | `DELETE /dashboard/products/delete` | authentication → AJAX authorization/permission → ids validator → CSRF → errors | `deleteProduct` | catalogue/details custom modal | CONFIRMED |
| Approve | `PATCH /dashboard/products/moderation/approve/:id` | authentication → AJAX authorization/permission → id validator → CSRF → errors | `approveProduct` | catalogue/details custom modal | CONFIRMED |
| Reject | `PATCH /dashboard/products/moderation/reject/:id` | authentication → AJAX authorization/permission → reason validator → CSRF → errors | `rejectProduct` | catalogue/details custom modal | CONFIRMED |
| Visibility | `PATCH /dashboard/products/visibility/:id` | authentication → AJAX authorization/permission → visibility validator → CSRF → errors | `setProductVisibility` | catalogue/details custom modal | CONFIRMED |
| Bulk moderation | `PATCH /dashboard/products/moderation/bulk` | authentication → AJAX authorization/permission → bulk validator → CSRF → errors | `bulkModerateProducts` | catalogue bulk command | CONFIRMED |

The create GET/POST registrations were restored because the repository already contained
the create view, shared form, controller handlers, validator, upload helper, service, and
tests. No new Product capability or form field was introduced.

## 2. Form contracts

### Create

Transport: `multipart/form-data`; native form method `POST`; JavaScript request method
`POST`.

| Field | Kind | Current rule | Persistence classification |
|---|---|---|---|
| `_csrf` | hidden | required by CSRF middleware | transport only |
| `provider` | ObjectId | required | schema-backed |
| `subDepartment` | ObjectId | required and must resolve to an active taxonomy relation | schema-backed |
| `region` | ObjectId | required by form/validator | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `name` | string | required, 2–200 characters | schema-backed Mixed value |
| `price` | number | required, greater than zero | schema-backed |
| `count` | integer | required, minimum 1 | schema-backed |
| `details` | string | required, maximum 2,000 characters | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `instructions` | string | required, maximum 500 characters | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `status` | enum | optional server-side; UI sends active or needs-rescue | schema-backed |
| `expireAt` | ISO date | UI requires a future date | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `images` | files | required; 1–10 JPEG/JPG/PNG images; maximum 5 MB each; magic-byte checked | schema-backed filenames |

The department is not submitted directly. It is derived from the selected active
subdepartment before upload and persistence.

### Edit

Transport: `multipart/form-data`; native form method `POST`; JavaScript request method
`PUT`.

| Field | Kind | Current rule | Mutation behavior |
|---|---|---|---|
| `_csrf` | hidden | required by CSRF middleware | transport only |
| `name` | string | optional validator, 2–200 when present | updates the selected admin locale in an existing localized object, otherwise preserves legacy scalar behavior |
| `price` | number | optional, greater than zero | schema-backed update |
| `count` | integer | optional, minimum 0 | schema-backed; zero is blocked while an active embedded order exists |
| `details` | string | optional, maximum 2,000 | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `instructions` | string | optional, maximum 500 | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `status` | enum | optional active/needs-rescue | schema-backed update |
| `expireAt` | ISO date | optional future date | LEGACY_ONLY / AMBIGUOUS: absent from current Product schema |
| `deletedImages` | JSON array of filenames | optional, maximum 10 entries | removes matching references from `product.images`; does not delete the old physical files |
| `images` | files | optional; same MIME/size/signature rules as create | appends new filenames after existing non-deleted references |

The edit form does not submit provider, department, subdepartment, region, Product type,
condition, discounts, variants, attributes, or AI-pricing fields.

## 3. Media contract decisions

- `deletedImages` is the canonical dashboard field. The browser serializes it as JSON;
  malformed JSON is sanitized to an empty array.
- `removedImages` is **OUT_OF_SCOPE**. No Product dashboard EJS, validator, or controller
  contract submits or consumes it.
- `imageMode` is **OUT_OF_SCOPE**. There is no `imageMode=replace` UI or backend contract.
- New images use append-only semantics.
- Existing selected images are removed from the stored filename array only. Physical-file
  deletion was not part of the legacy edit behavior and is not claimed here.
- Partially written new uploads are removed if upload or persistence fails before a
  successful save. Existing files are never part of that rollback set.
- `express-fileupload` parses multipart data in memory before CSRF so `_csrf` is available.
  File writes begin only after CSRF and validation reach the controller/service.

## 4. Variants and AI pricing

### Variants

The create/edit dashboard form has no fields for `type`, `attributes`, `variants`,
`selectedValues`, or `label`. Dashboard create therefore creates only the legacy/default
Product shape. Dashboard edit is preservation-only: existing variant and attribute data
is not assigned or cleared.

Status: **OUT_OF_SCOPE for dashboard mutation; CONFIRMED preservation behavior**.

### AI pricing

The create/edit dashboard form has no fields for `pricingMethod`, `aiPricingRequest`,
`aiPrice`, `providerPrice`, `priceRangeMin`, or `priceRangeMax`. Dashboard edit does not
assign those fields.

Status: **OUT_OF_SCOPE for dashboard mutation; CONFIRMED preservation behavior**.

## 5. Mutation side effects and responses

- Create uploads media only after active taxonomy validation, persists the Product, and
  schedules the existing expiry job only when the resulting document exposes `expireAt`.
- Edit checks the active-order zero-count guard before upload, saves once, then performs
  the existing best-effort expiry reschedule and provider notification.
- Create and edit return the existing JSON `ApiResponse` success envelope with
  `data.url=/dashboard/products`; the form console shows the response toast and redirects
  after 450 ms. No redirect or flash contract was added.
- Delete, moderation, visibility, and bulk moderation continue through the frozen Product
  moderation engine, including its relation guards, audit trail, and notifications.
- Invalid CSRF is rejected before controller/service execution. The multipart parser may
  populate in-memory request files, but no upload helper or DB mutation runs.

## 6. Known gaps and exclusions

| Surface | Decision |
|---|---|
| `region`, `details`, `instructions`, `expireAt` | LEGACY_ONLY / AMBIGUOUS until a separately approved schema/business-contract decision; this phase does not alter schema or promise persistence |
| Product variants mutation | OUT_OF_SCOPE |
| AI-pricing mutation | OUT_OF_SCOPE |
| `removedImages` | OUT_OF_SCOPE |
| `imageMode=replace` | OUT_OF_SCOPE |
| Gold Requests | OUT_OF_SCOPE separate legacy resource |
| Product Reports and report reasons | OUT_OF_SCOPE separate dashboard/API resources |
| Public/mobile Product API | OUT_OF_SCOPE and unchanged |
| Product schema, migrations, seeders | OUT_OF_SCOPE and unchanged |

## 7. Contract tests

- `test/dashboardProductsContracts.test.js` maps EJS actions to routes, freezes create/edit
  field inventories, verifies CSRF on every mutation, proves invalid multipart CSRF never
  reaches handlers, and tests the media/variant/AI decisions above.
- Existing Product catalogue, moderation, Product Details B2, Wave 3B/Wave 3C, Provider
  Product, and full-suite tests remain regression authority.
