# Swagger / OpenAPI Guide

> **Base codebase note.** This repo is a base project; a new business domain is being built
> on top of it. That has three consequences for API documentation:
>
> 1. **Document the new domain, not the old one.** The base's endpoints (haraj,
>    advertisements, settlements, OTO shipping, …) belong to the previous product. Do not
>    back-document them.
> 2. **The docs grow with the product.** Since new modules are created one at a time, the
>    spec is built incrementally — one domain file per module, added the moment that module
>    ships. This is not a compromise; it is the intended workflow.
> 3. **Schemas mirror the new DTOs.** A schema is written from the `@returnObj` DTO of a
>    *new-domain* model. Never copy a schema from an old model's shape.
>
> The envelope, status-key mapping, security scheme, and `lang` header are **base
> conventions** and are reused unchanged.

---

## 1. Is Swagger present today?

**Yes, through the approved zero-dependency path.** The project still has no
`swagger-jsdoc` or `swagger-ui-express` dependency, but it now contains:

- modular OpenAPI YAML under `docs/openapi/`;
- generated `public/api-docs/openapi.json`;
- a static professional documentation UI under `public/api-docs/`;
- BR1/BR2 manual-readiness checks and filters;
- Postman collections/environments under `postman/` and public export copies;
- contract/export tests in `test/swagger*.test.js` and `test/postmanExport.test.js`.

OpenAPI is source-controlled documentation, not runtime-generated JSDoc. Endpoint docs
continue to grow only with touched/approved API contracts.

### What serves as the API contract right now

| File | Covers |
|---|---|
| `.cursor/postman/Zafirra.postman_collection.json` | Main API collection |
| `.cursor/postman/advertisement-api/*.json` | Advertisements, rates |
| `scripts/zafirra-call-flow.postman_collection.json` | Voice/video call flow |
| `.cursor/implementation-plan/EP-*.md` | Original per-epic endpoint specs |
| `docs/notification-types-actions.md` | Notification types + `itemId` matrix |

For a touched endpoint, update its modular OpenAPI source and regenerate/verify the
public JSON and Postman delivery artifacts through the existing project scripts/tests.
Do not hand-edit a generated timestamp merely to create a diff.

---

## 2. Where Swagger files will live

Adding `swagger-ui-express` / `swagger-jsdoc` means adding packages, which `AGENTS.md`
forbids without explicit human approval. Two paths:

### Path A — zero new dependencies (recommended, approved by default)

Maintain hand-written OpenAPI 3.0 YAML as static files. No runtime cost, no new package;
the files are consumable by Swagger Editor, Redocly, Stoplight, Postman import, and code
generators.

```
docs/openapi/
├── openapi.yaml            # root: info, servers, security, tags, $ref-ed paths
├── components/
│   ├── responses.yaml      # SuccessResponse, ErrorResponse, ValidationErrorResponse
│   ├── schemas.yaml        # Client, Provider, Product, Order, Auction, …
│   └── security.yaml       # bearerAuth + the required lang header
└── paths/
    ├── auth.yaml
    ├── products.yaml
    ├── orders.yaml
    └── …                   # one file per domain, mirroring src/routes/api/
```

One YAML file per domain, mirroring `src/routes/api/<Domain>Route/`. Create a file only
when you touch that domain.

#### Bootstrap — the root file (write this once, on the first documented endpoint)

Do not invent your own root structure. The first agent to document an endpoint creates
exactly this, then every later task only appends to `paths:` and to the domain files.

```yaml
# docs/openapi/openapi.yaml
openapi: 3.0.3
info:
  title: Zafirra API
  version: 1.0.0
  description: >
    Mobile/public REST API. All responses use the fixed envelope
    { key, message, status } (+ data, + paginate on list endpoints).
    Clients branch on `key`, not on the HTTP status code.
servers:
  - url: https://{host}/api
    variables:
      host:
        default: your-host.example
security:
  - bearerAuth: []          # default; public endpoints override with `security: []`
tags: []                    # see §4 — add a tag the first time you document its domain
paths: {}                   # $ref out to docs/openapi/paths/<domain>.yaml
components:
  securitySchemes: {}       # see §8
  parameters: {}            # LangHeader — see §8
  schemas: {}               # see §5 and §7
  responses: {}             # see §7
```

