Initial project setup
This commit is contained in:
commit
4a3f4d4b06
39
.cursor/rules/erp-backend-core.mdc
Normal file
39
.cursor/rules/erp-backend-core.mdc
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
description: ERP backend core stack, security, and API conventions
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# ERP Backend — Core Conventions
|
||||
|
||||
Stack: Node.js 20+ · Express (CommonJS) · PostgreSQL 15+ · Prisma · Joi · Winston
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
- CommonJS only (`require` / `module.exports`) — no ESM
|
||||
- Validate all env vars on boot via `src/config/env.js`; refuse to start if invalid
|
||||
- Never commit `.env`; only `.env.example`
|
||||
- All protected routes: `authenticate` → `authorize(module, action)` → `validate(schema)` → controller
|
||||
- Soft delete: filter `deleted_at: null`; set `deleted_at = now()` instead of hard DELETE
|
||||
- Every mutation calls `auditLog({ tableName, recordId, action, oldValue, newValue, userId, requestId })`
|
||||
- Controllers return `ApiResponse`; throw `ApiError` for failures — no Prisma or business logic in controllers
|
||||
- Serialize `BigInt` as strings in JSON (`BigInt.prototype.toJSON`)
|
||||
|
||||
## Response envelope
|
||||
|
||||
```json
|
||||
{ "success": true, "message": "...", "data": {}, "meta": { "page": 1, "limit": 20, "total": 57 } }
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- JWT access (15m) in Bearer header; refresh tokens hashed (SHA-256) in DB, rotated on refresh
|
||||
- bcrypt passwords; account lockout after failed attempts
|
||||
- AES-256-GCM + HMAC blind index for PII (mobile, bank accounts) — encrypt in service layer
|
||||
- Rate limit `/api` globally; stricter limit on auth routes
|
||||
- File uploads: MIME allow-list, random filenames, serve via authenticated RBAC endpoint only
|
||||
|
||||
## API
|
||||
|
||||
- Routes under `/api/v1/`; pagination `?page=1&limit=20` (max 100); dates ISO 8601 UTC
|
||||
- RBAC actions: `view`, `create`, `edit`, `delete`, `approve`, `export`
|
||||
- Reference: `BACKEND_SETUP.md`, task checklist: `BACKEND_TASKS.md`
|
||||
43
.cursor/rules/erp-backend-modules.mdc
Normal file
43
.cursor/rules/erp-backend-modules.mdc
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
description: HMVC module pattern for ERP backend src/modules
|
||||
globs: src/modules/**/*.js
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# HMVC Module Pattern
|
||||
|
||||
Each module is self-contained with this file set:
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `*.routes.js` | HTTP verbs + middleware chain only |
|
||||
| `*.controller.js` | Parse request, call service, shape `ApiResponse` via `asyncHandler` |
|
||||
| `*.service.js` | Business logic, Prisma calls, `auditLog` |
|
||||
| `*.validation.js` | Joi schemas (body default; `'query'` for list filters) |
|
||||
| `*.repository.js` | **Complex modules only** (PO, GRN, Assets) — multi-table `prisma.$transaction` |
|
||||
|
||||
## Route middleware order
|
||||
|
||||
```js
|
||||
router.post('/', authenticate, authorize('VENDOR', 'create'), validate(createSchema), controller.create);
|
||||
```
|
||||
|
||||
## Service rules
|
||||
|
||||
- Simple masters/CRUD: call Prisma directly from service
|
||||
- Auto codes: `nextDocumentNumber('VENDOR')` inside a transaction
|
||||
- Pass `req.user.id` and `req.id` (request ID) into service for audit trails
|
||||
- Filter all reads with `deleted_at: null`
|
||||
|
||||
## Masters
|
||||
|
||||
- RBAC module code: `MASTERS`
|
||||
- UOM is the template — replicate four-file pattern for other masters
|
||||
- Aggregate sub-routers in `src/modules/masters/index.js`
|
||||
|
||||
## Do not
|
||||
|
||||
- Put Prisma calls or business logic in controllers
|
||||
- Skip `authorize()` on protected endpoints
|
||||
- Hard-delete transactional records
|
||||
- Return raw refresh tokens or passwords in API responses
|
||||
41
.cursor/rules/erp-backend-prisma.mdc
Normal file
41
.cursor/rules/erp-backend-prisma.mdc
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
description: Prisma schema and database conventions for ERP backend
|
||||
globs: prisma/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Prisma & Database Conventions
|
||||
|
||||
## Naming
|
||||
|
||||
- Models: `PascalCase` singular (e.g. `Vendor`)
|
||||
- Fields: `snake_case` (match requirements doc — no camelCase mapping)
|
||||
- Tables: `snake_case` plural via `@@map("vendors")`
|
||||
|
||||
## Required columns (every business table)
|
||||
|
||||
```
|
||||
is_active, created_by, updated_by, created_at, updated_at, deleted_at
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
- Primary keys: `BigInt @id @default(autoincrement())`
|
||||
- Soft delete everywhere on transactional tables
|
||||
- Document numbering via `document_series` + atomic increment in `generateCode.js`
|
||||
- Seed idempotently with `upsert` — modules, permissions, roles, bootstrap Super Admin
|
||||
|
||||
## Migration workflow
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name <description>
|
||||
npx prisma generate
|
||||
node prisma/seed.js
|
||||
```
|
||||
|
||||
## When adding a new table
|
||||
|
||||
1. Add model with common columns + `@@map`
|
||||
2. Create migration
|
||||
3. Update seed if new module/permissions needed
|
||||
4. Add service with `deleted_at: null` filters and audit logging
|
||||
42
.env.example
Normal file
42
.env.example
Normal file
@ -0,0 +1,42 @@
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# PostgreSQL
|
||||
DB_HOST=demo.venbait.in
|
||||
DB_PORT=5432
|
||||
DB_NAME=bharaterp
|
||||
DB_USER=bharaterpdevdbuser
|
||||
DB_PASSWORD=devdbuser@bharaterp
|
||||
DATABASE_URL=postgresql://bharaterpdevdbuser:devdbuser%40bharaterp@demo.venbait.in:5432/bharaterp?schema=public
|
||||
|
||||
# JWT
|
||||
JWT_ACCESS_SECRET=replace_with_strong_random_value_min_32_chars
|
||||
JWT_ACCESS_EXPIRY=15m
|
||||
JWT_REFRESH_SECRET=replace_with_another_strong_random_value_min_32_chars
|
||||
JWT_REFRESH_EXPIRY=7d
|
||||
|
||||
# Password hashing
|
||||
BCRYPT_SALT_ROUNDS=12
|
||||
|
||||
# Encryption
|
||||
ENCRYPTION_KEY=replace_with_64_char_hex_string
|
||||
ENCRYPTION_HMAC_KEY=replace_with_strong_hmac_key
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX=100
|
||||
AUTH_RATE_LIMIT_MAX=10
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Uploads
|
||||
UPLOAD_DIR=uploads
|
||||
MAX_FILE_SIZE_MB=5
|
||||
|
||||
# Account lockout
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOCKOUT_DURATION_MINUTES=30
|
||||
9
.eslintrc.json
Normal file
9
.eslintrc.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"env": { "node": true, "es2022": true, "jest": true },
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"parserOptions": { "ecmaVersion": 2022, "sourceType": "script" },
|
||||
"rules": {
|
||||
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.env
|
||||
logs/
|
||||
uploads/
|
||||
coverage/
|
||||
.DS_Store
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
1518
BACKEND_SETUP.md
Normal file
1518
BACKEND_SETUP.md
Normal file
File diff suppressed because it is too large
Load Diff
186
BACKEND_TASKS.md
Normal file
186
BACKEND_TASKS.md
Normal file
@ -0,0 +1,186 @@
|
||||
# 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`
|
||||
- [ ] Implement idempotent `prisma/seed.js` (modules → permissions → roles → role_permissions → Super Admin user)
|
||||
|
||||
### Seed requirements
|
||||
|
||||
| Step | Content |
|
||||
|------|---------|
|
||||
| Modules | `USERS`, `ROLES`, `MASTERS`, `VENDOR`, `PURCHASE_ORDER`, `GRN`, `ASSET` |
|
||||
| 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 route JSDoc
|
||||
- [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
|
||||
- [ ] 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 | Notes |
|
||||
|---|--------|-------|
|
||||
| 8 | **auth** | login, refresh, logout; rotating hashed refresh tokens; account lockout |
|
||||
| 9 | **masters** | UOM first as template, then replicate 13 sub-masters under `masters/index.js` |
|
||||
| 10 | **vendors** | CRUD + addresses/contacts/bank-details; encrypt bank account numbers |
|
||||
| 11 | **purchase-orders** | Header + line items; repository for transactions; submit/approve/reject/amend/cancel |
|
||||
| 12 | **grn** | Receipt + PO status recalc + asset auto-creation in one transaction |
|
||||
| 13 | **assets** | CRUD + transfer + transfer history |
|
||||
| — | **users** | CRUD, RBAC: `USERS` |
|
||||
| — | **roles** | CRUD + permission assignment, RBAC: `ROLES` |
|
||||
|
||||
Current progress:
|
||||
|
||||
- [x] `auth` module scaffold (`login`, `refresh`, `logout`) implemented
|
||||
- [x] `masters/uom` module implemented as template (CRUD + RBAC + validation + audit log)
|
||||
- [x] Replicated masters CRUD modules: `item-categories`, `brands`, `gst-rates`, `payment-terms`, `delivery-terms`, `asset-categories`, `departments`, `designations`, `document-series`
|
||||
- [x] Pending masters now completed: `item-subcategories`, `items`, `warehouses`, `plants`
|
||||
|
||||
### 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. API Routes (Phase 1)
|
||||
|
||||
```
|
||||
/api/v1/auth/{login,refresh,logout}
|
||||
/api/v1/users
|
||||
/api/v1/roles
|
||||
/api/v1/masters/{uom,item-categories,item-subcategories,items,brands,gst-rates,warehouses,payment-terms,delivery-terms,asset-categories,departments,designations,plants,document-series}
|
||||
/api/v1/vendors (+ /addresses, /contacts, /bank-details)
|
||||
/api/v1/purchase-orders (+ /submit, /approve, /reject, /amend, /cancel, /pdf)
|
||||
/api/v1/grn (+ /cancel, /pdf)
|
||||
/api/v1/assets (+ /transfer, /transfer-history)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
- [ ] `auditLog()` on every create/update/status-change/approve/reject
|
||||
- [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` (cap limit at 100); 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/<module>/<module>.test.js`
|
||||
|
||||
### Minimum Phase 1 coverage
|
||||
|
||||
- [ ] Auth: login, refresh, lockout
|
||||
- [ ] RBAC: allowed vs forbidden
|
||||
- [ ] Vendor: CRUD + status transitions
|
||||
- [ ] PO: status lifecycle
|
||||
- [ ] GRN: partial receipt → PO status recalculation
|
||||
|
||||
---
|
||||
|
||||
## 9. Documentation & DevOps
|
||||
|
||||
- [ ] Add `@swagger` JSDoc blocks to every route file
|
||||
- [ ] `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.
|
||||
32
SETUP_STATUS.md
Normal file
32
SETUP_STATUS.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Setup Status
|
||||
|
||||
Initial setup has been started with your DB config:
|
||||
|
||||
- Host: `demo.venbait.in`
|
||||
- Port: `5432`
|
||||
- Database: `bharaterp`
|
||||
- User: `bharaterpdevdbuser`
|
||||
|
||||
## What is ready
|
||||
|
||||
- Base Node/Express scaffold in `src/`
|
||||
- Environment files (`.env`, `.env.example`)
|
||||
- Prisma datasource in `prisma/schema.prisma`
|
||||
- Database bootstrap script: `scripts/setup-db.sh`
|
||||
|
||||
## Run DB setup
|
||||
|
||||
```bash
|
||||
cd /home/smart/Documents/ERP_BE
|
||||
./scripts/setup-db.sh /home/smart/Downloads/erp_phase1_ddl.sql
|
||||
```
|
||||
|
||||
The script:
|
||||
1. Creates `bharaterp` if it does not exist
|
||||
2. Executes the provided Phase 1 DDL on that database
|
||||
|
||||
## Next steps
|
||||
|
||||
1. Install npm dependencies (if network allows)
|
||||
2. Generate Prisma client: `npm run prisma:generate`
|
||||
3. Start API: `npm run dev`
|
||||
5
jest.config.js
Normal file
5
jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/tests/**/*.test.js'],
|
||||
setupFiles: ['dotenv/config'],
|
||||
};
|
||||
4
nodemon.json
Normal file
4
nodemon.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "js,json,yaml,yml"
|
||||
}
|
||||
7737
package-lock.json
generated
Normal file
7737
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal file
@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "erp-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "ERP backend (Phase 1)",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"lint": "eslint src --ext .js",
|
||||
"lint:fix": "eslint src --ext .js --fix",
|
||||
"format": "prettier --write \"src/**/*.js\"",
|
||||
"test": "jest --runInBand",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:deploy": "prisma migrate deploy",
|
||||
"prisma:seed": "node prisma/seed.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.js": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"helmet": "^7.2.0",
|
||||
"hpp": "^0.2.3",
|
||||
"joi": "^17.13.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.18.3",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.2",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^15.5.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^5.22.0",
|
||||
"supertest": "^6.3.4"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
891
prisma/schema.prisma
Normal file
891
prisma/schema.prisma
Normal file
@ -0,0 +1,891 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model asset_attachments {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
attachment_type String @default("DOCUMENT") @db.VarChar(50)
|
||||
file_name String @db.VarChar(255)
|
||||
file_path String @db.VarChar(500)
|
||||
file_type String? @db.VarChar(100)
|
||||
file_size Int?
|
||||
uploaded_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model asset_categories {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(150)
|
||||
code_prefix String @unique @db.VarChar(10)
|
||||
default_useful_life_years Int?
|
||||
default_depreciation_method String? @db.VarChar(20)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_asset_categories_created_byTousers users? @relation("asset_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_asset_categories_updated_byTousers users? @relation("asset_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
assets assets[]
|
||||
}
|
||||
|
||||
model asset_transfers {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
transfer_date DateTime @db.Date
|
||||
from_plant_id BigInt?
|
||||
to_plant_id BigInt?
|
||||
from_department_id BigInt?
|
||||
to_department_id BigInt?
|
||||
from_user_id BigInt?
|
||||
to_user_id BigInt?
|
||||
from_warehouse_id BigInt?
|
||||
to_warehouse_id BigInt?
|
||||
reason String?
|
||||
transferred_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
departments_asset_transfers_from_department_idTodepartments departments? @relation("asset_transfers_from_department_idTodepartments", fields: [from_department_id], references: [id], onUpdate: NoAction)
|
||||
plants_asset_transfers_from_plant_idToplants plants? @relation("asset_transfers_from_plant_idToplants", fields: [from_plant_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_from_user_idTousers users? @relation("asset_transfers_from_user_idTousers", fields: [from_user_id], references: [id], onUpdate: NoAction)
|
||||
warehouses_asset_transfers_from_warehouse_idTowarehouses warehouses? @relation("asset_transfers_from_warehouse_idTowarehouses", fields: [from_warehouse_id], references: [id], onUpdate: NoAction)
|
||||
departments_asset_transfers_to_department_idTodepartments departments? @relation("asset_transfers_to_department_idTodepartments", fields: [to_department_id], references: [id], onUpdate: NoAction)
|
||||
plants_asset_transfers_to_plant_idToplants plants? @relation("asset_transfers_to_plant_idToplants", fields: [to_plant_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_to_user_idTousers users? @relation("asset_transfers_to_user_idTousers", fields: [to_user_id], references: [id], onUpdate: NoAction)
|
||||
warehouses_asset_transfers_to_warehouse_idTowarehouses warehouses? @relation("asset_transfers_to_warehouse_idTowarehouses", fields: [to_warehouse_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_transferred_byTousers users? @relation("asset_transfers_transferred_byTousers", fields: [transferred_by], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_id], map: "idx_asset_transfers_asset_id")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model assets {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_code String @unique @db.VarChar(30)
|
||||
asset_name String @db.VarChar(200)
|
||||
asset_category_id BigInt
|
||||
brand_model String? @db.VarChar(200)
|
||||
manufacturer String? @db.VarChar(200)
|
||||
serial_number String? @db.VarChar(100)
|
||||
part_number String? @db.VarChar(100)
|
||||
plant_id BigInt
|
||||
department_id BigInt?
|
||||
warehouse_id BigInt?
|
||||
location_detail String? @db.VarChar(200)
|
||||
assigned_to_user_id BigInt?
|
||||
vendor_id BigInt?
|
||||
po_id BigInt?
|
||||
grn_id BigInt?
|
||||
grn_item_id BigInt?
|
||||
purchase_date DateTime? @db.Date
|
||||
purchase_cost Decimal @default(0) @db.Decimal(15, 4)
|
||||
useful_life_years Int?
|
||||
depreciation_method String? @db.VarChar(10)
|
||||
salvage_value Decimal? @default(0) @db.Decimal(15, 4)
|
||||
warranty_expiry_date DateTime? @db.Date
|
||||
amc_start_date DateTime? @db.Date
|
||||
amc_end_date DateTime? @db.Date
|
||||
amc_vendor_id BigInt?
|
||||
insurance_policy_no String? @db.VarChar(100)
|
||||
insurance_expiry_date DateTime? @db.Date
|
||||
condition String @default("NEW") @db.VarChar(20)
|
||||
status String @default("IN_USE") @db.VarChar(30)
|
||||
qr_code_value String? @db.VarChar(100)
|
||||
disposal_date DateTime? @db.Date
|
||||
disposal_reason String?
|
||||
disposal_value Decimal? @db.Decimal(15, 4)
|
||||
remarks String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
asset_attachments asset_attachments[]
|
||||
asset_transfers asset_transfers[]
|
||||
vendors_assets_amc_vendor_idTovendors vendors? @relation("assets_amc_vendor_idTovendors", fields: [amc_vendor_id], references: [id], onUpdate: NoAction)
|
||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_assigned_to_user_idTousers users? @relation("assets_assigned_to_user_idTousers", fields: [assigned_to_user_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
||||
grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction)
|
||||
grn_items grn_items? @relation(fields: [grn_item_id], references: [id], onUpdate: NoAction)
|
||||
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders? @relation(fields: [po_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_updated_byTousers users? @relation("assets_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||
warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_category_id], map: "idx_assets_category_id")
|
||||
@@index([department_id], map: "idx_assets_dept_id")
|
||||
@@index([plant_id], map: "idx_assets_plant_id")
|
||||
}
|
||||
|
||||
model audit_logs {
|
||||
id BigInt @id @default(autoincrement())
|
||||
table_name String @db.VarChar(100)
|
||||
record_id BigInt
|
||||
action String @db.VarChar(50)
|
||||
old_value Json?
|
||||
new_value Json?
|
||||
performed_by BigInt?
|
||||
request_id String? @db.VarChar(50)
|
||||
performed_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users users? @relation(fields: [performed_by], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([performed_at], map: "idx_audit_logs_performed_at")
|
||||
@@index([performed_by], map: "idx_audit_logs_performed_by")
|
||||
@@index([table_name, record_id], map: "idx_audit_logs_table_record")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model brands {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(150)
|
||||
brand_type String @default("OWN") @db.VarChar(20)
|
||||
contact_person String? @db.VarChar(150)
|
||||
phone String? @db.VarChar(15)
|
||||
email String? @db.VarChar(150)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_brands_created_byTousers users? @relation("brands_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_brands_updated_byTousers users? @relation("brands_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
items items[]
|
||||
purchase_orders purchase_orders[]
|
||||
}
|
||||
|
||||
model delivery_terms {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(100)
|
||||
description String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_delivery_terms_created_byTousers users? @relation("delivery_terms_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_delivery_terms_updated_byTousers users? @relation("delivery_terms_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders[]
|
||||
}
|
||||
|
||||
model departments {
|
||||
id BigInt @id @default(autoincrement())
|
||||
name String @unique @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
asset_transfers_asset_transfers_from_department_idTodepartments asset_transfers[] @relation("asset_transfers_from_department_idTodepartments")
|
||||
asset_transfers_asset_transfers_to_department_idTodepartments asset_transfers[] @relation("asset_transfers_to_department_idTodepartments")
|
||||
assets assets[]
|
||||
users users[]
|
||||
}
|
||||
|
||||
model designations {
|
||||
id BigInt @id @default(autoincrement())
|
||||
name String @unique @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users users[]
|
||||
}
|
||||
|
||||
model document_series {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(50)
|
||||
prefix String @db.VarChar(30)
|
||||
current_number Int @default(0)
|
||||
padding Int @default(5)
|
||||
description String? @db.VarChar(200)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users_document_series_created_byTousers users? @relation("document_series_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_document_series_updated_byTousers users? @relation("document_series_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model grn {
|
||||
id BigInt @id @default(autoincrement())
|
||||
grn_number String @unique @db.VarChar(30)
|
||||
grn_date DateTime @db.Date
|
||||
po_id BigInt
|
||||
vendor_id BigInt
|
||||
warehouse_id BigInt
|
||||
vendor_invoice_no String? @db.VarChar(100)
|
||||
vendor_invoice_date DateTime? @db.Date
|
||||
vendor_invoice_amount Decimal? @db.Decimal(15, 4)
|
||||
vehicle_no String? @db.VarChar(30)
|
||||
lr_no String? @db.VarChar(50)
|
||||
lr_date DateTime? @db.Date
|
||||
status String @default("POSTED") @db.VarChar(20)
|
||||
cancellation_reason String?
|
||||
cancelled_by BigInt?
|
||||
cancelled_at DateTime? @db.Timestamptz(6)
|
||||
received_by BigInt?
|
||||
quality_checked_by BigInt?
|
||||
remarks String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
assets assets[]
|
||||
users_grn_cancelled_byTousers users? @relation("grn_cancelled_byTousers", fields: [cancelled_by], references: [id], onUpdate: NoAction)
|
||||
users_grn_created_byTousers users? @relation("grn_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onUpdate: NoAction)
|
||||
users_grn_quality_checked_byTousers users? @relation("grn_quality_checked_byTousers", fields: [quality_checked_by], references: [id], onUpdate: NoAction)
|
||||
users_grn_received_byTousers users? @relation("grn_received_byTousers", fields: [received_by], references: [id], onUpdate: NoAction)
|
||||
users_grn_updated_byTousers users? @relation("grn_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||
warehouses warehouses @relation(fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||
grn_attachments grn_attachments[]
|
||||
grn_items grn_items[]
|
||||
|
||||
@@index([grn_date], map: "idx_grn_grn_date")
|
||||
@@index([po_id], map: "idx_grn_po_id")
|
||||
@@index([vendor_id], map: "idx_grn_vendor_id")
|
||||
}
|
||||
|
||||
model grn_attachments {
|
||||
id BigInt @id @default(autoincrement())
|
||||
grn_id BigInt
|
||||
file_name String @db.VarChar(255)
|
||||
file_path String @db.VarChar(500)
|
||||
file_type String? @db.VarChar(100)
|
||||
file_size Int?
|
||||
uploaded_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
grn grn @relation(fields: [grn_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model grn_items {
|
||||
id BigInt @id @default(autoincrement())
|
||||
grn_id BigInt
|
||||
po_item_id BigInt
|
||||
item_id BigInt
|
||||
line_no Int
|
||||
ordered_qty Decimal @db.Decimal(15, 4)
|
||||
previously_received_qty Decimal @default(0) @db.Decimal(15, 4)
|
||||
current_qty Decimal @db.Decimal(15, 4)
|
||||
accepted_qty Decimal @db.Decimal(15, 4)
|
||||
rejected_qty Decimal @default(0) @db.Decimal(15, 4)
|
||||
rejection_reason String?
|
||||
uom_id BigInt
|
||||
rate Decimal @db.Decimal(15, 4)
|
||||
batch_no String? @db.VarChar(100)
|
||||
mfg_date DateTime? @db.Date
|
||||
expiry_date DateTime? @db.Date
|
||||
storage_location String? @db.VarChar(100)
|
||||
remarks String?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
assets assets[]
|
||||
grn grn @relation(fields: [grn_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
items items @relation(fields: [item_id], references: [id], onUpdate: NoAction)
|
||||
purchase_order_items purchase_order_items @relation(fields: [po_item_id], references: [id], onUpdate: NoAction)
|
||||
uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@unique([grn_id, line_no])
|
||||
@@index([grn_id], map: "idx_grn_items_grn_id")
|
||||
@@index([item_id], map: "idx_grn_items_item_id")
|
||||
}
|
||||
|
||||
model gst_rates {
|
||||
id BigInt @id @default(autoincrement())
|
||||
rate_pct Decimal @unique @db.Decimal(5, 2)
|
||||
description String? @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
items items[]
|
||||
purchase_order_items purchase_order_items[]
|
||||
}
|
||||
|
||||
model hsn_codes {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(20)
|
||||
description String?
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
items items[]
|
||||
purchase_order_items purchase_order_items[]
|
||||
}
|
||||
|
||||
model item_categories {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_item_categories_created_byTousers users? @relation("item_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_item_categories_updated_byTousers users? @relation("item_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
item_subcategories item_subcategories[]
|
||||
items items[]
|
||||
}
|
||||
|
||||
model item_subcategories {
|
||||
id BigInt @id @default(autoincrement())
|
||||
item_category_id BigInt
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_item_subcategories_created_byTousers users? @relation("item_subcategories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction)
|
||||
users_item_subcategories_updated_byTousers users? @relation("item_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
items items[]
|
||||
}
|
||||
|
||||
model items {
|
||||
id BigInt @id @default(autoincrement())
|
||||
item_code String @unique @db.VarChar(50)
|
||||
item_name String @db.VarChar(200)
|
||||
item_category_id BigInt
|
||||
item_subcategory_id BigInt?
|
||||
uom_id BigInt
|
||||
hsn_code_id BigInt?
|
||||
gst_rate_id BigInt?
|
||||
brand_id BigInt?
|
||||
is_asset_item Boolean @default(false)
|
||||
description String?
|
||||
specification String?
|
||||
min_order_qty Decimal? @db.Decimal(15, 4)
|
||||
reorder_level Decimal? @db.Decimal(15, 4)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
grn_items grn_items[]
|
||||
brands brands? @relation(fields: [brand_id], references: [id], onUpdate: NoAction)
|
||||
users_items_created_byTousers users? @relation("items_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
gst_rates gst_rates? @relation(fields: [gst_rate_id], references: [id], onUpdate: NoAction)
|
||||
hsn_codes hsn_codes? @relation(fields: [hsn_code_id], references: [id], onUpdate: NoAction)
|
||||
item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction)
|
||||
item_subcategories item_subcategories? @relation(fields: [item_subcategory_id], references: [id], onUpdate: NoAction)
|
||||
uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction)
|
||||
users_items_updated_byTousers users? @relation("items_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
purchase_order_items purchase_order_items[]
|
||||
vendor_item_mapping vendor_item_mapping[]
|
||||
|
||||
@@index([item_category_id], map: "idx_items_category")
|
||||
@@index([item_name(ops: raw("gin_trgm_ops"))], map: "idx_items_name_trgm", type: Gin)
|
||||
}
|
||||
|
||||
model modules {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(50)
|
||||
name String @db.VarChar(100)
|
||||
parent_module_id BigInt?
|
||||
sort_order Int @default(0)
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
modules modules? @relation("modulesTomodules", fields: [parent_module_id], references: [id], onUpdate: NoAction)
|
||||
other_modules modules[] @relation("modulesTomodules")
|
||||
permissions permissions[]
|
||||
}
|
||||
|
||||
model password_reset_tokens {
|
||||
id BigInt @id @default(autoincrement())
|
||||
user_id BigInt
|
||||
token_hash String @unique @db.VarChar(64)
|
||||
expires_at DateTime @db.Timestamptz(6)
|
||||
used_at DateTime? @db.Timestamptz(6)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model payment_terms {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(100)
|
||||
credit_days Int @default(0)
|
||||
description String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_payment_terms_created_byTousers users? @relation("payment_terms_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_payment_terms_updated_byTousers users? @relation("payment_terms_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders[]
|
||||
vendors vendors[]
|
||||
}
|
||||
|
||||
model permissions {
|
||||
id BigInt @id @default(autoincrement())
|
||||
module_id BigInt
|
||||
action String @db.VarChar(50)
|
||||
is_active Boolean @default(true)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
modules modules @relation(fields: [module_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
role_permissions role_permissions[]
|
||||
|
||||
@@unique([module_id, action])
|
||||
}
|
||||
|
||||
model plants {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(150)
|
||||
gstin String? @db.VarChar(15)
|
||||
address String?
|
||||
city String? @db.VarChar(100)
|
||||
state String? @db.VarChar(100)
|
||||
pincode String? @db.VarChar(10)
|
||||
phone String? @db.VarChar(15)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
asset_transfers_asset_transfers_from_plant_idToplants asset_transfers[] @relation("asset_transfers_from_plant_idToplants")
|
||||
asset_transfers_asset_transfers_to_plant_idToplants asset_transfers[] @relation("asset_transfers_to_plant_idToplants")
|
||||
assets assets[]
|
||||
users_plants_created_byTousers users? @relation("plants_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_plants_created_by")
|
||||
users_plants_updated_byTousers users? @relation("plants_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_plants_updated_by")
|
||||
purchase_orders purchase_orders[]
|
||||
users_users_plant_idToplants users[] @relation("users_plant_idToplants")
|
||||
warehouses warehouses[]
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model po_approvals {
|
||||
id BigInt @id @default(autoincrement())
|
||||
po_id BigInt
|
||||
approval_level Int
|
||||
approver_role_id BigInt?
|
||||
approver_user_id BigInt?
|
||||
status String @default("PENDING") @db.VarChar(20)
|
||||
remarks String?
|
||||
acted_at DateTime? @db.Timestamptz(6)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
roles roles? @relation(fields: [approver_role_id], references: [id], onUpdate: NoAction)
|
||||
users users? @relation(fields: [approver_user_id], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([po_id], map: "idx_po_approvals_po_id")
|
||||
}
|
||||
|
||||
model po_attachments {
|
||||
id BigInt @id @default(autoincrement())
|
||||
po_id BigInt
|
||||
file_name String @db.VarChar(255)
|
||||
file_path String @db.VarChar(500)
|
||||
file_type String? @db.VarChar(100)
|
||||
file_size Int?
|
||||
uploaded_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
users users? @relation(fields: [uploaded_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model purchase_order_items {
|
||||
id BigInt @id @default(autoincrement())
|
||||
po_id BigInt
|
||||
item_id BigInt
|
||||
line_no Int
|
||||
ordered_qty Decimal @db.Decimal(15, 4)
|
||||
uom_id BigInt
|
||||
rate Decimal @db.Decimal(15, 4)
|
||||
discount_pct Decimal @default(0) @db.Decimal(5, 2)
|
||||
discount_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
gst_rate_id BigInt?
|
||||
taxable_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
tax_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
line_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
received_qty Decimal @default(0) @db.Decimal(15, 4)
|
||||
hsn_code_id BigInt?
|
||||
remarks String?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
grn_items grn_items[]
|
||||
gst_rates gst_rates? @relation(fields: [gst_rate_id], references: [id], onUpdate: NoAction)
|
||||
hsn_codes hsn_codes? @relation(fields: [hsn_code_id], references: [id], onUpdate: NoAction)
|
||||
items items @relation(fields: [item_id], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders @relation(fields: [po_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
uom uom @relation(fields: [uom_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@unique([po_id, line_no])
|
||||
@@index([item_id], map: "idx_po_items_item_id")
|
||||
@@index([po_id], map: "idx_po_items_po_id")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model purchase_orders {
|
||||
id BigInt @id @default(autoincrement())
|
||||
po_number String @unique @db.VarChar(30)
|
||||
po_date DateTime @db.Date
|
||||
po_type String @db.VarChar(30)
|
||||
vendor_id BigInt
|
||||
plant_id BigInt
|
||||
warehouse_id BigInt?
|
||||
brand_id BigInt?
|
||||
payment_term_id BigInt?
|
||||
delivery_term_id BigInt?
|
||||
expected_delivery_date DateTime? @db.Date
|
||||
sub_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
discount_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
tax_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
freight_charges Decimal @default(0) @db.Decimal(15, 4)
|
||||
other_charges Decimal @default(0) @db.Decimal(15, 4)
|
||||
grand_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
status String @default("DRAFT") @db.VarChar(30)
|
||||
revision_no Int @default(0)
|
||||
parent_po_id BigInt?
|
||||
terms_and_conditions String?
|
||||
remarks String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
assets assets[]
|
||||
grn grn[]
|
||||
po_approvals po_approvals[]
|
||||
po_attachments po_attachments[]
|
||||
purchase_order_items purchase_order_items[]
|
||||
brands brands? @relation(fields: [brand_id], references: [id], onUpdate: NoAction)
|
||||
users_purchase_orders_created_byTousers users? @relation("purchase_orders_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
delivery_terms delivery_terms? @relation(fields: [delivery_term_id], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders? @relation("purchase_ordersTopurchase_orders", fields: [parent_po_id], references: [id], onUpdate: NoAction)
|
||||
other_purchase_orders purchase_orders[] @relation("purchase_ordersTopurchase_orders")
|
||||
payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction)
|
||||
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
users_purchase_orders_updated_byTousers users? @relation("purchase_orders_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||
warehouses warehouses? @relation(fields: [warehouse_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
|
||||
|
||||
@@index([created_at], map: "idx_po_created_at")
|
||||
@@index([po_date], map: "idx_po_po_date")
|
||||
@@index([vendor_id], map: "idx_po_vendor_id")
|
||||
}
|
||||
|
||||
model refresh_tokens {
|
||||
id BigInt @id @default(autoincrement())
|
||||
user_id BigInt
|
||||
token_hash String @unique @db.VarChar(64)
|
||||
expires_at DateTime @db.Timestamptz(6)
|
||||
revoked_at DateTime? @db.Timestamptz(6)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([user_id], map: "idx_refresh_tokens_user_id")
|
||||
}
|
||||
|
||||
model role_permissions {
|
||||
id BigInt @id @default(autoincrement())
|
||||
role_id BigInt
|
||||
permission_id BigInt
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
permissions permissions @relation(fields: [permission_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@unique([role_id, permission_id])
|
||||
}
|
||||
|
||||
model roles {
|
||||
id BigInt @id @default(autoincrement())
|
||||
name String @unique @db.VarChar(100)
|
||||
description String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
po_approvals po_approvals[]
|
||||
role_permissions role_permissions[]
|
||||
users_roles_created_byTousers users? @relation("roles_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_roles_created_by")
|
||||
users_roles_updated_byTousers users? @relation("roles_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_roles_updated_by")
|
||||
users_users_role_idToroles users[] @relation("users_role_idToroles")
|
||||
}
|
||||
|
||||
model uom {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
grn_items grn_items[]
|
||||
items items[]
|
||||
purchase_order_items purchase_order_items[]
|
||||
users_uom_created_byTousers users? @relation("uom_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_uom_updated_byTousers users? @relation("uom_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model users {
|
||||
id BigInt @id @default(autoincrement())
|
||||
employee_code String @unique @db.VarChar(30)
|
||||
full_name String @db.VarChar(200)
|
||||
email String @unique @db.VarChar(200)
|
||||
mobile String? @db.VarChar(20)
|
||||
password_hash String @db.VarChar(255)
|
||||
role_id BigInt?
|
||||
department_id BigInt?
|
||||
designation_id BigInt?
|
||||
plant_id BigInt?
|
||||
reporting_to BigInt?
|
||||
status String @default("active") @db.VarChar(20)
|
||||
is_active Boolean @default(true)
|
||||
failed_login_attempts Int @default(0)
|
||||
locked_until DateTime? @db.Timestamptz(6)
|
||||
last_login_at DateTime? @db.Timestamptz(6)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
asset_attachments asset_attachments[]
|
||||
asset_categories_asset_categories_created_byTousers asset_categories[] @relation("asset_categories_created_byTousers")
|
||||
asset_categories_asset_categories_updated_byTousers asset_categories[] @relation("asset_categories_updated_byTousers")
|
||||
asset_transfers_asset_transfers_from_user_idTousers asset_transfers[] @relation("asset_transfers_from_user_idTousers")
|
||||
asset_transfers_asset_transfers_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers")
|
||||
asset_transfers_asset_transfers_transferred_byTousers asset_transfers[] @relation("asset_transfers_transferred_byTousers")
|
||||
assets_assets_assigned_to_user_idTousers assets[] @relation("assets_assigned_to_user_idTousers")
|
||||
assets_assets_created_byTousers assets[] @relation("assets_created_byTousers")
|
||||
assets_assets_updated_byTousers assets[] @relation("assets_updated_byTousers")
|
||||
audit_logs audit_logs[]
|
||||
brands_brands_created_byTousers brands[] @relation("brands_created_byTousers")
|
||||
brands_brands_updated_byTousers brands[] @relation("brands_updated_byTousers")
|
||||
delivery_terms_delivery_terms_created_byTousers delivery_terms[] @relation("delivery_terms_created_byTousers")
|
||||
delivery_terms_delivery_terms_updated_byTousers delivery_terms[] @relation("delivery_terms_updated_byTousers")
|
||||
document_series_document_series_created_byTousers document_series[] @relation("document_series_created_byTousers")
|
||||
document_series_document_series_updated_byTousers document_series[] @relation("document_series_updated_byTousers")
|
||||
grn_grn_cancelled_byTousers grn[] @relation("grn_cancelled_byTousers")
|
||||
grn_grn_created_byTousers grn[] @relation("grn_created_byTousers")
|
||||
grn_grn_quality_checked_byTousers grn[] @relation("grn_quality_checked_byTousers")
|
||||
grn_grn_received_byTousers grn[] @relation("grn_received_byTousers")
|
||||
grn_grn_updated_byTousers grn[] @relation("grn_updated_byTousers")
|
||||
grn_attachments grn_attachments[]
|
||||
item_categories_item_categories_created_byTousers item_categories[] @relation("item_categories_created_byTousers")
|
||||
item_categories_item_categories_updated_byTousers item_categories[] @relation("item_categories_updated_byTousers")
|
||||
item_subcategories_item_subcategories_created_byTousers item_subcategories[] @relation("item_subcategories_created_byTousers")
|
||||
item_subcategories_item_subcategories_updated_byTousers item_subcategories[] @relation("item_subcategories_updated_byTousers")
|
||||
items_items_created_byTousers items[] @relation("items_created_byTousers")
|
||||
items_items_updated_byTousers items[] @relation("items_updated_byTousers")
|
||||
password_reset_tokens password_reset_tokens[]
|
||||
payment_terms_payment_terms_created_byTousers payment_terms[] @relation("payment_terms_created_byTousers")
|
||||
payment_terms_payment_terms_updated_byTousers payment_terms[] @relation("payment_terms_updated_byTousers")
|
||||
plants_plants_created_byTousers plants[] @relation("plants_created_byTousers")
|
||||
plants_plants_updated_byTousers plants[] @relation("plants_updated_byTousers")
|
||||
po_approvals po_approvals[]
|
||||
po_attachments po_attachments[]
|
||||
purchase_orders_purchase_orders_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers")
|
||||
purchase_orders_purchase_orders_updated_byTousers purchase_orders[] @relation("purchase_orders_updated_byTousers")
|
||||
refresh_tokens refresh_tokens[]
|
||||
roles_roles_created_byTousers roles[] @relation("roles_created_byTousers")
|
||||
roles_roles_updated_byTousers roles[] @relation("roles_updated_byTousers")
|
||||
uom_uom_created_byTousers uom[] @relation("uom_created_byTousers")
|
||||
uom_uom_updated_byTousers uom[] @relation("uom_updated_byTousers")
|
||||
users_users_created_byTousers users? @relation("users_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
other_users_users_created_byTousers users[] @relation("users_created_byTousers")
|
||||
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
||||
designations designations? @relation(fields: [designation_id], references: [id], onUpdate: NoAction)
|
||||
plants_users_plant_idToplants plants? @relation("users_plant_idToplants", fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
users_users_reporting_toTousers users? @relation("users_reporting_toTousers", fields: [reporting_to], references: [id], onUpdate: NoAction)
|
||||
other_users_users_reporting_toTousers users[] @relation("users_reporting_toTousers")
|
||||
roles_users_role_idToroles roles? @relation("users_role_idToroles", fields: [role_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
|
||||
users_users_updated_byTousers users? @relation("users_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
other_users_users_updated_byTousers users[] @relation("users_updated_byTousers")
|
||||
vendor_addresses_vendor_addresses_created_byTousers vendor_addresses[] @relation("vendor_addresses_created_byTousers")
|
||||
vendor_addresses_vendor_addresses_updated_byTousers vendor_addresses[] @relation("vendor_addresses_updated_byTousers")
|
||||
vendor_bank_details_vendor_bank_details_created_byTousers vendor_bank_details[] @relation("vendor_bank_details_created_byTousers")
|
||||
vendor_bank_details_vendor_bank_details_updated_byTousers vendor_bank_details[] @relation("vendor_bank_details_updated_byTousers")
|
||||
vendor_contacts_vendor_contacts_created_byTousers vendor_contacts[] @relation("vendor_contacts_created_byTousers")
|
||||
vendor_contacts_vendor_contacts_updated_byTousers vendor_contacts[] @relation("vendor_contacts_updated_byTousers")
|
||||
vendor_item_mapping_vendor_item_mapping_created_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_created_byTousers")
|
||||
vendor_item_mapping_vendor_item_mapping_updated_byTousers vendor_item_mapping[] @relation("vendor_item_mapping_updated_byTousers")
|
||||
vendors_vendors_created_byTousers vendors[] @relation("vendors_created_byTousers")
|
||||
vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers")
|
||||
warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers")
|
||||
warehouses_warehouses_updated_byTousers warehouses[] @relation("warehouses_updated_byTousers")
|
||||
|
||||
@@index([role_id], map: "idx_users_role_id")
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model vendor_addresses {
|
||||
id BigInt @id @default(autoincrement())
|
||||
vendor_id BigInt
|
||||
address_type String @db.VarChar(20)
|
||||
address_line1 String? @db.VarChar(200)
|
||||
address_line2 String? @db.VarChar(200)
|
||||
city String? @db.VarChar(100)
|
||||
state String? @db.VarChar(100)
|
||||
pincode String? @db.VarChar(10)
|
||||
country String @default("India") @db.VarChar(100)
|
||||
gstin String? @db.VarChar(15)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users_vendor_addresses_created_byTousers users? @relation("vendor_addresses_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_vendor_addresses_updated_byTousers users? @relation("vendor_addresses_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model vendor_bank_details {
|
||||
id BigInt @id @default(autoincrement())
|
||||
vendor_id BigInt
|
||||
bank_name String @db.VarChar(150)
|
||||
branch String? @db.VarChar(150)
|
||||
account_number String @db.VarChar(255)
|
||||
account_number_index String? @db.VarChar(64)
|
||||
ifsc String @db.VarChar(15)
|
||||
account_holder_name String @db.VarChar(200)
|
||||
account_type String @default("CURRENT") @db.VarChar(20)
|
||||
is_primary Boolean @default(false)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users_vendor_bank_details_created_byTousers users? @relation("vendor_bank_details_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_vendor_bank_details_updated_byTousers users? @relation("vendor_bank_details_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model vendor_contacts {
|
||||
id BigInt @id @default(autoincrement())
|
||||
vendor_id BigInt
|
||||
contact_name String @db.VarChar(150)
|
||||
designation String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(15)
|
||||
email String? @db.VarChar(150)
|
||||
is_primary Boolean @default(false)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users_vendor_contacts_created_byTousers users? @relation("vendor_contacts_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_vendor_contacts_updated_byTousers users? @relation("vendor_contacts_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
}
|
||||
|
||||
model vendor_item_mapping {
|
||||
id BigInt @id @default(autoincrement())
|
||||
vendor_id BigInt
|
||||
item_id BigInt
|
||||
last_purchase_rate Decimal? @db.Decimal(15, 4)
|
||||
is_preferred Boolean @default(false)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users_vendor_item_mapping_created_byTousers users? @relation("vendor_item_mapping_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
items items @relation(fields: [item_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
users_vendor_item_mapping_updated_byTousers users? @relation("vendor_item_mapping_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@unique([vendor_id, item_id])
|
||||
}
|
||||
|
||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||
model vendors {
|
||||
id BigInt @id @default(autoincrement())
|
||||
vendor_code String @unique @db.VarChar(30)
|
||||
vendor_name String @db.VarChar(200)
|
||||
vendor_type String @db.VarChar(30)
|
||||
gstin String? @db.VarChar(15)
|
||||
pan String? @db.VarChar(10)
|
||||
payment_term_id BigInt?
|
||||
credit_period_days Int @default(0)
|
||||
status String @default("active") @db.VarChar(20)
|
||||
remarks String?
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
assets_assets_amc_vendor_idTovendors assets[] @relation("assets_amc_vendor_idTovendors")
|
||||
assets_assets_vendor_idTovendors assets[] @relation("assets_vendor_idTovendors")
|
||||
grn grn[]
|
||||
purchase_orders purchase_orders[]
|
||||
vendor_addresses vendor_addresses[]
|
||||
vendor_bank_details vendor_bank_details[]
|
||||
vendor_contacts vendor_contacts[]
|
||||
vendor_item_mapping vendor_item_mapping[]
|
||||
users_vendors_created_byTousers users? @relation("vendors_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction)
|
||||
users_vendors_updated_byTousers users? @relation("vendors_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([vendor_name(ops: raw("gin_trgm_ops"))], map: "idx_vendors_name_trgm", type: Gin)
|
||||
}
|
||||
|
||||
model warehouses {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(150)
|
||||
plant_id BigInt
|
||||
location String? @db.VarChar(200)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
asset_transfers_asset_transfers_from_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_from_warehouse_idTowarehouses")
|
||||
asset_transfers_asset_transfers_to_warehouse_idTowarehouses asset_transfers[] @relation("asset_transfers_to_warehouse_idTowarehouses")
|
||||
assets assets[]
|
||||
grn grn[]
|
||||
purchase_orders purchase_orders[]
|
||||
users_warehouses_created_byTousers users? @relation("warehouses_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
plants plants @relation(fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
users_warehouses_updated_byTousers users? @relation("warehouses_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
}
|
||||
49
prisma/seed.js
Normal file
49
prisma/seed.js
Normal file
@ -0,0 +1,49 @@
|
||||
/* eslint-disable no-console */
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const superAdminRole = await prisma.roles.findFirst({
|
||||
where: { name: 'Super Admin', deleted_at: null },
|
||||
});
|
||||
|
||||
if (!superAdminRole) {
|
||||
throw new Error('Super Admin role not found. Run erp_phase1_ddl.sql first.');
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash('Admin@123', 12);
|
||||
|
||||
await prisma.users.upsert({
|
||||
where: { email: 'admin@bharaterp.com' },
|
||||
update: {
|
||||
role_id: superAdminRole.id,
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
deleted_at: null,
|
||||
},
|
||||
create: {
|
||||
employee_code: 'EMP001',
|
||||
full_name: 'Super Admin',
|
||||
email: 'admin@bharaterp.com',
|
||||
password_hash: passwordHash,
|
||||
role_id: superAdminRole.id,
|
||||
status: 'active',
|
||||
is_active: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Bootstrap Super Admin ready: admin@bharaterp.com / Admin@123');
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(async (err) => {
|
||||
console.error('Seed failed', err);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
39
scripts/setup-db.sh
Executable file
39
scripts/setup-db.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DB_HOST="${DB_HOST:-demo.venbait.in}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-bharaterp}"
|
||||
DB_USER="${DB_USER:-bharaterpdevdbuser}"
|
||||
DB_PASSWORD="${DB_PASSWORD:-devdbuser@bharaterp}"
|
||||
DDL_PATH="${1:-/home/smart/Downloads/erp_phase1_ddl.sql}"
|
||||
PRISMA_VERSION="${PRISMA_VERSION:-5.22.0}"
|
||||
|
||||
if [ ! -f "$DDL_PATH" ]; then
|
||||
echo "DDL file not found: $DDL_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENCODED_PASSWORD="${DB_PASSWORD//@/%40}"
|
||||
POSTGRES_URL="postgresql://${DB_USER}:${ENCODED_PASSWORD}@${DB_HOST}:${DB_PORT}/postgres?schema=public"
|
||||
TARGET_URL="postgresql://${DB_USER}:${ENCODED_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=public"
|
||||
|
||||
echo "Checking Prisma CLI availability (npx prisma@${PRISMA_VERSION})"
|
||||
npx "prisma@${PRISMA_VERSION}" --version >/dev/null
|
||||
|
||||
echo "Trying to create database: $DB_NAME"
|
||||
set +e
|
||||
DATABASE_URL="$POSTGRES_URL" npx "prisma@${PRISMA_VERSION}" db execute --stdin --schema prisma/schema.prisma <<SQL
|
||||
CREATE DATABASE ${DB_NAME};
|
||||
SQL
|
||||
CREATE_EXIT=$?
|
||||
set -e
|
||||
|
||||
if [ "$CREATE_EXIT" -ne 0 ]; then
|
||||
echo "Create DB skipped (likely already exists or insufficient CREATE DATABASE privilege). Continuing..."
|
||||
fi
|
||||
|
||||
echo "Applying DDL file: $DDL_PATH"
|
||||
DATABASE_URL="$TARGET_URL" npx "prisma@${PRISMA_VERSION}" db execute --file "$DDL_PATH" --schema prisma/schema.prisma
|
||||
|
||||
echo "Database setup complete for $DB_NAME on $DB_HOST"
|
||||
55
src/app.js
Normal file
55
src/app.js
Normal file
@ -0,0 +1,55 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const hpp = require('hpp');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const env = require('./config/env');
|
||||
const swaggerSpec = require('./config/swagger');
|
||||
const morganMiddleware = require('./config/morgan');
|
||||
const requestId = require('./middlewares/requestId.middleware');
|
||||
const { generalLimiter } = require('./middlewares/rateLimiter.middleware');
|
||||
const errorMiddleware = require('./middlewares/error.middleware');
|
||||
const ApiError = require('./utils/ApiError');
|
||||
const routesV1 = require('./routes/v1');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: env.CORS_ORIGINS?.split(',').map((o) => o.trim()) || ['*'] }));
|
||||
app.use(hpp());
|
||||
app.use(compression());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(requestId);
|
||||
app.use(morganMiddleware);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ success: true, message: 'OK', uptime: process.uptime() });
|
||||
});
|
||||
|
||||
app.use('/api', generalLimiter);
|
||||
app.use('/api/v1', routesV1);
|
||||
|
||||
app.get('/api-docs.json', (_req, res) => {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
res.json(swaggerSpec);
|
||||
});
|
||||
|
||||
app.use(
|
||||
'/api-docs',
|
||||
helmet({ contentSecurityPolicy: false }),
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(null, {
|
||||
explorer: true,
|
||||
swaggerUrl: '/api-docs.json',
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
app.use((req, _res, next) => next(new ApiError(404, `Route not found: ${req.originalUrl}`)));
|
||||
app.use(errorMiddleware);
|
||||
|
||||
module.exports = app;
|
||||
45
src/config/env.js
Normal file
45
src/config/env.js
Normal file
@ -0,0 +1,45 @@
|
||||
const path = require('path');
|
||||
const dotenv = require('dotenv');
|
||||
const Joi = require('joi');
|
||||
|
||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||
|
||||
const envSchema = Joi.object({
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
|
||||
DATABASE_URL: Joi.string().required(),
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(5432),
|
||||
DB_NAME: Joi.string().required(),
|
||||
DB_USER: Joi.string().required(),
|
||||
DB_PASSWORD: Joi.string().required(),
|
||||
|
||||
JWT_ACCESS_SECRET: Joi.string().min(32).required(),
|
||||
JWT_ACCESS_EXPIRY: Joi.string().default('15m'),
|
||||
JWT_REFRESH_SECRET: Joi.string().min(32).required(),
|
||||
JWT_REFRESH_EXPIRY: Joi.string().default('7d'),
|
||||
|
||||
BCRYPT_SALT_ROUNDS: Joi.number().default(12),
|
||||
ENCRYPTION_KEY: Joi.string().length(64).required(),
|
||||
ENCRYPTION_HMAC_KEY: Joi.string().min(32).required(),
|
||||
|
||||
CORS_ORIGINS: Joi.string().default('*'),
|
||||
RATE_LIMIT_WINDOW_MS: Joi.number().default(900000),
|
||||
RATE_LIMIT_MAX: Joi.number().default(100),
|
||||
AUTH_RATE_LIMIT_MAX: Joi.number().default(10),
|
||||
|
||||
LOG_LEVEL: Joi.string().default('info'),
|
||||
UPLOAD_DIR: Joi.string().default('uploads'),
|
||||
MAX_FILE_SIZE_MB: Joi.number().default(5),
|
||||
|
||||
MAX_LOGIN_ATTEMPTS: Joi.number().default(5),
|
||||
LOCKOUT_DURATION_MINUTES: Joi.number().default(30),
|
||||
}).unknown();
|
||||
|
||||
const { error, value: env } = envSchema.validate(process.env);
|
||||
if (error) {
|
||||
throw new Error(`Environment validation error: ${error.message}`);
|
||||
}
|
||||
|
||||
module.exports = env;
|
||||
53
src/config/logger.js
Normal file
53
src/config/logger.js
Normal file
@ -0,0 +1,53 @@
|
||||
const winston = require('winston');
|
||||
require('winston-daily-rotate-file');
|
||||
const path = require('path');
|
||||
const env = require('./env');
|
||||
|
||||
const { combine, timestamp, printf, errors, json } = winston.format;
|
||||
const SENSITIVE_KEYS = ['password', 'token', 'authorization', 'refresh_token', 'access_token'];
|
||||
|
||||
const redact = winston.format((info) => {
|
||||
const scrub = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
const out = Array.isArray(obj) ? [] : {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
out[k] = SENSITIVE_KEYS.includes(k.toLowerCase()) ? '[REDACTED]' : scrub(v);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
return scrub(info);
|
||||
});
|
||||
|
||||
const devFormat = combine(
|
||||
redact(),
|
||||
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
errors({ stack: true }),
|
||||
printf(({ level, message, timestamp: ts, stack, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
|
||||
return `${ts} [${level}]: ${stack || message} ${metaStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
const prodFormat = combine(redact(), timestamp(), errors({ stack: true }), json());
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.LOG_LEVEL,
|
||||
format: env.NODE_ENV === 'production' ? prodFormat : devFormat,
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: path.join('logs', 'error-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
level: 'error',
|
||||
maxFiles: '30d',
|
||||
}),
|
||||
new winston.transports.DailyRotateFile({
|
||||
filename: path.join('logs', 'combined-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: '30d',
|
||||
}),
|
||||
],
|
||||
exitOnError: false,
|
||||
});
|
||||
|
||||
module.exports = logger;
|
||||
12
src/config/morgan.js
Normal file
12
src/config/morgan.js
Normal file
@ -0,0 +1,12 @@
|
||||
const morgan = require('morgan');
|
||||
const logger = require('./logger');
|
||||
|
||||
morgan.token('id', (req) => req.id);
|
||||
morgan.token('user', (req) => (req.user ? req.user.id : 'anonymous'));
|
||||
|
||||
const format =
|
||||
':id :remote-addr :method :url :status :res[content-length]B - :response-time ms user=:user';
|
||||
|
||||
module.exports = morgan(format, {
|
||||
stream: { write: (message) => logger.http(message.trim()) },
|
||||
});
|
||||
21
src/config/prisma.js
Normal file
21
src/config/prisma.js
Normal file
@ -0,0 +1,21 @@
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const logger = require('./logger');
|
||||
const env = require('./env');
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
log: [
|
||||
{ emit: 'event', level: 'error' },
|
||||
{ emit: 'event', level: 'warn' },
|
||||
...(env.NODE_ENV === 'development' ? [{ emit: 'event', level: 'query' }] : []),
|
||||
],
|
||||
});
|
||||
|
||||
prisma.$on('error', (e) => logger.error('Prisma error', { error: e.message }));
|
||||
prisma.$on('warn', (e) => logger.warn('Prisma warning', { warning: e.message }));
|
||||
if (env.NODE_ENV === 'development') {
|
||||
prisma.$on('query', (e) =>
|
||||
logger.debug('Prisma query', { query: e.query, duration: e.duration })
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = prisma;
|
||||
45
src/config/swagger.js
Normal file
45
src/config/swagger.js
Normal file
@ -0,0 +1,45 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('yaml');
|
||||
|
||||
const definition = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'ERP API',
|
||||
version: '1.0.0',
|
||||
description: 'Phase 1 - PO, GRN, Vendor, Assets, Masters, Users & RBAC',
|
||||
},
|
||||
servers: [{ url: '/api/v1' }],
|
||||
components: {
|
||||
securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } },
|
||||
schemas: {},
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
tags: [],
|
||||
paths: {},
|
||||
};
|
||||
|
||||
const docsDir = path.join(__dirname, '../docs');
|
||||
|
||||
if (fs.existsSync(docsDir)) {
|
||||
for (const file of fs.readdirSync(docsDir)) {
|
||||
if (!/\.ya?ml$/i.test(file)) continue;
|
||||
|
||||
const doc = yaml.parse(fs.readFileSync(path.join(docsDir, file), 'utf8'));
|
||||
if (!doc) continue;
|
||||
|
||||
if (doc.tags?.length) {
|
||||
definition.tags.push(...doc.tags);
|
||||
}
|
||||
|
||||
if (doc.components?.schemas) {
|
||||
Object.assign(definition.components.schemas, doc.components.schemas);
|
||||
}
|
||||
|
||||
if (doc.paths) {
|
||||
Object.assign(definition.paths, doc.paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = definition;
|
||||
1641
src/docs/completed-routes.yaml
Normal file
1641
src/docs/completed-routes.yaml
Normal file
File diff suppressed because it is too large
Load Diff
39
src/middlewares/auth.middleware.js
Normal file
39
src/middlewares/auth.middleware.js
Normal file
@ -0,0 +1,39 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const ApiError = require('../utils/ApiError');
|
||||
const env = require('../config/env');
|
||||
const prisma = require('../config/prisma');
|
||||
|
||||
module.exports = async (req, _res, next) => {
|
||||
try {
|
||||
const header = req.headers.authorization;
|
||||
if (!header || !header.startsWith('Bearer ')) {
|
||||
throw new ApiError(401, 'Authentication token missing');
|
||||
}
|
||||
|
||||
const token = header.split(' ')[1];
|
||||
const payload = jwt.verify(token, env.JWT_ACCESS_SECRET);
|
||||
|
||||
const user = await prisma.users.findFirst({
|
||||
where: { id: BigInt(payload.sub), deleted_at: null },
|
||||
include: {
|
||||
roles_users_role_idToroles: {
|
||||
include: {
|
||||
role_permissions: { include: { permissions: { include: { modules: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.is_active || user.status !== 'active') {
|
||||
throw new ApiError(401, 'Invalid or inactive user');
|
||||
}
|
||||
|
||||
user.role = user.roles_users_role_idToroles;
|
||||
req.user = user;
|
||||
return next();
|
||||
} catch (err) {
|
||||
if (err.name === 'TokenExpiredError') return next(new ApiError(401, 'Access token expired'));
|
||||
if (err.name === 'JsonWebTokenError') return next(new ApiError(401, 'Invalid access token'));
|
||||
return next(err instanceof ApiError ? err : new ApiError(401, 'Unauthorized'));
|
||||
}
|
||||
};
|
||||
31
src/middlewares/error.middleware.js
Normal file
31
src/middlewares/error.middleware.js
Normal file
@ -0,0 +1,31 @@
|
||||
const ApiError = require('../utils/ApiError');
|
||||
const logger = require('../config/logger');
|
||||
const env = require('../config/env');
|
||||
|
||||
module.exports = (err, req, res, _next) => {
|
||||
let statusCode = err.statusCode || 500;
|
||||
let message = err.message || 'Internal server error';
|
||||
let errors = err.errors || [];
|
||||
|
||||
if (!(err instanceof ApiError)) {
|
||||
statusCode = 500;
|
||||
message = env.NODE_ENV === 'production' ? 'Internal server error' : err.message;
|
||||
errors = [];
|
||||
}
|
||||
|
||||
logger.error(message, {
|
||||
requestId: req.id,
|
||||
statusCode,
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
user: req.user ? req.user.id.toString() : undefined,
|
||||
stack: err.stack,
|
||||
});
|
||||
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
errors,
|
||||
...(env.NODE_ENV === 'development' && { stack: err.stack }),
|
||||
});
|
||||
};
|
||||
21
src/middlewares/rateLimiter.middleware.js
Normal file
21
src/middlewares/rateLimiter.middleware.js
Normal file
@ -0,0 +1,21 @@
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const env = require('../config/env');
|
||||
|
||||
const generalLimiter = rateLimit({
|
||||
windowMs: env.RATE_LIMIT_WINDOW_MS,
|
||||
max: env.RATE_LIMIT_MAX,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { success: false, message: 'Too many requests, please try again later' },
|
||||
});
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: env.AUTH_RATE_LIMIT_MAX,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true,
|
||||
message: { success: false, message: 'Too many login attempts, please try again later' },
|
||||
});
|
||||
|
||||
module.exports = { generalLimiter, authLimiter };
|
||||
13
src/middlewares/rbac.middleware.js
Normal file
13
src/middlewares/rbac.middleware.js
Normal file
@ -0,0 +1,13 @@
|
||||
const ApiError = require('../utils/ApiError');
|
||||
|
||||
const authorize = (moduleCode, action) => (req, _res, next) => {
|
||||
const permissions = req.user?.role?.role_permissions || [];
|
||||
const allowed = permissions.some(
|
||||
(rp) => rp.permissions.modules.code === moduleCode && rp.permissions.action === action
|
||||
);
|
||||
|
||||
if (!allowed) return next(new ApiError(403, `Forbidden: requires ${moduleCode}:${action}`));
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = authorize;
|
||||
7
src/middlewares/requestId.middleware.js
Normal file
7
src/middlewares/requestId.middleware.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
req.id = req.headers['x-request-id'] || uuidv4();
|
||||
res.setHeader('X-Request-Id', req.id);
|
||||
next();
|
||||
};
|
||||
28
src/middlewares/upload.middleware.js
Normal file
28
src/middlewares/upload.middleware.js
Normal file
@ -0,0 +1,28 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const ApiError = require('../utils/ApiError');
|
||||
const env = require('../config/env');
|
||||
|
||||
const ALLOWED_MIME = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (_req, _file, cb) => cb(null, env.UPLOAD_DIR),
|
||||
filename: (_req, file, cb) => {
|
||||
const unique = crypto.randomBytes(16).toString('hex');
|
||||
cb(null, `${unique}${path.extname(file.originalname).toLowerCase()}`);
|
||||
},
|
||||
});
|
||||
|
||||
const fileFilter = (_req, file, cb) => {
|
||||
if (!ALLOWED_MIME.includes(file.mimetype)) {
|
||||
return cb(new ApiError(400, `Unsupported file type: ${file.mimetype}`), false);
|
||||
}
|
||||
return cb(null, true);
|
||||
};
|
||||
|
||||
module.exports = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: { fileSize: env.MAX_FILE_SIZE_MB * 1024 * 1024 },
|
||||
});
|
||||
20
src/middlewares/validate.middleware.js
Normal file
20
src/middlewares/validate.middleware.js
Normal file
@ -0,0 +1,20 @@
|
||||
const ApiError = require('../utils/ApiError');
|
||||
|
||||
const validate =
|
||||
(schema, source = 'body') =>
|
||||
(req, _res, next) => {
|
||||
const { error, value } = schema.validate(req[source], {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const errors = error.details.map((d) => ({ field: d.path.join('.'), message: d.message }));
|
||||
return next(new ApiError(422, 'Validation failed', errors));
|
||||
}
|
||||
|
||||
req[source] = value;
|
||||
return next();
|
||||
};
|
||||
|
||||
module.exports = validate;
|
||||
20
src/modules/auth/auth.controller.js
Normal file
20
src/modules/auth/auth.controller.js
Normal file
@ -0,0 +1,20 @@
|
||||
const asyncHandler = require('../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../utils/ApiResponse');
|
||||
const authService = require('./auth.service');
|
||||
|
||||
const login = asyncHandler(async (req, res) => {
|
||||
const tokens = await authService.login(req.body.email, req.body.password);
|
||||
res.json(new ApiResponse(200, tokens, 'Login successful'));
|
||||
});
|
||||
|
||||
const refresh = asyncHandler(async (req, res) => {
|
||||
const tokens = await authService.refresh(req.body.refresh_token);
|
||||
res.json(new ApiResponse(200, tokens, 'Token refreshed'));
|
||||
});
|
||||
|
||||
const logout = asyncHandler(async (req, res) => {
|
||||
await authService.logout(req.body.refresh_token);
|
||||
res.json(new ApiResponse(200, null, 'Logged out'));
|
||||
});
|
||||
|
||||
module.exports = { login, refresh, logout };
|
||||
13
src/modules/auth/auth.routes.js
Normal file
13
src/modules/auth/auth.routes.js
Normal file
@ -0,0 +1,13 @@
|
||||
const express = require('express');
|
||||
const validate = require('../../middlewares/validate.middleware');
|
||||
const { authLimiter } = require('../../middlewares/rateLimiter.middleware');
|
||||
const { loginSchema, refreshSchema } = require('./auth.validation');
|
||||
const controller = require('./auth.controller');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login', authLimiter, validate(loginSchema), controller.login);
|
||||
router.post('/refresh', validate(refreshSchema), controller.refresh);
|
||||
router.post('/logout', validate(refreshSchema), controller.logout);
|
||||
|
||||
module.exports = router;
|
||||
94
src/modules/auth/auth.service.js
Normal file
94
src/modules/auth/auth.service.js
Normal file
@ -0,0 +1,94 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const prisma = require('../../config/prisma');
|
||||
const ApiError = require('../../utils/ApiError');
|
||||
const env = require('../../config/env');
|
||||
|
||||
const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex');
|
||||
|
||||
const issueTokens = async (user) => {
|
||||
const accessToken = jwt.sign(
|
||||
{ sub: user.id.toString(), role_id: user.role_id?.toString() },
|
||||
env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: env.JWT_ACCESS_EXPIRY }
|
||||
);
|
||||
|
||||
const refreshToken = crypto.randomBytes(40).toString('hex');
|
||||
await prisma.refresh_tokens.create({
|
||||
data: {
|
||||
user_id: user.id,
|
||||
token_hash: hashToken(refreshToken),
|
||||
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
};
|
||||
|
||||
const login = async (email, password) => {
|
||||
const user = await prisma.users.findFirst({ where: { email, deleted_at: null } });
|
||||
if (!user) throw new ApiError(401, 'Invalid email or password');
|
||||
|
||||
if (user.locked_until && user.locked_until > new Date()) {
|
||||
throw new ApiError(423, 'Account locked due to too many failed attempts. Try again later.');
|
||||
}
|
||||
|
||||
const match = await bcrypt.compare(password, user.password_hash);
|
||||
if (!match) {
|
||||
const attempts = user.failed_login_attempts + 1;
|
||||
const lockout = attempts >= env.MAX_LOGIN_ATTEMPTS;
|
||||
await prisma.users.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
failed_login_attempts: lockout ? 0 : attempts,
|
||||
locked_until: lockout
|
||||
? new Date(Date.now() + env.LOCKOUT_DURATION_MINUTES * 60 * 1000)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
throw new ApiError(401, 'Invalid email or password');
|
||||
}
|
||||
|
||||
if (user.status !== 'active' || !user.is_active) {
|
||||
throw new ApiError(403, 'Account is inactive. Contact administrator.');
|
||||
}
|
||||
|
||||
await prisma.users.update({
|
||||
where: { id: user.id },
|
||||
data: { failed_login_attempts: 0, locked_until: null, last_login_at: new Date() },
|
||||
});
|
||||
|
||||
return issueTokens(user);
|
||||
};
|
||||
|
||||
const refresh = async (refreshToken) => {
|
||||
const tokenHash = hashToken(refreshToken);
|
||||
const record = await prisma.refresh_tokens.findFirst({
|
||||
where: { token_hash: tokenHash, revoked_at: null },
|
||||
});
|
||||
|
||||
if (!record || record.expires_at < new Date()) {
|
||||
throw new ApiError(401, 'Invalid or expired refresh token');
|
||||
}
|
||||
|
||||
await prisma.refresh_tokens.update({
|
||||
where: { id: record.id },
|
||||
data: { revoked_at: new Date() },
|
||||
});
|
||||
|
||||
const user = await prisma.users.findUnique({ where: { id: record.user_id } });
|
||||
if (!user || !user.is_active) throw new ApiError(401, 'User not found or inactive');
|
||||
|
||||
return issueTokens(user);
|
||||
};
|
||||
|
||||
const logout = async (refreshToken) => {
|
||||
const tokenHash = hashToken(refreshToken);
|
||||
await prisma.refresh_tokens.updateMany({
|
||||
where: { token_hash: tokenHash, revoked_at: null },
|
||||
data: { revoked_at: new Date() },
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { login, refresh, logout };
|
||||
12
src/modules/auth/auth.validation.js
Normal file
12
src/modules/auth/auth.validation.js
Normal file
@ -0,0 +1,12 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(8).required(),
|
||||
});
|
||||
|
||||
const refreshSchema = Joi.object({
|
||||
refresh_token: Joi.string().required(),
|
||||
});
|
||||
|
||||
module.exports = { loginSchema, refreshSchema };
|
||||
148
src/modules/masters/_shared/master.factory.js
Normal file
148
src/modules/masters/_shared/master.factory.js
Normal file
@ -0,0 +1,148 @@
|
||||
const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
|
||||
const normalizePayload = (payload, fields) => {
|
||||
const out = { ...payload };
|
||||
for (const f of fields) {
|
||||
if (Object.prototype.hasOwnProperty.call(out, f.name)) {
|
||||
if (f.type === 'string' && out[f.name] !== null && out[f.name] !== undefined) {
|
||||
out[f.name] = String(out[f.name]).trim();
|
||||
if (f.uppercase) out[f.name] = out[f.name].toUpperCase();
|
||||
}
|
||||
if (f.type === 'int' && out[f.name] !== null && out[f.name] !== undefined) {
|
||||
out[f.name] = Number(out[f.name]);
|
||||
}
|
||||
if (f.type === 'decimal' && out[f.name] !== null && out[f.name] !== undefined) {
|
||||
out[f.name] = Number(out[f.name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const buildMasterService = ({
|
||||
modelName,
|
||||
tableName,
|
||||
fields,
|
||||
uniqueField = null,
|
||||
softDelete = true,
|
||||
}) => {
|
||||
const model = prisma[modelName];
|
||||
|
||||
const createOne = async (payload, userId, requestId) => {
|
||||
const data = normalizePayload(payload, fields);
|
||||
|
||||
if (uniqueField && data[uniqueField] !== undefined) {
|
||||
const existing = await model.findFirst({
|
||||
where: {
|
||||
[uniqueField]: data[uniqueField],
|
||||
...(softDelete ? { deleted_at: null } : {}),
|
||||
},
|
||||
});
|
||||
if (existing) throw new ApiError(409, `${tableName} ${uniqueField} already exists`);
|
||||
}
|
||||
|
||||
if (fields.some((f) => f.name === 'created_by'))
|
||||
data.created_by = userId ? BigInt(userId) : null;
|
||||
if (fields.some((f) => f.name === 'updated_by'))
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const created = await model.create({ data });
|
||||
await auditLog({
|
||||
tableName,
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: created,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
return created;
|
||||
};
|
||||
|
||||
const list = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const searchFields = fields.filter((f) => f.searchable).map((f) => f.name);
|
||||
|
||||
const where = {
|
||||
...(softDelete ? { deleted_at: null } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search && searchFields.length
|
||||
? {
|
||||
OR: searchFields.map((name) => ({
|
||||
[name]: { contains: query.search, mode: 'insensitive' },
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }),
|
||||
model.count({ where }),
|
||||
]);
|
||||
|
||||
return { data, meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const getOne = async (id) => {
|
||||
const one = await model.findFirst({
|
||||
where: { id: BigInt(id), ...(softDelete ? { deleted_at: null } : {}) },
|
||||
});
|
||||
if (!one) throw new ApiError(404, `${tableName} not found`);
|
||||
return one;
|
||||
};
|
||||
|
||||
const updateOne = async (id, payload, userId, requestId) => {
|
||||
const existing = await getOne(id);
|
||||
const data = normalizePayload(payload, fields);
|
||||
if (fields.some((f) => f.name === 'updated_by'))
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const updated = await model.update({ where: { id: BigInt(id) }, data });
|
||||
|
||||
await auditLog({
|
||||
tableName,
|
||||
recordId: id,
|
||||
action: 'UPDATE',
|
||||
oldValue: existing,
|
||||
newValue: updated,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
const removeOne = async (id, userId, requestId) => {
|
||||
const existing = await getOne(id);
|
||||
if (softDelete) {
|
||||
await model.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: {
|
||||
deleted_at: new Date(),
|
||||
...(fields.some((f) => f.name === 'updated_by')
|
||||
? { updated_by: userId ? BigInt(userId) : null }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await model.delete({ where: { id: BigInt(id) } });
|
||||
}
|
||||
|
||||
await auditLog({
|
||||
tableName,
|
||||
recordId: id,
|
||||
action: 'DELETE',
|
||||
oldValue: existing,
|
||||
newValue: softDelete ? { deleted_at: new Date() } : null,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
return { createOne, list, getOne, updateOne, removeOne };
|
||||
};
|
||||
|
||||
module.exports = { buildMasterService };
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./asset-categories.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createAssetCategories(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'asset_categories created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listAssetCategories(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'asset_categories list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getAssetCategoriesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'asset_categories fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateAssetCategories(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'asset_categories updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteAssetCategories(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'asset_categories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./asset-categories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./asset-categories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,68 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'asset_categories',
|
||||
tableName: 'asset_categories',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'code_prefix',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'default_useful_life_years',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'default_depreciation_method',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createAssetCategories: service.createOne,
|
||||
listAssetCategories: service.list,
|
||||
getAssetCategoriesById: service.getOne,
|
||||
updateAssetCategories: service.updateOne,
|
||||
deleteAssetCategories: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code_prefix: Joi.string().allow('').optional(),
|
||||
default_useful_life_years: Joi.number().integer().optional(),
|
||||
default_depreciation_method: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/brands/brands.controller.js
Normal file
30
src/modules/masters/brands/brands.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./brands.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createBrands(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'brands created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listBrands(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'brands list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getBrandsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'brands fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateBrands(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'brands updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteBrands(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'brands deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/brands/brands.routes.js
Normal file
17
src/modules/masters/brands/brands.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./brands.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./brands.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
74
src/modules/masters/brands/brands.service.js
Normal file
74
src/modules/masters/brands/brands.service.js
Normal file
@ -0,0 +1,74 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'brands',
|
||||
tableName: 'brands',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'brand_type',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'contact_person',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createBrands: service.createOne,
|
||||
listBrands: service.list,
|
||||
getBrandsById: service.getOne,
|
||||
updateBrands: service.updateOne,
|
||||
deleteBrands: service.removeOne,
|
||||
};
|
||||
22
src/modules/masters/brands/brands.validation.js
Normal file
22
src/modules/masters/brands/brands.validation.js
Normal file
@ -0,0 +1,22 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
brand_type: Joi.string().allow('').optional(),
|
||||
contact_person: Joi.string().allow('').optional(),
|
||||
phone: Joi.string().allow('').optional(),
|
||||
email: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./delivery-terms.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createDeliveryTerms(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'delivery_terms created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listDeliveryTerms(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'delivery_terms list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getDeliveryTermsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'delivery_terms fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateDeliveryTerms(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'delivery_terms updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteDeliveryTerms(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'delivery_terms deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/delivery-terms/delivery-terms.routes.js
Normal file
17
src/modules/masters/delivery-terms/delivery-terms.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./delivery-terms.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./delivery-terms.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
56
src/modules/masters/delivery-terms/delivery-terms.service.js
Normal file
56
src/modules/masters/delivery-terms/delivery-terms.service.js
Normal file
@ -0,0 +1,56 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'delivery_terms',
|
||||
tableName: 'delivery_terms',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createDeliveryTerms: service.createOne,
|
||||
listDeliveryTerms: service.list,
|
||||
getDeliveryTermsById: service.getOne,
|
||||
updateDeliveryTerms: service.updateOne,
|
||||
deleteDeliveryTerms: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/departments/departments.controller.js
Normal file
30
src/modules/masters/departments/departments.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./departments.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createDepartments(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'departments created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listDepartments(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'departments list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getDepartmentsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'departments fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateDepartments(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'departments updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteDepartments(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'departments deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/departments/departments.routes.js
Normal file
17
src/modules/masters/departments/departments.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./departments.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./departments.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
44
src/modules/masters/departments/departments.service.js
Normal file
44
src/modules/masters/departments/departments.service.js
Normal file
@ -0,0 +1,44 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'departments',
|
||||
tableName: 'departments',
|
||||
uniqueField: 'name',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createDepartments: service.createOne,
|
||||
listDepartments: service.list,
|
||||
getDepartmentsById: service.getOne,
|
||||
updateDepartments: service.updateOne,
|
||||
deleteDepartments: service.removeOne,
|
||||
};
|
||||
17
src/modules/masters/departments/departments.validation.js
Normal file
17
src/modules/masters/departments/departments.validation.js
Normal file
@ -0,0 +1,17 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/designations/designations.controller.js
Normal file
30
src/modules/masters/designations/designations.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./designations.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createDesignations(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'designations created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listDesignations(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'designations list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getDesignationsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'designations fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateDesignations(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'designations updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteDesignations(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'designations deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/designations/designations.routes.js
Normal file
17
src/modules/masters/designations/designations.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./designations.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./designations.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
44
src/modules/masters/designations/designations.service.js
Normal file
44
src/modules/masters/designations/designations.service.js
Normal file
@ -0,0 +1,44 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'designations',
|
||||
tableName: 'designations',
|
||||
uniqueField: 'name',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createDesignations: service.createOne,
|
||||
listDesignations: service.list,
|
||||
getDesignationsById: service.getOne,
|
||||
updateDesignations: service.updateOne,
|
||||
deleteDesignations: service.removeOne,
|
||||
};
|
||||
17
src/modules/masters/designations/designations.validation.js
Normal file
17
src/modules/masters/designations/designations.validation.js
Normal file
@ -0,0 +1,17 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./document-series.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createDocumentSeries(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'document_series created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listDocumentSeries(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'document_series list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getDocumentSeriesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'document_series fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateDocumentSeries(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'document_series updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteDocumentSeries(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'document_series deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./document-series.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./document-series.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,68 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'document_series',
|
||||
tableName: 'document_series',
|
||||
uniqueField: 'code',
|
||||
softDelete: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'prefix',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'current_number',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'padding',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createDocumentSeries: service.createOne,
|
||||
listDocumentSeries: service.list,
|
||||
getDocumentSeriesById: service.getOne,
|
||||
updateDocumentSeries: service.updateOne,
|
||||
deleteDocumentSeries: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
prefix: Joi.string().allow('').optional(),
|
||||
current_number: Joi.number().integer().optional(),
|
||||
padding: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/gst-rates/gst-rates.controller.js
Normal file
30
src/modules/masters/gst-rates/gst-rates.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./gst-rates.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createGstRates(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'gst_rates created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listGstRates(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'gst_rates list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getGstRatesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'gst_rates fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateGstRates(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'gst_rates updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteGstRates(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'gst_rates deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/gst-rates/gst-rates.routes.js
Normal file
17
src/modules/masters/gst-rates/gst-rates.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./gst-rates.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./gst-rates.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
38
src/modules/masters/gst-rates/gst-rates.service.js
Normal file
38
src/modules/masters/gst-rates/gst-rates.service.js
Normal file
@ -0,0 +1,38 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'gst_rates',
|
||||
tableName: 'gst_rates',
|
||||
uniqueField: 'rate_pct',
|
||||
softDelete: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'rate_pct',
|
||||
type: 'decimal',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createGstRates: service.createOne,
|
||||
listGstRates: service.list,
|
||||
getGstRatesById: service.getOne,
|
||||
updateGstRates: service.updateOne,
|
||||
deleteGstRates: service.removeOne,
|
||||
};
|
||||
18
src/modules/masters/gst-rates/gst-rates.validation.js
Normal file
18
src/modules/masters/gst-rates/gst-rates.validation.js
Normal file
@ -0,0 +1,18 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
rate_pct: Joi.number().precision(2).optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
20
src/modules/masters/index.js
Normal file
20
src/modules/masters/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/uom', require('./uom/uom.routes'));
|
||||
router.use('/item-categories', require('./item-categories/item-categories.routes'));
|
||||
router.use('/brands', require('./brands/brands.routes'));
|
||||
router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
||||
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
||||
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
||||
router.use('/asset-categories', require('./asset-categories/asset-categories.routes'));
|
||||
router.use('/departments', require('./departments/departments.routes'));
|
||||
router.use('/designations', require('./designations/designations.routes'));
|
||||
router.use('/document-series', require('./document-series/document-series.routes'));
|
||||
router.use('/item-subcategories', require('./item-subcategories/item-subcategories.routes'));
|
||||
router.use('/items', require('./items/items.routes'));
|
||||
router.use('/warehouses', require('./warehouses/warehouses.routes'));
|
||||
router.use('/plants', require('./plants/plants.routes'));
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./item-categories.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createItemCategories(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'item_categories created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listItemCategories(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'item_categories list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getItemCategoriesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'item_categories fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateItemCategories(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'item_categories updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteItemCategories(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'item_categories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./item-categories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./item-categories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,50 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'item_categories',
|
||||
tableName: 'item_categories',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createItemCategories: service.createOne,
|
||||
listItemCategories: service.list,
|
||||
getItemCategoriesById: service.getOne,
|
||||
updateItemCategories: service.updateOne,
|
||||
deleteItemCategories: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./item-subcategories.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createItemSubcategories(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'item_subcategories created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listItemSubcategories(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'item_subcategories list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getItemSubcategoriesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'item_subcategories fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateItemSubcategories(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'item_subcategories updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteItemSubcategories(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'item_subcategories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./item-subcategories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./item-subcategories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,56 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'item_subcategories',
|
||||
tableName: 'item_subcategories',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'item_category_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createItemSubcategories: service.createOne,
|
||||
listItemSubcategories: service.list,
|
||||
getItemSubcategoriesById: service.getOne,
|
||||
updateItemSubcategories: service.updateOne,
|
||||
deleteItemSubcategories: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/items/items.controller.js
Normal file
30
src/modules/masters/items/items.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./items.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createItems(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'items created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listItems(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'items list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getItemsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'items fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateItems(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'items updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteItems(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'items deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/items/items.routes.js
Normal file
17
src/modules/masters/items/items.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./items.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./items.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
116
src/modules/masters/items/items.service.js
Normal file
116
src/modules/masters/items/items.service.js
Normal file
@ -0,0 +1,116 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'items',
|
||||
tableName: 'items',
|
||||
uniqueField: 'item_code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'item_code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'item_name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'item_category_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'item_subcategory_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'uom_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'hsn_code_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'gst_rate_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'brand_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_asset_item',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'specification',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'min_order_qty',
|
||||
type: 'decimal',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'reorder_level',
|
||||
type: 'decimal',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createItems: service.createOne,
|
||||
listItems: service.list,
|
||||
getItemsById: service.getOne,
|
||||
updateItems: service.updateOne,
|
||||
deleteItems: service.removeOne,
|
||||
};
|
||||
29
src/modules/masters/items/items.validation.js
Normal file
29
src/modules/masters/items/items.validation.js
Normal file
@ -0,0 +1,29 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_code: Joi.string().allow('').optional(),
|
||||
item_name: Joi.string().allow('').optional(),
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
item_subcategory_id: Joi.number().integer().optional(),
|
||||
uom_id: Joi.number().integer().optional(),
|
||||
hsn_code_id: Joi.number().integer().optional(),
|
||||
gst_rate_id: Joi.number().integer().optional(),
|
||||
brand_id: Joi.number().integer().optional(),
|
||||
is_asset_item: Joi.boolean().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
specification: Joi.string().allow('').optional(),
|
||||
min_order_qty: Joi.number().precision(4).optional(),
|
||||
reorder_level: Joi.number().precision(4).optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./payment-terms.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createPaymentTerms(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'payment_terms created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listPaymentTerms(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'payment_terms list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getPaymentTermsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'payment_terms fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updatePaymentTerms(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'payment_terms updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deletePaymentTerms(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'payment_terms deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/payment-terms/payment-terms.routes.js
Normal file
17
src/modules/masters/payment-terms/payment-terms.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./payment-terms.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./payment-terms.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
62
src/modules/masters/payment-terms/payment-terms.service.js
Normal file
62
src/modules/masters/payment-terms/payment-terms.service.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'payment_terms',
|
||||
tableName: 'payment_terms',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'credit_days',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createPaymentTerms: service.createOne,
|
||||
listPaymentTerms: service.list,
|
||||
getPaymentTermsById: service.getOne,
|
||||
updatePaymentTerms: service.updateOne,
|
||||
deletePaymentTerms: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
credit_days: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/plants/plants.controller.js
Normal file
30
src/modules/masters/plants/plants.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./plants.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createPlants(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'plants created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listPlants(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'plants list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getPlantsById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'plants fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updatePlants(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'plants updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deletePlants(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'plants deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/plants/plants.routes.js
Normal file
17
src/modules/masters/plants/plants.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./plants.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./plants.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
86
src/modules/masters/plants/plants.service.js
Normal file
86
src/modules/masters/plants/plants.service.js
Normal file
@ -0,0 +1,86 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'plants',
|
||||
tableName: 'plants',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'gstin',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'pincode',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createPlants: service.createOne,
|
||||
listPlants: service.list,
|
||||
getPlantsById: service.getOne,
|
||||
updatePlants: service.updateOne,
|
||||
deletePlants: service.removeOne,
|
||||
};
|
||||
24
src/modules/masters/plants/plants.validation.js
Normal file
24
src/modules/masters/plants/plants.validation.js
Normal file
@ -0,0 +1,24 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
gstin: Joi.string().allow('').optional(),
|
||||
address: Joi.string().allow('').optional(),
|
||||
city: Joi.string().allow('').optional(),
|
||||
state: Joi.string().allow('').optional(),
|
||||
pincode: Joi.string().allow('').optional(),
|
||||
phone: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
30
src/modules/masters/uom/uom.controller.js
Normal file
30
src/modules/masters/uom/uom.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./uom.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createUom(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'UOM created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listUom(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'UOM list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getUomById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'UOM fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateUom(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'UOM updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteUom(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'UOM deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
23
src/modules/masters/uom/uom.routes.js
Normal file
23
src/modules/masters/uom/uom.routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./uom.controller');
|
||||
const { createUomSchema, updateUomSchema, listUomQuerySchema } = require('./uom.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
authorize('MASTERS', 'view'),
|
||||
validate(listUomQuerySchema, 'query'),
|
||||
controller.list
|
||||
);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createUomSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateUomSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
104
src/modules/masters/uom/uom.service.js
Normal file
104
src/modules/masters/uom/uom.service.js
Normal file
@ -0,0 +1,104 @@
|
||||
const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const auditLog = require('../../../utils/auditLog');
|
||||
const { getPagination } = require('../../../utils/pagination');
|
||||
|
||||
const createUom = async (payload, userId, requestId) => {
|
||||
const exists = await prisma.uom.findFirst({
|
||||
where: { code: payload.code, deleted_at: null },
|
||||
});
|
||||
if (exists) throw new ApiError(409, 'UOM code already exists');
|
||||
|
||||
const data = {
|
||||
...payload,
|
||||
code: payload.code.toUpperCase().trim(),
|
||||
name: payload.name.trim(),
|
||||
created_by: userId ? BigInt(userId) : null,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
|
||||
const created = await prisma.uom.create({ data });
|
||||
await auditLog({
|
||||
tableName: 'uom',
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: created,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
return created;
|
||||
};
|
||||
|
||||
const listUom = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.uom.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }),
|
||||
prisma.uom.count({ where }),
|
||||
]);
|
||||
|
||||
return { data, meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const getUomById = async (id) => {
|
||||
const uom = await prisma.uom.findFirst({ where: { id: BigInt(id), deleted_at: null } });
|
||||
if (!uom) throw new ApiError(404, 'UOM not found');
|
||||
return uom;
|
||||
};
|
||||
|
||||
const updateUom = async (id, payload, userId, requestId) => {
|
||||
const existing = await getUomById(id);
|
||||
const updated = await prisma.uom.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: {
|
||||
...payload,
|
||||
...(payload.code ? { code: payload.code.toUpperCase().trim() } : {}),
|
||||
...(payload.name ? { name: payload.name.trim() } : {}),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'uom',
|
||||
recordId: id,
|
||||
action: 'UPDATE',
|
||||
oldValue: existing,
|
||||
newValue: updated,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
return updated;
|
||||
};
|
||||
|
||||
const deleteUom = async (id, userId, requestId) => {
|
||||
const existing = await getUomById(id);
|
||||
await prisma.uom.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'uom',
|
||||
recordId: id,
|
||||
action: 'DELETE',
|
||||
oldValue: existing,
|
||||
newValue: { deleted_at: new Date() },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { createUom, listUom, getUomById, updateUom, deleteUom };
|
||||
22
src/modules/masters/uom/uom.validation.js
Normal file
22
src/modules/masters/uom/uom.validation.js
Normal file
@ -0,0 +1,22 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createUomSchema = Joi.object({
|
||||
code: Joi.string().max(20).required(),
|
||||
name: Joi.string().max(100).required(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateUomSchema = Joi.object({
|
||||
code: Joi.string().max(20).optional(),
|
||||
name: Joi.string().max(100).optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const listUomQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema };
|
||||
30
src/modules/masters/warehouses/warehouses.controller.js
Normal file
30
src/modules/masters/warehouses/warehouses.controller.js
Normal file
@ -0,0 +1,30 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./warehouses.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createWarehouses(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'warehouses created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listWarehouses(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'warehouses list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getWarehousesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'warehouses fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateWarehouses(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'warehouses updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteWarehouses(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'warehouses deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
17
src/modules/masters/warehouses/warehouses.routes.js
Normal file
17
src/modules/masters/warehouses/warehouses.routes.js
Normal file
@ -0,0 +1,17 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../../middlewares/auth.middleware');
|
||||
const authorize = require('../../../middlewares/rbac.middleware');
|
||||
const validate = require('../../../middlewares/validate.middleware');
|
||||
const controller = require('./warehouses.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./warehouses.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
62
src/modules/masters/warehouses/warehouses.service.js
Normal file
62
src/modules/masters/warehouses/warehouses.service.js
Normal file
@ -0,0 +1,62 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'warehouses',
|
||||
tableName: 'warehouses',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'plant_id',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'location',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
module.exports = {
|
||||
createWarehouses: service.createOne,
|
||||
listWarehouses: service.list,
|
||||
getWarehousesById: service.getOne,
|
||||
updateWarehouses: service.updateOne,
|
||||
deleteWarehouses: service.removeOne,
|
||||
};
|
||||
20
src/modules/masters/warehouses/warehouses.validation.js
Normal file
20
src/modules/masters/warehouses/warehouses.validation.js
Normal file
@ -0,0 +1,20 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
plant_id: Joi.number().integer().optional(),
|
||||
location: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
76
src/modules/roles/roles.controller.js
Normal file
76
src/modules/roles/roles.controller.js
Normal file
@ -0,0 +1,76 @@
|
||||
const asyncHandler = require('../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../utils/ApiResponse');
|
||||
const service = require('./roles.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createRole(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'Role created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listRoles(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'Roles list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getRoleById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'Role fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateRole(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'Role updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteRole(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'Role deleted successfully'));
|
||||
});
|
||||
|
||||
const listPermissions = asyncHandler(async (_req, res) => {
|
||||
const data = await service.listPermissionCatalog();
|
||||
res.json(new ApiResponse(200, data, 'Permission catalog fetched'));
|
||||
});
|
||||
|
||||
const assignPermissions = asyncHandler(async (req, res) => {
|
||||
const data = await service.assignPermissions(
|
||||
req.params.id,
|
||||
req.body.permission_ids,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, data, 'Role permissions updated successfully'));
|
||||
});
|
||||
|
||||
const listCards = asyncHandler(async (_req, res) => {
|
||||
const data = await service.listRoleCards();
|
||||
res.json(new ApiResponse(200, data, 'Role cards fetched'));
|
||||
});
|
||||
|
||||
const getPermissionMatrix = asyncHandler(async (req, res) => {
|
||||
const data = await service.getPermissionMatrix(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'Permission matrix fetched'));
|
||||
});
|
||||
|
||||
const savePermissionMatrix = asyncHandler(async (req, res) => {
|
||||
const data = await service.savePermissionMatrix(
|
||||
req.params.id,
|
||||
req.body.matrix,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, data, 'Permission matrix saved successfully'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
getOne,
|
||||
update,
|
||||
remove,
|
||||
listPermissions,
|
||||
assignPermissions,
|
||||
listCards,
|
||||
getPermissionMatrix,
|
||||
savePermissionMatrix,
|
||||
};
|
||||
43
src/modules/roles/roles.routes.js
Normal file
43
src/modules/roles/roles.routes.js
Normal file
@ -0,0 +1,43 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../middlewares/auth.middleware');
|
||||
const authorize = require('../../middlewares/rbac.middleware');
|
||||
const validate = require('../../middlewares/validate.middleware');
|
||||
const controller = require('./roles.controller');
|
||||
const {
|
||||
createRoleSchema,
|
||||
updateRoleSchema,
|
||||
assignPermissionsSchema,
|
||||
permissionMatrixSchema,
|
||||
listRolesQuerySchema,
|
||||
} = require('./roles.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/permissions', authorize('ROLES', 'view'), controller.listPermissions);
|
||||
router.get('/cards', authorize('ROLES', 'view'), controller.listCards);
|
||||
router.get(
|
||||
'/',
|
||||
authorize('ROLES', 'view'),
|
||||
validate(listRolesQuerySchema, 'query'),
|
||||
controller.list
|
||||
);
|
||||
router.get('/:id', authorize('ROLES', 'view'), controller.getOne);
|
||||
router.post('/', authorize('ROLES', 'create'), validate(createRoleSchema), controller.create);
|
||||
router.put('/:id', authorize('ROLES', 'edit'), validate(updateRoleSchema), controller.update);
|
||||
router.put(
|
||||
'/:id/permissions',
|
||||
authorize('ROLES', 'edit'),
|
||||
validate(assignPermissionsSchema),
|
||||
controller.assignPermissions
|
||||
);
|
||||
router.get('/:id/permission-matrix', authorize('ROLES', 'view'), controller.getPermissionMatrix);
|
||||
router.put(
|
||||
'/:id/permission-matrix',
|
||||
authorize('ROLES', 'edit'),
|
||||
validate(permissionMatrixSchema),
|
||||
controller.savePermissionMatrix
|
||||
);
|
||||
router.delete('/:id', authorize('ROLES', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
355
src/modules/roles/roles.service.js
Normal file
355
src/modules/roles/roles.service.js
Normal file
@ -0,0 +1,355 @@
|
||||
const prisma = require('../../config/prisma');
|
||||
const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { getPagination } = require('../../utils/pagination');
|
||||
|
||||
const PERMISSION_ACTIONS = ['view', 'create', 'edit', 'delete', 'approve', 'export'];
|
||||
|
||||
const permissionSelect = {
|
||||
permissions: {
|
||||
select: {
|
||||
id: true,
|
||||
action: true,
|
||||
is_active: true,
|
||||
modules: { select: { id: true, code: true, name: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rolePermissionsInclude = { role_permissions: { include: permissionSelect } };
|
||||
|
||||
const sanitizeRole = (role) => {
|
||||
if (!role) return null;
|
||||
const { role_permissions, ...rest } = role;
|
||||
return {
|
||||
...rest,
|
||||
permissions: (role_permissions || []).map((rp) => ({
|
||||
id: rp.permissions.id,
|
||||
action: rp.permissions.action,
|
||||
is_active: rp.permissions.is_active,
|
||||
module: rp.permissions.modules,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const createRole = async (payload, userId, requestId) => {
|
||||
const existing = await prisma.roles.findFirst({
|
||||
where: { name: payload.name, deleted_at: null },
|
||||
});
|
||||
if (existing) throw new ApiError(409, 'Role name already exists');
|
||||
|
||||
const created = await prisma.roles.create({
|
||||
data: {
|
||||
name: payload.name.trim(),
|
||||
description: payload.description ?? null,
|
||||
is_active: payload.is_active ?? true,
|
||||
created_by: userId ? BigInt(userId) : null,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
include: rolePermissionsInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'roles',
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: sanitizeRole(created),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeRole(created);
|
||||
};
|
||||
|
||||
const listRoles = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
||||
{ description: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.roles.findMany({
|
||||
where,
|
||||
include: {
|
||||
role_permissions: { include: permissionSelect },
|
||||
_count: { select: { users_users_role_idToroles: true } },
|
||||
},
|
||||
orderBy: { created_at: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.roles.count({ where }),
|
||||
]);
|
||||
|
||||
const data = rows.map((role) => ({
|
||||
...sanitizeRole(role),
|
||||
user_count: role._count.users_users_role_idToroles,
|
||||
}));
|
||||
|
||||
return { data, meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const getRoleById = async (id) => {
|
||||
const role = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: {
|
||||
role_permissions: { include: permissionSelect },
|
||||
_count: { select: { users_users_role_idToroles: true } },
|
||||
},
|
||||
});
|
||||
if (!role) throw new ApiError(404, 'Role not found');
|
||||
return { ...sanitizeRole(role), user_count: role._count.users_users_role_idToroles };
|
||||
};
|
||||
|
||||
const updateRole = async (id, payload, userId, requestId) => {
|
||||
const existing = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: rolePermissionsInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Role not found');
|
||||
|
||||
if (payload.name) {
|
||||
const duplicate = await prisma.roles.findFirst({
|
||||
where: { name: payload.name, deleted_at: null, id: { not: BigInt(id) } },
|
||||
});
|
||||
if (duplicate) throw new ApiError(409, 'Role name already exists');
|
||||
}
|
||||
|
||||
const data = {
|
||||
...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
|
||||
...(payload.description !== undefined ? { description: payload.description || null } : {}),
|
||||
...(payload.is_active !== undefined ? { is_active: payload.is_active } : {}),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
|
||||
const updated = await prisma.roles.update({
|
||||
where: { id: BigInt(id) },
|
||||
data,
|
||||
include: rolePermissionsInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'roles',
|
||||
recordId: id,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeRole(existing),
|
||||
newValue: sanitizeRole(updated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeRole(updated);
|
||||
};
|
||||
|
||||
const deleteRole = async (id, userId, requestId) => {
|
||||
const existing = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: rolePermissionsInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Role not found');
|
||||
|
||||
const assignedUsers = await prisma.users.count({
|
||||
where: { role_id: BigInt(id), deleted_at: null },
|
||||
});
|
||||
if (assignedUsers > 0) {
|
||||
throw new ApiError(400, 'Cannot delete role assigned to active users');
|
||||
}
|
||||
|
||||
await prisma.roles.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false },
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'roles',
|
||||
recordId: id,
|
||||
action: 'DELETE',
|
||||
oldValue: sanitizeRole(existing),
|
||||
newValue: { deleted_at: new Date() },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
const listPermissionCatalog = async () => {
|
||||
const modules = await prisma.modules.findMany({
|
||||
where: { is_active: true },
|
||||
orderBy: { sort_order: 'asc' },
|
||||
include: {
|
||||
permissions: {
|
||||
where: { is_active: true },
|
||||
orderBy: { action: 'asc' },
|
||||
select: { id: true, action: true, is_active: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return modules.map((m) => ({
|
||||
id: m.id,
|
||||
code: m.code,
|
||||
name: m.name,
|
||||
permissions: m.permissions,
|
||||
}));
|
||||
};
|
||||
|
||||
const assignPermissions = async (id, permissionIds, userId, requestId) => {
|
||||
const role = await prisma.roles.findFirst({ where: { id: BigInt(id), deleted_at: null } });
|
||||
if (!role) throw new ApiError(404, 'Role not found');
|
||||
|
||||
const uniqueIds = [...new Set(permissionIds.map((pid) => BigInt(pid)))];
|
||||
|
||||
if (uniqueIds.length > 0) {
|
||||
const permissions = await prisma.permissions.findMany({
|
||||
where: { id: { in: uniqueIds }, is_active: true },
|
||||
include: { modules: { select: { code: true } } },
|
||||
});
|
||||
|
||||
if (permissions.length !== uniqueIds.length) {
|
||||
throw new ApiError(422, 'One or more permission_ids are invalid');
|
||||
}
|
||||
}
|
||||
|
||||
const before = await prisma.role_permissions.findMany({
|
||||
where: { role_id: BigInt(id) },
|
||||
include: permissionSelect,
|
||||
});
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.role_permissions.deleteMany({ where: { role_id: BigInt(id) } }),
|
||||
prisma.role_permissions.createMany({
|
||||
data: uniqueIds.map((permission_id) => ({ role_id: BigInt(id), permission_id })),
|
||||
}),
|
||||
prisma.roles.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: { updated_by: userId ? BigInt(userId) : null },
|
||||
}),
|
||||
]);
|
||||
|
||||
const updated = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(id) },
|
||||
include: rolePermissionsInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'role_permissions',
|
||||
recordId: id,
|
||||
action: 'UPDATE',
|
||||
oldValue: before.map((rp) => rp.permissions.id.toString()),
|
||||
newValue: uniqueIds.map((pid) => pid.toString()),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeRole(updated);
|
||||
};
|
||||
|
||||
const listRoleCards = async () => {
|
||||
const rows = await prisma.roles.findMany({
|
||||
where: { deleted_at: null },
|
||||
include: {
|
||||
role_permissions: { include: permissionSelect },
|
||||
_count: { select: { users_users_role_idToroles: true } },
|
||||
},
|
||||
orderBy: { created_at: 'asc' },
|
||||
});
|
||||
|
||||
return rows.map((role) => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
is_active: role.is_active,
|
||||
user_count: role._count.users_users_role_idToroles,
|
||||
permission_count: role.role_permissions.length,
|
||||
}));
|
||||
};
|
||||
|
||||
const getPermissionMatrix = async (id) => {
|
||||
const role = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: { role_permissions: { include: permissionSelect } },
|
||||
});
|
||||
if (!role) throw new ApiError(404, 'Role not found');
|
||||
|
||||
const granted = new Set(role.role_permissions.map((rp) => rp.permissions.id.toString()));
|
||||
|
||||
const modules = await prisma.modules.findMany({
|
||||
where: { is_active: true },
|
||||
orderBy: { sort_order: 'asc' },
|
||||
include: {
|
||||
permissions: { where: { is_active: true }, orderBy: { action: 'asc' } },
|
||||
},
|
||||
});
|
||||
|
||||
const matrix = modules.map((mod) => {
|
||||
const permissions = {};
|
||||
for (const action of PERMISSION_ACTIONS) {
|
||||
const perm = mod.permissions.find((p) => p.action === action);
|
||||
permissions[action] = {
|
||||
permission_id: perm?.id ?? null,
|
||||
granted: perm ? granted.has(perm.id.toString()) : false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
module_id: mod.id,
|
||||
code: mod.code,
|
||||
name: mod.name,
|
||||
permissions,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
role: { id: role.id, name: role.name },
|
||||
actions: PERMISSION_ACTIONS,
|
||||
modules: matrix,
|
||||
};
|
||||
};
|
||||
|
||||
const savePermissionMatrix = async (id, matrix, userId, requestId) => {
|
||||
const role = await prisma.roles.findFirst({ where: { id: BigInt(id), deleted_at: null } });
|
||||
if (!role) throw new ApiError(404, 'Role not found');
|
||||
|
||||
const moduleIds = matrix.map((row) => BigInt(row.module_id));
|
||||
const catalog = await prisma.permissions.findMany({
|
||||
where: { is_active: true, modules: { id: { in: moduleIds }, is_active: true } },
|
||||
select: { id: true, module_id: true, action: true },
|
||||
});
|
||||
|
||||
const permissionIds = [];
|
||||
for (const row of matrix) {
|
||||
for (const action of PERMISSION_ACTIONS) {
|
||||
if (!row.actions?.[action]) continue;
|
||||
const perm = catalog.find(
|
||||
(p) => p.module_id.toString() === String(row.module_id) && p.action === action
|
||||
);
|
||||
if (!perm)
|
||||
throw new ApiError(422, `Permission not found for module ${row.module_id}:${action}`);
|
||||
permissionIds.push(perm.id);
|
||||
}
|
||||
}
|
||||
|
||||
return assignPermissions(id, permissionIds, userId, requestId);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createRole,
|
||||
listRoles,
|
||||
getRoleById,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
listPermissionCatalog,
|
||||
assignPermissions,
|
||||
listRoleCards,
|
||||
getPermissionMatrix,
|
||||
savePermissionMatrix,
|
||||
};
|
||||
51
src/modules/roles/roles.validation.js
Normal file
51
src/modules/roles/roles.validation.js
Normal file
@ -0,0 +1,51 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createRoleSchema = Joi.object({
|
||||
name: Joi.string().max(100).required(),
|
||||
description: Joi.string().allow(null, '').optional(),
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updateRoleSchema = Joi.object({
|
||||
name: Joi.string().max(100).optional(),
|
||||
description: Joi.string().allow(null, '').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const assignPermissionsSchema = Joi.object({
|
||||
permission_ids: Joi.array().items(Joi.number().integer().positive()).default([]),
|
||||
});
|
||||
|
||||
const permissionMatrixSchema = Joi.object({
|
||||
matrix: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
module_id: Joi.number().integer().positive().required(),
|
||||
actions: Joi.object({
|
||||
view: Joi.boolean().required(),
|
||||
create: Joi.boolean().required(),
|
||||
edit: Joi.boolean().required(),
|
||||
delete: Joi.boolean().required(),
|
||||
approve: Joi.boolean().required(),
|
||||
export: Joi.boolean().required(),
|
||||
}).required(),
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required(),
|
||||
});
|
||||
|
||||
const listRolesQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createRoleSchema,
|
||||
updateRoleSchema,
|
||||
assignPermissionsSchema,
|
||||
permissionMatrixSchema,
|
||||
listRolesQuerySchema,
|
||||
};
|
||||
47
src/modules/users/users.controller.js
Normal file
47
src/modules/users/users.controller.js
Normal file
@ -0,0 +1,47 @@
|
||||
const asyncHandler = require('../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../utils/ApiResponse');
|
||||
const service = require('./users.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createUser(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'User created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listUsers(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'Users list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getUserById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'User fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateUser(req.params.id, req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'User updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteUser(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'User deleted successfully'));
|
||||
});
|
||||
|
||||
const summary = asyncHandler(async (_req, res) => {
|
||||
const data = await service.getScreenSummary();
|
||||
res.json(new ApiResponse(200, data, 'Users & roles summary fetched'));
|
||||
});
|
||||
|
||||
const filters = asyncHandler(async (_req, res) => {
|
||||
const data = await service.getFilterOptions();
|
||||
res.json(new ApiResponse(200, data, 'User filter options fetched'));
|
||||
});
|
||||
|
||||
const exportCsv = asyncHandler(async (req, res) => {
|
||||
const csv = await service.exportUsers(req.query);
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="users-export.csv"');
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove, summary, filters, exportCsv };
|
||||
35
src/modules/users/users.routes.js
Normal file
35
src/modules/users/users.routes.js
Normal file
@ -0,0 +1,35 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../middlewares/auth.middleware');
|
||||
const authorize = require('../../middlewares/rbac.middleware');
|
||||
const validate = require('../../middlewares/validate.middleware');
|
||||
const controller = require('./users.controller');
|
||||
const {
|
||||
createUserSchema,
|
||||
updateUserSchema,
|
||||
listUsersQuerySchema,
|
||||
exportUsersQuerySchema,
|
||||
} = require('./users.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/summary', authorize('USERS', 'view'), controller.summary);
|
||||
router.get('/filters', authorize('USERS', 'view'), controller.filters);
|
||||
router.get(
|
||||
'/export',
|
||||
authorize('USERS', 'export'),
|
||||
validate(exportUsersQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
authorize('USERS', 'view'),
|
||||
validate(listUsersQuerySchema, 'query'),
|
||||
controller.list
|
||||
);
|
||||
router.get('/:id', authorize('USERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('USERS', 'create'), validate(createUserSchema), controller.create);
|
||||
router.put('/:id', authorize('USERS', 'edit'), validate(updateUserSchema), controller.update);
|
||||
router.delete('/:id', authorize('USERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
363
src/modules/users/users.service.js
Normal file
363
src/modules/users/users.service.js
Normal file
@ -0,0 +1,363 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const prisma = require('../../config/prisma');
|
||||
const env = require('../../config/env');
|
||||
const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { getPagination } = require('../../utils/pagination');
|
||||
const { encrypt, decrypt } = require('../../utils/encryption');
|
||||
|
||||
const userInclude = {
|
||||
roles_users_role_idToroles: { select: { id: true, name: true } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
designations: { select: { id: true, name: true } },
|
||||
plants_users_plant_idToplants: { select: { id: true, code: true, name: true } },
|
||||
users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } },
|
||||
};
|
||||
|
||||
const sanitizeUser = (user) => {
|
||||
if (!user) return null;
|
||||
const {
|
||||
roles_users_role_idToroles,
|
||||
plants_users_plant_idToplants,
|
||||
users_users_reporting_toTousers,
|
||||
departments,
|
||||
designations,
|
||||
...rest
|
||||
} = user;
|
||||
delete rest.password_hash;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
mobile: rest.mobile ? decrypt(rest.mobile) : null,
|
||||
role: roles_users_role_idToroles || null,
|
||||
department: departments || null,
|
||||
designation: designations || null,
|
||||
plant: plants_users_plant_idToplants || null,
|
||||
reporting_manager: users_users_reporting_toTousers || null,
|
||||
};
|
||||
};
|
||||
|
||||
const buildUsersWhere = (query) => ({
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.role_id ? { role_id: BigInt(query.role_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ full_name: { contains: query.search, mode: 'insensitive' } },
|
||||
{ email: { contains: query.search, mode: 'insensitive' } },
|
||||
{ employee_code: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const toInitials = (name) => {
|
||||
if (!name) return '';
|
||||
return name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase() || '')
|
||||
.join('');
|
||||
};
|
||||
|
||||
const toUserListItem = (user) => {
|
||||
const row = sanitizeUser(user);
|
||||
return {
|
||||
id: row.id,
|
||||
full_name: row.full_name,
|
||||
email: row.email,
|
||||
initials: toInitials(row.full_name),
|
||||
employee_code: row.employee_code,
|
||||
role: row.role,
|
||||
department: row.department,
|
||||
plant: row.plant,
|
||||
last_login_at: row.last_login_at,
|
||||
status: row.status,
|
||||
is_active: row.is_active,
|
||||
};
|
||||
};
|
||||
|
||||
const assertFk = async (payload) => {
|
||||
if (payload.role_id) {
|
||||
const role = await prisma.roles.findFirst({
|
||||
where: { id: BigInt(payload.role_id), deleted_at: null, is_active: true },
|
||||
});
|
||||
if (!role) throw new ApiError(422, 'Invalid role_id');
|
||||
}
|
||||
if (payload.department_id) {
|
||||
const row = await prisma.departments.findFirst({
|
||||
where: { id: BigInt(payload.department_id), deleted_at: null },
|
||||
});
|
||||
if (!row) throw new ApiError(422, 'Invalid department_id');
|
||||
}
|
||||
if (payload.designation_id) {
|
||||
const row = await prisma.designations.findFirst({
|
||||
where: { id: BigInt(payload.designation_id), deleted_at: null },
|
||||
});
|
||||
if (!row) throw new ApiError(422, 'Invalid designation_id');
|
||||
}
|
||||
if (payload.plant_id) {
|
||||
const row = await prisma.plants.findFirst({
|
||||
where: { id: BigInt(payload.plant_id), deleted_at: null },
|
||||
});
|
||||
if (!row) throw new ApiError(422, 'Invalid plant_id');
|
||||
}
|
||||
if (payload.reporting_to) {
|
||||
const row = await prisma.users.findFirst({
|
||||
where: { id: BigInt(payload.reporting_to), deleted_at: null },
|
||||
});
|
||||
if (!row) throw new ApiError(422, 'Invalid reporting_to');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUserData = async (payload, { hashPassword = false } = {}) => {
|
||||
const data = { ...payload };
|
||||
|
||||
if (hashPassword && data.password) {
|
||||
data.password_hash = await bcrypt.hash(data.password, env.BCRYPT_SALT_ROUNDS);
|
||||
delete data.password;
|
||||
} else {
|
||||
delete data.password;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'mobile')) {
|
||||
data.mobile = data.mobile ? encrypt(data.mobile) : null;
|
||||
}
|
||||
|
||||
for (const key of ['role_id', 'department_id', 'designation_id', 'plant_id', 'reporting_to']) {
|
||||
if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const createUser = async (payload, userId, requestId) => {
|
||||
await assertFk(payload);
|
||||
|
||||
const existing = await prisma.users.findFirst({
|
||||
where: {
|
||||
deleted_at: null,
|
||||
OR: [{ email: payload.email }, { employee_code: payload.employee_code }],
|
||||
},
|
||||
});
|
||||
if (existing) throw new ApiError(409, 'User with this email or employee code already exists');
|
||||
|
||||
const data = await buildUserData(payload, { hashPassword: true });
|
||||
data.created_by = userId ? BigInt(userId) : null;
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const created = await prisma.users.create({ data, include: userInclude });
|
||||
|
||||
await auditLog({
|
||||
tableName: 'users',
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: sanitizeUser(created),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeUser(created);
|
||||
};
|
||||
|
||||
const listUsers = async (query) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = buildUsersWhere(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.users.findMany({
|
||||
where,
|
||||
include: userInclude,
|
||||
orderBy: { created_at: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.users.count({ where }),
|
||||
]);
|
||||
|
||||
return { data: rows.map(toUserListItem), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const getScreenSummary = async () => {
|
||||
const baseWhere = { deleted_at: null };
|
||||
|
||||
const [total, active, inactive, locked, rolesTotal] = await Promise.all([
|
||||
prisma.users.count({ where: baseWhere }),
|
||||
prisma.users.count({ where: { ...baseWhere, status: 'active' } }),
|
||||
prisma.users.count({ where: { ...baseWhere, status: 'inactive' } }),
|
||||
prisma.users.count({ where: { ...baseWhere, status: 'locked' } }),
|
||||
prisma.roles.count({ where: { deleted_at: null } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
users: { total, active, inactive, locked },
|
||||
roles: { total: rolesTotal },
|
||||
tabs: { users: total, roles: rolesTotal },
|
||||
};
|
||||
};
|
||||
|
||||
const getFilterOptions = async () => {
|
||||
const [roles, departments] = await Promise.all([
|
||||
prisma.roles.findMany({
|
||||
where: { deleted_at: null, is_active: true },
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
prisma.departments.findMany({
|
||||
where: { deleted_at: null, is_active: true },
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
roles,
|
||||
departments,
|
||||
statuses: [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'inactive', label: 'Inactive' },
|
||||
{ value: 'locked', label: 'Locked' },
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const escapeCsv = (value) => {
|
||||
const text = String(value ?? '');
|
||||
if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
||||
return text;
|
||||
};
|
||||
|
||||
const exportUsers = async (query) => {
|
||||
const where = buildUsersWhere(query);
|
||||
const rows = await prisma.users.findMany({
|
||||
where,
|
||||
include: userInclude,
|
||||
orderBy: { full_name: 'asc' },
|
||||
});
|
||||
|
||||
const header = [
|
||||
'Full Name',
|
||||
'Email',
|
||||
'Employee Code',
|
||||
'Role',
|
||||
'Department',
|
||||
'Plant',
|
||||
'Last Login',
|
||||
'Status',
|
||||
];
|
||||
|
||||
const lines = rows.map((user) => {
|
||||
const item = toUserListItem(user);
|
||||
return [
|
||||
item.full_name,
|
||||
item.email,
|
||||
item.employee_code,
|
||||
item.role?.name || '',
|
||||
item.department?.name || '',
|
||||
item.plant?.name || '',
|
||||
item.last_login_at ? new Date(item.last_login_at).toISOString() : '',
|
||||
item.status,
|
||||
]
|
||||
.map(escapeCsv)
|
||||
.join(',');
|
||||
});
|
||||
|
||||
return [header.join(','), ...lines].join('\n');
|
||||
};
|
||||
|
||||
const getUserById = async (id) => {
|
||||
const user = await prisma.users.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: userInclude,
|
||||
});
|
||||
if (!user) throw new ApiError(404, 'User not found');
|
||||
return sanitizeUser(user);
|
||||
};
|
||||
|
||||
const updateUser = async (id, payload, userId, requestId) => {
|
||||
const existing = await prisma.users.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: userInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'User not found');
|
||||
|
||||
await assertFk(payload);
|
||||
|
||||
if (payload.email || payload.employee_code) {
|
||||
const duplicate = await prisma.users.findFirst({
|
||||
where: {
|
||||
deleted_at: null,
|
||||
id: { not: BigInt(id) },
|
||||
OR: [
|
||||
...(payload.email ? [{ email: payload.email }] : []),
|
||||
...(payload.employee_code ? [{ employee_code: payload.employee_code }] : []),
|
||||
],
|
||||
},
|
||||
});
|
||||
if (duplicate) throw new ApiError(409, 'User with this email or employee code already exists');
|
||||
}
|
||||
|
||||
const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) });
|
||||
data.updated_by = userId ? BigInt(userId) : null;
|
||||
|
||||
const updated = await prisma.users.update({
|
||||
where: { id: BigInt(id) },
|
||||
data,
|
||||
include: userInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'users',
|
||||
recordId: id,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeUser(existing),
|
||||
newValue: sanitizeUser(updated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeUser(updated);
|
||||
};
|
||||
|
||||
const deleteUser = async (id, userId, requestId) => {
|
||||
const existing = await prisma.users.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: userInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'User not found');
|
||||
|
||||
if (userId && String(existing.id) === String(userId)) {
|
||||
throw new ApiError(400, 'You cannot delete your own account');
|
||||
}
|
||||
|
||||
await prisma.users.update({
|
||||
where: { id: BigInt(id) },
|
||||
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false },
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'users',
|
||||
recordId: id,
|
||||
action: 'DELETE',
|
||||
oldValue: sanitizeUser(existing),
|
||||
newValue: { deleted_at: new Date() },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createUser,
|
||||
listUsers,
|
||||
getUserById,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
getScreenSummary,
|
||||
getFilterOptions,
|
||||
exportUsers,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user