gwm : sub categort joining
This commit is contained in:
parent
22d25bf2f5
commit
6e06006687
1307
docs/MODULE_DATA_FLOW.html
Normal file
1307
docs/MODULE_DATA_FLOW.html
Normal file
File diff suppressed because it is too large
Load Diff
648
docs/MODULE_DATA_FLOW.md
Normal file
648
docs/MODULE_DATA_FLOW.md
Normal file
@ -0,0 +1,648 @@
|
||||
# ERP Backend — Module & Data Flow Diagram
|
||||
|
||||
> **Stack:** Express · PostgreSQL · Prisma · JWT + RBAC
|
||||
> **Base API:** `/api/v1`
|
||||
> **Phase 1 modules:** Auth, Users, Roles, Masters (14), Vendors, Purchase Orders, GRN, Assets (+ AMC / Insurance / Service / Alerts / Depreciation), Settings
|
||||
|
||||
Reference: [BACKEND_TASKS.md](../BACKEND_TASKS.md) · [BACKEND_SETUP.md](../BACKEND_SETUP.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. High-level system architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["Frontend / API Client"]
|
||||
FE[React App / Postman]
|
||||
end
|
||||
|
||||
subgraph Gateway["Express App (src/app.js)"]
|
||||
HELMET[Helmet + CORS + HPP]
|
||||
RATE[Rate Limiter /api]
|
||||
REQID[Request ID]
|
||||
ROUTES["/api/v1 Routes"]
|
||||
SWAGGER[Swagger /api-docs]
|
||||
ERR[Error Middleware]
|
||||
end
|
||||
|
||||
subgraph Middleware["Per-route chain"]
|
||||
AUTH[authenticate JWT]
|
||||
RBAC[authorize module + action]
|
||||
VAL[validate Joi schema]
|
||||
CTRL[Controller]
|
||||
end
|
||||
|
||||
subgraph Services["Business Layer"]
|
||||
SVC["*.service.js"]
|
||||
REPO["*.repository.js PO/GRN/Assets"]
|
||||
end
|
||||
|
||||
subgraph Data["PostgreSQL"]
|
||||
PRISMA[(Prisma ORM)]
|
||||
VIEWS[(SQL Views v_asset_*)]
|
||||
AUDIT[(audit_logs)]
|
||||
end
|
||||
|
||||
FE --> HELMET --> RATE --> REQID --> ROUTES
|
||||
ROUTES --> AUTH --> RBAC --> VAL --> CTRL --> SVC
|
||||
SVC --> PRISMA
|
||||
SVC --> REPO --> PRISMA
|
||||
SVC --> AUDIT
|
||||
SVC --> VIEWS
|
||||
CTRL --> ERR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Every API module map
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph System
|
||||
HEALTH[/health /healthz]
|
||||
DOCS[/api-docs]
|
||||
end
|
||||
|
||||
subgraph Security
|
||||
AUTH_MOD["/auth login|refresh|logout|me"]
|
||||
USERS["/users CRUD + export"]
|
||||
ROLES["/roles CRUD + permissions matrix"]
|
||||
end
|
||||
|
||||
subgraph Masters["/masters/* (14 sub-masters)"]
|
||||
M1[uom]
|
||||
M2[item-categories / subcategories / items]
|
||||
M3[brands / gst-rates]
|
||||
M4[payment-terms / delivery-terms]
|
||||
M5[asset-categories / subcategories]
|
||||
M6[departments / designations]
|
||||
M7[locations / plants / warehouses]
|
||||
M8[document-series]
|
||||
end
|
||||
|
||||
subgraph Procurement
|
||||
VEND["/vendors + addresses|contacts|bank|items"]
|
||||
PO["/purchase-orders workflow + PDF"]
|
||||
GRN["/grn receipt + cancel + PDF"]
|
||||
end
|
||||
|
||||
subgraph Assets["/assets"]
|
||||
A_CORE[CRUD + transfer]
|
||||
A_AMC[AMC contracts + renew]
|
||||
A_SVC[service visits + status]
|
||||
A_INS[insurance + renew]
|
||||
A_DEPR[depreciation preview]
|
||||
A_ALERT[expiry + service alerts]
|
||||
end
|
||||
|
||||
subgraph Config
|
||||
SET["/settings company + email SMTP"]
|
||||
end
|
||||
|
||||
System --> Security --> Masters --> Procurement --> Assets --> Config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Standard request data flow (all protected routes)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as Frontend
|
||||
participant API as Express Route
|
||||
participant AUTH as authenticate
|
||||
participant RBAC as authorize
|
||||
participant VAL as validate
|
||||
participant SVC as Service
|
||||
participant DB as PostgreSQL
|
||||
participant AUD as audit_logs
|
||||
|
||||
FE->>API: HTTP + Bearer JWT
|
||||
API->>AUTH: Verify token, load user + roles
|
||||
AUTH->>RBAC: Check module permission (view/create/edit/delete/approve/export)
|
||||
RBAC->>VAL: Joi validate body/query
|
||||
VAL->>SVC: Business logic
|
||||
SVC->>DB: Prisma read/write (deleted_at: null)
|
||||
SVC->>AUD: auditLog on mutations
|
||||
SVC-->>FE: ApiResponse { success, message, data, meta }
|
||||
```
|
||||
|
||||
**RBAC modules:** `USERS`, `ROLES`, `MASTERS`, `VENDOR`, `PURCHASE_ORDER`, `GRN`, `ASSET`, `SETTINGS`
|
||||
|
||||
---
|
||||
|
||||
## 4. Auth & session flow
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Login: POST /auth/login
|
||||
Login --> Active: Valid credentials
|
||||
Login --> Locked: MAX_LOGIN_ATTEMPTS exceeded
|
||||
Login --> RateLimited: Too many failed logins (IP)
|
||||
Active --> TokenIssued: accessToken 15m + refreshToken
|
||||
TokenIssued --> APIAccess: Bearer on /api/v1/*
|
||||
APIAccess --> Refresh: access expired
|
||||
Refresh --> TokenIssued: POST /auth/refresh (rotate hash in DB)
|
||||
APIAccess --> Logout: POST /auth/logout (revoke refresh)
|
||||
Locked --> Login: After LOCKOUT_DURATION_MINUTES
|
||||
```
|
||||
|
||||
| Layer | Storage | Reset |
|
||||
|---|---|---|
|
||||
| Global API rate limit | In-memory per IP | Restart server or wait 15 min |
|
||||
| Login rate limit | In-memory per IP | Restart or wait 15 min |
|
||||
| Account lockout | `users.locked_until` | Wait 30 min or DB reset |
|
||||
|
||||
---
|
||||
|
||||
## 5. Master data dependency graph
|
||||
|
||||
Masters must exist **before** transactional modules use them.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DS[document_series] --> VCODE[vendor_code / po_number / grn_number / asset_code]
|
||||
LOC[locations plants + warehouses] --> PO
|
||||
LOC --> GRN
|
||||
LOC --> ASSET
|
||||
|
||||
DEPT[departments] --> USERS
|
||||
DEPT --> ASSET
|
||||
DES[designations] --> USERS
|
||||
|
||||
UOM[uom] --> ITEMS
|
||||
IC[item_categories] --> ISUB[item_subcategories] --> ITEMS
|
||||
GST[gst_rates] --> ITEMS
|
||||
BRANDS[brands] --> ITEMS
|
||||
BRANDS --> PO
|
||||
|
||||
PT[payment_terms] --> VEND
|
||||
PT --> PO
|
||||
DT[delivery_terms] --> PO
|
||||
|
||||
ACAT[asset_categories] --> ASUB[asset_subcategories]
|
||||
ACAT --> ASSET
|
||||
ACAT --> GRN_AUTO[GRN auto-asset creation]
|
||||
ASUB --> ASSET
|
||||
ASUB --> GRN_AUTO
|
||||
|
||||
ITEMS --> PO_ITEMS
|
||||
ITEMS --> GRN_ITEMS
|
||||
VEND --> PO
|
||||
VEND --> GRN
|
||||
VEND --> AMC
|
||||
VEND --> SVC_VISIT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Procurement → Asset end-to-end flow (core business chain)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([Setup Masters + Vendor]) --> PO_CREATE
|
||||
|
||||
PO_CREATE["POST /purchase-orders<br/>status: DRAFT"] --> PO_EDIT["PUT /purchase-orders/:id<br/>add line items"]
|
||||
PO_EDIT --> PO_SUBMIT["POST .../submit<br/>→ PENDING_APPROVAL"]
|
||||
PO_SUBMIT --> PO_APPROVE{"Approve?"}
|
||||
PO_APPROVE -->|Yes| PO_APPROVED["status: APPROVED"]
|
||||
PO_APPROVE -->|No| PO_REJECTED["status: REJECTED → edit & resubmit"]
|
||||
PO_REJECTED --> PO_EDIT
|
||||
|
||||
PO_APPROVED --> GRN_CREATE["POST /grn<br/>receive against PO lines"]
|
||||
|
||||
subgraph GRN_TXN["GRN Transaction (repository)"]
|
||||
G1[Create grn POSTED]
|
||||
G2[Create grn_items per line]
|
||||
G3[Increment PO item received_qty]
|
||||
G4{is_asset_item?}
|
||||
G5[Auto-create assets 1 per accepted qty]
|
||||
G6[Recalculate PO status]
|
||||
G1 --> G2 --> G3 --> G4
|
||||
G4 -->|Yes| G5 --> G6
|
||||
G4 -->|No| G6
|
||||
end
|
||||
|
||||
GRN_CREATE --> GRN_TXN
|
||||
G6 --> PO_STATUS{Receipt status}
|
||||
PO_STATUS -->|partial| PARTIAL[PARTIALLY_RECEIVED]
|
||||
PO_STATUS -->|full| FULL[FULLY_RECEIVED]
|
||||
|
||||
G5 --> MANUAL_ASSET["POST /assets manual create also allowed"]
|
||||
PARTIAL --> GRN_CREATE
|
||||
FULL --> ASSET_OPS[Asset lifecycle ops]
|
||||
|
||||
GRN_CANCEL["POST /grn/:id/cancel"] --> REV1[Decrement received_qty]
|
||||
REV1 --> REV2[Soft-delete linked assets]
|
||||
REV2 --> REV3[GRN status CANCELLED]
|
||||
```
|
||||
|
||||
### PO status lifecycle
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> DRAFT
|
||||
DRAFT --> PENDING_APPROVAL: submit
|
||||
REJECTED --> PENDING_APPROVAL: resubmit
|
||||
PENDING_APPROVAL --> APPROVED: approve
|
||||
PENDING_APPROVAL --> REJECTED: reject
|
||||
APPROVED --> PARTIALLY_RECEIVED: GRN partial
|
||||
APPROVED --> FULLY_RECEIVED: GRN full
|
||||
PARTIALLY_RECEIVED --> FULLY_RECEIVED: more GRN
|
||||
APPROVED --> CLOSED: amend closes old PO
|
||||
DRAFT --> CANCELLED: cancel
|
||||
APPROVED --> CANCELLED: cancel if no receipts
|
||||
```
|
||||
|
||||
### GRN auto-asset creation (when `items.is_asset_item = true`)
|
||||
|
||||
| Field on Asset | Source |
|
||||
|---|---|
|
||||
| `asset_code` | `document_series` via `ASSET_{category.code}` |
|
||||
| `asset_name` | Item name (+ `#N` if qty > 1) |
|
||||
| `asset_category_id` / `asset_subcategory_id` | From GRN line payload |
|
||||
| `plant_id` | From PO |
|
||||
| `warehouse_id`, `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
||||
| `purchase_date` | `grn.grn_date` |
|
||||
| `purchase_cost` | `grn_items.rate` |
|
||||
| `useful_life_years`, `depreciation_method` | From `asset_categories` defaults |
|
||||
| `condition` | `NEW` |
|
||||
| `status` | `IN_USE` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Asset module — full lifecycle
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Create["Asset Creation Paths"]
|
||||
P1[Manual POST /assets]
|
||||
P2[Auto from GRN receipt]
|
||||
end
|
||||
|
||||
subgraph Validate["normalizeAssetPayload validations"]
|
||||
V1[asset_category + subcategory match]
|
||||
V2[plant / dept / warehouse / user refs]
|
||||
V3[vendor / PO / GRN / grn_item refs]
|
||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||
V5[depreciation_rate required if method=OTHER]
|
||||
V6[resolve rate from category defaults]
|
||||
end
|
||||
|
||||
Create --> Validate --> SAVE[(assets table)]
|
||||
SAVE --> CODE[asset_code from document_series]
|
||||
|
||||
SAVE --> READ["GET /assets/:id<br/>includes depreciation summary"]
|
||||
|
||||
SAVE --> TRANSFER["POST /assets/:id/transfer"]
|
||||
TRANSFER --> TH[(asset_transfers)]
|
||||
TRANSFER --> UPDATE_LOC[Update plant/dept/user/warehouse on asset]
|
||||
|
||||
SAVE --> UPDATE["PUT /assets/:id"]
|
||||
SAVE --> DELETE["DELETE /assets/:id soft delete"]
|
||||
|
||||
UPDATE --> DISPOSE{status DISPOSED/SCRAPPED?}
|
||||
DISPOSE -->|Yes| NEED_DATE[disposal_date required]
|
||||
```
|
||||
|
||||
### Asset status & condition enums
|
||||
|
||||
| Field | Values |
|
||||
|---|---|
|
||||
| `status` | `IN_USE`, `IDLE`, `UNDER_MAINTENANCE`, `DISPOSED`, `SCRAPPED` |
|
||||
| `condition` | `NEW`, `GOOD`, `FAIR`, `POOR` |
|
||||
|
||||
---
|
||||
|
||||
## 8. AMC contracts flow + renewal concept
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[(assets)] --> AMC_LIST["GET /assets/:id/amc"]
|
||||
A --> AMC_CREATE["POST /assets/:id/amc"]
|
||||
|
||||
AMC_CREATE --> CHECK1{is_active=true?}
|
||||
CHECK1 -->|Yes| DEACT[Deactivate other active AMC for same asset]
|
||||
CHECK1 --> AMC_ROW[(asset_amc_contracts)]
|
||||
DEACT --> AMC_ROW
|
||||
|
||||
AMC_ROW --> FIELDS["vendor_id, contract_type, start/end dates,<br/>renewal_date, annual_cost, visits_per_year, etc."]
|
||||
|
||||
AMC_ROW --> AMC_UPDATE["PUT /assets/:id/amc/:contractId"]
|
||||
AMC_ROW --> AMC_RENEW["PATCH .../renew"]
|
||||
|
||||
subgraph RENEW_FLOW["Renewal Flow (AMC)"]
|
||||
R1[Load existing contract]
|
||||
R2[Set old contract is_active=false]
|
||||
R3[Create NEW contract row with new dates]
|
||||
R4[New contract is_active=true]
|
||||
R5[auditLog action=RENEW]
|
||||
R1 --> R2 --> R3 --> R4 --> R5
|
||||
end
|
||||
|
||||
AMC_RENEW --> RENEW_FLOW
|
||||
|
||||
AMC_ROW --> SVC_LINK["Service visits can link amc_contract_id"]
|
||||
```
|
||||
|
||||
**Key rule:** Only **one active AMC** per asset (`deactivateOtherActive`).
|
||||
|
||||
**Renewal ≠ update dates on same row** — renewal creates a **new contract record** and deactivates the old one (history preserved).
|
||||
|
||||
---
|
||||
|
||||
## 9. Insurance policies flow + renewal concept
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[(assets)] --> INS_LIST["GET /assets/:id/insurance"]
|
||||
A --> INS_CREATE["POST /assets/:id/insurance"]
|
||||
|
||||
INS_CREATE --> CHECK{is_active=true?}
|
||||
CHECK -->|Yes| DEACT[Deactivate other active policies]
|
||||
CHECK --> POL[(asset_insurance_policies)]
|
||||
DEACT --> POL
|
||||
|
||||
POL --> FIELDS["policy_no, insurer, sum_insured,<br/>annual_premium, start/end dates,<br/>renewal_date, is_auto_renewal, premium_paid"]
|
||||
|
||||
POL --> INS_UPDATE["PUT /assets/:id/insurance/:policyId"]
|
||||
POL --> INS_RENEW["PATCH .../renew"]
|
||||
|
||||
subgraph RENEW_INS["Renewal Flow (Insurance)"]
|
||||
I1[Deactivate old policy]
|
||||
I2[Create new policy with new dates]
|
||||
I3[premium_paid defaults false on renew]
|
||||
I4[auditLog action=RENEW]
|
||||
I1 --> I2 --> I3 --> I4
|
||||
end
|
||||
|
||||
INS_RENEW --> RENEW_INS
|
||||
```
|
||||
|
||||
Same pattern as AMC: **one active policy**, renewal = **new row + deactivate old**.
|
||||
|
||||
---
|
||||
|
||||
## 10. Service visits flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[(assets)] --> SV_LIST["GET /assets/:id/service-visits"]
|
||||
A --> SV_CREATE["POST /assets/:id/service-visits"]
|
||||
|
||||
SV_CREATE --> VAL_SV["Validate amc_contract_id belongs to asset<br/>optional vendor_id ref"]
|
||||
VAL_SV --> SV_ROW[(asset_service_visits)]
|
||||
|
||||
SV_ROW --> FIELDS["visit_type, visit_date, complaint details,<br/>engineer, work_done, parts_replaced,<br/>next_service_date, downtime_hours,<br/>service_cost, is_under_amc,<br/>asset_condition_after"]
|
||||
|
||||
SV_ROW --> SV_UPDATE["PUT .../service-visits/:visitId"]
|
||||
SV_ROW --> SV_STATUS["PATCH .../status<br/>SCHEDULED|IN_PROGRESS|COMPLETED|etc."]
|
||||
|
||||
SV_ROW --> ALERT_VIEW["Feeds v_asset_next_service view"]
|
||||
```
|
||||
|
||||
### Visit types
|
||||
|
||||
`PREVENTIVE`, `BREAKDOWN`, `INSPECTION`, `INSTALLATION`, `CALIBRATION`, `OTHER`
|
||||
|
||||
### Link to AMC
|
||||
|
||||
- `amc_contract_id` optional but validated against the asset
|
||||
- `is_under_amc` flag for cost tracking
|
||||
- `next_service_date` drives **service alerts**
|
||||
|
||||
---
|
||||
|
||||
## 11. Depreciation flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Inputs
|
||||
CAT[asset_categories defaults]
|
||||
FORM[User form fields]
|
||||
end
|
||||
|
||||
CAT --> |default_useful_life_years<br/>default_depreciation_method| CREATE
|
||||
FORM --> CREATE["POST /assets or PUT /assets/:id"]
|
||||
FORM --> PREVIEW["POST /assets/depreciation/calculate"]
|
||||
|
||||
subgraph Methods["depreciation_method"]
|
||||
SLM[SLM Straight Line]
|
||||
WDV[WDV Written Down Value]
|
||||
OTHER[OTHER manual rate required]
|
||||
end
|
||||
|
||||
PREVIEW --> CALC[calculateDepreciation]
|
||||
CREATE --> STORE[(assets: method, rate, cost, salvage, life, purchase_date)]
|
||||
STORE --> READ["GET /assets/:id"]
|
||||
READ --> CALC
|
||||
|
||||
CALC --> OUT["annual_depreciation<br/>accumulated_depreciation<br/>book_value<br/>years_elapsed<br/>resolved depreciation_rate"]
|
||||
|
||||
SLM --> CALC
|
||||
WDV --> CALC
|
||||
OTHER --> CALC
|
||||
```
|
||||
|
||||
| Method | Rate auto-calc? | Formula concept |
|
||||
|---|---|---|
|
||||
| **SLM** | Yes (if rate omitted) | Equal yearly depreciation on original cost |
|
||||
| **WDV** | Yes (if rate omitted) | Depreciation on reducing book value year-by-year |
|
||||
| **OTHER** | No — user must send `depreciation_rate` | Custom % on cost |
|
||||
|
||||
**FE usage:**
|
||||
|
||||
- **Live preview** → `POST /assets/depreciation/calculate` (no save)
|
||||
- **Saved asset view** → `GET /assets/:id` → `data.depreciation` object
|
||||
- **Method dropdown** → `GET /assets/depreciation-methods`
|
||||
|
||||
---
|
||||
|
||||
## 12. Alerts flow (cross-asset dashboards)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Sources
|
||||
AMC[(asset_amc_contracts end_date)]
|
||||
INS[(asset_insurance_policies policy_end_date)]
|
||||
WAR[(assets warranty_expiry_date)]
|
||||
SVC[(asset_service_visits next_service_date)]
|
||||
end
|
||||
|
||||
AMC --> V1[v_asset_expiry_alerts]
|
||||
INS --> V1
|
||||
WAR --> V1
|
||||
SVC --> V2[v_asset_next_service]
|
||||
|
||||
V1 --> API1["GET /assets/alerts/expiry<br/>?days=30&type=AMC|INSURANCE|WARRANTY"]
|
||||
V2 --> API2["GET /assets/alerts/service<br/>?status=OVERDUE|DUE_THIS_WEEK|..."]
|
||||
|
||||
V1 --> LEVELS["EXPIRED / CRITICAL / WARNING / INFO"]
|
||||
V2 --> SSTAT["OVERDUE / DUE_THIS_WEEK / DUE_THIS_MONTH / UPCOMING"]
|
||||
```
|
||||
|
||||
**Requires DB views:** run `scripts/patch-assets-amc-insurance.sql` + `scripts/patch-assets-views.sql`
|
||||
|
||||
---
|
||||
|
||||
## 13. Vendor module data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
V[(vendors)] --> ADDR[vendor_addresses]
|
||||
V --> CONT[vendor_contacts]
|
||||
V --> BANK[vendor_bank_details AES encrypted]
|
||||
V --> MAP[vendor_item_mapping]
|
||||
|
||||
V --> PO[purchase_orders]
|
||||
V --> GRN[grn]
|
||||
V --> AMC[asset_amc_contracts]
|
||||
V --> SVC[asset_service_visits]
|
||||
|
||||
MAP --> ITEMS[items preferred vendor rates]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Users & roles permission flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[(users)] --> UR[user_roles]
|
||||
UR --> R[(roles)]
|
||||
R --> RP[role_permissions]
|
||||
RP --> P[(permissions)]
|
||||
P --> M[(modules)]
|
||||
|
||||
U --> PO_APPROVE[PO approve/reject]
|
||||
U --> ASSET_OPS[Asset CRUD/transfer]
|
||||
U --> AUDIT[performed_by in audit_logs]
|
||||
|
||||
LOGIN["GET /auth/me"] --> PERM[Returns user + flat permissions list for FE menu/RBAC]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Settings module
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CO[(company singleton)] --> PDF[PO/GRN/Asset PDF headers]
|
||||
EM[(email_settings SMTP encrypted)] --> MAIL[Future email notifications]
|
||||
LOGO["POST /settings/company/logo"] --> UPLOADS[(uploads/)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Cross-cutting concerns (every module)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
M[Every mutation] --> AUDIT[auditLog table_name, record_id, action, old/new JSON]
|
||||
M --> SOFT[Soft delete deleted_at = now]
|
||||
M --> USER[created_by / updated_by from req.user.id]
|
||||
M --> REQ[request_id from middleware]
|
||||
|
||||
DOC[Document numbers] --> SERIES[document_series atomic increment]
|
||||
SERIES --> VC[vendor_code]
|
||||
SERIES --> PO_NUM[po_number]
|
||||
SERIES --> GRN_NUM[grn_number]
|
||||
SERIES --> ASSET_CODE[asset_code per category prefix]
|
||||
|
||||
PII[mobile, bank accounts] --> ENC[AES-256-GCM + HMAC blind index]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Complete entity relationship (simplified)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
vendors ||--o{ purchase_orders : supplies
|
||||
vendors ||--o{ grn : delivers
|
||||
vendors ||--o{ asset_amc_contracts : maintains
|
||||
vendors ||--o{ asset_service_visits : services
|
||||
|
||||
purchase_orders ||--o{ purchase_order_items : contains
|
||||
purchase_orders ||--o{ grn : received_via
|
||||
purchase_orders ||--o{ assets : sourced_from
|
||||
|
||||
grn ||--o{ grn_items : lines
|
||||
grn_items ||--o{ assets : auto_created
|
||||
|
||||
items ||--o{ purchase_order_items : ordered
|
||||
items ||--o{ grn_items : received
|
||||
|
||||
asset_categories ||--o{ asset_subcategories : has
|
||||
asset_categories ||--o{ assets : classifies
|
||||
asset_subcategories ||--o{ assets : sub_classifies
|
||||
|
||||
assets ||--o{ asset_amc_contracts : has
|
||||
assets ||--o{ asset_insurance_policies : insured_by
|
||||
assets ||--o{ asset_service_visits : serviced
|
||||
assets ||--o{ asset_transfers : moved
|
||||
assets ||--o{ asset_attachments : files
|
||||
|
||||
asset_amc_contracts ||--o{ asset_service_visits : covers
|
||||
|
||||
locations ||--o{ assets : plant_warehouse
|
||||
departments ||--o{ assets : assigned_dept
|
||||
users ||--o{ assets : assigned_user
|
||||
|
||||
users ||--o{ user_roles : has
|
||||
roles ||--o{ role_permissions : grants
|
||||
permissions }o--|| modules : belongs_to
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Asset sub-module API quick reference
|
||||
|
||||
| Area | Endpoints | Notes |
|
||||
|---|---|---|
|
||||
| Core | `GET/POST /assets`, `GET/PUT/DELETE /assets/:id` | Includes computed `depreciation` on read |
|
||||
| Transfer | `POST /assets/:id/transfer`, `GET .../transfer-history` | Blocked if DISPOSED/SCRAPPED |
|
||||
| AMC | `GET/POST /assets/:id/amc`, `GET/PUT .../:contractId`, `PATCH .../renew` | One active contract |
|
||||
| Service | `GET/POST /assets/:id/service-visits`, `PUT`, `PATCH .../status` | Links optional AMC |
|
||||
| Insurance | `GET/POST /assets/:id/insurance`, `GET/PUT`, `PATCH .../renew` | One active policy |
|
||||
| Depreciation | `GET /depreciation-methods`, `POST /depreciation/calculate` | Preview only |
|
||||
| Alerts | `GET /alerts/expiry`, `GET /alerts/service` | SQL views |
|
||||
|
||||
---
|
||||
|
||||
## 19. Typical FE screen → API mapping
|
||||
|
||||
| Screen | APIs used |
|
||||
|---|---|
|
||||
| Login | `POST /auth/login`, `GET /auth/me` |
|
||||
| Master setup | `/masters/*` CRUD |
|
||||
| Vendor management | `/vendors/*` |
|
||||
| Create PO | Masters dropdowns + `POST /purchase-orders` |
|
||||
| Approve PO | `POST /purchase-orders/:id/approve` |
|
||||
| GRN receipt | `POST /grn` (auto assets for asset items) |
|
||||
| Asset list/detail | `GET /assets`, `GET /assets/:id` |
|
||||
| Asset create/edit form | Masters + `POST /depreciation/calculate` (preview) + `POST/PUT /assets` |
|
||||
| AMC tab | `/assets/:id/amc/*` + renew |
|
||||
| Insurance tab | `/assets/:id/insurance/*` + renew |
|
||||
| Service log | `/assets/:id/service-visits/*` |
|
||||
| Dashboard alerts | `/assets/alerts/expiry`, `/assets/alerts/service` |
|
||||
| Transfer asset | `POST /assets/:id/transfer` |
|
||||
|
||||
---
|
||||
|
||||
## 20. Renewal concept summary (for FE)
|
||||
|
||||
| Entity | What "Renew" does | Old record | New record |
|
||||
|---|---|---|---|
|
||||
| **AMC** | `PATCH /assets/:id/amc/:contractId/renew` | `is_active = false` | New row, `is_active = true`, new dates |
|
||||
| **Insurance** | `PATCH /assets/:id/insurance/:policyId/renew` | `is_active = false` | New row, `is_active = true`, new policy period |
|
||||
|
||||
Both preserve **full history** — never overwrite the old contract/policy row.
|
||||
|
||||
---
|
||||
|
||||
## Viewing diagrams
|
||||
|
||||
- **GitHub** — renders Mermaid natively in markdown
|
||||
- **VS Code** — install a Mermaid preview extension
|
||||
- **Online** — paste into [mermaid.live](https://mermaid.live)
|
||||
@ -1,5 +1,6 @@
|
||||
const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
@ -51,6 +52,21 @@ const config = {
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
const categoryInclude = {
|
||||
asset_categories: { select: { id: true, code: true, name: true } },
|
||||
};
|
||||
|
||||
const sanitizeSubcategory = (row) => {
|
||||
if (!row) return null;
|
||||
const { asset_categories, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
asset_category: asset_categories || null,
|
||||
asset_category_name: asset_categories?.name || null,
|
||||
asset_categories: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const assertAssetCategory = async (assetCategoryId) => {
|
||||
const category = await prisma.asset_categories.findFirst({
|
||||
where: { id: BigInt(assetCategoryId), deleted_at: null },
|
||||
@ -60,22 +76,83 @@ const assertAssetCategory = async (assetCategoryId) => {
|
||||
return category;
|
||||
};
|
||||
|
||||
const withCategory = async (row) => {
|
||||
if (!row) return null;
|
||||
const full = await prisma.asset_subcategories.findFirst({
|
||||
where: { id: row.id },
|
||||
include: categoryInclude,
|
||||
});
|
||||
return sanitizeSubcategory(full || row);
|
||||
};
|
||||
|
||||
const listAssetSubcategories = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.asset_category_id
|
||||
? { asset_category_id: BigInt(query.asset_category_id) }
|
||||
: {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
||||
{
|
||||
asset_categories: {
|
||||
name: { contains: query.search, mode: 'insensitive' },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.asset_subcategories.findMany({
|
||||
where,
|
||||
include: categoryInclude,
|
||||
orderBy: { created_at: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.asset_subcategories.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: rows.map(sanitizeSubcategory),
|
||||
meta: { page, limit, total },
|
||||
};
|
||||
};
|
||||
|
||||
const getAssetSubcategoriesById = async (id) => {
|
||||
const row = await prisma.asset_subcategories.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: categoryInclude,
|
||||
});
|
||||
if (!row) throw new ApiError(404, 'asset_subcategories not found');
|
||||
return sanitizeSubcategory(row);
|
||||
};
|
||||
|
||||
const createAssetSubcategories = async (payload, userId, requestId) => {
|
||||
await assertAssetCategory(payload.asset_category_id);
|
||||
return service.createOne(payload, userId, requestId);
|
||||
const created = await service.createOne(payload, userId, requestId);
|
||||
return withCategory(created);
|
||||
};
|
||||
|
||||
const updateAssetSubcategories = async (id, payload, userId, requestId) => {
|
||||
if (payload.asset_category_id !== undefined) {
|
||||
await assertAssetCategory(payload.asset_category_id);
|
||||
}
|
||||
return service.updateOne(id, payload, userId, requestId);
|
||||
const updated = await service.updateOne(id, payload, userId, requestId);
|
||||
return withCategory(updated);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAssetSubcategories,
|
||||
listAssetSubcategories: service.list,
|
||||
getAssetSubcategoriesById: service.getOne,
|
||||
listAssetSubcategories,
|
||||
getAssetSubcategoriesById,
|
||||
updateAssetSubcategories,
|
||||
deleteAssetSubcategories: service.removeOne,
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user