feat: domain CRUD modules + auth/jwt/rbac + seeds + infra fixes
This commit is contained in:
parent
db9a580f19
commit
bc3e929759
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,6 +2,11 @@ node_modules/
|
|||||||
/dist/
|
/dist/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
prisma/*.js
|
||||||
|
prisma/*.js.map
|
||||||
|
prisma/*.d.ts
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
|||||||
304
package-lock.json
generated
304
package-lock.json
generated
@ -11,9 +11,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
@ -2198,6 +2207,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/jwt": {
|
||||||
|
"version": "11.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz",
|
||||||
|
"integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
|
"jsonwebtoken": "9.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nestjs/passport": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||||
|
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/platform-express": {
|
"node_modules/@nestjs/platform-express": {
|
||||||
"version": "11.1.12",
|
"version": "11.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz",
|
||||||
@ -2627,11 +2659,19 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
@ -2642,7 +2682,6 @@
|
|||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@ -2688,7 +2727,6 @@
|
|||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
@ -2700,7 +2738,6 @@
|
|||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
|
||||||
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@ -2713,7 +2750,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
@ -2761,6 +2797,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/methods": {
|
"node_modules/@types/methods": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||||
@ -2768,35 +2814,77 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.7",
|
"version": "22.19.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
||||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/passport": {
|
||||||
|
"version": "1.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||||
|
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/passport-jwt": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonwebtoken": "*",
|
||||||
|
"@types/passport-strategy": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/passport-local": {
|
||||||
|
"version": "1.0.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz",
|
||||||
|
"integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/passport": "*",
|
||||||
|
"@types/passport-strategy": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/passport-strategy": {
|
||||||
|
"version": "0.2.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
||||||
|
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/passport": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/range-parser": {
|
"node_modules/@types/range-parser": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/send": {
|
"node_modules/@types/send": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@ -2806,7 +2894,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
@ -3966,6 +4053,20 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcrypt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.3.0",
|
||||||
|
"node-gyp-build": "^4.8.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
@ -4108,6 +4209,12 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@ -4842,6 +4949,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@ -7218,6 +7334,49 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -7315,6 +7474,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
@ -7329,6 +7524,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/log-symbols": {
|
"node_modules/log-symbols": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||||
@ -7701,6 +7902,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "8.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||||
|
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || ^20 || >= 21"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-emoji": {
|
"node_modules/node-emoji": {
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
|
||||||
@ -7717,6 +7927,17 @@
|
|||||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -7973,6 +8194,53 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/passport": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-strategy": "1.x.x",
|
||||||
|
"pause": "0.0.1",
|
||||||
|
"utils-merge": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-jwt": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"passport-strategy": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-local": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
|
||||||
|
"dependencies": {
|
||||||
|
"passport-strategy": "1.x.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/passport-strategy": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -8056,6 +8324,11 @@
|
|||||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pause": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||||
|
},
|
||||||
"node_modules/perfect-debounce": {
|
"node_modules/perfect-debounce": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
@ -8561,7 +8834,6 @@
|
|||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@ -9663,7 +9935,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
@ -9767,6 +10038,15 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/utils-merge": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
|||||||
@ -22,9 +22,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
|
|||||||
215
prisma/migrations/20260122040127_init_domain/migration.sql
Normal file
215
prisma/migrations/20260122040127_init_domain/migration.sql
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Role" AS ENUM ('ADMIN', 'DIRECTOR', 'MECHANIC', 'WORKER', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RepairStatus" AS ENUM ('RECEIVED', 'DIAGNOSIS', 'WAITING_APPROVAL', 'WAITING_PARTS', 'IN_WORK', 'ISSUED', 'CANCELED');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"firstName" TEXT NOT NULL,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
|
"role" "Role" NOT NULL DEFAULT 'OTHER',
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Contractor" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"inn" TEXT,
|
||||||
|
"address" TEXT,
|
||||||
|
"phone" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Contractor_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Unit" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Unit_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UnitIdentifier" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"unitId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"validFrom" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"validTo" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "UnitIdentifier_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RepairOrder" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"unitId" TEXT NOT NULL,
|
||||||
|
"contractorId" TEXT NOT NULL,
|
||||||
|
"status" "RepairStatus" NOT NULL DEFAULT 'RECEIVED',
|
||||||
|
"description" TEXT,
|
||||||
|
"issuedAt" TIMESTAMP(3),
|
||||||
|
"warrantyFrom" TIMESTAMP(3),
|
||||||
|
"warrantyTo" TIMESTAMP(3),
|
||||||
|
"warrantyDays" INTEGER,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RepairOrder_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RepairOrderAssignee" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"repairOrderId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "RepairOrderAssignee_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Trip" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"contractorId" TEXT NOT NULL,
|
||||||
|
"serviceObjectId" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"scheduledFor" TIMESTAMP(3) NOT NULL,
|
||||||
|
"completedAt" TIMESTAMP(3),
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Trip_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ServiceObject" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"contractorId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"address" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ServiceObject_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Calculation" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"repairOrderId" TEXT NOT NULL,
|
||||||
|
"templateId" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Calculation_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CalculationTemplate" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"contractorId" TEXT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CalculationTemplate_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CalculationItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"calculationId" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"quantity" DOUBLE PRECISION NOT NULL,
|
||||||
|
"unit" TEXT NOT NULL,
|
||||||
|
"price" DOUBLE PRECISION,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CalculationItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UnitIdentifier_unitId_type_value_validFrom_key" ON "UnitIdentifier"("unitId", "type", "value", "validFrom");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RepairOrder_status_idx" ON "RepairOrder"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RepairOrder_contractorId_idx" ON "RepairOrder"("contractorId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RepairOrder_unitId_idx" ON "RepairOrder"("unitId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RepairOrderAssignee_repairOrderId_userId_key" ON "RepairOrderAssignee"("repairOrderId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Trip_scheduledFor_idx" ON "Trip"("scheduledFor");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Calculation_repairOrderId_key" ON "Calculation"("repairOrderId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "CalculationItem_calculationId_type_idx" ON "CalculationItem"("calculationId", "type");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UnitIdentifier" ADD CONSTRAINT "UnitIdentifier_unitId_fkey" FOREIGN KEY ("unitId") REFERENCES "Unit"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RepairOrder" ADD CONSTRAINT "RepairOrder_unitId_fkey" FOREIGN KEY ("unitId") REFERENCES "Unit"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RepairOrder" ADD CONSTRAINT "RepairOrder_contractorId_fkey" FOREIGN KEY ("contractorId") REFERENCES "Contractor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RepairOrderAssignee" ADD CONSTRAINT "RepairOrderAssignee_repairOrderId_fkey" FOREIGN KEY ("repairOrderId") REFERENCES "RepairOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RepairOrderAssignee" ADD CONSTRAINT "RepairOrderAssignee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Trip" ADD CONSTRAINT "Trip_contractorId_fkey" FOREIGN KEY ("contractorId") REFERENCES "Contractor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Trip" ADD CONSTRAINT "Trip_serviceObjectId_fkey" FOREIGN KEY ("serviceObjectId") REFERENCES "ServiceObject"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ServiceObject" ADD CONSTRAINT "ServiceObject_contractorId_fkey" FOREIGN KEY ("contractorId") REFERENCES "Contractor"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Calculation" ADD CONSTRAINT "Calculation_repairOrderId_fkey" FOREIGN KEY ("repairOrderId") REFERENCES "RepairOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Calculation" ADD CONSTRAINT "Calculation_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "CalculationTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CalculationTemplate" ADD CONSTRAINT "CalculationTemplate_contractorId_fkey" FOREIGN KEY ("contractorId") REFERENCES "Contractor"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CalculationItem" ADD CONSTRAINT "CalculationItem_calculationId_fkey" FOREIGN KEY ("calculationId") REFERENCES "Calculation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -11,3 +11,217 @@ model DbHealthCheck {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Пользователи и роли (RBAC)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
DIRECTOR
|
||||||
|
MECHANIC
|
||||||
|
WORKER
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String // bcrypt hash
|
||||||
|
firstName String
|
||||||
|
lastName String
|
||||||
|
role Role @default(OTHER)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
assignedToOrders RepairOrderAssignee[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Контрагенты
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
model Contractor {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
inn String? // ИНН (опционально)
|
||||||
|
address String?
|
||||||
|
phone String?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
repairOrders RepairOrder[]
|
||||||
|
trips Trip[]
|
||||||
|
serviceObjects ServiceObject[]
|
||||||
|
calculationTemplates CalculationTemplate[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Агрегаты (оборудование)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
model Unit {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
identifiers UnitIdentifier[]
|
||||||
|
repairOrders RepairOrder[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model UnitIdentifier {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
unitId String
|
||||||
|
type String // 'SERIAL', 'INTERNAL', 'CUSTOMER'
|
||||||
|
value String
|
||||||
|
validFrom DateTime @default(now())
|
||||||
|
validTo DateTime?
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
unit Unit @relation(fields: [unitId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([unitId, type, value, validFrom])
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Ремонты (RepairOrder)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
enum RepairStatus {
|
||||||
|
RECEIVED
|
||||||
|
DIAGNOSIS
|
||||||
|
WAITING_APPROVAL
|
||||||
|
WAITING_PARTS
|
||||||
|
IN_WORK
|
||||||
|
ISSUED
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
model RepairOrder {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
unitId String
|
||||||
|
contractorId String
|
||||||
|
status RepairStatus @default(RECEIVED)
|
||||||
|
description String?
|
||||||
|
issuedAt DateTime? // дата выдачи
|
||||||
|
warrantyFrom DateTime? // начало гарантии
|
||||||
|
warrantyTo DateTime? // конец гарантии
|
||||||
|
warrantyDays Int? // срок гарантии в днях
|
||||||
|
version Int @default(1) // optimistic locking
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
unit Unit @relation(fields: [unitId], references: [id])
|
||||||
|
contractor Contractor @relation(fields: [contractorId], references: [id])
|
||||||
|
assignees RepairOrderAssignee[]
|
||||||
|
calculation Calculation?
|
||||||
|
|
||||||
|
@@index([status])
|
||||||
|
@@index([contractorId])
|
||||||
|
@@index([unitId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model RepairOrderAssignee {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
repairOrderId String
|
||||||
|
userId String
|
||||||
|
assignedAt DateTime @default(now())
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
repairOrder RepairOrder @relation(fields: [repairOrderId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([repairOrderId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Выезды
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
model Trip {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
contractorId String
|
||||||
|
serviceObjectId String?
|
||||||
|
description String?
|
||||||
|
scheduledFor DateTime
|
||||||
|
completedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
contractor Contractor @relation(fields: [contractorId], references: [id])
|
||||||
|
serviceObject ServiceObject? @relation(fields: [serviceObjectId], references: [id])
|
||||||
|
|
||||||
|
@@index([scheduledFor])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ServiceObject {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
contractorId String
|
||||||
|
name String
|
||||||
|
address String?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
contractor Contractor @relation(fields: [contractorId], references: [id])
|
||||||
|
trips Trip[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Калькуляции (MVP без денег)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
model Calculation {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
repairOrderId String @unique
|
||||||
|
templateId String?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
repairOrder RepairOrder @relation(fields: [repairOrderId], references: [id], onDelete: Cascade)
|
||||||
|
template CalculationTemplate? @relation(fields: [templateId], references: [id])
|
||||||
|
items CalculationItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CalculationTemplate {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
contractorId String?
|
||||||
|
name String
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
contractor Contractor? @relation(fields: [contractorId], references: [id])
|
||||||
|
calculations Calculation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CalculationItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
calculationId String
|
||||||
|
type String // 'WORK' or 'MATERIAL'
|
||||||
|
name String
|
||||||
|
quantity Float
|
||||||
|
unit String // единицы измерения
|
||||||
|
price Float? // отложено на будущее (MVP без денег)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
calculation Calculation @relation(fields: [calculationId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([calculationId, type])
|
||||||
|
}
|
||||||
|
|||||||
271
prisma/seed.ts
Normal file
271
prisma/seed.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { PrismaClient, Role, RepairStatus } from '@prisma/client';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Start seeding...');
|
||||||
|
|
||||||
|
// Clean existing data (optional, for dev)
|
||||||
|
// await prisma.calculationItem.deleteMany();
|
||||||
|
// await prisma.calculation.deleteMany();
|
||||||
|
// await prisma.calculationTemplate.deleteMany();
|
||||||
|
// await prisma.repairOrderAssignee.deleteMany();
|
||||||
|
// await prisma.repairOrder.deleteMany();
|
||||||
|
// await prisma.unitIdentifier.deleteMany();
|
||||||
|
// await prisma.unit.deleteMany();
|
||||||
|
// await prisma.serviceObject.deleteMany();
|
||||||
|
// await prisma.trip.deleteMany();
|
||||||
|
// await prisma.contractor.deleteMany();
|
||||||
|
// await prisma.user.deleteMany();
|
||||||
|
|
||||||
|
// Users
|
||||||
|
const adminPassword = await bcrypt.hash('admin123', 12);
|
||||||
|
const admin = await prisma.user.upsert({
|
||||||
|
where: { email: 'admin@crm.local' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'admin@crm.local',
|
||||||
|
password: adminPassword,
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'User',
|
||||||
|
role: Role.ADMIN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const directorPassword = await bcrypt.hash('director123', 12);
|
||||||
|
const director = await prisma.user.upsert({
|
||||||
|
where: { email: 'director@crm.local' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'director@crm.local',
|
||||||
|
password: directorPassword,
|
||||||
|
firstName: 'Director',
|
||||||
|
lastName: 'User',
|
||||||
|
role: Role.DIRECTOR,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mechanicPassword = await bcrypt.hash('mechanic123', 12);
|
||||||
|
const mechanic = await prisma.user.upsert({
|
||||||
|
where: { email: 'mechanic@crm.local' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'mechanic@crm.local',
|
||||||
|
password: mechanicPassword,
|
||||||
|
firstName: 'Mechanic',
|
||||||
|
lastName: 'User',
|
||||||
|
role: Role.MECHANIC,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const workerPassword = await bcrypt.hash('worker123', 12);
|
||||||
|
const worker = await prisma.user.upsert({
|
||||||
|
where: { email: 'worker@crm.local' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
email: 'worker@crm.local',
|
||||||
|
password: workerPassword,
|
||||||
|
firstName: 'Worker',
|
||||||
|
lastName: 'User',
|
||||||
|
role: Role.WORKER,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Users created:', { admin: admin.email, director: director.email, mechanic: mechanic.email, worker: worker.email });
|
||||||
|
|
||||||
|
// Contractors
|
||||||
|
const contractor1 = await prisma.contractor.create({
|
||||||
|
data: {
|
||||||
|
name: 'ООО "Ромашка"',
|
||||||
|
inn: '1234567890',
|
||||||
|
address: 'г. Москва, ул. Цветочная, д. 1',
|
||||||
|
phone: '+74951234567',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const contractor2 = await prisma.contractor.create({
|
||||||
|
data: {
|
||||||
|
name: 'ИП Петров',
|
||||||
|
inn: '0987654321',
|
||||||
|
address: 'г. Санкт-Петербург, пр. Невский, д. 42',
|
||||||
|
phone: '+78127654321',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Contractors created:', contractor1.name, contractor2.name);
|
||||||
|
|
||||||
|
// Units + Identifiers
|
||||||
|
const unit1 = await prisma.unit.create({ data: {} });
|
||||||
|
await prisma.unitIdentifier.createMany({
|
||||||
|
data: [
|
||||||
|
{ unitId: unit1.id, type: 'SERIAL', value: 'SN123456789' },
|
||||||
|
{ unitId: unit1.id, type: 'INTERNAL', value: 'INT001' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const unit2 = await prisma.unit.create({ data: {} });
|
||||||
|
await prisma.unitIdentifier.createMany({
|
||||||
|
data: [
|
||||||
|
{ unitId: unit2.id, type: 'SERIAL', value: 'SN987654321' },
|
||||||
|
{ unitId: unit2.id, type: 'CUSTOMER', value: 'CUST-A42' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Units created:', unit1.id, unit2.id);
|
||||||
|
|
||||||
|
// RepairOrders
|
||||||
|
const repair1 = await prisma.repairOrder.create({
|
||||||
|
data: {
|
||||||
|
unitId: unit1.id,
|
||||||
|
contractorId: contractor1.id,
|
||||||
|
status: RepairStatus.RECEIVED,
|
||||||
|
description: 'Не работает гидроцилиндр',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.repairOrderAssignee.create({
|
||||||
|
data: { repairOrderId: repair1.id, userId: mechanic.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const repair2 = await prisma.repairOrder.create({
|
||||||
|
data: {
|
||||||
|
unitId: unit2.id,
|
||||||
|
contractorId: contractor2.id,
|
||||||
|
status: RepairStatus.IN_WORK,
|
||||||
|
description: 'Замена насоса',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.repairOrderAssignee.createMany({
|
||||||
|
data: [
|
||||||
|
{ repairOrderId: repair2.id, userId: mechanic.id },
|
||||||
|
{ repairOrderId: repair2.id, userId: worker.id },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('RepairOrders created:', repair1.id, repair2.id);
|
||||||
|
|
||||||
|
// ServiceObjects + Trips
|
||||||
|
const serviceObj1 = await prisma.serviceObject.create({
|
||||||
|
data: {
|
||||||
|
contractorId: contractor1.id,
|
||||||
|
name: 'Склад №1',
|
||||||
|
address: 'г. Москва, ул. Складская, д. 5',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceObj2 = await prisma.serviceObject.create({
|
||||||
|
data: {
|
||||||
|
contractorId: contractor2.id,
|
||||||
|
name: 'Цех №3',
|
||||||
|
address: 'г. Санкт-Петербург, ул. Промышленная, д. 10',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trip1 = await prisma.trip.create({
|
||||||
|
data: {
|
||||||
|
contractorId: contractor1.id,
|
||||||
|
serviceObjectId: serviceObj1.id,
|
||||||
|
description: 'Диагностика оборудования',
|
||||||
|
scheduledFor: new Date(Date.now() + 24 * 60 * 60 * 1000), // tomorrow
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trip2 = await prisma.trip.create({
|
||||||
|
data: {
|
||||||
|
contractorId: contractor2.id,
|
||||||
|
serviceObjectId: serviceObj2.id,
|
||||||
|
description: 'Монтаж гидроцилиндра',
|
||||||
|
scheduledFor: new Date(Date.now() + 48 * 60 * 60 * 1000), // day after tomorrow
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Trips created:', trip1.id, trip2.id);
|
||||||
|
|
||||||
|
// Calculation Templates
|
||||||
|
const tmpl1 = await prisma.calculationTemplate.create({
|
||||||
|
data: {
|
||||||
|
contractorId: contractor1.id,
|
||||||
|
name: 'Ремонт гидроцилиндров (базовый)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tmpl2 = await prisma.calculationTemplate.create({
|
||||||
|
data: {
|
||||||
|
name: 'Общая диагностика',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('CalculationTemplates created:', tmpl1.id, tmpl2.id);
|
||||||
|
|
||||||
|
// Calculations + Items
|
||||||
|
const calc1 = await prisma.calculation.create({
|
||||||
|
data: {
|
||||||
|
repairOrderId: repair1.id,
|
||||||
|
templateId: tmpl1.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.calculationItem.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
calculationId: calc1.id,
|
||||||
|
type: 'WORK',
|
||||||
|
name: 'Диагностика',
|
||||||
|
quantity: 1,
|
||||||
|
unit: 'шт',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calculationId: calc1.id,
|
||||||
|
type: 'WORK',
|
||||||
|
name: 'Ремонт поршня',
|
||||||
|
quantity: 2,
|
||||||
|
unit: 'шт',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calculationId: calc1.id,
|
||||||
|
type: 'MATERIAL',
|
||||||
|
name: 'Сальник',
|
||||||
|
quantity: 4,
|
||||||
|
unit: 'шт',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const calc2 = await prisma.calculation.create({
|
||||||
|
data: {
|
||||||
|
repairOrderId: repair2.id,
|
||||||
|
templateId: tmpl2.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.calculationItem.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
calculationId: calc2.id,
|
||||||
|
type: 'WORK',
|
||||||
|
name: 'Замена насоса',
|
||||||
|
quantity: 1,
|
||||||
|
unit: 'шт',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
calculationId: calc2.id,
|
||||||
|
type: 'MATERIAL',
|
||||||
|
name: 'Насос НП-50',
|
||||||
|
quantity: 1,
|
||||||
|
unit: 'шт',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Calculations created:', calc1.id, calc2.id);
|
||||||
|
|
||||||
|
console.log('Seeding finished.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -1,12 +1,31 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { ContractorsService } from './contractors/contractors.service';
|
||||||
|
import { ContractorsModule } from './contractors/contractors.module';
|
||||||
|
import { UnitsModule } from './units/units.module';
|
||||||
|
import { RepairOrdersModule } from './repair-orders/repair-orders.module';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { TripsModule } from './trips/trips.module';
|
||||||
|
import { CalculationsModule } from './calculations/calculations.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [
|
||||||
|
PassportModule,
|
||||||
|
PrismaModule,
|
||||||
|
UsersModule,
|
||||||
|
ContractorsModule,
|
||||||
|
UnitsModule,
|
||||||
|
RepairOrdersModule,
|
||||||
|
AuthModule,
|
||||||
|
TripsModule,
|
||||||
|
CalculationsModule,
|
||||||
|
],
|
||||||
controllers: [AppController, HealthController],
|
controllers: [AppController, HealthController],
|
||||||
providers: [AppService],
|
providers: [AppService, ContractorsService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
18
src/auth/auth.controller.spec.ts
Normal file
18
src/auth/auth.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
|
||||||
|
describe('AuthController', () => {
|
||||||
|
let controller: AuthController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AuthController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<AuthController>(AuthController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
30
src/auth/auth.controller.ts
Normal file
30
src/auth/auth.controller.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthService, type LoginDto, type AuthResponse } from './auth.service';
|
||||||
|
import { Public } from './public.decorator';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('login')
|
||||||
|
@Public()
|
||||||
|
async login(@Body() loginDto: LoginDto): Promise<AuthResponse> {
|
||||||
|
try {
|
||||||
|
return await this.authService.login(loginDto);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof UnauthorizedException) throw e;
|
||||||
|
throw new UnauthorizedException('Login failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh')
|
||||||
|
@Public()
|
||||||
|
async refresh(@Body('refreshToken') refreshToken: string): Promise<AuthResponse> {
|
||||||
|
try {
|
||||||
|
return await this.authService.refresh(refreshToken);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof UnauthorizedException) throw e;
|
||||||
|
throw new UnauthorizedException('Refresh failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/auth/auth.module.ts
Normal file
22
src/auth/auth.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UsersModule,
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET || 'fallback-secret',
|
||||||
|
signOptions: { expiresIn: '15m' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
controllers: [AuthController],
|
||||||
|
exports: [AuthService, JwtStrategy],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
18
src/auth/auth.service.spec.ts
Normal file
18
src/auth/auth.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
describe('AuthService', () => {
|
||||||
|
let service: AuthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [AuthService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AuthService>(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
65
src/auth/auth.service.ts
Normal file
65
src/auth/auth.service.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { User } from '@prisma/client';
|
||||||
|
|
||||||
|
export type LoginDto = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthResponse = {
|
||||||
|
user: Omit<User, 'password'>;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly usersService: UsersService,
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async validateUser(email: string, pass: string): Promise<Omit<User, 'password'> | null> {
|
||||||
|
const user = await this.usersService.findByEmail(email);
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(pass, user.password);
|
||||||
|
if (!isMatch) return null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { password, ...result } = user;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(loginDto: LoginDto): Promise<AuthResponse> {
|
||||||
|
const user = await this.validateUser(loginDto.email, loginDto.password);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = { sub: user.id, email: user.email, role: user.role };
|
||||||
|
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
|
||||||
|
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
|
||||||
|
|
||||||
|
return { user, accessToken, refreshToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(refreshToken: string): Promise<AuthResponse> {
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verify(refreshToken);
|
||||||
|
const user = await this.usersService.findOne(payload.sub);
|
||||||
|
if (!user) throw new UnauthorizedException('User not found');
|
||||||
|
|
||||||
|
const newPayload = { sub: user.id, email: user.email, role: user.role };
|
||||||
|
const accessToken = this.jwtService.sign(newPayload, { expiresIn: '15m' });
|
||||||
|
const newRefreshToken = this.jwtService.sign(newPayload, { expiresIn: '7d' });
|
||||||
|
|
||||||
|
return { user, accessToken, refreshToken: newRefreshToken };
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/auth/get-user.decorator.ts
Normal file
9
src/auth/get-user.decorator.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user as { userId: string; email: string; role: Role };
|
||||||
|
},
|
||||||
|
);
|
||||||
34
src/auth/jwt-auth.guard.ts
Normal file
34
src/auth/jwt-auth.guard.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { IS_PUBLIC_KEY } from './public.decorator';
|
||||||
|
import { UnauthorizedException } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: any) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err: any, user: any, info: any) {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!user) {
|
||||||
|
const message = typeof info?.message === 'string' ? info.message : 'Unauthorized';
|
||||||
|
throw new UnauthorizedException(message);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/auth/jwt.strategy.ts
Normal file
19
src/auth/jwt.strategy.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: process.env.JWT_SECRET || 'fallback-secret',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: { sub: string; email: string; role: Role }) {
|
||||||
|
return { userId: payload.sub, email: payload.email, role: payload.role };
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/auth/public.decorator.ts
Normal file
4
src/auth/public.decorator.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
5
src/auth/rbac.decorator.ts
Normal file
5
src/auth/rbac.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
32
src/auth/rbac.guard.ts
Normal file
32
src/auth/rbac.guard.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
import { ROLES_KEY } from './rbac.decorator';
|
||||||
|
import { IS_PUBLIC_KEY } from './public.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RbacGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true; // No roles required
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
return requiredRoles.some((role) => user.role === role);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/calculations/calculations.controller.spec.ts
Normal file
18
src/calculations/calculations.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CalculationsController } from './calculations.controller';
|
||||||
|
|
||||||
|
describe('CalculationsController', () => {
|
||||||
|
let controller: CalculationsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [CalculationsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<CalculationsController>(CalculationsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
152
src/calculations/calculations.controller.ts
Normal file
152
src/calculations/calculations.controller.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
CalculationsService,
|
||||||
|
type CreateCalculationDto,
|
||||||
|
type UpdateCalculationDto,
|
||||||
|
type CreateTemplateDto,
|
||||||
|
type UpdateTemplateDto,
|
||||||
|
type CreateItemDto,
|
||||||
|
type UpdateItemDto,
|
||||||
|
} from './calculations.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('calculations')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class CalculationsController {
|
||||||
|
constructor(private readonly calculationsService: CalculationsService) {}
|
||||||
|
|
||||||
|
// Calculation CRUD
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async create(@Body() data: CreateCalculationDto) {
|
||||||
|
return this.calculationsService.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAll() {
|
||||||
|
return this.calculationsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Calculation not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() data: UpdateCalculationDto) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.update(id, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Calculation not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Calculation not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template CRUD
|
||||||
|
@Post('templates')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async createTemplate(@Body() data: CreateTemplateDto) {
|
||||||
|
return this.calculationsService.createTemplate(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('templates')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAllTemplates() {
|
||||||
|
return this.calculationsService.findAllTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('templates/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findTemplate(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.findTemplate(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('templates/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
updateTemplate(@Param('id', ParseUUIDPipe) id: string, @Body() data: UpdateTemplateDto) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.updateTemplate(id, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('templates/:id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
removeTemplate(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.removeTemplate(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item CRUD
|
||||||
|
@Post('items')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async addItem(@Body() data: CreateItemDto) {
|
||||||
|
return this.calculationsService.addItem(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('items/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
updateItem(@Param('id', ParseUUIDPipe) id: string, @Body() data: UpdateItemDto) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.updateItem(id, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('CalculationItem not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('items/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
removeItem(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.calculationsService.removeItem(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('CalculationItem not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/calculations/calculations.module.ts
Normal file
10
src/calculations/calculations.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CalculationsService } from './calculations.service';
|
||||||
|
import { CalculationsController } from './calculations.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [CalculationsService],
|
||||||
|
controllers: [CalculationsController],
|
||||||
|
exports: [CalculationsService],
|
||||||
|
})
|
||||||
|
export class CalculationsModule {}
|
||||||
18
src/calculations/calculations.service.spec.ts
Normal file
18
src/calculations/calculations.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CalculationsService } from './calculations.service';
|
||||||
|
|
||||||
|
describe('CalculationsService', () => {
|
||||||
|
let service: CalculationsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [CalculationsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CalculationsService>(CalculationsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
138
src/calculations/calculations.service.ts
Normal file
138
src/calculations/calculations.service.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Calculation, CalculationTemplate, CalculationItem } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateCalculationDto = {
|
||||||
|
repairOrderId: string;
|
||||||
|
templateId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateCalculationDto = Partial<CreateCalculationDto> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateTemplateDto = {
|
||||||
|
contractorId?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateTemplateDto = Partial<CreateTemplateDto> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateItemDto = {
|
||||||
|
calculationId: string;
|
||||||
|
type: 'WORK' | 'MATERIAL';
|
||||||
|
name: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
price?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateItemDto = Partial<CreateItemDto> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalculationsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// Template CRUD
|
||||||
|
async createTemplate(data: CreateTemplateDto): Promise<CalculationTemplate> {
|
||||||
|
if (data.contractorId) {
|
||||||
|
const contractor = await this.prisma.contractor.findUnique({ where: { id: data.contractorId } });
|
||||||
|
if (!contractor) throw new NotFoundException('Contractor not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.calculationTemplate.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllTemplates(): Promise<CalculationTemplate[]> {
|
||||||
|
return this.prisma.calculationTemplate.findMany({ where: { isActive: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTemplate(id: string): Promise<CalculationTemplate> {
|
||||||
|
const tmpl = await this.prisma.calculationTemplate.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!tmpl) throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
return tmpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTemplate(id: string, data: UpdateTemplateDto): Promise<CalculationTemplate> {
|
||||||
|
const existing = await this.prisma.calculationTemplate.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
|
||||||
|
return this.prisma.calculationTemplate.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTemplate(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.calculationTemplate.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
|
||||||
|
await this.prisma.calculationTemplate.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculation CRUD
|
||||||
|
async create(data: CreateCalculationDto): Promise<Calculation> {
|
||||||
|
const order = await this.prisma.repairOrder.findUnique({ where: { id: data.repairOrderId } });
|
||||||
|
if (!order) throw new NotFoundException('RepairOrder not found');
|
||||||
|
if (data.templateId) {
|
||||||
|
const tmpl = await this.prisma.calculationTemplate.findUnique({ where: { id: data.templateId } });
|
||||||
|
if (!tmpl) throw new NotFoundException('CalculationTemplate not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.calculation.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Calculation[]> {
|
||||||
|
return this.prisma.calculation.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: { repairOrder: true, template: true, items: true },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Calculation> {
|
||||||
|
const calc = await this.prisma.calculation.findUnique({
|
||||||
|
where: { id, isActive: true },
|
||||||
|
include: { repairOrder: true, template: true, items: true },
|
||||||
|
});
|
||||||
|
if (!calc) throw new NotFoundException('Calculation not found');
|
||||||
|
return calc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateCalculationDto): Promise<Calculation> {
|
||||||
|
const existing = await this.prisma.calculation.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Calculation not found');
|
||||||
|
|
||||||
|
return this.prisma.calculation.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.calculation.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Calculation not found');
|
||||||
|
|
||||||
|
await this.prisma.calculation.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item CRUD
|
||||||
|
async addItem(data: CreateItemDto): Promise<CalculationItem> {
|
||||||
|
const calc = await this.prisma.calculation.findUnique({ where: { id: data.calculationId } });
|
||||||
|
if (!calc) throw new NotFoundException('Calculation not found');
|
||||||
|
|
||||||
|
return this.prisma.calculationItem.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateItem(id: string, data: UpdateItemDto): Promise<CalculationItem> {
|
||||||
|
const existing = await this.prisma.calculationItem.findUnique({ where: { id } });
|
||||||
|
if (!existing) throw new NotFoundException('CalculationItem not found');
|
||||||
|
|
||||||
|
return this.prisma.calculationItem.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeItem(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.calculationItem.findUnique({ where: { id } });
|
||||||
|
if (!existing) throw new NotFoundException('CalculationItem not found');
|
||||||
|
|
||||||
|
await this.prisma.calculationItem.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/contractors/contractors.controller.spec.ts
Normal file
18
src/contractors/contractors.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ContractorsController } from './contractors.controller';
|
||||||
|
|
||||||
|
describe('ContractorsController', () => {
|
||||||
|
let controller: ContractorsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ContractorsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<ContractorsController>(ContractorsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
68
src/contractors/contractors.controller.ts
Normal file
68
src/contractors/contractors.controller.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ContractorsService, type CreateContractorDto, type UpdateContractorDto } from './contractors.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('contractors')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class ContractorsController {
|
||||||
|
constructor(private readonly contractorsService: ContractorsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async create(@Body() createContractorDto: CreateContractorDto) {
|
||||||
|
return this.contractorsService.create(createContractorDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAll() {
|
||||||
|
return this.contractorsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.contractorsService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Contractor not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() updateContractorDto: UpdateContractorDto) {
|
||||||
|
try {
|
||||||
|
return this.contractorsService.update(id, updateContractorDto);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Contractor not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.contractorsService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Contractor not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/contractors/contractors.module.ts
Normal file
10
src/contractors/contractors.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ContractorsService } from './contractors.service';
|
||||||
|
import { ContractorsController } from './contractors.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ContractorsController],
|
||||||
|
providers: [ContractorsService],
|
||||||
|
exports: [ContractorsService],
|
||||||
|
})
|
||||||
|
export class ContractorsModule {}
|
||||||
18
src/contractors/contractors.service.spec.ts
Normal file
18
src/contractors/contractors.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ContractorsService } from './contractors.service';
|
||||||
|
|
||||||
|
describe('ContractorsService', () => {
|
||||||
|
let service: ContractorsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [ContractorsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ContractorsService>(ContractorsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/contractors/contractors.service.ts
Normal file
49
src/contractors/contractors.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Contractor } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateContractorDto = {
|
||||||
|
name: string;
|
||||||
|
inn?: string;
|
||||||
|
address?: string;
|
||||||
|
phone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateContractorDto = Partial<CreateContractorDto> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContractorsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(data: CreateContractorDto): Promise<Contractor> {
|
||||||
|
const contractor = await this.prisma.contractor.create({ data });
|
||||||
|
return contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Contractor[]> {
|
||||||
|
return this.prisma.contractor.findMany({ where: { isActive: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Contractor> {
|
||||||
|
const contractor = await this.prisma.contractor.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!contractor) throw new NotFoundException('Contractor not found');
|
||||||
|
return contractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateContractorDto): Promise<Contractor> {
|
||||||
|
const existing = await this.prisma.contractor.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Contractor not found');
|
||||||
|
|
||||||
|
return this.prisma.contractor.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.contractor.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Contractor not found');
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
await this.prisma.contractor.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +1,36 @@
|
|||||||
import { Controller, Get, Res } from '@nestjs/common';
|
import { Controller, Get, Res } from '@nestjs/common';
|
||||||
import type { Response } from 'express';
|
import { type Response } from 'express';
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { PrismaService } from './prisma/prisma.service';
|
||||||
|
import { Public } from './auth/public.decorator';
|
||||||
|
|
||||||
@Controller('health')
|
@Controller()
|
||||||
export class HealthController {
|
export class HealthController {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
@Get()
|
@Get('health')
|
||||||
async getHealth(@Res() res: Response) {
|
@Public()
|
||||||
const payload = {
|
async health(@Res() res: Response) {
|
||||||
status: 'ok',
|
const start = Date.now();
|
||||||
timestamp: new Date().toISOString(),
|
let dbOk = false;
|
||||||
service: 'crm-backend',
|
|
||||||
version:
|
|
||||||
process.env.APP_VERSION ?? process.env.npm_package_version ?? 'unknown',
|
|
||||||
uptimeSeconds: Math.floor(process.uptime()),
|
|
||||||
db: {
|
|
||||||
ok: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.prisma.$queryRaw`SELECT 1`;
|
await this.prisma.$queryRaw`SELECT 1`;
|
||||||
} catch (e: any) {
|
dbOk = true;
|
||||||
payload.status = 'degraded';
|
} catch {
|
||||||
payload.db.ok = false;
|
dbOk = false;
|
||||||
res
|
|
||||||
.status(503)
|
|
||||||
.type('application/json; charset=utf-8')
|
|
||||||
.send(`${JSON.stringify(payload)}\n`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
const uptimeMs = process.uptime() * 1000;
|
||||||
|
const payload = {
|
||||||
|
status: dbOk ? 'ok' : 'error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'crm-backend',
|
||||||
|
version: process.env.npm_package_version || 'unknown',
|
||||||
|
uptimeSeconds: Math.floor(uptimeMs / 1000),
|
||||||
|
db: { ok: dbOk },
|
||||||
|
};
|
||||||
|
const statusCode = dbOk ? 200 : 503;
|
||||||
|
|
||||||
res
|
res
|
||||||
.status(200)
|
.status(statusCode)
|
||||||
.type('application/json; charset=utf-8')
|
.type('application/json; charset=utf-8')
|
||||||
.send(`${JSON.stringify(payload)}\n`);
|
.send(`${JSON.stringify(payload)}\n`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
async function bootstrap() {
|
|
||||||
dotenv.config({ path: '/opt/crm/.env' });
|
dotenv.config({ path: '/opt/crm/.env' });
|
||||||
|
|
||||||
|
// Load AppModule after env is loaded, so JwtModule/JwtStrategy read correct secrets.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { AppModule } = require('./app.module');
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/repair-orders/repair-orders.controller.spec.ts
Normal file
18
src/repair-orders/repair-orders.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { RepairOrdersController } from './repair-orders.controller';
|
||||||
|
|
||||||
|
describe('RepairOrdersController', () => {
|
||||||
|
let controller: RepairOrdersController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [RepairOrdersController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<RepairOrdersController>(RepairOrdersController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
107
src/repair-orders/repair-orders.controller.ts
Normal file
107
src/repair-orders/repair-orders.controller.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
NotFoundException,
|
||||||
|
ConflictException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { RepairOrdersService, type CreateRepairOrderDto, type UpdateRepairOrderDto } from './repair-orders.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { CurrentUser } from '../auth/get-user.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('repair-orders')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class RepairOrdersController {
|
||||||
|
constructor(private readonly repairOrdersService: RepairOrdersService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
async create(@Body() createRepairOrderDto: CreateRepairOrderDto, @CurrentUser() user: { role: Role }) {
|
||||||
|
return this.repairOrdersService.create(createRepairOrderDto, user.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAll() {
|
||||||
|
return this.repairOrdersService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.repairOrdersService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('RepairOrder not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
update(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() updateRepairOrderDto: UpdateRepairOrderDto,
|
||||||
|
@CurrentUser() user: { role: Role },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return this.repairOrdersService.update(id, updateRepairOrderDto, user.role);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException || e instanceof ConflictException) throw e;
|
||||||
|
throw new NotFoundException('RepairOrder not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.repairOrdersService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('RepairOrder not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignees
|
||||||
|
@Post(':id/assignees/:userId')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async addAssignee(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Param('userId', ParseUUIDPipe) userId: string,
|
||||||
|
@CurrentUser() user: { role: Role },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.repairOrdersService.addAssignee(id, userId, user.role);
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException || e instanceof ConflictException) throw e;
|
||||||
|
throw new NotFoundException('RepairOrder or User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id/assignees/:userId')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async removeAssignee(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Param('userId', ParseUUIDPipe) userId: string,
|
||||||
|
@CurrentUser() user: { role: Role },
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.repairOrdersService.removeAssignee(id, userId, user.role);
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException || e instanceof ConflictException) throw e;
|
||||||
|
throw new NotFoundException('RepairOrder or User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/repair-orders/repair-orders.module.ts
Normal file
10
src/repair-orders/repair-orders.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RepairOrdersService } from './repair-orders.service';
|
||||||
|
import { RepairOrdersController } from './repair-orders.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [RepairOrdersService],
|
||||||
|
controllers: [RepairOrdersController],
|
||||||
|
exports: [RepairOrdersService],
|
||||||
|
})
|
||||||
|
export class RepairOrdersModule {}
|
||||||
18
src/repair-orders/repair-orders.service.spec.ts
Normal file
18
src/repair-orders/repair-orders.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { RepairOrdersService } from './repair-orders.service';
|
||||||
|
|
||||||
|
describe('RepairOrdersService', () => {
|
||||||
|
let service: RepairOrdersService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [RepairOrdersService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<RepairOrdersService>(RepairOrdersService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
112
src/repair-orders/repair-orders.service.ts
Normal file
112
src/repair-orders/repair-orders.service.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { RepairOrder, RepairStatus, Role } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateRepairOrderDto = {
|
||||||
|
unitId: string;
|
||||||
|
contractorId: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateRepairOrderDto = Partial<
|
||||||
|
Omit<CreateRepairOrderDto, 'unitId' | 'contractorId'> & {
|
||||||
|
status?: RepairStatus;
|
||||||
|
issuedAt?: Date;
|
||||||
|
warrantyFrom?: Date;
|
||||||
|
warrantyTo?: Date;
|
||||||
|
warrantyDays?: number;
|
||||||
|
version?: number; // optimistic locking
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RepairOrdersService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(data: CreateRepairOrderDto, creatorRole: Role): Promise<RepairOrder> {
|
||||||
|
// Validate unit and contractor exist
|
||||||
|
const unit = await this.prisma.unit.findUnique({ where: { id: data.unitId } });
|
||||||
|
if (!unit) throw new NotFoundException('Unit not found');
|
||||||
|
const contractor = await this.prisma.contractor.findUnique({ where: { id: data.contractorId } });
|
||||||
|
if (!contractor) throw new NotFoundException('Contractor not found');
|
||||||
|
|
||||||
|
return this.prisma.repairOrder.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<RepairOrder[]> {
|
||||||
|
return this.prisma.repairOrder.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: { unit: { include: { identifiers: true } }, contractor: true, assignees: { include: { user: true } } },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<RepairOrder> {
|
||||||
|
const order = await this.prisma.repairOrder.findUnique({
|
||||||
|
where: { id, isActive: true },
|
||||||
|
include: { unit: { include: { identifiers: true } }, contractor: true, assignees: { include: { user: true } } },
|
||||||
|
});
|
||||||
|
if (!order) throw new NotFoundException('RepairOrder not found');
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateRepairOrderDto, updaterRole: Role): Promise<RepairOrder> {
|
||||||
|
const existing = await this.prisma.repairOrder.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('RepairOrder not found');
|
||||||
|
|
||||||
|
// Optimistic locking
|
||||||
|
if (data.version !== undefined && data.version !== existing.version) {
|
||||||
|
throw new ConflictException('RepairOrder was modified by another user');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-increment version
|
||||||
|
const newData = { ...data, version: existing.version + 1 };
|
||||||
|
|
||||||
|
// Business rules: only MECHANIC+ can change status
|
||||||
|
if (data.status && updaterRole === Role.WORKER) {
|
||||||
|
throw new ConflictException('Workers cannot change repair status');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.repairOrder.update({ where: { id }, data: newData });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.repairOrder.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('RepairOrder not found');
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
await this.prisma.repairOrder.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignees
|
||||||
|
async addAssignee(repairOrderId: string, userId: string, requestorRole: Role): Promise<void> {
|
||||||
|
const order = await this.prisma.repairOrder.findUnique({ where: { id: repairOrderId, isActive: true } });
|
||||||
|
if (!order) throw new NotFoundException('RepairOrder not found');
|
||||||
|
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id: userId, isActive: true } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
// Only MECHANIC+ can assign
|
||||||
|
if (requestorRole === Role.WORKER) {
|
||||||
|
throw new ConflictException('Workers cannot assign users to repairs');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.repairOrderAssignee.create({
|
||||||
|
data: { repairOrderId, userId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeAssignee(repairOrderId: string, userId: string, requestorRole: Role): Promise<void> {
|
||||||
|
const assignee = await this.prisma.repairOrderAssignee.findUnique({
|
||||||
|
where: { repairOrderId_userId: { repairOrderId, userId } },
|
||||||
|
});
|
||||||
|
if (!assignee) throw new NotFoundException('Assignee not found');
|
||||||
|
|
||||||
|
// Only MECHANIC+ can unassign
|
||||||
|
if (requestorRole === Role.WORKER) {
|
||||||
|
throw new ConflictException('Workers cannot unassign users from repairs');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.repairOrderAssignee.delete({ where: { id: assignee.id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/trips/trips.controller.spec.ts
Normal file
18
src/trips/trips.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { TripsController } from './trips.controller';
|
||||||
|
|
||||||
|
describe('TripsController', () => {
|
||||||
|
let controller: TripsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [TripsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<TripsController>(TripsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
121
src/trips/trips.controller.ts
Normal file
121
src/trips/trips.controller.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
TripsService,
|
||||||
|
type CreateTripDto,
|
||||||
|
type UpdateTripDto,
|
||||||
|
type CreateServiceObjectDto,
|
||||||
|
type UpdateServiceObjectDto,
|
||||||
|
} from './trips.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('trips')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class TripsController {
|
||||||
|
constructor(private readonly tripsService: TripsService) {}
|
||||||
|
|
||||||
|
// Trip CRUD
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
async create(@Body() createTripDto: CreateTripDto) {
|
||||||
|
return this.tripsService.create(createTripDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAll() {
|
||||||
|
return this.tripsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Trip not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() updateTripDto: UpdateTripDto) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.update(id, updateTripDto);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Trip not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Trip not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceObject CRUD
|
||||||
|
@Post('service-objects')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
async createServiceObject(@Body() data: CreateServiceObjectDto) {
|
||||||
|
return this.tripsService.createServiceObject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('service-objects')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAllServiceObjects() {
|
||||||
|
return this.tripsService.findAllServiceObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('service-objects/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findServiceObject(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.findServiceObject(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('ServiceObject not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('service-objects/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
updateServiceObject(@Param('id', ParseUUIDPipe) id: string, @Body() data: UpdateServiceObjectDto) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.updateServiceObject(id, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('ServiceObject not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('service-objects/:id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
removeServiceObject(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.tripsService.removeServiceObject(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('ServiceObject not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/trips/trips.module.ts
Normal file
10
src/trips/trips.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TripsService } from './trips.service';
|
||||||
|
import { TripsController } from './trips.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [TripsService],
|
||||||
|
controllers: [TripsController],
|
||||||
|
exports: [TripsService],
|
||||||
|
})
|
||||||
|
export class TripsModule {}
|
||||||
18
src/trips/trips.service.spec.ts
Normal file
18
src/trips/trips.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { TripsService } from './trips.service';
|
||||||
|
|
||||||
|
describe('TripsService', () => {
|
||||||
|
let service: TripsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [TripsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<TripsService>(TripsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
108
src/trips/trips.service.ts
Normal file
108
src/trips/trips.service.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Trip, ServiceObject } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateTripDto = {
|
||||||
|
contractorId: string;
|
||||||
|
serviceObjectId?: string;
|
||||||
|
description?: string;
|
||||||
|
scheduledFor: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateTripDto = Partial<
|
||||||
|
Omit<CreateTripDto, 'scheduledFor'> & {
|
||||||
|
scheduledFor?: Date;
|
||||||
|
completedAt?: Date;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type CreateServiceObjectDto = {
|
||||||
|
contractorId: string;
|
||||||
|
name: string;
|
||||||
|
address?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateServiceObjectDto = Partial<CreateServiceObjectDto> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TripsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// ServiceObject CRUD
|
||||||
|
async createServiceObject(data: CreateServiceObjectDto): Promise<ServiceObject> {
|
||||||
|
const contractor = await this.prisma.contractor.findUnique({ where: { id: data.contractorId } });
|
||||||
|
if (!contractor) throw new NotFoundException('Contractor not found');
|
||||||
|
|
||||||
|
return this.prisma.serviceObject.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllServiceObjects(): Promise<ServiceObject[]> {
|
||||||
|
return this.prisma.serviceObject.findMany({ where: { isActive: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findServiceObject(id: string): Promise<ServiceObject> {
|
||||||
|
const obj = await this.prisma.serviceObject.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!obj) throw new NotFoundException('ServiceObject not found');
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateServiceObject(id: string, data: UpdateServiceObjectDto): Promise<ServiceObject> {
|
||||||
|
const existing = await this.prisma.serviceObject.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('ServiceObject not found');
|
||||||
|
|
||||||
|
return this.prisma.serviceObject.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeServiceObject(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.serviceObject.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('ServiceObject not found');
|
||||||
|
|
||||||
|
await this.prisma.serviceObject.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trip CRUD
|
||||||
|
async create(data: CreateTripDto): Promise<Trip> {
|
||||||
|
const contractor = await this.prisma.contractor.findUnique({ where: { id: data.contractorId } });
|
||||||
|
if (!contractor) throw new NotFoundException('Contractor not found');
|
||||||
|
if (data.serviceObjectId) {
|
||||||
|
const obj = await this.prisma.serviceObject.findUnique({ where: { id: data.serviceObjectId } });
|
||||||
|
if (!obj) throw new NotFoundException('ServiceObject not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.trip.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Trip[]> {
|
||||||
|
return this.prisma.trip.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: { contractor: true, serviceObject: true },
|
||||||
|
orderBy: { scheduledFor: 'asc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Trip> {
|
||||||
|
const trip = await this.prisma.trip.findUnique({
|
||||||
|
where: { id, isActive: true },
|
||||||
|
include: { contractor: true, serviceObject: true },
|
||||||
|
});
|
||||||
|
if (!trip) throw new NotFoundException('Trip not found');
|
||||||
|
return trip;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateTripDto): Promise<Trip> {
|
||||||
|
const existing = await this.prisma.trip.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Trip not found');
|
||||||
|
|
||||||
|
return this.prisma.trip.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const existing = await this.prisma.trip.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('Trip not found');
|
||||||
|
|
||||||
|
await this.prisma.trip.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/units/units.controller.spec.ts
Normal file
18
src/units/units.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UnitsController } from './units.controller';
|
||||||
|
|
||||||
|
describe('UnitsController', () => {
|
||||||
|
let controller: UnitsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [UnitsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<UnitsController>(UnitsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
103
src/units/units.controller.ts
Normal file
103
src/units/units.controller.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UnitsService, type CreateIdentifierDto, type UpdateIdentifierDto } from './units.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('units')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class UnitsController {
|
||||||
|
constructor(private readonly unitsService: UnitsService) {}
|
||||||
|
|
||||||
|
// Unit CRUD
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
create() {
|
||||||
|
return this.unitsService.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findAll() {
|
||||||
|
return this.unitsService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Unit not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Unit not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier CRUD
|
||||||
|
@Post(':unitId/identifiers')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
addIdentifier(@Param('unitId', ParseUUIDPipe) unitId: string, @Body() data: CreateIdentifierDto) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.addIdentifier(unitId, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Unit not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':unitId/identifiers')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findIdentifiers(@Param('unitId', ParseUUIDPipe) unitId: string) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.findIdentifiers(unitId);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Unit not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('identifiers/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
updateIdentifier(@Param('id', ParseUUIDPipe) id: string, @Body() data: UpdateIdentifierDto) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.updateIdentifier(id, data);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Identifier not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('identifiers/:id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
removeIdentifier(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.unitsService.removeIdentifier(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('Identifier not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/units/units.module.ts
Normal file
10
src/units/units.module.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UnitsService } from './units.service';
|
||||||
|
import { UnitsController } from './units.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [UnitsService],
|
||||||
|
controllers: [UnitsController],
|
||||||
|
exports: [UnitsService],
|
||||||
|
})
|
||||||
|
export class UnitsModule {}
|
||||||
18
src/units/units.service.spec.ts
Normal file
18
src/units/units.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UnitsService } from './units.service';
|
||||||
|
|
||||||
|
describe('UnitsService', () => {
|
||||||
|
let service: UnitsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [UnitsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UnitsService>(UnitsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
84
src/units/units.service.ts
Normal file
84
src/units/units.service.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Unit, UnitIdentifier } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateUnitDto = {};
|
||||||
|
|
||||||
|
export type CreateIdentifierDto = {
|
||||||
|
type: 'SERIAL' | 'INTERNAL' | 'CUSTOMER';
|
||||||
|
value: string;
|
||||||
|
validFrom?: Date;
|
||||||
|
validTo?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateIdentifierDto = Partial<CreateIdentifierDto>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UnitsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// Unit CRUD
|
||||||
|
async create(): Promise<Unit> {
|
||||||
|
return this.prisma.unit.create({ data: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Unit[]> {
|
||||||
|
return this.prisma.unit.findMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Unit> {
|
||||||
|
const unit = await this.prisma.unit.findUnique({ where: { id } });
|
||||||
|
if (!unit) throw new NotFoundException('Unit not found');
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const unit = await this.prisma.unit.findUnique({ where: { id } });
|
||||||
|
if (!unit) throw new NotFoundException('Unit not found');
|
||||||
|
|
||||||
|
// Soft delete via cascade on identifiers (or keep unit, just mark identifiers inactive)
|
||||||
|
// For now, simply delete if no repair orders exist; otherwise throw
|
||||||
|
const hasOrders = await this.prisma.repairOrder.findFirst({ where: { unitId: id } });
|
||||||
|
if (hasOrders) throw new NotFoundException('Cannot delete unit with existing repair orders');
|
||||||
|
|
||||||
|
await this.prisma.unit.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier CRUD
|
||||||
|
async addIdentifier(unitId: string, data: CreateIdentifierDto): Promise<UnitIdentifier> {
|
||||||
|
const unit = await this.prisma.unit.findUnique({ where: { id: unitId } });
|
||||||
|
if (!unit) throw new NotFoundException('Unit not found');
|
||||||
|
|
||||||
|
return this.prisma.unitIdentifier.create({
|
||||||
|
data: { unitId, ...data },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateIdentifier(id: string, data: UpdateIdentifierDto): Promise<UnitIdentifier> {
|
||||||
|
const identifier = await this.prisma.unitIdentifier.findUnique({ where: { id } });
|
||||||
|
if (!identifier) throw new NotFoundException('Identifier not found');
|
||||||
|
|
||||||
|
return this.prisma.unitIdentifier.update({ where: { id }, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeIdentifier(id: string): Promise<void> {
|
||||||
|
const identifier = await this.prisma.unitIdentifier.findUnique({ where: { id } });
|
||||||
|
if (!identifier) throw new NotFoundException('Identifier not found');
|
||||||
|
|
||||||
|
// Soft delete: set validTo to now
|
||||||
|
await this.prisma.unitIdentifier.update({
|
||||||
|
where: { id },
|
||||||
|
data: { validTo: new Date() },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findIdentifiers(unitId: string): Promise<UnitIdentifier[]> {
|
||||||
|
const unit = await this.prisma.unit.findUnique({ where: { id: unitId } });
|
||||||
|
if (!unit) throw new NotFoundException('Unit not found');
|
||||||
|
|
||||||
|
return this.prisma.unitIdentifier.findMany({
|
||||||
|
where: { unitId },
|
||||||
|
orderBy: { validFrom: 'desc' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/users/users.controller.spec.ts
Normal file
18
src/users/users.controller.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
|
||||||
|
describe('UsersController', () => {
|
||||||
|
let controller: UsersController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [UsersController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<UsersController>(UsersController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
74
src/users/users.controller.ts
Normal file
74
src/users/users.controller.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
ConflictException,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UsersService, type CreateUserDto, type UpdateUserDto } from './users.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { RbacGuard } from '../auth/rbac.guard';
|
||||||
|
import { Roles } from '../auth/rbac.decorator';
|
||||||
|
import { Role } from '@prisma/client';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
async create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
try {
|
||||||
|
return await this.usersService.create(createUserDto);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ConflictException) throw e;
|
||||||
|
throw new ConflictException('User already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC)
|
||||||
|
findAll() {
|
||||||
|
return this.usersService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Roles(Role.ADMIN, Role.DIRECTOR, Role.MECHANIC, Role.WORKER)
|
||||||
|
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.usersService.findOne(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||||
|
try {
|
||||||
|
return this.usersService.update(id, updateUserDto);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Roles(Role.ADMIN)
|
||||||
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
try {
|
||||||
|
return this.usersService.remove(id);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NotFoundException) throw e;
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/users/users.module.ts
Normal file
12
src/users/users.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PassportModule],
|
||||||
|
providers: [UsersService],
|
||||||
|
controllers: [UsersController],
|
||||||
|
exports: [UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
18
src/users/users.service.spec.ts
Normal file
18
src/users/users.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
describe('UsersService', () => {
|
||||||
|
let service: UsersService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [UsersService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<UsersService>(UsersService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
78
src/users/users.service.ts
Normal file
78
src/users/users.service.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Injectable, ConflictException, NotFoundException } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { User, Role } from '@prisma/client';
|
||||||
|
|
||||||
|
export type CreateUserDto = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
role?: Role;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateUserDto = Partial<Omit<CreateUserDto, 'password'>> & {
|
||||||
|
password?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(data: CreateUserDto): Promise<Omit<User, 'password'>> {
|
||||||
|
const existing = await this.prisma.user.findUnique({ where: { email: data.email } });
|
||||||
|
if (existing) throw new ConflictException('User with this email already exists');
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(data.password, 12);
|
||||||
|
const user = await this.prisma.user.create({
|
||||||
|
data: { ...data, password: hashedPassword },
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { password, ...userWithoutPassword } = user;
|
||||||
|
return userWithoutPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Omit<User, 'password'>[]> {
|
||||||
|
const users = await this.prisma.user.findMany({ where: { isActive: true } });
|
||||||
|
return users.map(({ password, ...u }) => u);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Omit<User, 'password'>> {
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { password, ...userWithoutPassword } = user;
|
||||||
|
return userWithoutPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: string): Promise<User | null> {
|
||||||
|
return this.prisma.user.findUnique({ where: { email, isActive: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: UpdateUserDto): Promise<Omit<User, 'password'>> {
|
||||||
|
const existing = await this.prisma.user.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!existing) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
let hashedPassword: string | undefined;
|
||||||
|
if (data.password) {
|
||||||
|
hashedPassword = await bcrypt.hash(data.password, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.prisma.user.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...data, ...(hashedPassword && { password: hashedPassword }) },
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { password, ...userWithoutPassword } = user;
|
||||||
|
return userWithoutPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const user = await this.prisma.user.findUnique({ where: { id, isActive: true } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
// Soft delete
|
||||||
|
await this.prisma.user.update({ where: { id }, data: { isActive: false } });
|
||||||
|
}
|
||||||
|
}
|
||||||
126
test-api.sh
Executable file
126
test-api.sh
Executable file
@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_URL="http://127.0.0.1:3000"
|
||||||
|
ACCESS_TOKEN=""
|
||||||
|
REFRESH_TOKEN=""
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Health check
|
||||||
|
log "Health check"
|
||||||
|
curl -fsS "$BASE_URL/health" | jq .
|
||||||
|
|
||||||
|
# 2. Auth: login as admin
|
||||||
|
log "Login as admin"
|
||||||
|
LOGIN_RESP=$(curl -fsS -X POST "$BASE_URL/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"admin@crm.local","password":"admin123"}')
|
||||||
|
ACCESS_TOKEN=$(echo "$LOGIN_RESP" | jq -r .accessToken)
|
||||||
|
REFRESH_TOKEN=$(echo "$LOGIN_RESP" | jq -r .refreshToken)
|
||||||
|
if [[ -z "$ACCESS_TOKEN" || "$ACCESS_TOKEN" == "null" ]]; then
|
||||||
|
error "Login failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Access token obtained"
|
||||||
|
|
||||||
|
# Helper for authenticated requests
|
||||||
|
auth_get() {
|
||||||
|
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_post() {
|
||||||
|
curl -fsS -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" "$BASE_URL$1" -d "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Users: list (admin only)
|
||||||
|
log "List users"
|
||||||
|
auth_get "/users" | jq .
|
||||||
|
|
||||||
|
# 4. Contractors: list
|
||||||
|
log "List contractors"
|
||||||
|
auth_get "/contractors" | jq .
|
||||||
|
|
||||||
|
# 5. Create contractor (admin/director/mechanic)
|
||||||
|
log "Create contractor"
|
||||||
|
auth_post "/contractors" '{"name":"Test Contractor","address":"Test address"}' | jq .
|
||||||
|
|
||||||
|
# 6. Units: list
|
||||||
|
log "List units"
|
||||||
|
auth_get "/units" | jq .
|
||||||
|
|
||||||
|
# 7. Create unit and identifier
|
||||||
|
log "Create unit"
|
||||||
|
UNIT_RESP=$(auth_post "/units" '{}')
|
||||||
|
UNIT_ID=$(echo "$UNIT_RESP" | jq -r .id)
|
||||||
|
if [[ -z "$UNIT_ID" || "$UNIT_ID" == "null" ]]; then
|
||||||
|
error "Unit create failed"
|
||||||
|
echo "$UNIT_RESP" | jq .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Unit created: $UNIT_ID"
|
||||||
|
|
||||||
|
log "Add identifier to unit"
|
||||||
|
auth_post "/units/$UNIT_ID/identifiers" '{"type":"SERIAL","value":"TESTSN001"}' | jq .
|
||||||
|
|
||||||
|
# 8. RepairOrders: list
|
||||||
|
log "List repair orders"
|
||||||
|
auth_get "/repair-orders" | jq .
|
||||||
|
|
||||||
|
# 9. Create repair order (worker+)
|
||||||
|
log "Create repair order"
|
||||||
|
FIRST_CONTRACTOR_ID=$(auth_get "/contractors" | jq -r '.[0].id')
|
||||||
|
if [[ -z "$FIRST_CONTRACTOR_ID" || "$FIRST_CONTRACTOR_ID" == "null" ]]; then
|
||||||
|
error "No contractors found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
REPAIR_RESP=$(auth_post "/repair-orders" '{"unitId":"'$UNIT_ID'","contractorId":"'$FIRST_CONTRACTOR_ID'","description":"Test repair"}')
|
||||||
|
REPAIR_ID=$(echo "$REPAIR_RESP" | jq -r .id)
|
||||||
|
if [[ -z "$REPAIR_ID" || "$REPAIR_ID" == "null" ]]; then
|
||||||
|
error "Repair order create failed"
|
||||||
|
echo "$REPAIR_RESP" | jq .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Repair order created: $REPAIR_ID"
|
||||||
|
|
||||||
|
# 10. Trips: list
|
||||||
|
log "List trips"
|
||||||
|
auth_get "/trips" | jq .
|
||||||
|
|
||||||
|
# 11. Create trip (worker+)
|
||||||
|
log "Create trip"
|
||||||
|
auth_post "/trips" '{"contractorId":"'$FIRST_CONTRACTOR_ID'","scheduledFor":"'$(date -d '+1 day' -Iseconds)'","description":"Test trip"}' | jq .
|
||||||
|
|
||||||
|
# 12. Calculations: list
|
||||||
|
log "List calculations"
|
||||||
|
auth_get "/calculations" | jq .
|
||||||
|
|
||||||
|
# 13. Create calculation (mechanic+)
|
||||||
|
log "Create calculation"
|
||||||
|
auth_post "/calculations" '{"repairOrderId":"'$REPAIR_ID'"}' | jq .
|
||||||
|
|
||||||
|
# 14. Refresh token
|
||||||
|
log "Refresh token"
|
||||||
|
REFRESH_RESP=$(curl -fsS -X POST "$BASE_URL/auth/refresh" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"refreshToken\":\"$REFRESH_TOKEN\"}")
|
||||||
|
NEW_ACCESS=$(echo "$REFRESH_RESP" | jq -r .accessToken)
|
||||||
|
if [[ -z "$NEW_ACCESS" || "$NEW_ACCESS" == "null" ]]; then
|
||||||
|
error "Refresh failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ACCESS_TOKEN="$NEW_ACCESS"
|
||||||
|
log "Token refreshed"
|
||||||
|
|
||||||
|
log "All tests passed"
|
||||||
@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"incremental": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user