# Backend MVC Architecture — Standard & Migration Plan

**Status:** the backend is *routes → controller → model*, with a service layer in only **16 of 61 modules**.
**Evidence:** **1,119** direct model calls sit in controllers vs **148** in services (88% bypass the service layer).

This document defines the target architecture, the rules, and an incremental migration order that keeps all 1,668 tests green.

---

## 1. The target: four layers, one direction

```
  HTTP request
      │
      ▼
┌─────────────┐  *.routes.ts     mount path + guards + validate(schema). No logic.
│   ROUTES    │
└─────┬───────┘
      ▼
┌─────────────┐  *.schema.ts     Zod. Shape/type/range validation only.
│ VALIDATION  │
└─────┬───────┘
      ▼
┌─────────────┐  *.controller.ts HTTP ONLY: read req, call service, send response.
│ CONTROLLER  │                  No Mongoose. No business math. No cross-module writes.
└─────┬───────┘
      ▼
┌─────────────┐  *.service.ts    ALL business logic + ALL data access.
│  SERVICE    │                  Framework-free: no req/res, returns plain data or throws AppError.
└─────┬───────┘
      ▼
┌─────────────┐  models/*.ts     Schema, indexes, hooks, domain invariants.
│   MODEL     │
└─────────────┘
```

**The dependency rule:** every arrow points down. A service must never import `express`, `req`, or `res`. A controller must never import a Mongoose model.

---

## 2. Layer contracts

### Routes — `*.routes.ts`
```ts
router.post("/", requireOutlet("manager"), validate(createCustomerSchema), createCustomer);
```
Mount, guard, validate, delegate. Nothing else.

### Controller — `*.controller.ts`
Its whole job is the HTTP boundary:
```ts
export const createCustomer = asyncHandler(async (req: Request, res: Response) => {
  const customer = await customersService.create({
    shopId: res.locals.outlet!.shopId,
    performedBy: performedByFrom(res.locals.outlet!),
    input: req.body,
  });
  return sendSuccess(res, customer, "Customer created", 201);
});
```
- Extracts from `req` / `res.locals` (auth context), calls **one** service function, shapes the response.
- Never touches a model. Never does money math. Never orchestrates multi-model writes.
- Target: **under ~15 lines per handler**.

### Service — `*.service.ts`
```ts
export async function create(params: CreateCustomerParams): Promise<ICustomer> {
  const phone = normalizeMobile(params.input.phone);
  if (phone && await Customer.exists({ shopId: params.shopId, phone, isDeleted: false }))
    throw Err.conflict("A customer with this mobile already exists");
  return Customer.create({ ...params.input, phone, shopId: params.shopId, performedBy: params.performedBy });
}
```
- Owns business rules, invariants, money math, and **all** model access.
- Signals failure by **throwing** (`Err.badRequest`, `Err.conflict`, …) — the central `errorHandler` converts to HTTP.
- Takes plain params, returns plain data → unit-testable without supertest.

### Model — `models/*.ts`
Schema, indexes, hooks, sign conventions. Already correct today — no change needed.

---

## 3. Rules (the ones that actually bite)

1. **No Mongoose import in a controller.** This one rule enforces most of the architecture.
2. **No `req`/`res` in a service.** Pass what it needs (`shopId`, `performedBy`, `input`).
3. **Services throw, controllers respond.** No `sendError` inside a service.
4. **Cross-module writes go service→service**, never controller→other module's model. (E.g. `customersService` calls `walletService.recordCustomerAdvance` — that pattern already exists and is correct.)
5. **One shared money util.** `round2` is currently redefined 27× in **two different formulas** (17× plain, 11× with `Number.EPSILON`). Move to `utils/money.ts` and import everywhere.
6. **Keep the file naming convention** — `<module>.routes.ts` / `.controller.ts` / `.service.ts` / `.schema.ts`. It is consistent today and worth preserving.

---

## 4. Reference implementation (already in the codebase)

`modules/wallet/` is the model to copy:
- `wallet.service.ts` (1,108 lines) owns `postWalletTransaction`, the settlement formula, credit-limit enforcement, reversals.
- `customer-wallet.service.ts`, `service-payer.service.ts`, `service-receivable.service.ts` split sub-domains cleanly.
- `wallet.controller.ts` orchestrates them.

The wallet controller is still 3,203 lines — so the pattern is right, the *split* is incomplete.

---

## 5. Migration order (highest value first)

Ranked by size × DB-coupling × business risk. Each step is independently shippable and test-verified.

| # | Module | Controller | DB calls | Extract into |
|---|---|---|---|---|
| 1 | **customers** | 462 | 21 | `customers.service.ts` — settle/mirror logic, list enrichment, export shaping |
| 2 | **sales** | 2,314 | 70 | `sales.service.ts` (create/price/settle), `sale-return.service.ts` — pricing helpers already split |
| 3 | **wallet** | 3,203 | 128 | move remaining orchestration into `service-*.ts`; controller → thin |
| 4 | **stock-requests** | 2,022 | 53 | `stock-request.service.ts` + `delivery.service.ts` (OTP/GPS hand-off) |
| 5 | **cash-handovers-chain** | 1,532+1,216 | 97 | `handover.service.ts` (ledger write, secret-report checks) shared by both controllers |
| 6 | **stock-audits** | 1,486 | 55 | `stock-audit.service.ts` (variance, dispute resolution) |
| 7 | **auth** | 1,193 | 28 | `auth.service.ts` (OTP issue/verify, session mint, geofence) |
| 8 | **purchase-invoices / products / edit-requests** | ~900 ea | 17-34 | one service each |

**Why customers first:** small enough to land in one pass, exercises every rule (money mirror, cross-module service call, export shaping), and gives the team a concrete in-repo template.

---

## 6. Safety procedure per module

1. Create `<module>.service.ts`; move logic **verbatim** (no behaviour change).
2. Controller becomes a thin caller; replace `sendError` with thrown `Err.*`.
3. Run that module's suite + any suite touching it.
4. `npx tsc --noEmit`.
5. Commit one module per commit, so a regression is trivially bisectable.

**Non-negotiable:** the 1,668-case suite must stay green at every step. This is a *structural* refactor — no behaviour changes, no "while I'm here" fixes.

---

## 7. What NOT to change

- Model layer, sign conventions, index definitions — all correct.
- The `routes/controller/schema/service` naming convention.
- Guard composition in routes (`requireOutlet`, `validate`) — already exemplary.
- Idempotency and atomic-update patterns — leave them exactly as they are.
