This commit is contained in:
2025-07-14 12:25:25 +05:30
commit 8a026321ab
110 changed files with 20225 additions and 0 deletions

10
.env.example Normal file
View 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
View 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

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://reg.thewired.agency/

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

17
Dockerfile Normal file
View 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
View 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
View 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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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
View File

@@ -0,0 +1,4 @@
{
"schemaVersion":2,
"dockerfilePath":"./Dockerfile"
}

35
eslint.config.mjs Normal file
View 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
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

14442
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

108
package.json Normal file
View File

@@ -0,0 +1,108 @@
{
"name": "inventory_management_backend",
"version": "0.0.2",
"description": "",
"author": "Anuj S | hello@anujs.dev",
"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.8",
"@anujs.dev/inventory-types-utils": "^1.0.6",
"@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",
"moment": "^2.30.1",
"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
View 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();
}
}

47
src/app.module.ts Normal file
View File

@@ -0,0 +1,47 @@
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 { MongooseModule } from '@nestjs/mongoose';
import { OrderModule } from './order/order.module';
import { IngredientUsageModule } from './ingredient-usage/ingredient-usage.module';
import { CategoryModule } from './category/category.module';
import { FoodItemModule } from './food-item/food-item.module';
import { ReportsModule } from './reports/reports.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,
CategoryModule,
FoodItemModule,
ReportsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
src/app.service.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View 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
View 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
View 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;
}
}

View 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;
},
);

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View 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);

View File

@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class UserLoginDto {
@IsString()
email: string;
@IsString()
password: string;
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,46 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { CategoryService } from './category.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
@Controller('category')
export class CategoryController {
constructor(private readonly categoryService: CategoryService) {}
@Post()
create(@Body() createCategoryDto: CreateCategoryDto) {
return this.categoryService.create(createCategoryDto);
}
@Get()
findAll(@Query('relations') relations: boolean = false) {
return this.categoryService.findAll(relations);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.categoryService.findOne(id);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateCategoryDto: UpdateCategoryDto,
) {
return this.categoryService.update(id, updateCategoryDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.categoryService.remove(id);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { CategoryService } from './category.service';
import { CategoryController } from './category.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Category } from './entities/category.entity';
@Module({
imports: [TypeOrmModule.forFeature([Category])],
controllers: [CategoryController],
providers: [CategoryService],
exports: [CategoryService],
})
export class CategoryModule {}

View File

@@ -0,0 +1,52 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Category } from './entities/category.entity';
import { Repository } from 'typeorm';
@Injectable()
export class CategoryService {
constructor(
@InjectRepository(Category)
private readonly categoryRepository: Repository<Category>,
) {}
async create(createCategoryDto: CreateCategoryDto) {
return await this.categoryRepository.save(createCategoryDto);
}
async findAll(withRelations = false) {
return await this.categoryRepository.find({
order: {
created_at: 'DESC',
},
relations: withRelations ? ['food_items'] : [],
});
}
async findOne(id: string) {
const category = await this.categoryRepository.findOne({
where: {
id,
},
});
if (!category) {
throw new BadRequestException('Category not found');
}
return category;
}
async update(id: string, updateCategoryDto: UpdateCategoryDto) {
const entity = await this.findOne(id);
const updated = this.categoryRepository.merge(entity, updateCategoryDto);
await this.categoryRepository.save(updated);
}
async remove(id: string) {
const entity = await this.findOne(id);
await this.categoryRepository.remove(entity);
}
}

View File

@@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class CreateCategoryDto {
@IsString()
name: string;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateCategoryDto } from './create-category.dto';
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}

View File

@@ -0,0 +1,12 @@
import { BaseEntity } from 'src/common/base_entity';
import { FoodItem } from 'src/food-item/entities/food-item.entity';
import { Column, Entity, OneToMany } from 'typeorm';
@Entity({ name: 'categories' })
export class Category extends BaseEntity {
@Column({ unique: true })
name: string;
@OneToMany(() => FoodItem, (foodItem) => foodItem.category)
food_items: FoodItem[];
}

20
src/common/base_entity.ts Normal file
View File

@@ -0,0 +1,20 @@
import { BeforeUpdate, Column, PrimaryGeneratedColumn } from 'typeorm';
export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
created_at: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
})
updated_at: Date;
@BeforeUpdate()
updateTimestamp() {
this.updated_at = new Date();
}
}

20
src/db/data_source.ts Normal file
View 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);

View 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: 'Sattva',
});
await repository.save({
name: 'Swasthi',
});
}
}

View 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',
});
}
}

