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 { 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'; import { SupplierService } from '../users/supplier.service'; interface JournalEntry { coa_id: string; debit?: string; credit?: string; } @Injectable() export class TransactionService { constructor( @InjectRepository(Transactions) private transactionRepository: Repository, @InjectRepository(TransactionJournal) private transactionJournalRepository: Repository, @InjectRepository(COA) private coaRepository: Repository, private coaService: CoaService, private productService: ProductService, private userService: UsersService, private supplierService: SupplierService, private connection: Connection, ) {} async addPartnerSaldo(addSaldoSupplier: AddSaldoSupplier, currentUser: any) { const supplier = await this.supplierService.findByCode( addSaldoSupplier.supplier, ); // GET COA const coaBank = await this.coaService.findByName( `${coaType[coaType.BANK]}-SYSTEM`, ); const coaInventory = await this.coaService.findByName( `${coaType[coaType.INVENTORY]}-${supplier.code}`, ); const coaBudget = await this.coaService.findByName( `${coaType[coaType.BUDGET]}-${supplier.code}`, ); const coaContraBudget = await this.coaService.findByName( `${coaType[coaType.CONTRA_BUDGET]}-${supplier.code}`, ); //GET USER const userData = await this.userService.findByUsername( currentUser.username, ); await this.connection.transaction(async (manager) => { //INSERT TRANSACTION const transactionData = new Transactions(); transactionData.id = uuid.v4(); transactionData.amount = addSaldoSupplier.amount; transactionData.user = userData.id; transactionData.status = statusTransaction.SUCCESS; transactionData.type = typeTransaction.DEPOSIT_IRS; await manager.insert(Transactions, transactionData); await this.accountingTransaction({ createTransaction: false, transactionalEntityManager: manager, transaction: transactionData, amount: transactionData.amount, journals: [ { coa_id: coaBank.id, credit: transactionData.amount, }, { coa_id: coaInventory.id, debit: transactionData.amount, }, { coa_id: coaBudget.id, debit: transactionData.amount, }, { coa_id: coaContraBudget.id, credit: transactionData.amount, }, ], }); }); return true; } 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; } 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; } 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; } 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)); 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: () => { return `amount + ${diff.toString()}`; }, }) .where('id = :id', { id: coa.id }) .execute(); }), ); return transaction; } }