Rules for growing it:

- `paths` entries are `$ref`s into `docs/openapi/paths/<domain>.yaml`; never inline a path here.
- Add a `tags` entry the first time you document a domain, not before.
- `components` grows only when a schema is genuinely shared by two or more endpoints.

### Path B — served Swagger UI (requires approval)

Would add `swagger-ui-express` (+ optionally `swagger-jsdoc`) and mount
`app.use('/api-docs', ...)` inside `AppInitializer.initializeRoutes()`. If approved,
the UI **must** be gated in production (dashboard session or a separate credential) —
`express.static("./")` already over-exposes this server; do not add another open surface.

Do not take Path B on your own initiative.

---

## 3. How to add a new endpoint to the docs

1. Implement the endpoint first (route → validation → controller → service → DTO).
2. Open `docs/openapi/paths/<domain>.yaml` (create it if this domain has none).
3. Add exactly one `path` entry per new endpoint, with:
   - `tags` — from the tag list in §4
   - `summary` — one line, imperative
   - `security` — `[]` for `unRequireAuthRoutes()` endpoints, `bearerAuth` otherwise
   - `parameters` — including the required `lang` header
   - `requestBody` — with the correct media type (`multipart/form-data` when files are involved)
   - `responses` — 200 plus every error status the endpoint can actually produce
4. `$ref` shared schemas instead of inlining them.
5. Register the new file under `paths:` in `docs/openapi/openapi.yaml`.
6. Update the matching Postman request so both stay in sync.

**Golden rule: document only what this task touched.** Never bulk-document the project in
one pass — it produces drift the moment someone edits an untouched endpoint.

---

## 4. Tag organization

One tag per API domain of the **new product**, matching the route class you create for it.
Add a tag the first time you document that domain — never up front.

Expected tag set as the new domain is built out (adjust to the analysis):

```
Client Auth        # signup / activate / signin / profile — client side
Provider Auth      # signup / approval-gated signin — provider side
Categories
Products           # products, attributes, variants
Cart
Orders
Auctions           # auctions + bids
Wallet             # balance + transactions
Reviews
Favorites
Notifications
Settings           # settings, CMS pages, static content
Support            # complaints, contact messages
```

⚠️ Old-product tags (`Haraj`, `Advertisements`, `Financial`/settlements, `Shipping`/OTO,
`Payments`/hyperPay) belong to the base. Do **not** create them unless the analysis
explicitly carries that module into the new product.

Declare tags once, at the root of `docs/openapi/openapi.yaml`:

```yaml
tags:
  - name: Client Auth
    description: Client registration, activation, and session endpoints.
  - name: Provider Auth
    description: Provider (store / haraj) registration and approval-gated sign-in.
  - name: Products
    description: Provider product catalogue and client-facing product reads.
```

Never invent a tag that does not map to a real route class.

---

## 5. Schema organization

Schemas live in `docs/openapi/components/schemas.yaml` and mirror the DTOs returned by
`src/helpers/returnObject/returnObject.js` — **not** the Mongoose schemas. If the DTO
does not expose a field, the schema must not list it.

⚠️ Write the schema from the **new-domain** DTO you just built (designed per
`docs/DOMAIN_MODELING_GUIDE.md` § *Model Creation Workflow*, step 8). The `Client` example below
reflects the *base* DTO and is shown to illustrate the shape and conventions (id mapping,
`*Text` localization, absolute file URLs, date format) — replace its fields with the ones
your new analysis actually defines.

```yaml
components:
  schemas:
    Client:
      type: object
      properties:
        id:            { type: string, example: "665f1c2a9b4e1d0012ab34cd" }
        name:          { type: string, example: "محمد" }
        avatar:        { type: string, format: uri }
        countryCode:   { type: string, example: "+966" }
        phone:         { type: string, example: "0512345678" }
        fullPhone:     { type: string, example: "+9660512345678" }
        userType:      { type: string, enum: [client] }
        status:        { type: string, enum: [active, block, delete] }
        statusText:    { type: string }
        active:        { type: boolean }
        isNotify:      { type: boolean }
        notifyCount:   { type: integer }
        balance:       { type: number, format: float }
        createdAt:     { type: string, example: "2026/07/21" }
        token:         { type: string, description: "JWT — present only on sign-in/activate" }

    Provider:
      allOf:
        - $ref: '#/components/schemas/Client'
        - type: object
          properties:
            userType:       { type: string, enum: [store, haraj] }
            approvalStatus: { type: string, enum: [wait, accept, reject, cancelled] }
            commercialNumber: { type: string }
```

