# Project Architecture — Base Codebase

> ## ⚠️ Read this first
>
> **This document describes the BASE codebase, not the final product.**
>
> This repository is a starter/base project. A **new** business domain is being built on
> top of it. Therefore:
>
> - **The technical spine below is authoritative** — routing classes, validation chains,
>   the `{ key, message, status }` envelope, error handling, auth, uploads, i18n, cron,
>   socket, tests. Copy these patterns.
> - **The business domain below is NOT authoritative.** The 67 models, the endpoint
>   inventory, the dashboard modules — those describe the *previous* product (haraj,
>   advertisements, settlements, OTO shipping, …). They are reference material for style,
>   not a specification of what to build.
> - New models (`Product`, `ProductAttribute`, `ProductVariant`, `Category`, `Order`,
>   `Auction`, `Wallet`, `Review`, …) are being created from scratch. Do not assume any of
>   them already exists, and do not reuse a same-named old model without checking.
>
> Design guidance for the new domain: **`docs/DOMAIN_MODELING_GUIDE.md`**.
> Working rules: **`AGENTS.md` § Base Codebase vs New Business Domain**.

Produced by a full read-only scan of the repository. Facts here were verified against
the source; where behaviour is broken or dead, it is marked as such rather than
described as intended.

> Current-state reconciliation (2026-08-04): this document remains the historical base
> scan and some measured counts/API-doc statements below are stale after subsequent
> implementation waves. The current domain-first transition, updated counts, OpenAPI
> status, ownership map, and migration gates live in
> `docs/FINAL_PROJECT_ARCHITECTURE_BLUEPRINT.md`. Do not use old counts or the old
> “OpenAPI absent” statement as current truth.

### How to read each section

| Section | Status for the new product |
|---|---|
| Tech Stack, Entry Points, Run scripts, Folder Map | ✅ **Reuse as-is** |
| Routing System (the class pattern, buckets, aggregator) | ✅ **Reuse the pattern** |
| Controllers, Validations, Middlewares, Services/Helpers | ✅ **Reuse the pattern** |
| API Response Format, Error Handling | ✅ **Reuse as-is — this is the contract** |
| Auth & Permissions (JWT + UserToken + session) | ✅ **Reuse the mechanism** |
| Views/Dashboard, Public assets, Cron & Realtime | ✅ **Reuse the skeleton** |
| **API endpoint inventory** | ⚠️ Old product's endpoints — reference only |
| **Models section (the 67 models)** | ⚠️ Old product's domain — style reference only |
| **Dashboard route groups / modules** | ⚠️ Old product's modules — reference only |
| Known Issues & Risks | ✅ **Applies to the base you are inheriting — read it** |

### Verified counts (re-checked against the working tree)

| Metric | Count |
|---|---|
| API route folders (`src/routes/api/`) | 16 → 15 domain classes + `indexRoute` |
| API controllers (`src/controllers/api/*.js`) | 19 |
| Dashboard controller folders | 40 |
| Mongoose models (`src/models/*.js`) | 67 |
| Enums (`src/helpers/enums/*.js`) | 38 |
| Validators — api / dashboard / infosite / webview | 15 / 32 / 2 / 1 |
| EJS views — total / admin / infoSite / webview | 430 / 411 / 15 / 4 |
| Service folders (`src/services/*/`) | 14 |
| `returnObject.js` / `adminRoute.js` line counts | 5870 / 1676 |

---

## Where do I put X? (cheat sheet)

| I need to… | File |
|---|---|
| **Design a new business model** | `docs/DOMAIN_MODELING_GUIDE.md` first, then `src/models/<entity>Model.js` |
| Add an API endpoint | `src/routes/api/<Domain>Route/<Domain>Route.js` → then wire in `indexRoute.js` |
| Add API input rules | `src/utils/validations/api/<domain>.js` |
| Add API business logic | `src/helpers/api/<Domain>.js` |
| Shape an API response body | `src/helpers/returnObject/returnObject.js` (`exports.<entity>`) |
| Add a status / type value | `src/helpers/enums/<name>.enum.js` |
| Add a message string | `src/locales/ar/<ns>.json` **and** `src/locales/en/<ns>.json` |
| Add a dashboard page | `adminRoute.js` + `src/controllers/dashboard/<m>Controller/` + `views/admin/<m>/` |
| Add a dashboard input rule | `src/utils/validations/dashboard/<module>.js` |
| Add a collection | `src/models/<entity>Model.js` |
| Add a scheduled job | `src/helpers/cronJopFn/cronJobFn.js` (export it!) + `addCronJobToDB` |
| Add a socket event | `listeners/socketManger/<domain>Helper/` + `<domain>Validation/` + `socket.js` |
| Call an external API | `src/services/<vendor>/` — never inline `axios` in a controller |
| Add a unit test | `test/<name>.test.js` (plain Node + `assert`); `npm test` discovers it automatically |
| Send a push notification | `src/helpers/send-notification/` (per-user) or `send-notify/` (cohort) |
| Paginate | API: `ApiFeature.paginate()` · Dashboard: `src/helpers/pagination/pagination.js` |

---

## Overview

**Zafirra** is a Node.js monolith that serves three surfaces from one Express app:

1. **Mobile/public REST API** under `/api` — clients (buyers), providers (stores and
   "haraj" scrap dealers), products, cart/orders, auctions with live streaming and
   bidding, advertisements, wallet/financials, chat, and notifications.
2. **Admin dashboard** under `/dashboard` — server-rendered EJS (≈411 admin views),
   session + JWT auth, CRUD over every domain, exports, reports, and settings.
3. **Webview / info-site** under `/web` — payment checkout + callback, privacy page,
   account-deletion flow, and OTO shipping webhooks.

A Socket.IO layer runs on the same HTTP(S) server for auctions, live streaming, chat,
and voice/video call signalling. A DB-backed cron system schedules auction lifecycle,
payment deadlines, subscription expiry, and order timeouts.

