journaling

This commit is contained in:
Ilham Dwi Pratama S 2021-12-08 21:01:31 +07:00
parent 679bb758d8
commit d65af44a52
9 changed files with 220 additions and 9 deletions

31
package-lock.json generated
View File

@ -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",

View File

@ -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 {

View File

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

View File

@ -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[]
}

View File

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

View File

@ -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],

View File

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

View File

@ -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],

View File

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