feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

View File

@@ -0,0 +1,47 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import { IBankAccountsFilter } from './types/BankingTransactions.types';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
export class BankingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get('')
async getBankAccounts(@Query() filterDTO: IBankAccountsFilter) {
return this.bankingTransactionsApplication.getBankAccounts(filterDTO);
}
@Post()
async createTransaction(@Body() transactionDTO: CreateBankTransactionDto) {
return this.bankingTransactionsApplication.createTransaction(
transactionDTO,
);
}
@Delete(':id')
async deleteTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.deleteTransaction(
Number(transactionId),
);
}
@Get(':id')
async getTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.getTransaction(
Number(transactionId),
);
}
}

View File

@@ -0,0 +1,63 @@
import { Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { UncategorizedBankTransaction } from './models/UncategorizedBankTransaction';
import { BankTransactionLine } from './models/BankTransactionLine';
import { BankTransaction } from './models/BankTransaction';
import { BankTransactionAutoIncrement } from './commands/BankTransactionAutoIncrement.service';
import { BankingTransactionGLEntriesSubscriber } from './subscribers/CashflowTransactionSubscriber';
import { DecrementUncategorizedTransactionOnCategorizeSubscriber } from './subscribers/DecrementUncategorizedTransactionOnCategorize';
import { DeleteCashflowTransactionOnUncategorizeSubscriber } from './subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDeleteSubscriber } from './subscribers/PreventDeleteTransactionsOnDelete';
import { ValidateDeleteBankAccountTransactions } from './commands/ValidateDeleteBankAccountTransactions.service';
import { BankTransactionGLEntriesService } from './commands/BankTransactionGLEntries';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { DeleteCashflowTransaction } from './commands/DeleteCashflowTransaction.service';
import { CreateBankTransactionService } from './commands/CreateBankTransaction.service';
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
import { CommandBankTransactionValidator } from './commands/CommandCasflowValidator.service';
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
import { BranchesModule } from '../Branches/Branches.module';
import { RemovePendingUncategorizedTransaction } from './commands/RemovePendingUncategorizedTransaction.service';
import { BankingTransactionsController } from './BankingTransactions.controller';
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BankAccount } from './models/BankAccount';
import { LedgerModule } from '../Ledger/Ledger.module';
const models = [
RegisterTenancyModel(UncategorizedBankTransaction),
RegisterTenancyModel(BankTransaction),
RegisterTenancyModel(BankTransactionLine),
RegisterTenancyModel(BankAccount),
];
@Module({
imports: [
AutoIncrementOrdersModule,
LedgerModule,
BranchesModule,
DynamicListModule,
...models,
],
controllers: [BankingTransactionsController],
providers: [
BankTransactionAutoIncrement,
BankTransactionGLEntriesService,
ValidateDeleteBankAccountTransactions,
BankingTransactionGLEntriesSubscriber,
DecrementUncategorizedTransactionOnCategorizeSubscriber,
DeleteCashflowTransactionOnUncategorizeSubscriber,
PreventDeleteTransactionOnDeleteSubscriber,
BankingTransactionsApplication,
DeleteCashflowTransaction,
CreateBankTransactionService,
GetBankTransactionService,
GetBankAccountsService,
CommandBankTransactionValidator,
BranchTransactionDTOTransformer,
RemovePendingUncategorizedTransaction,
],
exports: [...models, RemovePendingUncategorizedTransaction],
})
export class BankingTransactionsModule {}

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { DeleteCashflowTransaction } from './commands/DeleteCashflowTransaction.service';
import { CreateBankTransactionService } from './commands/CreateBankTransaction.service';
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
import {
IBankAccountsFilter,
} from './types/BankingTransactions.types';
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
@Injectable()
export class BankingTransactionsApplication {
constructor(
private readonly createTransactionService: CreateBankTransactionService,
private readonly deleteTransactionService: DeleteCashflowTransaction,
private readonly getCashflowTransactionService: GetBankTransactionService,
private readonly getBankAccountsService: GetBankAccountsService,
) {}
/**
* Creates a new cashflow transaction.
* @param {ICashflowNewCommandDTO} transactionDTO
* @returns
*/
public createTransaction(transactionDTO: CreateBankTransactionDto) {
return this.createTransactionService.newCashflowTransaction(transactionDTO);
}
/**
* Deletes the given cashflow transaction.
* @param {number} cashflowTransactionId - Cashflow transaction id.
* @returns {Promise<{ oldCashflowTransaction: ICashflowTransaction }>}
*/
public deleteTransaction(cashflowTransactionId: number) {
return this.deleteTransactionService.deleteCashflowTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves specific cashflow transaction.
* @param {number} cashflowTransactionId
* @returns
*/
public getTransaction(cashflowTransactionId: number) {
return this.getCashflowTransactionService.getBankTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves the cashflow accounts.
* @param {IBankAccountsFilter} filterDTO
*/
public getBankAccounts(filterDTO: IBankAccountsFilter) {
return this.getBankAccountsService.getBankAccounts(filterDTO);
}
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '../../AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class BankTransactionAutoIncrement {
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
) {}
/**
* Retrieve the next unique invoice number.
* @return {string}
*/
public getNextTransactionNumber = (): Promise<string> => {
return this.autoIncrementOrdersService.getNextTransactionNumber('cashflow');
};
/**
* Increment the invoice next number.
*/
public incrementNextTransactionNumber = () => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'cashflow',
);
};
}

View File

@@ -0,0 +1,102 @@
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { BankTransaction } from '../models/BankTransaction';
import { transformCashflowTransactionType } from '../utils';
import { Ledger } from '@/modules/Ledger/Ledger';
export class BankTransactionGL {
private bankTransactionModel: BankTransaction;
/**
* @param {BankTransaction} bankTransactionModel - The bank transaction model.
*/
constructor(bankTransactionModel: BankTransaction) {
this.bankTransactionModel = bankTransactionModel;
}
/**
* Retrieves the common entry of cashflow transaction.
* @returns {Partial<ILedgerEntry>}
*/
private get commonEntry() {
const { entries, ...transaction } = this.bankTransactionModel;
return {
date: this.bankTransactionModel.date,
currencyCode: this.bankTransactionModel.currencyCode,
exchangeRate: this.bankTransactionModel.exchangeRate,
transactionType: 'CashflowTransaction',
transactionId: this.bankTransactionModel.id,
transactionNumber: this.bankTransactionModel.transactionNumber,
transactionSubType: transformCashflowTransactionType(
this.bankTransactionModel.transactionType,
),
referenceNumber: this.bankTransactionModel.referenceNo,
note: this.bankTransactionModel.description,
branchId: this.bankTransactionModel.branchId,
userId: this.bankTransactionModel.userId,
};
}
/**
* Retrieves the cashflow debit GL entry.
* @returns {ILedgerEntry}
*/
private get cashflowDebitGLEntry(): ILedgerEntry {
const commonEntry = this.commonEntry;
return {
...commonEntry,
accountId: this.bankTransactionModel.cashflowAccountId,
credit: this.bankTransactionModel.isCashCredit
? this.bankTransactionModel.localAmount
: 0,
debit: this.bankTransactionModel.isCashDebit
? this.bankTransactionModel.localAmount
: 0,
accountNormal: this.bankTransactionModel?.cashflowAccount?.accountNormal,
index: 1,
};
}
/**
* Retrieves the cashflow credit GL entry.
* @returns {ILedgerEntry}
*/
private get cashflowCreditGLEntry(): ILedgerEntry {
return {
...this.commonEntry,
credit: this.bankTransactionModel.isCashDebit
? this.bankTransactionModel.localAmount
: 0,
debit: this.bankTransactionModel.isCashCredit
? this.bankTransactionModel.localAmount
: 0,
accountId: this.bankTransactionModel.creditAccountId,
accountNormal: this.bankTransactionModel.creditAccount.accountNormal,
index: 2,
};
}
/**
* Retrieves the cashflow transaction GL entry.
* @returns {ILedgerEntry[]}
*/
private getJournalEntries(): ILedgerEntry[] {
const debitEntry = this.cashflowDebitGLEntry;
const creditEntry = this.cashflowCreditGLEntry;
return [debitEntry, creditEntry];
}
/**
* Retrieves the cashflow GL ledger.
* @returns {Ledger}
*/
public getCashflowLedger() {
const entries = this.getJournalEntries();
return new Ledger(entries);
}
}