Domain language: Arabic-first (`defaultLocale: 'ar'`), bilingual `ar`/`en` via `i18n`.

---

## Tech Stack

From `package.json` (node `v18.17.1`):

| Area | Packages |
|---|---|
| HTTP | `express@^5.1.0`, `cors`, `helmet`, `morgan`, `http-status-codes` |
| DB | `mongoose@^8.13.2`, `mongodb`, `mongoose-sequence`, `mongoose-i18n-localize`, `connect-mongo` |
| Auth | `jsonwebtoken`, `passport`, `passport-jwt`, `passport-local`, `passport-facebook`, `express-session`, `cookie-parser`, `csurf` |
| Validation | `express-validator@^7.2.1`, `libphonenumber-js`, `persianjs`, `magic-bytes.js` |
| Views | `ejs`, `express-ejs-layouts`, `connect-flash`, `jquery` (served statically) |
| Files | `express-fileupload`, `multer`, `sharp`, `fs-extra`, `exceljs`, `pdfkit`, `qrcode` |
| Realtime | `socket.io`, `socket.io-client` |
| Media/RTC | `agora-token` (auction live), Vonage/OpenTok via `axios` |
| Push | `apn` (iOS), `googleapis` (FCM v1) |
| Jobs | `cron` |
| Misc | `moment`, `i18n`, `axios`, `node-geocoder`, `nodemailer`, `openai`, `module-alias`, `dotenv` |
| Dev | `@faker-js/faker`, `nodemon` |

**Not present:** no `swagger-*`, no `jest`/`mocha`, no `bcrypt` (passwords use Node
`crypto` HMAC), no TypeScript, no linter config.

Path aliases (`package.json._moduleAliases` + `jsconfig.json`):

```
@root       → src/
@listeners  → listeners/socketManger
@returnObj  → src/helpers/returnObject/returnObject.js
@src-routes → src/routes
```

---

## Entry Points

- **`app.js`** — class `Server`. Registers `module-alias`, loads `.env` via
  `loadEnvFile('./.env')`, connects Mongo, wires routes through `AppInitializer`,
  starts the server, attaches Socket.IO, and calls `scheduleCronJobs()`.
  - `node app dev` → HTTP on `LOCALE_ADDRESS:HTTP`.
  - no `dev` argv → HTTPS on `SERVER_ADDRESS:HTTPS` (reads key/cert/ca file paths from
    env) **and additionally** starts HTTP, unless `my <port>` argv is given.
  - Socket.IO is attached per protocol; `_socketIoAttached` exists to guard double
    attachment but is not currently checked in `startServerWithProtocol`.
- **`src/routes/index/index.js`** — class `AppInitializer`: global middleware, i18n,
  view engine, the three mounts, 404 page, and the final JSON error handler.
- **`listeners/socketManger/socket.js`** — class `SocketEvents`, all realtime handlers.

### Run scripts (`package.json`)

| Script | Command | Notes |
|---|---|---|
| `npm start` | `nodemon` | production-ish (HTTPS + HTTP) |
| `npm run start:dev` | `nodemon app dev` | HTTP only — **use this for local work** |
| `npm run start:dev:single` | `nodemon app dev single` | single-instance dev |
| `npm run start:forever` | `forever start app.js` | requires global `forever` |
| `npm test` | `node --test test/*.test.js` runs every test file in `test/` | safe unit/contract suite; excludes manual scripts and has no DB/server bootstrap |
| `npm run seed` | seeds index/city/village/district/permissions/exams | ⚠️ writes to DB |
| `npm run destroy` | destroys those seeds | ⚠️ **destructive** |
| `npm run delete-all-collections` | `src/helpers/deleteCollections.js` | ⚠️ **destructive** |
| `npm run listPermissions` | regenerates `src/helpers/permissions/permissions.js` from `adminRoute.js` + merges i18n keys | safe, writes source files |
| `npm run export` / `import` | `src/collections/export.js` / `import.js` | ⚠️ directory is gitignored/absent |

Agents must never run `destroy`, `delete-all-collections`, `seed`, `export`, or
`import` on their own initiative.

---

## Folder Map

| Folder | Purpose | Notes |
|---|---|---|
| `app.js` | Server bootstrap | class `Server` |
| `src/routes/index/` | `AppInitializer` — middleware, mounts, error handler | do not restructure |
| `src/routes/api/` | 15 domain route classes + `indexRoute` aggregator (16 folders) | class pattern, two auth buckets |
| `src/routes/dashboard/` | `adminRoute.js` (~1.7k lines, flat router), `authRoute.js`, `infoSiteRoute.js` | `infoSiteRoute` is **required but never mounted** (dead) |
| `src/routes/webview/` | `/web` routes: checkout, callback, privacy, delete-account, OTO webhooks | only class-based router outside `api/` |
| `src/controllers/api/` | 19 thin wrappers delegating to `helpers/api/` | keep them thin |
| `src/controllers/dashboard/` | 40 EJS controllers, one folder each | render on GET, JSON on mutation |
| `src/controllers/webview/` | webview controller | payment + account flows |
| `src/models/` | 67 flat Mongoose models | `class X extends Schema` style |
| `src/helpers/api/` | Real API business logic (`Auth.js`, `Order.js`, `Product.js`, `Auction.js`, …) + `ApiResponse`, `ApiError`, `ApiFeature` | this is the service layer |
| `src/helpers/returnObject/` | `@returnObj` — the single response serializer (~5.9k lines) | every API DTO lives here |
| `src/helpers/enums/` | 38 frozen enums | source of truth for statuses |
| `src/helpers/` (other) | pagination, notifications, wallet, permissions catalogue, cron functions, file/name/image helpers, `modelMap` | mixed utility layer |
| `src/middlewares/` | request IDs, dashboard authentication/authorization, CSRF error handling, token/language helpers | `requestId` is global; `rbacPolicy` is design-only and not wired |
| `src/services/` | structured logger plus agora, cache, convertCurrency, CronManger, email, errorHandler, location, notification, passport, sendSMS, shipping (OTO), socketForService, vonage, authenticationWebsite | infrastructure and external integrations |
| `src/utils/` | validations, showErrors, token, generateCode, db, url, server, auction, resizeFiles, deleteFiles, distance, … | infrastructure utilities |
| `src/locales/{ar,en}/` | i18n JSON catalogues (loaded statically at boot) | add keys to **both** |
| `src/seeder/` | seed + `permissions.js` generator | DB-writing |
| `listeners/socketManger/` | Socket.IO event class + per-domain helpers/validators | auction, live, chat, call |
| `views/admin` (411 ejs) · `views/infoSite` (15) · `views/webview` (4) | Server-rendered UI | layouts in `views/admin/layouts/` |
| `public/` | Static assets served at `/` — `admin/` (3.8k files), `includes/`, `infoSite/`, `webview/`, `notification/` (FCM web push) | uploads land in `public/assets/uploads` (gitignored) |
| `docs/` | Project documentation (this folder) | |
| `test/` | 32 self-contained Node test files | Node test runner + `assert`; default suite is isolated from DB/server/external services |
| `scripts/` | Socket call-flow test script + Postman collection | manual tooling |
| `tasks/` | `todo.md`, `lessons.md` | working notes |
| `.cursor/` | implementation plans, skills, Postman collections, analysis | historical spec material |