View 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`);
}
}

View File

@@ -0,0 +1,12 @@
import { IsString, IsUUID } from 'class-validator';
export class CreateFoodItemDto {
@IsString()
name: string;
@IsUUID()
category: string;
@IsString()
unit: string;
}

View File

@@ -0,0 +1,5 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateFoodItemDto } from './create-food-item.dto';
import { FoodItem } from '../entities/food-item.entity';
export class UpdateFoodItemDto extends PartialType(FoodItem) {}

View File

@@ -0,0 +1,19 @@
import { Category } from 'src/category/entities/category.entity';
import { BaseEntity } from 'src/common/base_entity';
import { InwardEntry } from 'src/inventory/inward_entry.entity';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
@Entity()
export class FoodItem extends BaseEntity {
@Column()
name: string;
@Column({})
unit: string;
@ManyToOne(() => Category, (category) => category.food_items)
category: Category;
@OneToMany(() => InwardEntry, (inwardEntry) => inwardEntry.foodItem)
inward_entries: InwardEntry[];
}

View File

@@ -0,0 +1,46 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { FoodItemService } from './food-item.service';
import { CreateFoodItemDto } from './dto/create-food-item.dto';
import { UpdateFoodItemDto } from './dto/update-food-item.dto';
@Controller('food-item')
export class FoodItemController {
constructor(private readonly foodItemService: FoodItemService) {}
@Post()
create(@Body() createFoodItemDto: CreateFoodItemDto) {
return this.foodItemService.create(createFoodItemDto);
}
@Get()
findAll(@Query('relations') relations: boolean = false) {
return this.foodItemService.findAll(relations);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.foodItemService.findOne(id);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateFoodItemDto: UpdateFoodItemDto,
) {
return this.foodItemService.update(id, updateFoodItemDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.foodItemService.remove(id);
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { FoodItemService } from './food-item.service';
import { FoodItemController } from './food-item.controller';
import { CategoryModule } from 'src/category/category.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FoodItem } from './entities/food-item.entity';
@Module({
imports: [TypeOrmModule.forFeature([FoodItem]), CategoryModule],
controllers: [FoodItemController],
providers: [FoodItemService],
exports: [FoodItemService],
})
export class FoodItemModule {}

View File

@@ -0,0 +1,61 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { CreateFoodItemDto } from './dto/create-food-item.dto';
import { UpdateFoodItemDto } from './dto/update-food-item.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { FoodItem } from './entities/food-item.entity';
import { Repository } from 'typeorm';
import { CategoryService } from 'src/category/category.service';
@Injectable()
export class FoodItemService {
constructor(
@InjectRepository(FoodItem)
private readonly foodItemRepository: Repository<FoodItem>,
private readonly categoryService: CategoryService,
) {}
async create(createFoodItemDto: CreateFoodItemDto) {
const category = await this.categoryService.findOne(
createFoodItemDto.category,
);
return await this.foodItemRepository.save({
name: createFoodItemDto.name,
category,
unit: createFoodItemDto.unit,
});
}
async findAll(withRelations: boolean) {
return await this.foodItemRepository.find({
order: {
created_at: 'DESC',
},
relations: withRelations ? ['category'] : [],
});
}
async findOne(id: string) {
const food = await this.foodItemRepository.findOne({
where: {
id,
},
});
if (!food) {
throw new BadRequestException('Food item not found');
}
return food;
}
async update(id: string, updateFoodItemDto: UpdateFoodItemDto) {
const foodItem = await this.findOne(id);
const updated = this.foodItemRepository.merge(foodItem, updateFoodItemDto);
await this.foodItemRepository.save(updated);
}
async remove(id: string) {
const foodItem = await this.findOne(id);
await this.foodItemRepository.remove(foodItem);
}
}

View File

@@ -0,0 +1 @@
export class CreateIngredientUsageDto {}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateIngredientUsageDto } from './create-ingredient-usage.dto';
export class UpdateIngredientUsageDto extends PartialType(CreateIngredientUsageDto) {}

View 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;
}

View 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,
) {}
}

View 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 {}

View 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' },
});
}
}

View File

@@ -0,0 +1,45 @@
import {
IsDate,
IsDateString,
IsDecimal,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
isString,
IsString,
IsUUID,
} from 'class-validator';
export class CreateInwardEntryDto {
@IsUUID()
@IsNotEmpty()
food_item_id: string;
@IsOptional()
@IsString()
hsn_sac: string;
@IsOptional()
@IsNumber()
// @IsPositive()
gst_rate: number;
@IsNumber()
@IsPositive()
quantity: number;
@IsNumber()
rate: number;
@IsNumber()
@IsPositive()
amount: number;
@IsString()
company_name: string;
@IsDateString()
manufacturing_date: string;
}

View 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[];
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1,119 @@
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.sub);
}
@Patch('inwards/:id')
async updateInward(
@CurrentUser() user,
@Param('id') id: string,
@Body() updateInwardDto: UpdateInwardDto,
) {
return await this.inventoryService.updateInward(
id,
updateInwardDto,
user.sub,
);
}
@Delete('inwards/:id')
async deleteInward(@CurrentUser() user, @Param('id') id: string) {
return await this.inventoryService.deleteInward(id, user.sub);
}
@Delete('inward-entry/:entryId')
async deleteInwardEntry(
@CurrentUser() user,
@Param('entryId') entryId: string,
) {
return await this.inventoryService.deleteInwardEntry(entryId, user.sub);
}
// --------------- 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.sub,
);
}
@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.sub,
);
}
@Delete('inventory/:name')
async deleteInventoryItem(@CurrentUser() user, @Param('name') name: string) {
return await this.inventoryService.deleteInventoryItem(name, user.sub);
}
}

View 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', precision: 10, scale: 2 })
stock: number;
@Column()
unit: string;
}

View File

@@ -0,0 +1,21 @@
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';
import { FoodItemModule } from 'src/food-item/food-item.module';
@Module({
imports: [
TypeOrmModule.forFeature([Inventory, Inward, InwardEntry]),
VendorModule,
FoodItemModule,
],
controllers: [InventoryController],
providers: [InventoryService],
exports: [InventoryService],
})
export class InventoryModule {}

View File

@@ -0,0 +1,379 @@
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';
import { FoodItemService } from 'src/food-item/food-item.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,
private readonly foodItemService: FoodItemService,
) {}
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 { manufacturing_date, food_item_id, ...data } = entry;
const foodItem = await this.foodItemService.findOne(food_item_id);
const inwardEntry = this.inwardEntryRepository.create({
...data,
foodItem,
manufacuting_date: new Date(entry.manufacturing_date),
inward: savedInward,
company_name: data.company_name,
});
await this.inwardEntryRepository.save(inwardEntry);
await this.modifyStock(
foodItem.name,
entry.quantity,
'add',
foodItem.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', 'foodItem'],
});
if (!inward)
throw new NotFoundException(`Inward entry with ID ${id} not found.`);
for (const entry of inward.entries) {
await this.modifyStock(
entry.foodItem.name,
entry.quantity,
'subtract',
entry.foodItem.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.foodItem.name,
entry.quantity,
'subtract',
entry.foodItem.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 = Number(existingItem.stock) + Number(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 = Number(existingItem.stock) - Number(stock);
} else {
existingItem.stock = Number(existingItem.stock) + Number(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;
}
}

View 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;
}

View File

@@ -0,0 +1,40 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
import { BaseEntity } from '../common/base_entity';
import { Inward } from './inward.entity';
import { FoodItem } from 'src/food-item/entities/food-item.entity';
@Entity()
export class InwardEntry extends BaseEntity {
@ManyToOne(() => Inward, (inward) => inward.entries, {
onDelete: 'CASCADE',
nullable: false,
})
@JoinColumn()
inward: Inward;
@ManyToOne(() => FoodItem, (foodItem) => foodItem.inward_entries, {
eager: true,
})
foodItem: FoodItem;
@Column({ nullable: true })
hsn_sac: string;
@Column({ type: 'decimal', precision: 10, scale: 2 })
gst_rate: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
quantity: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
rate: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
amount: number;
@Column({})
company_name: string;
@Column({})
manufacuting_date: Date;
}

View File

@@ -0,0 +1,4 @@
export enum PaymentStatus {
PAID = 'PAID',
UNPAID = 'UNPAID',
}

View 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[];
}

View 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 {}

View 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: [],
});
});
});
});

View 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'],
});
}
}

View File

@@ -0,0 +1,123 @@
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;
},
) {
console.log(body.quantity, body.unit);
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.sub);
}
@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();
}
}

View 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', precision: 10, scale: 2, default: 0 })
stock: number;
@Column()
unit: string;
}

View 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 {}

View 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);
const inv = 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,
},
);
}
}

View 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', precision: 10, scale: 2 })
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
View 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
View 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 {}

33
src/log/log.schema.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type LogDocument = Log & Document;
export type ErrorLogDocument = ErrorLog & 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
View 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();
}
private async fetchLogs(
model: Model<LogDocument>,
page = 1,
limit = 50,
context?: string,
extraFilter: Record<string, any> = {},
) {
const filter: Record<string, any> = { ...extraFilter };
if (context) filter.context = context;
return model.find(filter).sort({ timestamp: -1 }).exec();
}
/* public API ------------------------------------------------------------ */
getAppLogs(page = 1, limit = 50, context?: string) {
return this.fetchLogs(this.logModel, page, limit, context);
}
getErrorLogs(page = 1, limit = 50, context?: string) {
return this.fetchLogs(this.errorLogModel, page, limit, context);
}
getLogsByContext(context: string, page = 1, limit = 50) {
return this.fetchLogs(this.logModel, page, limit, context);
}
getLogsByUser(user: string, page = 1, limit = 50) {
return this.fetchLogs(this.logModel, page, limit, undefined, {
'metadata.user': user,
});
}
getLogsByMetadataKey(key: string, value: string, page = 1, limit = 50) {
return this.fetchLogs(this.logModel, page, limit, undefined, {
[`metadata.${key}`]: value,
});
}
/** “Kitchen” = any log where metadata.user.role === 'Cook' */
getLogsByKitchen(userId: string, page = 1, limit = 50) {
return this.fetchLogs(this.logModel, page, limit, undefined, {
'metadata.user.role': 'Cook',
'metadata.user.id': userId,
});
}
/** Convenience wrapper */
getTransferLogs(page = 1, limit = 50) {
return this.getLogsByContext('KitchenInventoryService', page, limit);
}
}

27
src/main.ts Normal file
View File

@@ -0,0 +1,27 @@
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', ['http://localhost:3000']],
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();

View 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;
}

View 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[];
}

View 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.sub);
}
@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.sub);
}
@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
View 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
View 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 {}

164
src/menu/menu.service.ts Normal file
View File

@@ -0,0 +1,164 @@
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) }, { isActive: false }],
order: { publishDate: 'DESC' },
});
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), isActive: true },
});
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}`,
});
}
}

