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..6d3e967 100644 --- a/k8s/staging/deployment.yaml +++ b/k8s/staging/deployment.yaml @@ -15,7 +15,7 @@ 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: diff --git a/package.json b/package.json index 637bced..68ed9e8 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,23 @@ "@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", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", + "crypto": "^1.0.1", + "decimal.js": "^10.3.1", "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", "reflect-metadata": "^0.1.13", @@ -47,6 +56,8 @@ "@types/express": "^4.17.13", "@types/jest": "^27.0.1", "@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..22dd095 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,8 +6,10 @@ 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'; @Module({ imports: [ @@ -19,10 +21,11 @@ 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), }), @@ -49,6 +52,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..b3eb1bb --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common'; +import { LocalAuthGuard } from './local-auth.guard'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './jwt-auth.guard'; +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); + } + + @UseGuards(JwtAuthGuard) + @Get('profile') + getProfile(@Request() req) { + return req.user; + } +} 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..e0212c1 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,37 @@ +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, + }; + + return { + access_token: this.jwtService.sign(payload), + }; + } +} 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..08eccd1 --- /dev/null +++ b/src/auth/jwt.strategy.ts @@ -0,0 +1,19 @@ +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..1b088c3 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, 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..8d4d3ca 100644 --- a/src/configurable/configurable.controller.ts +++ b/src/configurable/configurable.controller.ts @@ -7,20 +7,20 @@ import { Param, Delete, ParseUUIDPipe, - HttpStatus, + HttpStatus, Query, } from '@nestjs/common'; -import { ConfigurableService } from './configurable.service'; +import { RoleService } from './roles.service'; @Controller({ path: 'config', version: '1', }) export class ConfigurableController { - constructor(private readonly usersService: ConfigurableService) {} + constructor(private readonly usersService: RoleService) {} @Get() - async findAll() { - const [data, count] = await this.usersService.findAll(); + async findAll(@Query('page') page: number) { + const [data, count] = await this.usersService.findAllRoles(page); return { data, diff --git a/src/configurable/configurable.module.ts b/src/configurable/configurable.module.ts index da4f11e..5325245 100644 --- a/src/configurable/configurable.module.ts +++ b/src/configurable/configurable.module.ts @@ -2,11 +2,12 @@ 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'; @Module({ imports: [TypeOrmModule.forFeature([Roles])], controllers: [ConfigurableController], - providers: [ConfigurableService], + providers: [RoleService], + exports: [RoleService] }) 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/roles.service.ts similarity index 79% rename from src/configurable/configurable.service.ts rename to src/configurable/roles.service.ts index 5236a1b..9404376 100644 --- a/src/configurable/configurable.service.ts +++ b/src/configurable/roles.service.ts @@ -4,14 +4,20 @@ import { Roles } from './entities/roles.entity'; import { InjectRepository } from '@nestjs/typeorm'; @Injectable() -export class ConfigurableService { +export class RoleService { constructor( @InjectRepository(Roles) private rolesRepository: Repository, ) {} - findAll() { - return this.rolesRepository.findAndCount(); + findAllRoles(page) { + return this.rolesRepository.findAndCount({ + skip: page * 10, + take: 10, + order: { + version: 'DESC', + }, + }); } async findOne(id: string) { diff --git a/src/helper/enum-list.ts b/src/helper/enum-list.ts new file mode 100644 index 0000000..303ff60 --- /dev/null +++ b/src/helper/enum-list.ts @@ -0,0 +1,33 @@ +export enum statusTransaction { + PENDING, + SUCCESS, + FAILED, +} + +export enum typeTransaction { + DISTRIBUTION, + ORDER, + DEPOSIT_IRS, +} + +export enum productType { + NORMAL, + PROMO, +} + +export enum coaType { + WALLET, + INCOME, + INVENTORY, + COST_OF_SALES, + SALES, + BANK, + EXPENSE, + ACCOUNT_RECEIVABLE, + ACCOUNT_PAYABLE +} + +export enum balanceType { + DEBIT, + CREDIT, +} \ No newline at end of file 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-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..f8a3c67 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { 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'; diff --git a/src/product/dto/create-product.dto.ts b/src/product/dto/product/create-product.dto.ts similarity index 75% rename from src/product/dto/create-product.dto.ts rename to src/product/dto/product/create-product.dto.ts index ed442af..be1ab0c 100644 --- a/src/product/dto/create-product.dto.ts +++ b/src/product/dto/product/create-product.dto.ts @@ -10,6 +10,12 @@ export class CreateProductDto { @IsNotEmpty() status: string; + @IsNotEmpty() + price: number; + + @IsNotEmpty() + markUpPrice: number; + @IsUUID() subCategoriesId: 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/sub-categories/create-sub-categories-product.dto.ts b/src/product/dto/sub-categories/create-sub-categories-product.dto.ts index 6fb097d..385db53 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,9 @@ import { IsNotEmpty, IsUUID } from 'class-validator'; import { CreateCategoriesProductDto } from '../categories/create-categories-product.dto'; export class CreateSubCategoriesProductDto extends CreateCategoriesProductDto { + @IsNotEmpty() + name: string; + @IsUUID() categoryId: string; } diff --git a/src/product/entities/product-history-price.entity.ts b/src/product/entities/product-history-price.entity.ts index c744910..e7d7d16 100644 --- a/src/product/entities/product-history-price.entity.ts +++ b/src/product/entities/product-history-price.entity.ts @@ -2,20 +2,11 @@ import { Entity, Column, PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, - OneToMany, ManyToOne, } from 'typeorm'; import { Product } from './product.entity'; import { BaseModel } from '../../config/basemodel.entity'; - -enum Type { - NORMAL, - PROMO, -} +import { productType } from '../../helper/enum-list'; @Entity() export class ProductHistoryPrice extends BaseModel { @@ -23,14 +14,23 @@ export class ProductHistoryPrice extends BaseModel { id: string; @ManyToOne(() => Product, (product) => product.id) - productId: string; + product: Product; + + @Column() + price: number; + + @Column() + markUpPrice: 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..6cfd1bd 100644 --- a/src/product/entities/product-sub-category.entity.ts +++ b/src/product/entities/product-sub-category.entity.ts @@ -2,17 +2,15 @@ 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; @@ -21,4 +19,7 @@ export class ProductSubCategories extends BaseModel{ @ManyToOne(() => ProductCategories, (categories) => categories.subCategories) category: ProductCategories; + + @OneToMany(() => Product, (product) => product.subCategories) + product: Product; } diff --git a/src/product/entities/product.entity.ts b/src/product/entities/product.entity.ts index 78b9696..f754f38 100644 --- a/src/product/entities/product.entity.ts +++ b/src/product/entities/product.entity.ts @@ -13,7 +13,7 @@ import { ProductSubCategories } from './product-sub-category.entity'; import { BaseModel } from '../../config/basemodel.entity'; @Entity() -export class Product extends BaseModel{ +export class Product extends BaseModel { @PrimaryGeneratedColumn('uuid') id: string; @@ -26,9 +26,21 @@ export class Product extends BaseModel{ @Column() status: string; + @Column() + price: number; + + @Column({ + nullable: true, + }) + basePrice: number; + @ManyToOne( - () => ProductSubCategories, - (subCategories) => subCategories.category, + () => { + return ProductSubCategories; + }, + (subCategories) => { + return subCategories.product; + }, ) subCategories: ProductSubCategories; } diff --git a/src/product/product-sub-categories.service.ts b/src/product/product-sub-categories.service.ts index 6674eb0..cd36294 100644 --- a/src/product/product-sub-categories.service.ts +++ b/src/product/product-sub-categories.service.ts @@ -4,19 +4,26 @@ 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 categories = await this.productCategoriesService.findOne( + createSubCategoriesProductDto.categoryId, ); + const result = await this.productSubCategoriesRepository.insert({ + name: createSubCategoriesProductDto.name, + category: categories, + }); + return this.productSubCategoriesRepository.findOneOrFail( result.identifiers[0].id, ); @@ -70,11 +77,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); } diff --git a/src/product/product.controller.ts b/src/product/product.controller.ts index b00e6b2..691f3e1 100644 --- a/src/product/product.controller.ts +++ b/src/product/product.controller.ts @@ -7,11 +7,18 @@ import { Param, Delete, ParseUUIDPipe, - HttpStatus, Query, + HttpStatus, + Query, } 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'; @Controller({ path: 'product', @@ -21,8 +28,18 @@ export class ProductController { constructor( private readonly productService: ProductService, private readonly productCategoriesService: ProductCategoriesService, + private readonly productSubCategoriesService: ProductSubCategoriesService, ) {} + @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,6 +53,19 @@ export class ProductController { }; } + @Post('sub-categories') + async createSubCategories( + @Body() createSubCategoriesProductDto: CreateSubCategoriesProductDto, + ) { + return { + data: await this.productSubCategoriesService.create( + createSubCategoriesProductDto, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + @Get() async findAll(@Query('page') page: number) { const [data, count] = await this.productService.findAll(page); @@ -48,6 +78,45 @@ export class ProductController { }; } + @Get('by-categories') + async findByCategories( + @Query('page') page: number, + @Query('categories') categories: string, + ) { + const [data, count] = await this.productService.findAll(page); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('categories') + async findAllCategories(@Query('page') page: number) { + const [data, count] = await this.productCategoriesService.findAll(page); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('sub-categories') + async findAllSubCategories(@Query('page') page: number) { + const [data, count] = await this.productSubCategoriesService.findAll(page); + + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + @Get(':id') async findOne(@Param('id', ParseUUIDPipe) id: string) { return { @@ -57,4 +126,90 @@ export class ProductController { }; } + @Get('categories/:id') + async findOneCategories(@Param('id', ParseUUIDPipe) id: string) { + return { + data: await this.productCategoriesService.findOne(id), + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('sub-categories/:id') + async findOneSubCategories(@Param('id', ParseUUIDPipe) id: string) { + return { + data: await this.productSubCategoriesService.findOne(id), + 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..e514519 100644 --- a/src/product/product.module.ts +++ b/src/product/product.module.ts @@ -2,9 +2,28 @@ import { 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'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + Product, + ProductCategories, + ProductHistoryPrice, + ProductSubCategories, + ]), + ], controllers: [ProductController], - providers: [ProductService, ProductCategoriesService], + providers: [ + ProductService, + ProductCategoriesService, + ProductSubCategoriesService, + ], + exports: [ProductService], }) export class ProductModule {} diff --git a/src/product/product.service.ts b/src/product/product.service.ts index 3b05d59..ac5f9bb 100644 --- a/src/product/product.service.ts +++ b/src/product/product.service.ts @@ -1,28 +1,65 @@ import { HttpException, HttpStatus, Injectable } 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'; -@Injectable() export class ProductService { constructor( @InjectRepository(Product) private productRepository: Repository, + @InjectRepository(ProductHistoryPrice) + private productHistoryPrice: Repository, + private productSubCategoriesService: ProductSubCategoriesService, ) {} 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, + subCategories: subCategories, + price: createProductDto.price, + }); + + await this.productHistoryPrice.insert({ + product: result.identifiers[0], + type: productType.NORMAL, + price: createProductDto.price, + markUpPrice: createProductDto.markUpPrice, + startDate: new Date(), + endDate: null, + }); return this.productRepository.findOneOrFail(result.identifiers[0].id); } findAll(page) { return this.productRepository.findAndCount({ + skip: page * 10, + relations: ['subCategories'], + take: 10, + order: { + version: 'DESC', + }, + }); + } + + findAllByCategories(page, categories) { + return this.productRepository.findAndCount({ + join: { + alias: 'subCategories', + innerJoin: { subCategories: 'roles.users' }, + }, skip: page * 10, take: 10, order: { @@ -31,9 +68,9 @@ export class ProductService { }); } - async findOne(id: string) { + async findOne(code: string) { try { - return await this.productRepository.findOneOrFail(id); + return await this.productRepository.findOneOrFail({ code: code }); } catch (e) { if (e instanceof EntityNotFoundError) { throw new HttpException( @@ -48,4 +85,73 @@ export class ProductService { } } } + + 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: 'Data 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, + subCategories: 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, + markUpPrice: 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: 'Data 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..d828f5f --- /dev/null +++ b/src/transaction/coa.service.ts @@ -0,0 +1,89 @@ +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(() => UsersService)) + private userService: UsersService, + ) {} + + async create(inputCoaDto: InputCoaDto) { + const user = inputCoaDto.user + let coaData = new COA(); + coaData.user = user.id; + coaData.name = coaType[inputCoaDto.type] + '-' + user.username; + 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: 'Data 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..6f27f52 --- /dev/null +++ b/src/transaction/dto/add-saldo-supplier.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { balanceType, coaType, statusTransaction, typeTransaction } from 'src/helper/enum-list'; +import { EntityManager } from 'typeorm'; + + +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/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..5e068dc --- /dev/null +++ b/src/transaction/dto/input-coa.dto.ts @@ -0,0 +1,21 @@ +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'; + +export class InputCoaDto { + @IsUUID() + user: User; + + @IsNotEmpty() + type: coaType; + + @IsNotEmpty() + balanceType: balanceType; + + @IsUUID() + relatedUserId: string; + + @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..462682c --- /dev/null +++ b/src/transaction/dto/order-transaction.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; + +export class OrderTransactionDto { + @IsNotEmpty() + productCode: 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..a463b68 --- /dev/null +++ b/src/transaction/entities/coa.entity.ts @@ -0,0 +1,29 @@ +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() + user: string; + + @Column({ + nullable:true + }) + relatedUser: 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..ad5ba0a --- /dev/null +++ b/src/transaction/entities/transaction-journal.entity.ts @@ -0,0 +1,42 @@ +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 { TransactionType } from './transaction-type.entity'; +import { balanceType } from '../../helper/enum-list'; + +@Entity() +export class TransactionJournal extends BaseModel { + @Column('text') + type: balanceType; + + @Column() + amount: number; + + @OneToOne( + () => { + return Transactions; + }, + (trans) => { + return trans.id; + }, + ) + transaction: Transactions; + + @ManyToOne( + () => { + return COA; + }, + (coa) => { + return coa.id; + }, + ) + coa: COA; +} diff --git a/src/transaction/entities/transaction-type.entity.ts b/src/transaction/entities/transaction-type.entity.ts new file mode 100644 index 0000000..c146c85 --- /dev/null +++ b/src/transaction/entities/transaction-type.entity.ts @@ -0,0 +1,11 @@ +import { + Entity, + Column, +} from 'typeorm'; +import { BaseModel } from '../../config/basemodel.entity'; + +@Entity() +export class TransactionType extends BaseModel { + @Column() + name: string; +} 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..af235db --- /dev/null +++ b/src/transaction/entities/transactions.entity.ts @@ -0,0 +1,34 @@ +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'; + +@Entity() +export class Transactions extends BaseModel { + @Column() + amount: number; + + @Column() + status: statusTransaction; + + @Column() + type: typeTransaction; + + @Column() + user: string; + + @Column({ + nullable: true, + }) + userDestination: string; +} diff --git a/src/transaction/ppob_callback.controller.ts b/src/transaction/ppob_callback.controller.ts index a6cd453..0044656 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({ diff --git a/src/transaction/transaction.controller.ts b/src/transaction/transaction.controller.ts index bf71aab..1d60c85 100644 --- a/src/transaction/transaction.controller.ts +++ b/src/transaction/transaction.controller.ts @@ -6,10 +6,14 @@ import { Patch, Param, Delete, + Request, + HttpStatus, } from '@nestjs/common'; import { TransactionService } from './transaction.service'; -import { CreateTransactionDto } from './dto/create-transaction.dto'; +import { DistributeTransactionDto } from './dto/distribute-transaction.dto'; +import { OrderTransactionDto } from './dto/order-transaction.dto'; import { UpdateTransactionDto } from './dto/update-transaction.dto'; +import { AddSaldoSupplier } from './dto/add-saldo-supplier.dto'; @Controller({ path: 'transaction', @@ -18,31 +22,55 @@ 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') + create( + @Body() createTransactionDto: DistributeTransactionDto, + @Request() req, ) { - return this.transactionService.update(+id, updateTransactionDto); + return this.transactionService.distributeDeposit( + createTransactionDto, + req.user, + ); } - @Delete(':id') - remove(@Param('id') id: string) { - return this.transactionService.remove(+id); + @Post('distribute-admin') + distributeAdmin( + @Body() createTransactionDto: DistributeTransactionDto, + @Request() req, + ) { + return { + data: 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.addIRSWallet( + addSaldoSupplier, + req.user, + ), + statusCode: HttpStatus.CREATED, + message: 'success', + }; + } + + @Post('order') + orderTransaction( + @Body() orderTransactionDto: OrderTransactionDto, + @Request() req, + ) { + return this.transactionService.orderTransaction( + orderTransactionDto, + req.user, + ); } } diff --git a/src/transaction/transaction.module.ts b/src/transaction/transaction.module.ts index 7a8e1e8..8b1f631 100644 --- a/src/transaction/transaction.module.ts +++ b/src/transaction/transaction.module.ts @@ -1,10 +1,29 @@ -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 { TransactionType } from './entities/transaction-type.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'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + TransactionType, + COA, + TransactionJournal, + Transactions, + ]), + ProductModule, + 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..e4d07a2 100644 --- a/src/transaction/transaction.service.ts +++ b/src/transaction/transaction.service.ts @@ -1,26 +1,461 @@ -import { Injectable } from '@nestjs/common'; -import { CreateTransactionDto } from './dto/create-transaction.dto'; -import { UpdateTransactionDto } from './dto/update-transaction.dto'; +import { HttpException, HttpStatus, Injectable } 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, EntityManager, Repository } from 'typeorm'; +import { COA } from './entities/coa.entity'; +import { TransactionType } from './entities/transaction-type.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 * as irsService from '../helper/irs-service'; +import { CreateJournalDto } from './dto/create-journal.dto'; +import { UsersService } from 'src/users/users.service'; +import { AddSaldoSupplier } from './dto/add-saldo-supplier.dto'; + +interface JournalEntry { + coa_id: string; + debit?: string; + credit?: string; +} @Injectable() export class TransactionService { - create(createTransactionDto: CreateTransactionDto) { - return 'This action adds a new transaction'; + constructor( + @InjectRepository(Transactions) + private transactionRepository: Repository, + @InjectRepository(TransactionType) + private transactionTypeRepository: Repository, + @InjectRepository(TransactionJournal) + private transactionJournalRepository: Repository, + @InjectRepository(COA) + private coaRepository: Repository, + private coaService: CoaService, + private productService: ProductService, + private userService: UsersService, + private connection: Connection, + ) {} + + async addIRSWallet(addSaldoSupplier: AddSaldoSupplier, currentUser: any) { + // GET COA + const coaBank = await this.coaService.findByName( + `${coaType[coaType.BANK]}-SYSTEM`, + ); + + const coaInventory = await this.coaService.findByName( + `${coaType[coaType.INVENTORY]}-${addSaldoSupplier.supplier}`, + ); + + //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_IRS; + + await manager.insert(Transactions, transactionData); + + await this.accountingTransaction({ + createTransaction: false, + transactionalEntityManager: manager, + transaction: transactionData, + amount: transactionData.amount, + journals: [ + { + coa_id: coaBank.id, + debit: transactionData.amount, + }, + { + coa_id: coaInventory.id, + credit: transactionData.amount, + }, + ], + }); + }); + + return true; } - findAll() { - return `This action returns all transaction`; + async distributeFromAdmin( + distributeTransactionDto: DistributeTransactionDto, + currentUser: any, + ) { + // GET COA + const coaAR = await this.coaService.findByUser( + distributeTransactionDto.destination, + coaType.ACCOUNT_RECEIVABLE, + ); + const coaWallet = await this.coaService.findByUser( + distributeTransactionDto.destination, + coaType.WALLET, + ); + + //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 = distributeTransactionDto.amount; + transactionData.user = userData.id; + 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, + }, + ], + }); + }); + + return true; } - 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, + ); + + await this.connection.transaction(async (manager) => { + const transactionData = new Transactions(); + + transactionData.id = uuid.v4(); + transactionData.amount = distributeTransactionDto.amount; + transactionData.user = userData.id; + 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 PRODUCT + const product = await this.productService.findOne( + orderTransactionDto.productCode, + ); + + //GET USER + const userData = await this.userService.findByUsername( + currentUser.username, + ); + let supervisorData = []; + + if (userData.superior != null) { + supervisorData.push( + await this.userService.findByUsername(currentUser.username), + ); + + if (supervisorData[0].superior != null) { + supervisorData.push( + await this.userService.findByUsername(currentUser.username), + ); + + if (supervisorData[0].superior != null) { + supervisorData.push( + await this.userService.findByUsername(currentUser.username), + ); + } + } + } + + //GET COA + const coaAccount = await this.coaService.findByUser( + userData.id, + coaType.WALLET, + ); + + const coaInventory = await this.coaService.findByName( + `${coaType[coaType.INVENTORY]}-IRS`, + ); + + const coaCostOfSales = await this.coaService.findByName( + `${coaType[coaType.COST_OF_SALES]}-SYSTEM`, + ); + + const coaSales = await this.coaService.findByName( + `${coaType[coaType.SALES]}-SYSTEM`, + ); + + const coaExpense = await this.coaService.findByName( + `${coaType[coaType.EXPENSE]}-SYSTEM`, + ); + + supervisorData = supervisorData.map(async (it) => { + const coaAccount = await this.coaService.findByUser( + it.id, + coaType.WALLET, + ); + + return { + coa_id: coaAccount.id, + credit: 0, + }; + }); + + 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; + transactionData.user = userData.id; + 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: coaInventory.id, + credit: product.basePrice, + }, + { + coa_id: coaCostOfSales.id, + debit: product.basePrice, + }, + { + coa_id: coaAccount.id, + debit: product.price, + }, + { + coa_id: coaSales.id, + credit: product.price, + }, + { + coa_id: coaExpense.id, + credit: 0, + }, + ].concat(supervisorData), + }); + }); + } catch (e) { + throw e; + } + + return true; } - remove(id: number) { - return `This action removes a #${id} transaction`; + 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 = transaction; + + return this.transactionJournalRepository.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) => { + (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-user.dto.ts b/src/users/dto/create-user.dto.ts index 8b676ac..d9f00b8 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,9 +1,21 @@ -import { IsNotEmpty } from 'class-validator'; +import { IsNotEmpty, IsOptional, IsUUID, ValidateIf } from 'class-validator'; export class CreateUserDto { @IsNotEmpty() - firstName: string; + username: string; @IsNotEmpty() - lastName: string; + password: string; + + @IsUUID() + roleId: string; + + @IsNotEmpty() + superior: boolean; + + // @ValidateIf((o) => { + // return !!o.superior; + // }) + // @IsUUID() + // superior: string; } diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index 417e96f..5a121c6 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -1,45 +1,42 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - UpdateDateColumn, - DeleteDateColumn, - VersionColumn, - CreateDateColumn, -} from 'typeorm'; +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 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; - - @DeleteDateColumn({ - type: 'timestamp with time zone', - nullable: true, - }) - deletedAt: Date; - - @VersionColumn() - version: number; + @ManyToOne( + () => { + return Roles; + }, + (roles) => { + return roles.id; + }, + ) + roles: Roles; } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 7e45858..addb15e 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -8,10 +8,13 @@ import { Delete, ParseUUIDPipe, HttpStatus, + 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'; @Controller({ path: 'users', @@ -21,17 +24,18 @@ export class UsersController { constructor(private readonly usersService: UsersService) {} @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', }; } + @Public() @Get() - async findAll() { - const [data, count] = await this.usersService.findAll(); + async findAll(@Query('page') page: number) { + const [data, count] = await this.usersService.findAll(page); return { data, @@ -41,6 +45,34 @@ export class UsersController { }; } + @Get('find-by-supperior') + async findBySuperrior(@Request() req, @Query('page') page: number) { + const [data, count] = await this.usersService.findBySuperrior( + req.user.userId, + page, + ); + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + + @Get('find-by-roles/:id') + async findByRoles( + @Param('id', ParseUUIDPipe) id: string, + @Query('page') page: number, + ) { + const [data, count] = await this.usersService.findByRoles(id, page); + return { + data, + count, + statusCode: HttpStatus.OK, + message: 'success', + }; + } + @Get(':id') async findOne(@Param('id', ParseUUIDPipe) id: string) { return { diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 1c38291..c9ef16d 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,12 +1,15 @@ -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'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([User]), forwardRef(() => TransactionModule), ConfigurableModule], controllers: [UsersController], providers: [UsersService], + exports: [UsersService], }) export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 0ea0727..e7ff8c6 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,30 +1,178 @@ -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, 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'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: 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); + const salt = randomStringGenerator(); - return this.usersRepository.findOneOrFail(result.identifiers[0].id); + const userData = new User(); + + userData.id = uuid.v4(); + userData.username = createUserDto.username; + userData.password = await hashPassword(createUserDto.password, salt); + userData.salt = salt; + userData.superior = superior; + userData.roles = roles; + + await this.connection.transaction(async (manager) => { + const result = await manager.insert(User, userData); + + const dataCoaWallet = new InputCoaDto(); + + dataCoaWallet.user = userData; + dataCoaWallet.balanceType = balanceType.CREDIT; + dataCoaWallet.type = coaType.WALLET; + dataCoaWallet.coaEntityManager = manager; + + 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; + + 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(dataCoaAP); + await this.coaService.create(dataCoaAR); + } + + await this.coaService.create(dataCoaWallet); + }); + + return userData; } - findAll() { - return this.usersRepository.findAndCount(); + findAll(page: number) { + return this.usersRepository.findAndCount({ + skip: page * 10, + take: 10, + order: { + version: 'DESC', + }, + }); + } + + findByRoles(relationId: string, page: number) { + return this.usersRepository.findAndCount({ + skip: page * 10, + take: 10, + where: { + roles: relationId, + }, + order: { + updatedAt: 'DESC', + }, + }); + } + + findBySuperrior(superriorId: string, page: number) { + return this.usersRepository.findAndCount({ + skip: page * 10, + take: 10, + where: { + superior: superriorId, + }, + order: { + updatedAt: 'DESC', + }, + }); + } + + async findExist(id: string) { + try { + return await this.usersRepository.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; + } + } + } + + async findByUsername(username: string) { + try { + return await this.usersRepository.findOneOrFail({ + username: username, + }); + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new HttpException( + { + statusCode: HttpStatus.NOT_FOUND, + error: 'Data not found', + }, + HttpStatus.NOT_FOUND, + ); + } else { + throw e; + } + } } async findOne(id: string) { + const coa = await this.coaService.findByUser(id, coaType.WALLET); + try { - return await this.usersRepository.findOneOrFail(id); + const userData = await this.usersRepository.findOneOrFail({ + where: { + id: id, + }, + relations: ['roles', 'superior'], + }); + + return { + ...userData, + wallet: coa.amount, + }; } catch (e) { if (e instanceof EntityNotFoundError) { throw new HttpException( @@ -57,7 +205,7 @@ export class UsersService { } } - const result = await this.usersRepository.update(id, updateUserDto); + // const result = await this.usersRepository.update(id, updateUserDto); return this.usersRepository.findOneOrFail(id); } @@ -81,4 +229,13 @@ export class UsersService { await this.usersRepository.delete(id); } + + async findOneByUsername(username: string) { + return this.usersRepository.findOneOrFail({ + where: { + username, + }, + relations: ['roles'], + }); + } } diff --git a/yarn.lock b/yarn.lock index 80c017f..bd50c2d 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,6 +971,20 @@ 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" @@ -973,6 +1000,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" @@ -1475,7 +1535,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== @@ -1628,6 +1688,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" @@ -1996,6 +2061,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" @@ -2036,7 +2106,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 +2217,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" @@ -3742,6 +3819,39 @@ 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" @@ -3820,6 +3930,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 +3970,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" @@ -4006,6 +4151,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 +4416,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 +4484,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" @@ -4832,6 +5015,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"