---

## Routing System

### Global middleware order (`src/routes/index/index.js`)

`requestId` → structured HTTP access log → `express.json` (50mb) → `urlencoded`
→ explicit static mounts for `public/` and the configured frontend directory
→ `express-ejs-layouts` → `body-parser` → `connect-flash` → `cors()` → `cookie-parser`
→ `csrfTokens` (a 4-arg error handler, see Error Handling).

The repository root is intentionally **not** a static mount. Each response carries
`X-Request-Id`; a valid incoming value is preserved, otherwise a UUID is generated.
The access log is one JSON record per completed request and omits bodies, query strings,
headers, cookies, and credentials.

Then: view engine `ejs`, views dir = `views/`, i18n locale set per request from the
`lang` **header** (default `ar`), static i18n catalogue built by `loadCatalog()`.

### Mounts

```js
app.use("/api",       upload(), apiRoutes);
app.use("/dashboard", setLayout, session('admin'), setLang, authRouter, adminRouter);
app.use("/web",       setLayout, session('web'),   setLang, webviewRouter);
```

Unmatched requests render `views/admin/404Page/error-404.ejs`.

### API routing (`src/routes/api/indexRoute/indexRoute.js`)

The aggregator applies, in order:

1. `GlobalValidator.validateLang()` — `lang` header must be `ar`/`en`.
2. **Public** buckets: `AuthRoute.unRequireAuthRoutes()`, `MoreRoute`, `HomeRoute`,
   `AdvertisementRoute`, `AuctionRoute`, `ShippingRoute`.
3. `requireAuth` (passport JWT).
4. **Authenticated** buckets: `HomeRoute.registerRoutes()`, `AuthRoute`, `ChatRoute.chat()`,
   `MoreRoute.more()`, `OrderRoute.order()`, `FinancialRoute.financial()`, `ProductRoute`,
   `UserRoute`, `CartRoute`, `ShippingRoute`, `AdvertisementRoute`, `AuctionRoute`,
   `BidRoute`, `HarajRoute`.

`PaymentRoute.payment()` is currently **commented out** in the aggregator.

Route class shape:

```js
class AuthRoutes {
  constructor() { this.router = express.Router(); }
  unRequireAuthRoutes() { const router = express.Router(); router.post('/signup-client', AuthValidator.getValidationChain(AuthValidator.validateSignupClient()), auth.signupClient); return router; }
  registerRoutes()      { this.router.get('/profile', auth.profile); return this.getRouter(); }
  getRouter()           { return this.router; }
}
module.exports = new AuthRoutes();
```

### API endpoint inventory (paths relative to `/api`)

> ⚠️ **Previous product's endpoints.** Listed so you can see the routing conventions in
> action and avoid path collisions. Not a specification of the new product's API.

| Domain | Endpoints |
|---|---|
| **Auth** | `POST /signup-client`, `POST /signup-provider`, `POST /signin`, `PATCH /activate`, `PATCH /send-code`, `PATCH /forget-password` *(public)* · `PATCH /change-password`, `GET /profile`, `PATCH /update-profile`, `PATCH /change-phone`, `PATCH /update-password`, `POST /signout`, `DELETE /delete-account` |
| **Home** | `GET /sliders`, `GET /rescue`, `GET /home/client-web`, `GET /home`, `GET /home-provider`, `GET /home/client-app`, `PATCH /isAvailable` |
| **Product** | `POST /product`, `PUT /product`, `DELETE /product`, `GET /productDetails`, `GET /product/provider`, `POST /gold-request` |
| **Cart** | `POST/GET/DELETE/PATCH /cart`, `GET /cart/count` |
| **Order** | `GET /orders`, `GET /order`, `GET /orderDetails`, `GET /productDetails`, `POST /confirmOrder`, `POST /payment`, `PATCH /order/{accept,reject,cancel,finish,preparing,deliveredShipping}`, `PATCH /shippingData`, `GET /shippingCompanies` |
| **Auction** | `POST /create-auction`, `PATCH /update-auction`, `DELETE /delete-auction`, `GET /list-auctions`, `GET /auction-details`, `POST /pay-auction-deposit`, `POST /pay-auction`, `PATCH /accept-auction`, `PATCH /reject-auction`, `PATCH /cancel-auction`, `PATCH /auction-delivered`, `POST /start-live`, `GET /live-comments`, `GET /call/credential` |
| **Bid** | `POST /pay-deposit`, `POST /place`, `GET /auction/:auctionId`, `GET /my-bids`, `GET /bids-list` |
| **Advertisement** | `GET /advertisements`, `GET /advertisements/details`, `POST /advertisements`, `PUT /advertisements/update`, `DELETE /advertisements/delete`, `PUT /advertisements/refresh`, `POST /advertisements/report`, `GET /client/advertisements`, `GET /client`, `GET/POST/DELETE /favorites` |
| **Haraj** | `GET /haraj-home`, `GET /available-harajs`, `PATCH /request-haraj` (+2 status patches) |
| **Chat** | `GET /chats`, `GET /chat/:id`, `GET /chat/messages`, `POST /chat/message` |
| **Financial** | `GET /due-financials`, `POST /requestSettlement`, `GET /settlements`, `GET /debts`, `GET /debt-details`, `GET /settlement` |
| **More** | wallet, packages/subscription, notifications (list/toggle/count/delete), contact, banners, complaint, debits, rates, language, about/fqs/privacy/terms/setting/payments/intros/regions/reasons/departments/subdepartments |
| **Shipping** | `GET /shipping/awb/:subOrderId` |
| **Payment** | `GET /hyperPay-brands`, `GET /paymentMethods`, `POST /hyperPay-index`, `POST /hyperPay-result` — **route not mounted** |