View File

@@ -0,0 +1,34 @@
import {
IsEnum,
IsNotEmpty,
IsObject,
IsOptional,
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;
}[];
};
};
@IsString()
@IsOptional()
deliverAt: string;
}
export class UpdateOrderDto {
@IsEnum(OrderStatus)
status: OrderStatus;
}

View 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[];
}

View File

@@ -0,0 +1,63 @@
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[];
@Column({ nullable: true })
deliverAt: string;
}

View File

@@ -0,0 +1,7 @@
export const ORDER_TRANSITIONS = {
PENDING: ['ACCEPTED'],
ACCEPTED: ['PREPARING', 'CANCELLED'],
PREPARING: ['COMPLETED', 'CANCELLED'],
COMPLETED: ['DELIVERED'],
CANCELLED: [],
};

View 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
View 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 {}

300
src/order/order.service.ts Normal file
View File

@@ -0,0 +1,300 @@
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'],
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,
deliverAt: createOrderDto.deliverAt,
});
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,
OrderStatus.DELIVERED,
]),
},
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,
},
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;
}
}

View File

@@ -0,0 +1 @@
export class CreateReportDto {}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateReportDto } from './create-report.dto';
export class UpdateReportDto extends PartialType(CreateReportDto) {}

View File

@@ -0,0 +1 @@
export class Report {}

