diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7a9dfa0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/k8s/staging/deployment.yaml b/k8s/staging/deployment.yaml index 4300c95..217c96e 100644 --- a/k8s/staging/deployment.yaml +++ b/k8s/staging/deployment.yaml @@ -15,12 +15,19 @@ spec: spec: containers: - name: ppob-backend - image: registry-harbor.app.bangun-kreatif.com/empatnusabangsa/ppob-backend:2 + image: registry-harbor.app.bangun-kreatif.com/empatnusabangsa/ppob-backend: ports: - containerPort: 5000 envFrom: - secretRef: name: ppob-backend-env + volumeMounts: + - name: storage + mountPath: /home/node/files + volumes: + - name: storage + persistentVolumeClaim: + claimName: ppob-backend-pvc imagePullSecrets: - name: regcred diff --git a/k8s/staging/pvc.yaml b/k8s/staging/pvc.yaml new file mode 100644 index 0000000..5b3fc42 --- /dev/null +++ b/k8s/staging/pvc.yaml @@ -0,0 +1,13 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: ppob-backend-pvc + namespace : empatnusabangsa-staging + annotations: + volume.beta.kubernetes.io/storage-class: "managed-nfs-storage" +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/package.json b/package.json index 637bced..b788325 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", - "start:dev": "nest start --watch", + "start:formatted": "nest start | pino-pretty", + "start:dev": "nest start --watch | pino-pretty", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", @@ -24,16 +25,30 @@ "@nestjs/common": "^8.0.0", "@nestjs/config": "^1.0.1", "@nestjs/core": "^8.0.0", + "@nestjs/jwt": "^8.0.0", "@nestjs/mapped-types": "*", + "@nestjs/passport": "^8.0.1", "@nestjs/platform-express": "^8.0.0", "@nestjs/platform-fastify": "^8.0.8", "@nestjs/typeorm": "^8.0.2", + "axios": "^0.24.0", + "bluebird": "^3.7.2", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", + "crypto": "^1.0.1", + "csv-parser": "^3.0.0", + "decimal.js": "^10.3.1", + "fs": "^0.0.1-security", + "fs-extra": "^10.0.0", "joi": "^17.4.2", + "lodash": "^4.17.21", "nestjs-pino": "^2.3.1", + "passport": "^0.5.0", + "passport-jwt": "^4.0.0", + "passport-local": "^1.0.0", "pg": "^8.7.1", "pino-http": "^6.3.0", + "pino-pretty": "^7.3.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -46,7 +61,10 @@ "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", + "@types/multer": "^1.4.7", "@types/node": "^16.0.0", + "@types/passport-jwt": "^3.0.6", + "@types/passport-local": "^1.0.34", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", diff --git a/src/app.module.ts b/src/app.module.ts index 94340fc..a6673bb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,8 +6,11 @@ import { UsersModule } from './users/users.module'; import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; import { LoggerModule } from 'nestjs-pino'; import { TransactionModule } from './transaction/transaction.module'; -import configuration from './config/configuration'; +import { ProductModule } from './product/product.module'; import { ConfigurableModule } from './configurable/configurable.module'; +import { AuthModule } from './auth/auth.module'; +import configuration from './config/configuration'; +import { MulterModule } from '@nestjs/platform-express'; @Module({ imports: [ @@ -19,14 +22,18 @@ import { ConfigurableModule } from './configurable/configurable.module'; .valid('development', 'production', 'test', 'provision') .default('development'), PORT: Joi.number().default(3000), - DATABASE_CLIENT: Joi.valid('mysql', 'postgres'), - DATABASE_HOST: Joi.string(), - DATABASE_NAME: Joi.string(), - DATABASE_USERNAME: Joi.string(), + SECRET: Joi.string().required(), + DATABASE_CLIENT: Joi.valid('mysql', 'postgres').required(), + DATABASE_HOST: Joi.string().required(), + DATABASE_NAME: Joi.string().required(), + DATABASE_USERNAME: Joi.string().required(), DATABASE_PASSWORD: Joi.string().empty('').default(''), DATABASE_PORT: Joi.number().default(5432), }), }), + MulterModule.register({ + dest: './files', + }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => { @@ -49,6 +56,8 @@ import { ConfigurableModule } from './configurable/configurable.module'; UsersModule, TransactionModule, ConfigurableModule, + ProductModule, + AuthModule, ], }) export class AppModule {} diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..cbc862f --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common'; +import { LocalAuthGuard } from './local-auth.guard'; +import { AuthService } from './auth.service'; +import { Public } from './public.decorator'; + +@Controller({ + path: 'auth', + version: '1', +}) +export class AuthController { + constructor(private authService: AuthService) {} + + @Public() + @UseGuards(LocalAuthGuard) + @Post('login') + async login(@Request() req) { + return this.authService.login(req.user); + } + + @Get('profile') + getProfile(@Request() req) { + return this.authService.getProfile(req.user.userId); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..4ffd567 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,41 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { UsersModule } from '../users/users.module'; +import { PassportModule } from '@nestjs/passport'; +import { LocalStrategy } from './local.strategy'; +import { AuthController } from './auth.controller'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtStrategy } from './jwt.strategy'; +import { APP_GUARD } from '@nestjs/core'; +import { JwtAuthGuard } from './jwt-auth.guard'; + +@Module({ + imports: [ + UsersModule, + PassportModule, + ConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + return { + secret: configService.get('secret'), + signOptions: { expiresIn: '1d' }, + }; + }, + }), + ], + providers: [ + AuthService, + LocalStrategy, + JwtStrategy, + { + provide: APP_GUARD, + useClass: JwtAuthGuard, + }, + ], + controllers: [AuthController], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..56f4b63 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; +import { hashPassword } from '../helper/hash_password'; +import { JwtService } from '@nestjs/jwt'; +import { User } from '../users/entities/user.entity'; + +@Injectable() +export class AuthService { + constructor( + private usersService: UsersService, + private jwtService: JwtService, + ) {} + + async validateUser(username: string, pass: string): Promise { + const user = await this.usersService.findOneByUsername(username); + + if (user && user.password === (await hashPassword(pass, user.salt))) { + const { password, ...result } = user; + + return result; + } + + return null; + } + + async login(user: User) { + const payload = { + username: user.username, + sub: user.id, + role: user.roles.name, + partner: user.partner?.id, + }; + + return { + access_token: this.jwtService.sign(payload), + }; + } + + getProfile = async (userId: string) => { + return this.usersService.findOne(userId); + }; +} diff --git a/src/auth/jwt-auth.guard.ts b/src/auth/jwt-auth.guard.ts new file mode 100644 index 0000000..9388eae --- /dev/null +++ b/src/auth/jwt-auth.guard.ts @@ -0,0 +1,24 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { Reflector } from '@nestjs/core'; +import { IS_PUBLIC_KEY } from './public.decorator'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + + if (isPublic) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..43d220d --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,22 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('secret'), + }); + } + + async validate(payload: any) { + return { + userId: payload.sub, + username: payload.username, + }; + } +} diff --git a/src/auth/local-auth.guard.ts b/src/auth/local-auth.guard.ts new file mode 100644 index 0000000..ccf962b --- /dev/null +++ b/src/auth/local-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/src/auth/local.strategy.ts b/src/auth/local.strategy.ts new file mode 100644 index 0000000..b43a0aa --- /dev/null +++ b/src/auth/local.strategy.ts @@ -0,0 +1,21 @@ +import { Strategy } from 'passport-local'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(private authService: AuthService) { + super(); + } + + async validate(username: string, password: string): Promise { + const user = await this.authService.validateUser(username, password); + + if (!user) { + throw new UnauthorizedException(); + } + + return user; + } +} diff --git a/src/auth/public.decorator.ts b/src/auth/public.decorator.ts new file mode 100644 index 0000000..b3845e1 --- /dev/null +++ b/src/auth/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 17bbed2..fc2a677 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,6 +1,7 @@ export default () => { return { port: parseInt(process.env.PORT, 10) || 3000, + secret: process.env.SECRET, database: { client: process.env.DATABASE_CLIENT, host: process.env.DATABASE_HOST, @@ -9,5 +10,7 @@ export default () => { password: process.env.DATABASE_PASSWORD || '', name: process.env.DATABASE_NAME, }, + upload_dir: __dirname + '/../uploads', + upload_url_path: '/files/', }; }; diff --git a/src/configurable/commission.service.ts b/src/configurable/commission.service.ts new file mode 100644 index 0000000..cf10d3b --- /dev/null +++ b/src/configurable/commission.service.ts @@ -0,0 +1,68 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { EntityNotFoundError, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CommissionSetting } from './entities/commission_setting.entity'; + +@Injectable() +export class CommissionService { + constructor( + @InjectRepository(CommissionSetting) + private commissionRepository: Repository, + ) {} + + findAllCommission(page, pageSize?) { + return this.commissionRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + order: { + version: 'DESC', + }, + }); + } + + async findOne(role: string) { + try { + return await this.commissionRepository.findOneOrFail({ + where: { + role: role, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Commission not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async updateCommission(id: string, request) { + try { + await this.commissionRepository.findOneOrFail(id); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Commission not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + + const result = await this.commissionRepository.update(id, { + commission: request.value, + }); + + return this.commissionRepository.findOneOrFail(id); + } +} diff --git a/src/configurable/configurable.controller.spec.ts b/src/configurable/configurable.controller.spec.ts index 579b37e..2d26cbd 100644 --- a/src/configurable/configurable.controller.spec.ts +++ b/src/configurable/configurable.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurableController } from './configurable.controller'; -import { ConfigurableService } from './configurable.service'; +import { RoleService } from './roles.service'; describe('ConfigurableController', () => { let controller: ConfigurableController; @@ -8,7 +8,7 @@ describe('ConfigurableController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ConfigurableController], - providers: [ConfigurableService], + providers: [RoleService], }).compile(); controller = module.get(ConfigurableController); diff --git a/src/configurable/configurable.controller.ts b/src/configurable/configurable.controller.ts index d1c99e6..c814ebe 100644 --- a/src/configurable/configurable.controller.ts +++ b/src/configurable/configurable.controller.ts @@ -1,26 +1,40 @@ import { + Body, Controller, Get, - Post, - Body, - Put, - Param, - Delete, - ParseUUIDPipe, HttpStatus, + Param, + ParseUUIDPipe, + Post, + Put, + Query, + Res, + UploadedFile, + UseInterceptors, } from '@nestjs/common'; -import { ConfigurableService } from './configurable.service'; +import { RoleService } from './roles.service'; +import { CommissionService } from './commission.service'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { editFileName } from '../helper/file-handler'; +import { Public } from 'src/auth/public.decorator'; @Controller({ path: 'config', version: '1', }) export class ConfigurableController { - constructor(private readonly usersService: ConfigurableService) {} + constructor( + private readonly roleService: RoleService, + private readonly commissionService: CommissionService, + ) {} - @Get() - async findAll() { - const [data, count] = await this.usersService.findAll(); + @Get('/roles') + async findAll( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + ) { + const [data, count] = await this.roleService.findAllRoles(page, pageSize); return { data, @@ -30,13 +44,84 @@ export class ConfigurableController { }; } - @Get(':id') - async findOne(@Param('id', ParseUUIDPipe) id: string) { + @Get('/commission') + async findCommission( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + ) { + const [data, count] = await this.commissionService.findAllCommission( + page, + pageSize, + ); + return { - data: await this.usersService.findOne(id), + data, + count, statusCode: HttpStatus.OK, message: 'success', }; } + @Get('/roles/for-membership') + async findAllForMembership( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + ) { + const [data, count] = await this.roleService.findAllRolesForCreateMember( + page, + pageSize, + ); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Public() + @Get('/image/:imgpath') + seeUploadedFile(@Param('imgpath') image, @Res() res) { + return res.sendFile(image, { root: './files' }); + } + + @Get(':id') + async findOne(@Param('id', ParseUUIDPipe) id: string) { + return { + data: await this.roleService.findOne(id), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Public() + @Post('/upload-files') + @UseInterceptors( + FileInterceptor('file', { + storage: diskStorage({ + destination: './files', + filename: editFileName, + }), + }), + ) + async uploadedFile(@UploadedFile() file: Express.Multer.File) { + const response = { + originalname: file, + filename: file.filename, + }; + return response; + } + + @Put('/commission/:id') + async updateCommission( + @Param('id', ParseUUIDPipe) id: string, + @Body() request, + ) { + return { + data: await this.commissionService.updateCommission(id, request), + statusCode: HttpStatus.OK, + message: 'success', + }; + } } diff --git a/src/configurable/configurable.module.ts b/src/configurable/configurable.module.ts index da4f11e..45c0081 100644 --- a/src/configurable/configurable.module.ts +++ b/src/configurable/configurable.module.ts @@ -2,11 +2,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Roles } from './entities/roles.entity'; import { ConfigurableController } from './configurable.controller'; -import { ConfigurableService } from './configurable.service'; +import { RoleService } from './roles.service'; +import { CommissionService } from './commission.service'; +import { CommissionSetting } from './entities/commission_setting.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Roles])], + imports: [TypeOrmModule.forFeature([Roles, CommissionSetting])], controllers: [ConfigurableController], - providers: [ConfigurableService], + providers: [RoleService, CommissionService], + exports: [RoleService, CommissionService], }) export class ConfigurableModule {} diff --git a/src/configurable/configurable.service.spec.ts b/src/configurable/configurable.service.spec.ts index be931a1..bced459 100644 --- a/src/configurable/configurable.service.spec.ts +++ b/src/configurable/configurable.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ConfigurableService } from './configurable.service'; +import { RoleService } from './roles.service'; -describe('ConfigurableService', () => { - let service: ConfigurableService; +describe('RoleService', () => { + let service: RoleService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [ConfigurableService], + providers: [RoleService], }).compile(); - service = module.get(ConfigurableService); + service = module.get(RoleService); }); it('should be defined', () => { diff --git a/src/configurable/configurable.service.ts b/src/configurable/configurable.service.ts deleted file mode 100644 index 5236a1b..0000000 --- a/src/configurable/configurable.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { EntityNotFoundError, Repository } from 'typeorm'; -import { Roles } from './entities/roles.entity'; -import { InjectRepository } from '@nestjs/typeorm'; - -@Injectable() -export class ConfigurableService { - constructor( - @InjectRepository(Roles) - private rolesRepository: Repository, - ) {} - - findAll() { - return this.rolesRepository.findAndCount(); - } - - async findOne(id: string) { - try { - return await this.rolesRepository.findOneOrFail(id); - } catch (e) { - if (e instanceof EntityNotFoundError) { - throw new HttpException( - { - statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', - }, - HttpStatus.NOT_FOUND, - ); - } else { - throw e; - } - } - } -} diff --git a/src/configurable/entities/commission_setting.entity.ts b/src/configurable/entities/commission_setting.entity.ts new file mode 100644 index 0000000..8f0de02 --- /dev/null +++ b/src/configurable/entities/commission_setting.entity.ts @@ -0,0 +1,16 @@ +import { Entity, Column, OneToOne, JoinColumn } from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { Roles } from './roles.entity'; + +@Entity() +export class CommissionSetting extends BaseModel { + @Column() + name: string; + + @Column() + commission: number; + + @OneToOne(() => Roles) + @JoinColumn() + role: Roles; +} diff --git a/src/configurable/roles.service.ts b/src/configurable/roles.service.ts new file mode 100644 index 0000000..75b46bd --- /dev/null +++ b/src/configurable/roles.service.ts @@ -0,0 +1,58 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { EntityNotFoundError, In, Not, Repository } from 'typeorm'; +import { Roles } from './entities/roles.entity'; +import { InjectRepository } from '@nestjs/typeorm'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Roles) + private rolesRepository: Repository, + ) {} + + findAllRoles(page, pageSize?) { + return this.rolesRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + order: { + version: 'DESC', + }, + }); + } + + findAllRolesForCreateMember(page, pageSize?) { + return this.rolesRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + where: { + id: Not( + In([ + '3196cdf4-ae5f-4677-9bcd-98be35c72321', + '21dceea2-416e-4b55-b74c-12605e1f8d1b', + ]), + ), + }, + order: { + version: 'DESC', + }, + }); + } + + async findOne(id: string) { + try { + return await this.rolesRepository.findOneOrFail(id); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Data Role not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } +} diff --git a/src/helper/csv-parser.ts b/src/helper/csv-parser.ts new file mode 100644 index 0000000..91b2675 --- /dev/null +++ b/src/helper/csv-parser.ts @@ -0,0 +1,25 @@ +import { createReadStream } from 'fs'; +import * as csvParser from 'csv-parser'; +import * as path from 'path'; + +export async function parsingFile(dataFile) { + const parsingData: any = await new Promise((resolve, reject) => { + const results = []; + + const file = createReadStream( + path.join(process.cwd(), `./files/${dataFile}`), + ); + + file + .pipe( + csvParser({ + headers: false, + }), + ) + .on('data', (data) => results.push(data)) + .on('end', () => { + resolve(results); + }); + }); + return parsingData; +} diff --git a/src/helper/enum-list.ts b/src/helper/enum-list.ts new file mode 100644 index 0000000..e8ffaaf --- /dev/null +++ b/src/helper/enum-list.ts @@ -0,0 +1,46 @@ +export enum statusTransaction { + PENDING, + SUCCESS, + FAILED, + APPROVED, + REJECTED, +} + +export enum typeTransaction { + DISTRIBUTION, + ORDER, + DEPOSIT_SUPPLIER, + DEPOSIT_RETURN, + WITHDRAW, +} + +export enum productType { + NORMAL, + PROMO, +} + +export enum coaType { + WALLET, + INCOME, + INVENTORY, + COST_OF_SALES, + SALES, + BANK, + EXPENSE, + ACCOUNT_RECEIVABLE, + ACCOUNT_PAYABLE, + BUDGET, + CONTRA_BUDGET, + PROFIT, + WITHDRAW, +} + +export enum balanceType { + DEBIT, + CREDIT, +} + +export enum accountType { + PARTNER, + CUSTOMER, +} diff --git a/src/helper/file-handler.ts b/src/helper/file-handler.ts new file mode 100644 index 0000000..f0b4695 --- /dev/null +++ b/src/helper/file-handler.ts @@ -0,0 +1,12 @@ +import * as path from 'path'; + +export const editFileName = (req, file, callback) => { + const name = file.originalname.split('.')[0]; + const fileExtName = path.extname(file.originalname); + // const fileExtName = 'asdasd'; + const randomName = Array(4) + .fill(null) + .map(() => Math.round(Math.random() * 16).toString(16)) + .join(''); + callback(null, `${name}-${randomName}${fileExtName}`); +}; diff --git a/src/helper/hash_password.ts b/src/helper/hash_password.ts new file mode 100644 index 0000000..4a11e04 --- /dev/null +++ b/src/helper/hash_password.ts @@ -0,0 +1,13 @@ +import * as crypto from 'crypto'; + +export function hashPassword(password, salt): Promise { + return new Promise((resolve, reject) => { + crypto.pbkdf2(password, salt, 50, 100, 'sha512', (err, values) => { + if (err) { + return reject(err); + } + + resolve(values.toString('hex')); + }); + }); +} diff --git a/src/helper/irs-api.ts b/src/helper/irs-api.ts new file mode 100644 index 0000000..1c5b439 --- /dev/null +++ b/src/helper/irs-api.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +const irs_url = 'http://h2h.elangpangarep.com/api/h2h'; +const irs_id = 'PT0005'; +const irs_pin = '04JFGC'; +const irs_user = 'D10BD0'; +const irs_pass = '6251F3'; + +export const doTransaction = async (productCode, destination, idtrx) => { + try { + const res = await axios.get( + `${irs_url}?id=${irs_id}&pin=${irs_pin}&user=${irs_user}&pass=${irs_pass}&kodeproduk=${productCode}&tujuan=${destination}&counter=1&idtrx=${idtrx}`, + ); + + return res.data; + } catch (err) { + throw err; + } +}; diff --git a/src/helper/irs-service.ts b/src/helper/irs-service.ts new file mode 100644 index 0000000..4aea493 --- /dev/null +++ b/src/helper/irs-service.ts @@ -0,0 +1,25 @@ +import * as axios from 'axios'; + +export const createTransaction = async (kode, tujuan) => { + const codeTransaksi = generateRequestId(); + + return axios.default + .get( + `http://h2h.elangpangarep.com/api/h2h?id=PT0005&pin=04JFGC&user=D10BD0&pass=6251F3&kodeproduk=${kode}&tujuan=${tujuan}&counter=1&idtrx=${codeTransaksi}`, + ) + .then((response) => { + return response.data; + }); +}; + +export const generateRequestId = () => { + return `${new Date() + .toLocaleString('en-us', { + year: '2-digit', + month: '2-digit', + day: '2-digit', + }) + .replace(/(\d+)\/(\d+)\/(\d+)/, '$3$1$2')}${Math.random() + .toPrecision(3) + .replace('0.', '')}`; +}; diff --git a/src/helper/jwt.strategy.ts b/src/helper/jwt.strategy.ts new file mode 100644 index 0000000..2ec61b2 --- /dev/null +++ b/src/helper/jwt.strategy.ts @@ -0,0 +1,22 @@ +// import { PassportStrategy } from '@nestjs/passport'; +// import { ExtractJwt, Strategy } from 'passport-jwt'; +// import { Injectable } from '@nestjs/common'; +// import { AuthService } from '../auth/auth.service'; + +// @Injectable() +// export class JwtStrategy extends PassportStrategy(Strategy) { +// constructor(private readonly authService: AuthService) { +// super({ +// jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), +// secretOrKey: process.env.SECRETKEY, +// }); +// } + +// async validate(payload: JwtPayload): Promise { +// const user = await this.authService.validateUser(payload); +// if (!user) { +// throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED); +// } +// return user; +// } +// } \ No newline at end of file diff --git a/src/ledger/entities/coa.entity.ts b/src/ledger/entities/coa.entity.ts deleted file mode 100644 index a4dbaa0..0000000 --- a/src/ledger/entities/coa.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, ManyToOne, ManyToMany, JoinTable, -} from 'typeorm'; -import { Product } from '../../product/entities/product.entity'; -import { User } from '../../users/entities/user.entity'; -import { BaseModel } from '../../config/basemodel.entity'; - -enum type { - SYSTEM_BANk, - INCOME, -} - -enum balanceType { - DEBIT, - CREDIT, -} - -@Entity() -export class Roles extends BaseModel{ - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - name: string; - - @Column('text') - type: type; - - @Column('text') - balanceType: balanceType; - - @Column() - amount: number; - - @ManyToMany(() => User) - @JoinTable() - user: User[]; -} diff --git a/src/main.ts b/src/main.ts index 1bb8975..c19e714 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,14 @@ import { NestFactory } from '@nestjs/core'; -import { - FastifyAdapter, - NestFastifyApplication, -} from '@nestjs/platform-fastify'; import { AppModule } from './app.module'; -import {ValidationPipe, VersioningType} from '@nestjs/common'; +import { ValidationPipe, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Logger } from 'nestjs-pino'; +import { NestExpressApplication } from '@nestjs/platform-express'; async function bootstrap() { - const app = await NestFactory.create( + const app = await NestFactory.create( AppModule, - new FastifyAdapter(), + // new FastifyAdapter(), { bufferLogs: true }, ); @@ -31,13 +28,14 @@ async function bootstrap() { const configService = app.get(ConfigService); const port = configService.get('port'); - await app.listen(port, '0.0.0.0', (error, address) => { - if (error) { - logger.error(error); - process.exit(1); - } else { - logger.log(`Server listening on ${address}`); - } + await app.listen(port, '0.0.0.0', () => { + logger.log('Service Started'); + // if (error) { + // logger.error(error); + // process.exit(1); + // } else { + // logger.log(`Server listening on ${address}`); + // } }); } diff --git a/src/product/dto/categories/create-categories-product.dto.ts b/src/product/dto/categories/create-categories-product.dto.ts index e878820..78203f5 100644 --- a/src/product/dto/categories/create-categories-product.dto.ts +++ b/src/product/dto/categories/create-categories-product.dto.ts @@ -3,4 +3,7 @@ import { IsNotEmpty, IsUUID } from 'class-validator'; export class CreateCategoriesProductDto { @IsNotEmpty() name: string; + + @IsNotEmpty() + code: string; } diff --git a/src/product/dto/create-product.dto.ts b/src/product/dto/product/create-product.dto.ts similarity index 67% rename from src/product/dto/create-product.dto.ts rename to src/product/dto/product/create-product.dto.ts index ed442af..74433dc 100644 --- a/src/product/dto/create-product.dto.ts +++ b/src/product/dto/product/create-product.dto.ts @@ -10,6 +10,15 @@ export class CreateProductDto { @IsNotEmpty() status: string; + @IsNotEmpty() + price: number; + + @IsNotEmpty() + markUpPrice: number; + @IsUUID() subCategoriesId: string; + + @IsUUID() + supplierId: string; } diff --git a/src/product/dto/product/update-price-product.dto.ts b/src/product/dto/product/update-price-product.dto.ts new file mode 100644 index 0000000..9a51190 --- /dev/null +++ b/src/product/dto/product/update-price-product.dto.ts @@ -0,0 +1,17 @@ +import { IsNotEmpty } from 'class-validator'; +import { productType } from '../../../helper/enum-list'; + +export class UpdatePriceProductDto { + @IsNotEmpty() + price: number; + + @IsNotEmpty() + markUpPrice: number; + + @IsNotEmpty() + type: productType; + + startDate: Date; + + endDate: Date; +} diff --git a/src/product/dto/product/update-product.dto.ts b/src/product/dto/product/update-product.dto.ts new file mode 100644 index 0000000..30534c5 --- /dev/null +++ b/src/product/dto/product/update-product.dto.ts @@ -0,0 +1,6 @@ +import { OmitType, PartialType } from '@nestjs/mapped-types'; +import { CreateProductDto } from './create-product.dto'; + +export class UpdateProductDto extends PartialType( + OmitType(CreateProductDto, ['price'] as const), +) {} diff --git a/src/product/dto/product/upload-product.dto.ts b/src/product/dto/product/upload-product.dto.ts new file mode 100644 index 0000000..00f025d --- /dev/null +++ b/src/product/dto/product/upload-product.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class UploadProductDto { + @IsNotEmpty() + fileName: string; + + @IsNotEmpty() + supplierCode: string; +} diff --git a/src/product/dto/sub-categories/create-sub-categories-product.dto.ts b/src/product/dto/sub-categories/create-sub-categories-product.dto.ts index 6fb097d..a451dd3 100644 --- a/src/product/dto/sub-categories/create-sub-categories-product.dto.ts +++ b/src/product/dto/sub-categories/create-sub-categories-product.dto.ts @@ -2,6 +2,12 @@ import { IsNotEmpty, IsUUID } from 'class-validator'; import { CreateCategoriesProductDto } from '../categories/create-categories-product.dto'; export class CreateSubCategoriesProductDto extends CreateCategoriesProductDto { + @IsNotEmpty() + name: string; + + @IsNotEmpty() + code: string; + @IsUUID() categoryId: string; } diff --git a/src/product/entities/product-category.entity.ts b/src/product/entities/product-category.entity.ts index 86da379..873538f 100644 --- a/src/product/entities/product-category.entity.ts +++ b/src/product/entities/product-category.entity.ts @@ -19,9 +19,14 @@ export class ProductCategories extends BaseModel { @Column() name: string; + @Column({ + nullable: true, + }) + code: string; + @OneToMany( () => ProductSubCategories, (subCategories) => subCategories.category, ) - subCategories: ProductSubCategories; + sub_categories: ProductSubCategories; } diff --git a/src/product/entities/product-history-price.entity.ts b/src/product/entities/product-history-price.entity.ts index c744910..033b34d 100644 --- a/src/product/entities/product-history-price.entity.ts +++ b/src/product/entities/product-history-price.entity.ts @@ -1,21 +1,9 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, - OneToMany, - ManyToOne, -} from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; import { Product } from './product.entity'; import { BaseModel } from '../../config/basemodel.entity'; - -enum Type { - NORMAL, - PROMO, -} +import { productType } from '../../helper/enum-list'; +import { User } from '../../users/entities/user.entity'; +import { Partner } from '../../users/entities/partner.entity'; @Entity() export class ProductHistoryPrice extends BaseModel { @@ -23,14 +11,26 @@ export class ProductHistoryPrice extends BaseModel { id: string; @ManyToOne(() => Product, (product) => product.id) - productId: string; + product: Product; + + @ManyToOne(() => Partner, (partner) => partner.id) + partner: Partner; + + @Column() + price: number; + + @Column() + mark_up_price: number; @Column({ type: 'date' }) - startDate: string; + startDate: Date; - @Column({ type: 'date' }) - endDate: string; + @Column({ + type: 'date', + nullable: true, + }) + endDate: Date; @Column('text') - type: Type; + type: productType; } diff --git a/src/product/entities/product-sub-category.entity.ts b/src/product/entities/product-sub-category.entity.ts index ff50c6b..f60eec2 100644 --- a/src/product/entities/product-sub-category.entity.ts +++ b/src/product/entities/product-sub-category.entity.ts @@ -2,23 +2,29 @@ import { Entity, Column, PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, ManyToOne, + OneToMany, } from 'typeorm'; import { ProductCategories } from './product-category.entity'; import { BaseModel } from '../../config/basemodel.entity'; +import { Product } from './product.entity'; @Entity() -export class ProductSubCategories extends BaseModel{ +export class ProductSubCategories extends BaseModel { @PrimaryGeneratedColumn('uuid') id: string; @Column() name: string; - @ManyToOne(() => ProductCategories, (categories) => categories.subCategories) + @Column({ + nullable: true, + }) + code: string; + + @ManyToOne(() => ProductCategories, (categories) => categories.sub_categories) category: ProductCategories; + + @OneToMany(() => Product, (product) => product.sub_categories) + product: Product; } diff --git a/src/product/entities/product.entity.ts b/src/product/entities/product.entity.ts index 78b9696..1b23f0e 100644 --- a/src/product/entities/product.entity.ts +++ b/src/product/entities/product.entity.ts @@ -11,9 +11,11 @@ import { } from 'typeorm'; import { ProductSubCategories } from './product-sub-category.entity'; import { BaseModel } from '../../config/basemodel.entity'; +import { Supplier } from '../../users/entities/supplier.entity'; +import { ProductHistoryPrice } from './product-history-price.entity'; @Entity() -export class Product extends BaseModel{ +export class Product extends BaseModel { @PrimaryGeneratedColumn('uuid') id: string; @@ -26,9 +28,45 @@ export class Product extends BaseModel{ @Column() status: string; + @Column({ + nullable: true, + }) + price: number; + + @Column({ + nullable: true, + }) + basePrice: number; + @ManyToOne( - () => ProductSubCategories, - (subCategories) => subCategories.category, + () => { + return ProductSubCategories; + }, + (subCategories) => { + return subCategories.product; + }, ) - subCategories: ProductSubCategories; + sub_categories: ProductSubCategories; + + @ManyToOne( + () => { + return Supplier; + }, + (partner) => { + return partner.id; + }, + ) + supplier: Supplier; + + @OneToMany( + () => { + return ProductHistoryPrice; + }, + (php) => { + return php.product; + }, + ) + priceHistory: ProductHistoryPrice; + + currentPrice: ProductHistoryPrice; } diff --git a/src/product/history-price/history-price.service.spec.ts b/src/product/history-price/history-price.service.spec.ts new file mode 100644 index 0000000..0a9952d --- /dev/null +++ b/src/product/history-price/history-price.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HistoryPriceService } from './history-price.service'; + +describe('HistoryPriceService', () => { + let service: HistoryPriceService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HistoryPriceService], + }).compile(); + + service = module.get(HistoryPriceService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/product/history-price/history-price.service.ts b/src/product/history-price/history-price.service.ts new file mode 100644 index 0000000..694dedd --- /dev/null +++ b/src/product/history-price/history-price.service.ts @@ -0,0 +1,106 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { EntityNotFoundError, IsNull, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ProductHistoryPrice } from '../entities/product-history-price.entity'; + +@Injectable() +export class ProductHistoryPriceService { + constructor( + @InjectRepository(ProductHistoryPrice) + private productHistoryPriceService: Repository, + ) {} + + async create(dataProduct: ProductHistoryPrice) { + const result = await this.productHistoryPriceService.save(dataProduct); + + return result; + } + + async findOne(product: string, partner: string) { + try { + return await this.productHistoryPriceService.findOneOrFail({ + where: { + product: product, + endDate: IsNull(), + partner: partner ? partner : IsNull(), + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Price not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findById(id: string) { + try { + return await this.productHistoryPriceService.findOneOrFail(id); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Price not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findOneByProductId( + page: number, + productId: string, + supplierId: string, + pageSize?: number, + ) { + try { + const query = this.productHistoryPriceService + .createQueryBuilder('product_history_price') + .leftJoin('product_history_price.product', 'product') + .where({ product: productId }) + .andWhere('product_history_price.endDate IS NULL'); + + if (supplierId !== 'null' && supplierId) { + query.andWhere('product.supplier = :supplierId', { + supplierId: supplierId, + }); + } + + const data = await query + .orderBy('product_history_price.createdAt', 'DESC') + .skip(page * (pageSize || 10)) + .take(pageSize || 10) + .getMany(); + + const totalData = await query.getCount(); + + return { + data, + count: totalData, + }; + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Product History Price not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } +} diff --git a/src/product/product-categories.service.ts b/src/product/product-categories.service.ts index 081a411..5afab21 100644 --- a/src/product/product-categories.service.ts +++ b/src/product/product-categories.service.ts @@ -13,6 +13,20 @@ export class ProductCategoriesService { ) {} async create(CreateCategoriesProductDto: CreateCategoriesProductDto) { + const check = await this.productCategoriesRepository.findOne({ + code: CreateCategoriesProductDto.code, + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Category Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + const result = await this.productCategoriesRepository.insert( CreateCategoriesProductDto, ); @@ -22,10 +36,10 @@ export class ProductCategoriesService { ); } - findAll(page) { + findAll(page, pageSize?) { return this.productCategoriesRepository.findAndCount({ - skip: page * 10, - take: 10, + skip: page * (pageSize || 10), + take: pageSize || 10, order: { version: 'DESC', }, @@ -40,7 +54,29 @@ export class ProductCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Categories not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByCode(code: string) { + try { + return await this.productCategoriesRepository.findOneOrFail({ + where: { + code: code, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Product Categories not found', }, HttpStatus.NOT_FOUND, ); @@ -61,7 +97,7 @@ export class ProductCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Categories not found', }, HttpStatus.NOT_FOUND, ); @@ -86,7 +122,7 @@ export class ProductCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Categories not found', }, HttpStatus.NOT_FOUND, ); diff --git a/src/product/product-sub-categories.service.ts b/src/product/product-sub-categories.service.ts index 6674eb0..9890ef7 100644 --- a/src/product/product-sub-categories.service.ts +++ b/src/product/product-sub-categories.service.ts @@ -1,31 +1,86 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { EntityNotFoundError, Repository } from 'typeorm'; +import { EntityNotFoundError, In, Repository } from 'typeorm'; import { ProductSubCategories } from './entities/product-sub-category.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { CreateSubCategoriesProductDto } from './dto/sub-categories/create-sub-categories-product.dto'; import { UpdateSubCategoriesProductDto } from './dto/sub-categories/update-sub-categories-product.dto'; +import { ProductCategoriesService } from './product-categories.service'; @Injectable() export class ProductSubCategoriesService { constructor( @InjectRepository(ProductSubCategories) private productSubCategoriesRepository: Repository, + private productCategoriesService: ProductCategoriesService, ) {} - async create(CreateCategoriesProductDto: CreateSubCategoriesProductDto) { - const result = await this.productSubCategoriesRepository.insert( - CreateCategoriesProductDto, + async create(createSubCategoriesProductDto: CreateSubCategoriesProductDto) { + const check = await this.productSubCategoriesRepository.findOne({ + code: createSubCategoriesProductDto.code, + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Sub Category Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const categories = await this.productCategoriesService.findOne( + createSubCategoriesProductDto.categoryId, ); + const result = await this.productSubCategoriesRepository.insert({ + name: createSubCategoriesProductDto.name, + code: createSubCategoriesProductDto.code, + category: categories, + }); + return this.productSubCategoriesRepository.findOneOrFail( result.identifiers[0].id, ); } - findAll(page) { + async findAll(page, category: string, pageSize?) { + let filterCategories; + if (category) { + filterCategories = category.split(',').map((data) => data.trim()); + } + + const baseQuery = this.productSubCategoriesRepository + .createQueryBuilder('product_sub_categories') + .leftJoinAndSelect('product_sub_categories.category', 'category'); + + if (category && filterCategories.length > 0) { + baseQuery.where({ + category: In(filterCategories), + }); + } + + const data = await baseQuery + .skip(page * (pageSize || 10)) + .take(pageSize || 10) + .getMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + findAllByCategories(page, category) { return this.productSubCategoriesRepository.findAndCount({ skip: page * 10, take: 10, + where: { + category: category, + }, + relations: ['category'], order: { version: 'DESC', }, @@ -40,7 +95,7 @@ export class ProductSubCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Sub Categories not found', }, HttpStatus.NOT_FOUND, ); @@ -50,6 +105,14 @@ export class ProductSubCategoriesService { } } + async findOneForCSVParser(code: string) { + return await this.productSubCategoriesRepository.findOne({ + where: { + code: code, + }, + }); + } + async update( id: string, updateCategoriesProductDto: UpdateSubCategoriesProductDto, @@ -61,7 +124,7 @@ export class ProductSubCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Sub Categories not found', }, HttpStatus.NOT_FOUND, ); @@ -70,11 +133,15 @@ export class ProductSubCategoriesService { } } - const result = await this.productSubCategoriesRepository.update( - id, - updateCategoriesProductDto, + const categories = await this.productCategoriesService.findOne( + updateCategoriesProductDto.categoryId, ); + const result = await this.productSubCategoriesRepository.update(id, { + name: updateCategoriesProductDto.name, + category: categories, + }); + return this.productSubCategoriesRepository.findOneOrFail(id); } @@ -86,7 +153,7 @@ export class ProductSubCategoriesService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product Sub Categories not found', }, HttpStatus.NOT_FOUND, ); diff --git a/src/product/product.controller.ts b/src/product/product.controller.ts index b00e6b2..fee15ec 100644 --- a/src/product/product.controller.ts +++ b/src/product/product.controller.ts @@ -1,17 +1,27 @@ import { - Controller, - Get, - Post, Body, - Put, - Param, + Controller, Delete, + Get, + HttpStatus, + Param, ParseUUIDPipe, - HttpStatus, Query, + Post, + Put, + Query, + Request, } from '@nestjs/common'; import { ProductService } from './product.service'; import { ProductCategoriesService } from './product-categories.service'; import { CreateCategoriesProductDto } from './dto/categories/create-categories-product.dto'; +import { UpdateCategoriesProductDto } from '../product/dto/categories/update-categories-product.dto'; +import { UpdateSubCategoriesProductDto } from '../product/dto/sub-categories/update-sub-categories-product.dto'; +import { ProductSubCategoriesService } from './product-sub-categories.service'; +import { CreateSubCategoriesProductDto } from './dto/sub-categories/create-sub-categories-product.dto'; +import { CreateProductDto } from './dto/product/create-product.dto'; +import { UpdateProductDto } from './dto/product/update-product.dto'; +import { ProductHistoryPriceService } from './history-price/history-price.service'; +import { UploadProductDto } from './dto/product/upload-product.dto'; @Controller({ path: 'product', @@ -21,8 +31,19 @@ export class ProductController { constructor( private readonly productService: ProductService, private readonly productCategoriesService: ProductCategoriesService, + private readonly productSubCategoriesService: ProductSubCategoriesService, + private readonly productHistoryPriceService: ProductHistoryPriceService, ) {} + @Post() + async create(@Body() createProductDto: CreateProductDto) { + return { + data: await this.productService.create(createProductDto), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + @Post('categories') async createCategories( @Body() createCategoriesProductDto: CreateCategoriesProductDto, @@ -36,9 +57,116 @@ export class ProductController { }; } - @Get() - async findAll(@Query('page') page: number) { - const [data, count] = await this.productService.findAll(page); + @Post('sub-categories') + async createSubCategories( + @Body() createSubCategoriesProductDto: CreateSubCategoriesProductDto, + ) { + return { + data: await this.productSubCategoriesService.create( + createSubCategoriesProductDto, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('upload-product') + async createProductBaseOnCSV(@Body() uploadProductDto: UploadProductDto) { + await this.productService.processUploadCSV( + uploadProductDto.fileName, + uploadProductDto.supplierCode, + ); + + return { + data: 'Done', + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Get('all') + async findAll( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('sub-category') subcategory: string, + @Query('supplier') supplier: string, + ) { + const data = await this.productService.findAll( + page, + supplier == 'null' ? null : supplier, + subcategory == 'null' ? null : subcategory, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('test') + async test(@Request() req) { + const data = await this.productService.processUploadCSV('',''); + + return { + data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('by-categories-all') + async findByCategoriesAll( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('sub-category') subcategory: string, + @Query('supplier') supplier: string, + ) { + const data = await this.productService.findAllBySubCategories( + page, + subcategory, + supplier, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('by-categories') + async findByCategories( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('sub-category') subcategory: string, + @Request() req, + ) { + const data = await this.productService.findAllForPartner( + page, + pageSize, + subcategory, + req.user.username, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('categories') + async findAllCategories( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + ) { + const [data, count] = await this.productCategoriesService.findAll( + page, + pageSize, + ); return { data, @@ -48,13 +176,121 @@ export class ProductController { }; } - @Get(':id') - async findOne(@Param('id', ParseUUIDPipe) id: string) { + @Get('sub-categories') + async findAllSubCategories( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('category') category: string, + ) { + const data = await this.productSubCategoriesService.findAll( + page, + category == 'null' ? null : category, + pageSize, + ); + return { - data: await this.productService.findOne(id), + ...data, statusCode: HttpStatus.OK, message: 'success', }; } + @Get(':id') + async findOne(@Param('id', ParseUUIDPipe) id: string) { + return { + data: await this.productService.findOneById(id), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('price-history/:id') + async findPriceHistoryByProductId( + @Param('id', ParseUUIDPipe) id: string, + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('supplier') supplier: string, + ) { + const data = await this.productHistoryPriceService.findOneByProductId( + page, + id, + supplier, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put(':id') + async update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateProductDto: UpdateProductDto, + ) { + return { + data: await this.productService.update(id, updateProductDto), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('categories/:id') + async updateCategories( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateCategoriesDto: UpdateCategoriesProductDto, + ) { + return { + data: await this.productCategoriesService.update(id, updateCategoriesDto), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('sub-categories/:id') + async updateSubCategories( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateSubCategoriesDto: UpdateSubCategoriesProductDto, + ) { + return { + data: await this.productSubCategoriesService.update( + id, + updateSubCategoriesDto, + ), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Delete(':id') + async remove(@Param('id', ParseUUIDPipe) id: string) { + await this.productService.remove(id); + + return { + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Delete('categories/:id') + async removeCategories(@Param('id', ParseUUIDPipe) id: string) { + await this.productCategoriesService.remove(id); + + return { + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Delete('sub-categories/:id') + async removeSubCategories(@Param('id', ParseUUIDPipe) id: string) { + await this.productSubCategoriesService.remove(id); + + return { + statusCode: HttpStatus.OK, + message: 'success', + }; + } } diff --git a/src/product/product.module.ts b/src/product/product.module.ts index 58e4c5a..231478e 100644 --- a/src/product/product.module.ts +++ b/src/product/product.module.ts @@ -1,10 +1,33 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { ProductService } from './product.service'; import { ProductController } from './product.controller'; import { ProductCategoriesService } from './product-categories.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Product } from './entities/product.entity'; +import { ProductCategories } from './entities/product-category.entity'; +import { ProductHistoryPrice } from './entities/product-history-price.entity'; +import { ProductSubCategories } from './entities/product-sub-category.entity'; +import { ProductSubCategoriesService } from './product-sub-categories.service'; +import { UsersModule } from '../users/users.module'; +import { ProductHistoryPriceService } from './history-price/history-price.service'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + Product, + ProductCategories, + ProductHistoryPrice, + ProductSubCategories, + ]), + forwardRef(() => UsersModule), + ], controllers: [ProductController], - providers: [ProductService, ProductCategoriesService], + providers: [ + ProductService, + ProductCategoriesService, + ProductSubCategoriesService, + ProductHistoryPriceService, + ], + exports: [ProductService, ProductHistoryPriceService], }) export class ProductModule {} diff --git a/src/product/product.service.ts b/src/product/product.service.ts index 3b05d59..55573ef 100644 --- a/src/product/product.service.ts +++ b/src/product/product.service.ts @@ -1,45 +1,363 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus } from '@nestjs/common'; import { EntityNotFoundError, Repository } from 'typeorm'; import { Product } from './entities/product.entity'; -import { ProductCategories } from './entities/product-category.entity'; -import { ProductSubCategories } from './entities/product-sub-category.entity'; import { InjectRepository } from '@nestjs/typeorm'; -import { CreateProductDto } from '../product/dto/create-product.dto'; -import { CreateCategoriesProductDto } from './dto/categories/create-categories-product.dto'; -import { CreateSubCategoriesProductDto } from './dto/sub-categories/create-sub-categories-product.dto'; +import { CreateProductDto } from './dto/product/create-product.dto'; +import { ProductSubCategoriesService } from './product-sub-categories.service'; +import { UpdateProductDto } from './dto/product/update-product.dto'; +import { ProductHistoryPrice } from './entities/product-history-price.entity'; +import { productType } from '../helper/enum-list'; +import { UpdatePriceProductDto } from './dto/product/update-price-product.dto'; +import { UsersService } from '../users/users.service'; +import { SupplierService } from '../users/supplier/supplier.service'; +import { parsingFile } from '../helper/csv-parser'; +import { PartnerService } from '../users/partner/partner.service'; +import { mapSeries } from 'bluebird'; +import { isNull } from 'util'; -@Injectable() export class ProductService { constructor( @InjectRepository(Product) private productRepository: Repository, + @InjectRepository(ProductHistoryPrice) + private productHistoryPrice: Repository, + private productSubCategoriesService: ProductSubCategoriesService, + private usersService: UsersService, + private supplierService: SupplierService, + private partnerService: PartnerService, ) {} async create(createProductDto: CreateProductDto) { - const result = await this.productRepository.insert(createProductDto); + const subCategories = await this.productSubCategoriesService.findOne( + createProductDto.subCategoriesId, + ); + + const result = await this.productRepository.insert({ + name: createProductDto.name, + code: createProductDto.code, + status: createProductDto.status, + sub_categories: subCategories, + price: createProductDto.price, + }); + + await this.productHistoryPrice.insert({ + product: result.identifiers[0], + type: productType.NORMAL, + price: createProductDto.price, + mark_up_price: createProductDto.markUpPrice, + startDate: new Date(), + endDate: null, + }); return this.productRepository.findOneOrFail(result.identifiers[0].id); } - findAll(page) { - return this.productRepository.findAndCount({ - skip: page * 10, - take: 10, - order: { - version: 'DESC', - }, + async processUploadCSV(uploadFile: string, supplierCode: string) { + const supplierData = await this.supplierService.findByCode(supplierCode); + + const data = await parsingFile(uploadFile); + data.shift(); + await mapSeries(data, async (it) => { + let dataHistoryPrice; + let partnerData; + + const subCategories = + await this.productSubCategoriesService.findOneForCSVParser(it[2]); + + if (!subCategories) { + return; + } + + const productData = await this.productRepository.findOne({ + code: it[0], + supplier: supplierData, + }); + if (productData) { + //TODO : Handle Update Product + productData.name = it[1]; + productData.status = it[5] == 'active' ? 'ACTIVE' : 'NOT ACTIVE'; + await this.productRepository.save(productData); + + //TODO : Handle History Price + if (it[6] != '-' && it[6] != '') { + partnerData = await this.partnerService.findOne(it[6]); + dataHistoryPrice = await this.productHistoryPrice.findOne({ + where: { + product: productData.id, + partner: partnerData.id, + }, + }); + } else { + dataHistoryPrice = await this.productHistoryPrice.findOne({ + product: productData, + }); + } + + if (!dataHistoryPrice) { + return; + } + + dataHistoryPrice.endDate = new Date(); + await this.productHistoryPrice.save(dataHistoryPrice); + + await this.productHistoryPrice.insert({ + product: productData, + mark_up_price: it[4], + price: it[3], + type: productType.NORMAL, + startDate: new Date(), + partner: it[6] != '-' ? partnerData : null, + }); + } else { + let partnerData; + if (it[6] != '-' && it[6] != '') { + partnerData = await this.partnerService.findOne(it[6]); + } + const savedProduct = await this.productRepository.insert({ + name: it[1], + code: it[0], + status: it[5] == 'active' ? 'ACTIVE' : 'NOT ACTIVE', + sub_categories: subCategories, + supplier: supplierData, + }); + + return await this.productHistoryPrice.insert({ + product: savedProduct.identifiers[0], + mark_up_price: it[4], + price: it[3], + type: productType.NORMAL, + startDate: new Date(), + endDate: null, + partner: partnerData, + }); + } }); + + return data; } - async findOne(id: string) { + async findAll( + page: number, + supplier: string, + subCategories: string, + pageSize?: number, + ) { + let filterSupplier, filterSubCategories; + + if (supplier) { + filterSupplier = supplier.split(',').map((data) => { + return data.trim(); + }); + } + + if (subCategories) { + filterSubCategories = subCategories.split(',').map((data) => { + return data.trim(); + }); + } + + const baseQuery = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.sub_categories', 'sub_categories') + .leftJoin('sub_categories.category', 'category') + .leftJoin('product.supplier', 'supplier') + .where('supplier.status = :status', { status: true }) + .innerJoinAndMapOne( + 'product.currentPrice', + 'product.priceHistory', + 'current_price', + 'current_price.partner_id is null and current_price.end_date is NULL', + ) + .select(['product.id']) + .addSelect([ + 'product.name', + 'product.code', + 'sub_categories.name', + 'supplier.name', + 'category.name', + 'product.status', + ]) + .addSelect('current_price.price') + .addSelect( + '(current_price.price + current_price.mark_up_price) as mark_up_price', + ); + + if (subCategories && filterSubCategories.length > 0) { + baseQuery.where('product.sub_categories_id IN (:...subCategoryId)', { + subCategoryId: filterSubCategories, + }); + } + + if (supplier && filterSupplier.length > 0) { + baseQuery.where('supplier.id IN (:...supplierId)', { + supplierId: filterSupplier, + }); + } + + const data = await baseQuery + .offset(page * (pageSize || 10)) + .limit(pageSize || 10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findAllByCategories(page, subCategories, supplier) { + const baseQuery = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.sub_categories', 'sub_categories') + .where( + 'sub_categories.category_id = :id and product.supplier_id = :supplier_id', + { + id: subCategories, + supplier_id: supplier, + }, + ) + .leftJoinAndMapOne( + 'product.currentPrice', + 'product.priceHistory', + 'current_price', + 'current_price.partner_id is null', + ); + + const data = await baseQuery + .skip(page * 10) + .take(10) + .getMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findAllBySubCategories(page, subCategories, supplier, pageSize?) { + if (supplier != 'null' && !supplier) { + supplier = (await this.supplierService.findByActive()).id; + } + + const baseQuery = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.sub_categories', 'sub_categories') + .where( + `product.supplier_id = :supplier_id and product.status = 'ACTIVE'`, + { + supplier_id: supplier, + }, + ) + .leftJoinAndMapOne( + 'product.currentPrice', + 'product.priceHistory', + 'current_price', + 'current_price.partner_id is NULL and current_price.end_date is NULL', + ) + .select(['product.id']) + .addSelect(['product.name', 'product.code', 'sub_categories.name']) + .addSelect( + '(current_price.price + current_price.mark_up_price) as price', + ); + + if (subCategories != 'null' && subCategories) { + baseQuery.where('product.sub_categories_id = :id', { + id: subCategories, + }); + } + + const data = await baseQuery + .offset(page * 10) + .limit(10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findAllForPartner( + page: number, + pageSize: number, + subCategories: string, + username: string, + ) { + const user = await this.usersService.findOneByUsername(username); + const supplier = await this.supplierService.findByActive(); + + let filterSupplier, filterSubCategories; + + if (subCategories) { + filterSubCategories = subCategories.split(',').map((data) => data.trim()); + } else { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Sub Categories not inlcude', + }, + HttpStatus.NOT_FOUND, + ); + } + + const baseQuery = this.productRepository + .createQueryBuilder('product') + .leftJoin('product.sub_categories', 'sub_categories') + .where( + `product.sub_categories_id IN (:...subCategoryId) and product.supplier_id = :supplier_id and product.status = 'ACTIVE'`, + { + subCategoryId: filterSubCategories, + supplier_id: supplier.id, + }, + ) + .leftJoinAndMapOne( + 'product.currentPrice', + 'product.priceHistory', + 'current_price', + 'current_price.partner_id = :id_partner and current_price.end_date is NULL', + { + id_partner: user.partner.id, + }, + ) + .select(['product.id']) + .addSelect(['product.name', 'product.code', 'sub_categories.name']) + .addSelect( + '(current_price.price + current_price.mark_up_price) as price', + ); + + const data = await baseQuery + .offset(page * 10) + .limit(10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findOne(code: string) { try { - return await this.productRepository.findOneOrFail(id); + return await this.productRepository.findOneOrFail({ + relations: ['supplier'], + where: { + code: code, + }, + }); } catch (e) { if (e instanceof EntityNotFoundError) { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'Product not found', }, HttpStatus.NOT_FOUND, ); @@ -48,4 +366,96 @@ export class ProductService { } } } + + async findOneById(id: string) { + try { + return await this.productRepository.findOneOrFail({ + relations: ['supplier'], + where: { + id: id, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Product not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async update(id: string, updateProductDto: UpdateProductDto) { + try { + await this.productRepository.findOneOrFail(id); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Product not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + + const subCategories = await this.productSubCategoriesService.findOne( + updateProductDto.subCategoriesId, + ); + + const result = await this.productRepository.update(id, { + name: updateProductDto.name, + code: updateProductDto.code, + status: updateProductDto.status, + sub_categories: subCategories, + }); + + return this.productRepository.findOneOrFail(id); + } + + async updatePrice( + code: string, + updatePriceProductDto: UpdatePriceProductDto, + ) { + const product = await this.findOne(code); + + await this.productHistoryPrice.insert({ + product: product, + type: updatePriceProductDto.type, + price: updatePriceProductDto.price, + mark_up_price: updatePriceProductDto.markUpPrice, + startDate: updatePriceProductDto.startDate, + endDate: updatePriceProductDto.endDate, + }); + + return true; + } + + async remove(id: string) { + try { + await this.productRepository.findOneOrFail(id); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Product not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + + await this.productRepository.delete(id); + } } diff --git a/src/transaction/coa.service.ts b/src/transaction/coa.service.ts new file mode 100644 index 0000000..3fb4a7e --- /dev/null +++ b/src/transaction/coa.service.ts @@ -0,0 +1,144 @@ +import { + forwardRef, + HttpException, + HttpStatus, + Inject, + Injectable, +} from '@nestjs/common'; +import { EntityNotFoundError, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { COA } from './entities/coa.entity'; +import { balanceType, coaType } from '../helper/enum-list'; +import { InputCoaDto } from './dto/input-coa.dto'; +import { UsersService } from 'src/users/users.service'; + +export class CoaService { + constructor( + @InjectRepository(COA) + private coaRepository: Repository, + @Inject( + forwardRef(() => { + return UsersService; + }), + ) + private userService: UsersService, + ) {} + + async create(inputCoaDto: InputCoaDto) { + const coaData = new COA(); + let name = ''; + if (inputCoaDto.user) { + coaData.user = inputCoaDto.user.id; + name = inputCoaDto.user.username; + } + + if (inputCoaDto.supplier) { + coaData.supplier = inputCoaDto.supplier.id; + name = inputCoaDto.supplier.code; + } + + coaData.name = `${coaType[inputCoaDto.type]}-${name}`; + coaData.balanceType = inputCoaDto.balanceType; + coaData.type = inputCoaDto.type; + coaData.amount = 0; + + const result = await inputCoaDto.coaEntityManager.insert(COA, coaData); + + if ( + inputCoaDto.type == coaType.ACCOUNT_RECEIVABLE || + inputCoaDto.type == coaType.ACCOUNT_PAYABLE + ) { + coaData.relatedUser = inputCoaDto.relatedUserId; + await inputCoaDto.coaEntityManager.save(coaData); + } + + return coaData; + } + + async findByUser(id: string, typeOfCoa: coaType) { + try { + return await this.coaRepository.findOneOrFail({ + user: id, + type: typeOfCoa, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'COA not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByTwoUser(from: string, destination: string, typeOfCoa: coaType) { + try { + return await this.coaRepository.findOneOrFail({ + user: from, + relatedUser: destination, + type: typeOfCoa, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'COA not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByUserWithRelated( + id: string, + relatedId: string, + typeOfCoa: coaType, + ) { + try { + return await this.coaRepository.findOneOrFail({ + user: id, + type: typeOfCoa, + relatedUser: relatedId, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Coa Data not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByName(name: string) { + try { + return await this.coaRepository.findOneOrFail({ name: name }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'COA Data not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } +} diff --git a/src/transaction/dto/add-saldo-supplier.dto.ts b/src/transaction/dto/add-saldo-supplier.dto.ts new file mode 100644 index 0000000..518c5b5 --- /dev/null +++ b/src/transaction/dto/add-saldo-supplier.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class AddSaldoSupplier { + @IsNotEmpty() + supplier: string; + + @IsNotEmpty() + amount: number; +} diff --git a/src/transaction/dto/create-journal.dto.ts b/src/transaction/dto/create-journal.dto.ts new file mode 100644 index 0000000..0cbf949 --- /dev/null +++ b/src/transaction/dto/create-journal.dto.ts @@ -0,0 +1,36 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { balanceType, coaType, statusTransaction, typeTransaction } from 'src/helper/enum-list'; +import { EntityManager } from 'typeorm'; +import { Transactions } from '../entities/transactions.entity'; + +interface JournalEntry { + coa_id: string; + debit?: number; + credit?: number; +} + +export class CreateJournalDto { + @IsNotEmpty() + transactionalEntityManager: EntityManager; + + @IsNotEmpty() + createTransaction?: boolean; + + @IsNotEmpty() + userId?: string; + + @IsNotEmpty() + transaction?: Transactions; + + @IsNotEmpty() + type?: typeTransaction; + + @IsNotEmpty() + amount?: number; + + @IsNotEmpty() + transactionStatus?: statusTransaction; + + @IsNotEmpty() + journals: JournalEntry[] +} diff --git a/src/transaction/dto/create-transaction.dto.ts b/src/transaction/dto/create-transaction.dto.ts deleted file mode 100644 index 6f59387..0000000 --- a/src/transaction/dto/create-transaction.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreateTransactionDto {} diff --git a/src/transaction/dto/deposit_return.dto.ts b/src/transaction/dto/deposit_return.dto.ts new file mode 100644 index 0000000..7d1110b --- /dev/null +++ b/src/transaction/dto/deposit_return.dto.ts @@ -0,0 +1,7 @@ +import { DistributeTransactionDto } from './distribute-transaction.dto'; +import { IsNotEmpty } from 'class-validator'; + +export class DepositReturnDto extends DistributeTransactionDto { + @IsNotEmpty() + image_prove: string; +} diff --git a/src/transaction/dto/distribute-transaction.dto.ts b/src/transaction/dto/distribute-transaction.dto.ts new file mode 100644 index 0000000..7d79186 --- /dev/null +++ b/src/transaction/dto/distribute-transaction.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class DistributeTransactionDto { + @IsNotEmpty() + amount: number; + + @IsNotEmpty() + destination: string; +} diff --git a/src/transaction/dto/input-coa.dto.ts b/src/transaction/dto/input-coa.dto.ts new file mode 100644 index 0000000..b6c580d --- /dev/null +++ b/src/transaction/dto/input-coa.dto.ts @@ -0,0 +1,25 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { balanceType, coaType } from 'src/helper/enum-list'; +import { User } from 'src/users/entities/user.entity'; +import { EntityManager } from 'typeorm'; +import { Supplier } from '../../users/entities/supplier.entity'; + +export class InputCoaDto { + @IsUUID() + user: User; + + @IsNotEmpty() + type: coaType; + + @IsNotEmpty() + balanceType: balanceType; + + @IsUUID() + relatedUserId: string; + + @IsUUID() + supplier: Supplier; + + @IsNotEmpty() + coaEntityManager: EntityManager; +} diff --git a/src/transaction/dto/order-transaction.dto.ts b/src/transaction/dto/order-transaction.dto.ts new file mode 100644 index 0000000..413a4b2 --- /dev/null +++ b/src/transaction/dto/order-transaction.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class OrderTransactionDto { + @IsNotEmpty() + productCode: string; + + @IsNotEmpty() + phoneNumber: string; +} diff --git a/src/transaction/dto/update-transaction.dto.ts b/src/transaction/dto/update-transaction.dto.ts index bade684..6402c55 100644 --- a/src/transaction/dto/update-transaction.dto.ts +++ b/src/transaction/dto/update-transaction.dto.ts @@ -1,4 +1,4 @@ import { PartialType } from '@nestjs/mapped-types'; -import { CreateTransactionDto } from './create-transaction.dto'; +import { DistributeTransactionDto } from './distribute-transaction.dto'; -export class UpdateTransactionDto extends PartialType(CreateTransactionDto) {} +export class UpdateTransactionDto extends PartialType(DistributeTransactionDto) {} diff --git a/src/transaction/entities/coa.entity.ts b/src/transaction/entities/coa.entity.ts new file mode 100644 index 0000000..f60bb7a --- /dev/null +++ b/src/transaction/entities/coa.entity.ts @@ -0,0 +1,36 @@ +import { Entity, Column } from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { coaType, balanceType } from '../../helper/enum-list'; + +@Entity() +export class COA extends BaseModel { + @Column() + name: string; + + @Column('text') + type: coaType; + + @Column('text') + balanceType: balanceType; + + @Column() + amount: number; + + @Column({ + type: 'uuid', + nullable: true, + }) + user: string; + + @Column({ + type: 'uuid', + nullable: true, + }) + relatedUser: string; + + @Column({ + type: 'uuid', + nullable: true, + }) + supplier: string; +} diff --git a/src/transaction/entities/transaction-journal.entity.ts b/src/transaction/entities/transaction-journal.entity.ts new file mode 100644 index 0000000..b3a9664 --- /dev/null +++ b/src/transaction/entities/transaction-journal.entity.ts @@ -0,0 +1,41 @@ +import { + Entity, + Column, + ManyToOne, + ManyToMany, + JoinTable, + OneToOne, +} from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { COA } from './coa.entity'; +import { Transactions } from './transactions.entity'; +import { balanceType } from '../../helper/enum-list'; + +@Entity() +export class TransactionJournal extends BaseModel { + @Column('text') + type: balanceType; + + @Column() + amount: number; + + @ManyToOne( + () => { + return Transactions; + }, + (transaction) => { + return transaction.id; + }, + ) + transaction_head: Transactions; + + @ManyToOne( + () => { + return COA; + }, + (coa) => { + return coa.id; + }, + ) + coa: COA; +} diff --git a/src/transaction/entities/transaction.entity.ts b/src/transaction/entities/transaction.entity.ts deleted file mode 100644 index 9d16154..0000000 --- a/src/transaction/entities/transaction.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class Transaction {} diff --git a/src/transaction/entities/transactions.entity.ts b/src/transaction/entities/transactions.entity.ts new file mode 100644 index 0000000..c9b4c74 --- /dev/null +++ b/src/transaction/entities/transactions.entity.ts @@ -0,0 +1,66 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + UpdateDateColumn, + DeleteDateColumn, + VersionColumn, + CreateDateColumn, + ManyToOne, + ManyToMany, + JoinTable, +} from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { statusTransaction, typeTransaction } from '../../helper/enum-list'; +import { Partner } from '../../users/entities/partner.entity'; +import { ProductHistoryPrice } from '../../product/entities/product-history-price.entity'; +import { User } from '../../users/entities/user.entity'; +import { UserDetail } from '../../users/entities/user_detail.entity'; +import { TransactionJournal } from './transaction-journal.entity'; + +@Entity() +export class Transactions extends BaseModel { + @Column() + amount: number; + + @Column() + status: statusTransaction; + + @Column() + type: typeTransaction; + + @Column({ + type: 'uuid', + nullable: true, + }) + user: string; + + @Column({ + nullable: true, + }) + user_destination: string; + + @ManyToOne(() => ProductHistoryPrice, (product) => product.id) + product_price: ProductHistoryPrice; + + @Column({ + nullable: true, + }) + image_prove: string; + + @Column({ + nullable: true, + }) + supplier_trx_id: string; + + @Column({ + nullable: true, + }) + phone_number: string; + + mark_up_price: number; + + userData: UserDetail; + + transactionJournal: TransactionJournal; +} diff --git a/src/transaction/ppob_callback.controller.ts b/src/transaction/ppob_callback.controller.ts index a6cd453..e7064ec 100644 --- a/src/transaction/ppob_callback.controller.ts +++ b/src/transaction/ppob_callback.controller.ts @@ -11,7 +11,7 @@ import { Req, } from '@nestjs/common'; import { TransactionService } from './transaction.service'; -import { CreateTransactionDto } from './dto/create-transaction.dto'; +import { DistributeTransactionDto } from './dto/distribute-transaction.dto'; import { FastifyRequest } from 'fastify'; @Controller({ @@ -24,7 +24,20 @@ export class PpobCallbackController { constructor(private readonly transactionService: TransactionService) {} @Get() - get(@Req() request: FastifyRequest) { + async get(@Req() request: FastifyRequest) { + const response = request.query; + + if (response['statuscode'] == 2) { + //TODO: UPDATE GAGAL + const updateTransaction = + await this.transactionService.callbackOrderFailed(response['clientid']); + } else { + //TODO: UPDATE BERHASIL + const updateTransaction = + await this.transactionService.callbackOrderSuccess( + response['clientid'], + ); + } this.logger.log({ requestQuery: request.query, }); diff --git a/src/transaction/transaction.controller.ts b/src/transaction/transaction.controller.ts index bf71aab..d540938 100644 --- a/src/transaction/transaction.controller.ts +++ b/src/transaction/transaction.controller.ts @@ -1,15 +1,20 @@ import { + Body, Controller, Get, - Post, - Body, - Patch, + HttpStatus, Param, - Delete, + ParseUUIDPipe, + Post, + Put, + Query, + Request, } from '@nestjs/common'; import { TransactionService } from './transaction.service'; -import { CreateTransactionDto } from './dto/create-transaction.dto'; -import { UpdateTransactionDto } from './dto/update-transaction.dto'; +import { DistributeTransactionDto } from './dto/distribute-transaction.dto'; +import { OrderTransactionDto } from './dto/order-transaction.dto'; +import { AddSaldoSupplier } from './dto/add-saldo-supplier.dto'; +import { DepositReturnDto } from './dto/deposit_return.dto'; @Controller({ path: 'transaction', @@ -18,31 +23,227 @@ import { UpdateTransactionDto } from './dto/update-transaction.dto'; export class TransactionController { constructor(private readonly transactionService: TransactionService) {} - @Post() - create(@Body() createTransactionDto: CreateTransactionDto) { - return this.transactionService.create(createTransactionDto); - } - - @Get() - findAll() { - return this.transactionService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.transactionService.findOne(+id); - } - - @Patch(':id') - update( - @Param('id') id: string, - @Body() updateTransactionDto: UpdateTransactionDto, + @Post('distribute') + async create( + @Body() createTransactionDto: DistributeTransactionDto, + @Request() req, ) { - return this.transactionService.update(+id, updateTransactionDto); + return { + data: await this.transactionService.distributeDeposit( + createTransactionDto, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; } - @Delete(':id') - remove(@Param('id') id: string) { - return this.transactionService.remove(+id); + @Post('distribute-admin') + async distributeAdmin( + @Body() createTransactionDto: DistributeTransactionDto, + @Request() req, + ) { + return { + data: await this.transactionService.distributeFromAdmin( + createTransactionDto, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('add-saldo-supplier') + async addSaldoSupplier( + @Body() addSaldoSupplier: AddSaldoSupplier, + @Request() req, + ) { + return { + data: await this.transactionService.addSupplierSaldo( + addSaldoSupplier, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('order') + async orderTransaction( + @Body() orderTransactionDto: OrderTransactionDto, + @Request() req, + ) { + return { + data: await this.transactionService.orderTransaction( + orderTransactionDto, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('order-prod') + async orderTransactionProd( + @Body() orderTransactionDto: OrderTransactionDto, + @Request() req, + ) { + return { + data: await this.transactionService.orderTransactionProd( + orderTransactionDto, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('deposit-return') + async depositReturn( + @Body() depositReturnDto: DepositReturnDto, + @Request() req, + ) { + return { + data: await this.transactionService.createDepositReturn( + req.user, + depositReturnDto, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Get('history') + async getHistoryTransactionUser( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Request() req, + ) { + const data = await this.transactionService.transactionHistoryByUser( + page, + req.user.userId, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('history-deposit') + async getHistoryDepositUser( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Query('user-destination') userDestination: string, + @Request() req, + ) { + const data = await this.transactionService.topUpHistoryByUser( + page, + req.user.userId, + userDestination, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('deposit-return') + async findDepositReturn( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Request() req, + ) { + const [data, count] = + await this.transactionService.getAllDepositReturnFromUser( + req.user.userId, + page, + pageSize, + ); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('total-order') + async findTotalOrder(@Request() req) { + const data = await this.transactionService.getTotalSell(); + + return { + data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('deposit-return/confirmation') + async findDepositReturnConfirmation( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Request() req, + ) { + const data = await this.transactionService.getAllDepositReturnToUser( + req.user.userId, + page, + pageSize, + ); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('deposit-return/confirmation/:id/:status') + async confirmDepositReturn( + @Param('id', ParseUUIDPipe) id: string, + @Param('status') status: string, + @Request() req, + ) { + return { + data: await this.transactionService.confirmationDepositReturn( + id, + req.user, + status, + ), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('deposit-return/confirmation-admin/:id') + async confirmDepositReturnAdmin( + @Param('id', ParseUUIDPipe) id: string, + @Request() req, + @Body() status: string, + ) { + return { + data: await this.transactionService.confirmationAdminDepositReturn( + id, + req.user, + status, + ), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('withdraw/:id') + async withdrawProfit(@Param('id', ParseUUIDPipe) id: string, @Request() req) { + return { + data: await this.transactionService.withdrawBenefit(id), + statusCode: HttpStatus.OK, + message: 'success', + }; } } diff --git a/src/transaction/transaction.module.ts b/src/transaction/transaction.module.ts index 7a8e1e8..13bae9e 100644 --- a/src/transaction/transaction.module.ts +++ b/src/transaction/transaction.module.ts @@ -1,10 +1,25 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TransactionService } from './transaction.service'; import { TransactionController } from './transaction.controller'; import { PpobCallbackController } from './ppob_callback.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { COA } from './entities/coa.entity'; +import { TransactionJournal } from './entities/transaction-journal.entity'; +import { Transactions } from './entities/transactions.entity'; +import { CoaService } from './coa.service'; +import { ProductModule } from '../product/product.module'; +import { UsersModule } from 'src/users/users.module'; +import { ConfigurableModule } from '../configurable/configurable.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([COA, TransactionJournal, Transactions]), + ProductModule, + ConfigurableModule, + forwardRef(() => UsersModule), + ], controllers: [TransactionController, PpobCallbackController], - providers: [TransactionService], + providers: [TransactionService, CoaService], + exports: [CoaService], }) export class TransactionModule {} diff --git a/src/transaction/transaction.service.ts b/src/transaction/transaction.service.ts index 0f49e43..dc64cee 100644 --- a/src/transaction/transaction.service.ts +++ b/src/transaction/transaction.service.ts @@ -1,26 +1,1121 @@ -import { Injectable } from '@nestjs/common'; -import { CreateTransactionDto } from './dto/create-transaction.dto'; -import { UpdateTransactionDto } from './dto/update-transaction.dto'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { DistributeTransactionDto } from './dto/distribute-transaction.dto'; +import { OrderTransactionDto } from './dto/order-transaction.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Transactions } from './entities/transactions.entity'; +import { Connection, EntityNotFoundError, Repository } from 'typeorm'; +import { COA } from './entities/coa.entity'; +import { TransactionJournal } from './entities/transaction-journal.entity'; +import { CoaService } from './coa.service'; +import * as uuid from 'uuid'; +import { uniq } from 'lodash'; +import { Decimal } from 'decimal.js'; +import { + balanceType, + coaType, + statusTransaction, + typeTransaction, +} from '../helper/enum-list'; +import { ProductService } from '../product/product.service'; +import { CreateJournalDto } from './dto/create-journal.dto'; +import { UsersService } from 'src/users/users.service'; +import { AddSaldoSupplier } from './dto/add-saldo-supplier.dto'; +import { SupplierService } from '../users/supplier/supplier.service'; +import { ProductHistoryPriceService } from '../product/history-price/history-price.service'; +import { CommissionService } from '../configurable/commission.service'; +import { DepositReturnDto } from './dto/deposit_return.dto'; +import { UserDetail } from '../users/entities/user_detail.entity'; +import { doTransaction } from '../helper/irs-api'; +import { ProductHistoryPrice } from '../product/entities/product-history-price.entity'; + +interface JournalEntry { + coa_id: string; + debit?: string; + credit?: string; +} @Injectable() export class TransactionService { - create(createTransactionDto: CreateTransactionDto) { - return 'This action adds a new transaction'; + private readonly logger = new Logger(TransactionService.name); + + constructor( + @InjectRepository(Transactions) + private transactionRepository: Repository, + @InjectRepository(TransactionJournal) + private transactionJournalRepository: Repository, + @InjectRepository(COA) + private coaRepository: Repository, + private coaService: CoaService, + private productService: ProductService, + private productHistoryPriceService: ProductHistoryPriceService, + private userService: UsersService, + private commissionService: CommissionService, + private supplierService: SupplierService, + private connection: Connection, + ) {} + + async addSupplierSaldo(addSaldoSupplier: AddSaldoSupplier, currentUser: any) { + const supplier = await this.supplierService.findByCode( + addSaldoSupplier.supplier, + ); + // GET COA + const coaBank = await this.coaService.findByName( + `${coaType[coaType.BANK]}-SYSTEM`, + ); + + const coaInventory = await this.coaService.findByName( + `${coaType[coaType.INVENTORY]}-${supplier.code}`, + ); + + const coaBudget = await this.coaService.findByName( + `${coaType[coaType.BUDGET]}-${supplier.code}`, + ); + + const coaContraBudget = await this.coaService.findByName( + `${coaType[coaType.CONTRA_BUDGET]}-${supplier.code}`, + ); + + //GET USER + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + await this.connection.transaction(async (manager) => { + //INSERT TRANSACTION + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = addSaldoSupplier.amount; + transactionData.user = userData.id; + transactionData.status = statusTransaction.SUCCESS; + transactionData.type = typeTransaction.DEPOSIT_SUPPLIER; + + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaBank.id, + credit: transactionData.amount, + }, + { + coa_id: coaInventory.id, + debit: transactionData.amount, + }, + { + coa_id: coaBudget.id, + debit: transactionData.amount, + }, + { + coa_id: coaContraBudget.id, + credit: transactionData.amount, + }, + ], + }); + }); + + return true; } - findAll() { - return `This action returns all transaction`; + async distributeFromAdmin( + distributeTransactionDto: DistributeTransactionDto, + currentUser: any, + ) { + //GET USER + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + const supplier = await this.supplierService.findByActive(); + + try { + if (userData.roles.name != 'Admin') { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Roles Not Admin', + }, + HttpStatus.NOT_ACCEPTABLE, + ); + } + + // GET COA + const coaBudget = await this.coaService.findByName( + `${coaType[coaType.BUDGET]}-${supplier.code}`, + ); + + if (coaBudget.amount < distributeTransactionDto.amount) { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: `Transaction Failed because saldo not enough`, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const coaContraBudget = await this.coaService.findByName( + `${coaType[coaType.CONTRA_BUDGET]}-${supplier.code}`, + ); + const coaAR = await this.coaService.findByTwoUser( + distributeTransactionDto.destination, + currentUser.userId, + coaType.ACCOUNT_RECEIVABLE, + ); + const coaWallet = await this.coaService.findByUser( + distributeTransactionDto.destination, + coaType.WALLET, + ); + + await this.connection.transaction(async (manager) => { + //INSERT TRANSACTION + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = distributeTransactionDto.amount; + transactionData.user = userData.id; + transactionData.user_destination = distributeTransactionDto.destination; + transactionData.status = statusTransaction.SUCCESS; + transactionData.type = typeTransaction.DISTRIBUTION; + + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaAR.id, + debit: transactionData.amount, + }, + { + coa_id: coaWallet.id, + credit: transactionData.amount, + }, + { + coa_id: coaBudget.id, + credit: transactionData.amount, + }, + { + coa_id: coaContraBudget.id, + debit: transactionData.amount, + }, + ], + }); + }); + + return true; + } catch (e) { + throw e; + } } - findOne(id: number) { - return `This action returns a #${id} transaction`; + async distributeDeposit( + distributeTransactionDto: DistributeTransactionDto, + currentUser: any, + ) { + //GET USER + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + // GET COA + const coaSenderWallet = await this.coaService.findByUser( + userData.id, + coaType.WALLET, + ); + + const coaAP = await this.coaService.findByUserWithRelated( + distributeTransactionDto.destination, + userData.id, + coaType.ACCOUNT_PAYABLE, + ); + + const coaReceiverWallet = await this.coaService.findByUser( + distributeTransactionDto.destination, + coaType.WALLET, + ); + + const coaAR = await this.coaService.findByUserWithRelated( + distributeTransactionDto.destination, + userData.id, + coaType.ACCOUNT_RECEIVABLE, + ); + + if (coaSenderWallet.amount < distributeTransactionDto.amount) { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: `Transaction Failed because saldo not enough`, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + await this.connection.transaction(async (manager) => { + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = distributeTransactionDto.amount; + transactionData.user = userData.id; + transactionData.user_destination = distributeTransactionDto.destination; + transactionData.status = statusTransaction.SUCCESS; + transactionData.type = typeTransaction.DISTRIBUTION; + + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaSenderWallet.id, + debit: transactionData.amount, + }, + { + coa_id: coaReceiverWallet.id, + credit: transactionData.amount, + }, + { + coa_id: coaAR.id, + debit: transactionData.amount, + }, + { + coa_id: coaAP.id, + credit: transactionData.amount, + }, + ], + }); + }); + + return true; } - update(id: number, updateTransactionDto: UpdateTransactionDto) { - return `This action updates a #${id} transaction`; + async orderTransaction( + orderTransactionDto: OrderTransactionDto, + currentUser: any, + ) { + //GET USER + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + //GET PRODUCT + const product = await this.productService.findOne( + orderTransactionDto.productCode, + ); + + const product_price = await this.productHistoryPriceService.findOne( + product.id, + userData.partner?.id, + ); + + let supervisorData = []; + + let profit = product_price.mark_up_price; + + if (!userData.partner) { + //GET SALES + supervisorData = await this.calculateCommission( + supervisorData, + profit, + userData, + ); + profit = supervisorData + .map((item) => { + return item.credit; + }) + .reduce((prev, curr) => { + return prev + curr; + }, 0); + } + + //GET COA + const coaAccount = await this.coaService.findByUser( + userData.id, + coaType.WALLET, + ); + + const coaInventory = await this.coaService.findByName( + `${coaType[coaType.INVENTORY]}-${product.supplier.code}`, + ); + + const coaCostOfSales = await this.coaService.findByName( + `${coaType[coaType.COST_OF_SALES]}-${product.supplier.code}`, + ); + + const coaSales = await this.coaService.findByName( + `${coaType[coaType.SALES]}-SYSTEM`, + ); + + const coaExpense = await this.coaService.findByName( + `${coaType[coaType.EXPENSE]}-SYSTEM`, + ); + + if (coaAccount.amount <= product.price) { + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: `Transaction Failed because saldo not enough`, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + try { + await this.connection.transaction(async (manager) => { + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = + product_price.mark_up_price + product_price.price; + transactionData.user = userData.id; + transactionData.status = statusTransaction.SUCCESS; + transactionData.type = typeTransaction.ORDER; + transactionData.product_price = product_price; + transactionData.phone_number = orderTransactionDto.phoneNumber; + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaInventory.id, + credit: product_price.price, + }, + { + coa_id: coaCostOfSales.id, + debit: product_price.price, + }, + { + coa_id: coaAccount.id, + debit: product_price.mark_up_price + product_price.price, + }, + { + coa_id: coaSales.id, + credit: product_price.mark_up_price + product_price.price, + }, + { + coa_id: coaExpense.id, + debit: userData.partner ? 0 : profit, + }, + ].concat(supervisorData), + }); + }); + } catch (e) { + throw e; + } + + return true; } - remove(id: number) { - return `This action removes a #${id} transaction`; + async orderTransactionProd( + orderTransactionDto: OrderTransactionDto, + currentUser: any, + ) { + //TODO GET USER DATA + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + //TODO GET PRODUCT AND PRICE + const product = await this.productService.findOne( + orderTransactionDto.productCode, + ); + + const product_price = await this.productHistoryPriceService.findOne( + product.id, + userData.partner?.id, + ); + + //TODO HIT API SUPPLIER + const trxId = Array(6) + .fill(null) + .map(() => Math.round(Math.random() * 16).toString(16)) + .join(''); + + const hitSupplier = await doTransaction( + orderTransactionDto.productCode, + orderTransactionDto.phoneNumber, + trxId, + ); + + this.logger.log({ + responseAPISupplier: hitSupplier, + }); + + //TODO TRANSACTION DAT + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = product_price.mark_up_price + product_price.price; + transactionData.user = userData.id; + transactionData.type = typeTransaction.ORDER; + transactionData.product_price = product_price; + transactionData.phone_number = orderTransactionDto.phoneNumber; + transactionData.supplier_trx_id = trxId; + + if (!hitSupplier.success) { + transactionData.status = statusTransaction.FAILED; + await this.transactionRepository.insert(transactionData); + throw new HttpException( + { + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: hitSupplier.msg, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } else { + transactionData.status = statusTransaction.PENDING; + await this.transactionRepository.insert(transactionData); + } + + if (hitSupplier.harga > product_price.price) { + product_price.endDate = new Date(); + + let newProductPrice = new ProductHistoryPrice(); + newProductPrice = product_price; + newProductPrice.id = uuid.v4(); + newProductPrice.price = hitSupplier.harga; + + await this.productHistoryPriceService.create(product_price); + await this.productHistoryPriceService.create(newProductPrice); + } + + return hitSupplier; + } + + async createDepositReturn(currentUser, depositReturnDto: DepositReturnDto) { + const userData = await this.userService.findByUsername( + currentUser.username, + ); + + try { + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = depositReturnDto.amount; + transactionData.user = userData.id; + transactionData.user_destination = depositReturnDto.destination; + transactionData.status = statusTransaction.PENDING; + transactionData.type = typeTransaction.DEPOSIT_RETURN; + transactionData.image_prove = depositReturnDto.image_prove; + await this.connection.transaction(async (manager) => { + await manager.insert(Transactions, transactionData); + }); + + return transactionData; + } catch (e) { + throw e; + } + } + + async confirmationDepositReturn( + id: string, + userData, + statusApproval: string, + ) { + const transactionData = await this.findApprovalDepositReturn(id); + + const coaSenderWallet = await this.coaService.findByUser( + transactionData.user, + coaType.WALLET, + ); + + const coaAP = await this.coaService.findByUserWithRelated( + transactionData.user, + userData.userId, + coaType.ACCOUNT_PAYABLE, + ); + + const coaReceiverWallet = await this.coaService.findByUser( + transactionData.user, + coaType.WALLET, + ); + + const coaAR = await this.coaService.findByUserWithRelated( + transactionData.user, + userData.userId, + coaType.ACCOUNT_RECEIVABLE, + ); + + try { + await this.connection.transaction(async (manager) => { + transactionData.status = + statusApproval === 'accept' + ? statusTransaction.APPROVED + : statusTransaction.REJECTED; + + await manager.save(transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaSenderWallet.id, + credit: transactionData.amount, + }, + { + coa_id: coaReceiverWallet.id, + debit: transactionData.amount, + }, + { + coa_id: coaAR.id, + credit: transactionData.amount, + }, + { + coa_id: coaAP.id, + debit: transactionData.amount, + }, + ], + }); + }); + + return transactionData; + } catch (e) { + throw e; + } + + return transactionData; + } + + async confirmationAdminDepositReturn( + id: string, + userData, + statusApproval: string, + ) { + const transactionData = await this.findApprovalDepositReturn(id); + + const coaAR = await this.coaService.findByUserWithRelated( + userData.userId, + transactionData.user_destination, + coaType.ACCOUNT_RECEIVABLE, + ); + + const coaBank = await this.coaService.findByName( + `${coaType[coaType.BANK]}-SYSTEM`, + ); + + try { + await this.connection.transaction(async (manager) => { + transactionData.status = + statusApproval === 'Accept' + ? statusTransaction.APPROVED + : statusTransaction.REJECTED; + + await manager.save(transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaAR.id, + credit: transactionData.amount, + }, + { + coa_id: coaBank.id, + debit: transactionData.amount, + }, + ], + }); + }); + + return transactionData; + } catch (e) { + throw e; + } + + return transactionData; + } + + async callbackOrderFailed(supplier_trx_id: string) { + const dataTransaction = await this.transactionRepository.findOne({ + where: { + supplier_trx_id: supplier_trx_id, + }, + }); + dataTransaction.status = statusTransaction.FAILED; + + await this.transactionRepository.save(dataTransaction); + } + + async callbackOrderSuccess(supplier_trx_id: string) { + const dataTransaction = await this.transactionRepository.findOne({ + where: { + supplier_trx_id: supplier_trx_id, + }, + }); + dataTransaction.status = statusTransaction.FAILED; + + const userData = await this.userService.findExist(dataTransaction.user); + + let supervisorData = []; + + const product_price = await this.productHistoryPriceService.findById( + dataTransaction.product_price.id, + ); + + const product = await this.productService.findOneById( + product_price.product.id, + ); + + let profit = product_price.mark_up_price; + + if (!userData.partner) { + //GET SALES + supervisorData = await this.calculateCommission( + supervisorData, + profit, + userData, + ); + profit = supervisorData + .map((item) => { + return item.credit; + }) + .reduce((prev, curr) => { + return prev + curr; + }, 0); + } + + //GET COA + const coaAccount = await this.coaService.findByUser( + userData.id, + coaType.WALLET, + ); + + const coaInventory = await this.coaService.findByName( + `${coaType[coaType.INVENTORY]}-${product.supplier.code}`, + ); + + const coaCostOfSales = await this.coaService.findByName( + `${coaType[coaType.COST_OF_SALES]}-${product.supplier.code}`, + ); + + const coaSales = await this.coaService.findByName( + `${coaType[coaType.SALES]}-SYSTEM`, + ); + + const coaExpense = await this.coaService.findByName( + `${coaType[coaType.EXPENSE]}-SYSTEM`, + ); + + try { + await this.connection.transaction(async (manager) => { + await manager.save(dataTransaction); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: dataTransaction, + amount: dataTransaction.amount, + journals: [ + { + coa_id: coaInventory.id, + credit: product_price.price, + }, + { + coa_id: coaCostOfSales.id, + debit: product_price.price, + }, + { + coa_id: coaAccount.id, + debit: product_price.mark_up_price + product_price.price, + }, + { + coa_id: coaSales.id, + credit: product_price.mark_up_price + product_price.price, + }, + { + coa_id: coaExpense.id, + debit: userData.partner ? 0 : profit, + }, + ].concat(supervisorData), + }); + }); + } catch (e) { + throw e; + } + } + + async transactionHistoryByUser( + page: number, + user: string, + pageSize?: number, + ) { + const baseQuery = this.transactionRepository + .createQueryBuilder('transaction') + .select('transaction.id', 'id') + .addSelect('transaction.created_at', 'created_at') + .where('transaction.user = :id and transaction.type = 1', { + id: user, + }) + .leftJoin('transaction.product_price', 'product_price') + .leftJoin('product_price.product', 'product') + .addSelect('product_price.mark_up_price', 'mark_up_price') + .addSelect('product.name', 'name') + .addSelect('product.id', 'product_id'); + + // .leftJoinAndSelect('transaction.product_price', 'product_price') + // .leftJoinAndSelect('product_price.product', 'product'); + + const data = await baseQuery + .offset(page * (pageSize || 10)) + .limit(pageSize || 10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async topUpHistoryByUser( + page: number, + user: string, + destinationUser: string, + pageSize?: number, + ) { + const baseQuery = this.transactionRepository + .createQueryBuilder('transaction') + .where( + 'transaction.user = :id and transaction.type = 0 and transaction.user_destination = :destinationId', + { + id: user, + destinationId: destinationUser, + }, + ) + .select(['id', 'created_at as transaction_date', 'amount']); + + const data = await baseQuery + .offset(page * (pageSize || 10)) + .limit(pageSize || 10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findApprovalDepositReturn(id: string) { + try { + return await this.transactionRepository.findOneOrFail({ + where: { + id: id, + type: typeTransaction.DEPOSIT_RETURN, + status: statusTransaction.PENDING, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Return Deposit not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async getAllDepositReturnFromUser( + user: string, + page: number, + pageSize?: number, + ) { + return this.transactionRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + where: { + user: user, + type: typeTransaction.DEPOSIT_RETURN, + }, + order: { + createdAt: 'DESC', + }, + }); + } + + async getAllDepositReturnToUser( + user: string, + page: number, + pageSize?: number, + ) { + const baseQuery = this.transactionRepository + .createQueryBuilder('transaction') + .where('transaction.user_destination = :id and transaction.type = 3', { + id: user, + }) + .leftJoinAndMapOne( + 'transaction.userData', + UserDetail, + 'userData', + 'userData.user = transaction.user', + ) + .select('transaction.id', 'id') + .addSelect([ + 'transaction.created_at', + 'image_prove', + 'amount', + 'status', + 'userData.name', + ]); + + const data = await baseQuery + .offset(page * (pageSize || 10)) + .limit(pageSize || 10) + .getRawMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + return this.transactionRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + where: { + user_destination: user, + type: typeTransaction.DEPOSIT_RETURN, + }, + order: { + createdAt: 'DESC', + }, + }); + } + + async withdrawBenefit(user) { + const userData = await this.userService.findExist(user); + + const coaProfit = await this.coaService.findByUser(user, coaType.PROFIT); + + const coaBank = await this.coaService.findByName( + `${coaType[coaType.BANK]}-SYSTEM`, + ); + + try { + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = coaProfit.amount; + transactionData.user = userData.id; + transactionData.status = statusTransaction.APPROVED; + transactionData.type = typeTransaction.WITHDRAW; + + await this.connection.transaction(async (manager) => { + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaBank.id, + credit: transactionData.amount, + }, + { + coa_id: coaProfit.id, + debit: transactionData.amount, + }, + ], + }); + }); + + return transactionData; + } catch (e) { + throw e; + } + } + + async getTotalSell() { + const { total_amount } = await this.transactionRepository + .createQueryBuilder('transactions') + .select('SUM(transactions.amount) as total_amount') + .getRawOne(); + + return parseInt(total_amount); + } + + async getTotalProfit() { + const { total_amount } = await this.transactionRepository + .createQueryBuilder('transactions') + .select('SUM(transactions.amount) as total_amount') + .getRawOne(); + + return parseInt(total_amount); + } + + async calculateCommission(data, totalPrice, userData) { + const supervisorData = []; + + supervisorData.push( + await this.userService.findByUsername(userData.superior.username), + ); + + //GET Supervisor + supervisorData.push( + await this.userService.findByUsername( + supervisorData[0].superior.username, + ), + ); + + return Promise.all( + supervisorData.map(async (it) => { + const coaAccount = await this.coaService.findByUser( + it.id, + coaType.PROFIT, + ); + const commissionValue = await this.commissionService.findOne( + it.roles.id, + ); + + return { + coa_id: coaAccount.id, + credit: (totalPrice * commissionValue.commission) / 100, + }; + }), + ); + } + + async accountingTransaction(createJournalDto: CreateJournalDto) { + const creditSum = createJournalDto.journals + .map((it) => { + return it.credit; + }) + .filter((it) => { + return it; + }) + .reduce((a, b) => { + return a.plus(b); + }, new Decimal(0)); + const debitSum = createJournalDto.journals + .map((it) => { + return it.debit; + }) + .filter((it) => { + return it; + }) + .reduce((a, b) => { + return a.plus(b); + }, new Decimal(0)); + const coaIds = uniq( + createJournalDto.journals.map((it) => { + return it.coa_id; + }), + ); + + if (!creditSum.equals(debitSum)) { + throw new Error(`credit and debit doesn't match`); + } + + const coas = await this.coaRepository.findByIds(coaIds); + + const transaction = createJournalDto.transaction; + + await Promise.all( + createJournalDto.journals.map((journal) => { + const coa = coas.find((it) => { + return it.id === journal.coa_id; + }); + + if (!coa) { + throw new Error(`coa ${journal.coa_id} not found`); + } + + const journalEntry = new TransactionJournal(); + + journalEntry.coa = coa; + journalEntry.type = journal.debit + ? balanceType.DEBIT + : balanceType.CREDIT; + journalEntry.amount = journal.debit ? journal.debit : journal.credit; + journalEntry.transaction_head = transaction; + + return createJournalDto.transactionalEntityManager.save(journalEntry); + }), + ); + + await Promise.all( + coaIds.map((coaId) => { + const journalPerCoa = createJournalDto.journals.filter((journal) => { + return journal.coa_id == coaId; + }); + + const creditSum = journalPerCoa + .map((it) => { + return it.credit; + }) + .filter((it) => { + return it; + }) + .reduce((a, b) => { + return a.plus(b); + }, new Decimal(0)); + const debitSum = journalPerCoa + .map((it) => { + return it.debit; + }) + .filter((it) => { + return it; + }) + .reduce((a, b) => { + return a.plus(b); + }, new Decimal(0)); + + const coa = coas.find((it) => { + return it.id.toLowerCase() === coaId.toLowerCase(); + }); + + let balance = new Decimal(coa.amount); + + if (coa.balanceType == balanceType.DEBIT) { + balance = balance.plus(debitSum.minus(creditSum)); + } else if (coa.balanceType == balanceType.CREDIT) { + balance = balance.plus(creditSum.minus(debitSum)); + } + + const diff = balance.minus(new Decimal(coa.amount)); + + return createJournalDto.transactionalEntityManager + .createQueryBuilder() + .update(COA) + .set({ + amount: () => { + return `amount + ${diff.toString()}`; + }, + }) + .where('id = :id', { id: coa.id }) + .execute(); + }), + ); + + return transaction; } } diff --git a/src/users/dto/create-partner.dto.ts b/src/users/dto/create-partner.dto.ts new file mode 100644 index 0000000..a4f1bc9 --- /dev/null +++ b/src/users/dto/create-partner.dto.ts @@ -0,0 +1,24 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreatePartnerDto { + @IsNotEmpty() + name: string; + + @IsNotEmpty() + address: string; + + @IsNotEmpty() + owner: string; + + @IsNotEmpty() + code: string; + + @IsNotEmpty() + npwp: string; + + @IsNotEmpty() + password_account: string; + + @IsNotEmpty() + phone_number: string; +} diff --git a/src/users/dto/create-supplier.dto.ts b/src/users/dto/create-supplier.dto.ts new file mode 100644 index 0000000..8a2e585 --- /dev/null +++ b/src/users/dto/create-supplier.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreateSupplierDto { + @IsNotEmpty() + name: string; + + @IsNotEmpty() + code: string; +} diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 8b676ac..d2defaf 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,9 +1,30 @@ -import { IsNotEmpty } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsUUID, ValidateIf } from 'class-validator'; +import { Partner } from '../entities/partner.entity'; +import { Column } from 'typeorm'; export class CreateUserDto { @IsNotEmpty() - firstName: string; + username: string; @IsNotEmpty() - lastName: string; + name: string; + + @IsNotEmpty() + phone_number: string; + + @IsNotEmpty() + password: string; + + @IsUUID() + roleId: string; + + @IsNotEmpty() + superior: boolean; + + partner: Partner; + // @ValidateIf((o) => { + // return !!o.superior; + // }) + // @IsUUID() + // superior: string; } diff --git a/src/users/dto/update-partner.dto.ts b/src/users/dto/update-partner.dto.ts new file mode 100644 index 0000000..2e2f4c1 --- /dev/null +++ b/src/users/dto/update-partner.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreatePartnerDto } from './create-partner.dto'; + +export class UpdatePartnerDto extends PartialType(CreatePartnerDto) { +} diff --git a/src/users/dto/update-supplier.dto.ts b/src/users/dto/update-supplier.dto.ts new file mode 100644 index 0000000..40a7218 --- /dev/null +++ b/src/users/dto/update-supplier.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateSupplierDto } from './create-supplier.dto'; + +export class UpdateSupplierDto extends PartialType(CreateSupplierDto) {} diff --git a/src/users/entities/partner.entity.ts b/src/users/entities/partner.entity.ts new file mode 100644 index 0000000..dba4b0e --- /dev/null +++ b/src/users/entities/partner.entity.ts @@ -0,0 +1,32 @@ +import { Roles } from 'src/configurable/entities/roles.entity'; +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { hashPassword } from '../../helper/hash_password'; + +@Entity() +export class Partner extends BaseModel { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column({ + nullable: true, + }) + code: string; + + @Column() + npwp: string; + + @Column() + address: string; + + @Column({ + nullable: true, + }) + phone_number: string; + + @Column({ default: true }) + status: boolean; +} diff --git a/src/users/entities/supplier.entity.ts b/src/users/entities/supplier.entity.ts new file mode 100644 index 0000000..b265ca3 --- /dev/null +++ b/src/users/entities/supplier.entity.ts @@ -0,0 +1,24 @@ +import { Roles } from 'src/configurable/entities/roles.entity'; +import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { hashPassword } from '../../helper/hash_password'; +import { COA } from '../../transaction/entities/coa.entity'; + +@Entity() +export class Supplier extends BaseModel { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + code: string; + + @Column() + status: boolean; + + coa: COA; + + coa_undistribute: COA; +} diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index 417e96f..4f6427f 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -1,45 +1,65 @@ +import { Roles } from 'src/configurable/entities/roles.entity'; import { - Entity, Column, + Entity, + ManyToOne, + OneToOne, PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, } from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; +import { Partner } from './partner.entity'; +import { UserDetail } from './user_detail.entity'; +import { COA } from '../../transaction/entities/coa.entity'; @Entity() -export class User { +export class User extends BaseModel { @PrimaryGeneratedColumn('uuid') id: string; @Column() - firstName: string; + username: string; @Column() - lastName: string; + password: string; + + @Column() + salt: string; @Column({ default: true }) isActive: boolean; - @CreateDateColumn({ - type: 'timestamp with time zone', - nullable: false, - }) - createdAt: Date; + @ManyToOne( + () => { + return User; + }, + (user) => { + return user.id; + }, + ) + superior: User; - @UpdateDateColumn({ - type: 'timestamp with time zone', - nullable: false, - }) - updatedAt: Date; + @ManyToOne( + () => { + return Roles; + }, + (roles) => { + return roles.id; + }, + ) + roles: Roles; - @DeleteDateColumn({ - type: 'timestamp with time zone', - nullable: true, - }) - deletedAt: Date; + @ManyToOne( + () => { + return Partner; + }, + (partner) => { + return partner.id; + }, + ) + partner: Partner; - @VersionColumn() - version: number; + @OneToOne(() => UserDetail, (userDetail) => userDetail.user) + userDetail: UserDetail; + + coa: COA; } diff --git a/src/users/entities/userDetail.entity.ts b/src/users/entities/userDetail.entity.ts deleted file mode 100644 index 579325b..0000000 --- a/src/users/entities/userDetail.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, -} from 'typeorm'; - -@Entity() -export class User { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - firstName: string; -} diff --git a/src/users/entities/user_detail.entity.ts b/src/users/entities/user_detail.entity.ts new file mode 100644 index 0000000..cb67b8c --- /dev/null +++ b/src/users/entities/user_detail.entity.ts @@ -0,0 +1,24 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { User } from './user.entity'; + +@Entity() +export class UserDetail { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + phone_number: string; + + @OneToOne(() => User, (user) => user.userDetail) + @JoinColumn() + user: User; +} diff --git a/src/users/partner/partner.service.spec.ts b/src/users/partner/partner.service.spec.ts new file mode 100644 index 0000000..81edeba --- /dev/null +++ b/src/users/partner/partner.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PartnerService } from './partner.service'; + +describe('PartnerService', () => { + let service: PartnerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PartnerService], + }).compile(); + + service = module.get(PartnerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/users/partner/partner.service.ts b/src/users/partner/partner.service.ts new file mode 100644 index 0000000..cf2422f --- /dev/null +++ b/src/users/partner/partner.service.ts @@ -0,0 +1,159 @@ +import { + forwardRef, + HttpException, + HttpStatus, + Inject, + Injectable, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Connection, EntityNotFoundError, Not, Repository } from 'typeorm'; +import { CoaService } from '../../transaction/coa.service'; +import { CreatePartnerDto } from '../dto/create-partner.dto'; +import { Partner } from '../entities/partner.entity'; +import * as uuid from 'uuid'; +import { UsersService } from '../users.service'; +import { CreateUserDto } from '../dto/create-user.dto'; +import { UpdatePartnerDto } from '../dto/update-partner.dto'; +import { UpdateUserDto } from '../dto/update-user.dto'; +import { when } from 'joi'; + +@Injectable() +export class PartnerService { + constructor( + @InjectRepository(Partner) + private partnerRepository: Repository, + @Inject( + forwardRef(() => { + return CoaService; + }), + ) + private coaService: CoaService, + private userService: UsersService, + private connection: Connection, + ) {} + + async create(createPartnerDto: CreatePartnerDto, currentUser: any) { + const check = await this.partnerRepository.findOne({ + npwp: createPartnerDto.npwp, + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'NPWP Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const partnerData = new Partner(); + + partnerData.id = uuid.v4(); + partnerData.name = createPartnerDto.name; + partnerData.npwp = createPartnerDto.npwp; + partnerData.address = createPartnerDto.address; + partnerData.code = createPartnerDto.code; + partnerData.status = true; + + await this.connection.transaction(async (manager) => { + const result = await manager.insert(Partner, partnerData); + }); + + const dataUser = new CreateUserDto(); + + dataUser.username = `admin_${partnerData.name}`; + dataUser.name = partnerData.name; + dataUser.phone_number = partnerData.phone_number; + dataUser.roleId = '21dceea2-416e-4b55-b74c-12605e1f8d1b'; + dataUser.superior = false; + dataUser.partner = partnerData; + dataUser.password = createPartnerDto.password_account; + dataUser.phone_number = createPartnerDto.phone_number; + + await this.userService.create(dataUser, currentUser); + + return partnerData; + } + + async update( + id: string, + updatePartnerDto: UpdatePartnerDto, + currentUser: any, + ) { + const check = await this.partnerRepository.findOne({ + npwp: updatePartnerDto.npwp, + id: Not(id), + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'NPWP Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const partnerData = new Partner(); + + partnerData.id = id; + partnerData.name = updatePartnerDto.name; + partnerData.address = updatePartnerDto.address; + + if (updatePartnerDto.npwp) { + partnerData.npwp = updatePartnerDto.npwp; + } + + await this.connection.transaction(async (manager) => { + await manager.update(Partner, { id: id }, partnerData); + }); + + const dataUser = new UpdateUserDto(); + const userData = await this.userService.findOneByPartner(id); + + dataUser.username = `admin_${partnerData.name}`; + dataUser.partner = partnerData; + + await this.userService.update(userData.id, dataUser, currentUser); + + return partnerData; + } + + setStatus = async (id: string, type: string) => { + const partnerData = await this.partnerRepository.findOne({ + id: id, + }); + + if (type === 'active') { + partnerData.status = true; + } else { + partnerData.status = false; + } + + await this.connection.transaction(async (manager) => { + await manager.update(Partner, { id: id }, partnerData); + }); + + return partnerData; + }; + + async findOne(code: string) { + return await this.partnerRepository.findOne({ + where: { + code: code, + }, + }); + } + + findAllPartner(page, pageSize?) { + return this.partnerRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + order: { + version: 'DESC', + }, + }); + } +} diff --git a/src/users/supplier/supplier.service.spec.ts b/src/users/supplier/supplier.service.spec.ts new file mode 100644 index 0000000..9eb9cce --- /dev/null +++ b/src/users/supplier/supplier.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SupplierService } from './supplier.service'; + +describe('PartnerService', () => { + let service: SupplierService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SupplierService], + }).compile(); + + service = module.get(SupplierService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/users/supplier/supplier.service.ts b/src/users/supplier/supplier.service.ts new file mode 100644 index 0000000..e322890 --- /dev/null +++ b/src/users/supplier/supplier.service.ts @@ -0,0 +1,216 @@ +import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Connection, EntityNotFoundError, Not, Repository } from 'typeorm'; +import { Supplier } from '../entities/supplier.entity'; +import { InputCoaDto } from '../../transaction/dto/input-coa.dto'; +import { balanceType, coaType } from '../../helper/enum-list'; +import { CreateSupplierDto } from '../dto/create-supplier.dto'; +import { CoaService } from '../../transaction/coa.service'; +import * as uuid from 'uuid'; +import { UpdateSupplierDto } from '../dto/update-supplier.dto'; +import { COA } from '../../transaction/entities/coa.entity'; + +@Injectable() +export class SupplierService { + constructor( + @InjectRepository(Supplier) + private supplierRepository: Repository, + @Inject( + forwardRef(() => { + return CoaService; + }), + ) + private coaService: CoaService, + private connection: Connection, + ) {} + + async create(createSupplierDto: CreateSupplierDto) { + const check = await this.supplierRepository.findOne({ + code: createSupplierDto.code, + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Supplier Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const supplierData = new Supplier(); + supplierData.id = uuid.v4(); + supplierData.name = createSupplierDto.name; + supplierData.code = createSupplierDto.code; + supplierData.status = false; + + await this.connection.transaction(async (manager) => { + const result = await manager.insert(Supplier, supplierData); + + const dataCoaInventory = new InputCoaDto(); + dataCoaInventory.supplier = supplierData; + dataCoaInventory.balanceType = balanceType.DEBIT; + dataCoaInventory.type = coaType.INVENTORY; + dataCoaInventory.coaEntityManager = manager; + await this.coaService.create(dataCoaInventory); + + const dataCoaCostOfSales = new InputCoaDto(); + dataCoaCostOfSales.supplier = supplierData; + dataCoaCostOfSales.balanceType = balanceType.DEBIT; + dataCoaCostOfSales.type = coaType.COST_OF_SALES; + dataCoaCostOfSales.coaEntityManager = manager; + await this.coaService.create(dataCoaCostOfSales); + + const dataCoaBudget = new InputCoaDto(); + dataCoaBudget.supplier = supplierData; + dataCoaBudget.balanceType = balanceType.DEBIT; + dataCoaBudget.type = coaType.BUDGET; + dataCoaBudget.coaEntityManager = manager; + await this.coaService.create(dataCoaBudget); + + const dataCoaContraBudget = new InputCoaDto(); + dataCoaContraBudget.supplier = supplierData; + dataCoaContraBudget.balanceType = balanceType.CREDIT; + dataCoaContraBudget.type = coaType.CONTRA_BUDGET; + dataCoaContraBudget.coaEntityManager = manager; + await this.coaService.create(dataCoaContraBudget); + }); + + return supplierData; + } + + async update(id: string, updateSupplierDto: UpdateSupplierDto) { + const check = await this.supplierRepository.findOne({ + code: updateSupplierDto.code, + id: Not(id), + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Supplier Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const supplierData = new Supplier(); + + supplierData.name = updateSupplierDto.name; + supplierData.status = true; + + await this.connection.transaction(async (manager) => { + await manager.update(Supplier, { id: id }, supplierData); + }); + + return supplierData; + } + + setStatus = async (id: string, type: string) => { + const supplierData = new Supplier(); + + if (type === 'active') { + supplierData.status = true; + } else { + supplierData.status = false; + } + + await this.connection.transaction(async (manager) => { + await manager.update(Supplier, { id: id }, supplierData); + }); + + return supplierData; + }; + + async findAllSupplier(page, pageSize?) { + const baseQuery = this.supplierRepository + .createQueryBuilder('supplier') + .leftJoinAndMapOne( + 'supplier.coa', + COA, + 'coa', + `coa.supplier = supplier.id and coa.type = '2'`, + ) + .leftJoinAndMapOne( + 'supplier.coa_undistribute', + COA, + 'coa_undistribute', + `coa_undistribute.supplier = supplier.id and coa_undistribute.type = '9'`, + ) + .select(['supplier', 'coa.amount', 'coa_undistribute.amount']); + + const data = await baseQuery + .skip(page * (pageSize || 10)) + .take(pageSize || 10) + .getMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findByCode(code: string) { + try { + return await this.supplierRepository.findOneOrFail({ + code: code, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Supplier not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByActive() { + try { + return await this.supplierRepository.findOneOrFail({ + status: true, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Supplier Data not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findByActiveAll() { + try { + return await this.supplierRepository.find({ + status: true, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Supplier Data not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 7e45858..a127b27 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,37 +1,185 @@ import { - Controller, - Get, - Post, Body, - Put, - Param, + Controller, Delete, - ParseUUIDPipe, + Get, HttpStatus, + Param, + ParseUUIDPipe, + Post, + Put, + Query, + Request, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { Public } from '../auth/public.decorator'; +import { CreateSupplierDto } from './dto/create-supplier.dto'; +import { SupplierService } from './supplier/supplier.service'; +import { PartnerService } from './partner/partner.service'; +import { CreatePartnerDto } from './dto/create-partner.dto'; +import { UpdatePartnerDto } from './dto/update-partner.dto'; +import { UpdateSupplierDto } from './dto/update-supplier.dto'; @Controller({ path: 'users', version: '1', }) export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor( + private readonly usersService: UsersService, + private readonly supplierService: SupplierService, + private readonly partnerService: PartnerService, + ) {} @Post() - async create(@Body() createUserDto: CreateUserDto) { + async create(@Request() req, @Body() createUserDto: CreateUserDto) { return { - data: await this.usersService.create(createUserDto), + data: await this.usersService.create(createUserDto, req.user), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('supplier') + async createSupplier(@Body() createPartnerDto: CreateSupplierDto) { + return { + data: await this.supplierService.create(createPartnerDto), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Get('supplier/:id/:type') + async updateSupplier( + @Param('id', ParseUUIDPipe) id: string, + @Param('type') type: string, + ) { + return { + data: await this.supplierService.setStatus(id, type), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Put('supplier/:id') + async setStatusSupplier( + @Param('id', ParseUUIDPipe) id: string, + @Body() updatePartnerDto: UpdateSupplierDto, + ) { + return { + data: await this.supplierService.update(id, updatePartnerDto), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('partner') + async createPartner( + @Request() req, + @Body() createPartnerDto: CreatePartnerDto, + ) { + return { + data: await this.partnerService.create(createPartnerDto, req.user), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Get('partner/:id/:type') + async setStatusPartner( + @Param('id', ParseUUIDPipe) id: string, + @Param('type') type: string, + ) { + return { + data: await this.partnerService.setStatus(id, type), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Put('partner/:id') + async updatePartner( + @Param('id', ParseUUIDPipe) id: string, + @Request() req, + @Body() updatePartnerDto: UpdatePartnerDto, + ) { + return { + data: await this.partnerService.update(id, updatePartnerDto, req.user), statusCode: HttpStatus.CREATED, message: 'success', }; } @Get() - async findAll() { - const [data, count] = await this.usersService.findAll(); + async findAll(@Request() req, @Query('page') page: number) { + const data = await this.usersService.findAll(page, req.user.userId); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Public() + @Get('supplier') + async findAllSupplier( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Request() req, + ) { + const data = await this.supplierService.findAllSupplier(page, pageSize); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('partner') + async findAllPartner( + @Query('page') page: number, + @Query('pageSize') pageSize: number, + @Request() req, + ) { + const [data, count] = await this.partnerService.findAllPartner( + page, + pageSize, + ); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('find-by-supperior') + async findBySuperrior(@Request() req, @Query('page') page: number) { + const data = await this.usersService.findBySuperrior(req.user.userId, page); + + return { + ...data, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('find-by-roles/:id') + async findByRoles( + @Param('id', ParseUUIDPipe) id: string, + @Query('page') page: number, + @Query('pageSize') pageSize: number, + ) { + const [data, count] = await this.usersService.findByRoles( + id, + page, + pageSize, + ); return { data, @@ -50,13 +198,39 @@ export class UsersController { }; } + @Get(':id/:type') + async setStatusMembership( + @Param('id', ParseUUIDPipe) id: string, + @Param('type') type: string, + ) { + return { + data: await this.usersService.setStatus(id, type), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + @Put(':id') async update( @Param('id', ParseUUIDPipe) id: string, + @Request() req, @Body() updateUserDto: UpdateUserDto, ) { return { - data: await this.usersService.update(id, updateUserDto), + data: await this.usersService.update(id, updateUserDto, req.user), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Put('change-password/:id') + async updatePassword( + @Param('id', ParseUUIDPipe) id: string, + @Request() req, + @Body() updateUserDto: UpdateUserDto, + ) { + return { + data: await this.usersService.updatePassword(id, updateUserDto, req.user), statusCode: HttpStatus.OK, message: 'success', }; diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 1c38291..449f2cc 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,12 +1,24 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { User } from './entities/user.entity'; +import { TransactionModule } from 'src/transaction/transaction.module'; +import { ConfigurableModule } from 'src/configurable/configurable.module'; +import { SupplierService } from './supplier/supplier.service'; +import { Supplier } from './entities/supplier.entity'; +import { Partner } from './entities/partner.entity'; +import { PartnerService } from './partner/partner.service'; +import { UserDetail } from './entities/user_detail.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [ + TypeOrmModule.forFeature([User, Supplier, Partner, UserDetail]), + forwardRef(() => TransactionModule), + ConfigurableModule, + ], controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, SupplierService, PartnerService], + exports: [UsersService, SupplierService, PartnerService], }) export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 0ea0727..197d5e9 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,28 +1,224 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + forwardRef, + HttpException, + HttpStatus, + Inject, + Injectable, +} from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { EntityNotFoundError, Repository } from 'typeorm'; +import { Connection, EntityNotFoundError, Not, Repository } from 'typeorm'; import { User } from './entities/user.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; +import { hashPassword } from '../helper/hash_password'; +import { CoaService } from 'src/transaction/coa.service'; +import { balanceType, coaType } from 'src/helper/enum-list'; +import { RoleService } from 'src/configurable/roles.service'; +import { InputCoaDto } from 'src/transaction/dto/input-coa.dto'; +import * as uuid from 'uuid'; +import { UserDetail } from './entities/user_detail.entity'; +import { COA } from '../transaction/entities/coa.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, + @InjectRepository(UserDetail) + private userDetailRepository: Repository, + @Inject( + forwardRef(() => { + return CoaService; + }), + ) + private coaService: CoaService, + private roleService: RoleService, + private connection: Connection, ) {} - async create(createUserDto: CreateUserDto) { - const result = await this.usersRepository.insert(createUserDto); + async create(createUserDto: CreateUserDto, currentUser: any) { + const roles = await this.roleService.findOne(createUserDto.roleId); + const superior = await this.findByUsername(currentUser.username); - return this.usersRepository.findOneOrFail(result.identifiers[0].id); + const check = await this.usersRepository.findOne({ + username: createUserDto.username, + }); + + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Username Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + const salt = randomStringGenerator(); + + const userData = new User(); + + userData.id = uuid.v4(); + userData.username = createUserDto.username; + userData.password = await hashPassword(createUserDto.password, salt); + userData.salt = salt; + if (createUserDto.superior) { + userData.superior = superior; + } else { + userData.superior = null; + userData.partner = createUserDto.partner; + } + userData.roles = roles; + + await this.connection.transaction(async (manager) => { + const result = await manager.insert(User, userData); + + const userDetailData = new UserDetail(); + userDetailData.name = createUserDto.name; + userDetailData.phone_number = createUserDto.phone_number; + userDetailData.user = userData; + const user_detail = await manager.insert(UserDetail, userDetailData); + + const dataCoaWallet = new InputCoaDto(); + dataCoaWallet.user = userData; + dataCoaWallet.balanceType = balanceType.CREDIT; + dataCoaWallet.type = coaType.WALLET; + dataCoaWallet.coaEntityManager = manager; + await this.coaService.create(dataCoaWallet); + + const dataCoaAR = new InputCoaDto(); + dataCoaAR.user = userData; + dataCoaAR.balanceType = balanceType.DEBIT; + dataCoaAR.relatedUserId = superior.id; + dataCoaAR.type = coaType.ACCOUNT_RECEIVABLE; + dataCoaAR.coaEntityManager = manager; + await this.coaService.create(dataCoaAR); + + if (roles.name == 'Supervisor' || roles.name == 'Sales') { + const dataCOAProfit = new InputCoaDto(); + dataCOAProfit.user = userData; + dataCOAProfit.balanceType = balanceType.CREDIT; + dataCOAProfit.type = coaType.PROFIT; + dataCOAProfit.coaEntityManager = manager; + await this.coaService.create(dataCOAProfit); + } + + if (createUserDto.superior) { + const dataCoaAP = new InputCoaDto(); + dataCoaAP.user = userData; + dataCoaAP.balanceType = balanceType.CREDIT; + dataCoaAP.relatedUserId = superior.id; + dataCoaAP.type = coaType.ACCOUNT_PAYABLE; + dataCoaAP.coaEntityManager = manager; + await this.coaService.create(dataCoaAP); + } + }); + + return userData; } - findAll() { - return this.usersRepository.findAndCount(); + async findAll(page: number, id: string) { + const baseQuery = this.usersRepository + .createQueryBuilder('user') + .where('user.id != :id', { + id: id, + }) + .leftJoinAndSelect('user.roles', 'roles', `roles.id = user.roles_id`) + .leftJoinAndMapOne( + 'user.user_detail', + UserDetail, + 'user_detail', + `user_detail.user = user.id`, + ) + .leftJoinAndMapOne( + 'user.coa', + COA, + 'coa', + `coa.user = user.id and coa.type = '0'`, + ) + .select([ + 'user.id', + 'user.username', + 'user.isActive', + 'user.createdAt', + 'roles.id', + 'roles.name', + 'user_detail', + 'coa.amount', + ]); + + const data = await baseQuery + .orderBy('user.createdAt', 'DESC') + .skip(page * 10) + .take(10) + .getMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; } - async findOne(id: string) { + findByRoles(relationId: string, page: number, pageSize?: number) { + return this.usersRepository.findAndCount({ + skip: page * (pageSize || 10), + take: pageSize || 10, + where: { + roles: relationId, + }, + order: { + updatedAt: 'DESC', + }, + }); + } + + async findBySuperrior(superriorId: string, page: number) { + const baseQuery = this.usersRepository + .createQueryBuilder('user') + .where('user.id != :id and user.superior_id = :superior', { + id: superriorId, + superior: superriorId, + }) + .leftJoinAndSelect('user.roles', 'roles', `roles.id = user.roles_id`) + .leftJoinAndMapOne( + 'user.user_detail', + UserDetail, + 'user_detail', + `user_detail.user = user.id`, + ) + .leftJoinAndMapOne( + 'user.coa', + COA, + 'coa', + `coa.user = user.id and coa.type = '0'`, + ) + .select([ + 'user.id', + 'user.username', + 'user.isActive', + 'roles.id', + 'roles.name', + 'user_detail', + 'coa.amount', + ]); + + const data = await baseQuery + .skip(page * 10) + .take(10) + .getMany(); + + const totalData = await baseQuery.getCount(); + + return { + data, + count: totalData, + }; + } + + async findExist(id: string) { try { return await this.usersRepository.findOneOrFail(id); } catch (e) { @@ -30,7 +226,7 @@ export class UsersService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'User not found', }, HttpStatus.NOT_FOUND, ); @@ -40,15 +236,92 @@ export class UsersService { } } - async update(id: string, updateUserDto: UpdateUserDto) { + async findByUsername(username: string) { try { - await this.usersRepository.findOneOrFail(id); + return await this.usersRepository.findOneOrFail({ + where: { + username: username, + }, + relations: ['superior', 'roles', 'partner'], + }); } catch (e) { if (e instanceof EntityNotFoundError) { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'User not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async findOne(id: string) { + const coa = await this.coaService.findByUser(id, coaType.WALLET); + + try { + const userData = await this.usersRepository + .createQueryBuilder('users') + .leftJoinAndSelect('users.roles', 'roles') + .leftJoinAndSelect('users.superior', 'superior') + .leftJoinAndSelect('users.userDetail', 'userDetail') + .where('users.id = :id', { + id: id, + }) + .select([ + 'users.id', + 'users.username', + 'users.isActive', + 'users.createdAt', + 'roles.id', + 'roles.name', + 'superior.id', + 'superior.username', + 'userDetail.id', + 'userDetail.name', + 'userDetail.phone_number', + ]) + .getOne(); + + return { + ...userData, + wallet: coa.amount, + }; + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'User not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + async update(id: string, updateUserDto: UpdateUserDto, currentUser: any) { + let userData; + let userDetailData; + + try { + userData = await this.usersRepository.findOneOrFail(id); + userDetailData = await this.userDetailRepository.findOneOrFail({ + where: { + user: userData, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'User not found', }, HttpStatus.NOT_FOUND, ); @@ -57,11 +330,78 @@ export class UsersService { } } - const result = await this.usersRepository.update(id, updateUserDto); + const check = await this.usersRepository.findOne({ + username: updateUserDto.username, + id: Not(id), + }); - return this.usersRepository.findOneOrFail(id); + if (check) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_ACCEPTABLE, + error: 'Username Already Exist', + }, + HttpStatus.NOT_FOUND, + ); + } + + userData.username = updateUserDto.username; + userData.partner = updateUserDto.partner; + userDetailData.name = updateUserDto.name; + userDetailData.phone_number = updateUserDto.phone_number; + + await this.connection.transaction(async (manager) => { + await manager.save(userData); + await manager.save(userDetailData); + }); + + return userData; } + async updatePassword( + id: string, + updateUserDto: UpdateUserDto, + currentUser: any, + ) { + try { + const dataUser = await this.usersRepository.findOneOrFail(id); + dataUser.password = await hashPassword( + updateUserDto.password, + dataUser.salt, + ); + const result = await this.usersRepository.save(dataUser); + return dataUser; + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'User not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } + + setStatus = async (id: string, type: string) => { + const userData = new User(); + + if (type === 'active') { + userData.isActive = true; + } else { + userData.isActive = false; + } + + await this.connection.transaction(async (manager) => { + await manager.update(User, { id: id }, userData); + }); + + return userData; + }; + async remove(id: string) { try { await this.usersRepository.findOneOrFail(id); @@ -70,7 +410,7 @@ export class UsersService { throw new HttpException( { statusCode: HttpStatus.NOT_FOUND, - error: 'Data not found', + error: 'User not found', }, HttpStatus.NOT_FOUND, ); @@ -81,4 +421,37 @@ export class UsersService { await this.usersRepository.delete(id); } + + async findOneByUsername(username: string) { + return this.usersRepository.findOneOrFail({ + where: { + username, + isActive: true, + }, + relations: ['roles', 'partner'], + }); + } + + async findOneByPartner(partnerId: string) { + try { + return this.usersRepository.findOneOrFail({ + relations: ['roles'], + where: { + partner: partnerId, + }, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'User not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } + } } diff --git a/yarn.lock b/yarn.lock index 80c017f..039e2b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,11 +675,24 @@ tslib "2.3.1" uuid "8.3.2" +"@nestjs/jwt@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-8.0.0.tgz#6c811c17634252dd1dcd5dabf409dbd692b812da" + integrity sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ== + dependencies: + "@types/jsonwebtoken" "8.5.4" + jsonwebtoken "8.5.1" + "@nestjs/mapped-types@*": version "1.0.0" resolved "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.0.tgz" integrity sha512-26AW5jHadLXtvHs+M+Agd9KZ92dDlBrmD0rORlBlvn2KvsWs4JRaKl2mUsrW7YsdZeAu3Hc4ukqyYyDdyCmMWQ== +"@nestjs/passport@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/passport/-/passport-8.0.1.tgz#f1ed39a19489f794d1fe3fef592b4523bc48da68" + integrity sha512-vn/ZJLXQKvSf9D0BvEoNFJLfzl9AVqfGtDyQMfWDLbaNpoEB2FyeaHGxdiX6H71oLSrQV78c/yuhfantzwdjdg== + "@nestjs/platform-express@^8.0.0": version "8.2.3" resolved "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.2.3.tgz" @@ -904,7 +917,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.13": +"@types/express@*", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -958,11 +971,32 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jsonwebtoken@*": + version "8.5.6" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.6.tgz#1913e5a61e70a192c5a444623da4901a7b1a9d42" + integrity sha512-+P3O/xC7nzVizIi5VbF34YtqSonFsdnbXBnWUCYRiKOi1f9gA4sEFvXkrGr/QVV23IbMYvcoerI7nnhDUiWXRQ== + dependencies: + "@types/node" "*" + +"@types/jsonwebtoken@8.5.4": + version "8.5.4" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz#50ccaf0aa6f5d7b9956e70fe323b76e582991913" + integrity sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg== + dependencies: + "@types/node" "*" + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + "@types/node@*", "@types/node@^16.0.0": version "16.11.10" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.10.tgz#2e3ad0a680d96367103d3e670d41c2fed3da61ae" @@ -973,6 +1007,39 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/passport-jwt@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/passport-jwt/-/passport-jwt-3.0.6.tgz#41cc8b5803d5f5f06eb33e19c453b42716def4f1" + integrity sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ== + dependencies: + "@types/express" "*" + "@types/jsonwebtoken" "*" + "@types/passport-strategy" "*" + +"@types/passport-local@^1.0.34": + version "1.0.34" + resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.34.tgz#84d3b35b2fd4d36295039ded17fe5f3eaa62f4f6" + integrity sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport-strategy" "*" + +"@types/passport-strategy@*": + version "0.2.35" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.35.tgz#e52f5212279ea73f02d9b06af67efe9cefce2d0c" + integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.7.tgz#85892f14932168158c86aecafd06b12f5439467a" + integrity sha512-JtswU8N3kxBYgo+n9of7C97YQBT+AYPP2aBfNGTzABqPAZnK/WOAaKfh3XesUYMZRrXFuoPc2Hv0/G/nQFveHw== + dependencies: + "@types/express" "*" + "@types/prettier@^2.1.5": version "2.4.2" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" @@ -1435,6 +1502,16 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +args@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" + integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ== + dependencies: + camelcase "5.0.0" + chalk "2.4.2" + leven "2.1.0" + mri "1.1.4" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" @@ -1475,7 +1552,7 @@ avvio@^7.1.2: fastq "^1.6.1" queue-microtask "^1.1.2" -axios@0.24.0: +axios@0.24.0, axios@^0.24.0: version "0.24.0" resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz" integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== @@ -1567,6 +1644,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + body-parser@1.19.0: version "1.19.0" resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz" @@ -1628,6 +1710,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" @@ -1680,6 +1767,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camelcase@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -1695,6 +1787,15 @@ caniuse-lite@^1.0.30001280: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz#8573685bdae4d733ef18f78d44ba0ca5fe9e896b" integrity sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg== +chalk@2.4.2, chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -1714,15 +1815,6 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -1872,6 +1964,11 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.7: + version "2.0.16" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" + integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + colors@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -1996,6 +2093,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -2013,6 +2115,13 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csv-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/csv-parser/-/csv-parser-3.0.0.tgz#b88a6256d79e090a97a1b56451f9327b01d710e7" + integrity sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ== + dependencies: + minimist "^1.2.0" + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2022,6 +2131,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -2036,7 +2150,7 @@ debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" -decimal.js@^10.2.1: +decimal.js@^10.2.1, decimal.js@^10.3.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== @@ -2147,6 +2261,13 @@ duplexify@^4.1.2: readable-stream "^3.1.1" stream-shift "^1.0.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -2729,7 +2850,7 @@ fresh@0.5.2: resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-extra@10.0.0: +fs-extra@10.0.0, fs-extra@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== @@ -2758,6 +2879,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fs@^0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" + integrity sha1-invTcYa23d84E/I4WLV+yq9eQdQ= + fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -3631,6 +3757,11 @@ joi@^17.4.2: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -3742,11 +3873,49 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +leven@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -3820,6 +3989,36 @@ lodash.has@4.5.2: resolved "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz" integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -3830,6 +4029,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.set@4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz" @@ -3991,6 +4195,11 @@ mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mri@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" + integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -4006,6 +4215,11 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multer@1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz" @@ -4266,6 +4480,34 @@ parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +passport-jwt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065" + integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg== + dependencies: + jsonwebtoken "^8.2.0" + passport-strategy "^1.0.0" + +passport-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.5.0.tgz#7914aaa55844f9dce8c3aa28f7d6b73647ee0169" + integrity sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4306,6 +4548,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + pg-connection-string@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz" @@ -4367,7 +4614,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== -pino-abstract-transport@v0.5.0: +pino-abstract-transport@^0.5.0, pino-abstract-transport@v0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz#4b54348d8f73713bfd14e3dc44228739aa13d9c0" integrity sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ== @@ -4385,6 +4632,24 @@ pino-http@^6.3.0: pino "^7.0.5" pino-std-serializers "^5.0.0" +pino-pretty@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-7.3.0.tgz#277fdc2306a2f6d727a1127e7747d1c078efdd4b" + integrity sha512-HAhShJ2z2QzxXhYAn6XfwYpF13o1PQbjzSNA9q+30FAvhjOmeACit9lprhV/mCOw/8YFWSyyNk0YCq2EDYGYpw== + dependencies: + args "^5.0.1" + colorette "^2.0.7" + dateformat "^4.6.3" + fast-safe-stringify "^2.0.7" + joycon "^3.1.1" + pino-abstract-transport "^0.5.0" + pump "^3.0.0" + readable-stream "^3.6.0" + rfdc "^1.3.0" + secure-json-parse "^2.4.0" + sonic-boom "^2.2.0" + strip-json-comments "^3.1.1" + pino-std-serializers@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz" @@ -4720,7 +4985,7 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.1.4, rfdc@^1.2.0: +rfdc@^1.1.4, rfdc@^1.2.0, rfdc@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== @@ -4815,7 +5080,7 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -secure-json-parse@^2.0.0: +secure-json-parse@^2.0.0, secure-json-parse@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz" integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg== @@ -4832,6 +5097,11 @@ semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -4953,6 +5223,13 @@ sonic-boom@^1.0.2: atomic-sleep "^1.0.0" flatstr "^1.0.12" +sonic-boom@^2.2.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.4.1.tgz#6e1c3762c677425c6ffbd7bd106c4f8258b45b39" + integrity sha512-WgtVLfGl347/zS1oTuLaOAvVD5zijgjphAJHgbbnBJGgexnr+C1ULSj0j7ftoGxpuxR4PaV635NkwFemG8m/5w== + dependencies: + atomic-sleep "^1.0.0" + sonic-boom@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.3.1.tgz#e6572184fb3adf145dbfeccff48141bbd1009e4c"