`requireAuth` additionally whitelists a set of paths so anonymous access falls through
even inside the authenticated buckets: `/home`, `/contact`, `/complaint`,
`/advertisements/details`, `/home/client-app`, `/home/client-web`, `/productDetails`,
`/rescue`, `/product/provider`, `/client`.

### Dashboard routing

`src/routes/dashboard/adminRoute/adminRoute.js` is one flat `express.Router()` with ~65
required controllers. Consistent chains:

- GET page: `csrfProtection, authentication, authorization, controller.fn`
- Mutation/AJAX: `authentication, authorizationAjax, [uploadsFiles()], <validator>(), csrfProtection, showErrorsApi, controller.fn`

Per-group URL shape: `/<group>`, `/<group>/create`, `/<group>/edit/:id`,
`/<group>/delete`, `/<group>/:type/:id`, `/<group>/filter`, `/<group>/exportAll`.
Express 5 dropped optional string params, so four groups use named regex paths
(`/^\/auctions(?:\/(?<type>[^\/]+))?$/` etc.); `/auctions/filter` **must** stay
registered before the `/auctions` regex.

---

## Controllers

### API controllers — thin wrappers only

```js
// src/controllers/api/authController.js
const Auth = require("@root/helpers/api/Auth");
module.exports = {
  signupClient(req, res) { Auth.signupClient(req, res); },
  signIn(req, res)      { Auth.signIn(req, res); },
  // …
};
```

No logic, no DB access, no response building. All of that belongs in
`src/helpers/api/<Domain>.js`.

### API business logic (`src/helpers/api/*.js`)

Class exported as an instance. Canonical method body:

```js
async signupClient(req, res) {
  const { lang } = req.headers;
  try {
    const dataBody = checkValidations(req);            // matchedData → whitelisted fields
    const id       = initId();
    const dir      = makeDir(`users/clients/${id}`);
    if (req.files?.avatar) dataBody.avatar = await uploadAnyFile(req, "image", dir, "avatar");

    const user = await Client.create({ ...dataBody, _id: id, activationCode: await generateCode(), activationCodeExpire: Date.now() + 60_000 });

    const apiResponse = new ApiResponse('needActive', i18n.__({ phrase: 'auth.accountCreatedSuccessfully', locale: lang }), StatusCodes.OK, await returnObject.client(user, lang));
    res.send(apiResponse);
  } catch (error) {
    console.log("Error:", error);
    throw errorHandler({ res, statusName: 'exception', i18nMessage: 'common.returnDeveloper', lang });
  }
}
```

Domain files: `Auth`, `User`, `Product`, `Order` (~2k lines), `Cart`, `Chat`, `Auction`,
`Haraj`, `Home`, `More`, `Financial`, `Payment` (strategy pattern: wallet/online/cash),
`Banner`, `Advertisement`, `ShippingHelper`, `OtoWebhook`.

### Dashboard controllers

`module.exports = { async fn(req,res){…} }` (some files use `exports.fn = …`).
Four recurring patterns:

- **Listing page** — `checkValidations(req)` → `handleType` status map → `ApiFeature`
  pagination → `pagination()` HTML string → `res.render("admin/<module>/index", { title, currentMenu, user: req.admin, csrfToken: req.csrfToken(), lang, i18n, moment, filterData, exportData, counts, paginationHtml })`.
- **AJAX filter** — renders the table fragment with `layout: false` and `res.send(html)`.
- **Show/edit** — `:type` URL segment selects the template: `admin/<module>/${type}`.
- **Mutation** — file handling → `Model.updateOne` → audit `report(adminId, {ar,en,ur}, method, url)`
  → optional `sendNotification` → **JSON**: `res.send(new ApiResponse("success", i18n.__("common.editSuccessful"), 200, { url: "/dashboard/<module>/all" }))`.
  The front-end uses `data.url` as the redirect target.

So: **GET = HTML, everything else = JSON envelope** consumed by jQuery/toastr.

---

## Models

