feat: add files
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Postgres Database
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
JWT_SECRET=
|
||||
MONGO_URI=
|
||||
LOG_DB_NAME=
|
||||
domain= # production domain
|
||||
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
migrations
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM node:21-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN ls
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
4
captain-definition
Normal file
4
captain-definition
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"schemaVersion":2,
|
||||
"dockerfilePath":"./Dockerfile"
|
||||
}
|
||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
14399
package-lock.json
generated
Normal file
14399
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
106
package.json
Normal file
106
package.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "inventory_management_backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migration:generate": "npm run build && npx typeorm migration:generate -d ./dist/db/data_source.js",
|
||||
"migration:run": "npm run build && npx typeorm migration:run -d ./dist/db/data_source.js",
|
||||
"seed": "npm run build && typeorm-extension seed:run -d ./src/db/data_source.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anujs.dev/inventory-core": "^1.0.2",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/mongoose": "^11.0.1",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"mongoose": "^8.11.0",
|
||||
"nest-winston": "^1.10.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.13.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20",
|
||||
"typeorm-extension": "^3.6.3",
|
||||
"winston": "^3.17.0",
|
||||
"winston-mongodb": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"cz-jira-smart-commit": "^3.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/app.controller.ts
Normal file
14
src/app.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { Public } from './auth/decorator/public.decorator';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
44
src/app.module.ts
Normal file
44
src/app.module.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { dataSourceOptions } from './db/data_source';
|
||||
import { RoleManagerModule } from './role_manager/role_manager.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { KitchenModule } from './kitchen/kitchen.module';
|
||||
import { LogModule } from './log/log.module';
|
||||
import { VendorModule } from './vendor/vendor.module';
|
||||
import { InventoryModule } from './inventory/inventory.module';
|
||||
import { KitchenInventoryModule } from './kitchen_inventory/kitchen_inventory.module';
|
||||
import { MenuModule } from './menu/menu.module';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import 'winston-mongodb';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { OrderModule } from './order/order.module';
|
||||
import { IngredientUsageModule } from './ingredient-usage/ingredient-usage.module';
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRoot(dataSourceOptions),
|
||||
RoleManagerModule,
|
||||
UserModule,
|
||||
AuthModule,
|
||||
KitchenModule,
|
||||
LogModule,
|
||||
VendorModule,
|
||||
InventoryModule,
|
||||
KitchenInventoryModule,
|
||||
MenuModule,
|
||||
MongooseModule.forRoot(
|
||||
`${process.env.MONGO_URI}/${process.env.LOG_DB_NAME}?authSource=admin`,
|
||||
),
|
||||
OrderModule,
|
||||
IngredientUsageModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
src/app.service.ts
Normal file
8
src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
48
src/auth/auth.controller.ts
Normal file
48
src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Public } from './decorator/public.decorator';
|
||||
import { UserLoginDto } from './dto/user_login.dto';
|
||||
import { CurrentUser } from './decorator/current_user.decorator';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly userService: UserService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('me')
|
||||
async currentUser(@CurrentUser() user: { sub: string; role: number }) {
|
||||
return await this.userService.findById(user.sub);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Public()
|
||||
async login(
|
||||
@Body() body: UserLoginDto,
|
||||
@Res({ passthrough: true }) response,
|
||||
) {
|
||||
const user = await this.authService.validateUser(
|
||||
body.email.toLowerCase(),
|
||||
body.password,
|
||||
);
|
||||
const token = await this.authService.login(user);
|
||||
response.cookie('token', token, {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: true,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return { response: 'ok' };
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout(@Res({ passthrough: true }) res) {
|
||||
res.cookie('token', '');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
38
src/auth/auth.module.ts
Normal file
38
src/auth/auth.module.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './guard/jwt.guard';
|
||||
import { RolesGuard } from './guard/role.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '7d' },
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: RolesGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
55
src/auth/auth.service.ts
Normal file
55
src/auth/auth.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UserService } from '../user/user.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly logService: LogService,
|
||||
) {}
|
||||
|
||||
async validateUser(email: string, password: string) {
|
||||
const user = await this.userService.findByEmailWithPassword(email);
|
||||
const context = AuthService.name;
|
||||
|
||||
if (user && (await bcrypt.compare(password, user.password))) {
|
||||
const { password, ...result } = user;
|
||||
|
||||
await this.logService.logInfo('User validated successfully', context, {
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Validated credentials for ${user.email}`,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
//! enable this to log invalid login attempts
|
||||
// await this.logService.logError(
|
||||
// 'Invalid login attempt',
|
||||
// context,
|
||||
// undefined,
|
||||
// {
|
||||
// user: email,
|
||||
// activity: `Failed login attempt for ${email}`,
|
||||
// },
|
||||
// );
|
||||
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
async login(user: any): Promise<string> {
|
||||
const payload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
sub: user.id,
|
||||
role: user.role.name,
|
||||
};
|
||||
|
||||
const token = this.jwtService.sign(payload);
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
8
src/auth/decorator/current_user.decorator.ts
Normal file
8
src/auth/decorator/current_user.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
4
src/auth/decorator/public.decorator.ts
Normal file
4
src/auth/decorator/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/decorator/roles.decorator.ts
Normal file
5
src/auth/decorator/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../../role_manager/role.entity';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
9
src/auth/dto/user_login.dto.ts
Normal file
9
src/auth/dto/user_login.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UserLoginDto {
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
51
src/auth/guard/jwt.guard.ts
Normal file
51
src/auth/guard/jwt.guard.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { IS_PUBLIC_KEY } from '../decorator/public.decorator';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private reflector: Reflector,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
// @ts-ignore
|
||||
const token = request.cookies?.token;
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.configService.get('JWT_SECRET'),
|
||||
});
|
||||
request['user'] = payload;
|
||||
} catch (e) {
|
||||
e;
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
39
src/auth/guard/role.guard.ts
Normal file
39
src/auth/guard/role.guard.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorator/roles.decorator';
|
||||
import { User } from '../../user/user.entity';
|
||||
import { Role } from '../../role_manager/role.entity';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
||||
ROLES_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
if (!requiredRoles) {
|
||||
return true; // No roles required, allow access.
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user: User = request.user;
|
||||
console.log(user.role);
|
||||
|
||||
// @ts-expect-error role is defined
|
||||
console.log(requiredRoles.includes(user.role));
|
||||
|
||||
// @ts-expect-error role is defined
|
||||
if (!user || !user.role || !requiredRoles.includes(user.role)) {
|
||||
throw new UnauthorizedException('Insufficient role permissions');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
16
src/common/base_entity.ts
Normal file
16
src/common/base_entity.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
created_at: Date;
|
||||
|
||||
@Column({
|
||||
type: 'timestamp',
|
||||
onUpdate: 'CURRENT_TIMESTAMP',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
updated_at: Date;
|
||||
}
|
||||
20
src/db/data_source.ts
Normal file
20
src/db/data_source.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { config } from 'dotenv';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { SeederOptions } from 'typeorm-extension';
|
||||
|
||||
config();
|
||||
|
||||
export const dataSourceOptions: DataSourceOptions & SeederOptions = {
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
username: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
entities: ['dist/**/*.entity.js'],
|
||||
migrations: ['dist/db/migrations/*.js'],
|
||||
synchronize: false,
|
||||
seeds: ['dist/db/seeds/*.seeder.js'],
|
||||
logging: true,
|
||||
};
|
||||
export default new DataSource(dataSourceOptions);
|
||||
26
src/db/seeds/kitchen.seeder.ts
Normal file
26
src/db/seeds/kitchen.seeder.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Kitchen } from '../../kitchen/kitchen.entity';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
|
||||
|
||||
export default class KitchenSeeder implements Seeder {
|
||||
track?: true;
|
||||
async run(
|
||||
dataSource: DataSource,
|
||||
factoryManager: SeederFactoryManager,
|
||||
): Promise<any> {
|
||||
const repository = dataSource.getRepository(Kitchen);
|
||||
|
||||
if ((await repository.count()) > 0) {
|
||||
console.log(`"Kitchen" table already seeded`);
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.save({
|
||||
name: 'Kitchen 1',
|
||||
});
|
||||
|
||||
await repository.save({
|
||||
name: 'Kitchen 2',
|
||||
});
|
||||
}
|
||||
}
|
||||
30
src/db/seeds/role.seeder.ts
Normal file
30
src/db/seeds/role.seeder.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Role } from '../../role_manager/role.entity';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
|
||||
|
||||
export default class RoleSeeder implements Seeder {
|
||||
track?: true;
|
||||
async run(
|
||||
dataSource: DataSource,
|
||||
factoryManager: SeederFactoryManager,
|
||||
): Promise<any> {
|
||||
const repository = dataSource.getRepository(Role);
|
||||
|
||||
if ((await repository.count()) > 0) {
|
||||
console.log(`"Role" table already seeded`);
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.save({
|
||||
name: 'Admin',
|
||||
});
|
||||
|
||||
await repository.save({
|
||||
name: 'Cook',
|
||||
});
|
||||
|
||||
await repository.save({
|
||||
name: 'User',
|
||||
});
|
||||
}
|
||||
}
|
||||
60
src/db/seeds/user.seeder.ts
Normal file
60
src/db/seeds/user.seeder.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../user/user.entity';
|
||||
import { Role } from '../../role_manager/role.entity';
|
||||
import RoleSeeder from './role.seeder';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
export default class UserSeeder implements Seeder {
|
||||
async run(
|
||||
dataSource: DataSource,
|
||||
factoryManager: SeederFactoryManager,
|
||||
): Promise<any> {
|
||||
const roleSeeder = new RoleSeeder();
|
||||
await roleSeeder.run(dataSource, factoryManager); // Ensure roles are seeded first
|
||||
|
||||
const userRepository = dataSource.getRepository(User);
|
||||
const roleRepository = dataSource.getRepository(Role);
|
||||
|
||||
// Fetch roles to associate with users
|
||||
const adminRole = await roleRepository.findOneBy({ name: 'Admin' });
|
||||
const cookRole = await roleRepository.findOneBy({ name: 'Cook' });
|
||||
const userRole = await roleRepository.findOneBy({ name: 'User' });
|
||||
|
||||
if ((await userRepository.count()) > 0) {
|
||||
console.log(`"User" table already seeded`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed users with roles
|
||||
await userRepository.save([
|
||||
{
|
||||
name: 'Admin',
|
||||
email: 'admin@test.com',
|
||||
password: bcrypt.hashSync('admin', 10),
|
||||
phone: 1234567890,
|
||||
role: adminRole as Role,
|
||||
},
|
||||
]);
|
||||
|
||||
await userRepository.save([
|
||||
{
|
||||
name: 'Cook 1',
|
||||
email: 'cook1@test.com',
|
||||
password: bcrypt.hashSync('cook', 10),
|
||||
phone: 1234567891,
|
||||
role: cookRole as Role,
|
||||
},
|
||||
]);
|
||||
|
||||
await userRepository.save([
|
||||
{
|
||||
name: 'User',
|
||||
email: 'user@test.com',
|
||||
password: bcrypt.hashSync('user', 10),
|
||||
phone: 1234567892,
|
||||
role: userRole as Role,
|
||||
},
|
||||
]);
|
||||
console.log(`"User" table seeded successfully`);
|
||||
}
|
||||
}
|
||||
1
src/ingredient-usage/dto/create-ingredient-usage.dto.ts
Normal file
1
src/ingredient-usage/dto/create-ingredient-usage.dto.ts
Normal file
@@ -0,0 +1 @@
|
||||
export class CreateIngredientUsageDto {}
|
||||
4
src/ingredient-usage/dto/update-ingredient-usage.dto.ts
Normal file
4
src/ingredient-usage/dto/update-ingredient-usage.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateIngredientUsageDto } from './create-ingredient-usage.dto';
|
||||
|
||||
export class UpdateIngredientUsageDto extends PartialType(CreateIngredientUsageDto) {}
|
||||
30
src/ingredient-usage/entities/ingredient-usage.entity.ts
Normal file
30
src/ingredient-usage/entities/ingredient-usage.entity.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BaseEntity } from 'src/common/base_entity';
|
||||
import { Inventory } from 'src/inventory/inventory.entity';
|
||||
import { Kitchen } from 'src/kitchen/kitchen.entity';
|
||||
import { Order } from 'src/order/entities/order.entity';
|
||||
import { User } from 'src/user/user.entity';
|
||||
import { Entity, ManyToOne, Column, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class IngredientUsageLog extends BaseEntity {
|
||||
@ManyToOne(() => Kitchen)
|
||||
kitchen: Kitchen;
|
||||
|
||||
@ManyToOne(() => Inventory)
|
||||
inventory: Inventory;
|
||||
|
||||
@Column('decimal')
|
||||
quantity: number;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.ingredientUsageLogs)
|
||||
user: User;
|
||||
|
||||
@CreateDateColumn()
|
||||
usedAt: Date;
|
||||
|
||||
@ManyToOne(() => Order, (order) => order.ingredientUsageLogs)
|
||||
order: Order;
|
||||
|
||||
@Column({ nullable: false })
|
||||
description: string;
|
||||
}
|
||||
19
src/ingredient-usage/ingredient-usage.controller.ts
Normal file
19
src/ingredient-usage/ingredient-usage.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { IngredientUsageService } from './ingredient-usage.service';
|
||||
import { CreateIngredientUsageDto } from './dto/create-ingredient-usage.dto';
|
||||
import { UpdateIngredientUsageDto } from './dto/update-ingredient-usage.dto';
|
||||
|
||||
@Controller('ingredient-usage')
|
||||
export class IngredientUsageController {
|
||||
constructor(
|
||||
private readonly ingredientUsageService: IngredientUsageService,
|
||||
) {}
|
||||
}
|
||||
13
src/ingredient-usage/ingredient-usage.module.ts
Normal file
13
src/ingredient-usage/ingredient-usage.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IngredientUsageService } from './ingredient-usage.service';
|
||||
import { IngredientUsageController } from './ingredient-usage.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { IngredientUsageLog } from './entities/ingredient-usage.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([IngredientUsageLog])],
|
||||
controllers: [IngredientUsageController],
|
||||
providers: [IngredientUsageService],
|
||||
exports: [IngredientUsageService],
|
||||
})
|
||||
export class IngredientUsageModule {}
|
||||
116
src/ingredient-usage/ingredient-usage.service.ts
Normal file
116
src/ingredient-usage/ingredient-usage.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { IngredientUsageLog } from './entities/ingredient-usage.entity';
|
||||
import { Order } from 'src/order/entities/order.entity';
|
||||
import { Kitchen } from 'src/kitchen/kitchen.entity';
|
||||
import { Inventory } from 'src/inventory/inventory.entity';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
|
||||
@Injectable()
|
||||
export class IngredientUsageService {
|
||||
constructor(
|
||||
@InjectRepository(IngredientUsageLog)
|
||||
private readonly ingredientUsageRepository: Repository<IngredientUsageLog>,
|
||||
private readonly logService: LogService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async logUsage(
|
||||
order: Order,
|
||||
kitchen: Kitchen,
|
||||
inventory: Inventory,
|
||||
quantity: number,
|
||||
userId: string,
|
||||
description?: string,
|
||||
): Promise<IngredientUsageLog> {
|
||||
const context = IngredientUsageService.name;
|
||||
|
||||
try {
|
||||
if (quantity <= 0) {
|
||||
throw new BadRequestException('Quantity must be greater than zero');
|
||||
}
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
|
||||
const usage = this.ingredientUsageRepository.create({
|
||||
order,
|
||||
kitchen,
|
||||
inventory,
|
||||
quantity,
|
||||
user,
|
||||
description,
|
||||
});
|
||||
|
||||
const savedUsage = await this.ingredientUsageRepository.save(usage);
|
||||
|
||||
await this.logService.logInfo('Ingredient usage recorded', context, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Logged usage of ${quantity} units of ${inventory.name} for order ${order.id}`,
|
||||
orderId: order.id,
|
||||
kitchenId: kitchen.id,
|
||||
inventoryId: inventory.id,
|
||||
});
|
||||
|
||||
return savedUsage;
|
||||
} catch (error) {
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logError(
|
||||
'Failed to log ingredient usage',
|
||||
context,
|
||||
error.stack,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Failed to log usage for inventory ${inventory?.id}`,
|
||||
orderId: order?.id,
|
||||
kitchenId: kitchen?.id,
|
||||
inventoryId: inventory?.id,
|
||||
quantity,
|
||||
},
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getUsageByOrder(orderId: string): Promise<IngredientUsageLog[]> {
|
||||
if (!orderId) {
|
||||
throw new BadRequestException('Order ID is required');
|
||||
}
|
||||
|
||||
return this.ingredientUsageRepository.find({
|
||||
where: { order: { id: orderId } },
|
||||
relations: ['inventory', 'kitchen', 'user'],
|
||||
order: { usedAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getUsageByKitchen(kitchenId: string): Promise<IngredientUsageLog[]> {
|
||||
if (!kitchenId) {
|
||||
throw new BadRequestException('Kitchen ID is required');
|
||||
}
|
||||
|
||||
return this.ingredientUsageRepository.find({
|
||||
where: { kitchen: { id: kitchenId } },
|
||||
relations: ['inventory', 'order', 'user'],
|
||||
order: { usedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getUsageByUser(userId: string): Promise<IngredientUsageLog[]> {
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User ID is required');
|
||||
}
|
||||
|
||||
return this.ingredientUsageRepository.find({
|
||||
where: { user: { id: userId } },
|
||||
relations: ['inventory', 'order', 'kitchen'],
|
||||
order: { usedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
39
src/inventory/dto/create-inward-entry.dto.ts
Normal file
39
src/inventory/dto/create-inward-entry.dto.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
IsDecimal,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateInwardEntryDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
hsn_sac: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
// @IsPositive()
|
||||
gst_rate: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
quantity: number;
|
||||
|
||||
@IsNumber()
|
||||
rate: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
unit: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
amount: number;
|
||||
}
|
||||
32
src/inventory/dto/create-inward.ts
Normal file
32
src/inventory/dto/create-inward.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
IsDateString,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { CreateInwardEntryDto } from './create-inward-entry.dto';
|
||||
|
||||
export class CreateInwardDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
vendorId: string;
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
bill_no: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
bill_url?: string;
|
||||
|
||||
@IsDateString()
|
||||
date: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateInwardEntryDto)
|
||||
entries: CreateInwardEntryDto[];
|
||||
}
|
||||
15
src/inventory/dto/update-inventory.ts
Normal file
15
src/inventory/dto/update-inventory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class updateInventoryItemDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
stock: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
unit: string;
|
||||
}
|
||||
12
src/inventory/dto/update-inward.ts
Normal file
12
src/inventory/dto/update-inward.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IsEnum, IsOptional, IsUrl } from 'class-validator';
|
||||
import { PaymentStatus } from '../payment-status.enum';
|
||||
|
||||
export default class UpdateInwardDto {
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
bill_url: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(PaymentStatus)
|
||||
payment_status: PaymentStatus;
|
||||
}
|
||||
131
src/inventory/inventory.controller.ts
Normal file
131
src/inventory/inventory.controller.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
NotFoundException,
|
||||
Patch,
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { CreateInwardDto } from './dto/create-inward';
|
||||
import { InventoryService } from './inventory.service';
|
||||
import UpdateInwardDto from './dto/update-inward';
|
||||
import { updateInventoryItemDto } from './dto/update-inventory';
|
||||
import { CreateUserDto } from 'src/user/dto/create-user';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
|
||||
@Controller('im')
|
||||
export class InventoryController {
|
||||
constructor(private readonly inventoryService: InventoryService) {}
|
||||
|
||||
// --------------- INWARD MANAGEMENT ---------------
|
||||
|
||||
@Get('inwards')
|
||||
async getAllInwards() {
|
||||
return await this.inventoryService.findAllInwards();
|
||||
}
|
||||
|
||||
@Get('inwards/:id')
|
||||
async getInwardById(@Param('id') id: string) {
|
||||
return await this.inventoryService.findInwardById(id);
|
||||
}
|
||||
|
||||
@Post('inwards')
|
||||
async createInward(@CurrentUser() user, @Body() dto: CreateInwardDto) {
|
||||
return await this.inventoryService.createInward(
|
||||
dto,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch('inwards/:id')
|
||||
async updateInward(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() updateInwardDto: UpdateInwardDto,
|
||||
) {
|
||||
return await this.inventoryService.updateInward(
|
||||
id,
|
||||
updateInwardDto,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('inwards/:id')
|
||||
async deleteInward(@CurrentUser() user, @Param('id') id: string) {
|
||||
return await this.inventoryService.deleteInward(
|
||||
id,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('inward-entry/:entryId')
|
||||
async deleteInwardEntry(
|
||||
@CurrentUser() user,
|
||||
@Param('entryId') entryId: string,
|
||||
) {
|
||||
return await this.inventoryService.deleteInwardEntry(
|
||||
entryId,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- INVENTORY MANAGEMENT ---------------
|
||||
|
||||
@Get('inventory')
|
||||
async getAllInventory() {
|
||||
return await this.inventoryService.findAllInventory();
|
||||
}
|
||||
|
||||
@Get('inventory/:name')
|
||||
async getInventoryByName(@Param('name') name: string) {
|
||||
return await this.inventoryService.findInventoryByName(name);
|
||||
}
|
||||
|
||||
@Post('inventory/add')
|
||||
async addInventory(
|
||||
@CurrentUser() user,
|
||||
@Body() body: { name: string; stock: number; unit: string },
|
||||
) {
|
||||
return await this.inventoryService.addToInventory(
|
||||
body.name,
|
||||
body.stock,
|
||||
body.unit,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('inventory/modify')
|
||||
async modifyInventoryStock(
|
||||
@Body() body: { name: string; stock: number; method: 'add' | 'subtract' },
|
||||
) {
|
||||
return await this.inventoryService.modifyStock(
|
||||
body.name,
|
||||
body.stock,
|
||||
body.method,
|
||||
);
|
||||
}
|
||||
|
||||
@Put('inventory/:id')
|
||||
async updateInventoryItem(
|
||||
@CurrentUser() user,
|
||||
@Param('id') name: string,
|
||||
@Body() updateInventoryItemDto: updateInventoryItemDto,
|
||||
) {
|
||||
return await this.inventoryService.updateInventoryItem(
|
||||
name,
|
||||
updateInventoryItemDto,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('inventory/:name')
|
||||
async deleteInventoryItem(@CurrentUser() user, @Param('name') name: string) {
|
||||
return await this.inventoryService.deleteInventoryItem(
|
||||
name,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
14
src/inventory/inventory.entity.ts
Normal file
14
src/inventory/inventory.entity.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Inventory extends BaseEntity {
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'decimal' })
|
||||
stock: number;
|
||||
|
||||
@Column()
|
||||
unit: string;
|
||||
}
|
||||
19
src/inventory/inventory.module.ts
Normal file
19
src/inventory/inventory.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InventoryService } from './inventory.service';
|
||||
import { InventoryController } from './inventory.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Inventory } from './inventory.entity';
|
||||
import { Inward } from './inward.entity';
|
||||
import { InwardEntry } from './inward_entry.entity';
|
||||
import { VendorModule } from 'src/vendor/vendor.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Inventory, Inward, InwardEntry]),
|
||||
VendorModule,
|
||||
],
|
||||
controllers: [InventoryController],
|
||||
providers: [InventoryService],
|
||||
exports: [InventoryService],
|
||||
})
|
||||
export class InventoryModule {}
|
||||
372
src/inventory/inventory.service.ts
Normal file
372
src/inventory/inventory.service.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import {
|
||||
Injectable,
|
||||
Inject,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Inward } from './inward.entity';
|
||||
import { InwardEntry } from './inward_entry.entity';
|
||||
import { Vendor } from 'src/vendor/vendor.entity';
|
||||
import { CreateInwardDto } from './dto/create-inward';
|
||||
import { VendorService } from 'src/vendor/vendor.service';
|
||||
import { Inventory } from './inventory.entity';
|
||||
import UpdateInwardDto from './dto/update-inward';
|
||||
import { updateInventoryItemDto } from './dto/update-inventory';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class InventoryService {
|
||||
constructor(
|
||||
@InjectRepository(Inventory)
|
||||
private inventoryRepository: Repository<Inventory>,
|
||||
|
||||
@InjectRepository(Inward)
|
||||
private inwardRepository: Repository<Inward>,
|
||||
|
||||
@InjectRepository(InwardEntry)
|
||||
private inwardEntryRepository: Repository<InwardEntry>,
|
||||
|
||||
private readonly vendorService: VendorService,
|
||||
private readonly logService: LogService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async findAllInwards() {
|
||||
return await this.inwardRepository.find({
|
||||
relations: ['vendor', 'entries'],
|
||||
order: { date: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findInwardById(id: string) {
|
||||
const inward = await this.inwardRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['vendor', 'entries'],
|
||||
});
|
||||
|
||||
if (!inward) {
|
||||
throw new NotFoundException(`Inward entry with ID ${id} not found.`);
|
||||
}
|
||||
|
||||
return inward;
|
||||
}
|
||||
|
||||
async findAllInventory() {
|
||||
return await this.inventoryRepository.find({
|
||||
order: { updated_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findByIds(ids: string[]): Promise<Inventory[]> {
|
||||
return await this.inventoryRepository.find({
|
||||
where: { id: In(ids) },
|
||||
});
|
||||
}
|
||||
|
||||
async findInventoryByName(name: string) {
|
||||
const entry = await this.inventoryRepository.findOne({
|
||||
where: { name },
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async createInward(dto: CreateInwardDto, userId: string): Promise<Inward> {
|
||||
const vendor = await this.vendorService.findOne(dto.vendorId);
|
||||
if (!vendor) throw new NotFoundException('Vendor not found');
|
||||
|
||||
const inward = this.inwardRepository.create({
|
||||
vendor,
|
||||
bill_no: dto.bill_no,
|
||||
bill_url: dto.bill_url,
|
||||
date: new Date(dto.date),
|
||||
});
|
||||
|
||||
const savedInward = await this.inwardRepository.save(inward);
|
||||
|
||||
for (const entry of dto.entries) {
|
||||
const inwardEntry = this.inwardEntryRepository.create({
|
||||
...entry,
|
||||
inward: savedInward,
|
||||
});
|
||||
await this.inwardEntryRepository.save(inwardEntry);
|
||||
await this.modifyStock(
|
||||
entry.description,
|
||||
entry.quantity,
|
||||
'add',
|
||||
inwardEntry.unit,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inward entry created',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Created inward entry for vendor ${vendor.name}`,
|
||||
vendorId: vendor.id,
|
||||
billNo: dto.bill_no,
|
||||
},
|
||||
);
|
||||
|
||||
return savedInward;
|
||||
}
|
||||
|
||||
async updateInward(
|
||||
id: string,
|
||||
updateInwardDto: UpdateInwardDto,
|
||||
userId: string,
|
||||
) {
|
||||
const inward = await this.inwardRepository.findOne({ where: { id } });
|
||||
if (!inward)
|
||||
throw new NotFoundException(`Inward entry with ID ${id} not found.`);
|
||||
|
||||
if (updateInwardDto.payment_status === 'PAID') {
|
||||
// @ts-ignore
|
||||
updateInwardDto.payment_date = new Date();
|
||||
} else if (updateInwardDto.payment_status === 'UNPAID') {
|
||||
// @ts-ignore
|
||||
updateInwardDto.payment_date = null;
|
||||
}
|
||||
|
||||
const updated = this.inwardRepository.merge(inward, updateInwardDto);
|
||||
const saved = await this.inwardRepository.save(updated);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inward entry updated',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Updated inward entry ${id} with payment status ${updateInwardDto.payment_status}`,
|
||||
},
|
||||
);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
async deleteInward(id: string, userId: string) {
|
||||
const inward = await this.inwardRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['entries'],
|
||||
});
|
||||
if (!inward)
|
||||
throw new NotFoundException(`Inward entry with ID ${id} not found.`);
|
||||
|
||||
for (const entry of inward.entries) {
|
||||
await this.modifyStock(
|
||||
entry.description,
|
||||
entry.quantity,
|
||||
'subtract',
|
||||
entry.unit,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
await this.inwardEntryRepository.delete({ inward: { id } });
|
||||
await this.inwardRepository.remove(inward);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inward entry deleted',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Deleted inward entry ${id}`,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
message: `Inward entry ${id} and its items deleted successfully.`,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteInwardEntry(entryId: string, userId: string) {
|
||||
const entry = await this.inwardEntryRepository.findOne({
|
||||
where: { id: entryId },
|
||||
});
|
||||
if (!entry)
|
||||
throw new NotFoundException(`Inward entry with ID ${entryId} not found.`);
|
||||
|
||||
await this.modifyStock(
|
||||
entry.description,
|
||||
entry.quantity,
|
||||
'subtract',
|
||||
entry.unit,
|
||||
userId,
|
||||
);
|
||||
await this.inwardEntryRepository.remove(entry);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inward entry item deleted',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Deleted inward entry item ${entryId}`,
|
||||
},
|
||||
);
|
||||
|
||||
return { message: `Inward entry ${entryId} deleted successfully.` };
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const entry = await this.inventoryRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async addToInventory(
|
||||
name: string,
|
||||
stock: number,
|
||||
unit: string,
|
||||
userId: string,
|
||||
) {
|
||||
let existingItem = await this.inventoryRepository.findOne({
|
||||
where: { name },
|
||||
});
|
||||
|
||||
if (!existingItem) {
|
||||
existingItem = this.inventoryRepository.create({ name, stock, unit });
|
||||
} else {
|
||||
existingItem.stock += stock;
|
||||
}
|
||||
|
||||
const item = await this.inventoryRepository.save(existingItem);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inventory item added',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Added ${stock} ${unit} of ${name} to inventory`,
|
||||
inventoryId: item.id,
|
||||
},
|
||||
);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async updateInventoryItem(
|
||||
id: string,
|
||||
updateInventoryItemDto: updateInventoryItemDto,
|
||||
userId: string,
|
||||
) {
|
||||
const inventoryItem = await this.inventoryRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
if (!inventoryItem)
|
||||
throw new BadRequestException('Inventory Item not found');
|
||||
|
||||
const updatedInventoryItem = this.inventoryRepository.merge(
|
||||
inventoryItem,
|
||||
updateInventoryItemDto,
|
||||
);
|
||||
await this.inventoryRepository.save(updatedInventoryItem);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inventory item updated',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Updated inventory item ${inventoryItem.name}`,
|
||||
inventoryId: inventoryItem.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async deleteInventoryItem(name: string, userId: string) {
|
||||
const entry = await this.findInventoryByName(name);
|
||||
if (!entry) throw new BadRequestException(`Item not found`);
|
||||
|
||||
try {
|
||||
await this.inventoryRepository.remove(entry);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inventory item deleted',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Deleted inventory item ${entry.name}`,
|
||||
inventoryId: entry.id,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logError(
|
||||
'Failed to delete inventory item',
|
||||
InventoryService.name,
|
||||
error.stack,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Failed to delete inventory item ${entry.name}`,
|
||||
inventoryId: entry.id,
|
||||
},
|
||||
);
|
||||
|
||||
throw new BadRequestException(
|
||||
`${entry.name} already assigned to kitchen, Unable to delete`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async modifyStock(
|
||||
name: string,
|
||||
stock: number,
|
||||
method: 'add' | 'subtract',
|
||||
unit?: string,
|
||||
userId?: string,
|
||||
) {
|
||||
let existingItem = await this.inventoryRepository.findOne({
|
||||
where: { name },
|
||||
});
|
||||
|
||||
if (!existingItem) {
|
||||
if (method === 'subtract') {
|
||||
throw new Error(
|
||||
`Cannot subtract stock: Item "${name}" does not exist.`,
|
||||
);
|
||||
}
|
||||
existingItem = this.inventoryRepository.create({ name, stock, unit });
|
||||
} else {
|
||||
if (method === 'subtract') {
|
||||
if (existingItem.stock < stock) {
|
||||
throw new Error(`Not enough stock available for "${name}".`);
|
||||
}
|
||||
existingItem.stock -= stock;
|
||||
} else {
|
||||
existingItem.stock += stock;
|
||||
}
|
||||
}
|
||||
|
||||
const item = await this.inventoryRepository.save(existingItem);
|
||||
|
||||
if (userId) {
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Inventory stock modified',
|
||||
InventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `${method === 'add' ? 'Added' : 'Subtracted'} ${stock} ${item.unit} of ${name}`,
|
||||
inventoryId: item.id,
|
||||
newStock: item.stock,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
29
src/inventory/inward.entity.ts
Normal file
29
src/inventory/inward.entity.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Vendor } from 'src/vendor/vendor.entity';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { InwardEntry } from './inward_entry.entity';
|
||||
import { PaymentStatus } from './payment-status.enum';
|
||||
|
||||
@Entity()
|
||||
export class Inward extends BaseEntity {
|
||||
@ManyToOne(() => Vendor, (vendor) => vendor.inwards)
|
||||
vendor: Vendor;
|
||||
|
||||
@Column()
|
||||
bill_no: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
bill_url?: string;
|
||||
|
||||
@Column()
|
||||
date: Date;
|
||||
|
||||
@OneToMany(() => InwardEntry, (entry) => entry.inward, { cascade: true })
|
||||
entries: InwardEntry[];
|
||||
|
||||
@Column({ type: 'enum', enum: PaymentStatus, default: PaymentStatus.UNPAID })
|
||||
payment_status: PaymentStatus;
|
||||
|
||||
@Column({ nullable: true })
|
||||
payment_date: Date;
|
||||
}
|
||||
40
src/inventory/inward_entry.entity.ts
Normal file
40
src/inventory/inward_entry.entity.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Inward } from './inward.entity';
|
||||
|
||||
@Entity()
|
||||
export class InwardEntry extends BaseEntity {
|
||||
@ManyToOne(() => Inward, (inward) => inward.entries, {
|
||||
onDelete: 'CASCADE',
|
||||
nullable: false,
|
||||
})
|
||||
@JoinColumn()
|
||||
inward: Inward;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
hsn_sac: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 5, scale: 2 })
|
||||
gst_rate: number;
|
||||
|
||||
@Column({ type: 'decimal' })
|
||||
quantity: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
rate: number;
|
||||
|
||||
@Column()
|
||||
unit: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({})
|
||||
company_name: string;
|
||||
|
||||
@Column({})
|
||||
manufacuting_date: Date;
|
||||
}
|
||||
4
src/inventory/payment-status.enum.ts
Normal file
4
src/inventory/payment-status.enum.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum PaymentStatus {
|
||||
PAID = 'PAID',
|
||||
UNPAID = 'UNPAID',
|
||||
}
|
||||
23
src/kitchen/kitchen.entity.ts
Normal file
23
src/kitchen/kitchen.entity.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Column, Entity, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { User } from '../user/user.entity';
|
||||
import { KitchenInventory } from 'src/kitchen_inventory/kitchen_inventory.entity';
|
||||
import { KitchenTransfer } from 'src/kitchen_inventory/kitchen_transfer.entity';
|
||||
|
||||
@Entity()
|
||||
export class Kitchen extends BaseEntity {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@OneToMany(() => User, (user) => user.kitchen)
|
||||
users: User[];
|
||||
|
||||
@OneToMany(() => KitchenInventory, (ki) => ki.kitchen)
|
||||
kitchenInventory: KitchenInventory[];
|
||||
|
||||
@OneToMany(() => KitchenTransfer, (transfer) => transfer.fromKitchen)
|
||||
transfersFrom: KitchenTransfer[];
|
||||
|
||||
@OneToMany(() => KitchenTransfer, (transfer) => transfer.toKitchen)
|
||||
transfersTo: KitchenTransfer[];
|
||||
}
|
||||
11
src/kitchen/kitchen.module.ts
Normal file
11
src/kitchen/kitchen.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { KitchenService } from './kitchen.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Kitchen } from './kitchen.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Kitchen])],
|
||||
providers: [KitchenService],
|
||||
exports: [KitchenService],
|
||||
})
|
||||
export class KitchenModule {}
|
||||
54
src/kitchen/kitchen.service.spec.ts
Normal file
54
src/kitchen/kitchen.service.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { KitchenService } from './kitchen.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Kitchen } from './kitchen.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
describe('KitchenService', () => {
|
||||
let service: KitchenService;
|
||||
let kitchenRepository: Repository<Kitchen>;
|
||||
const mockKitchenRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
KitchenService,
|
||||
{
|
||||
provide: getRepositoryToken(Kitchen),
|
||||
useValue: mockKitchenRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<KitchenService>(KitchenService);
|
||||
kitchenRepository = module.get<Repository<Kitchen>>(
|
||||
getRepositoryToken(Kitchen),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('findKitchen', () => {
|
||||
it("should return a kitchen by it's id", async () => {
|
||||
const kitchen = { id: '1', name: 'Kitchen 1' };
|
||||
mockKitchenRepository.findOne.mockResolvedValue(kitchen);
|
||||
|
||||
await expect(service.findKitchen('1')).resolves.toEqual(kitchen);
|
||||
expect(mockKitchenRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: '1' },
|
||||
relations: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
33
src/kitchen/kitchen.service.ts
Normal file
33
src/kitchen/kitchen.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Kitchen } from './kitchen.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class KitchenService {
|
||||
constructor(
|
||||
@InjectRepository(Kitchen) private kitchenRepository: Repository<Kitchen>,
|
||||
) {}
|
||||
|
||||
async findKitchen(id: string, withUser: boolean = false): Promise<Kitchen> {
|
||||
const kitchen = await this.kitchenRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: withUser ? ['users'] : ['kitchenInventory'],
|
||||
});
|
||||
if (!kitchen) {
|
||||
throw new BadRequestException('Kitchen not found');
|
||||
}
|
||||
|
||||
return kitchen;
|
||||
}
|
||||
|
||||
async getAllKitchens(withUser: boolean = false): Promise<Kitchen[]> {
|
||||
return await this.kitchenRepository.find({
|
||||
relations: withUser
|
||||
? ['users']
|
||||
: ['kitchenInventory', 'kitchenInventory.inventory'],
|
||||
});
|
||||
}
|
||||
}
|
||||
125
src/kitchen_inventory/kitchen_inventory.controller.ts
Normal file
125
src/kitchen_inventory/kitchen_inventory.controller.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Controller, Post, Body, Param, Get, Delete } from '@nestjs/common';
|
||||
import { KitchenInventoryService } from './kitchen_inventory.service';
|
||||
import { Roles } from 'src/auth/decorator/roles.decorator';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
|
||||
@Controller('kitchen-inventory')
|
||||
export class KitchenInventoryController {
|
||||
constructor(
|
||||
private readonly kitchenInventoryService: KitchenInventoryService,
|
||||
) {}
|
||||
@Get('/transfers/all')
|
||||
@Roles('Admin', 'Cook')
|
||||
async getKitchenTransfers() {
|
||||
return await this.kitchenInventoryService.getAllKitchenTransfers();
|
||||
}
|
||||
|
||||
@Get('/transfers/kitchen')
|
||||
@Roles('Cook')
|
||||
async getKitchenTransfersByUser(@CurrentUser() user: { sub: string }) {
|
||||
return await this.kitchenInventoryService.getKitchenTransfersByUser(
|
||||
user.sub,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('add')
|
||||
@Roles('Admin')
|
||||
async addInventoryToKitchen(
|
||||
@CurrentUser() user,
|
||||
@Body()
|
||||
body: {
|
||||
kitchenId: string;
|
||||
inventoryId: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
},
|
||||
) {
|
||||
return await this.kitchenInventoryService.addInventoryToKitchen(
|
||||
body.kitchenId,
|
||||
body.inventoryId,
|
||||
body.quantity,
|
||||
body.unit,
|
||||
user.sub,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('add-multiple/:kitchenId')
|
||||
async addMultipleInventoriesToKitchen(
|
||||
@CurrentUser() user,
|
||||
@Param('kitchenId') kitchenId: string,
|
||||
@Body()
|
||||
body: {
|
||||
inventories: { inventoryId: string; quantity: number; unit: string }[];
|
||||
},
|
||||
) {
|
||||
return this.kitchenInventoryService.addMultipleInventoriesToKitchen(
|
||||
kitchenId,
|
||||
body.inventories,
|
||||
user.sub,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete('ki/:id')
|
||||
async deleteFromKitchenInventory(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
await this.kitchenInventoryService.deleteFromKitchen(
|
||||
id,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('transfer')
|
||||
@Roles('Admin', 'Cook')
|
||||
async transferInventory(
|
||||
@CurrentUser() user: { sub: string },
|
||||
@Body()
|
||||
body: {
|
||||
fromKitchenId: string;
|
||||
toKitchenId: string;
|
||||
inventoryId: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
reason: string;
|
||||
},
|
||||
) {
|
||||
return await this.kitchenInventoryService.transferInventory(
|
||||
body.fromKitchenId,
|
||||
body.toKitchenId,
|
||||
body.inventoryId,
|
||||
body.quantity,
|
||||
body.unit,
|
||||
body.reason,
|
||||
user.sub,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
async getKitchenInventoryByKitchen(@CurrentUser() user) {
|
||||
return await this.kitchenInventoryService.getKitchenInventoryByCook(
|
||||
user.sub,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':kitchenId')
|
||||
async getKitchenInventory(@Param('kitchenId') kitchenId: string) {
|
||||
return await this.kitchenInventoryService.getKitchenInventory(kitchenId);
|
||||
}
|
||||
|
||||
@Get(':kitchenId/:inventoryId')
|
||||
async getKitchenInventoryItem(
|
||||
@Param('kitchenId') kitchenId: string,
|
||||
@Param('inventoryId') inventoryId: string,
|
||||
) {
|
||||
return await this.kitchenInventoryService.getKitchenInventoryItem(
|
||||
kitchenId,
|
||||
inventoryId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async getAllKitchenInventory() {
|
||||
return await this.kitchenInventoryService.getKitchenInventories();
|
||||
}
|
||||
}
|
||||
22
src/kitchen_inventory/kitchen_inventory.entity.ts
Normal file
22
src/kitchen_inventory/kitchen_inventory.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { BaseEntity } from 'src/common/base_entity';
|
||||
import { Inventory } from 'src/inventory/inventory.entity';
|
||||
import { Kitchen } from 'src/kitchen/kitchen.entity';
|
||||
import { Column, Entity, ManyToOne, Unique } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
@Unique(['kitchen', 'inventory'])
|
||||
export class KitchenInventory extends BaseEntity {
|
||||
@ManyToOne(() => Kitchen, (kitchen) => kitchen.kitchenInventory, {
|
||||
nullable: false,
|
||||
})
|
||||
kitchen: Kitchen;
|
||||
|
||||
@ManyToOne(() => Inventory, { nullable: false })
|
||||
inventory: Inventory;
|
||||
|
||||
@Column({ type: 'decimal', default: 0 })
|
||||
stock: number;
|
||||
|
||||
@Column()
|
||||
unit: string;
|
||||
}
|
||||
20
src/kitchen_inventory/kitchen_inventory.module.ts
Normal file
20
src/kitchen_inventory/kitchen_inventory.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { KitchenInventoryService } from './kitchen_inventory.service';
|
||||
import { KitchenInventoryController } from './kitchen_inventory.controller';
|
||||
import { KitchenModule } from 'src/kitchen/kitchen.module';
|
||||
import { InventoryModule } from 'src/inventory/inventory.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { KitchenInventory } from './kitchen_inventory.entity';
|
||||
import { KitchenTransfer } from './kitchen_transfer.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([KitchenInventory, KitchenTransfer]),
|
||||
KitchenModule,
|
||||
InventoryModule,
|
||||
],
|
||||
controllers: [KitchenInventoryController],
|
||||
providers: [KitchenInventoryService],
|
||||
exports: [KitchenInventoryService],
|
||||
})
|
||||
export class KitchenInventoryModule {}
|
||||
336
src/kitchen_inventory/kitchen_inventory.service.ts
Normal file
336
src/kitchen_inventory/kitchen_inventory.service.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { KitchenInventory } from './kitchen_inventory.entity';
|
||||
import { Kitchen } from '../kitchen/kitchen.entity';
|
||||
import { KitchenTransfer } from './kitchen_transfer.entity';
|
||||
import { Inventory } from 'src/inventory/inventory.entity';
|
||||
import { InventoryService } from 'src/inventory/inventory.service';
|
||||
import { KitchenService } from 'src/kitchen/kitchen.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
import {
|
||||
InventoryDependencies,
|
||||
InventoryManager,
|
||||
} from '@anujs.dev/inventory-core';
|
||||
|
||||
@Injectable()
|
||||
export class KitchenInventoryService {
|
||||
private inventoryManager: InventoryManager;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(KitchenInventory)
|
||||
private readonly kitchenInventoryRepository: Repository<KitchenInventory>,
|
||||
@InjectRepository(KitchenTransfer)
|
||||
private readonly kitchenTransferRepository: Repository<KitchenTransfer>,
|
||||
private readonly kitchenService: KitchenService,
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly userService: UserService,
|
||||
private readonly logService: LogService,
|
||||
) {
|
||||
const deps: InventoryDependencies = {
|
||||
findKitchen: async (kitchenId: string): Promise<Kitchen | null> => {
|
||||
return await this.kitchenService.findKitchen(kitchenId);
|
||||
},
|
||||
findInventory: async (inventoryId: string): Promise<Inventory | null> => {
|
||||
return await this.inventoryService.findOne(inventoryId);
|
||||
},
|
||||
modifyMainInventoryStock: async (
|
||||
inventoryName: string,
|
||||
quantity: number,
|
||||
operation: 'subtract' | 'add',
|
||||
): Promise<void> => {
|
||||
await this.inventoryService.modifyStock(
|
||||
inventoryName,
|
||||
quantity,
|
||||
operation,
|
||||
);
|
||||
},
|
||||
findKitchenInventory: async (
|
||||
kitchenId: string,
|
||||
inventoryId: string,
|
||||
): Promise<KitchenInventory | null> => {
|
||||
return await this.kitchenInventoryRepository.findOne({
|
||||
where: {
|
||||
kitchen: { id: kitchenId },
|
||||
inventory: { id: inventoryId },
|
||||
},
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
},
|
||||
saveKitchenInventory: async (
|
||||
ki: KitchenInventory,
|
||||
): Promise<KitchenInventory> => {
|
||||
return await this.kitchenInventoryRepository.save(ki);
|
||||
},
|
||||
findInventories: async (ids: string[]) => {
|
||||
return await this.inventoryService.findByIds(ids);
|
||||
},
|
||||
logActivity: async (
|
||||
message: string,
|
||||
details: Record<string, any>,
|
||||
): Promise<void> => {
|
||||
await this.logService.logInfo(
|
||||
message,
|
||||
KitchenInventoryService.name,
|
||||
details,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
this.inventoryManager = new InventoryManager(deps);
|
||||
}
|
||||
|
||||
async getKitchenInventory(kitchenId: string) {
|
||||
const kitchen = await this.kitchenService.findKitchen(kitchenId);
|
||||
if (!kitchen) {
|
||||
throw new NotFoundException('Kitchen not found');
|
||||
}
|
||||
|
||||
return await this.kitchenInventoryRepository.find({
|
||||
where: { kitchen: { id: kitchenId } },
|
||||
relations: ['inventory', 'kitchen'],
|
||||
order: { updated_at: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getKitchenInventories() {
|
||||
return await this.kitchenInventoryRepository.find({
|
||||
relations: ['kitchen', 'inventory'],
|
||||
order: { updated_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getKitchenInventoryItem(kitchenId: string, inventoryId: string) {
|
||||
const kitchen = await this.kitchenService.findKitchen(kitchenId);
|
||||
const inventory = await this.inventoryService.findOne(inventoryId);
|
||||
|
||||
if (!kitchen || !inventory) {
|
||||
throw new NotFoundException('Kitchen or Inventory not found');
|
||||
}
|
||||
|
||||
const kitchenInventory = await this.kitchenInventoryRepository.findOne({
|
||||
where: { kitchen, inventory },
|
||||
relations: ['inventory'],
|
||||
});
|
||||
|
||||
if (!kitchenInventory) {
|
||||
throw new NotFoundException('Inventory item not found in this kitchen');
|
||||
}
|
||||
|
||||
return kitchenInventory;
|
||||
}
|
||||
|
||||
async getAllKitchenTransfers() {
|
||||
return await this.kitchenTransferRepository.find({
|
||||
relations: ['fromKitchen', 'toKitchen', 'inventory', 'user'],
|
||||
});
|
||||
}
|
||||
|
||||
async getKitchenTransfersByUser(userId: string) {
|
||||
return await this.kitchenTransferRepository.find({
|
||||
where: { user: { id: userId } },
|
||||
relations: ['fromKitchen', 'toKitchen', 'inventory', 'user'],
|
||||
order: { created_at: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getKitchenInventoryByCook(id: string) {
|
||||
const user = await this.userService.findById(id);
|
||||
|
||||
return await this.kitchenInventoryRepository.find({
|
||||
where: {
|
||||
kitchen: {
|
||||
id: user.kitchen?.id,
|
||||
},
|
||||
},
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
}
|
||||
|
||||
async addInventoryToKitchen(
|
||||
kitchenId: string,
|
||||
inventoryId: string,
|
||||
quantity: number,
|
||||
unit: string,
|
||||
userId: string,
|
||||
) {
|
||||
const user = await this.userService.findById(userId);
|
||||
return await this.inventoryManager.addInventoryToKitchen(
|
||||
kitchenId,
|
||||
inventoryId,
|
||||
quantity,
|
||||
unit,
|
||||
{ name: user.name, email: user.email, role: user.role?.name },
|
||||
);
|
||||
}
|
||||
|
||||
async deleteFromKitchen(id: string, userId: string) {
|
||||
const item = await this.kitchenInventoryRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
if (!item) {
|
||||
throw new BadRequestException('Kitchen Inventory Item not found');
|
||||
}
|
||||
|
||||
await this.kitchenInventoryRepository.remove(item);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Deleted kitchen inventory item',
|
||||
KitchenInventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Deleted ${item.inventory.name} from ${item.kitchen.name}`,
|
||||
inventoryId: item.inventory.id,
|
||||
kitchenId: item.kitchen.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async addMultipleInventoriesToKitchen(
|
||||
kitchenId: string,
|
||||
inventories: {
|
||||
inventoryId: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
}[],
|
||||
userId: string,
|
||||
) {
|
||||
const user = await this.userService.findById(userId);
|
||||
try {
|
||||
return await this.inventoryManager.addMultipleInventoriesToKitchen(
|
||||
kitchenId,
|
||||
inventories,
|
||||
{ name: user.name, email: user.email, role: user.role?.name },
|
||||
);
|
||||
} catch (error) {
|
||||
throw new BadRequestException(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async transferInventory(
|
||||
fromKitchenId: string,
|
||||
toKitchenId: string,
|
||||
inventoryId: string,
|
||||
quantity: number,
|
||||
unit: string,
|
||||
reason: string,
|
||||
userId: string,
|
||||
) {
|
||||
if (fromKitchenId === toKitchenId) {
|
||||
throw new BadRequestException(
|
||||
'Cannot transfer inventory within the same kitchen.',
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
const fromKitchen = await this.kitchenService.findKitchen(fromKitchenId);
|
||||
const toKitchen = await this.kitchenService.findKitchen(toKitchenId);
|
||||
const inventory = await this.inventoryService.findOne(inventoryId);
|
||||
|
||||
if (!fromKitchen || !toKitchen || !inventory) {
|
||||
throw new NotFoundException('Kitchen or Inventory not found');
|
||||
}
|
||||
|
||||
const fromKitchenInventory = await this.kitchenInventoryRepository.findOne({
|
||||
where: {
|
||||
kitchen: { id: fromKitchen.id },
|
||||
inventory: { id: inventory.id },
|
||||
},
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
|
||||
if (!fromKitchenInventory || fromKitchenInventory.stock < quantity) {
|
||||
throw new BadRequestException('Not enough stock in source kitchen.');
|
||||
}
|
||||
|
||||
fromKitchenInventory.stock -= quantity;
|
||||
await this.kitchenInventoryRepository.save(fromKitchenInventory);
|
||||
|
||||
let toKitchenInventory = await this.kitchenInventoryRepository.findOne({
|
||||
where: { kitchen: { id: toKitchen.id }, inventory: { id: inventory.id } },
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
|
||||
await this.kitchenInventoryRepository.save({
|
||||
id: toKitchenInventory?.id,
|
||||
kitchen: toKitchen,
|
||||
inventory,
|
||||
stock: Number(toKitchenInventory?.stock ?? 0) + Number(quantity),
|
||||
unit,
|
||||
});
|
||||
|
||||
const transfer = this.kitchenTransferRepository.create({
|
||||
fromKitchen,
|
||||
toKitchen,
|
||||
inventory,
|
||||
quantity,
|
||||
unit,
|
||||
reason,
|
||||
user,
|
||||
});
|
||||
|
||||
await this.kitchenTransferRepository.save(transfer);
|
||||
|
||||
await this.logService.logInfo(
|
||||
'Inventory transferred between kitchens',
|
||||
KitchenInventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Transferred ${quantity} ${unit} of ${inventory.name} from ${fromKitchen.name} to ${toKitchen.name}`,
|
||||
fromKitchenId,
|
||||
toKitchenId,
|
||||
inventoryId,
|
||||
quantity,
|
||||
unit,
|
||||
reason,
|
||||
},
|
||||
);
|
||||
|
||||
return transfer;
|
||||
}
|
||||
|
||||
async modifyKitchenStock(
|
||||
kitchenId: string,
|
||||
inventoryId: string,
|
||||
quantity: number,
|
||||
userId: string,
|
||||
) {
|
||||
const item = await this.kitchenInventoryRepository.findOne({
|
||||
where: {
|
||||
kitchen: { id: kitchenId },
|
||||
inventory: { id: inventoryId },
|
||||
},
|
||||
relations: ['kitchen', 'inventory'],
|
||||
});
|
||||
|
||||
if (!item || item.stock < quantity) {
|
||||
throw new BadRequestException(
|
||||
`Not enough ${item?.inventory.name ?? 'inventory'} in kitchen`,
|
||||
);
|
||||
}
|
||||
|
||||
item.stock -= quantity;
|
||||
await this.kitchenInventoryRepository.save(item);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Kitchen inventory stock modified',
|
||||
KitchenInventoryService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Used ${quantity} ${item.unit} of ${item.inventory.name} from ${item.kitchen.name}`,
|
||||
kitchenId,
|
||||
inventoryId,
|
||||
quantity,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/kitchen_inventory/kitchen_transfer.entity.ts
Normal file
33
src/kitchen_inventory/kitchen_transfer.entity.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Entity, ManyToOne, Column } from 'typeorm';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Kitchen } from '../kitchen/kitchen.entity';
|
||||
import { Inventory } from 'src/inventory/inventory.entity';
|
||||
import { User } from 'src/user/user.entity';
|
||||
|
||||
@Entity()
|
||||
export class KitchenTransfer extends BaseEntity {
|
||||
@ManyToOne(() => Kitchen, (kitchen) => kitchen.transfersFrom, {
|
||||
nullable: false,
|
||||
})
|
||||
fromKitchen: Kitchen;
|
||||
|
||||
@ManyToOne(() => Kitchen, (kitchen) => kitchen.transfersTo, {
|
||||
nullable: false,
|
||||
})
|
||||
toKitchen: Kitchen;
|
||||
|
||||
@ManyToOne(() => Inventory, { nullable: false })
|
||||
inventory: Inventory;
|
||||
|
||||
@Column({ type: 'decimal' })
|
||||
quantity: number;
|
||||
|
||||
@Column()
|
||||
unit: string;
|
||||
|
||||
@Column()
|
||||
reason: string;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.transfers, { nullable: false })
|
||||
user: User;
|
||||
}
|
||||
58
src/log/log.controller.ts
Normal file
58
src/log/log.controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { LogService } from './log.service';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Controller('logs')
|
||||
export class LogsController {
|
||||
constructor(
|
||||
private readonly logsService: LogService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Get('app')
|
||||
async getAppLogs(@Query('limit') limit?: number) {
|
||||
return this.logsService.getAppLogs(limit || 50);
|
||||
}
|
||||
|
||||
@Get('errors')
|
||||
async getErrorLogs(@Query('limit') limit?: number) {
|
||||
return this.logsService.getErrorLogs(limit || 50);
|
||||
}
|
||||
|
||||
@Get('inventory-transfers')
|
||||
async getInventoryTransfers() {
|
||||
return this.logsService.getTransferLogs();
|
||||
}
|
||||
|
||||
@Get('by-context')
|
||||
async getLogsByContext(
|
||||
@Query('context') context: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.logsService.getLogsByContext(context, limit || 50);
|
||||
}
|
||||
|
||||
@Get('by-user')
|
||||
async getLogsByUser(
|
||||
@Query('user') user: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.logsService.getLogsByUser(user, limit || 50);
|
||||
}
|
||||
|
||||
@Get('by-metadata')
|
||||
async getLogsByMetadata(
|
||||
@Query('key') key: string,
|
||||
@Query('value') value: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.logsService.getLogsByMetadataKey(key, value, limit || 50);
|
||||
}
|
||||
|
||||
@Get('by-kitchen')
|
||||
async getLogsByKitchen(@CurrentUser() user, @Query('limit') limit?: number) {
|
||||
const app_user = await this.userService.findById(user.sub);
|
||||
return this.logsService.getLogsByKitchen(app_user.id);
|
||||
}
|
||||
}
|
||||
20
src/log/log.module.ts
Normal file
20
src/log/log.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { LogService } from './log.service';
|
||||
import { LogsController } from './log.controller';
|
||||
import { ErrorLogSchema, Log, LogSchema } from './log.schema';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{ name: Log.name, schema: LogSchema, collection: 'app_logs' },
|
||||
{ name: 'ErrorLog', schema: ErrorLogSchema, collection: 'error_logs' },
|
||||
]),
|
||||
],
|
||||
providers: [LogService],
|
||||
controllers: [LogsController],
|
||||
exports: [LogService],
|
||||
})
|
||||
export class LogModule {}
|
||||
32
src/log/log.schema.ts
Normal file
32
src/log/log.schema.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
export type LogDocument = Log & Document;
|
||||
|
||||
@Schema({ collection: 'app_logs', timestamps: true })
|
||||
export class Log {
|
||||
@Prop({ required: true })
|
||||
level: string;
|
||||
|
||||
@Prop({ required: true })
|
||||
message: string;
|
||||
|
||||
@Prop()
|
||||
context: string;
|
||||
|
||||
@Prop({ type: Object })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@Prop()
|
||||
stack?: string;
|
||||
|
||||
@Prop({ default: Date.now })
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const LogSchema = SchemaFactory.createForClass(Log);
|
||||
|
||||
@Schema({ collection: 'error_logs', timestamps: true })
|
||||
export class ErrorLog extends Log {}
|
||||
|
||||
export const ErrorLogSchema = SchemaFactory.createForClass(ErrorLog);
|
||||
109
src/log/log.service.ts
Normal file
109
src/log/log.service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Log, LogDocument } from './log.schema';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
constructor(
|
||||
@InjectModel(Log.name) private logModel: Model<LogDocument>,
|
||||
@InjectModel('ErrorLog') private errorLogModel: Model<LogDocument>,
|
||||
) {}
|
||||
|
||||
async logInfo(
|
||||
message: string,
|
||||
context: string,
|
||||
metadata: Record<string, any> = {},
|
||||
): Promise<void> {
|
||||
const log = new this.logModel({
|
||||
level: 'info',
|
||||
message,
|
||||
context,
|
||||
metadata,
|
||||
});
|
||||
await log.save();
|
||||
}
|
||||
|
||||
async logWarn(
|
||||
message: string,
|
||||
context: string,
|
||||
metadata: Record<string, any> = {},
|
||||
): Promise<void> {
|
||||
const log = new this.logModel({
|
||||
level: 'warn',
|
||||
message,
|
||||
context,
|
||||
metadata,
|
||||
});
|
||||
await log.save();
|
||||
}
|
||||
|
||||
async logError(
|
||||
message: string,
|
||||
context: string,
|
||||
stack?: string,
|
||||
metadata: Record<string, any> = {},
|
||||
): Promise<void> {
|
||||
const errorLog = new this.errorLogModel({
|
||||
level: 'error',
|
||||
message,
|
||||
context,
|
||||
stack,
|
||||
metadata,
|
||||
});
|
||||
await errorLog.save();
|
||||
}
|
||||
|
||||
async getAppLogs(limit = 50) {
|
||||
return this.logModel.find().sort({ timestamp: -1 }).limit(limit).exec();
|
||||
}
|
||||
|
||||
async getErrorLogs(limit = 50) {
|
||||
return this.errorLogModel
|
||||
.find()
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async getLogsByContext(context: string, limit = 50) {
|
||||
return this.logModel
|
||||
.find({ context })
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async getLogsByUser(user: string, limit = 50) {
|
||||
return this.logModel
|
||||
.find({ 'metadata.user': user })
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async getLogsByMetadataKey(
|
||||
key: string,
|
||||
value: string,
|
||||
limit = 50,
|
||||
): Promise<LogDocument[]> {
|
||||
return this.logModel
|
||||
.find({ [`metadata.${key}`]: value })
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async getLogsByKitchen(user: string, limit = 50): Promise<LogDocument[]> {
|
||||
return this.logModel
|
||||
.find({ [`metadata.user.role`]: 'Cook' })
|
||||
.sort({ timestamp: -1 })
|
||||
.limit(limit)
|
||||
.exec();
|
||||
}
|
||||
|
||||
async getTransferLogs() {
|
||||
return this.getLogsByContext('InventoryTransfer');
|
||||
}
|
||||
}
|
||||
28
src/main.ts
Normal file
28
src/main.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe, VersioningType } from '@nestjs/common';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
prefix: 'v',
|
||||
defaultVersion: '1',
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.enableCors();
|
||||
} else {
|
||||
app.enableCors({
|
||||
origin: 'https://inv-hb.thewired.agency',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
credentials: true,
|
||||
allowedHeaders: 'Content-Type,Authorization',
|
||||
});
|
||||
}
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.use(cookieParser());
|
||||
|
||||
await app.listen(process.env.PORT ?? 3001);
|
||||
}
|
||||
bootstrap();
|
||||
21
src/menu/dto/create-menu.ts
Normal file
21
src/menu/dto/create-menu.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsDate,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { FoodType, UserTypes } from '../menu.entity';
|
||||
|
||||
export class CreateMenuDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsArray()
|
||||
@IsEnum(FoodType, { each: true })
|
||||
type: FoodType[];
|
||||
|
||||
@IsDateString()
|
||||
publishDate: Date;
|
||||
}
|
||||
13
src/menu/dto/update-menu.ts
Normal file
13
src/menu/dto/update-menu.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsOptional, IsString, IsArray, IsEnum } from 'class-validator';
|
||||
import { FoodType, UserTypes } from '../menu.entity';
|
||||
|
||||
export class UpdateMenuDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsEnum(FoodType, { each: true })
|
||||
type?: FoodType[];
|
||||
}
|
||||
78
src/menu/menu.controller.ts
Normal file
78
src/menu/menu.controller.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { MenuService } from './menu.service';
|
||||
import { Menu } from './menu.entity';
|
||||
import { CreateMenuDto } from './dto/create-menu';
|
||||
import { UpdateMenuDto } from './dto/update-menu';
|
||||
import { Roles } from 'src/auth/decorator/roles.decorator';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
@Controller('menu')
|
||||
export class MenuController {
|
||||
constructor(private readonly menuService: MenuService) {}
|
||||
|
||||
@Post()
|
||||
@Roles('Admin')
|
||||
async create(
|
||||
@CurrentUser() user,
|
||||
@Body() createMenuDto: CreateMenuDto,
|
||||
): Promise<void> {
|
||||
this.menuService.create(createMenuDto, `${user.name} | ${user.email}`);
|
||||
}
|
||||
|
||||
@Get('today')
|
||||
async getMenu() {
|
||||
return this.menuService.getMenu();
|
||||
}
|
||||
|
||||
@Get('today/user')
|
||||
async getTodayMenu() {
|
||||
return this.menuService.getTodayMenu();
|
||||
}
|
||||
|
||||
@Get('past')
|
||||
@Roles('Admin')
|
||||
async getPastMenu(@Query('mark') mark: string) {
|
||||
const markInactive = mark === 'true';
|
||||
return this.menuService.getPastMenus(markInactive);
|
||||
}
|
||||
|
||||
@Get('upcoming')
|
||||
@Roles('Admin')
|
||||
async getUpcomingMenu() {
|
||||
return this.menuService.getUpComingMenus();
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll(@Query('isActive') isActive: boolean = true): Promise<Menu[]> {
|
||||
return this.menuService.getMenu();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string): Promise<Menu> {
|
||||
return this.menuService.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Roles('Admin')
|
||||
async update(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() updateMenuDto: UpdateMenuDto,
|
||||
): Promise<void> {
|
||||
this.menuService.update(id, updateMenuDto, `${user.name} | ${user.email}`);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('Admin')
|
||||
async delete(@CurrentUser() user, @Param('id') id: string): Promise<void> {
|
||||
return this.menuService.delete(id, user.sub);
|
||||
}
|
||||
}
|
||||
32
src/menu/menu.entity.ts
Normal file
32
src/menu/menu.entity.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { BaseEntity } from 'src/common/base_entity';
|
||||
import { Column, Entity } from 'typeorm';
|
||||
|
||||
export enum FoodType {
|
||||
BREAKFAST = 'BREAKFAST',
|
||||
LUNCH = 'LUNCH',
|
||||
DINNER = 'DINNER',
|
||||
SNACKS = 'SNACKS',
|
||||
}
|
||||
|
||||
export enum UserTypes {
|
||||
JAINS = 'JAINS',
|
||||
DASAS = 'DASAS',
|
||||
INDIAN_GUESTS = 'INDIAN GUESTS',
|
||||
INTERNATIONAL_GUESTS = 'INTERNATIONAL_GUESTS',
|
||||
STAFF = 'STAFF',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Menu extends BaseEntity {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'simple-array' })
|
||||
type: FoodType[];
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@Column()
|
||||
publishDate: Date;
|
||||
}
|
||||
12
src/menu/menu.module.ts
Normal file
12
src/menu/menu.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuController } from './menu.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Menu } from './menu.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Menu])],
|
||||
controllers: [MenuController],
|
||||
providers: [MenuService],
|
||||
})
|
||||
export class MenuModule {}
|
||||
180
src/menu/menu.service.ts
Normal file
180
src/menu/menu.service.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
Between,
|
||||
LessThan,
|
||||
MoreThan,
|
||||
MoreThanOrEqual,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { Menu } from './menu.entity';
|
||||
import { CreateMenuDto } from './dto/create-menu';
|
||||
import { UpdateMenuDto } from './dto/update-menu';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class MenuService {
|
||||
static IST_TIMEZONE = 'Asia/Kolkata';
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Menu)
|
||||
private readonly menuRepository: Repository<Menu>,
|
||||
private readonly logService: LogService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async create(menuData: CreateMenuDto, userId: string): Promise<void> {
|
||||
const menu = this.menuRepository.create(menuData);
|
||||
await this.menuRepository.save(menu);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Menu created', MenuService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Added menu item: ${menu.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(isActive: boolean): Promise<Menu[]> {
|
||||
return this.menuRepository.find({ where: { isActive } });
|
||||
}
|
||||
|
||||
async getPastMenus(markInactive = false): Promise<Menu[]> {
|
||||
const now = new Date();
|
||||
const todayStartIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 00:00:00.000',
|
||||
);
|
||||
const todayStartUTC = new Date(`${todayStartIST} GMT+0530`);
|
||||
|
||||
const pastMenus = await this.menuRepository.find({
|
||||
where: { publishDate: LessThan(todayStartUTC) },
|
||||
});
|
||||
|
||||
if (markInactive) {
|
||||
const toUpdate = pastMenus.filter((m) => m.isActive);
|
||||
for (const menu of toUpdate) {
|
||||
menu.isActive = false;
|
||||
}
|
||||
await this.menuRepository.save(toUpdate);
|
||||
|
||||
await this.logService.logInfo(
|
||||
'Marked past menus inactive',
|
||||
MenuService.name,
|
||||
{
|
||||
activity: `Marked ${toUpdate.length} past menu(s) as inactive`,
|
||||
count: toUpdate.length,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return pastMenus;
|
||||
}
|
||||
async getUpComingMenus(): Promise<Menu[]> {
|
||||
const now = new Date();
|
||||
const todayStartIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 00:00:00.000',
|
||||
);
|
||||
const todayStartUTC = new Date(`${todayStartIST} GMT+0530`);
|
||||
const todayEndIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 23:59:59.999',
|
||||
);
|
||||
|
||||
const todayEndUTC = new Date(todayEndIST + ' GMT+0530');
|
||||
const upcomingMenus = await this.menuRepository.find({
|
||||
where: { publishDate: MoreThan(todayEndUTC) },
|
||||
});
|
||||
|
||||
return upcomingMenus;
|
||||
}
|
||||
|
||||
async getTodayMenu(): Promise<Menu[]> {
|
||||
const now = new Date();
|
||||
const todayStartIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 00:00:00',
|
||||
);
|
||||
const todayEndIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 23:59:59.999',
|
||||
);
|
||||
|
||||
const todayStartUTC = new Date(todayStartIST + ' GMT+0530');
|
||||
const todayEndUTC = new Date(todayEndIST + ' GMT+0530');
|
||||
|
||||
return await this.menuRepository.find({
|
||||
where: {
|
||||
publishDate: Between(todayStartUTC, todayEndUTC),
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getMenu(): Promise<Menu[]> {
|
||||
const now = new Date();
|
||||
const todayStartIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 00:00:00',
|
||||
);
|
||||
const todayEndIST = formatInTimeZone(
|
||||
now,
|
||||
MenuService.IST_TIMEZONE,
|
||||
'yyyy-MM-dd 23:59:59.999',
|
||||
);
|
||||
|
||||
const todayStartUTC = new Date(todayStartIST + ' GMT+0530');
|
||||
const todayEndUTC = new Date(todayEndIST + ' GMT+0530');
|
||||
|
||||
return await this.menuRepository.find({
|
||||
where: {
|
||||
publishDate: MoreThanOrEqual(todayStartUTC),
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Menu> {
|
||||
const menu = await this.menuRepository.findOne({ where: { id } });
|
||||
if (!menu) {
|
||||
throw new BadRequestException('Menu item not found');
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
menuData: UpdateMenuDto,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const menu = await this.findOne(id);
|
||||
const mergedMenu = this.menuRepository.merge(menu, menuData);
|
||||
await this.menuRepository.save(mergedMenu);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Menu updated', MenuService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Updated menu item: ${menu.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string, userId: string): Promise<void> {
|
||||
const menu = await this.findOne(id);
|
||||
menu.isActive = false;
|
||||
await this.menuRepository.save(menu);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Menu deleted (soft)', MenuService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Soft deleted menu item: ${menu.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/order/dto/create-order.dto.ts
Normal file
24
src/order/dto/create-order.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IsEnum, IsNotEmpty, IsObject, IsString } from 'class-validator';
|
||||
import { OrderStatus } from '../entities/order.entity';
|
||||
import { FoodType, UserTypes } from 'src/menu/menu.entity';
|
||||
|
||||
export class CreateOrderDto {
|
||||
@IsEnum(FoodType)
|
||||
foodType: FoodType;
|
||||
|
||||
@IsObject()
|
||||
@IsNotEmpty()
|
||||
orderDetails: {
|
||||
[key in UserTypes]?: {
|
||||
items: {
|
||||
menu: string;
|
||||
count: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class UpdateOrderDto {
|
||||
@IsEnum(OrderStatus)
|
||||
status: OrderStatus;
|
||||
}
|
||||
34
src/order/dto/update-progress.dto.ts
Normal file
34
src/order/dto/update-progress.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { OrderStatus } from '../entities/order.entity';
|
||||
|
||||
class IngredientUsageDto {
|
||||
@IsString()
|
||||
inventoryId: string;
|
||||
|
||||
quantity: number;
|
||||
|
||||
@IsString()
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class UpdateOrderProgressDto {
|
||||
@IsEnum(OrderStatus)
|
||||
status: OrderStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => IngredientUsageDto)
|
||||
usedIngredients?: IngredientUsageDto[];
|
||||
}
|
||||
60
src/order/entities/order.entity.ts
Normal file
60
src/order/entities/order.entity.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { BaseEntity } from 'src/common/base_entity';
|
||||
import { IngredientUsageLog } from 'src/ingredient-usage/entities/ingredient-usage.entity';
|
||||
import { Kitchen } from 'src/kitchen/kitchen.entity';
|
||||
import { FoodType, UserTypes } from 'src/menu/menu.entity';
|
||||
import { User } from 'src/user/user.entity';
|
||||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||
|
||||
export enum OrderStatus {
|
||||
PENDING = 'PENDING',
|
||||
ACCEPTED = 'ACCEPTED',
|
||||
PREPARING = 'PREPARING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
DELIVERED = 'DELIVERED',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Order extends BaseEntity {
|
||||
@ManyToOne(() => User, (user) => user.orders, { nullable: false })
|
||||
user: User;
|
||||
|
||||
@ManyToOne(() => Kitchen, { nullable: true })
|
||||
kitchen: Kitchen | null;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: FoodType,
|
||||
})
|
||||
foodType: FoodType;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({
|
||||
type: 'json',
|
||||
nullable: false,
|
||||
})
|
||||
orderDetails: {
|
||||
[key in UserTypes]?: {
|
||||
items: {
|
||||
menu: string;
|
||||
count: number;
|
||||
description: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
@Column({ default: false })
|
||||
isAccepted: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: OrderStatus,
|
||||
default: OrderStatus.PENDING,
|
||||
})
|
||||
status: OrderStatus;
|
||||
|
||||
@OneToMany(() => IngredientUsageLog, (log) => log.order)
|
||||
ingredientUsageLogs: IngredientUsageLog[];
|
||||
}
|
||||
7
src/order/order.constants.ts
Normal file
7
src/order/order.constants.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const ORDER_TRANSITIONS = {
|
||||
PENDING: ['ACCEPTED'],
|
||||
ACCEPTED: ['PREPARING', 'CANCELLED'],
|
||||
PREPARING: ['COMPLETED', 'CANCELLED'],
|
||||
COMPLETED: ['DELIVERED'],
|
||||
CANCELLED: [],
|
||||
};
|
||||
115
src/order/order.controller.ts
Normal file
115
src/order/order.controller.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { OrderService } from './order.service';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { Order } from './entities/order.entity';
|
||||
import { Roles } from 'src/auth/decorator/roles.decorator';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
import { UpdateOrderProgressDto } from './dto/update-progress.dto';
|
||||
import { Public } from 'src/auth/decorator/public.decorator';
|
||||
|
||||
@Controller('order')
|
||||
export class OrderController {
|
||||
constructor(private readonly orderService: OrderService) {}
|
||||
|
||||
@Post()
|
||||
@Roles('User')
|
||||
async create(
|
||||
@CurrentUser() user,
|
||||
@Body() createOrderDto: CreateOrderDto,
|
||||
): Promise<Order> {
|
||||
return await this.orderService.createOrder(user.sub, createOrderDto);
|
||||
}
|
||||
|
||||
@Get('all-stats')
|
||||
@Roles('Admin')
|
||||
async getOrderStats() {
|
||||
return await this.orderService.getOrderCountsByStatus();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('Admin', 'Cook')
|
||||
async findAll(): Promise<Order[]> {
|
||||
return await this.orderService.getAllOrders();
|
||||
}
|
||||
|
||||
@Get('mine')
|
||||
@Roles('User', 'Cook')
|
||||
async getUserOrders(@CurrentUser() user) {
|
||||
return await this.orderService.getOrdersByUserId(user.sub);
|
||||
}
|
||||
|
||||
@Get('mine/history')
|
||||
@Roles('User', 'Cook')
|
||||
async getUserOrdersHisotry(@CurrentUser() user) {
|
||||
return await this.orderService.getUserCompletedOrders(user.sub);
|
||||
}
|
||||
|
||||
@Get('accepted')
|
||||
@Roles('Cook')
|
||||
async getAcceptedOrdersByKitchen(@CurrentUser() user) {
|
||||
console.log('sub', user.id);
|
||||
return await this.orderService.getOrdersByKitchenCook(user.sub);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string): Promise<Order | null> {
|
||||
return await this.orderService.getOrderById(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles('Admin', 'Cook')
|
||||
async updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() updateOrderDto: UpdateOrderProgressDto,
|
||||
): Promise<Order> {
|
||||
return await this.orderService.updateOrderStatusById(
|
||||
id,
|
||||
updateOrderDto.status,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/accept')
|
||||
@Roles('Admin', 'Cook')
|
||||
async acceptOrder(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
): Promise<Order> {
|
||||
return await this.orderService.acceptOrderByCook(id, user.sub);
|
||||
}
|
||||
|
||||
@Patch(':id/accept/:kitchenId')
|
||||
@Roles('Admin')
|
||||
async assignOrderToKitchen(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Param('kitchenId') kitchenId: string,
|
||||
): Promise<Order> {
|
||||
return await this.orderService.assignOrderToKitchen(
|
||||
id,
|
||||
user.sub,
|
||||
kitchenId,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/progress')
|
||||
@Roles('Cook')
|
||||
async updateOrderProgress(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() progressDto: UpdateOrderProgressDto,
|
||||
): Promise<Order> {
|
||||
return await this.orderService.updateOrderProgressByCook(
|
||||
id,
|
||||
user.sub,
|
||||
progressDto,
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/order/order.module.ts
Normal file
28
src/order/order.module.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OrderService } from './order.service';
|
||||
import { OrderController } from './order.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Order } from './entities/order.entity';
|
||||
import { KitchenModule } from 'src/kitchen/kitchen.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { MenuModule } from 'src/menu/menu.module';
|
||||
import { IngredientUsageController } from 'src/ingredient-usage/ingredient-usage.controller';
|
||||
import { IngredientUsageModule } from 'src/ingredient-usage/ingredient-usage.module';
|
||||
import { InventoryModule } from 'src/inventory/inventory.module';
|
||||
import { KitchenInventoryModule } from 'src/kitchen_inventory/kitchen_inventory.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Order]),
|
||||
KitchenModule,
|
||||
UserModule,
|
||||
InventoryModule,
|
||||
MenuModule,
|
||||
KitchenInventoryModule,
|
||||
IngredientUsageModule,
|
||||
],
|
||||
controllers: [OrderController],
|
||||
providers: [OrderService],
|
||||
exports: [OrderService],
|
||||
})
|
||||
export class OrderModule {}
|
||||
296
src/order/order.service.ts
Normal file
296
src/order/order.service.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Not, Repository } from 'typeorm';
|
||||
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { Order, OrderStatus } from './entities/order.entity';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { KitchenService } from 'src/kitchen/kitchen.service';
|
||||
import { InventoryService } from 'src/inventory/inventory.service';
|
||||
import { IngredientUsageService } from 'src/ingredient-usage/ingredient-usage.service';
|
||||
import { KitchenInventoryService } from 'src/kitchen_inventory/kitchen_inventory.service';
|
||||
import { UpdateOrderProgressDto } from './dto/update-progress.dto';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
|
||||
const ORDER_TRANSITIONS = {
|
||||
PENDING: ['ACCEPTED'],
|
||||
ACCEPTED: ['PREPARING', 'CANCELLED'],
|
||||
PREPARING: ['COMPLETED', 'CANCELLED', 'PREPARING'],
|
||||
COMPLETED: ['DELIVERED'],
|
||||
CANCELLED: [],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly kitchenService: KitchenService,
|
||||
private readonly inventoryService: InventoryService,
|
||||
private readonly kitchenInventoryService: KitchenInventoryService,
|
||||
private readonly ingredientUsageService: IngredientUsageService,
|
||||
private readonly logService: LogService,
|
||||
@InjectRepository(Order)
|
||||
private readonly orderRepository: Repository<Order>,
|
||||
) {}
|
||||
|
||||
async createOrder(
|
||||
userId: string,
|
||||
createOrderDto: CreateOrderDto,
|
||||
): Promise<Order> {
|
||||
const user = await this.userService.findById(userId);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
const order = this.orderRepository.create({
|
||||
user,
|
||||
foodType: createOrderDto.foodType,
|
||||
orderDetails: createOrderDto.orderDetails,
|
||||
status: OrderStatus.PENDING,
|
||||
isAccepted: false,
|
||||
});
|
||||
|
||||
const saved = await this.orderRepository.save(order);
|
||||
|
||||
await this.logService.logInfo('Order created', OrderService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Created order with ID ${saved.id}`,
|
||||
});
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
async acceptOrderByCook(orderId: string, userId: string): Promise<Order> {
|
||||
const order = await this.orderRepository.findOne({
|
||||
where: { id: orderId },
|
||||
});
|
||||
if (!order) throw new NotFoundException('Order not found');
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
if (user.role.name !== 'Cook')
|
||||
throw new BadRequestException('User is not a cook');
|
||||
|
||||
const kitchenId = user.kitchen?.id;
|
||||
if (!kitchenId)
|
||||
throw new BadRequestException('User is not associated with a kitchen');
|
||||
|
||||
const kitchen = await this.kitchenService.findKitchen(kitchenId);
|
||||
if (!kitchen) throw new NotFoundException('Kitchen not found');
|
||||
|
||||
order.kitchen = kitchen;
|
||||
order.isAccepted = true;
|
||||
order.status = OrderStatus.ACCEPTED;
|
||||
|
||||
const updated = await this.orderRepository.save(order);
|
||||
|
||||
await this.logService.logInfo('Order accepted by cook', OrderService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Cook ${user.name} accepted order ${order.id}`,
|
||||
});
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async assignOrderToKitchen(
|
||||
orderId: string,
|
||||
userId: string,
|
||||
kitchenId: string,
|
||||
): Promise<Order> {
|
||||
const order = await this.orderRepository.findOne({
|
||||
where: { id: orderId },
|
||||
});
|
||||
if (!order) throw new NotFoundException('Order not found');
|
||||
|
||||
const kitchen = await this.kitchenService.findKitchen(kitchenId);
|
||||
if (!kitchen) throw new NotFoundException('Kitchen not found');
|
||||
|
||||
order.kitchen = kitchen;
|
||||
order.isAccepted = true;
|
||||
order.status = OrderStatus.ACCEPTED;
|
||||
|
||||
const updated = await this.orderRepository.save(order);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo(
|
||||
'Order manually assigned to kitchen',
|
||||
OrderService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Order ${order.id} assigned to ${kitchen.name}`,
|
||||
},
|
||||
);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async updateOrderProgressByCook(
|
||||
orderId: string,
|
||||
userId: string,
|
||||
dto: UpdateOrderProgressDto,
|
||||
): Promise<Order> {
|
||||
const order = await this.orderRepository.findOne({
|
||||
where: { id: orderId },
|
||||
relations: ['kitchen'],
|
||||
});
|
||||
if (!order) throw new NotFoundException('Order not found');
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
if (!user || user.role.name !== 'Cook') {
|
||||
throw new BadRequestException('Only cooks can update orders');
|
||||
}
|
||||
|
||||
const kitchenId = user.kitchen?.id;
|
||||
if (!kitchenId || kitchenId !== order.kitchen?.id) {
|
||||
throw new BadRequestException('User not authorized for this order');
|
||||
}
|
||||
|
||||
const allowedTransitions = ORDER_TRANSITIONS[order.status];
|
||||
if (!allowedTransitions.includes(dto.status)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid transition from ${order.status} to ${dto.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
order.status = dto.status;
|
||||
if (dto.description) {
|
||||
order.description = dto.description;
|
||||
}
|
||||
|
||||
if (dto.usedIngredients?.length) {
|
||||
for (const usage of dto.usedIngredients) {
|
||||
const inventory = await this.inventoryService.findOne(
|
||||
usage.inventoryId,
|
||||
);
|
||||
if (!inventory) {
|
||||
throw new NotFoundException(
|
||||
`Inventory item ${usage.inventoryId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.kitchenInventoryService.modifyKitchenStock(
|
||||
kitchenId,
|
||||
usage.inventoryId,
|
||||
usage.quantity,
|
||||
userId,
|
||||
);
|
||||
|
||||
await this.ingredientUsageService.logUsage(
|
||||
order,
|
||||
order.kitchen,
|
||||
inventory,
|
||||
usage.quantity,
|
||||
user.id,
|
||||
usage.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await this.orderRepository.save(order);
|
||||
|
||||
await this.logService.logInfo(
|
||||
'Order progress updated by cook',
|
||||
OrderService.name,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Order ${order.id} marked as ${order.status}`,
|
||||
},
|
||||
);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
async updateOrderStatusById(
|
||||
orderId: string,
|
||||
status: OrderStatus,
|
||||
): Promise<Order> {
|
||||
const order = await this.orderRepository.findOne({
|
||||
where: { id: orderId },
|
||||
});
|
||||
if (!order) throw new NotFoundException('Order not found');
|
||||
|
||||
order.status = status;
|
||||
return this.orderRepository.save(order);
|
||||
}
|
||||
|
||||
async getOrderById(orderId: string): Promise<Order | null> {
|
||||
return this.orderRepository.findOne({
|
||||
where: { id: orderId },
|
||||
relations: [
|
||||
'user',
|
||||
'kitchen',
|
||||
'ingredientUsageLogs',
|
||||
'ingredientUsageLogs.inventory',
|
||||
'ingredientUsageLogs.user',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getOrdersByUserId(userId: string): Promise<Order[]> {
|
||||
return this.orderRepository.find({
|
||||
where: { user: { id: userId } },
|
||||
order: { updated_at: 'DESC' },
|
||||
relations: ['kitchen'],
|
||||
});
|
||||
}
|
||||
|
||||
async getUserCompletedOrders(userId: string): Promise<Order[]> {
|
||||
return await this.orderRepository.find({
|
||||
where: {
|
||||
user: { id: userId },
|
||||
status: In([OrderStatus.CANCELLED, OrderStatus.COMPLETED]),
|
||||
},
|
||||
order: { updated_at: 'DESC' },
|
||||
relations: ['kitchen'],
|
||||
});
|
||||
}
|
||||
|
||||
async getAllOrders(): Promise<Order[]> {
|
||||
return this.orderRepository.find({
|
||||
relations: ['user', 'kitchen'],
|
||||
order: { updated_at: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getOrdersByKitchenCook(userId: string): Promise<Order[]> {
|
||||
const user = await this.userService.findById(userId);
|
||||
if (!user.kitchen) {
|
||||
throw new BadRequestException('Failed to find kitchen id');
|
||||
}
|
||||
return this.orderRepository.find({
|
||||
where: {
|
||||
kitchen: { id: user.kitchen?.id },
|
||||
isAccepted: true,
|
||||
status: Not(OrderStatus.COMPLETED),
|
||||
},
|
||||
order: { updated_at: 'DESC' },
|
||||
relations: ['kitchen', 'user'],
|
||||
});
|
||||
}
|
||||
|
||||
async getOrderCountsByStatus(): Promise<Record<OrderStatus, number>> {
|
||||
const orders = await this.orderRepository
|
||||
.createQueryBuilder('order')
|
||||
.select('order.status', 'status')
|
||||
.addSelect('COUNT(order.id)', 'count')
|
||||
.groupBy('order.status')
|
||||
.getRawMany();
|
||||
|
||||
const statusCounts: Record<OrderStatus, number> = {
|
||||
[OrderStatus.PENDING]: 0,
|
||||
[OrderStatus.ACCEPTED]: 0,
|
||||
[OrderStatus.PREPARING]: 0,
|
||||
[OrderStatus.COMPLETED]: 0,
|
||||
[OrderStatus.CANCELLED]: 0,
|
||||
[OrderStatus.DELIVERED]: 0,
|
||||
};
|
||||
|
||||
orders.forEach((item) => {
|
||||
statusCounts[item.status as OrderStatus] = parseInt(item.count, 10);
|
||||
});
|
||||
|
||||
return statusCounts;
|
||||
}
|
||||
}
|
||||
12
src/role_manager/role.entity.ts
Normal file
12
src/role_manager/role.entity.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { User } from '../user/user.entity';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Column, Entity, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Column({ unique: true })
|
||||
name: string;
|
||||
|
||||
@OneToMany(() => User, (user) => user.role)
|
||||
users: User[];
|
||||
}
|
||||
11
src/role_manager/role_manager.module.ts
Normal file
11
src/role_manager/role_manager.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RoleManagerService } from './role_manager.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Role } from './role.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Role])],
|
||||
providers: [RoleManagerService],
|
||||
exports: [RoleManagerService],
|
||||
})
|
||||
export class RoleManagerModule {}
|
||||
41
src/role_manager/role_manager.service.ts
Normal file
41
src/role_manager/role_manager.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Role } from './role.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class RoleManagerService {
|
||||
constructor(
|
||||
@InjectRepository(Role) private readonly roleRepository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
async createRole(name: string): Promise<void> {
|
||||
const exists = await this.roleRepository.exists({
|
||||
where: { name },
|
||||
});
|
||||
if (exists) {
|
||||
throw new BadRequestException('Role already exists');
|
||||
}
|
||||
await this.roleRepository.save({ name });
|
||||
}
|
||||
|
||||
async getRoles(): Promise<Role[]> {
|
||||
return this.roleRepository.find();
|
||||
}
|
||||
|
||||
async getRoleById(id: string): Promise<Role> {
|
||||
const role = await this.roleRepository.findOneBy({ id });
|
||||
if (!role) {
|
||||
throw new BadRequestException('Role not found');
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
async deleteRole(id: string) {
|
||||
const role = await this.roleRepository.findOneBy({ id });
|
||||
if (!role) {
|
||||
throw new BadRequestException('Role not found');
|
||||
}
|
||||
await this.roleRepository.delete({ id });
|
||||
}
|
||||
}
|
||||
25
src/user/dto/create-user.ts
Normal file
25
src/user/dto/create-user.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
phone: number;
|
||||
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
roleId: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
kitchenId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userType?: string;
|
||||
}
|
||||
15
src/user/dto/update-user.ts
Normal file
15
src/user/dto/update-user.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: number;
|
||||
}
|
||||
108
src/user/user.controller.ts
Normal file
108
src/user/user.controller.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto } from './dto/create-user';
|
||||
import { UpdateUserDto } from './dto/update-user';
|
||||
import { Public } from '../auth/decorator/public.decorator';
|
||||
import { Role } from '../role_manager/role.entity';
|
||||
import { Roles } from '../auth/decorator/roles.decorator';
|
||||
import { RoleManagerService } from 'src/role_manager/role_manager.service';
|
||||
import { KitchenService } from 'src/kitchen/kitchen.service';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Post()
|
||||
@Roles('Admin')
|
||||
async create(@CurrentUser() user, @Body() createUserDto: CreateUserDto) {
|
||||
try {
|
||||
return this.userService.create(
|
||||
createUserDto,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
@Get('roles')
|
||||
@Roles('Admin')
|
||||
async getRoles() {
|
||||
return await this.userService.getAllRoles();
|
||||
}
|
||||
|
||||
@Get('kitchens')
|
||||
@Roles('Admin')
|
||||
async getKitchens() {
|
||||
return await this.userService.getKitchens();
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
) {
|
||||
return this.userService.update(id, updateUserDto, user.sub);
|
||||
}
|
||||
|
||||
@Patch(':id/pwd')
|
||||
async updatePassword(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() { password }: { password: string },
|
||||
) {
|
||||
await this.userService.updatePassword(
|
||||
id,
|
||||
password,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@CurrentUser() user, @Param('id') id: string) {
|
||||
await this.userService.delete(id, `${user.name} | ${user.email}`);
|
||||
return { message: `User with ID ${id} has been deactivated` };
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('Admin')
|
||||
async findAll(@Query('activeOnly') activeOnly: string) {
|
||||
const isActive = activeOnly !== 'false';
|
||||
return this.userService.findAll(isActive);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findById(@Param('id') id: string) {
|
||||
return this.userService.findById(id);
|
||||
}
|
||||
|
||||
@Get('email/:email')
|
||||
async findByEmail(@Param('email') email: string) {
|
||||
return this.userService.findByEmail(email);
|
||||
}
|
||||
|
||||
@Get('phone/:phone')
|
||||
async findByPhone(@Param('phone') phone: number) {
|
||||
return this.userService.findByPhone(phone);
|
||||
}
|
||||
|
||||
@Get('check/:email')
|
||||
@Public()
|
||||
async checkIfEmailExists(@Param('email') email: string) {
|
||||
return {
|
||||
status: await this.userService.checkIfUserExists(email.toLowerCase()),
|
||||
};
|
||||
}
|
||||
}
|
||||
46
src/user/user.entity.ts
Normal file
46
src/user/user.entity.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Role } from '../role_manager/role.entity';
|
||||
import { Kitchen } from '../kitchen/kitchen.entity';
|
||||
import { KitchenTransfer } from 'src/kitchen_inventory/kitchen_transfer.entity';
|
||||
import { Order } from 'src/order/entities/order.entity';
|
||||
import { IngredientUsageLog } from 'src/ingredient-usage/entities/ingredient-usage.entity';
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
@Column({})
|
||||
name: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
|
||||
@Column({ unique: true, type: 'bigint' })
|
||||
phone: number;
|
||||
|
||||
@Column({ select: false })
|
||||
password: string;
|
||||
|
||||
@ManyToOne(() => Role, (role) => role.users)
|
||||
role: Role;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@ManyToOne(() => Kitchen, (kitchen) => kitchen.users)
|
||||
kitchen?: Kitchen;
|
||||
|
||||
@Column({ nullable: true })
|
||||
userType?: string;
|
||||
|
||||
@OneToMany(() => KitchenTransfer, (transfer) => transfer.user)
|
||||
transfers: KitchenTransfer[];
|
||||
|
||||
@OneToMany(() => Order, (order) => order.user)
|
||||
orders: Order[];
|
||||
|
||||
@OneToMany(
|
||||
() => IngredientUsageLog,
|
||||
(ingredientUsageLog) => ingredientUsageLog.user,
|
||||
)
|
||||
ingredientUsageLogs: IngredientUsageLog[];
|
||||
}
|
||||
20
src/user/user.module.ts
Normal file
20
src/user/user.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { RoleManagerModule } from '../role_manager/role_manager.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { KitchenModule } from 'src/kitchen/kitchen.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
RoleManagerModule,
|
||||
TypeOrmModule.forFeature([User]),
|
||||
KitchenModule,
|
||||
],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
219
src/user/user.service.ts
Normal file
219
src/user/user.service.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { User } from './user.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { RoleManagerService } from '../role_manager/role_manager.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { CreateUserDto } from './dto/create-user';
|
||||
import { UpdateUserDto } from './dto/update-user';
|
||||
import { KitchenService } from '../kitchen/kitchen.service';
|
||||
import { LogService } from '../log/log.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User) private readonly userRepository: Repository<User>,
|
||||
private readonly roleManagerService: RoleManagerService,
|
||||
private readonly kitchenService: KitchenService,
|
||||
private readonly logService: LogService,
|
||||
) {}
|
||||
|
||||
async create(createUserDto: CreateUserDto, performedBy: string) {
|
||||
const role = await this.roleManagerService.getRoleById(
|
||||
createUserDto.roleId,
|
||||
);
|
||||
const { name, email, phone, password, kitchenId, userType } = createUserDto;
|
||||
|
||||
if (!role) {
|
||||
throw new BadRequestException(
|
||||
`Role with ID ${createUserDto.roleId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const appuser = this.userRepository.create({
|
||||
name,
|
||||
email: email.toLowerCase(),
|
||||
phone,
|
||||
password: hashedPassword,
|
||||
role,
|
||||
});
|
||||
|
||||
if (role.name === 'Cook') {
|
||||
appuser.kitchen = await this.kitchenService.findKitchen(
|
||||
kitchenId as string,
|
||||
);
|
||||
}
|
||||
|
||||
if (role.name === 'User') {
|
||||
appuser.userType = userType;
|
||||
}
|
||||
|
||||
try {
|
||||
const { password, ...saved } = await this.userRepository.save(appuser);
|
||||
const user = await this.findById(performedBy);
|
||||
await this.logService.logInfo('New user created', UserService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Created user ${saved.name} (${saved.email})`,
|
||||
});
|
||||
return saved;
|
||||
} catch (error) {
|
||||
const user = await this.findById(performedBy);
|
||||
await this.logService.logError(
|
||||
'Failed to create user',
|
||||
UserService.name,
|
||||
error.stack,
|
||||
{
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Failed to create user ${email}`,
|
||||
},
|
||||
);
|
||||
if (error.code === '23505') {
|
||||
throw new BadRequestException('Email or phone number already exists');
|
||||
}
|
||||
throw new BadRequestException('Failed to create user');
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
userId: string,
|
||||
updateData: UpdateUserDto,
|
||||
performedBy: string,
|
||||
): Promise<User> {
|
||||
const appuser = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: ['role'],
|
||||
});
|
||||
|
||||
if (!appuser) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
|
||||
this.userRepository.merge(appuser, updateData);
|
||||
const updatedUser = await this.userRepository.save(appuser);
|
||||
|
||||
const user = await this.findById(performedBy);
|
||||
await this.logService.logInfo('User updated', UserService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Updated user ${appuser.name}`,
|
||||
});
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async updatePassword(userId: string, password: string, performedBy: string) {
|
||||
const appuser = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!appuser) {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const updated = this.userRepository.merge(appuser, {
|
||||
password: hashedPassword,
|
||||
});
|
||||
await this.userRepository.save(updated);
|
||||
|
||||
const user = await this.findById(performedBy);
|
||||
await this.logService.logInfo('Password updated', UserService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Updated password for ${appuser.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(userId: string, performedBy: string): Promise<void> {
|
||||
const appuser = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!appuser) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
|
||||
appuser.isActive = false;
|
||||
await this.userRepository.save(appuser);
|
||||
|
||||
const user = await this.findById(performedBy);
|
||||
await this.logService.logInfo('User deactivated', UserService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role.name },
|
||||
activity: `Deactivated user ${appuser.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(activeOnly: boolean = true): Promise<User[]> {
|
||||
if (activeOnly) {
|
||||
return await this.userRepository.find({
|
||||
where: { isActive: true },
|
||||
relations: ['role', 'kitchen'],
|
||||
});
|
||||
}
|
||||
return this.userRepository.find({ relations: ['role'] });
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
relations: ['role'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with email ${email} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async findByEmailWithPassword(email: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { email },
|
||||
relations: ['role'],
|
||||
select: ['role', 'password', 'id', 'email', 'phone', 'name'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with email ${email} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async findById(userId: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: ['role', 'kitchen'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async findByPhone(phone: number): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { phone: phone },
|
||||
relations: ['role'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with number ${phone} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async checkIfUserExists(email: string): Promise<boolean> {
|
||||
const user = await this.userRepository.findOne({ where: { email } });
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
return !!user;
|
||||
}
|
||||
|
||||
async getAllRoles() {
|
||||
return await this.roleManagerService.getRoles();
|
||||
}
|
||||
|
||||
async getKitchens() {
|
||||
return await this.kitchenService.getAllKitchens();
|
||||
}
|
||||
}
|
||||
67
src/vendor/dto/create-vendor.ts
vendored
Normal file
67
src/vendor/dto/create-vendor.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsEmail,
|
||||
Matches,
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateVendorDto {
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Name cannot be empty' })
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Store name cannot be empty' })
|
||||
store_name: string;
|
||||
|
||||
@IsString()
|
||||
@Matches(/^\d{10}$/, { message: 'Phone number must be exactly 10 digits' })
|
||||
phone_number: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
gstin?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: 'Invalid email format' })
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
city?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
state?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pincode?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
vendor_code: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bank_name?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
account_number?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
ifsc_code?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
branch_name?: string;
|
||||
}
|
||||
77
src/vendor/dto/update-vendor.ts
vendored
Normal file
77
src/vendor/dto/update-vendor.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsEmail,
|
||||
Matches,
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
import { CreateVendorDto } from './create-vendor';
|
||||
|
||||
export class UpdateVendorDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Name cannot be empty' })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Store name cannot be empty' })
|
||||
store_name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{10}$/, { message: 'Phone number must be exactly 10 digits' })
|
||||
phone_number?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
gstin?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: 'Invalid email format' })
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
city?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
state?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pincode?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
is_active?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: 'Vendor code cannot be empty' })
|
||||
vendor_code?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bank_name?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
account_number?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
ifsc_code?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
branch_name?: string;
|
||||
}
|
||||
58
src/vendor/vendor.controller.ts
vendored
Normal file
58
src/vendor/vendor.controller.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { VendorService } from './vendor.service';
|
||||
import { CreateVendorDto } from './dto/create-vendor';
|
||||
import { Vendor } from './vendor.entity';
|
||||
import { Roles } from 'src/auth/decorator/roles.decorator';
|
||||
import { UpdateVendorDto } from './dto/update-vendor';
|
||||
import { CurrentUser } from 'src/auth/decorator/current_user.decorator';
|
||||
|
||||
@Controller('vendor')
|
||||
export class VendorController {
|
||||
constructor(private readonly vendorService: VendorService) {}
|
||||
|
||||
@Post()
|
||||
@Roles('Admin')
|
||||
async create(
|
||||
@CurrentUser() user,
|
||||
@Body() vendorData: CreateVendorDto,
|
||||
): Promise<void> {
|
||||
await this.vendorService.create(vendorData, `${user.name} | ${user.email}`);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('Admin')
|
||||
async findAll(
|
||||
@Query('inwards') loadInwards: boolean = false,
|
||||
): Promise<Vendor[]> {
|
||||
console.log('load', loadInwards);
|
||||
return this.vendorService.findAll(loadInwards);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles('Admin')
|
||||
async update(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() updateVendorDto: UpdateVendorDto,
|
||||
) {
|
||||
this.vendorService.update(
|
||||
id,
|
||||
updateVendorDto,
|
||||
`${user.name} | ${user.email}`,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles('Admin')
|
||||
async findOne(@Param('id') id: string): Promise<Vendor> {
|
||||
return this.vendorService.findOne(id);
|
||||
}
|
||||
}
|
||||
54
src/vendor/vendor.entity.ts
vendored
Normal file
54
src/vendor/vendor.entity.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Inward } from 'src/inventory/inward.entity';
|
||||
import { BaseEntity } from '../common/base_entity';
|
||||
import { Column, Entity, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Vendor extends BaseEntity {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
store_name: string;
|
||||
|
||||
@Column()
|
||||
phone_number: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
gstin?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
email?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
address?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
city?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
state?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
pincode?: number;
|
||||
|
||||
@Column({ default: true })
|
||||
is_active: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
vendor_code: string;
|
||||
|
||||
@OneToMany(() => Inward, (inward) => inward.vendor)
|
||||
inwards: Inward[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
bank_name?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'bigint' })
|
||||
account_number?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
ifsc_code?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
branch_name?: string;
|
||||
}
|
||||
13
src/vendor/vendor.module.ts
vendored
Normal file
13
src/vendor/vendor.module.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { VendorService } from './vendor.service';
|
||||
import { VendorController } from './vendor.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Vendor } from './vendor.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Vendor])],
|
||||
controllers: [VendorController],
|
||||
providers: [VendorService],
|
||||
exports: [VendorService],
|
||||
})
|
||||
export class VendorModule {}
|
||||
71
src/vendor/vendor.service.ts
vendored
Normal file
71
src/vendor/vendor.service.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Vendor } from './vendor.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateVendorDto } from './dto/create-vendor';
|
||||
import { UpdateVendorDto } from './dto/update-vendor';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class VendorService {
|
||||
constructor(
|
||||
@InjectRepository(Vendor)
|
||||
private readonly vendorRepository: Repository<Vendor>,
|
||||
private readonly logService: LogService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async create(vendorData: CreateVendorDto, userId: string): Promise<void> {
|
||||
const vendor = this.vendorRepository.create(vendorData);
|
||||
await this.vendorRepository.save(vendor);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Vendor created', VendorService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Added vendor: ${vendor.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(loadInwards: boolean = false): Promise<Vendor[]> {
|
||||
if (loadInwards) {
|
||||
return await this.vendorRepository.find({ relations: ['inwards'] });
|
||||
}
|
||||
return await this.vendorRepository.find();
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Vendor> {
|
||||
const vendor = await this.vendorRepository.findOne({ where: { id } });
|
||||
if (!vendor) {
|
||||
throw new NotFoundException(`Vendor with ID ${id} not found`);
|
||||
}
|
||||
return vendor;
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string): Promise<void> {
|
||||
const vendor = await this.findOne(id);
|
||||
await this.vendorRepository.remove(vendor);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Vendor deleted', VendorService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Deleted vendor: ${vendor.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
updateVendorDto: UpdateVendorDto,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
const vendor = await this.findOne(id);
|
||||
this.vendorRepository.merge(vendor, updateVendorDto);
|
||||
await this.vendorRepository.save(vendor);
|
||||
|
||||
const user = await this.userService.findById(userId);
|
||||
await this.logService.logInfo('Vendor updated', VendorService.name, {
|
||||
user: { name: user.name, email: user.email, role: user.role?.name },
|
||||
activity: `Updated vendor: ${vendor.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
25
test/app.e2e-spec.ts
Normal file
25
test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
9
test/jest-e2e.json
Normal file
9
test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user