View File

@@ -0,0 +1,54 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { BankTransaction } from '../models/BankTransaction';
import { BankTransactionGL } from './BankTransactionGL';
@Injectable()
export class BankTransactionGLEntriesService {
constructor(
private readonly ledgerStorage: LedgerStorageService,
@Inject(BankTransaction.name)
private readonly bankTransactionModel: typeof BankTransaction,
) {}
/**
* Write the journal entries of the given cashflow transaction.
* @param {number} tenantId
* @param {ICashflowTransaction} cashflowTransaction
* @return {Promise<void>}
*/
public writeJournalEntries = async (
cashflowTransactionId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Retrieves the cashflow transactions with associated entries.
const transaction = await this.bankTransactionModel
.query(trx)
.findById(cashflowTransactionId)
.withGraphFetched('cashflowAccount')
.withGraphFetched('creditAccount');
// Retrieves the cashflow transaction ledger.
const ledger = new BankTransactionGL(transaction).getCashflowLedger();
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Delete the journal entries.
* @param {number} cashflowTransactionId - Cashflow transaction id.
* @return {Promise<void>}
*/
public revertJournalEntries = async (
cashflowTransactionId: number,
trx?: Knex.Transaction,
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
cashflowTransactionId,
'CashflowTransaction',
trx,
);
};
}

View File

@@ -0,0 +1,109 @@
import { includes, camelCase, upperFirst, sumBy } from 'lodash';
import { getCashflowTransactionType } from '../utils';
import {
CASHFLOW_DIRECTION,
CASHFLOW_TRANSACTION_TYPE,
ERRORS,
} from '../constants';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { BankTransaction } from '../models/BankTransaction';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
@Injectable()
export class CommandBankTransactionValidator {
/**
* Validates the lines accounts type should be cash or bank account.
* @param {Account} accounts -
*/
public validateCreditAccountWithCashflowType = (
creditAccount: Account,
cashflowTransactionType: CASHFLOW_TRANSACTION_TYPE
): void => {
const transactionTypeMeta = getCashflowTransactionType(
cashflowTransactionType
);
const noneCashflowAccount = !includes(
transactionTypeMeta.creditType,
creditAccount.accountType
);
if (noneCashflowAccount) {
throw new ServiceError(ERRORS.CREDIT_ACCOUNTS_HAS_INVALID_TYPE);
}
};
/**
* Validates the cashflow transaction type.
* @param {string} transactionType
* @returns {string}
*/
public validateCashflowTransactionType = (transactionType: string) => {
const transformedType = upperFirst(
camelCase(transactionType)
) as CASHFLOW_TRANSACTION_TYPE;
// Retrieve the given transaction type meta.
const transactionTypeMeta = getCashflowTransactionType(transformedType);
// Throw service error in case not the found the given transaction type.
if (!transactionTypeMeta) {
throw new ServiceError(ERRORS.CASHFLOW_TRANSACTION_TYPE_INVALID);
}
return transformedType;
};
/**
* Validate the given transaction should be categorized.
* @param {CashflowTransaction} cashflowTransaction
*/
public validateTransactionShouldCategorized(
cashflowTransaction: BankTransaction
) {
if (!cashflowTransaction.uncategorize) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
}
}
/**
* Validate the given transcation shouldn't be categorized.
* @param {CashflowTransaction} cashflowTransaction
*/
public validateTransactionsShouldNotCategorized(
cashflowTransactions: Array<UncategorizedBankTransaction>
) {
const categorized = cashflowTransactions.filter((t) => t.categorized);
if (categorized?.length > 0) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED, '', {
ids: categorized.map((t) => t.id),
});
}
}
/**
* Validate the uncategorize transaction type.
* @param {uncategorizeTransaction}
* @param {string} transactionType
* @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
*/
public validateUncategorizeTransactionType(
uncategorizeTransactions: Array<UncategorizedBankTransaction>,
transactionType: string
) {
const amount = sumBy(uncategorizeTransactions, 'amount');
const isDepositTransaction = amount > 0;
const isWithdrawalTransaction = amount <= 0;
const type = getCashflowTransactionType(
transactionType as CASHFLOW_TRANSACTION_TYPE
);
if (
(type.direction === CASHFLOW_DIRECTION.IN && isDepositTransaction) ||
(type.direction === CASHFLOW_DIRECTION.OUT && isWithdrawalTransaction)
) {
return;
}
throw new ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID);
}
}

View File

@@ -0,0 +1,171 @@
import { Inject, Injectable } from '@nestjs/common';
import { pick } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import * as composeAsync from 'async/compose';
import { CASHFLOW_TRANSACTION_TYPE } from '../constants';
import { transformCashflowTransactionType } from '../utils';
import { CommandBankTransactionValidator } from './CommandCasflowValidator.service';
import { BankTransactionAutoIncrement } from './BankTransactionAutoIncrement.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { events } from '@/common/events/events';
import { Account } from '@/modules/Accounts/models/Account.model';
import { BankTransaction } from '../models/BankTransaction';
import {
ICashflowNewCommandDTO,
ICommandCashflowCreatedPayload,
ICommandCashflowCreatingPayload,
} from '../types/BankingTransactions.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
@Injectable()
export class CreateBankTransactionService {
constructor(
private validator: CommandBankTransactionValidator,
private uow: UnitOfWork,
private eventPublisher: EventEmitter2,
private autoIncrement: BankTransactionAutoIncrement,
private branchDTOTransform: BranchTransactionDTOTransformer,
@Inject(BankTransaction.name)
private bankTransactionModel: TenantModelProxy<typeof BankTransaction>,
@Inject(Account.name)
private accountModel: TenantModelProxy<typeof Account>,
) {}
/**
* Authorize the cashflow creating transaction.
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO
*/
public authorize = async (
newCashflowTransactionDTO: ICashflowNewCommandDTO,
creditAccount: Account,
) => {
const transactionType = transformCashflowTransactionType(
newCashflowTransactionDTO.transactionType,
);
// Validates the cashflow transaction type.
this.validator.validateCashflowTransactionType(transactionType);
// Retrieve accounts of the cashflow lines object.
this.validator.validateCreditAccountWithCashflowType(
creditAccount,
transactionType as CASHFLOW_TRANSACTION_TYPE,
);
};
/**
* Transformes owner contribution DTO to cashflow transaction.
* @param {ICashflowNewCommandDTO} newCashflowTransactionDTO - New transaction DTO.
* @returns {ICashflowTransactionInput} - Cashflow transaction object.
*/
private transformCashflowTransactionDTO = async (
newCashflowTransactionDTO: CreateBankTransactionDto,
cashflowAccount: Account,
userId: number,
): Promise<BankTransaction> => {
const amount = newCashflowTransactionDTO.amount;
const fromDTO = pick(newCashflowTransactionDTO, [
'date',
'referenceNo',
'description',
'transactionType',
'exchangeRate',
'cashflowAccountId',
'creditAccountId',
'branchId',
'plaidTransactionId',
'uncategorizedTransactionId',
]);
// Retreive the next invoice number.
const autoNextNumber = await this.autoIncrement.getNextTransactionNumber();
// Retrieve the transaction number.
const transactionNumber =
newCashflowTransactionDTO.transactionNumber || autoNextNumber;
const initialDTO = {
amount,
...fromDTO,
transactionNumber,
currencyCode: cashflowAccount.currencyCode,
exchangeRate: fromDTO?.exchangeRate || 1,
transactionType: transformCashflowTransactionType(
fromDTO.transactionType,
),
userId,
...(newCashflowTransactionDTO.publish
? {
publishedAt: new Date(),
}
: {}),
};
return composeAsync(this.branchDTOTransform.transformDTO<BankTransaction>)(
initialDTO,
) as BankTransaction;
};
/**
* Owner contribution money in.
* @param {ICashflowOwnerContributionDTO} ownerContributionDTO
* @param {number} userId - User id.
* @returns {Promise<ICashflowTransaction>}
*/
public newCashflowTransaction = async (
newTransactionDTO: ICashflowNewCommandDTO,
userId?: number,
): Promise<BankTransaction> => {
// Retrieves the cashflow account or throw not found error.
const cashflowAccount = await this.accountModel()
.query()
.findById(newTransactionDTO.cashflowAccountId)
.throwIfNotFound();
// Retrieves the credit account or throw not found error.
const creditAccount = await this.accountModel()
.query()
.findById(newTransactionDTO.creditAccountId)
.throwIfNotFound();
// Authorize before creating cashflow transaction.
await this.authorize(newTransactionDTO, creditAccount);
// Transformes owner contribution DTO to cashflow transaction.
const cashflowTransactionObj = await this.transformCashflowTransactionDTO(
newTransactionDTO,
cashflowAccount,
userId,
);
// Creates a new cashflow transaction under UOW envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onCashflowTransactionCreate` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCreating,
{
trx,
newTransactionDTO,
} as ICommandCashflowCreatingPayload,
);
// Inserts cashflow owner contribution transaction.
const cashflowTransaction = await this.bankTransactionModel()
.query(trx)
.upsertGraph(cashflowTransactionObj);
// Triggers `onCashflowTransactionCreated` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCreated,
{
newTransactionDTO,
cashflowTransaction,
trx,
} as ICommandCashflowCreatedPayload,
);
return cashflowTransaction;
});
};
}