> ⚠️ **These are the BASE (previous product's) models.** Read this section to learn the
> *style*, not to learn the domain. New-product models are designed from the analysis per
> `docs/DOMAIN_MODELING_GUIDE.md`. Do not reuse an old model to host new behaviour.

67 flat files in `src/models/`, named `<entity>Model.js`. Style: `class XSchema extends Schema`
(or `extends UserSchema`), instantiated once, then `model('X', schema)`.

### Convention audit (measured across all 67 models)

| Convention | Reality in the base | Verdict for new models |
|---|---|---|
| `{ timestamps: true, versionKey: false }` | 64/67 | ✅ adopt |
| `class X extends Schema` | 58/67 | ✅ adopt |
| Frozen enums via `Object.values(Enum)` | consistent | ✅ adopt |
| `mongoose-sequence` for human-readable numbers | 6 models | ✅ adopt where needed |
| `{ i18n: true }` + `mongoose-i18n-localize` | 8 models | ✅ adopt for user-visible text |
| Declared indexes | **only 8/67** | ❌ **do better — declare them** |
| Soft delete | **inconsistent**: `isDeleted` boolean (6) vs `status: 'delete'` (user models); no `deletedAt` anywhere | ❌ **standardize once for the new domain** |
| `select: false` on sensitive fields | **0/67** — protection lives only in the `@returnObj` DTO | ❌ **add it — defense in depth** |
| `pre(/^find/)` auto-populate | **31/67** — fires on every query incl. counts | ❌ **do not copy — populate in the service layer** |

The last four rows are the base's main modeling debts. New models must not inherit them.

### User hierarchy

`src/models/userModel.js` exports `{ UserSchema, userSchema }` — the shared base:

`avatar, countryCode, userType, name, phone, email, password, activationCode,
activationCodeExpire, isNotify, notifyCount, status (active|block|delete),
active (Boolean, default false), updatedPhone, balance, authFlow, location (2dsphere),
address (i18n), subscribe`

Hooks: `pre(/^find/)` populates `subscribe`; `pre('save')` hashes the password with
`crypto.createHmac('sha256', process.env.CRYPTO_HASH)` (unsalted HMAC — **not** bcrypt);
`comparePassword` re-derives and compares; `pre(['updateOne','findOneAndUpdate'])`
cascade-deletes `Device` + `UserToken` rows on soft-delete (`status: 'delete'`).

Derived: `Client` (`clientModel.js`), `Provider` (`providerModel.js` — adds `nationalId`,
`isAvailable`, `city`, `commercialImage`, `whatsappNumber`, `approvalStatus`,
`premiumSubscription`), `ProviderMeta` (copy of the provider schema, used as the
pending/edit-request mirror at signup), `Admin` (`adminModel.js` — `role` ref + `isSuperAdmin`,
`pre(/^find/)` always populates `role`).

### Other notable models

`productModel`, `orderModel`, `cartModel`, `auctionModel`, `bidModel`,
`auctionSubscriptionModel`, `auctionLiveCommentModel`, `advertisementModel`,
`adEditRequestModel`, `harajRequestsModel`, `chatModel`, `messageModel`,
`notificationModel`, `devicesModel`, `userTokensModel`, `financialTransactionsModel`,
`balanceHistoryModel`, `settlementModel`, `invoiceModel`, `couponModel`, `packageModel`,
`subscriptionModel`, `premiumSubscriptionModel`, `roleModel`, `permissionModel`
(**entirely commented out — no Permission collection exists**), `settingsModel`,
`siteSettingModel`, `smsSmtpModel`, `cronJobModel`, `countryModel`/`cityModel`/
`districtModel`/`villageModel`, `reportModel`/`adminReportsModel`, `callCredentialModel`,
`otoTokenModel`.

---

## Validations

Location: `src/utils/validations/{api,dashboard,infosite,webview}/` — 15 API, 32 dashboard,
2 infosite, 1 webview.

Each file exports a class of **static methods returning arrays of `express-validator`
chains**. Wiring glue, defined in every validator class:

```js
static getValidationChain(fn) { return [fn, showErrors]; }
```

Route usage:

```js
router.post('/signup-client', AuthValidator.getValidationChain(AuthValidator.validateSignupClient()), auth.signupClient);
```

`showErrorsApi` (`src/utils/showErrors/showErrorsApi.js`) takes the **first** error,
normalizes `msg` (which may be a string or a nested `{key,message,status}` object),
falls back to `common.validationError`, and responds
`res.status(status).json({ key, message, status })`.

Controllers then call `checkValidations(req)` (`src/controllers/api/sharedController.js`),
which throws if errors remain and otherwise returns `matchedData(req)` — **this is why
unknown request fields are dropped automatically**. Always declare every field you intend
to consume.

`GlobalValidator` (`src/utils/validations/api/global.js`) provides `validateLang()`,
`validateSecretKey()`, and `validateImageFile()` (magic-byte signature check via
`magic-bytes.js`, not client MIME).

Auth validator highlights (`src/utils/validations/api/auth.js`):

- `phone` — Persian/Arabic digits normalized (`persianjs`), validated against
  `countryCode` with `libphonenumber-js`, then uniqueness-checked across
  `Client → Provider → ProviderMeta` on `phone`/`updatedPhone` (also the
  `oppositePhone` variant) with `status != delete`.
- `password` — `/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/`.
- Provider signup — `userType` ∈ `[store, haraj]`; `commercialNumber` required only for
  `store`; bank group (`bankName`, `accountName`, `accountNumber` `/^\d{9,18}$/`,
  `iban` `/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/`) is all-or-nothing; `longitude`/`latitude` ranges.
- `activate` — `activationCode` `/^\d{4}$/`, `deviceId`, `deviceType` ∈ `[android, ios, web]`.

---

## Middlewares

| File | Status | Behaviour |
|---|---|---|
| `requestId.js` | **live** | Preserves a safe `X-Request-Id` (1–128 restricted characters) or generates a UUID; sets `req.requestId` and the response header. |
| `rbacPolicy.js` | **design-only** | Pure, tested access-decision evaluator. It is deliberately not wired into dashboard routes; see `docs/RBAC_DESIGN.md`. |
| `authentication.js` | **live** | Reads `req.session.token`, `jwt.verify`, loads `Admin` (role auto-populated), rejects non-`active`, sets `req.admin` + avatar. Two branches call an undefined `i1__(...)`; the `catch` only `console.log`s (no `next(error)`), so a thrown error hangs the request. |
| `authorization.js` | **no-op** | Builds permission regexes from `req.admin.role.permissions` and computes `isAuthorized`, but every redirect/deny line is commented out. |
| `authorizationAjax.js` | **no-op** | Same; the deny branch is commented out and references an undefined `errorRes`. |
| `csrfTokens.js` | live-ish | 4-arg handler for `EBADCSRFTOKEN`; it targets a missing `.html` file even though the view is EJS, and calls `next()` without the error for non-CSRF failures. Route coverage is audited in `docs/CSRF_AUDIT.md`. |
| `verifyToken.js` | **dead** | Not imported by any route. |
| `verifyTokenAjax.js` | **dead** | Not imported; references an undefined `errorRes`. |
| `setLang.js` | **dead** | Not imported; `AppInitializer` uses its own bound `setLang`. |

API auth is separate: `src/services/passport/passport.js` exports `requireAuth`,
`optionalAuth`, `authorize`. JWT (Bearer) → `payload.endedAt` expiry check → model via
`modelMap(payload.userType)` → `active: true` → token must still exist in `UserToken`
→ `status` checks (`delete` → unauthorized, `block` → blocked) → `req.user`.

---

## Services / Helpers

| Service | Responsibility |
|---|---|
| `services/logger` | structured application logging plus JSON HTTP logging using existing `morgan`, with recursive sensitive-data redaction and production stack suppression |
| `services/passport` | API JWT auth (`requireAuth`, `optionalAuth`) |
| `services/errorHandler` | Central error emitter — **always responds and then throws** |
| `services/notification` | FCM v1 + APNs fan-out (`handelNotification`), VoIP pushes, device pruning on invalid tokens |
| `services/sendSMS` | Provider-driven SMS (`yamamah`, `mobily`, …) from the active `SmsSmtp` row |
| `services/email` | nodemailer over SMTP settings stored in DB |
| `services/CronManger` | `CronManagerInMemory` (node-cron singleton) + `cronJob.js` (DB-backed registry) |
| `services/agora` | Auction live-stream RTC tokens (host/audience, deterministic channel + uid) |
| `services/vonage` | Video session + token for calls |
| `services/shipping/otoShipping.js` | OTO courier API — token refresh, delivery fee, create order, status, webhook registration |
| `services/socketForService` | Bridge letting non-request code emit Socket.IO events |
| `services/location`, `convertCurrency`, `cache`, `authenticationWebsite` | Geocoding, FX, Redis query cache (**broken/dead**), legacy website session auth |

Key helpers:

- `@returnObj` — every API DTO. Never return a raw document.
- `ApiFeature` — `filter()`, `sort()`, `limitFields()`, `search()`, `paginate(count)` (default limit 20).
- `pagination/pagination.js` — dashboard-only, returns pre-rendered Bootstrap HTML.
- `modelMap` — `userType` → Mongoose model (`admin→Admin`, `client→Client`, `store|haraj|provider→Provider`).
- `send-notification/` — per-user notification; `send-notify/` — dashboard cohort broadcast.
- `wallet/` — admin balance adjustment + `checkBalance` guard.
- `enums/` — 38 frozen enums; use them, never string literals.
- `cronJopFn/cronJobFn.js` (~1.6k lines) — every scheduled handler.

---

## Auth & Permissions

### API (mobile)

1. Signup (`/api/signup-client`, `/api/signup-provider`) → user created with
   `active: false`, `activationCode` + 60s expiry, response `key: "needActive"` (HTTP 200).
   *SMS dispatch is currently commented out in `Auth.js`.*
2. `/api/activate` with the 4-digit code + `deviceId`/`deviceType` → activates.
3. `/api/signin` → if `!user.active` returns `needActive` and regenerates the code;
   providers additionally require `approvalStatus === accept`; otherwise issues a JWT via
   `src/utils/token/token.js` (`{ sub, phone, userType, endedAt: now + 10d }`, `expiresIn: 10d`)
   and stores it in `UserToken`, plus registers the device.
4. `Authorization: Bearer <token>` on subsequent calls. `signout` deletes the `UserToken` row.

### Dashboard

Session cookie (`admin`, backed by `connect-mongo`, TTL 10 days) carrying a JWT that
`authentication.js` verifies against `Admin`. CSRF via `csurf` on all page/mutation routes.

### RBAC — stored but **not enforced**

- `roleModel.js`: `{ name: {ar,en}, description, isAdmin, isDeleted, permissions: [String] }`.
  Permissions are plain route-path strings (`/supervisions/edit/:id`).
- `permissionModel.js` is entirely commented out; there is no Permission collection.
- The catalogue is generated by `npm run listPermissions` → `src/helpers/permissions/permissions.js`
  (`[{ parent, child: [{ method: [...], route }] }]`), which also merges i18n keys into
  `src/locales/*/permissions.json` (with `:` stripped from the key).
- `src/helpers/permissions.js` (the 719-line hand-written catalogue) is **dead** — nothing imports it.
- **Enforcement is disabled**: `authorization.js` / `authorizationAjax.js` compute the
  decision but every deny branch is commented out; the sidebar has no permission gating;
  `isSuperAdmin` is used only to *find* a notification recipient. Any admin with
  `status === 'active'` can reach every `/dashboard/*` route.

---

## Views / Dashboard

430 `.ejs` files: `views/admin` (411), `views/infoSite` (15), `views/webview` (4).

Layouts: `views/admin/layouts/layout.ejs` (dashboard, set by the `/dashboard` mount),
`layoutAuth.ejs` (login), `views/infoSite/layouts/layout.ejs`, `views/webview/master.ejs`.
`layout extractStyles` / `extractScripts` are enabled, so views push into `style`/`script`.

Recurring per-module CRUD template: `index.ejs`, `create.ejs`, `edit.ejs`, `show.ejs`,
`dataTable.ejs`, `tds.ejs`. Shared partials live in `views/admin/includes/` (33 files:
filters, checkAll, deleteAll, dataTable, excel, pdf, map, flatPicker, validation…).

`views/admin/users/` = `clients` (18), `providers` (20), `providersEditRequest` (5),
`providersMeta` (5), `supervisors` (8).

### Public assets

`public/` is served at `/`. `public/admin/` (3808 files: `app-assets/`, `assets/`),
`public/includes/` (ckeditor, jquery, toastr, lightgallery, fancybox),
`public/infoSite/`, `public/webview/`, `public/notification/` (Firebase web push:
`firebase.js`, `firebase-messaging-sw.js`, `sendNotification.js`).
User uploads: `public/assets/uploads/…` (gitignored).

---

## API Response Format

### Success — `src/helpers/api/ApiResponse.js`

```json
{
  "key": "success",
  "message": "تمت العملية بنجاح",
  "status": 200,
  "data": { }
}
```

With `type === 'api'` an extra block is appended:

```json
"paginate": { "currentPage": 1, "lastPage": 5, "perPage": 20, "total": 93 }
```

Note: `key` is **not** always `"success"`. Signup and inactive sign-in return
`key: "needActive"` with HTTP 200.

### Error — `src/helpers/api/ApiError.js`

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

Key → HTTP status mapping (`ApiError.GetCode`):

| key | status |
|---|---|
| `success` | 200 |
| `needActive` | 203 |
| `fail` | 400 |
| `notFound` | 400 (`responseError` also emits the legacy `key: "fail"`) |
| `unauthorized` | 419 |
| `blocked` | 423 |
| `exception` | 500 |
| *(unknown)* | 200 |

Validation errors come out of `showErrorsApi` in the same three-field shape
(`key` defaults `fail`, `status` defaults 400).

Socket payloads use the same envelope, e.g.
`io.to(room).emit('chat:message-received', new ApiResponse('success', …, 200, payload))`.

---

## Error Handling

`src/services/errorHandler/error.handler.js` has two calling conventions:

```js
// API style
throw errorHandler({ res, statusName: 'fail', i18nMessage: 'auth.userNotFound', lang });
// Dashboard style
errorHandler(error, req, res);
```

It **writes the response and then throws**, which is why call sites use
`throw errorHandler({...})` — the throw aborts the rest of the handler after the response
has already been sent. Do not "fix" this in passing; match the existing pattern.

The final Express error handler (`AppInitializer.initializeErrorHandling`) returns JSON
for both API-shaped errors (`{key, message, status}` passthrough) and anything else
(`key = 'exception'` for ≥500, `'fail'` otherwise). It never leaks a stack trace.

---

## Swagger / API Docs

**There is no Swagger/OpenAPI in this project today.** No `swagger-jsdoc`,
`swagger-ui-express`, `@apidevtools/*`, no `openapi.yaml`, no JSDoc `@openapi` blocks.
(The only `swagger` grep hits are unrelated strings in `src/locales/*/settings.json`.)

The current de-facto API contract lives in Postman collections:

- `.cursor/postman/Zafirra.postman_collection.json`
- `.cursor/postman/advertisement-api/*.json`
- `scripts/zafirra-call-flow.postman_collection.json`

See `docs/SWAGGER_GUIDE.md` for the rules and the approved zero-dependency path forward.

---

## Tests

No third-party test framework is installed. `npm test` uses Node's built-in test runner
with the `test/*.test.js` glob, which covers all 32 test files instead of maintaining a
short, easy-to-stale filename list. The explicit directory glob excludes manual
`scripts/test-*.js` tools. The files use plain Node + `assert`; existing executable test
runners remain compatible with `node --test`.

Before Phase 1, the script hard-coded five filenames while 27 test files existed, so 22
tests were silently skipped. Using bare `node --test` is also unsuitable here because it
discovers `scripts/test-call-socket-flow.js`, a manual driver that writes a Postman
artifact. The explicit test-directory glob fixes both discovery problems.

The default suite is self-contained: it does not connect to MongoDB, start the
application server, or call external services. Phase 1 adds focused contracts for
response/error compatibility, request IDs, logger redaction, static-mount safety, and
the non-enforcing RBAC policy. Future integration tests that need DB/server/external
state must use a separately named script rather than entering the default unit suite.

`scripts/test-call-socket-flow.js` is a manual socket-flow driver, not part of `npm test`.

---

## Cron & Realtime

**Cron** — `scheduleCronJobs()` runs at boot, loads `CronJob.find({status: ACTIVE})`,
maps `job.fn` to a handler in `src/helpers/cronJopFn/cronJobFn.js`, and fires immediately
for deadlines that already passed while the server was down. Schedules are one-shot
`Date` deadlines, not cron expressions. Handlers: `startAuction`, `endAuction`,
`auctionEndingReminder`, `paymentDeadline`, `paymentReminder`, `receiveConfirmDeadline`,
`harajLateResponse`, `productExpireToNeedsRescue`, `sendReminderNotification`,
`subscriptionExpireNotification`, `cancelUnpaidOrder`.
Two `fn` names are written to the DB with **no matching handler** (`endExpireCoupon`,
`subscriptionDailyLimitReset`) — those rows get marked FINISHED at boot.

**Socket.IO** — `listeners/socketManger/socket.js`. Handshake query must carry
`userId, lang, userType, deviceType, deviceId` (validated; on failure it emits
`auction:error` and registers no handlers). Rooms: `auction:<id>`, `chat:<id>`, `user:<id>`.

Inbound: `auction:enter|bid|ended`, `start-live|enter-live|add-comment|exit-live`,
`chat:enter|message|exit`, `call:start|answer|finish`, `disconnect`.
Outbound: `connected`, `auction:error|viewers-updated|bid-updated|finished`,
`live:started|enter-ack|viewers-updated|comment|exit`,
`chat:error|message-received|participant-joined|participant-left|participants-updated`,
`call:error|waiting|answered|rejected|finished`.

---

## Known Issues & Risks

These were found during the scan. They are **recorded, not fixed** — fixing any of them
is a separate, explicitly-requested task.

### Security

1. **RBAC is not enforced.** `authorization.js` / `authorizationAjax.js` deny-branches are
   commented out. Any active admin reaches every `/dashboard/*` route regardless of role.
2. **OTP leaks into an API response.** `returnObject.client` returns `activationCode` in
   the client DTO. Any caller of `/api/signup-client`, `/api/signin`, `/api/profile`
   receives the live OTP.
3. **Passwords use unsalted HMAC-SHA256** (`crypto.createHmac('sha256', CRYPTO_HASH)`),
   not bcrypt/argon2. Identical passwords produce identical hashes; no per-user salt,
   no work factor.
4. **Resolved in Phase 1:** the repository-root `express.static("./")` mount was removed.
   Only explicit public and frontend build directories remain mounted.
5. Session secret is derived from a literal template (`secretKeySession${name}`), not from env.
6. `cors()` is enabled with no origin allowlist.
7. `helmet` is a dependency but is **not** applied in `AppInitializer`.
8. `httpsOptions` uses `rejectUnauthorized: false`.
9. Hardcoded third-party API keys exist in `src/services/location/location.js` and
   `src/services/convertCurrency/convertCurrency.js`.
10. Body limits are effectively unbounded on the `body-parser` layer
    (`limit: '500mb', parameterLimit: 100000000000`).
11. No rate limiting on `/api/signin`, `/api/send-code`, or `/api/activate` beyond the
    in-memory attempt maps inside `Auth.js` (per-process, lost on restart).

### Correctness / dead code

12. `authentication.js` calls an undefined `i1__(...)` in two branches; its `catch` never
    responds, so failures hang the request.
13. `csrfTokens.js` targets a `.html` file that does not exist and swallows non-CSRF
    errors by calling `next()` without forwarding the error.
14. `verifyToken.js`, `verifyTokenAjax.js`, `setLang.js`, `src/helpers/permissions.js`,
    `src/routes/dashboard/infoSiteRoute/infoSiteRoute.js` are all unreferenced.
15. `PaymentRoute.payment()` is commented out of the API aggregator, so
    `/api/hyperPay-*` and `/api/paymentMethods` are unreachable.
16. `generateCode()` produces **6** digits, while `validateActivate()` enforces `/^\d{4}$/`.
17. `services/cache/cache.js` requires a Redis client from `app.js`, which exports a
    `Server` class — dead/broken; `cleanCache.js` has a wrong relative path.
18. `utils/db/db.js` does not `await mongoose.connect`, so the success log fires before
    the connection resolves and failures escape the `try/catch`.
19. `helpers/wallet/wallet.js` catch block references an undefined `errorRes`.
20. `services/location/location.js` never invokes its callback.
21. `ApiResponse` `type === 'site'` branch references an undefined `appUrl`.
22. `socket.js` destructures `handleEndLive`, which `liveHelper/index.js` does not export.
23. `ProviderSchema.userType` enum is `[UserTypeEnum.PROVIDER]` only, while provider
    signup validates `userType ∈ [store, haraj]`. Verify against live data before
    touching provider creation.

### Organizational

24. `src/routes/dashboard/adminRoute/adminRoute.js` is ~1.7k lines and
    `src/helpers/returnObject/returnObject.js` is ~5.9k lines — both are change-risk
    hotspots and merge-conflict magnets.
25. Two divergent permission catalogues (one generated, one dead) with different route shapes.
26. `README.md` is empty (0 bytes).
27. No `.env.example`, so required configuration is undiscoverable without reading `.env`.
28. Mixed module styles (`module.exports = {}` vs `exports.fn =`), mixed comma-chained
    `const` declarations, inconsistent indentation.
29. No linter, no formatter, no CI test gate (`.gitlab-cd.yml` deploys only) — verified:
    no `.eslintrc*`, no `.prettierrc*`, no `eslint.config.*`.
30. **Declared-but-unused dependencies:** `helmet` and `multer` remain unused
    (`express-fileupload` handles uploads). `morgan` is now used by the structured HTTP
    logger. Dependency cleanup is outside Phase 1.
31. `csurf` coverage is incomplete: 62 of 155 registered mutation routes include
    `csrfProtection`, while 93 do not. See `docs/CSRF_AUDIT.md`; no enforcement was
    added during the audit.

### Future recommendations (do not implement without a request)

- Add `.env.example` with variable names only.
- Strip `activationCode` from `returnObject.client`.
- Decide the RBAC question explicitly: enforce it or delete the dead middleware.
- Delete the five unreferenced files listed above.
- Split `adminRoute.js` per domain and `returnObject.js` per entity.
- Introduce OpenAPI incrementally (see `docs/SWAGGER_GUIDE.md`).
- Apply `helmet` and an origin allowlist for `cors`.
- Add `.env.example`-driven config validation at boot.

---

## Important Notes For Agents

1. **Never return a raw Mongoose document.** Go through `@returnObj`.
2. **Never invent a response shape.** `ApiResponse` / `ApiError` only.
3. **`key` is not `status`.** `needActive` returns HTTP 200 with `key: "needActive"`.
   Mobile clients branch on `key`.
4. **Whitelist inputs by declaring them in the validator** — `matchedData` drops the rest.
5. **Add i18n keys to `ar` and `en` together.** The catalogue is loaded statically at
   boot (`updateFiles: false`), so a missing key surfaces as the raw key string.
6. **Public vs authenticated** is decided by which bucket the route goes in
   (`unRequireAuthRoutes()` vs `registerRoutes()`), not by a per-route middleware.
7. **Dashboard mutations return JSON with `data.url`**, not a redirect.
8. **Multipart dashboard routes need `uploadsFiles()` before `csrfProtection`.**
9. **Route ordering matters** in `adminRoute.js` — `/x/filter` before the `/x` regex.
10. **Do not run destructive npm scripts.** `destroy`, `delete-all-collections`, `seed`,
    `export`, `import` are off-limits without explicit instruction.
11. **Use `npm run start:dev`** for local runs; plain `npm start` needs TLS cert files.
12. **`app.js` reads `.env` directly at boot.** Never print its contents.