View File

@@ -0,0 +1,61 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ReportsService } from './reports.service';
@Controller('reports')
export class ReportsController {
constructor(private readonly reportsService: ReportsService) {}
@Get('inward-outward')
async getInwardOutwardReport() {
return this.reportsService.generateInwardOutwardReport();
}
@Get('graph-ingredient-usage')
async getGraphReport() {
return this.reportsService.getMenuIngredientUsageGraphData();
}
@Get('kitchen-inward-outward')
async getKitchenInwardOutwardReport() {
return this.reportsService.getKitchenStockUsageAndCostEstimate();
}
@Get('kitchen-inward-outward2')
async getKitchenInwardOutwardReport2() {
return this.reportsService.getKitchenUsageStatsMultiRange();
}
@Get('/cost-estimator')
async getEstimatedCost(
@Query('menu') menuName: string,
@Query('servings') servings: number = 1,
) {
const ingredients =
await this.reportsService.getEstimatedIngredientsForMenu(menuName);
const latestRates = await this.reportsService.getLatestRates();
const breakdown = ingredients.map((i) => {
const rate = latestRates[i.ingredient] || 0;
const costPerUnit = rate;
const totalQty = i.quantityPerOrder * servings;
const totalCost = costPerUnit * totalQty;
return {
ingredient: i.ingredient,
unit: i.unit,
quantity: totalQty,
rate: costPerUnit,
cost: totalCost,
};
});
const totalCost = breakdown.reduce((sum, i) => sum + i.cost, 0);
return {
menuName,
servings,
ingredients: breakdown,
totalCost,
};
}
}

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { ReportsService } from './reports.service';
import { ReportsController } from './reports.controller';
import { KitchenInventoryModule } from 'src/kitchen_inventory/kitchen_inventory.module';
import { OrderModule } from 'src/order/order.module';
import { IngredientUsageModule } from 'src/ingredient-usage/ingredient-usage.module';
import { InventoryModule } from 'src/inventory/inventory.module';
@Module({
imports: [
KitchenInventoryModule,
OrderModule,
IngredientUsageModule,
InventoryModule,
],
controllers: [ReportsController],
providers: [ReportsService],
})
export class ReportsModule {}

View File