Enum values must be copied from `src/helpers/enums/*.enum.js`, never guessed.

---

## 6. Path organization

```yaml
# docs/openapi/paths/auth.yaml
/signup-client:
  post:
    tags: [Client Auth]
    summary: Register a new client account
    security: []                      # public — registered in unRequireAuthRoutes()
    parameters:
      - $ref: '#/components/parameters/LangHeader'
    requestBody:
      required: true
      content:
        multipart/form-data:
          schema:
            type: object
            required: [name, countryCode, phone, password, confirmPassword]
            properties:
              name:            { type: string, example: "محمد" }
              countryCode:     { type: string, example: "+966" }
              phone:           { type: string, example: "0512345678" }
              password:        { type: string, format: password,
                                 description: "Min 8 chars, must include upper, lower, digit, and symbol" }
              confirmPassword: { type: string, format: password }
              avatar:          { type: string, format: binary }
    responses:
      '200':
        description: Account created; activation required
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/SuccessResponse'
                - type: object
                  properties:
                    key:  { type: string, enum: [needActive] }
                    data: { $ref: '#/components/schemas/Client' }
      '400': { $ref: '#/components/responses/ValidationError' }
      '500': { $ref: '#/components/responses/ServerError' }
```

Path grouping rules:

- One file per domain, matching the route class.
- Paths are written **relative to the `/api` mount** (`/signup-client`, not `/api/signup-client`);
  `servers` carries the `/api` base.
- Public endpoints get `security: []`; authenticated ones inherit the global `bearerAuth`.
- The order of paths inside a file should match the order in the route class, so a reader
  can diff them by eye.

---

## 7. Common response schemas

This project uses a fixed three-field envelope (`ApiResponse` / `ApiError`).

```yaml
components:
  schemas:
    SuccessResponse:
      type: object
      required: [key, message, status]
      properties:
        key:     { type: string, example: success }
        message: { type: string, example: "تمت العملية بنجاح" }
        status:  { type: integer, example: 200 }
        data:    { type: object, nullable: true }
        paginate:
          type: object
          description: Present only on list endpoints (type "api").
          properties:
            currentPage: { type: integer, example: 1 }
            lastPage:    { type: integer, example: 5 }
            perPage:     { type: integer, example: 20 }
            total:       { type: integer, example: 93 }

    ErrorResponse:
      type: object
      required: [key, message, status]
      properties:
        key:     { type: string, enum: [fail, notFound, unauthorized, blocked, exception] }
        message: { type: string, example: "رقم الجوال مستخدم من قبل" }
        status:  { type: integer, example: 400 }
```

### Validation error schema

`showErrorsApi` returns only the **first** error, in the same envelope:

```yaml
    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          properties:
            key:     { type: string, enum: [fail] }
            status:  { type: integer, enum: [400] }
            message: { type: string, description: "Localized message for the first failing field only." }
```

### Reusable responses

```yaml
  responses:
    ValidationError:
      description: Validation failed (first failing field only)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ValidationErrorResponse' }
    Unauthorized:
      description: Missing, expired, or revoked token
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example: { key: unauthorized, message: "يجب تسجيل الدخول", status: 419 }
    Blocked:
      description: Account blocked
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example: { key: blocked, message: "تم إيقاف حسابك", status: 423 }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ServerError:
      description: Unexpected error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
          example: { key: exception, message: "حدث خطأ، برجاء التواصل مع الدعم", status: 500 }
```

### Status code table (must match `ApiError.GetCode`)

| `key` | HTTP | Meaning |
|---|---|---|
| `success` | 200 | OK |
| `needActive` | 203 *(sent as 200 by `Auth`)* | Account exists but needs OTP activation |
| `fail` | 400 | Validation or business-rule failure |
| `notFound` | 404 | Entity missing |
| `unauthorized` | 419 | Missing/expired/revoked token |
| `blocked` | 423 | Account blocked |
| `exception` | 500 | Unexpected |