View File

@@ -0,0 +1,87 @@
import { Knex } from 'knex';
import { ERRORS } from '../constants';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { BankTransaction } from '../models/BankTransaction';
import { BankTransactionLine } from '../models/BankTransactionLine';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ServiceError } from '@/modules/Items/ServiceError';
import { events } from '@/common/events/events';
import {
ICommandCashflowDeletedPayload,
ICommandCashflowDeletingPayload,
} from '../types/BankingTransactions.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteCashflowTransaction {
constructor(
private readonly uow: UnitOfWork,
private readonly eventEmitter: EventEmitter2,
@Inject(BankTransaction.name)
private readonly bankTransaction: TenantModelProxy<typeof BankTransaction>,
@Inject(BankTransactionLine.name)
private readonly bankTransactionLine: TenantModelProxy<
typeof BankTransactionLine
>,
) {}
/**
* Deletes the cashflow transaction with associated journal entries.
* @param {number} tenantId -
* @param {number} userId - User id.
*/
public deleteCashflowTransaction = async (
cashflowTransactionId: number,
trx?: Knex.Transaction,
): Promise<BankTransaction> => {
// Retrieve the cashflow transaction.
const oldCashflowTransaction = await this.bankTransaction()
.query()
.findById(cashflowTransactionId);
// Throw not found error if the given transaction id not found.
this.throwErrorIfTransactionNotFound(oldCashflowTransaction);
// Starting database transaction.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onCashflowTransactionDelete` event.
await this.eventEmitter.emitAsync(events.cashflow.onTransactionDeleting, {
trx,
oldCashflowTransaction,
} as ICommandCashflowDeletingPayload);
// Delete cashflow transaction associated lines first.
await this.bankTransactionLine()
.query(trx)
.where('cashflow_transaction_id', cashflowTransactionId)
.delete();
// Delete cashflow transaction.
await this.bankTransaction()
.query(trx)
.findById(cashflowTransactionId)
.delete();
// Triggers `onCashflowTransactionDeleted` event.
await this.eventEmitter.emitAsync(events.cashflow.onTransactionDeleted, {
trx,
cashflowTransactionId,
oldCashflowTransaction,
} as ICommandCashflowDeletedPayload);
return oldCashflowTransaction;
}, trx);
};
/**
* Throw not found error if the given transaction id not found.
* @param transaction
*/
private throwErrorIfTransactionNotFound(transaction) {
if (!transaction) {
throw new ServiceError(ERRORS.CASHFLOW_TRANSACTION_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,70 @@
import { Knex } from 'knex';
import { ERRORS } from '../constants';
import {
IPendingTransactionRemovedEventPayload,
IPendingTransactionRemovingEventPayload,
} from '../types/BankingTransactions.types';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { ServiceError } from '@/modules/Items/ServiceError';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class RemovePendingUncategorizedTransaction {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransaction: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* REmoves the pending uncategorized transaction.
* @param {number} uncategorizedTransactionId -
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public async removePendingTransaction(
uncategorizedTransactionId: number,
trx?: Knex.Transaction,
): Promise<void> {
const pendingTransaction = await this.uncategorizedBankTransaction()
.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
if (!pendingTransaction.isPending) {
throw new ServiceError(ERRORS.TRANSACTION_NOT_PENDING);
}
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoving,
{
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovingEventPayload,
);
// Removes the pending uncategorized transaction.
await this.uncategorizedBankTransaction()
.query(trx)
.findById(uncategorizedTransactionId)
.delete();
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoved,
{
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovedEventPayload,
);
});
}
}

View File

@@ -0,0 +1,30 @@
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../constants';
import { ServiceError } from '../../Items/ServiceError';
import { BankTransactionLine } from '../models/BankTransactionLine';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class ValidateDeleteBankAccountTransactions {
constructor(
@Inject(BankTransactionLine.name)
private readonly bankTransactionLineModel: TenantModelProxy<
typeof BankTransactionLine
>,
) {}
/**
* Validate the account has no associated cashflow transactions.
* @param {number} accountId
*/
public validateAccountHasNoCashflowEntries = async (accountId: number) => {
const associatedLines = await this.bankTransactionLineModel()
.query()
.where('creditAccountId', accountId)
.orWhere('cashflowAccountId', accountId);
if (associatedLines.length > 0) {
throw new ServiceError(ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS);
}
};
}

View File

@@ -0,0 +1,149 @@
import { ACCOUNT_TYPE } from "@/constants/accounts";
export const ERRORS = {
CASHFLOW_TRANSACTION_TYPE_INVALID: 'CASHFLOW_TRANSACTION_TYPE_INVALID',
CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE: 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE',
CASHFLOW_TRANSACTION_NOT_FOUND: 'CASHFLOW_TRANSACTION_NOT_FOUND',
CASHFLOW_ACCOUNTS_IDS_NOT_FOUND: 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND',
CREDIT_ACCOUNTS_IDS_NOT_FOUND: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND',
CREDIT_ACCOUNTS_HAS_INVALID_TYPE: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE',
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED',
UNCATEGORIZED_TRANSACTION_TYPE_INVALID:
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION:
'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED',
TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
};
export enum CASHFLOW_DIRECTION {
IN = 'In',
OUT = 'Out',
}
export enum CASHFLOW_TRANSACTION_TYPE {
ONWERS_DRAWING = 'OwnerDrawing',
OWNER_CONTRIBUTION = 'OwnerContribution',
OTHER_INCOME = 'OtherIncome',
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
TRANSFER_TO_ACCOUNT = 'TransferToAccount',
OTHER_EXPENSE = 'OtherExpense',
}
export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
type: 'OwnerDrawing',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EQUITY],
},
[`${CASHFLOW_TRANSACTION_TYPE.OWNER_CONTRIBUTION}`]: {
type: 'OwnerContribution',
direction: CASHFLOW_DIRECTION.IN,
creditType: [ACCOUNT_TYPE.EQUITY],
},
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_INCOME}`]: {
type: 'OtherIncome',
direction: CASHFLOW_DIRECTION.IN,
creditType: [ACCOUNT_TYPE.INCOME, ACCOUNT_TYPE.OTHER_INCOME],
},
[`${CASHFLOW_TRANSACTION_TYPE.TRANSFER_FROM_ACCOUNT}`]: {
type: 'TransferFromAccount',
direction: CASHFLOW_DIRECTION.IN,
creditType: [
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CREDIT_CARD,
],
},
[`${CASHFLOW_TRANSACTION_TYPE.TRANSFER_TO_ACCOUNT}`]: {
type: 'TransferToAccount',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CREDIT_CARD,
],
},
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: {
type: 'OtherExpense',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [
ACCOUNT_TYPE.EXPENSE,
ACCOUNT_TYPE.OTHER_EXPENSE,
ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
],
},
};
export interface ICashflowTransactionTypeMeta {
type: string;
direction: CASHFLOW_DIRECTION;
creditType: string[];
}
export const BankTransactionsSampleData = [
{
Amount: '6,410.19',
Date: '2024-03-26',
Payee: 'MacGyver and Sons',
'Reference No.': 'REF-1',
Description: 'Commodi quo labore.',
},
{
Amount: '8,914.17',
Date: '2024-01-05',
Payee: 'Eichmann - Bergnaum',
'Reference No.': 'REF-1',
Description: 'Quia enim et.',
},
{
Amount: '6,200.88',
Date: '2024-02-17',
Payee: 'Luettgen, Mraz and Legros',
'Reference No.': 'REF-1',
Description: 'Occaecati consequuntur cum impedit illo.',
},
];
export const CashflowTransactionTypes = {
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
};
export const TransactionTypes = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment received',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
LandedCost: 'transaction_type.landed_cost',
CashflowTransaction: CashflowTransactionTypes,
};

View File

@@ -0,0 +1,58 @@
import {
IsBoolean,
IsDate,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export class CreateBankTransactionDto {
@IsDate()
date: Date;
@IsString()
transactionNumber: string;
@IsString()
referenceNo: string;
@IsString()
transactionType: string;
@IsString()
description: string;
@IsNumber()
amount: number;
@IsNumber()
exchangeRate: number;
@IsString()
currencyCode: string;
@IsNumber()
creditAccountId: number;
@IsNumber()
cashflowAccountId: number;
@IsBoolean()
publish: boolean;
@IsOptional()
@IsNumber()
branchId?: number;
@IsOptional()
@IsString()
plaidTransactionId?: string;
@IsOptional()
@IsString()
plaidAccountId?: string;
@IsOptional()
@IsNumber()
uncategorizedTransactionId?: number;
}

View File

@@ -0,0 +1,139 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import { castArray } from 'lodash';
import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class BankAccount extends TenantBaseModel {
public name!: string;
public slug!: string;
public code!: string;
public index!: number;
public accountType!: string;
public predefined!: boolean;
public currencyCode!: string;
public active!: boolean;
public bankBalance!: number;
public lastFeedsUpdatedAt!: string | null;
public amount!: number;
public plaidItemId!: number;
public plaidItem!: PlaidItem;
/**
* Table name.
*/
static get tableName() {
return 'accounts';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['accountTypeLabel'];
}
/**
* Retrieve account type label.
*/
get accountTypeLabel(): string {
return AccountTypesUtils.getType(this.accountType, 'label');
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
return {
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
const types = castArray(accountType);
return types.indexOf(this.accountType) !== -1;
}
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
}
// /**
// * Model settings.
// */
// static get meta() {
// return CashflowAccountSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
}

View File

@@ -0,0 +1,239 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
import {
getCashflowAccountTransactionsTypes,
getCashflowTransactionType,
} from '../utils';
import { CASHFLOW_DIRECTION, CASHFLOW_TRANSACTION_TYPE } from '../constants';
import { BankTransactionLine } from './BankTransactionLine';
import { Account } from '@/modules/Accounts/models/Account.model';
export class BankTransaction extends BaseModel {
transactionType: string;
amount: number;
exchangeRate: number;
uncategorize: boolean;
uncategorizedTransaction!: boolean;
currencyCode: string;
date: Date;
transactionNumber: string;
referenceNo: string;
description: string;
cashflowAccountId: number;
creditAccountId: number;
categorizeRefType: string;
categorizeRefId: number;
uncategorized: boolean;
branchId: number;
userId: number;
publishedAt: Date;
entries: BankTransactionLine[];
cashflowAccount: Account;
creditAccount: Account;
uncategorizedTransactionId: number;
/**
* Table name.
* @returns {string}
*/
static get tableName() {
return 'cashflow_transactions';
}
/**
* Timestamps columns.
* @returns {Array<string>}
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
* @returns {Array<string>}
*/
static get virtualAttributes() {
return [
'localAmount',
'transactionTypeFormatted',
'isPublished',
'typeMeta',
'isCashCredit',
'isCashDebit',
];
}
/**
* Retrieves the local amount of cashflow transaction.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Detarmines whether the cashflow transaction is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
/**
* Transaction type formatted.
* @returns {string}
*/
// get transactionTypeFormatted() {
// return getCashflowTransactionFormattedType(this.transactionType);
// }
get typeMeta() {
return getCashflowTransactionType(
this.transactionType as CASHFLOW_TRANSACTION_TYPE,
);
}
/**
* Detarmines whether the cashflow transaction cash credit type.
* @returns {boolean}
*/
get isCashCredit() {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.OUT;
}
/**
* Detarmines whether the cashflow transaction cash debit type.
* @returns {boolean}
*/
get isCashDebit() {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN;
}
/**
* Detarmines whether the transaction imported from uncategorized transaction.
* @returns {boolean}
*/
get isCategroizedTranasction() {
return !!this.uncategorizedTransaction;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filter the published transactions.
*/
published(query) {
query.whereNot('published_at', null);
},
/**
* Filter the not categorized transactions.
*/
notCategorized(query) {
query.whereNull('cashflowTransactions.uncategorizedTransactionId');
},
/**
* Filter the categorized transactions.
*/
categorized(query) {
query.whereNotNull('cashflowTransactions.uncategorizedTransactionId');
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { BankTransactionLine } = require('./BankTransactionLine');
const {
AccountTransaction,
} = require('../../Accounts/models/AccountTransaction.model');
const { Account } = require('../../Accounts/models/Account.model');
const {
MatchedBankTransaction,
} = require('../../BankingMatching/models/MatchedBankTransaction');
return {
/**
* Cashflow transaction entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: BankTransactionLine,
join: {
from: 'cashflow_transactions.id',
to: 'cashflow_transaction_lines.cashflowTransactionId',
},
filter: (query) => {
query.orderBy('index', 'ASC');
},
},
/**
* Cashflow transaction has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'cashflow_transactions.id',
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'CashflowTransaction');
},
},
/**
* Cashflow transaction may has associated cashflow account.
*/
cashflowAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'cashflow_transactions.cashflowAccountId',
to: 'accounts.id',
},
},
/**
* Cashflow transcation may has associated to credit account.
*/
creditAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'cashflow_transactions.creditAccountId',
to: 'accounts.id',
},
},
/**
* Cashflow transaction may belongs to matched bank transaction.
*/
matchedBankTransaction: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'cashflow_transactions.id',
to: 'matched_bank_transactions.referenceId',
},
filter: (query) => {
const referenceTypes = getCashflowAccountTransactionsTypes();
query.whereIn('reference_type', referenceTypes);
},
},
};
}
}

View File

@@ -0,0 +1,53 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class BankTransactionLine extends TenantBaseModel{
/**
* Table name.
*/
static get tableName() {
return 'cashflow_transaction_lines';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Determine whether the model is resourceable.
* @returns {boolean}
*/
static get resourceable(): boolean {
return false;
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Account } = require('../../Accounts/models/Account.model');
return {
cashflowAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'cashflow_transaction_lines.cashflowAccountId',
to: 'accounts.id',
},
},
creditAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'cashflow_transaction_lines.creditAccountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -0,0 +1,240 @@
/* eslint-disable global-require */
import * as moment from 'moment';
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
export class UncategorizedBankTransaction extends BaseModel {
readonly amount!: number;
readonly date!: Date | string;
readonly categorized!: boolean;
readonly accountId!: number;
readonly referenceNo!: string;
readonly payee!: string;
readonly description!: string;
readonly plaidTransactionId!: string;
readonly recognizedTransactionId!: number;
readonly excludedAt: Date;
readonly pending: boolean;
readonly categorizeRefId!: number;
readonly categorizeRefType!: string;
/**
* Table name.
*/
static get tableName() {
return 'uncategorized_cashflow_transactions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'withdrawal',
'deposit',
'isDepositTransaction',
'isWithdrawalTransaction',
'isRecognized',
'isExcluded',
'isPending',
];
}
// static get meta() {
// return UncategorizedCashflowTransactionMeta;
// }
/**
* Retrieves the withdrawal amount.
* @returns {number}
*/
public get withdrawal() {
return this.amount < 0 ? Math.abs(this.amount) : 0;
}
/**
* Retrieves the deposit amount.
* @returns {number}
*/
public get deposit(): number {
return this.amount > 0 ? Math.abs(this.amount) : 0;
}
/**
* Detarmines whether the transaction is deposit transaction.
*/
public get isDepositTransaction(): boolean {
return 0 < this.deposit;
}
/**
* Detarmines whether the transaction is withdrawal transaction.
*/
public get isWithdrawalTransaction(): boolean {
return 0 < this.withdrawal;
}
/**
* Detarmines whether the transaction is recognized.
*/
public get isRecognized(): boolean {
return !!this.recognizedTransactionId;
}
/**
* Detarmines whether the transaction is excluded.
* @returns {boolean}
*/
public get isExcluded(): boolean {
return !!this.excludedAt;
}
/**
* Detarmines whether the transaction is pending.
* @returns {boolean}
*/
public get isPending(): boolean {
return !!this.pending;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the not excluded transactions.
*/
notExcluded(query) {
query.whereNull('excluded_at');
},
/**
* Filters the excluded transactions.
*/
excluded(query) {
query.whereNotNull('excluded_at');
},
/**
* Filter out the recognized transactions.
* @param query
*/
recognized(query) {
query.whereNotNull('recognizedTransactionId');
},
/**
* Filter out the not recognized transactions.
* @param query
*/
notRecognized(query) {
query.whereNull('recognizedTransactionId');
},
categorized(query) {
query.whereNotNull('categorizeRefType');
query.whereNotNull('categorizeRefId');
},
notCategorized(query) {
query.whereNull('categorizeRefType');
query.whereNull('categorizeRefId');
},
/**
* Filters the not pending transactions.
*/
notPending(query) {
query.where('pending', false);
},
/**
* Filters the pending transactions.
*/
pending(query) {
query.where('pending', true);
},
minAmount(query, minAmount) {
query.where('amount', '>=', minAmount);
},
maxAmount(query, maxAmount) {
query.where('amount', '<=', maxAmount);
},
toDate(query, toDate) {
const dateFormat = 'YYYY-MM-DD';
const _toDate = moment(toDate).endOf('day').format(dateFormat);
query.where('date', '<=', _toDate);
},
fromDate(query, fromDate) {
const dateFormat = 'YYYY-MM-DD';
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
query.where('date', '>=', _fromDate);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Account } = require('../../Accounts/models/Account.model');
const {
RecognizedBankTransaction,
} = require('../../BankingTranasctionsRegonize/models/RecognizedBankTransaction');
const {
MatchedBankTransaction,
} = require('../../BankingMatching/models/MatchedBankTransaction');
return {
/**
* Transaction may has associated to account.
*/
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'uncategorized_cashflow_transactions.accountId',
to: 'accounts.id',
},
},
/**
* Transaction may has association to recognized transaction.
*/
recognizedTransaction: {
relation: Model.HasOneRelation,
modelClass: RecognizedBankTransaction,
join: {
from: 'uncategorized_cashflow_transactions.recognizedTransactionId',
to: 'recognized_bank_transactions.id',
},
},
/**
* Uncategorized transaction may has association to matched transaction.
*/
matchedBankTransactions: {
relation: Model.HasManyRelation,
modelClass: MatchedBankTransaction,
join: {
from: 'uncategorized_cashflow_transactions.id',
to: 'matched_bank_transactions.uncategorizedTransactionId',
},
},
};
}
}

View File

@@ -0,0 +1,66 @@
import { Account } from '../../Accounts/models/Account.model';
import { Transformer } from '../../Transformer/Transformer';
export class CashflowAccountTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'lastFeedsUpdatedAt',
'lastFeedsUpdatedAtFormatted',
'lastFeedsUpdatedFromNow',
];
};
/**
* Exclude these attributes to sale invoice object.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return [
'predefined',
'index',
'accountRootType',
'accountTypeLabel',
'accountParentType',
'isBalanceSheetAccount',
'isPlSheet',
];
};
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: Account): string => {
return this.formatNumber(account.amount, {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the last feeds update at formatted date.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedAtFormatted(account: Account): string {
return account.lastFeedsUpdatedAt
? this.formatDate(account.lastFeedsUpdatedAt)
: '';
}
/**
* Retrieves the last feeds updated from now.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedFromNow(account: Account): string {
return account.lastFeedsUpdatedAt
? this.formatDateFromNow(account.lastFeedsUpdatedAt)
: '';
}
}

View File

@@ -0,0 +1,55 @@
import { Transformer } from '../../Transformer/Transformer';
export class BankTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'transactionTypeFormatted',
'formattedDate',
'formattedCreatedAt',
];
};
/**
* Formatted amount.
* @param {} transaction
* @returns {string}
*/
protected formattedAmount = (transaction) => {
return this.formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
/**
* Formatted transaction type.
* @param transaction
* @returns {string}
*/
protected transactionTypeFormatted = (transaction) => {
return this.context.i18n.t(transaction.transactionType);
};
/**
* Retrieve the formatted transaction date.
* @param invoice
* @returns {string}
*/
protected formattedDate = (invoice): string => {
return this.formatDate(invoice.date);
};
/**
* Retrieve the formatted created at date.
* @param invoice
* @returns {string}
*/
protected formattedCreatedAt = (invoice): string => {
return this.formatDate(invoice.createdAt);
};
}

View File

@@ -0,0 +1,70 @@
import { Transformer } from "@/modules/Transformer/Transformer";
export class BankTransactionsTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['deposit', 'withdrawal', 'formattedDeposit', 'formattedWithdrawal'];
};
/**
* Exclude these attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return [
'credit',
'debit',
'index',
'index_group',
'item_id',
'item_quantity',
'contact_type',
'contact_id',
];
};
/**
* Deposit amount attribute.
* @param transaction
* @returns
*/
protected deposit = (transaction) => {
return transaction.debit;
};
/**
* Withdrawal amount attribute.
* @param transaction
* @returns
*/
protected withdrawal = (transaction) => {
return transaction.credit;
};
/**
* Formatted withdrawal amount.
* @param transaction
* @returns
*/
protected formattedWithdrawal = (transaction) => {
return this.formatNumber(transaction.credit, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
/**
* Formatted deposit account.
* @param transaction
* @returns
*/
protected formattedDeposit = (transaction) => {
return this.formatNumber(transaction.debit, {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
};
}

View File

@@ -0,0 +1,54 @@
// @ts-nocheck
import { Injectable, Inject } from '@nestjs/common';
import { BankAccount } from '../models/BankAccount';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { CashflowAccountTransformer } from './BankAccountTransformer';
import { ACCOUNT_TYPE } from '@/constants/accounts';
import { IBankAccountsFilter } from '../types/BankingTransactions.types';
@Injectable()
export class GetBankAccountsService {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformer: TransformerInjectable,
@Inject(BankAccount.name)
private readonly bankAccountModel: typeof BankAccount,
) {}
/**
* Retrieve the cash flow accounts.
* @param {ICashflowAccountsFilter} filterDTO - Filter DTO.
* @returns {ICashflowAccount[]}
*/
public async getBankAccounts(
filterDTO: IBankAccountsFilter,
): Promise<BankAccount[]> {
// Parsees accounts list filter DTO.
const filter = this.dynamicListService.parseStringifiedFilter(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
this.bankAccountModel,
filter,
);
// Retrieve accounts model based on the given query.
const accounts = await this.bankAccountModel.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.whereIn('account_type', [
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.CREDIT_CARD,
]);
builder.modify('inactiveMode', filter.inactiveMode);
});
// Retrieves the transformed accounts.
const transformed = await this.transformer.transform(
accounts,
new CashflowAccountTransformer(),
);
return transformed;
}
}

View File

@@ -0,0 +1,51 @@
import { Inject, Injectable } from '@nestjs/common';
import { ERRORS } from '../constants';
import { BankTransaction } from '../models/BankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ServiceError } from '@/modules/Items/ServiceError';
import { BankTransactionTransformer } from './BankTransactionTransformer';
@Injectable()
export class GetBankTransactionService {
constructor(
@Inject(BankTransaction.name)
private readonly bankTransactionModel: typeof BankTransaction,
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve the given cashflow transaction.
* @param {number} cashflowTransactionId
* @returns
*/
public async getBankTransaction(cashflowTransactionId: number) {
const cashflowTransaction = await this.bankTransactionModel
.query()
.findById(cashflowTransactionId)
.withGraphFetched('entries.cashflowAccount')
.withGraphFetched('entries.creditAccount')
.withGraphFetched('transactions.account')
.orderBy('date', 'DESC')
.throwIfNotFound();
this.throwErrorCashflowTransactionNotFound(cashflowTransaction);
// Transforms the cashflow transaction model to POJO.
return this.transformer.transform(
cashflowTransaction,
new BankTransactionTransformer(),
);
}
/**
* Throw not found error if the given cashflow is undefined.
* @param {BankTransaction} bankTransaction - Bank transaction.
*/
private throwErrorCashflowTransactionNotFound(
bankTransaction: BankTransaction,
) {
if (!bankTransaction) {
throw new ServiceError(ERRORS.CASHFLOW_TRANSACTION_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,48 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
@Injectable()
export class GetPendingBankAccountTransactions {
constructor(
private readonly transformerService: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction
) {}
/**
* Retrieves the given bank accounts pending transaction.
* @param {GetPendingTransactionsQuery} filter - Pending transactions query.
*/
async getPendingTransactions(filter?: GetPendingTransactionsQuery) {
const _filter = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.modify('pending');
if (_filter?.accountId) {
q.where('accountId', _filter.accountId);
}
})
.pagination(_filter.page - 1, _filter.pageSize);
const data = await this.transformerService.transform(
results,
new GetPendingBankAccountTransactionTransformer()
);
return { data, pagination };
}
}
interface GetPendingTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}

View File

@@ -0,0 +1,72 @@
import { Transformer } from '@/modules/Transformer/Transformer';
export class GetPendingBankAccountTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedDate',
'formattedDepositAmount',
'formattedWithdrawalAmount',
];
};
/**
* Exclude all attributes.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return [];
};
/**
* Formattes the transaction date.
* @param transaction
* @returns {string}
*/
public formattedDate(transaction) {
return this.formatDate(transaction.date);
}
/**
* Formatted amount.
* @param transaction
* @returns {string}
*/
public formattedAmount(transaction) {
return this.formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
});
}
/**
* Formatted deposit amount.
* @param transaction
* @returns {string}
*/
protected formattedDepositAmount(transaction) {
if (transaction.isDepositTransaction) {
return this.formatNumber(transaction.deposit, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
/**
* Formatted withdrawal amount.
* @param transaction
* @returns {string}
*/
protected formattedWithdrawalAmount(transaction) {
if (transaction.isWithdrawalTransaction) {
return this.formatNumber(transaction.withdrawal, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
}

View File

@@ -0,0 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetRecognizedTransactionService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Retrieves the recognized transaction of the given uncategorized transaction.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
*/
public async getRecognizedTransaction(uncategorizedTransactionId: number) {
const uncategorizedTransaction =
await this.uncategorizedBankTransactionModel()
.query()
.findById(uncategorizedTransactionId)
.withGraphFetched('matchedBankTransactions')
.withGraphFetched('recognizedTransaction.assignAccount')
.withGraphFetched('recognizedTransaction.bankRule')
.withGraphFetched('account')
.throwIfNotFound();
return this.transformer.transform(
uncategorizedTransaction,
new GetRecognizedTransactionTransformer(),
);
}
}

View File

@@ -0,0 +1,261 @@
import { Transformer } from "@/modules/Transformer/Transformer";
export class GetRecognizedTransactionTransformer extends Transformer {
/**
* Include these attributes to sale credit note object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'uncategorizedTransactionId',
'referenceNo',
'description',
'payee',
'amount',
'formattedAmount',
'date',
'formattedDate',
'assignedAccountId',
'assignedAccountName',
'assignedAccountCode',
'assignedPayee',
'assignedMemo',
'assignedCategory',
'assignedCategoryFormatted',
'withdrawal',
'deposit',
'isDepositTransaction',
'isWithdrawalTransaction',
'formattedDepositAmount',
'formattedWithdrawalAmount',
'bankRuleId',
'bankRuleName',
];
};
/**
* Exclude all attributes.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Get the uncategorized transaction id.
* @param transaction
* @returns {number}
*/
public uncategorizedTransactionId = (transaction): number => {
return transaction.id;
}
/**
* Get the reference number of the transaction.
* @param {object} transaction
* @returns {string}
*/
public referenceNo(transaction: any): string {
return transaction.referenceNo;
}
/**
* Get the description of the transaction.
* @param {object} transaction
* @returns {string}
*/
public description(transaction: any): string {
return transaction.description;
}
/**
* Get the payee of the transaction.
* @param {object} transaction
* @returns {string}
*/
public payee(transaction: any): string {
return transaction.payee;
}
/**
* Get the amount of the transaction.
* @param {object} transaction
* @returns {number}
*/
public amount(transaction: any): number {
return transaction.amount;
}
/**
* Get the formatted amount of the transaction.
* @param {object} transaction
* @returns {string}
*/
public formattedAmount(transaction: any): string {
return this.formatNumber(transaction.formattedAmount, {
money: true,
});
}
/**
* Get the date of the transaction.
* @param {object} transaction
* @returns {string}
*/
public date(transaction: any): string {
return transaction.date;
}
/**
* Get the formatted date of the transaction.
* @param {object} transaction
* @returns {string}
*/
public formattedDate(transaction: any): string {
return this.formatDate(transaction.date);
}
/**
* Get the assigned account ID of the transaction.
* @param {object} transaction
* @returns {number}
*/
public assignedAccountId(transaction: any): number {
return transaction.recognizedTransaction.assignedAccountId;
}
/**
* Get the assigned account name of the transaction.
* @param {object} transaction
* @returns {string}
*/
public assignedAccountName(transaction: any): string {
return transaction.recognizedTransaction.assignAccount.name;
}
/**
* Get the assigned account code of the transaction.
* @param {object} transaction
* @returns {string}
*/
public assignedAccountCode(transaction: any): string {
return transaction.recognizedTransaction.assignAccount.code;
}
/**
* Get the assigned payee of the transaction.
* @param {object} transaction
* @returns {string}
*/
public getAssignedPayee(transaction: any): string {
return transaction.recognizedTransaction.assignedPayee;
}
/**
* Get the assigned memo of the transaction.
* @param {object} transaction
* @returns {string}
*/
public assignedMemo(transaction: any): string {
return transaction.recognizedTransaction.assignedMemo;
}
/**
* Get the assigned category of the transaction.
* @param {object} transaction
* @returns {string}
*/
public assignedCategory(transaction: any): string {
return transaction.recognizedTransaction.assignedCategory;
}
/**
*
* @returns {string}
*/
public assignedCategoryFormatted() {
return 'Other Income'
}
/**
* Check if the transaction is a withdrawal.
* @param {object} transaction
* @returns {boolean}
*/
public isWithdrawal(transaction: any): boolean {
return transaction.withdrawal;
}
/**
* Check if the transaction is a deposit.
* @param {object} transaction
* @returns {boolean}
*/
public isDeposit(transaction: any): boolean {
return transaction.deposit;
}
/**
* Check if the transaction is a deposit transaction.
* @param {object} transaction
* @returns {boolean}
*/
public isDepositTransaction(transaction: any): boolean {
return transaction.isDepositTransaction;
}
/**
* Check if the transaction is a withdrawal transaction.
* @param {object} transaction
* @returns {boolean}
*/
public isWithdrawalTransaction(transaction: any): boolean {
return transaction.isWithdrawalTransaction;
}
/**
* Get formatted deposit amount.
* @param {any} transaction
* @returns {string}
*/
protected formattedDepositAmount(transaction) {
if (transaction.isDepositTransaction) {
return this.formatNumber(transaction.deposit, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
/**
* Get formatted withdrawal amount.
* @param transaction
* @returns {string}
*/
protected formattedWithdrawalAmount(transaction) {
if (transaction.isWithdrawalTransaction) {
return this.formatNumber(transaction.withdrawal, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
/**
* Get the transaction bank rule id.
* @param transaction
* @returns {string}
*/
protected bankRuleId(transaction) {
return transaction.recognizedTransaction.bankRuleId;
}
/**
* Get the transaction bank rule name.
* @param transaction
* @returns {string}
*/
protected bankRuleName(transaction) {
return transaction.recognizedTransaction.bankRule.name;
}
}

View File

@@ -0,0 +1,67 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { IGetRecognizedTransactionsQuery } from '../types/BankingTransactions.types';
@Injectable()
export class GetRecognizedTransactionsService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves the recognized transactions of the given account.
* @param {number} tenantId
* @param {IGetRecognizedTransactionsQuery} filter -
*/
async getRecognizedTranactions(filter?: IGetRecognizedTransactionsQuery) {
const _query = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId');
// Exclude the excluded transactions.
q.modify('notExcluded');
// Exclude the pending transactions.
q.modify('notPending');
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
})
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
results,
new GetRecognizedTransactionTransformer(),
);
return { data, pagination };
}
}

View File

@@ -0,0 +1,32 @@
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
@Injectable()
export class GetUncategorizedBankTransactionService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransaction: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves specific uncategorized cashflow transaction.
* @param {number} tenantId - Tenant id.
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
*/
public async getTransaction(uncategorizedTransactionId: number) {
const transaction = await this.uncategorizedBankTransaction
.query()
.findById(uncategorizedTransactionId)
.withGraphFetched('account')
.throwIfNotFound();
return this.transformer.transform(
transaction,
new UncategorizedTransactionTransformer(),
);
}
}

View File

@@ -0,0 +1,73 @@
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
import { IGetUncategorizedTransactionsQuery } from '../types/BankingTransactions.types';
@Injectable()
export class GetUncategorizedTransactions {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves the uncategorized cashflow transactions.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account Id.
*/
public async getTransactions(
accountId: number,
query: IGetUncategorizedTransactionsQuery
) {
// Parsed query with default values.
const _query = {
page: 1,
pageSize: 20,
...query,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.where('accountId', accountId);
q.where('categorized', false);
q.modify('notExcluded');
q.modify('notPending');
q.withGraphFetched('account');
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC');
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
results,
new UncategorizedTransactionTransformer()
);
return {
data,
pagination,
};
}
}

View File

@@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { BankTransactionAutoIncrement } from '../commands/BankTransactionAutoIncrement.service';
import { BankTransactionGLEntriesService } from '../commands/BankTransactionGLEntries';
import { events } from '@/common/events/events';
import { ICommandCashflowCreatedPayload, ICommandCashflowDeletedPayload } from '../types/BankingTransactions.types';
@Injectable()
export class BankingTransactionGLEntriesSubscriber {
/**
* @param {BankTransactionGLEntriesService} bankTransactionGLEntries - Bank transaction GL entries service.
* @param {BankTransactionAutoIncrement} cashflowTransactionAutoIncrement - Cashflow transaction auto increment service.
*/
constructor(
private readonly bankTransactionGLEntries: BankTransactionGLEntriesService,
private readonly cashflowTransactionAutoIncrement: BankTransactionAutoIncrement,
) {}
/**
* Writes the journal entries once the cashflow transaction create.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionCreated)
public async writeJournalEntriesOnceTransactionCreated({
cashflowTransaction,
trx,
}: ICommandCashflowCreatedPayload) {
// Can't write GL entries if the transaction not published yet.
if (!cashflowTransaction.isPublished) return;
await this.bankTransactionGLEntries.writeJournalEntries(
cashflowTransaction.id,
trx,
);
}
/**
* Increment the cashflow transaction number once the transaction created.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionCreated)
public async incrementTransactionNumberOnceTransactionCreated({}: ICommandCashflowCreatedPayload) {
this.cashflowTransactionAutoIncrement.incrementNextTransactionNumber();
}
/**
* Deletes the GL entries once the cashflow transaction deleted.
* @param {ICommandCashflowDeletedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionDeleted)
public async revertGLEntriesOnceTransactionDeleted({
cashflowTransactionId,
trx,
}: ICommandCashflowDeletedPayload) {
await this.bankTransactionGLEntries.revertJournalEntries(
cashflowTransactionId,
trx,
);
};
}

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { ValidateDeleteBankAccountTransactions } from '../commands/ValidateDeleteBankAccountTransactions.service';
import { OnEvent } from '@nestjs/event-emitter';
import { IAccountEventDeletePayload } from '@/interfaces/Account';
@Injectable()
export class CashflowWithAccountSubscriber {
constructor(
private readonly validateDeleteBankAccount: ValidateDeleteBankAccountTransactions,
) {}
/**
* Validate chart account has no associated cashflow transactions on delete.
* @param {IAccountEventDeletePayload} payload -
*/
@OnEvent(events.accounts.onDelete)
public async validateAccountHasNoCashflowTransactionsOnDelete({
oldAccount,
}: IAccountEventDeletePayload) {
await this.validateDeleteBankAccount.validateAccountHasNoCashflowEntries(
oldAccount.id
);
};
}

View File

@@ -0,0 +1,88 @@
import PromisePool from '@supercharge/promise-pool';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload,
} from '../types/BankingTransactions.types';
import { OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
@Injectable()
export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
) {}
/**
* Decrement the uncategoirzed transactions on the account once categorizing.
* @param {ICashflowTransactionCategorizedPayload}
*/
@OnEvent(events.cashflow.onTransactionCategorized)
public async decrementUnCategorizedTransactionsOnCategorized({
uncategorizedTransactions,
trx,
}: ICashflowTransactionCategorizedPayload) {
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(
async (uncategorizedTransaction: UncategorizedBankTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1);
},
);
}
/**
* Increment the uncategorized transaction on the given account on uncategorizing.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.cashflow.onTransactionUncategorized)
public async incrementUnCategorizedTransactionsOnUncategorized({
uncategorizedTransactions,
trx,
}: ICashflowTransactionUncategorizedPayload) {
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(
async (uncategorizedTransaction: UncategorizedBankTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
},
);
}
/**
* Increments uncategorized transactions count once creating a new transaction.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionUncategorizedCreated)
public async incrementUncategoirzedTransactionsOnCreated({
uncategorizedTransaction,
trx,
}: any) {
if (!uncategorizedTransaction.accountId) return;
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) return;
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
}
}

View File

@@ -0,0 +1,35 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PromisePool } from '@supercharge/promise-pool';
import { DeleteCashflowTransaction } from '../commands/DeleteCashflowTransaction.service';
import { events } from '@/common/events/events';
import { ICashflowTransactionUncategorizedPayload } from '@/modules/BankingCategorize/types/BankingCategorize.types';
@Injectable()
export class DeleteCashflowTransactionOnUncategorizeSubscriber {
constructor(
private readonly deleteCashflowTransactionService: DeleteCashflowTransaction,
) {}
/**
* Deletes the cashflow transaction once uncategorize the bank transaction.
* @param {ICashflowTransactionUncategorizedPayload} payload
*/
@OnEvent(events.cashflow.onTransactionUncategorized)
public async deleteCashflowTransactionOnUncategorize({
oldMainUncategorizedTransaction,
trx,
}: ICashflowTransactionUncategorizedPayload) {
// Cannot continue if the main transaction does not reference to cashflow type.
if (
oldMainUncategorizedTransaction.categorizeRefType !==
'CashflowTransaction'
) {
return;
}
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
oldMainUncategorizedTransaction.categorizeRefId,
trx
);
}
}

View File

@@ -0,0 +1,42 @@
import { events } from '@/common/events/events';
import { ERRORS } from '../constants';
import { OnEvent } from '@nestjs/event-emitter';
import { ServiceError } from '@/modules/Items/ServiceError';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { ICommandCashflowDeletingPayload } from '../types/BankingTransactions.types';
@Injectable()
export class PreventDeleteTransactionOnDeleteSubscriber {
constructor(
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Prevent delete cashflow transaction has converted from uncategorized transaction.
* @param {ICommandCashflowDeletingPayload} payload
*/
@OnEvent(events.cashflow.onTransactionDeleting)
public async preventDeleteCashflowTransactionHasUncategorizedTransaction({
oldCashflowTransaction,
trx,
}: ICommandCashflowDeletingPayload) {
if (oldCashflowTransaction.uncategorizedTransactionId) {
const foundTransactions = await this.uncategorizedBankTransactionModel
.query(trx)
.where({
categorized: true,
categorizeRefId: oldCashflowTransaction.id,
categorizeRefType: 'CashflowTransaction',
});
// Throw the error if the cashflow transaction still linked to uncategorized transaction.
if (foundTransactions.length > 0) {
throw new ServiceError(
ERRORS.CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED,
'Cannot delete cashflow transaction converted from uncategorized transaction.',
);
}
}
}
}

View File

@@ -0,0 +1,130 @@
import { Knex } from 'knex';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { BankTransaction } from '../models/BankTransaction';
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
export interface IPendingTransactionRemovingEventPayload {
uncategorizedTransactionId: number;
pendingTransaction: UncategorizedBankTransaction;
trx?: Knex.Transaction;
}
export interface IPendingTransactionRemovedEventPayload {
uncategorizedTransactionId: number;
pendingTransaction: UncategorizedBankTransaction;
trx?: Knex.Transaction;
}
export interface IGetRecognizedTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface ICashflowCommandDTO {
date: Date;
transactionNumber: string;
referenceNo: string;
transactionType: string;
description: string;
amount: number;
exchangeRate: number;
currencyCode: string;
creditAccountId: number;
cashflowAccountId: number;
publish: boolean;
branchId?: number;
plaidTransactionId?: string;
}
export interface ICashflowNewCommandDTO extends ICashflowCommandDTO {
plaidAccountId?: string;
uncategorizedTransactionId?: number;
}
export interface IBankAccountsFilter {
inactiveMode: boolean;
stringifiedFilterRoles?: string;
sortOrder: string;
columnSortBy: string;
}
export enum CashflowDirection {
IN = 'in',
OUT = 'out',
}
export interface ICommandCashflowCreatingPayload {
trx: Knex.Transaction;
newTransactionDTO: ICashflowNewCommandDTO;
}
export interface ICommandCashflowCreatedPayload {
newTransactionDTO: CreateBankTransactionDto;
cashflowTransaction: BankTransaction;
trx: Knex.Transaction;
}
export interface ICommandCashflowDeletingPayload {
oldCashflowTransaction: BankTransaction;
trx: Knex.Transaction;
}
export interface ICommandCashflowDeletedPayload {
cashflowTransactionId: number;
oldCashflowTransaction: BankTransaction;
trx: Knex.Transaction;
}
export interface ICashflowTransactionCategorizedPayload {
uncategorizedTransactions: Array<UncategorizedBankTransaction>;
cashflowTransaction: BankTransaction;
oldUncategorizedTransactions: Array<UncategorizedBankTransaction>;
categorizeDTO: any;
trx: Knex.Transaction;
}
export interface ICashflowTransactionUncategorizingPayload {
uncategorizedTransactionId: number;
oldUncategorizedTransactions: Array<UncategorizedBankTransaction>;
trx: Knex.Transaction;
}
export interface ICashflowTransactionUncategorizedPayload {
uncategorizedTransactionId: number;
uncategorizedTransactions: Array<UncategorizedBankTransaction>;
oldMainUncategorizedTransaction: UncategorizedBankTransaction;
oldUncategorizedTransactions: Array<UncategorizedBankTransaction>;
trx: Knex.Transaction;
}
export enum CashflowAction {
Create = 'Create',
Delete = 'Delete',
View = 'View',
}
export interface CategorizeTransactionAsExpenseDTO {
expenseAccountId: number;
exchangeRate: number;
referenceNo: string;
description: string;
branchId?: number;
}
export interface IGetUncategorizedTransactionsQuery {
page?: number;
pageSize?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}

View File

@@ -0,0 +1,125 @@
import { upperFirst, camelCase, first, sumBy, isObject } from 'lodash';
import {
CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META,
CashflowTransactionTypes,
ERRORS,
ICashflowTransactionTypeMeta,
TransactionTypes,
} from './constants';
import { ICashflowNewCommandDTO } from './types/BankingTransactions.types';
import { UncategorizedBankTransaction } from './models/UncategorizedBankTransaction';
import { ICategorizeCashflowTransactioDTO } from '../BankingCategorize/types/BankingCategorize.types';
import { ServiceError } from '../Items/ServiceError';
/**
* Ensures the given transaction type to transformed to appropriate format.
* @param {string} type
* @returns {string}
*/
export const transformCashflowTransactionType = (type) => {
return upperFirst(camelCase(type));
};
/**
* Retrieve the cashflow transaction type meta.
* @param {CASHFLOW_TRANSACTION_TYPE} transactionType
* @returns {ICashflowTransactionTypeMeta}
*/
export function getCashflowTransactionType(
transactionType: CASHFLOW_TRANSACTION_TYPE,
): ICashflowTransactionTypeMeta {
const _transactionType = transformCashflowTransactionType(transactionType);
return CASHFLOW_TRANSACTION_TYPE_META[_transactionType];
}
/**
* Retrieve cashflow accounts transactions types
* @returns {string}
*/
export const getCashflowAccountTransactionsTypes = () => {
return Object.values(CASHFLOW_TRANSACTION_TYPE_META).map((meta) => meta.type);
};
/**
* Tranasformes the given uncategorized transaction and categorized DTO
* to cashflow create DTO.
* @param {IUncategorizedCashflowTransaction} uncategorizeModel
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO
* @returns {ICashflowNewCommandDTO}
*/
export const transformCategorizeTransToCashflow = (
uncategorizeTransactions: Array<UncategorizedBankTransaction>,
categorizeDTO: ICategorizeCashflowTransactioDTO,
): ICashflowNewCommandDTO => {
const uncategorizeTransaction = first(uncategorizeTransactions);
const amount = sumBy(uncategorizeTransactions, 'amount');
const amountAbs = Math.abs(amount);
return {
date: categorizeDTO.date,
referenceNo: categorizeDTO.referenceNo,
description: categorizeDTO.description,
cashflowAccountId: uncategorizeTransaction.accountId,
creditAccountId: categorizeDTO.creditAccountId,
exchangeRate: categorizeDTO.exchangeRate || 1,
currencyCode: categorizeDTO.currencyCode,
amount: amountAbs,
transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType,
branchId: categorizeDTO?.branchId,
publish: true,
};
};
export const validateUncategorizedTransactionsNotExcluded = (
transactions: Array<UncategorizedBankTransaction>,
) => {
const excluded = transactions.filter((tran) => tran.isExcluded);
if (excluded?.length > 0) {
throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION, '', {
ids: excluded.map((t) => t.id),
});
}
};
export const validateTransactionShouldBeCategorized = (
uncategorizedTransaction: any,
) => {
if (!uncategorizedTransaction.categorized) {
throw new ServiceError(ERRORS.TRANSACTION_NOT_CATEGORIZED);
}
};
/**
* Retrieves the formatted type of account transaction.
* @param {string} referenceType
* @param {string} transactionType
* @returns {string}
*/
export const getTransactionTypeLabel = (
referenceType: string,
transactionType?: string,
) => {
const _referenceType = upperFirst(camelCase(referenceType));
const _transactionType = upperFirst(camelCase(transactionType));
return isObject(TransactionTypes[_referenceType])
? TransactionTypes[_referenceType][_transactionType]
: TransactionTypes[_referenceType] || null;
};
/**
* Retrieves the formatted type of cashflow transaction.
* @param {string} transactionType
* @returns {string¿}
*/
export const getCashflowTransactionFormattedType = (
transactionType: string,
) => {
const _transactionType = upperFirst(camelCase(transactionType));
return CashflowTransactionTypes[_transactionType] || null;
};