@@ -0,0 +1,788 @@
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import {
buildInwardOutwardReport,
getDayRange,
getMonthRange,
} from '@anujs.dev/inventory-core';
import { LogService } from 'src/log/log.service';
type InwardBatch = {
quantity: number;
amount: number;
rate: number;
timestamp: number;
};
type InwardGrouped = Record<string, InwardBatch[]>;
type OutwardGrouped = Record<string, { quantity: number }>;
@Injectable()
export class ReportsService {
constructor(
private readonly dataSource: DataSource,
private logService: LogService,
) {}
async inwardByPeriod(start: Date, end: Date): Promise<InwardGrouped> {
const result = await this.dataSource.query(
`
SELECT
fi.name as food_name,
ie.rate::float,
SUM(ie.quantity)::float AS total_quantity,
SUM(ie.amount)::float AS total_amount,
ie.created_at
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
WHERE ie.created_at BETWEEN $1 AND $2
GROUP BY fi.name, ie.rate, ie.created_at
ORDER BY fi.name, ie.created_at, ie.rate
`,
[start, end],
);
const grouped: InwardGrouped = {};
for (const row of result) {
if (!grouped[row.food_name]) grouped[row.food_name] = [];
grouped[row.food_name].push({
rate: row.rate,
quantity: row.total_quantity,
amount: row.total_amount,
timestamp: new Date(row.created_at).getTime(),
});
}
return grouped;
}
async outwardByPeriod(start: Date, end: Date): Promise<OutwardGrouped> {
const result = await this.dataSource.query(
`
SELECT
i.name as food_name,
SUM(log.quantity)::float AS total_quantity
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
WHERE log."usedAt" BETWEEN $1 AND $2
GROUP BY i.name
`,
[start, end],
);
const outward: OutwardGrouped = {};
for (const row of result) {
outward[row.food_name] = {
quantity: row.total_quantity,
};
}
return outward;
}
async generateInwardOutwardReport(useFIFO = true) {
const now = new Date();
const threeDaysAgo = new Date();
threeDaysAgo.setDate(now.getDate() - 2);
const periods = {
today: getDayRange(0),
last3Days: {
start: new Date(threeDaysAgo.setHours(0, 0, 0, 0)),
end: now,
},
total: { start: new Date('1970-01-01'), end: now },
};
// 1. System-wide inward/outward reports
const systemReport = {};
for (const [pname, range] of Object.entries(periods)) {
const inward = await this.inwardByPeriod(range.start, range.end);
const outward = await this.outwardByPeriod(range.start, range.end);
systemReport[pname] = buildInwardOutwardReport(inward, outward, useFIFO);
}
// 2. Get latest rates
const latestRateQuery = await this.dataSource.query(`
SELECT DISTINCT ON (fi.name)
fi.name,
ie.rate::float
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
ORDER BY fi.name, ie.created_at DESC
`);
const latestRates: Record<string, number> = {};
for (const row of latestRateQuery) {
latestRates[row.name] = row.rate;
}
// 3. Current Stock from KitchenInventory
const stockResult = await this.dataSource.query(`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
ki.stock::float
FROM kitchen_inventory ki
JOIN kitchen k ON k.id = ki."kitchenId"
JOIN inventory i ON i.id = ki."inventoryId"
`);
const kitchens: Record<string, any> = {};
for (const row of stockResult) {
const kitchen = row.kitchen_name;
const qty = Number(row.stock);
const rate = latestRates[row.inventory_name] || 0;
if (!kitchens[kitchen])
kitchens[kitchen] = {
summary: {},
currentStock: { totalQuantity: 0, totalValue: 0 },
};
kitchens[kitchen].currentStock.totalQuantity += qty;
kitchens[kitchen].currentStock.totalValue += qty * rate;
}
// 4. Inward from MongoDB logs
for (const [label, range] of Object.entries(periods)) {
const logs = await this.logService.getTransferLogs();
const filtered = logs.filter(
(log) =>
log.message === 'Inventory transferred to kitchen' &&
new Date(log.timestamp) >= range.start &&
new Date(log.timestamp) <= range.end,
);
for (const log of filtered) {
const kitchen = log.metadata.kitchen?.name;
const ingredient = log.metadata.inventory?.name;
const qty = parseFloat(log.metadata.quantity || 0);
const rate = latestRates[ingredient] || 0;
if (!kitchens[kitchen])
kitchens[kitchen] = {
summary: {},
currentStock: { totalQuantity: 0, totalValue: 0 },
};
if (!kitchens[kitchen].summary[label])
kitchens[kitchen].summary[label] = {
inward: { quantity: 0, value: 0 },
outward: { quantity: 0, value: 0 },
};
kitchens[kitchen].summary[label].inward.quantity += qty;
kitchens[kitchen].summary[label].inward.value += qty * rate;
}
}
// 5. Outward usage from IngredientUsageLog
for (const [label, range] of Object.entries(periods)) {
const result = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
SUM(log.quantity)::float AS total_quantity
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
JOIN kitchen k ON k.id = log."kitchenId"
WHERE log."usedAt" BETWEEN $1 AND $2
GROUP BY k.name, i.name
`,
[range.start, range.end],
);
for (const row of result) {
const kitchen = row.kitchen_name;
const ingredient = row.inventory_name;
const qty = Number(row.total_quantity);
const rate = latestRates[ingredient] || 0;
const value = qty * rate;
if (!kitchens[kitchen])
kitchens[kitchen] = {
summary: {},
currentStock: { totalQuantity: 0, totalValue: 0 },
};
if (!kitchens[kitchen].summary[label])
kitchens[kitchen].summary[label] = {
inward: { quantity: 0, value: 0 },
outward: { quantity: 0, value: 0 },
};
kitchens[kitchen].summary[label].outward.quantity += qty;
kitchens[kitchen].summary[label].outward.value += value;
}
}
return {
system: systemReport,
kitchens,
};
}
async getMenuIngredientUsageGraphData() {
const data = await this.dataSource.query(
`
SELECT
o.id as order_id,
o."orderDetails" as order_details,
o."created_at" as created_at,
o."foodType" as food_type,
log.quantity as ingredient_quantity,
i.name as ingredient,
i.unit as ingredient_unit
FROM "order" o
LEFT JOIN ingredient_usage_log log ON log."orderId" = o.id
LEFT JOIN inventory i ON i.id = log."inventoryId"
`,
);
const aggregate = {};
for (const row of data) {
if (!row.order_id || !row.order_details) continue;
let details;
try {
details =
typeof row.order_details === 'string'
? JSON.parse(row.order_details)
: row.order_details;
} catch (e) {
continue;
}
for (const userType in details) {
if (!details[userType]?.items) continue;
for (const item of details[userType].items) {
const menu = item.menu;
const count = Number(item.count);
const ingredient = row.ingredient || 'Unknown';
const unit = row.ingredient_unit || 'Unknown';
const key = `${menu}||${userType}||${ingredient}`;
if (!aggregate[key]) {
aggregate[key] = {
menu,
userType,
ingredient,
unit,
totalOrderCount: 0,
ingredientUsed: 0,
orders: [],
};
}
aggregate[key].totalOrderCount += count;
if (row.ingredient_quantity) {
aggregate[key].ingredientUsed += +row.ingredient_quantity;
}
aggregate[key].orders.push({
orderId: row.order_id,
orderDate: row.created_at,
count,
foodType: row.food_type,
});
}
}
}
// 🔁 Fetch latest rates
const latestRatesResult = await this.dataSource.query(`
SELECT DISTINCT ON (fi.name)
fi.name,
ie.rate::float
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
ORDER BY fi.name, ie.created_at DESC
`);
const latestRates: Record<string, number> = {};
for (const row of latestRatesResult) {
latestRates[row.name] = row.rate;
}
// 🧠 Add estimated cost per portion
const result = Object.values(aggregate).map((r: any) => {
const rate = latestRates[r.ingredient] || 0;
const quantityPerPortion = r.ingredientUsed / r.totalOrderCount;
const cost = rate * quantityPerPortion;
r.estimatedCostPerPortion = cost;
r.quantityPerPortion = quantityPerPortion;
// De-duplicate orders
r.orders = Object.values(
r.orders.reduce((acc, o) => {
if (!acc[o.orderId]) acc[o.orderId] = o;
else acc[o.orderId].count += o.count;
return acc;
}, {}),
);
return r;
});
return result;
}
async getKitchenStockAndUsageReportWithTotals() {
const end = new Date();
const start = new Date();
start.setDate(end.getDate() - 2); // last 3 days
// CURRENT STOCK from KitchenInventory
const stockResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
ki.stock::float,
ki.unit
FROM kitchen_inventory ki
JOIN kitchen k ON k.id = ki."kitchenId"
JOIN inventory i ON i.id = ki."inventoryId"
`,
);
const stockPerKitchen: Record<
string,
{
perItem: InwardGrouped;
totalInward: number;
}
> = {};
for (const row of stockResult) {
if (!stockPerKitchen[row.kitchen_name])
stockPerKitchen[row.kitchen_name] = {
perItem: {},
totalInward: 0,
};
const itemList = stockPerKitchen[row.kitchen_name].perItem;
if (!itemList[row.inventory_name]) itemList[row.inventory_name] = [];
itemList[row.inventory_name].push({
rate: 0,
quantity: row.stock,
amount: 0,
timestamp: Date.now(),
});
stockPerKitchen[row.kitchen_name].totalInward += row.stock;
}
// OUTWARD USAGE
const usageResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
SUM(log.quantity)::float AS total_quantity
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
JOIN kitchen k ON k.id = log."kitchenId"
WHERE log."usedAt" BETWEEN $1 AND $2
GROUP BY k.name, i.name
`,
[start, end],
);
const usagePerKitchen: Record<
string,
{
perItem: OutwardGrouped;
totalOutward: number;
}
> = {};
for (const row of usageResult) {
if (!usagePerKitchen[row.kitchen_name])
usagePerKitchen[row.kitchen_name] = {
perItem: {},
totalOutward: 0,
};
usagePerKitchen[row.kitchen_name].perItem[row.inventory_name] = {
quantity: row.total_quantity,
};
usagePerKitchen[row.kitchen_name].totalOutward += row.total_quantity;
}
// Combine using kitchen names
const kitchenNames = new Set([
...Object.keys(stockPerKitchen),
...Object.keys(usagePerKitchen),
]);
const result = {};
for (const kitchenName of kitchenNames) {
result[kitchenName] = {
currentStock: stockPerKitchen[kitchenName]?.perItem || {},
usedInLast3Days: usagePerKitchen[kitchenName]?.perItem || {},
totalInward: stockPerKitchen[kitchenName]?.totalInward || 0,
totalOutward: usagePerKitchen[kitchenName]?.totalOutward || 0,
};
}
return {
range: { start, end },
kitchens: result,
};
}
async getKitchenStockUsageAndCostEstimate() {
const end = new Date();
const start = new Date();
start.setDate(end.getDate() - 2); // last 3 days
// Get latest rates per inventory item
const rateRows = await this.dataSource.query(
`
SELECT DISTINCT ON (fi.name)
fi.name,
ie.rate::float
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
ORDER BY fi.name, ie.created_at DESC
`,
);
const latestRates: Record<string, number> = {};
for (const row of rateRows) {
latestRates[row.name] = row.rate;
}
// Current Stock (from KitchenInventory)
const stockResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
ki.stock::float,
ki.unit
FROM kitchen_inventory ki
JOIN kitchen k ON k.id = ki."kitchenId"
JOIN inventory i ON i.id = ki."inventoryId"
`,
);
const stockPerKitchen: Record<
string,
{
perItem: InwardGrouped;
totalInward: number;
}
> = {};
for (const row of stockResult) {
if (!stockPerKitchen[row.kitchen_name])
stockPerKitchen[row.kitchen_name] = {
perItem: {},
totalInward: 0,
};
const itemList = stockPerKitchen[row.kitchen_name].perItem;
if (!itemList[row.inventory_name]) itemList[row.inventory_name] = [];
itemList[row.inventory_name].push({
rate: 0,
quantity: row.stock,
amount: 0,
timestamp: Date.now(),
});
stockPerKitchen[row.kitchen_name].totalInward += row.stock;
}
// OUTWARD USAGE + COST ESTIMATE
const usageResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
SUM(log.quantity)::float AS total_quantity
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
JOIN kitchen k ON k.id = log."kitchenId"
WHERE log."usedAt" BETWEEN $1 AND $2
GROUP BY k.name, i.name
`,
[start, end],
);
const usagePerKitchen: Record<
string,
{
perItem: OutwardGrouped;
totalOutward: number;
estimatedCost: number;
}
> = {};
for (const row of usageResult) {
const kitchen = row.kitchen_name;
const ingredient = row.inventory_name;
const quantity = Number(row.total_quantity) || 0;
const rate = latestRates[ingredient] || 0;
const cost = quantity * rate;
if (!usagePerKitchen[kitchen]) {
usagePerKitchen[kitchen] = {
perItem: {},
totalOutward: 0,
estimatedCost: 0,
};
}
usagePerKitchen[kitchen].perItem[ingredient] = { quantity };
usagePerKitchen[kitchen].totalOutward += quantity;
usagePerKitchen[kitchen].estimatedCost += cost;
}
// Combine
const kitchenNames = new Set([
...Object.keys(stockPerKitchen),
...Object.keys(usagePerKitchen),
]);
const result = {};
for (const kitchenName of kitchenNames) {
result[kitchenName] = {
currentStock: stockPerKitchen[kitchenName]?.perItem || {},
usedInLast3Days: usagePerKitchen[kitchenName]?.perItem || {},
totalInward: stockPerKitchen[kitchenName]?.totalInward || 0,
totalOutward: usagePerKitchen[kitchenName]?.totalOutward || 0,
estimatedCostInLast3Days:
usagePerKitchen[kitchenName]?.estimatedCost || 0,
};
}
return {
range: { start, end },
kitchens: result,
};
}
async getKitchenUsageStatsMultiRange() {
const today = new Date();
const yesterday = new Date();
yesterday.setDate(today.getDate() - 1);
const threeDaysAgo = new Date();
threeDaysAgo.setDate(today.getDate() - 2);
const ranges = {
today: { start: new Date(today.setHours(0, 0, 0, 0)), end: new Date() },
last3Days: {
start: new Date(threeDaysAgo.setHours(0, 0, 0, 0)),
end: new Date(),
},
total: { start: new Date('1970-01-01'), end: new Date() },
};
// 1. Latest Rates
const latestRateQuery = await this.dataSource.query(`
SELECT DISTINCT ON (fi.name)
fi.name,
ie.rate::float
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
ORDER BY fi.name, ie.created_at DESC
`);
const latestRates: Record<string, number> = {};
for (const row of latestRateQuery) {
latestRates[row.name] = row.rate;
}
// 2. Stock from KitchenInventory (static across ranges)
const stockResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
ki.stock::float,
ki.unit
FROM kitchen_inventory ki
JOIN kitchen k ON k.id = ki."kitchenId"
JOIN inventory i ON i.id = ki."inventoryId"
`,
);
const stockPerKitchen: Record<string, InwardGrouped> = {};
for (const row of stockResult) {
if (!stockPerKitchen[row.kitchen_name])
stockPerKitchen[row.kitchen_name] = {};
const rate = latestRates[row.inventory_name] || 0;
if (!stockPerKitchen[row.kitchen_name][row.inventory_name])
stockPerKitchen[row.kitchen_name][row.inventory_name] = [];
stockPerKitchen[row.kitchen_name][row.inventory_name].push({
quantity: row.stock,
rate,
amount: rate * row.stock,
timestamp: Date.now(),
});
}
// 3. Usage per range
const kitchenNames = new Set<string>();
const usageByRange: Record<
string,
Record<
string,
{
perItem: OutwardGrouped;
totalOutward: number;
estimatedCost: number;
}
>
> = {};
for (const [label, { start, end }] of Object.entries(ranges)) {
const usageResult = await this.dataSource.query(
`
SELECT
k.name as kitchen_name,
i.name as inventory_name,
SUM(log.quantity)::float AS total_quantity
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
JOIN kitchen k ON k.id = log."kitchenId"
WHERE log."usedAt" BETWEEN $1 AND $2
GROUP BY k.name, i.name
`,
[start, end],
);
for (const row of usageResult) {
const kitchen = row.kitchen_name;
const ingredient = row.inventory_name;
const quantity = Number(row.total_quantity);
const rate = latestRates[ingredient] || 0;
const cost = quantity * rate;
kitchenNames.add(kitchen);
if (!usageByRange[kitchen]) usageByRange[kitchen] = {};
if (!usageByRange[kitchen][label])
usageByRange[kitchen][label] = {
perItem: {},
totalOutward: 0,
estimatedCost: 0,
};
usageByRange[kitchen][label].perItem[ingredient] = { quantity };
usageByRange[kitchen][label].totalOutward += quantity;
usageByRange[kitchen][label].estimatedCost += cost;
}
}
// 4. Combine into final object
const kitchens = {};
for (const kitchenName of kitchenNames) {
kitchens[kitchenName] = {
currentStock: stockPerKitchen[kitchenName] || {},
usageStats: {
today: usageByRange[kitchenName]?.today || {
perItem: {},
totalOutward: 0,
estimatedCost: 0,
},
last3Days: usageByRange[kitchenName]?.last3Days || {
perItem: {},
totalOutward: 0,
estimatedCost: 0,
},
total: usageByRange[kitchenName]?.total || {
perItem: {},
totalOutward: 0,
estimatedCost: 0,
},
},
};
}
return {
range: ranges,
kitchens,
};
}
async getEstimatedIngredientsForMenu(menuName: string) {
const result = await this.dataSource.query(
`
SELECT
i.name AS ingredient,
i.unit,
SUM(log.quantity)::float AS total_quantity,
COUNT(DISTINCT o.id) AS total_orders
FROM ingredient_usage_log log
JOIN inventory i ON i.id = log."inventoryId"
JOIN "order" o ON o.id = log."orderId"
WHERE o."orderDetails"::text ILIKE $1
GROUP BY i.name, i.unit
`,
[`%${menuName}%`],
);
return result.map((row) => ({
ingredient: row.ingredient,
unit: row.unit,
quantityPerOrder: row.total_quantity / row.total_orders,
}));
}
async getLatestRates(): Promise<Record<string, number>> {
const result = await this.dataSource.query(
`
SELECT DISTINCT ON (fi.name)
fi.name,
ie.rate::float
FROM inward_entry ie
JOIN food_item fi ON fi.id = ie."foodItemId"
ORDER BY fi.name, ie.created_at DESC
`,
);
const rates: Record<string, number> = {};
for (const row of result) {
rates[row.name] = row.rate;
}
return rates;
}
async estimateMenuCost(menuName: string) {
const ingredients = await this.getEstimatedIngredientsForMenu(menuName);
const latestRates = await this.getLatestRates();
const detailed = ingredients.map((i) => {
const rate = latestRates[i.ingredient] || 0;
const cost = rate * i.quantityPerOrder;
return {
ingredient: i.ingredient,
unit: i.unit,
quantityPerPortion: i.quantityPerOrder,
rate,
cost,
};
});
const totalCost = detailed.reduce((sum, i) => sum + i.cost, 0);
return {
menuName,
totalCost,
ingredients: detailed,
};
}
}

View 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[];
}

View 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 {}

View 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 });
}
}

View 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;
}

View 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;
}

101
src/user/user.controller.ts Normal file
View File

@@ -0,0 +1,101 @@
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.sub);
} 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.sub);
}
@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
View 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
View 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
View 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();
}
}

Some files were not shown because too many files have changed in this diff Show More