diff --git a/package-lock.json b/package-lock.json index 6c5e34d..3947f09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1108,6 +1108,15 @@ "uuid": "8.3.2" } }, + "@nestjs/jwt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-8.0.0.tgz", + "integrity": "sha512-fz2LQgYY2zmuD8S+8UE215anwKyXlnB/1FwJQLVR47clNfMeFMK8WCxmn6xdPhF5JKuV1crO6FVabb1qWzDxqQ==", + "requires": { + "@types/jsonwebtoken": "8.5.4", + "jsonwebtoken": "8.5.1" + } + }, "@nestjs/mapped-types": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.0.1.tgz", @@ -1514,6 +1523,14 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/jsonwebtoken": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", + "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==", + "requires": { + "@types/node": "*" + } + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -1523,8 +1540,7 @@ "@types/node": { "version": "16.11.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.11.tgz", - "integrity": "sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==", - "dev": true + "integrity": "sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==" }, "@types/parse-json": { "version": "4.0.0", @@ -1541,6 +1557,17 @@ "@types/express": "*" } }, + "@types/passport-jwt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz", + "integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, "@types/passport-local": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz", diff --git a/src/helper/enum-list.ts b/src/helper/enum-list.ts index 68e79ca..aa0600c 100644 --- a/src/helper/enum-list.ts +++ b/src/helper/enum-list.ts @@ -17,6 +17,13 @@ export enum productType { export enum coaType { WALLET, INCOME, + INVENTORY, + COST_OF_SALES, + SALES, + BANK, + EXPENSE, + ACCOUNT_RECEIVABLE, + ACCOUNT_PAYABLE } export enum balanceType { diff --git a/src/transaction/coa.service.ts b/src/transaction/coa.service.ts index a5ce977..679ae12 100644 --- a/src/transaction/coa.service.ts +++ b/src/transaction/coa.service.ts @@ -1,15 +1,34 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +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 { 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 = await this.userService.findExist(inputCoaDto.userId) + + const result = await this.coaRepository.insert({ + user:user.id, + name: inputCoaDto.balanceType + '-' + user.username, + balanceType:inputCoaDto.balanceType, + type:inputCoaDto.type + }); + + return this.coaRepository.findOneOrFail( + result.identifiers[0].id, + ); + } + async findByUser(id: string, typeOfCoa: coaType) { try { return await this.coaRepository.findOneOrFail({ user: id, type: typeOfCoa }); diff --git a/src/transaction/dto/create-journal.dto.ts b/src/transaction/dto/create-journal.dto.ts new file mode 100644 index 0000000..fd218c2 --- /dev/null +++ b/src/transaction/dto/create-journal.dto.ts @@ -0,0 +1,35 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { balanceType, coaType, statusTransaction, typeTransaction } from 'src/helper/enum-list'; +import { EntityManager } from 'typeorm'; + +interface JournalEntry { + coa_id: string; + debit?: number; + credit?: number; +} + +export class CreateJournalDto { + @IsNotEmpty() + transactionalEntityManager: EntityManager; + + @IsNotEmpty() + createTransaction?: boolean; + + @IsNotEmpty() + userId?: string; + + @IsNotEmpty() + transactionId?: string; + + @IsNotEmpty() + type?: typeTransaction; + + @IsNotEmpty() + amount?: number; + + @IsNotEmpty() + transactionStatus?: statusTransaction; + + @IsNotEmpty() + journals: JournalEntry[] +} diff --git a/src/transaction/dto/input-coa.dto.ts b/src/transaction/dto/input-coa.dto.ts new file mode 100644 index 0000000..40b70ae --- /dev/null +++ b/src/transaction/dto/input-coa.dto.ts @@ -0,0 +1,13 @@ +import { IsNotEmpty, IsUUID } from 'class-validator'; +import { balanceType, coaType } from 'src/helper/enum-list'; + +export class InputCoaDto { + @IsUUID() + userId: string; + + @IsNotEmpty() + type: coaType; + + @IsNotEmpty() + balanceType: balanceType; +} diff --git a/src/transaction/transaction.module.ts b/src/transaction/transaction.module.ts index 97c15fd..d9cef39 100644 --- a/src/transaction/transaction.module.ts +++ b/src/transaction/transaction.module.ts @@ -1,4 +1,4 @@ -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'; @@ -10,6 +10,7 @@ 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: [ @@ -21,6 +22,7 @@ import { ProductModule } from '../product/product.module'; Transactions, ]), ProductModule, + forwardRef(() => UsersModule), ], controllers: [TransactionController, PpobCallbackController], providers: [TransactionService, CoaService], diff --git a/src/transaction/transaction.service.ts b/src/transaction/transaction.service.ts index a99e55a..77f6ab5 100644 --- a/src/transaction/transaction.service.ts +++ b/src/transaction/transaction.service.ts @@ -3,11 +3,14 @@ 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, Repository } from 'typeorm'; +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, @@ -16,6 +19,14 @@ import { } from '../helper/enum-list'; import { ProductService } from '../product/product.service'; import * as irsService from '../helper/irs-service'; +import { CreateJournalDto } from './dto/create-journal.dto'; + +interface JournalEntry { + coa_id: string; + debit?: string; + credit?: string; +} + @Injectable() export class TransactionService { @@ -145,4 +156,80 @@ export class TransactionService { return true; } + + async accountingTransaction(createJournalDto: CreateJournalDto ) { + + createJournalDto.transactionId = createJournalDto.transactionId ?? uuid.v4(); + + let creditSum = createJournalDto.journals.map(it => it.credit).filter(it => it).reduce((a, b) => a.plus(b), new Decimal(0)); + let debitSum = createJournalDto.journals.map(it => it.debit).filter(it => it).reduce((a, b) => a.plus(b), new Decimal(0)); + let coaIds = uniq(createJournalDto.journals.map(it => it.coa_id)); + + if (!creditSum.equals(debitSum)) { + throw new Error(`credit and debit doesn't match`); + } + + const coas = await this.coaRepository.findByIds(coaIds); + + let transaction: Transactions; + + if(createJournalDto.createTransaction) { + transaction = new Transactions(); + transaction.id = createJournalDto.transactionId; + transaction.type = createJournalDto.type; + transaction.amount = createJournalDto.amount; + transaction.user = createJournalDto.userId; + transaction.status = createJournalDto.transactionStatus; + + await this.transactionRepository.save(transaction); + } else { + transaction = await this.transactionRepository.findOneOrFail(createJournalDto.transactionId); + } + + await Promise.all(createJournalDto.journals.map(journal => { + const coa = coas.find(it => 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 => { + let journalPerCoa = createJournalDto.journals.filter(journal => journal.coa_id == coaId); + + let creditSum = journalPerCoa.map(it => it.credit).filter(it => it).reduce((a, b) => a.plus(b), new Decimal(0)); + let debitSum = journalPerCoa.map(it => it.debit).filter(it => it).reduce((a, b) => a.plus(b), new Decimal(0)); + + let coa = coas.find(it => 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: () => "amount + " + diff.toString() + }) + .where("id = :id", { id: coa.id }) + .execute(); + })); + + return transaction; + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index fd4e301..c9ef16d 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,4 +1,4 @@ -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'; @@ -7,7 +7,7 @@ import { TransactionModule } from 'src/transaction/transaction.module'; import { ConfigurableModule } from 'src/configurable/configurable.module'; @Module({ - imports: [TypeOrmModule.forFeature([User]), TransactionModule, ConfigurableModule], + imports: [TypeOrmModule.forFeature([User]), forwardRef(() => TransactionModule), ConfigurableModule], controllers: [UsersController], providers: [UsersService], exports: [UsersService], diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 4e8ae5e..c8a33da 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,4 +1,4 @@ -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'; @@ -7,14 +7,16 @@ 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 { coaType } from 'src/helper/enum-list'; +import { balanceType, coaType } from 'src/helper/enum-list'; import { RoleService } from 'src/configurable/roles.service'; +import { InputCoaDto } from 'src/transaction/dto/input-coa.dto'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, + @Inject(forwardRef(() => CoaService)) private coaService: CoaService, private roleService: RoleService ) {} @@ -32,6 +34,25 @@ export class UsersService { roles:roles }); + let dataCoaWallet = new InputCoaDto(); + dataCoaWallet.userId = result.identifiers[0].id; + dataCoaWallet.balanceType = balanceType.CREDIT; + dataCoaWallet.type = coaType.WALLET; + + let dataCoaAR = new InputCoaDto(); + dataCoaAR.userId = result.identifiers[0].id; + dataCoaAR.balanceType = balanceType.CREDIT; + dataCoaAR.type = coaType.ACCOUNT_RECEIVABLE; + + let dataCoaPayable = new InputCoaDto(); + dataCoaPayable.userId = result.identifiers[0].id; + dataCoaPayable.balanceType = balanceType.CREDIT; + dataCoaPayable.type = coaType.ACCOUNT_PAYABLE; + + await this.coaService.create(dataCoaWallet); + await this.coaService.create(dataCoaAR); + await this.coaService.create(dataCoaPayable); + return this.usersRepository.findOneOrFail(result.identifiers[0].id); }