# PRP-XXX: <Short, action-oriented title>

name: "<One-line imperative summary — e.g. 'Build the FAQ module: public API reads + admin CRUD, scoped `faq`'>"
description: |
  <2–6 sentences. What the change IS, why it exists, and the single guiding
  constraint. State the mental model the implementer must hold. If this PRP aligns
  the public API to the website's needs, name the contract (Postman folder + the
  `/api/v1` version). Always prefer "reuse the Knowledge Base module patterns and
  shared building blocks; only ADD the new module" over "invent a new pattern".>

  **Guiding constraint:** <the one rule that resolves most judgment calls — e.g.
  "match the Knowledge Base module exactly (service + api.controller + admin
  controller + views + model + migration + scope); never hand-roll a response
  envelope, validator, auth check, or query a shared util already provides.">

---

## Metadata

| Field | Value |
|-------|-------|
| **Date Created** | YYYY-MM-DD |
| **Priority** | 🔴 Critical / 🟠 High / 🟡 Normal |
| **Type** | New module / New endpoint / Admin screen / Behavior-alignment / Bug-fix |
| **Module** | `src/modules/<feature>/` |
| **New files** | <CREATE: `<feature>.service.js`, `<feature>.api.controller.js`, `src/admin/<feature>.controller.js`, `src/models/<feature>.js`, `src/routes/api/v1/<feature>.routes.js`, `src/views/<feature>/*.ejs`, `migrations/<ts>-<change>.js`, optional `seeders/<ts>-*.js`> |
| **Edited files (integration)** | `src/models/index.js` (register + associate), `src/routes/api/v1/index.js` (mount), `src/routes/admin/index.js` (routes), `src/views/layouts/admin.ejs` (nav), `src/models/apiClient.js` (default scopes) + `apiKeyAuth` scope, `src/config/index.js` + `.env.example` (only if new settings) |
| **DB changes** | <tables/columns added or changed + the migration filename, OR "none">  → see [database-guide.md](database-guide.md) |
| **Reused (DO NOT re-implement)** | `asyncHandler`, `apiResponse` (`ok`/`created`/`fail`/`ApiError`), `validate` (zod), `apiKeyAuth('<scope>')`, `requireAdmin`/`requireRole`, `apiLimiter`/`strictLimiter`, `errorHandler`, `webHelpers` (flash/method-override), `helpers` (`slugify`/`markdownToText`/`publicId`/`ticketReference`), `sendAndLog`, `brevo.client`, `config`, `logger`, `layouts/admin.ejs`, `partials/flash.ejs`, `partials/pagination.ejs`, `public/css/admin.css` |
| **API contract** | `docs/postman/ShipRex_API.postman_collection.json` → folder **`<folder name>`** (`/api/v1`) |
| **Reference module (read-only)** | **Knowledge Base** — `src/modules/knowledge-base/*`, `src/admin/kb.controller.js`, `src/models/kbArticle.js`, `src/routes/api/v1/kb.routes.js`, `src/views/kb/*`, init-schema migration |
| **Docs to update** | `docs/postman/*` (always, if endpoints change); `docs/feature-development-guide.md` / `database-guide.md` (only if a new shared pattern is introduced) |
| **Workflow rule** | `git add -A && git commit` after **every** completed task (message given per task). Endpoint change ⇒ Postman update in the **same** commit. Schema change ⇒ migration in the **same** commit. |

> ⚠️ **Standards rule:** every task MUST follow
> [feature-development-guide.md](feature-development-guide.md),
> [architecture.md](architecture.md), and [database-guide.md](database-guide.md).
> Each task that adds/changes a **public endpoint** ends with a **"📮 Postman"**
> line (request + example response added to the collection). Each task that
> changes a **visible surface** (API response or admin screen) ends with a
> **"✅ Verify"** line. Each task that changes the **schema** ends with a
> **"🗄️ DB"** line (migration + model in lockstep).
>
> ⚠️ **Realm rule:** public logic lives under `/api/v1` (API-key + scope); admin
> logic lives under `/admin` (session). Never cross them. No admin data via `/api`;
> no API key on `/admin`.

---

## Goal

<Precise end state. For the public API: which endpoints exist, their scope, their
exact request/response shape (matching Postman). For the admin: which screens
exist, the actions on each, role gating, and the loading/empty/error/flash states.
"The API returns exactly what the contract documents — no invented fields or verbs.">