⚠️ Document the **actual** HTTP status the endpoint returns. Signup and inactive sign-in
send HTTP **200** with `key: "needActive"`, not 203. Clients branch on `key`, so both
must be documented accurately.

---

## 8. Security schemes

```yaml
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        Obtained from POST /signin or PATCH /activate. Valid for 10 days and additionally
        checked against the UserToken collection on every request — signing out revokes it
        server-side.

  parameters:
    LangHeader:
      name: lang
      in: header
      required: true
      schema: { type: string, enum: [ar, en], default: ar }
      description: Response language. Rejected by GlobalValidator.validateLang() if not ar/en.

security:
  - bearerAuth: []          # global default; public endpoints override with `security: []`
```

Do not document `SECRET_KEY`, `CRYPTO_HASH`, `JWT_SECRET`, or any other env value —
not as a description, not as an example, not as a default.

---

## 9. Examples

Signup request (`multipart/form-data`):

```
name:            محمد
countryCode:     +966
phone:           0512345678
password:        ********
confirmPassword: ********
avatar:          <binary>
```

Signup response — **note: no password, no activationCode**:

```json
{
  "key": "needActive",
  "message": "تم إنشاء الحساب بنجاح",
  "status": 200,
  "data": {
    "id": "665f1c2a9b4e1d0012ab34cd",
    "name": "محمد",
    "avatar": "https://<host>/assets/uploads/users/clients/665f.../avatar.png",
    "countryCode": "+966",
    "phone": "0512345678",
    "fullPhone": "+9660512345678",
    "userType": "client",
    "status": "active",
    "active": false,
    "balance": 0,
    "createdAt": "2026/07/21"
  }
}
```

Provider signup response uses the same envelope with `data: Provider`
(`userType: "store" | "haraj"`, `approvalStatus: "wait"`).

Validation error:

```json
{ "key": "fail", "message": "رقم الجوال مستخدم من قبل", "status": 400 }
```

Unauthorized:

```json
{ "key": "unauthorized", "message": "يجب تسجيل الدخول", "status": 419 }
```

Paginated list:

```json
{
  "key": "success",
  "message": "تمت العملية بنجاح",
  "status": 200,
  "data": [ { "id": "…", "title": "…" } ],
  "paginate": { "currentPage": 1, "lastPage": 5, "perPage": 20, "total": 93 }
}
```

---

## 10. Swagger prohibitions

Never document, in a schema, an example, a description, or a default:

- `password`, `confirmPassword` values (the **field** may be documented; a real value may not)
- `activationCode` / OTP — not in any response schema, not in any example
- `JWT_SECRET`, `CRYPTO_HASH`, `SECRET_KEY`, `OTO_REFRESH_TOKEN`, `AGORA_APP_CERTIFICATE`,
  or any other `.env` value
- Real customer data — phone numbers, names, addresses, IBANs, commercial numbers.
  Use obvious placeholders.
- Stack traces or raw driver/SDK error messages
- Internal Mongo details — `__v`, `$`-operators, aggregation pipelines, collection names
- Admin/dashboard-only endpoints in a public-facing spec
- **Endpoints that do not exist in code.** In particular `/hyperPay-*` and `/paymentMethods`
  are currently unreachable (`PaymentRoute.payment()` is commented out of the aggregator) —
  do not document them as live.

Also avoid:

- Bulk-documenting untouched endpoints
- **Back-documenting the base's old-product endpoints** (haraj, advertisements,
  settlements, OTO shipping, hyperPay) — they are not part of the new product's contract
- Inventing enum values instead of copying from `src/helpers/enums/`
- Copying a schema from an old model's shape instead of the new DTO you built
- Claiming a status code the handler cannot actually return
- Adding a Swagger UI route without explicit approval

---

## 11. Definition of done for a docs update

- [ ] Only endpoints touched by this task were added or changed.
- [ ] Every documented field exists in the actual `@returnObj` DTO.
- [ ] Every documented status code is one the handler can really return.
- [ ] `security` matches the route's bucket (`unRequireAuthRoutes` → `security: []`).
- [ ] The `lang` header parameter is present.
- [ ] No password, OTP, token value, secret, or real customer data appears anywhere.
- [ ] Enum values copied verbatim from `src/helpers/enums/`.
- [ ] The corresponding Postman request was updated too.
