# ERP Backend — Development Tasks (Phase 1) Reference: [BACKEND_SETUP.md](./BACKEND_SETUP.md) Use this checklist when building or extending the backend. Follow the build order in Section 1. --- ## 1. Project Scaffold - [x] Create `package.json` with dependencies and scripts from the spec - [x] Install dependencies (`npm install`) - [x] Add `.env.example` (never commit `.env`) - [x] Configure ESLint, Prettier, Husky, lint-staged - [x] Add `jest.config.js` with `dotenv/config` setup file --- ## 2. Database & Prisma - [x] Define `prisma/schema.prisma`: - Auth: `User`, `Role`, `Module`, `Permission`, `RolePermission`, `RefreshToken`, `PasswordResetToken` - System: `AuditLog`, `DocumentSeries` - Business: Vendor (+ sub-tables), PurchaseOrder (+ items), GRN (+ items), Asset (+ transfers), all masters - [x] Use `PascalCase` models, `snake_case` fields, `@@map` to plural table names - [ ] Add common columns on every business table: `is_active`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at` - [ ] Run `npx prisma migrate dev --name init` - [x] Run `npx prisma generate` - [x] Implement `prisma/seed.js` — bootstrap Super Admin user (modules/permissions/roles from DDL) - [ ] Full idempotent seed (modules → permissions → roles → role_permissions) if not using DDL ### Seed requirements | Step | Content | |------|---------| | Modules | `USERS`, `ROLES`, `MASTERS`, `VENDOR`, `PURCHASE_ORDER`, `GRN`, `ASSET`, `SETTINGS`, `AUDIT_LOGS` | | Permissions | Each module × `view`, `create`, `edit`, `delete`, `approve`, `export` (skip where N/A) | | Roles | Super Admin, Admin, Purchase Manager, Store Manager, Accounts, Asset Manager | | Bootstrap user | Super Admin, bcrypt-hashed password, `status = active` | --- ## 3. Core Infrastructure - [x] `src/config/env.js` — Joi validation; app must refuse to boot on invalid/missing env - [x] `src/config/logger.js` — Winston + daily rotate; redact sensitive keys - [x] `src/config/morgan.js` — HTTP logs into Winston with request ID + user - [x] `src/config/prisma.js` — singleton client with query/error logging - [x] `src/config/swagger.js` — OpenAPI 3.0 from `src/docs/completed-routes.yaml` - [x] `src/utils/` — ApiError, ApiResponse, asyncHandler, encryption, auditLog, pagination, generateCode - [x] `src/middlewares/` — requestId, error, validate, rateLimiter, auth, rbac, upload - [x] `src/app.js` — Helmet, CORS, HPP, compression, rate limit, routes, Swagger, 404, error handler - [x] `src/server.js` — graceful shutdown, BigInt JSON serializer, unhandled rejection/exception handlers - [x] Verify `GET /health` responds --- ## 4. Modules (HMVC Pattern) Build in this order. Each module: `*.routes.js` → `*.controller.js` → `*.service.js` → `*.validation.js` (+ `*.repository.js` for complex modules). | # | Module | Status | Notes | |---|--------|--------|-------| | 8 | **auth** | [x] Done | login, refresh, logout, me, profile, avatar, change/forgot/reset password | | 9 | **masters** | [x] Done | 15 sub-masters under `masters/index.js` (UOM template replicated) | | — | **users** | [x] Done | CRUD + summary/filters/export; RBAC: `USERS` | | — | **roles** | [x] Done | CRUD + permission assignment + matrix; RBAC: `ROLES` | | 10 | **vendors** | [x] Done | CRUD + addresses/contacts/bank-details; encrypt bank accounts | | 11 | **purchase-orders** | [x] Done | Header + line items; submit/approve/reject/amend/cancel; PDF export | | 12 | **grn** | [x] Done | Receipt + PO status recalc + asset auto-creation | | 13 | **assets** | [x] Done | CRUD + transfer + AMC/service visits/insurance + expiry alerts | | 14 | **settings** | [x] Done | Company profile + SMTP email settings; RBAC: `SETTINGS` | Current progress: - [x] `auth` module (`login`, `refresh`, `logout`) - [x] `users` module (screen APIs: summary, filters, export, CRUD) - [x] `roles` module (cards, permission catalog, matrix, CRUD) - [x] `masters/uom` template + 13 replicated sub-masters - [x] Swagger docs for all completed routes (`src/docs/completed-routes.yaml`) - [x] `vendors` module (CRUD, status, addresses, contacts, bank-details) - [x] `purchase-orders` module (CRUD, workflow, PDF) - [x] `grn` module (transactional receipt, PO status recalc, asset auto-creation) - [x] `assets` module (CRUD, transfer, AMC, service visits, insurance, alerts) - [x] `GET /auth/me` (current user + permissions for FE) ### Layer rules | Layer | Responsibility | |-------|----------------| | Routes | HTTP mapping + middleware chain (`authenticate` → `authorize` → `validate` → controller) | | Controller | Parse request, call service, return `ApiResponse` — no business logic, no Prisma | | Service | Business logic, Prisma (or repository), `auditLog` on every mutation | | Repository | Multi-table `prisma.$transaction` only (PO, GRN, Assets) | | Validation | Joi schemas for body/query | --- ## 5. Module API List (Phase 1) Base path: `/api/v1` · Auth: `Authorization: Bearer ` (except public auth routes) ### System | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/health` | — | App health (root, not under v1) | | [x] | GET | `/healthz` | — | API v1 health | | [x] | GET | `/api-docs` | — | Swagger UI | | [x] | GET | `/api-docs.json` | — | OpenAPI spec | --- ### Auth (`/auth`) | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | POST | `/auth/login` | Public | Returns `accessToken`, `refreshToken` | | [x] | POST | `/auth/refresh` | Public | Rotating refresh token | | [x] | POST | `/auth/logout` | Public | Revoke refresh token | | [x] | POST | `/auth/forgot-password` | Public | Sends reset link via SMTP (`email_settings`) | | [x] | POST | `/auth/reset-password` | Public | Reset password with token from email | | [x] | GET | `/auth/me` | Authenticated | Current user + permissions (for FE) | | [x] | PUT | `/auth/profile` | Authenticated | Update `full_name` / `email` / `mobile` | | [x] | POST | `/auth/profile/avatar` | Authenticated | Upload avatar (`multipart`, field `avatar`) | | [x] | POST | `/auth/change-password` | Authenticated | Change password (revokes refresh tokens) | **DB patch:** `scripts/patch-users-avatar.sql` — adds `users.avatar_path`. **Env:** `FRONTEND_URL` (reset link base), `PASSWORD_RESET_EXPIRY_MINUTES` (default 60). --- ### Users (`/users`) — module: `USERS` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/users/summary` | view | Dashboard cards (total/active/inactive/locked, roles count) | | [x] | GET | `/users/filters` | view | Role, department, status dropdowns | | [x] | GET | `/users/export` | export | CSV export (same filters as list) | | [x] | GET | `/users` | view | Paginated list (`search`, `status`, `role_id`, `department_id`) | | [x] | GET | `/users/:id` | view | User detail | | [x] | POST | `/users` | create | Create user with `role_ids[]` (bcrypt password, mobile encrypted) | | [x] | PUT | `/users/:id` | edit | Update user; optional `role_ids[]` replaces assigned roles | | [x] | DELETE | `/users/:id` | delete | Soft delete | --- ### Roles (`/roles`) — module: `ROLES` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/roles/permissions` | view | Permission catalog (modules × actions) | | [x] | GET | `/roles/cards` | view | Role cards (name, description, user/permission counts) | | [x] | GET | `/roles` | view | Paginated role list | | [x] | GET | `/roles/:id` | view | Role detail with permissions | | [x] | POST | `/roles` | create | Create role with optional initial permissions (`permission_ids[]` and/or `view_modules[]`) | | [x] | PUT | `/roles/:id` | edit | Update role | | [x] | DELETE | `/roles/:id` | delete | Soft delete (blocked if users assigned) | | [x] | PUT | `/roles/:id/permissions` | edit | Assign by `permission_ids[]` (replaces all) | | [x] | GET | `/roles/:id/permission-matrix` | view | Matrix UI (view/create/edit/delete/approve/export) | | [x] | PUT | `/roles/:id/permission-matrix` | edit | Save matrix checkboxes | --- ### Masters (`/masters/*`) — module: `MASTERS` Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DELETE /:id` with RBAC `view|create|edit|delete`. | Status | Sub-master | Base path | |--------|------------|-----------| | [x] | UOM | `/masters/uom` | | [x] | Item Categories | `/masters/item-categories` | Optional asset defaults: `code_prefix`, `default_useful_life_years`, `default_depreciation_method` | | [x] | Item Subcategories | `/masters/item-subcategories` | Filter: `item_category_id` — shared by items + assets | | [x] | Items | `/masters/items` | `item_code` auto via `ITEM` document series; HSN via `hsn_code_id` | | [x] | Brands | `/masters/brands` | | [x] | GST Rates | `/masters/gst-rates` | | [x] | HSN Codes | `/masters/hsn-codes` | Search by `code` or `description` | | [x] | Payment Terms | `/masters/payment-terms` | | [x] | Delivery Terms | `/masters/delivery-terms` | | [~] | Asset Categories | removed — use Item Categories | | [~] | Asset Subcategories | removed — use Item Subcategories | | [x] | Departments | `/masters/departments` | | [x] | Designations | `/masters/designations` | | [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`); no plant↔warehouse hierarchy | | [x] | Plants | `/masters/plants` | Alias — plant locations only | | [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only | **DB patch:** `scripts/patch-locations-drop-parent-id.sql` — drops `locations.parent_id` (flat locations). | [x] | Document Series | `/masters/document-series` | **Masters total:** 13 modules × 5 endpoints (+ plants/warehouses aliases) --- ### Vendors (`/vendors`) — module: `VENDOR` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/vendors` | view | List (`search`, `status`, `vendor_type`) | | [x] | GET | `/vendors/gst-treatments` | view | GST treatment dropdown options | | [x] | GET | `/vendors/source-of-supply` | view | Indian states/UTs dropdown options | | [x] | POST | `/vendors` | create | Auto `vendor_code` via VENDOR series | | [x] | GET | `/vendors/:id` | view | Detail + addresses, contacts, bank details, item mappings | | [x] | PUT | `/vendors/:id` | edit | Update vendor | | [x] | PATCH | `/vendors/:id/status` | edit | `active` / `inactive` / `blacklisted` | | [x] | DELETE | `/vendors/:id` | delete | Soft delete | | [x] | GET | `/vendors/:vendorId/addresses` | view | List addresses | | [x] | POST | `/vendors/:vendorId/addresses` | create | Add address | | [x] | GET | `/vendors/:vendorId/addresses/:addressId` | view | Get address | | [x] | PUT | `/vendors/:vendorId/addresses/:addressId` | edit | Update address | | [x] | DELETE | `/vendors/:vendorId/addresses/:addressId` | delete | Deactivate address | | [x] | GET | `/vendors/:vendorId/contacts` | view | List contacts | | [x] | POST | `/vendors/:vendorId/contacts` | create | Add contact | | [x] | GET | `/vendors/:vendorId/contacts/:contactId` | view | Get contact | | [x] | PUT | `/vendors/:vendorId/contacts/:contactId` | edit | Update contact | | [x] | DELETE | `/vendors/:vendorId/contacts/:contactId` | delete | Deactivate contact | | [x] | GET | `/vendors/:vendorId/bank-details` | view | List bank details (decrypted) | | [x] | POST | `/vendors/:vendorId/bank-details` | create | Add bank detail (encrypted) | | [x] | GET | `/vendors/:vendorId/bank-details/:bankDetailId` | view | Get bank detail | | [x] | PUT | `/vendors/:vendorId/bank-details/:bankDetailId` | edit | Update bank detail | | [x] | DELETE | `/vendors/:vendorId/bank-details/:bankDetailId` | delete | Deactivate bank detail | | [x] | GET | `/vendors/:vendorId/items` | view | List vendor–item mappings | | [x] | POST | `/vendors/:vendorId/items` | create | Map item to vendor | | [x] | GET | `/vendors/:vendorId/items/:mappingId` | view | Get item mapping | | [x] | PUT | `/vendors/:vendorId/items/:mappingId` | edit | Update rate / preferred flag | | [x] | DELETE | `/vendors/:vendorId/items/:mappingId` | delete | Deactivate item mapping | --- ### Purchase Orders (`/purchase-orders`) — module: `PURCHASE_ORDER` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/purchase-orders` | view | List POs | | [x] | GET | `/purchase-orders/:id` | view | PO detail + line items | | [x] | POST | `/purchase-orders` | create | Create PO | | [x] | PUT | `/purchase-orders/:id` | edit | Update PO | | [x] | DELETE | `/purchase-orders/:id` | delete | Soft delete | | [x] | POST | `/purchase-orders/:id/submit` | edit | Submit for approval | | [x] | POST | `/purchase-orders/:id/approve` | approve | Approve PO | | [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO (`reject_reason` required; stored on PO) | | [x] | POST | `/purchase-orders/:id/amend` | edit | Amend PO | | [x] | POST | `/purchase-orders/:id/cancel` | edit | Cancel PO | | [x] | GET | `/purchase-orders/:id/pdf` | view | PDF export | | [x] | GET | `/purchase-orders/:poId/attachments` | view | List PO attachments | | [x] | POST | `/purchase-orders/:poId/attachments` | edit | Upload file (multipart `file`) | | [x] | GET | `/purchase-orders/:poId/attachments/:attachmentId` | view | Attachment metadata | | [x] | GET | `/purchase-orders/:poId/attachments/:attachmentId/download` | view | Download file (authenticated) | | [x] | DELETE | `/purchase-orders/:poId/attachments/:attachmentId` | delete | Delete attachment + file | **Totals:** `taxable_amount = sub_total + freight + other − discount`; GST (`tax_total` / CGST+SGST / IGST) is calculated on `taxable_amount` (not on `sub_total`). Column persisted via `scripts/patch-po-taxable-amount.sql`. --- ### GRN (`/grn`) — module: `GRN` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/grn` | view | List GRNs | | [x] | GET | `/grn/:id` | view | GRN detail + line items | | [x] | POST | `/grn` | create | Create GRN (transactional) | | [x] | PUT | `/grn/:id` | edit | Update GRN | | [x] | POST | `/grn/:id/cancel` | edit | Cancel GRN | | [x] | GET | `/grn/:id/pdf` | view | PDF export | | [x] | GET | `/grn/:grnId/attachments` | view | List GRN attachments | | [x] | POST | `/grn/:grnId/attachments` | edit | Upload file (multipart `file`) | | [x] | GET | `/grn/:grnId/attachments/:attachmentId` | view | Attachment metadata | | [x] | GET | `/grn/:grnId/attachments/:attachmentId/download` | view | Download file (authenticated) | | [x] | DELETE | `/grn/:grnId/attachments/:attachmentId` | delete | Delete attachment + file | --- ### Assets (`/assets`) — module: `ASSET` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/assets` | view | List assets | | [x] | GET | `/assets/:id` | view | Asset detail | | [x] | POST | `/assets` | create | Create asset | | [x] | PUT | `/assets/:id` | edit | Update asset | | [x] | DELETE | `/assets/:id` | delete | Soft delete | | [x] | POST | `/assets/:id/transfer` | edit | Transfer asset | | [x] | GET | `/assets/:id/transfer-history` | view | Transfer history | | [x] | GET | `/assets/:assetId/attachments` | view | List asset attachments | | [x] | POST | `/assets/:assetId/attachments` | edit | Upload file (multipart `file`; optional `attachment_type`, AMC/visit/insurance link) | | [x] | GET | `/assets/:assetId/attachments/:attachmentId` | view | Attachment metadata | | [x] | GET | `/assets/:assetId/attachments/:attachmentId/download` | view | Download file (authenticated) | | [x] | DELETE | `/assets/:assetId/attachments/:attachmentId` | delete | Delete attachment + file | **AMC contracts** (`/assets/:id/amc`) | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/assets/:id/amc` | view | List AMC contracts | | [x] | POST | `/assets/:id/amc` | create | Add AMC contract | | [x] | GET | `/assets/:id/amc/:contractId` | view | AMC detail | | [x] | PUT | `/assets/:id/amc/:contractId` | edit | Update AMC | | [x] | PATCH | `/assets/:id/amc/:contractId/renew` | edit | Renew AMC (deactivate old, create new) | **Service visits** (`/assets/:id/service-visits`) | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/assets/:id/service-visits` | view | List visits | | [x] | POST | `/assets/:id/service-visits` | create | Log visit | | [x] | GET | `/assets/:id/service-visits/:visitId` | view | Visit detail | | [x] | PUT | `/assets/:id/service-visits/:visitId` | edit | Update visit | | [x] | PATCH | `/assets/:id/service-visits/:visitId/status` | edit | Update visit status | **Insurance** (`/assets/:id/insurance`) | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/assets/:id/insurance` | view | List policies | | [x] | POST | `/assets/:id/insurance` | create | Add policy | | [x] | GET | `/assets/:id/insurance/:policyId` | view | Policy detail | | [x] | PUT | `/assets/:id/insurance/:policyId` | edit | Update policy | | [x] | PATCH | `/assets/:id/insurance/:policyId/renew` | edit | Renew policy | **Alerts** (cross-asset) | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/assets/alerts/expiry` | view | AMC/Insurance/Warranty expiry (`?days=30/60/90&type=`) | | [x] | GET | `/assets/alerts/service` | view | Overdue/upcoming service (`?status=OVERDUE`) | --- ### Settings (`/settings`) — module: `SETTINGS` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/settings/company` | view | Company profile (org name, GSTIN, contact, address, logo/favicon URLs) | | [x] | PUT | `/settings/company` | edit | Update company profile | | [x] | POST | `/settings/company/logo` | edit | Upload logo (`multipart/form-data`, field `logo`) | | [x] | POST | `/settings/company/favicon` | edit | Upload favicon (`multipart/form-data`, field `favicon`) | | [x] | GET | `/settings/email` | view | SMTP settings (`has_smtp_password` flag; password never returned) | | [x] | PUT | `/settings/email` | edit | Update SMTP settings (password encrypted at rest) | **DB patch:** `scripts/patch-company-email-settings.sql` — creates `company` + `email_settings` singleton tables and `SETTINGS` module permissions for Super Admin. **DB patch:** `scripts/patch-company-gstin.sql` — adds `company.gstin` for PDF document headers. **DB patch:** `scripts/patch-company-favicon.sql` — adds `company.favicon_path`. --- ### Audit Logs (`/audit-logs`) — module: `AUDIT_LOGS` | Status | Method | Endpoint | RBAC | Notes | |--------|--------|----------|------|-------| | [x] | GET | `/audit-logs/filters` | view | Distinct `table_names`, `actions`, `performers` for FE dropdowns | | [x] | GET | `/audit-logs` | view | Filtered list; **empty by default** until at least one filter is applied | | [x] | GET | `/audit-logs/:id` | view | Full detail with `old_value` / `new_value` JSON | | [x] | GET | `/audit-logs/export` | export | CSV export (filters required) | **List filters:** `table_name`, `record_id`, `action`, `performed_by`, `request_id`, `date_from`, `date_to`, `search` (+ `page`, `limit`). **DB patch:** `scripts/patch-audit-logs-module.sql` — `AUDIT_LOGS` module with `view` + `export` for Super Admin. --- **DB patch:** run `scripts/patch-assets-amc-insurance.sql` then `scripts/patch-assets-views.sql` on deployed DB. --- ### API progress summary | Module | Endpoints done | Endpoints total | Status | |--------|----------------|-----------------|--------| | System | 4 | 4 | [x] Done | | Auth | 9 | 9 | [x] Done | | Users | 8 | 8 | [x] Done | | Roles | 10 | 10 | [x] Done | | Masters | 70 | 70 | [x] Done | | Vendors | 27 | 27 | [x] Done | | Purchase Orders | 11 | 11 | [x] Done | | GRN | 11 | 11 | [x] Done | | Assets | 24 | 24 | [x] Done | | Settings | 5 | 5 | [x] Done | | Audit Logs | 4 | 4 | [x] Done | | **Total** | **172** | **172** | **[x] Phase 1 APIs + Asset extensions** | --- ## 6. Security Tasks - [x] JWT access tokens (15m) in `Authorization: Bearer` header - [x] Refresh tokens: opaque, SHA-256 hashed in DB, rotated on each refresh - [x] bcrypt password hashing (`BCRYPT_SALT_ROUNDS=12`) - [x] Account lockout after `MAX_LOGIN_ATTEMPTS` failures - [x] `authenticate` + `authorize(module, action)` on every protected route - [x] AES-256-GCM field encryption + HMAC blind index for searchable PII - [x] Rate limiting: global on `/api`, stricter on `/auth/login` and `/auth/forgot-password` - [x] Helmet, CORS (no `*` in production), HPP, compression - [x] File uploads: MIME allow-list, random filenames, RBAC-protected download (not direct web serve) - [x] `auditLog()` on every create/update/status-change/approve/reject (completed modules) - [x] `X-Request-Id` on every request for log correlation ### RBAC permission actions `view`, `create`, `edit`, `delete`, `approve`, `export` --- ## 7. API Conventions - Versioning: `/api/v1/...` - Pagination: `?page=1&limit=20` (limit accepted as sent by FE, min 1, default 20); response `meta: { page, limit, total }` - Filtering: `?status=...&search=...` per module - Sorting: `?sort=-created_at` (`-` = descending) - IDs: `BigInt` serialized as strings in JSON - Dates: ISO 8601 UTC - Soft delete: set `deleted_at`, never hard-delete transactional records - Response envelope: `{ success, message, data?, meta?, errors? }` --- ## 8. Testing - [ ] Jest + Supertest against `app.js` (no live server) - [ ] Separate test DB via `.env.test` + `prisma migrate deploy` - [ ] Mirror structure: `tests/modules//.test.js` ### Minimum Phase 1 coverage - [ ] Auth: login, refresh, lockout - [ ] RBAC: allowed vs forbidden - [ ] Vendor: CRUD + status transitions - [x] PO: status lifecycle - [x] GRN: partial receipt → PO status recalculation --- ## 9. Documentation & DevOps - [x] OpenAPI spec for completed routes (`src/docs/completed-routes.yaml` + `/api-docs`) - [ ] Add `@swagger` JSDoc blocks to route files (optional; YAML spec in use) - [ ] `Dockerfile` (Node 20 Alpine) - [ ] `docker-compose.yml` (API + Postgres 15) - [ ] Verify `docker-compose up` boots API + Postgres - [ ] Run `npm audit` in CI --- ## 10. Future Development Guidelines When adding a new module or endpoint: 1. Add Prisma model with common columns + soft delete 2. Add module/permissions to seed (if new domain) 3. Create the four-file HMVC module (five if multi-table transactions) 4. Register routes in `src/routes/v1/index.js` 5. Apply middleware chain: `authenticate` → `authorize(MODULE, action)` → `validate(schema)` → controller 6. Call `auditLog()` in service for all mutations 7. Use `nextDocumentNumber()` for auto-generated codes 8. Encrypt PII at service layer with blind index for search 9. Add Swagger JSDoc + Jest tests 10. Breaking API changes → new version (`/api/v2`) ### Complex transaction modules Use `*.repository.js` with `prisma.$transaction` when a single operation touches multiple tables (e.g. PO header + items, GRN + PO qty update + asset creation). ### Masters replication Copy the UOM module pattern for new masters: simple name/code fields, RBAC code `MASTERS`, no document numbering.