## Why

- <Business value / the bug it fixes / the website need it unblocks.>
- <What breaks or is missing today. Reference files with `path:line`.>
- <Why now / what it unblocks.>

## What (surface changes, summarized)

| Surface | Today | After this PRP |
|---------|-------|----------------|
| `POST /api/v1/<...>` | — | <scope, body, response, rate-limit> |
| `GET /api/v1/<...>` | — | <scope, query, paginated response> |
| `/admin/<feature>` (list) | — | <filters, columns, actions, role gating> |
| `/admin/<feature>/:id` (show/edit) | — | <fields, actions, flash> |
| <cross-module touchpoint> | <stale> | <aligned> |

### Public API endpoints (the contract)

| Method | Path | Scope | Used by | Notes (auth / body / response / status) |
|--------|------|-------|---------|-----------------------------------------|
| GET | `/api/v1/<...>` | `<scope>` | website | <pagination? envelope? 200> |
| POST | `/api/v1/<...>` | `<scope>` | website | <strictLimiter? validated body? 201?> |

### Admin routes

| Method | Path | Role | Screen/action |
|--------|------|------|---------------|
| GET | `/admin/<feature>` | any admin | list |
| POST/PUT/DELETE | `/admin/<feature>/...` | `<role>` | <create/update/delete> |

---

## DB schema changes

<Per [database-guide.md](database-guide.md). List new tables/columns with type,
null, default, enum values, indexes, and FK `onDelete`. Name the migration file.
Confirm model ↔ migration lockstep. If "none", say so.>

```
migrations/<timestamp>-<change>.js
  + table <x> ( id, …, created_at, updated_at )   [indexes: …]
  ~ column <table>.<col> CHANGE …
```

---

## ⛔ STOP — read & understand BEFORE writing code

Read in order; answer the "Check yourself" after each. If you can't, re-read.

1. **[architecture.md](architecture.md)** — the two realms, the module pattern,
   the response envelope, reused building blocks.
   *Check yourself:* Where does business logic live, and what does a controller do?
   (Service holds logic + Sequelize; controller is a thin HTTP adapter.)

2. **[feature-development-guide.md](feature-development-guide.md)** — layer
   responsibilities, conventions, build order, validation loop, security rules.
   *Check yourself:* What does `validate(schema)` do to the request? (Replaces
   `req.body`/`req.query` with the parsed, coerced value.)

3. **[database-guide.md](database-guide.md)** — conventions + how to add/change
   columns; model ↔ migration lockstep.
   *Check yourself:* What two files change together for a new column, in one commit?
   (the model + a new migration.)

4. **The reference module — Knowledge Base.** Read `kb.service.js`,
   `kb.api.controller.js`, `src/admin/kb.controller.js`, `kb.routes.js`, the KB
   views, and the model files.
   *Check yourself:* How does a service signal "not found", and how is that turned
   into JSON vs an admin redirect? (`throw new ApiError(404,…)`; `errorHandler`
   renders JSON for `/api`, HTML for admin; admin controllers also `req.flash` +
   redirect.)

5. **The Postman folder for this feature** in
   `docs/postman/ShipRex_API.postman_collection.json` (after you draft it, or the
   existing one if aligning). Read every request: method, URL, scope, body, the
   example response, status code.
   *Check yourself:* For each endpoint — required scope and exact success status?

6. **Integration wiring:** `src/routes/api/v1/index.js`,
   `src/routes/admin/index.js`, `src/models/index.js`,
   `src/views/layouts/admin.ejs`, `src/models/apiClient.js`.
   *Check yourself:* Where is a new public route mounted, and where is its scope
   declared? (`api/v1/index.js`; scope enforced by `apiKeyAuth('<scope>')` +
   listed in `ApiClient.scopes`.)

> **Mental model:** <one or two sentences capturing the domain truth that makes a
> wrong implementation obviously wrong.>

---

## Known gotchas & critical edge cases (READ TWICE)

