mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||
import { RecognizedBankTransaction } from './models/RecognizedBankTransaction';
|
||||
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
|
||||
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
|
||||
import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service';
|
||||
import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions';
|
||||
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
|
||||
import { BankRulesModule } from '../BankRules/BankRules.module';
|
||||
|
||||
const models = [RegisterTenancyModel(RecognizedBankTransaction)];
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BankingTransactionsModule,
|
||||
forwardRef(() => BankRulesModule),
|
||||
...models,
|
||||
],
|
||||
providers: [
|
||||
GetAutofillCategorizeTransactionService,
|
||||
RevertRecognizedTransactionsService,
|
||||
RecognizeTranasctionsService,
|
||||
TriggerRecognizedTransactionsSubscriber,
|
||||
],
|
||||
exports: [
|
||||
...models,
|
||||
GetAutofillCategorizeTransactionService,
|
||||
RevertRecognizedTransactionsService,
|
||||
RecognizeTranasctionsService,
|
||||
],
|
||||
})
|
||||
export class BankingTransactionsRegonizeModule {}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface RevertRecognizedTransactionsCriteria {
|
||||
batch?: string;
|
||||
accountId?: number;
|
||||
}
|
||||
|
||||
export interface RecognizeTransactionsCriteria {
|
||||
batch?: string;
|
||||
accountId?: number;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { lowerCase } from 'lodash';
|
||||
import { UncategorizedBankTransaction } from '../BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { BankRuleApplyIfTransactionType, BankRuleConditionComparator, BankRuleConditionType, IBankRuleCondition } from '../BankRules/types';
|
||||
import { BankRule } from '../BankRules/models/BankRule';
|
||||
import { BankRuleCondition } from '../BankRules/models/BankRuleCondition';
|
||||
|
||||
const conditionsMatch = (
|
||||
transaction: UncategorizedBankTransaction,
|
||||
conditions: BankRuleCondition[],
|
||||
conditionsType: BankRuleConditionType = BankRuleConditionType.And
|
||||
) => {
|
||||
const method =
|
||||
conditionsType === BankRuleConditionType.And ? 'every' : 'some';
|
||||
|
||||
return conditions[method]((condition) => {
|
||||
switch (determineFieldType(condition.field)) {
|
||||
case 'number':
|
||||
return matchNumberCondition(transaction, condition);
|
||||
case 'text':
|
||||
return matchTextCondition(transaction, condition);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const matchNumberCondition = (
|
||||
transaction: UncategorizedBankTransaction,
|
||||
condition: BankRuleCondition
|
||||
) => {
|
||||
const conditionValue = parseFloat(condition.value);
|
||||
const transactionAmount =
|
||||
condition.field === 'amount'
|
||||
? Math.abs(transaction[condition.field])
|
||||
: (transaction[condition.field] as unknown as number);
|
||||
|
||||
switch (condition.comparator) {
|
||||
case BankRuleConditionComparator.Equals:
|
||||
case BankRuleConditionComparator.Equal:
|
||||
return transactionAmount === conditionValue;
|
||||
|
||||
case BankRuleConditionComparator.BiggerOrEqual:
|
||||
return transactionAmount >= conditionValue;
|
||||
|
||||
case BankRuleConditionComparator.Bigger:
|
||||
return transactionAmount > conditionValue;
|
||||
|
||||
case BankRuleConditionComparator.Smaller:
|
||||
return transactionAmount < conditionValue;
|
||||
|
||||
case BankRuleConditionComparator.SmallerOrEqual:
|
||||
return transactionAmount <= conditionValue;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const matchTextCondition = (
|
||||
transaction: UncategorizedBankTransaction,
|
||||
condition: BankRuleCondition
|
||||
): boolean => {
|
||||
const transactionValue = transaction[condition.field] as string;
|
||||
|
||||
switch (condition.comparator) {
|
||||
case BankRuleConditionComparator.Equals:
|
||||
case BankRuleConditionComparator.Equal:
|
||||
return transactionValue === condition.value;
|
||||
case BankRuleConditionComparator.Contains:
|
||||
const fieldValue = lowerCase(transactionValue);
|
||||
const conditionValue = lowerCase(condition.value);
|
||||
|
||||
return fieldValue.includes(conditionValue);
|
||||
case BankRuleConditionComparator.NotContain:
|
||||
return !transactionValue?.includes(condition.value.toString());
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const matchTransactionType = (
|
||||
bankRule: BankRule,
|
||||
transaction: UncategorizedBankTransaction
|
||||
): boolean => {
|
||||
return (
|
||||
(transaction.isDepositTransaction &&
|
||||
bankRule.applyIfTransactionType ===
|
||||
BankRuleApplyIfTransactionType.Deposit) ||
|
||||
(transaction.isWithdrawalTransaction &&
|
||||
bankRule.applyIfTransactionType ===
|
||||
BankRuleApplyIfTransactionType.Withdrawal)
|
||||
);
|
||||
};
|
||||
|
||||
export const bankRulesMatchTransaction = (
|
||||
transaction: UncategorizedBankTransaction,
|
||||
bankRules: BankRule[]
|
||||
) => {
|
||||
return bankRules.find((rule) => {
|
||||
return (
|
||||
matchTransactionType(rule, transaction) &&
|
||||
conditionsMatch(transaction, rule.conditions, rule.conditionsType)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const determineFieldType = (field: string): string => {
|
||||
switch (field) {
|
||||
case 'amount':
|
||||
return 'number';
|
||||
case 'description':
|
||||
case 'payee':
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { castArray, isEmpty } from 'lodash';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { bankRulesMatchTransaction } from '../_utils';
|
||||
import { RecognizeTransactionsCriteria } from '../_types';
|
||||
import { BankRule } from '@/modules/BankRules/models/BankRule';
|
||||
import { RecognizedBankTransaction } from '../models/RecognizedBankTransaction';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class RecognizeTranasctionsService {
|
||||
constructor(
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedCashflowTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
|
||||
@Inject(RecognizedBankTransaction.name)
|
||||
private readonly recognizedBankTransactionModel: TenantModelProxy<
|
||||
typeof RecognizedBankTransaction
|
||||
>,
|
||||
|
||||
@Inject(BankRule.name)
|
||||
private readonly bankRuleModel: TenantModelProxy<typeof BankRule>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Marks the uncategorized transaction as recognized from the given bank rule.
|
||||
* @param {BankRule} bankRule -
|
||||
* @param {UncategorizedCashflowTransaction} transaction -
|
||||
* @param {Knex.Transaction} trx -
|
||||
*/
|
||||
private async markBankRuleAsRecognized(
|
||||
bankRule: BankRule,
|
||||
transaction: UncategorizedBankTransaction,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const recognizedTransaction = await this.recognizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.insert({
|
||||
bankRuleId: bankRule.id,
|
||||
uncategorizedTransactionId: transaction.id,
|
||||
assignedCategory: bankRule.assignCategory,
|
||||
assignedAccountId: bankRule.assignAccountId,
|
||||
assignedPayee: bankRule.assignPayee,
|
||||
assignedMemo: bankRule.assignMemo,
|
||||
});
|
||||
|
||||
await this.uncategorizedCashflowTransactionModel()
|
||||
.query(trx)
|
||||
.findById(transaction.id)
|
||||
.patch({
|
||||
recognizedTransactionId: recognizedTransaction.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recognized the uncategorized transactions.
|
||||
* @param {number|Array<number>} ruleId - The target rule id/ids.
|
||||
* @param {RecognizeTransactionsCriteria}
|
||||
* @param {Knex.Transaction} trx -
|
||||
*/
|
||||
public async recognizeTransactions(
|
||||
ruleId?: number | Array<number>,
|
||||
transactionsCriteria?: RecognizeTransactionsCriteria,
|
||||
trx?: Knex.Transaction,
|
||||
) {
|
||||
const uncategorizedTranasctions =
|
||||
await this.uncategorizedCashflowTransactionModel()
|
||||
.query(trx)
|
||||
.onBuild((query) => {
|
||||
query.modify('notRecognized');
|
||||
query.modify('notCategorized');
|
||||
|
||||
// Filter the transactions based on the given criteria.
|
||||
if (transactionsCriteria?.batch) {
|
||||
query.where('batch', transactionsCriteria.batch);
|
||||
}
|
||||
if (transactionsCriteria?.accountId) {
|
||||
query.where('accountId', transactionsCriteria.accountId);
|
||||
}
|
||||
});
|
||||
|
||||
const bankRules = await this.bankRuleModel()
|
||||
.query(trx)
|
||||
.onBuild((q) => {
|
||||
const rulesIds = !isEmpty(ruleId) ? castArray(ruleId) : [];
|
||||
|
||||
if (rulesIds?.length > 0) {
|
||||
q.whereIn('id', rulesIds);
|
||||
}
|
||||
q.withGraphFetched('conditions');
|
||||
});
|
||||
|
||||
const bankRulesByAccountId = transformToMapBy(
|
||||
bankRules,
|
||||
'applyIfAccountId',
|
||||
);
|
||||
// Try to recognize the transaction.
|
||||
const regonizeTransaction = async (
|
||||
transaction: UncategorizedBankTransaction,
|
||||
) => {
|
||||
const allAccountsBankRules = bankRulesByAccountId.get(`null`);
|
||||
const accountBankRules = bankRulesByAccountId.get(
|
||||
`${transaction.accountId}`,
|
||||
);
|
||||
const recognizedBankRule = bankRulesMatchTransaction(
|
||||
transaction,
|
||||
accountBankRules,
|
||||
);
|
||||
if (recognizedBankRule) {
|
||||
await this.markBankRuleAsRecognized(
|
||||
recognizedBankRule,
|
||||
transaction,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
};
|
||||
const result = await PromisePool.withConcurrency(MIGRATION_CONCURRENCY)
|
||||
.for(uncategorizedTranasctions)
|
||||
.process((transaction: UncategorizedBankTransaction, index, pool) => {
|
||||
return regonizeTransaction(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} uncategorizedTransaction
|
||||
*/
|
||||
public async regonizeTransaction(
|
||||
uncategorizedTransaction: UncategorizedBankTransaction,
|
||||
) {}
|
||||
}
|
||||
|
||||
const MIGRATION_CONCURRENCY = 10;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { castArray } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { RevertRecognizedTransactionsCriteria } from '../_types';
|
||||
import { RecognizedBankTransaction } from '../models/RecognizedBankTransaction';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class RevertRecognizedTransactionsService {
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(RecognizedBankTransaction.name)
|
||||
private readonly recognizedBankTransactionModel: TenantModelProxy<
|
||||
typeof RecognizedBankTransaction
|
||||
>,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Revert and unlinks the recognized transactions based on the given bank rule
|
||||
* and transactions criteria..
|
||||
* @param {number|Array<number>} bankRuleId - Bank rule id.
|
||||
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria -
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async revertRecognizedTransactions(
|
||||
ruleId?: number | Array<number>,
|
||||
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
const rulesIds = castArray(ruleId);
|
||||
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Retrieves all the recognized transactions of the banbk rule.
|
||||
const uncategorizedTransactions =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.onBuild((q) => {
|
||||
q.withGraphJoined('recognizedTransaction');
|
||||
q.whereNotNull('recognizedTransaction.id');
|
||||
|
||||
if (rulesIds.length > 0) {
|
||||
q.whereIn('recognizedTransaction.bankRuleId', rulesIds);
|
||||
}
|
||||
if (transactionsCriteria?.accountId) {
|
||||
q.where('accountId', transactionsCriteria.accountId);
|
||||
}
|
||||
if (transactionsCriteria?.batch) {
|
||||
q.where('batch', transactionsCriteria.batch);
|
||||
}
|
||||
});
|
||||
const uncategorizedTransactionIds = uncategorizedTransactions.map(
|
||||
(r) => r.id,
|
||||
);
|
||||
// Unlink the recognized transactions out of un-categorized transactions.
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.whereIn('id', uncategorizedTransactionIds)
|
||||
.patch({
|
||||
recognizedTransactionId: null,
|
||||
});
|
||||
// Delete the recognized bank transactions that associated to bank rule.
|
||||
await this.recognizedBankTransactionModel()
|
||||
.query(trx)
|
||||
.whereIn('uncategorizedTransactionId', uncategorizedTransactionIds)
|
||||
.delete();
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { isEqual, omit } from 'lodash';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { IBankRuleEventCreatedPayload, IBankRuleEventDeletedPayload, IBankRuleEventEditedPayload } from '@/modules/BankRules/types';
|
||||
|
||||
@Injectable()
|
||||
export class TriggerRecognizedTransactionsSubscriber {
|
||||
/**
|
||||
* Triggers the recognize uncategorized transactions job on rule created.
|
||||
* @param {IBankRuleEventCreatedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onCreated)
|
||||
private async recognizedTransactionsOnRuleCreated({
|
||||
bankRule,
|
||||
}: IBankRuleEventCreatedPayload) {
|
||||
const payload = { ruleId: bankRule.id };
|
||||
|
||||
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize uncategorized transactions job on rule edited.
|
||||
* @param {IBankRuleEventEditedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onEdited)
|
||||
private async recognizedTransactionsOnRuleEdited({
|
||||
editRuleDTO,
|
||||
oldBankRule,
|
||||
bankRule,
|
||||
}: IBankRuleEventEditedPayload) {
|
||||
const payload = { ruleId: bankRule.id };
|
||||
|
||||
// Cannot continue if the new and old bank rule values are the same,
|
||||
// after excluding `createdAt` and `updatedAt` dates.
|
||||
if (
|
||||
isEqual(
|
||||
omit(bankRule, ['createdAt', 'updatedAt']),
|
||||
omit(oldBankRule, ['createdAt', 'updatedAt'])
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// await this.agenda.now(
|
||||
// 'rerecognize-uncategorized-transactions-job',
|
||||
// payload
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize uncategorized transactions job on rule deleted.
|
||||
* @param {IBankRuleEventDeletedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.bankRules.onDeleted)
|
||||
private async recognizedTransactionsOnRuleDeleted({
|
||||
ruleId,
|
||||
}: IBankRuleEventDeletedPayload) {
|
||||
const payload = { ruleId };
|
||||
|
||||
// await this.agenda.now(
|
||||
// 'revert-recognized-uncategorized-transactions-job',
|
||||
// payload
|
||||
// );
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the recognize bank transactions once the imported file commit.
|
||||
* @param {IImportFileCommitedEventPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.import.onImportCommitted)
|
||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||
importId,
|
||||
|
||||
// @ts-ignore
|
||||
}: IImportFileCommitedEventPayload) {
|
||||
// const importFile = await Import.query().findOne({ importId });
|
||||
// const batch = importFile.paramsParsed.batch;
|
||||
// const payload = { transactionsCriteria: { batch } };
|
||||
|
||||
// // Cannot continue if the imported resource is not bank account transactions.
|
||||
// if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
|
||||
|
||||
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||
|
||||
// @Service()
|
||||
// export class RegonizeTransactionsJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'recognize-uncategorized-transactions-job',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
|
||||
// const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
|
||||
// try {
|
||||
// await regonizeTransactions.recognizeTransactions(
|
||||
// tenantId,
|
||||
// ruleId,
|
||||
// transactionsCriteria
|
||||
// );
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,45 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||
// import { RevertRecognizedTransactions } from '../commands/RevertRecognizedTransactions.service';
|
||||
|
||||
// @Service()
|
||||
// export class ReregonizeTransactionsJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'rerecognize-uncategorized-transactions-job',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
|
||||
// const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||
// const revertRegonizedTransactions = Container.get(
|
||||
// RevertRecognizedTransactions
|
||||
// );
|
||||
|
||||
// try {
|
||||
// await revertRegonizedTransactions.revertRecognizedTransactions(
|
||||
// tenantId,
|
||||
// ruleId,
|
||||
// transactionsCriteria
|
||||
// );
|
||||
// await regonizeTransactions.recognizeTransactions(
|
||||
// tenantId,
|
||||
// ruleId,
|
||||
// transactionsCriteria
|
||||
// );
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,38 @@
|
||||
// import Container, { Service } from 'typedi';
|
||||
// import { RevertRecognizedTransactions } from '../commands/RevertRecognizedTransactions.service';
|
||||
|
||||
// @Service()
|
||||
// export class RevertRegonizeTransactionsJob {
|
||||
// /**
|
||||
// * Constructor method.
|
||||
// */
|
||||
// constructor(agenda) {
|
||||
// agenda.define(
|
||||
// 'revert-recognized-uncategorized-transactions-job',
|
||||
// { priority: 'high', concurrency: 2 },
|
||||
// this.handler
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Triggers sending invoice mail.
|
||||
// */
|
||||
// private handler = async (job, done: Function) => {
|
||||
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
|
||||
// const revertRegonizedTransactions = Container.get(
|
||||
// RevertRecognizedTransactions
|
||||
// );
|
||||
|
||||
// try {
|
||||
// await revertRegonizedTransactions.revertRecognizedTransactions(
|
||||
// tenantId,
|
||||
// ruleId,
|
||||
// transactionsCriteria
|
||||
// );
|
||||
// done();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// done(error);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,81 @@
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Model } from 'objection';
|
||||
|
||||
export class RecognizedBankTransaction extends BaseModel {
|
||||
public bankRuleId!: number;
|
||||
public uncategorizedTransactionId!: number;
|
||||
public assignedCategory!: string;
|
||||
public assignedAccountId!: number;
|
||||
public assignedPayee!: string;
|
||||
public assignedMemo!: string;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'recognized_bank_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const {
|
||||
UncategorizedBankTransaction,
|
||||
} = require('../../BankingTransactions/models/UncategorizedBankTransaction');
|
||||
const { Account } = require('../../Accounts/models/Account.model');
|
||||
const { BankRule } = require('../../BankRules/models/BankRule');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Recognized bank transaction may belongs to uncategorized transactions.
|
||||
*/
|
||||
uncategorizedTransactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: UncategorizedBankTransaction,
|
||||
join: {
|
||||
from: 'recognized_bank_transactions.uncategorizedTransactionId',
|
||||
to: 'uncategorized_cashflow_transactions.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Recognized bank transaction may belongs to assign account.
|
||||
*/
|
||||
assignAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account,
|
||||
join: {
|
||||
from: 'recognized_bank_transactions.assignedAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Recognized bank transaction may belongs to bank rule.
|
||||
*/
|
||||
bankRule: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: BankRule,
|
||||
join: {
|
||||
from: 'recognized_bank_transactions.bankRuleId',
|
||||
to: 'bank_rules.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { castArray, first, uniq } from 'lodash';
|
||||
import { GetAutofillCategorizeTransctionTransformer } from './GetAutofillCategorizeTransactionTransformer';
|
||||
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetAutofillCategorizeTransactionService {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(UncategorizedBankTransaction.name)
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the autofill values of categorize transactions form.
|
||||
* @param {Array<number> | number} uncategorizeTransactionsId - Uncategorized transactions ids.
|
||||
*/
|
||||
public async getAutofillCategorizeTransaction(
|
||||
uncategorizeTransactionsId: Array<number> | number,
|
||||
) {
|
||||
const uncategorizeTransactionsIds = uniq(
|
||||
castArray(uncategorizeTransactionsId),
|
||||
);
|
||||
const uncategorizedTransactions =
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query()
|
||||
.whereIn('id', uncategorizeTransactionsIds)
|
||||
.withGraphFetched('recognizedTransaction.assignAccount')
|
||||
.withGraphFetched('recognizedTransaction.bankRule')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(
|
||||
{},
|
||||
new GetAutofillCategorizeTransctionTransformer(),
|
||||
{
|
||||
uncategorizedTransactions,
|
||||
firstUncategorizedTransaction: first(uncategorizedTransactions),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { sumBy } from 'lodash';
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
|
||||
export class GetAutofillCategorizeTransctionTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes to the object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'amount',
|
||||
'formattedAmount',
|
||||
'isRecognized',
|
||||
'date',
|
||||
'formattedDate',
|
||||
'creditAccountId',
|
||||
'debitAccountId',
|
||||
'referenceNo',
|
||||
'transactionType',
|
||||
'recognizedByRuleId',
|
||||
'recognizedByRuleName',
|
||||
'isWithdrawalTransaction',
|
||||
'isDepositTransaction',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the transaction is recognized.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isRecognized() {
|
||||
return !!this.options.firstUncategorizedTransaction?.recognizedTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the total amount of uncategorized transactions.
|
||||
* @returns {number}
|
||||
*/
|
||||
public amount() {
|
||||
return sumBy(this.options.uncategorizedTransactions, 'amount');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total amount of uncategorized transactions.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedAmount() {
|
||||
return this.formatNumber(this.amount(), {
|
||||
currencyCode: 'USD',
|
||||
money: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the transaction is deposit.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isDepositTransaction() {
|
||||
const amount = this.amount();
|
||||
|
||||
return amount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the transaction is withdrawal.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
public isWithdrawalTransaction() {
|
||||
const amount = this.amount();
|
||||
|
||||
return amount < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string}
|
||||
*/
|
||||
public date() {
|
||||
return this.options.firstUncategorizedTransaction?.date || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted date of uncategorized transaction.
|
||||
* @returns {string}
|
||||
*/
|
||||
public formattedDate() {
|
||||
return this.formatDate(this.date());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string}
|
||||
*/
|
||||
public referenceNo() {
|
||||
return this.options.firstUncategorizedTransaction?.referenceNo || null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
public creditAccountId() {
|
||||
return (
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.assignedAccountId || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
public debitAccountId() {
|
||||
return this.options.firstUncategorizedTransaction?.accountId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the assigned category of recognized transaction, if is not recognized
|
||||
* returns the default transaction type depends on the transaction normal.
|
||||
* @returns {string}
|
||||
*/
|
||||
public transactionType() {
|
||||
const assignedCategory =
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.assignedCategory;
|
||||
|
||||
return (
|
||||
assignedCategory ||
|
||||
(this.isDepositTransaction() ? 'other_income' : 'other_expense')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public payee() {
|
||||
return (
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.assignedPayee || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
public memo() {
|
||||
return (
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.assignedMemo || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the rule id the transaction recongized by.
|
||||
* @returns {string}
|
||||
*/
|
||||
public recognizedByRuleId() {
|
||||
return (
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.bankRuleId || null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the rule name the transaction recongized by.
|
||||
* @returns {string}
|
||||
*/
|
||||
public recognizedByRuleName() {
|
||||
return (
|
||||
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||
?.bankRule?.name || null
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user