```text
# GOTCHA 1 — Response envelope. Public success = apiResponse.ok(res, data, meta?)/created.
#   Public errors = throw new ApiError(status, code, message, details?). Never build error JSON.

# GOTCHA 2 — validate() REPLACES req.body/req.query with the parsed value. Use z.coerce.* for
#   query numbers. Downstream reads the clean data, not the raw input.

# GOTCHA 3 — Scope + rate limit. Every public route: apiKeyAuth('<scope>'); expensive/abuse-prone
#   writes also get strictLimiter. New scope ⇒ add to ApiClient.scopes default + document it.

# GOTCHA 4 — Realm separation. No admin data via /api; no API key on /admin. Don't add a public
#   endpoint that returns internal notes / admin-only fields (see ticket_replies.is_internal_note).

# GOTCHA 5 — Admin forms use POST + ?_method=PUT|DELETE (webHelpers.methodOverride). Checkboxes
#   that are unchecked send nothing — handle that in the controller/service if it matters.

# GOTCHA 6 — Model ↔ migration lockstep. A schema change is BOTH a model edit AND a new migration,
#   same commit. Never sync() a real DB. FULLTEXT is added in a migration, not via the model index.

# GOTCHA 7 — Side-effects never block. Email/AI/Brevo go through sendAndLog / a *.client.js with
#   graceful degradation. A mail failure must not fail lead/ticket capture.

# GOTCHA 8 — Secrets via config only. New setting ⇒ add to src/config/index.js AND .env.example.
#   Never read process.env in feature code. Never put secrets in the Postman collection.

# GOTCHA <n> — <feature-specific trap, with file:line if applicable>.
```

---

## Implementation Blueprint

### Module structure (create these)
```
src/modules/<feature>/
├─ <feature>.service.js          # business logic + Sequelize; throws ApiError
├─ <feature>.api.controller.js   # public JSON controller + exported zod schemas
└─ <feature>.client.js           # only if it calls an external service
src/admin/<feature>.controller.js
src/models/<feature>.js
src/routes/api/v1/<feature>.routes.js
src/views/<feature>/{list,form,show}.ejs
migrations/<timestamp>-<change>.js
```

### Strategy in one sentence
<e.g. "Each endpoint is a thin controller over a service method over Sequelize; the
admin screens are thin EJS over the same service; no logic is duplicated between
the two surfaces.">

---

## Tasks (do them in this order)

> Each task: **Read first**, **Change**, **Pattern to copy**, **Edge cases**,
> **🗄️ DB** (if schema), **📮 Postman** (if endpoint), **✅ Verify**, **✅ Commit**.
> Keep tasks small and independently committable.

### Task 1 — Model(s) + migration (+ associations, + seeder if needed)
**Read first:** `database-guide.md`; the KB model files + init-schema migration.
**Change:** create `src/models/<feature>.js`; register + associate in
`src/models/index.js`; write `migrations/<ts>-<change>.js` mirroring it.
**Edge cases:** FK `onDelete`; enums; indexes (incl. FULLTEXT if searched).
**🗄️ DB:** migration runs clean on a scratch DB; `down` reverses it.
**✅ Verify:** `node -e "require('dotenv').config();require('./src/models');console.log('OK')"`.
**✅ Commit:** `PRP-XXX Task 1: <feature> model + migration`

### Task 2 — Service (`<feature>.service.js`)
**Read first:** `kb.service.js`.
**Change:** one method per operation; list methods return
`{ rows, count, page, limit, pages }`; throw `ApiError` on domain failures.
**Edge cases:** GOTCHA 1, 4, 7.
**✅ Verify:** require-load check.
**✅ Commit:** `PRP-XXX Task 2: <feature> service`

### Task 3 — Public API (controller + routes + scope)
**Read first:** `kb.api.controller.js`, `kb.routes.js`, `api/v1/index.js`,
`apiKeyAuth.js`, `apiClient.js`.
**Change:** controller with `asyncHandler` + exported zod schemas; routes with
`apiKeyAuth('<scope>')` (+ `strictLimiter` on writes) + `validate`; mount in
`api/v1/index.js`; add `<scope>` to `ApiClient.scopes` default.
**Edge cases:** GOTCHA 1, 2, 3, 4.
**📮 Postman:** add a folder/requests with example responses.
**✅ Verify:** boot + curl (no key → 401; bad body → 422 details[]; happy path → 200/201).
**✅ Commit:** `PRP-XXX Task 3: <feature> public API (+scope, +Postman)`

### Task 4 … N — Admin screen(s) (one per task)
**Read first:** matching KB admin controller + view + the nav in `layouts/admin.ejs`.
**Change:** `src/admin/<feature>.controller.js`, routes under the `requireAdmin`
block in `routes/admin/index.js`, views in `src/views/<feature>/`, nav item.
**Edge cases:** GOTCHA 5; loading/empty/error/flash; role gating.
**✅ Verify:** log in, exercise create/edit/delete, confirm flash + list refresh.
**✅ Commit:** `PRP-XXX Task N: <feature> admin <screen>`

### Task N+1 — Contract + docs sweep
**Change:** finalize the Postman folder + `postman/README.md`; add any new env keys
to `.env.example`; update the guides only if a new shared pattern was introduced.
**✅ Verify:** run the Postman folder against a live server; responses match examples.
**✅ Commit:** `PRP-XXX Task N+1: <feature> contract + docs`

---

## Integration Points

```yaml
MODELS:        src/models/<feature>.js  (+ register/associate in src/models/index.js)
MIGRATION:     migrations/<timestamp>-<change>.js
PUBLIC ROUTES: src/routes/api/v1/<feature>.routes.js  (+ mount in api/v1/index.js)
ADMIN ROUTES:  src/routes/admin/index.js  (requireAdmin block)
ADMIN VIEWS:   src/views/<feature>/*.ejs
ADMIN NAV:     src/views/layouts/admin.ejs
SCOPE:         src/models/apiClient.js (default scopes) + apiKeyAuth('<scope>')
CONFIG/ENV:    src/config/index.js + .env.example   (only if new settings)
CONTRACT:      docs/postman/ShipRex_API.postman_collection.json (+ postman/README.md)
REUSED (do not re-implement):
  asyncHandler, apiResponse, validate, apiKeyAuth, requireAdmin/requireRole,
  apiLimiter/strictLimiter, errorHandler, webHelpers, helpers, sendAndLog,
  brevo.client, config, logger, admin layout/partials, admin.css
```

---

## Validation Loop

### Level 1 — Static / load
```bash
node -e "require('dotenv').config(); require('./src/app'); require('./src/models'); console.log('OK')"
```
### Level 2 — Schema
```bash
DB_NAME=shiprex_cms_test DB_USER=root DB_PASSWORD= npx sequelize-cli db:migrate
```
### Level 3 — Runtime (public API)
- Boot; `/health` shows DB up.
- Each endpoint: no key → 401; bad body → 422 with `details[]`; happy path → 200/201
  with the documented shape; scope mismatch → 403.
### Level 4 — Admin
- Log in; open the screen; exercise every mutation; confirm flash + list refresh;
  non-permitted role blocked.
### Level 5 — Contract
- Run the Postman folder; live responses match the documented examples (no invented
  fields, correct status codes).

---

## Final validation checklist
- [ ] App + models load; migration runs clean on a scratch DB; `down` reverses it.
- [ ] Module matches the Knowledge Base structure; logic only in the service.
- [ ] Every public response uses `apiResponse`; every error is an `ApiError`.
- [ ] Every public route has a **scope**, **validation**, and (writes) a **rate limit**.
- [ ] No admin data exposed via `/api`; no API key accepted on `/admin`.
- [ ] Model ↔ migration in lockstep; both in one commit.
- [ ] Admin screens handle loading/empty/error + flash; role gating verified.
- [ ] Postman collection updated with requests + example responses (same commit as code).
- [ ] New env keys in `src/config/index.js` and `.env.example`; no secrets committed.
- [ ] Each task committed separately (`git log` shows PRP-XXX Task N).

---

## Rollback
All changes are additive (a new module + small edits to routes/models-index/nav/
scope/Postman). To revert: `git revert` the per-task commits. For schema, run
`npx sequelize-cli db:migrate:undo` (the migration's `down`). Note any destructive
column changes and the data they cannot restore.

---

## Anti-patterns to avoid
- ❌ Business logic or Sequelize queries in a controller.
- ❌ Hand-rolled error JSON instead of `ApiError`.
- ❌ A public endpoint without scope / validation / rate limit / Postman entry.
- ❌ A model change without a matching migration (or vice-versa).
- ❌ Blocking a request on email/AI instead of `sendAndLog`/async.
- ❌ Re-implementing pagination, flash, layout, slugify, or the response envelope.
- ❌ Exposing admin/internal fields through `/api`; accepting API keys on `/admin`.
- ❌ Reading `process.env` directly; hardcoding secrets (incl. in Postman).
- ❌ Skipping the per-task commit or the contract-in-lockstep rule.
