mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: Categorize the bank synced transactions
This commit is contained in:
@@ -13,9 +13,9 @@ export default class CashflowController {
|
|||||||
router() {
|
router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(Container.get(CommandCashflowTransaction).router());
|
||||||
router.use(Container.get(GetCashflowTransaction).router());
|
router.use(Container.get(GetCashflowTransaction).router());
|
||||||
router.use(Container.get(GetCashflowAccounts).router());
|
router.use(Container.get(GetCashflowAccounts).router());
|
||||||
router.use(Container.get(CommandCashflowTransaction).router());
|
|
||||||
router.use(Container.get(DeleteCashflowTransaction).router());
|
router.use(Container.get(DeleteCashflowTransaction).router());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { Router, Request, Response, NextFunction } from 'express';
|
|||||||
import { param } from 'express-validator';
|
import { param } from 'express-validator';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
|
import { DeleteCashflowTransaction } from '../../../services/Cashflow/DeleteCashflowTransactionService';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class DeleteCashflowTransaction extends BaseController {
|
export default class DeleteCashflowTransactionController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
deleteCashflowService: DeleteCashflowTransactionService;
|
private deleteCashflowService: DeleteCashflowTransaction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller router.
|
* Controller router.
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { AbilitySubject, CashflowAction } from '@/interfaces';
|
|||||||
@Service()
|
@Service()
|
||||||
export default class GetCashflowAccounts extends BaseController {
|
export default class GetCashflowAccounts extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
getCashflowAccountsService: GetCashflowAccountsService;
|
private getCashflowAccountsService: GetCashflowAccountsService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
private getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller router.
|
* Controller router.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { AbilitySubject, CashflowAction } from '@/interfaces';
|
|||||||
@Service()
|
@Service()
|
||||||
export default class GetCashflowAccounts extends BaseController {
|
export default class GetCashflowAccounts extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
private getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controller router.
|
* Controller router.
|
||||||
|
|||||||
@@ -6,18 +6,27 @@ import { ServiceError } from '@/exceptions';
|
|||||||
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||||
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NewCashflowTransactionController extends BaseController {
|
export default class NewCashflowTransactionController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private newCashflowTranscationService: NewCashflowTransactionService;
|
private newCashflowTranscationService: NewCashflowTransactionService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private cashflowApplication: CashflowApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
public router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/transactions/uncategorized',
|
||||||
|
this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/transactions',
|
'/transactions',
|
||||||
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
|
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
|
||||||
@@ -26,13 +35,61 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
this.asyncMiddleware(this.newCashflowTransaction),
|
this.asyncMiddleware(this.newCashflowTransaction),
|
||||||
this.catchServiceErrors
|
this.catchServiceErrors
|
||||||
);
|
);
|
||||||
|
router.post(
|
||||||
|
'/transactions/:id/uncategorize',
|
||||||
|
this.revertCategorizedCashflowTransaction,
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/transactions/:id/categorize',
|
||||||
|
this.categorizeCashflowTransactionValidationSchema,
|
||||||
|
this.validationResult,
|
||||||
|
this.categorizeCashflowTransaction,
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/transaction/:id/categorize/expense',
|
||||||
|
this.categorizeAsExpenseValidationSchema,
|
||||||
|
this.validationResult,
|
||||||
|
this.categorizesCashflowTransactionAsExpense,
|
||||||
|
this.catchServiceErrors
|
||||||
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize as expense validation schema.
|
||||||
|
*/
|
||||||
|
public get categorizeAsExpenseValidationSchema() {
|
||||||
|
return [
|
||||||
|
check('expense_account_id').exists(),
|
||||||
|
check('date').isISO8601().exists(),
|
||||||
|
check('reference_no').optional(),
|
||||||
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize cashflow tranasction validation schema.
|
||||||
|
*/
|
||||||
|
public get categorizeCashflowTransactionValidationSchema() {
|
||||||
|
return [
|
||||||
|
check('date').exists().isISO8601().toDate(),
|
||||||
|
|
||||||
|
check('to_account_id').exists().isInt().toInt(),
|
||||||
|
check('from_account_id').exists().isInt().toInt(),
|
||||||
|
|
||||||
|
check('transaction_type').exists(),
|
||||||
|
check('reference_no').optional(),
|
||||||
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
check('description').optional(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New cashflow transaction validation schema.
|
* New cashflow transaction validation schema.
|
||||||
*/
|
*/
|
||||||
get newTransactionValidationSchema() {
|
public get newTransactionValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('date').exists().isISO8601().toDate(),
|
check('date').exists().isISO8601().toDate(),
|
||||||
check('reference_no').optional({ nullable: true }).trim().escape(),
|
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||||
@@ -48,12 +105,10 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
check('credit_account_id').exists().isInt().toInt(),
|
check('credit_account_id').exists().isInt().toInt(),
|
||||||
|
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('publish').default(false).isBoolean().toBoolean(),
|
check('publish').default(false).isBoolean().toBoolean(),
|
||||||
];
|
];
|
||||||
}
|
}√
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new cashflow transaction.
|
* Creates a new cashflow transaction.
|
||||||
@@ -76,7 +131,6 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
ownerContributionDTO,
|
ownerContributionDTO,
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: cashflowTransaction.id,
|
id: cashflowTransaction.id,
|
||||||
message: 'New cashflow transaction has been created successfully.',
|
message: 'New cashflow transaction has been created successfully.',
|
||||||
@@ -86,11 +140,118 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert the categorized cashflow transaction.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private revertCategorizedCashflowTransaction = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { id: cashflowTransactionId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data= await this.cashflowApplication.uncategorizeTransaction(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ data });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize the cashflow transaction.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private categorizeCashflowTransaction = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { id: cashflowTransactionId } = req.params;
|
||||||
|
const cashflowTransaction = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.cashflowApplication.categorizeTransaction(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId,
|
||||||
|
cashflowTransaction
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The cashflow transaction has been created successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize the transaction as expense transaction.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
private categorizesCashflowTransactionAsExpense = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { id: cashflowTransactionId } = req.params;
|
||||||
|
const cashflowTransaction = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.cashflowApplication.categorizeAsExpense(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId,
|
||||||
|
cashflowTransaction
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'The cashflow transaction has been created successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the uncategorized cashflow transactions.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
public getUncategorizedCashflowTransactions = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
const { tenantId } = req;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.cashflowApplication.getUncategorizedTransactions(
|
||||||
|
tenantId
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the service errors.
|
* Handle the service errors.
|
||||||
* @param error
|
* @param error
|
||||||
* @param req
|
* @param {Request} req
|
||||||
* @param res
|
* @param {res
|
||||||
* @param next
|
* @param next
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.createTable(
|
||||||
|
'uncategorized_cashflow_transactions',
|
||||||
|
(table) => {
|
||||||
|
table.increments('id');
|
||||||
|
table.date('date').index();
|
||||||
|
table.decimal('amount');
|
||||||
|
table.string('reference_no').index();
|
||||||
|
table
|
||||||
|
.integer('account_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts');
|
||||||
|
table.string('description');
|
||||||
|
table.string('categorize_ref_type');
|
||||||
|
table.integer('categorize_ref_id').unsigned();
|
||||||
|
table.boolean('categorized').defaultTo(false);
|
||||||
|
table.timestamps();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.dropTableIfExists('uncategorized_cashflow_transactions');
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('expenses_transactions', (table) => {
|
||||||
|
table
|
||||||
|
.integer('categorized_transaction_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('uncategorized_cashflow_transactions');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -233,3 +233,27 @@ export interface ICashflowTransactionSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
||||||
|
|
||||||
|
export interface ICategorizeCashflowTransactioDTO {
|
||||||
|
fromAccountId: number;
|
||||||
|
toAccountId: number;
|
||||||
|
referenceNo: string;
|
||||||
|
transactionNumber: string;
|
||||||
|
transactionType: string;
|
||||||
|
exchangeRate: number;
|
||||||
|
description: string;
|
||||||
|
branchId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUncategorizedCashflowTransaction {
|
||||||
|
id?: number;
|
||||||
|
amount: number;
|
||||||
|
date: Date;
|
||||||
|
currencyCode: string;
|
||||||
|
accountId: number;
|
||||||
|
description: string;
|
||||||
|
referenceNo: string;
|
||||||
|
categorizeRefType: string;
|
||||||
|
categorizeRefId: number;
|
||||||
|
categorized: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { IAccount } from './Account';
|
import { IAccount } from './Account';
|
||||||
|
import { IUncategorizedCashflowTransaction } from './CashFlow';
|
||||||
|
|
||||||
export interface ICashflowAccountTransactionsFilter {
|
export interface ICashflowAccountTransactionsFilter {
|
||||||
page: number;
|
page: number;
|
||||||
@@ -124,8 +125,34 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
cashflowTransactionId: number;
|
||||||
|
cashflowTransaction: ICashflowTransaction;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
export interface ICashflowTransactionUncategorizedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
|
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
export enum CashflowAction {
|
export enum CashflowAction {
|
||||||
Create = 'Create',
|
Create = 'Create',
|
||||||
Delete = 'Delete',
|
Delete = 'Delete',
|
||||||
View = 'View',
|
View = 'View',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CategorizeTransactionAsExpenseDTO {
|
||||||
|
expenseAccountId: number;
|
||||||
|
exchangeRate: number;
|
||||||
|
referenceNo: string;
|
||||||
|
description: string;
|
||||||
|
branchId?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from '@/services/Banki
|
|||||||
import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber';
|
import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber';
|
||||||
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
|
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
|
||||||
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
|
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
|
||||||
|
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -212,6 +213,9 @@ export const susbcribers = () => {
|
|||||||
SyncItemTaxRateOnEditTaxSubscriber,
|
SyncItemTaxRateOnEditTaxSubscriber,
|
||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
PlaidUpdateTransactionsOnItemCreatedSubscriber
|
PlaidUpdateTransactionsOnItemCreatedSubscriber,
|
||||||
|
|
||||||
|
// Cashflow
|
||||||
|
DeleteCashflowTransactionOnUncategorize,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import TaxRate from 'models/TaxRate';
|
|||||||
import TaxRateTransaction from 'models/TaxRateTransaction';
|
import TaxRateTransaction from 'models/TaxRateTransaction';
|
||||||
import Attachment from 'models/Attachment';
|
import Attachment from 'models/Attachment';
|
||||||
import PlaidItem from 'models/PlaidItem';
|
import PlaidItem from 'models/PlaidItem';
|
||||||
|
import UncategorizedCashflowTransaction from 'models/UncategorizedCashflowTransaction';
|
||||||
|
|
||||||
export default (knex) => {
|
export default (knex) => {
|
||||||
const models = {
|
const models = {
|
||||||
@@ -126,7 +127,8 @@ export default (knex) => {
|
|||||||
TaxRate,
|
TaxRate,
|
||||||
TaxRateTransaction,
|
TaxRateTransaction,
|
||||||
Attachment,
|
Attachment,
|
||||||
PlaidItem
|
PlaidItem,
|
||||||
|
UncategorizedCashflowTransaction
|
||||||
};
|
};
|
||||||
return mapValues(models, (model) => model.bindKnex(knex));
|
return mapValues(models, (model) => model.bindKnex(knex));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
transactionType: string;
|
transactionType: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
exchangeRate: number;
|
exchangeRate: number;
|
||||||
|
uncategorize: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export default class Expense extends mixin(TenantModel, [
|
|||||||
const ExpenseCategory = require('models/ExpenseCategory');
|
const ExpenseCategory = require('models/ExpenseCategory');
|
||||||
const Media = require('models/Media');
|
const Media = require('models/Media');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
|
const UncategorizedCashflowTransaction = require('models/UncategorizedCashflowTransaction');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentAccount: {
|
paymentAccount: {
|
||||||
@@ -215,6 +216,10 @@ export default class Expense extends mixin(TenantModel, [
|
|||||||
to: 'branches.id',
|
to: 'branches.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
media: {
|
media: {
|
||||||
relation: Model.ManyToManyRelation,
|
relation: Model.ManyToManyRelation,
|
||||||
modelClass: Media.default,
|
modelClass: Media.default,
|
||||||
@@ -230,6 +235,18 @@ export default class Expense extends mixin(TenantModel, [
|
|||||||
query.where('model_name', 'Expense');
|
query.where('model_name', 'Expense');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the related uncategorized cashflow transaction.
|
||||||
|
*/
|
||||||
|
categorized: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: UncategorizedCashflowTransaction.default,
|
||||||
|
join: {
|
||||||
|
from: 'expenses_transactions.categorizedTransactionId',
|
||||||
|
to: 'uncategorized_cashflow_transactions.id',
|
||||||
|
},
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import TenantModel from 'models/TenantModel';
|
||||||
|
import { Model } from 'objection';
|
||||||
|
|
||||||
|
export default class UncategorizedCashflowTransaction extends TenantModel {
|
||||||
|
amount: number;
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'uncategorized_cashflow_transactions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the withdrawal amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public withdrawal() {
|
||||||
|
return this.amount > 0 ? Math.abs(this.amount) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the deposit amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public deposit() {
|
||||||
|
return this.amount < 0 ? Math.abs(this.amount) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['withdrawal', 'deposit'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const Account = require('models/Account');
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Transaction may has associated to account.
|
||||||
|
*/
|
||||||
|
account: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Account.default,
|
||||||
|
join: {
|
||||||
|
from: 'uncategorized_cashflow_transactions.accountId',
|
||||||
|
to: 'accounts.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
transformPlaidTrxsToCashflowCreate,
|
transformPlaidTrxsToCashflowCreate,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
||||||
import DeleteCashflowTransactionService from '@/services/Cashflow/DeleteCashflowTransactionService';
|
import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
const CONCURRENCY_ASYNC = 10;
|
const CONCURRENCY_ASYNC = 10;
|
||||||
@@ -26,7 +26,7 @@ export class PlaidSyncDb {
|
|||||||
private createCashflowTransactionService: NewCashflowTransactionService;
|
private createCashflowTransactionService: NewCashflowTransactionService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private deleteCashflowTransactionService: DeleteCashflowTransactionService;
|
private deleteCashflowTransactionService: DeleteCashflowTransaction;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs the plaid accounts to the system accounts.
|
* Syncs the plaid accounts to the system accounts.
|
||||||
|
|||||||
104
packages/server/src/services/Cashflow/CashflowApplication.ts
Normal file
104
packages/server/src/services/Cashflow/CashflowApplication.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService';
|
||||||
|
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
|
||||||
|
import { CategorizeCashflowTransaction } from './CategorizeCashflowTransaction';
|
||||||
|
import {
|
||||||
|
CategorizeTransactionAsExpenseDTO,
|
||||||
|
ICategorizeCashflowTransactioDTO,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { CategorizeTransactionAsExpense } from './CategorizeTransactionAsExpense';
|
||||||
|
import { GetUncategorizedTransactions } from './GetUncategorizedTransactions';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class CashflowApplication {
|
||||||
|
@Inject()
|
||||||
|
private deleteTransactionService: DeleteCashflowTransaction;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uncategorizeTransactionService: UncategorizeCashflowTransaction;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private categorizeTransactionService: CategorizeCashflowTransaction;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private categorizeAsExpenseService: CategorizeTransactionAsExpense;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getUncategorizedTransactionsService: GetUncategorizedTransactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given cashflow transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public deleteTransaction(tenantId: number, cashflowTransactionId: number) {
|
||||||
|
return this.deleteTransactionService.deleteCashflowTransaction(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncategorize the given cashflow transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public uncategorizeTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
cashflowTransactionId: number
|
||||||
|
) {
|
||||||
|
return this.uncategorizeTransactionService.uncategorize(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize the given cashflow transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public categorizeTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
cashflowTransactionId: number,
|
||||||
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
|
) {
|
||||||
|
return this.categorizeTransactionService.categorize(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId,
|
||||||
|
categorizeDTO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorizes the given cashflow transaction as expense transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
* @param {CategorizeTransactionAsExpenseDTO} transactionDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public categorizeAsExpense(
|
||||||
|
tenantId: number,
|
||||||
|
cashflowTransactionId: number,
|
||||||
|
transactionDTO: CategorizeTransactionAsExpenseDTO
|
||||||
|
) {
|
||||||
|
return this.categorizeAsExpenseService.categorize(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionId,
|
||||||
|
transactionDTO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the uncategorized cashflow transactions.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public getUncategorizedTransactions(tenantId: number) {
|
||||||
|
return this.getUncategorizedTransactionsService.getTransactions(tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import * as R from 'ramda';
|
|
||||||
import {
|
import {
|
||||||
ILedgerEntry,
|
ILedgerEntry,
|
||||||
ICashflowTransaction,
|
ICashflowTransaction,
|
||||||
AccountNormal,
|
AccountNormal,
|
||||||
ICashflowTransactionLine,
|
|
||||||
} from '../../interfaces';
|
} from '../../interfaces';
|
||||||
import {
|
import {
|
||||||
transformCashflowTransactionType,
|
transformCashflowTransactionType,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import {
|
||||||
|
ICashflowTransactionCategorizedPayload,
|
||||||
|
ICashflowTransactionUncategorizingPayload,
|
||||||
|
ICategorizeCashflowTransactioDTO,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { transformCategorizeTransToCashflow } from './utils';
|
||||||
|
import { CommandCashflowValidator } from './CommandCasflowValidator';
|
||||||
|
import NewCashflowTransactionService from './NewCashflowTransactionService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class CategorizeCashflowTransaction {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private commandValidators: CommandCashflowValidator;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private createCashflow: NewCashflowTransactionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize the given cashflow transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO
|
||||||
|
*/
|
||||||
|
public async categorize(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
|
) {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// Retrieves the uncategorized transaction or throw an error.
|
||||||
|
const transaction = await UncategorizedCashflowTransaction.query()
|
||||||
|
.findById(uncategorizedTransactionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Validates the transaction shouldn't be categorized before.
|
||||||
|
this.commandValidators.validateTransactionShouldNotCategorized(transaction);
|
||||||
|
|
||||||
|
// Edits the cashflow transaction under UOW env.
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onTransactionCategorizing` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionCategorizing,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionUncategorizingPayload
|
||||||
|
);
|
||||||
|
// Transformes the categorize DTO to the cashflow transaction.
|
||||||
|
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
|
||||||
|
transaction,
|
||||||
|
categorizeDTO
|
||||||
|
);
|
||||||
|
// Creates a new cashflow transaction.
|
||||||
|
const cashflowTransaction =
|
||||||
|
await this.createCashflow.newCashflowTransaction(
|
||||||
|
tenantId,
|
||||||
|
cashflowTransactionDTO
|
||||||
|
);
|
||||||
|
// Updates the uncategorized transaction as categorized.
|
||||||
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
|
.findById(uncategorizedTransactionId)
|
||||||
|
.patch({
|
||||||
|
categorized: true,
|
||||||
|
categorizeRefType: 'CashflowTransaction',
|
||||||
|
categorizeRefId: cashflowTransaction.id,
|
||||||
|
});
|
||||||
|
// Triggers `onCashflowTransactionCategorized` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionCategorized,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
// cashflowTransaction,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionCategorizedPayload
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
CategorizeTransactionAsExpenseDTO,
|
||||||
|
ICashflowTransactionCategorizedPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { CreateExpense } from '../Expenses/CRUD/CreateExpense';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class CategorizeTransactionAsExpense {
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private createExpenseService: CreateExpense;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize the transaction as expense transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
* @param {CategorizeTransactionAsExpenseDTO} transactionDTO
|
||||||
|
*/
|
||||||
|
public async categorize(
|
||||||
|
tenantId: number,
|
||||||
|
cashflowTransactionId: number,
|
||||||
|
transactionDTO: CategorizeTransactionAsExpenseDTO
|
||||||
|
) {
|
||||||
|
const { CashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const transaction = await CashflowTransaction.query()
|
||||||
|
.findById(cashflowTransactionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onTransactionUncategorizing` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionCategorizingAsExpense,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionCategorizedPayload
|
||||||
|
);
|
||||||
|
// Creates a new expense transaction.
|
||||||
|
const expenseTransaction = await this.createExpenseService.newExpense(
|
||||||
|
tenantId,
|
||||||
|
{
|
||||||
|
|
||||||
|
},
|
||||||
|
1
|
||||||
|
);
|
||||||
|
// Updates the item on the storage and fetches the updated once.
|
||||||
|
const cashflowTransaction = await CashflowTransaction.query(
|
||||||
|
trx
|
||||||
|
).patchAndFetchById(cashflowTransactionId, {
|
||||||
|
categorizeRefType: 'Expense',
|
||||||
|
categorizeRefId: expenseTransaction.id,
|
||||||
|
uncategorized: true,
|
||||||
|
});
|
||||||
|
// Triggers `onTransactionUncategorized` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionCategorizedAsExpense,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
cashflowTransaction,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionUncategorizedPayload
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { IAccount } from '@/interfaces';
|
|||||||
import { getCashflowTransactionType } from './utils';
|
import { getCashflowTransactionType } from './utils';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants';
|
import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants';
|
||||||
|
import CashflowTransaction from '@/models/CashflowTransaction';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandCashflowValidator {
|
export class CommandCashflowValidator {
|
||||||
@@ -46,4 +47,28 @@ export class CommandCashflowValidator {
|
|||||||
}
|
}
|
||||||
return transformedType;
|
return transformedType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given transaction should be categorized.
|
||||||
|
* @param {CashflowTransaction} cashflowTransaction
|
||||||
|
*/
|
||||||
|
public validateTransactionShouldCategorized(
|
||||||
|
cashflowTransaction: CashflowTransaction
|
||||||
|
) {
|
||||||
|
if (!cashflowTransaction.uncategorize) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given transcation shouldn't be categorized.
|
||||||
|
* @param {CashflowTransaction} cashflowTransaction
|
||||||
|
*/
|
||||||
|
public validateTransactionShouldNotCategorized(
|
||||||
|
cashflowTransaction: CashflowTransaction
|
||||||
|
) {
|
||||||
|
if (cashflowTransaction.uncategorize) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ import UnitOfWork from '@/services/UnitOfWork';
|
|||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CommandCashflowTransactionService {
|
export class DeleteCashflowTransaction {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the cashflow transaction with associated journal entries.
|
* Deletes the cashflow transaction with associated journal entries.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { CashflowTransactionTransformer } from './CashflowTransactionTransformer
|
|||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
import { ICashflowTransaction } from '@/interfaces';
|
import { ICashflowTransaction } from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import I18nService from '@/services/I18n/I18nService';
|
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -12,9 +11,6 @@ export default class GetCashflowTransactionsService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private i18nService: I18nService;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private transfromer: TransformerInjectable;
|
private transfromer: TransformerInjectable;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { UncategorizedTransactionTransformer } from './UncategorizedTransactionTransformer';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetUncategorizedTransactions {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the uncategorized cashflow transactions.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
public async getTransactions(tenantId: number) {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const { results, pagination } =
|
||||||
|
await UncategorizedCashflowTransaction.query()
|
||||||
|
.where('categorized', false)
|
||||||
|
.withGraphFetched('account')
|
||||||
|
.pagination(0, 10);
|
||||||
|
|
||||||
|
const data = await this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
results,
|
||||||
|
new UncategorizedTransactionTransformer()
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { isEmpty, pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import {
|
import {
|
||||||
ICashflowNewCommandDTO,
|
ICashflowNewCommandDTO,
|
||||||
ICashflowTransaction,
|
ICashflowTransaction,
|
||||||
ICashflowTransactionLine,
|
|
||||||
ICommandCashflowCreatedPayload,
|
ICommandCashflowCreatedPayload,
|
||||||
ICommandCashflowCreatingPayload,
|
ICommandCashflowCreatingPayload,
|
||||||
ICashflowTransactionInput,
|
ICashflowTransactionInput,
|
||||||
@@ -126,7 +125,7 @@ export default class NewCashflowTransactionService {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
newTransactionDTO: ICashflowNewCommandDTO,
|
newTransactionDTO: ICashflowNewCommandDTO,
|
||||||
userId?: number
|
userId?: number
|
||||||
): Promise<{ cashflowTransaction: ICashflowTransaction }> => {
|
): Promise<ICashflowTransaction> => {
|
||||||
const { CashflowTransaction, Account } = this.tenancy.models(tenantId);
|
const { CashflowTransaction, Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Retrieves the cashflow account or throw not found error.
|
// Retrieves the cashflow account or throw not found error.
|
||||||
@@ -175,7 +174,7 @@ export default class NewCashflowTransactionService {
|
|||||||
trx,
|
trx,
|
||||||
} as ICommandCashflowCreatedPayload
|
} as ICommandCashflowCreatedPayload
|
||||||
);
|
);
|
||||||
return { cashflowTransaction };
|
return cashflowTransaction;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import {
|
||||||
|
ICashflowTransactionUncategorizedPayload,
|
||||||
|
ICashflowTransactionUncategorizingPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UncategorizeCashflowTransaction {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uncategorizes the given cashflow transaction.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} cashflowTransactionId
|
||||||
|
*/
|
||||||
|
public async uncategorize(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number
|
||||||
|
) {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const oldUncategorizedTransaction =
|
||||||
|
await UncategorizedCashflowTransaction.query()
|
||||||
|
.findById(uncategorizedTransactionId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Updates the transaction under UOW.
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onTransactionUncategorizing` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionUncategorizing,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionUncategorizingPayload
|
||||||
|
);
|
||||||
|
// Removes the ref relation with the related transaction.
|
||||||
|
const uncategorizedTransaction =
|
||||||
|
await UncategorizedCashflowTransaction.query(trx).updateAndFetchById(
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
{
|
||||||
|
categorized: false,
|
||||||
|
categorizeRefId: null,
|
||||||
|
categorizeRefType: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Triggers `onTransactionUncategorized` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.cashflow.onTransactionUncategorized,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
oldUncategorizedTransaction,
|
||||||
|
trx,
|
||||||
|
} as ICashflowTransactionUncategorizedPayload
|
||||||
|
);
|
||||||
|
return uncategorizedTransaction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UncategorizeTransactionByRef {
|
||||||
|
private uncategorizeTransactionService: UncategorizeCashflowTransaction;
|
||||||
|
|
||||||
|
public uncategorize(tenantId: number, refId: number, refType: string) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { formatNumber } from '@/utils';
|
||||||
|
|
||||||
|
export class UncategorizedTransactionTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale invoice object.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['formattetDepositAmount', 'formattedWithdrawalAmount'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted deposit amount.
|
||||||
|
* @param transaction
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattetDepositAmount(transaction) {
|
||||||
|
return formatNumber(transaction.deposit, {
|
||||||
|
currencyCode: transaction.currencyCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted withdrawal amount.
|
||||||
|
* @param transaction
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedWithdrawalAmount(transaction) {
|
||||||
|
return formatNumber(transaction.withdrawal, {
|
||||||
|
currencyCode: transaction.currencyCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ export const ERRORS = {
|
|||||||
CREDIT_ACCOUNTS_IDS_NOT_FOUND: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND',
|
CREDIT_ACCOUNTS_IDS_NOT_FOUND: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND',
|
||||||
CREDIT_ACCOUNTS_HAS_INVALID_TYPE: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE',
|
CREDIT_ACCOUNTS_HAS_INVALID_TYPE: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE',
|
||||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||||
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions'
|
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
|
||||||
|
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
||||||
|
TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED'
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CASHFLOW_DIRECTION {
|
export enum CASHFLOW_DIRECTION {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
||||||
|
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DeleteCashflowTransactionOnUncategorize {
|
||||||
|
@Inject()
|
||||||
|
private deleteCashflowTransactionService: DeleteCashflowTransaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches events with handlers.
|
||||||
|
*/
|
||||||
|
public attach = (bus) => {
|
||||||
|
bus.subscribe(
|
||||||
|
events.cashflow.onTransactionUncategorized,
|
||||||
|
this.deleteCashflowTransactionOnUncategorize.bind(this)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the cashflow transaction on uncategorize transaction.
|
||||||
|
* @param {ICashflowTransactionUncategorizedPayload} payload
|
||||||
|
*/
|
||||||
|
public async deleteCashflowTransactionOnUncategorize({
|
||||||
|
tenantId,
|
||||||
|
oldUncategorizedTransaction,
|
||||||
|
trx,
|
||||||
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
|
// Deletes the cashflow transaction.
|
||||||
|
if (
|
||||||
|
oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
|
||||||
|
) {
|
||||||
|
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||||
|
tenantId,
|
||||||
|
oldUncategorizedTransaction.categorizeRefId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import { upperFirst, camelCase } from 'lodash';
|
import { upperFirst, camelCase, omit } from 'lodash';
|
||||||
import {
|
import {
|
||||||
CASHFLOW_TRANSACTION_TYPE,
|
CASHFLOW_TRANSACTION_TYPE,
|
||||||
CASHFLOW_TRANSACTION_TYPE_META,
|
CASHFLOW_TRANSACTION_TYPE_META,
|
||||||
ICashflowTransactionTypeMeta,
|
ICashflowTransactionTypeMeta,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import {
|
||||||
|
ICashflowNewCommandDTO,
|
||||||
|
ICashflowTransaction,
|
||||||
|
ICategorizeCashflowTransactioDTO,
|
||||||
|
IUncategorizedCashflowTransaction,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the given transaction type to transformed to appropriate format.
|
* Ensures the given transaction type to transformed to appropriate format.
|
||||||
@@ -32,3 +38,29 @@ export function getCashflowTransactionType(
|
|||||||
export const getCashflowAccountTransactionsTypes = () => {
|
export const getCashflowAccountTransactionsTypes = () => {
|
||||||
return Object.values(CASHFLOW_TRANSACTION_TYPE_META).map((meta) => meta.type);
|
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 = (
|
||||||
|
uncategorizeModel: IUncategorizedCashflowTransaction,
|
||||||
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
|
): ICashflowNewCommandDTO => {
|
||||||
|
return {
|
||||||
|
date: uncategorizeModel.date,
|
||||||
|
referenceNo: categorizeDTO.referenceNo || uncategorizeModel.referenceNo,
|
||||||
|
description: categorizeDTO.description || uncategorizeModel.description,
|
||||||
|
cashflowAccountId: uncategorizeModel.accountId,
|
||||||
|
creditAccountId: categorizeDTO.fromAccountId || categorizeDTO.toAccountId,
|
||||||
|
exchangeRate: categorizeDTO.exchangeRate || 1,
|
||||||
|
currencyCode: uncategorizeModel.currencyCode,
|
||||||
|
amount: uncategorizeModel.amount,
|
||||||
|
transactionNumber: categorizeDTO.transactionNumber,
|
||||||
|
transactionType: categorizeDTO.transactionType,
|
||||||
|
publish: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -392,6 +392,15 @@ export default {
|
|||||||
|
|
||||||
onTransactionDeleting: 'onCashflowTransactionDeleting',
|
onTransactionDeleting: 'onCashflowTransactionDeleting',
|
||||||
onTransactionDeleted: 'onCashflowTransactionDeleted',
|
onTransactionDeleted: 'onCashflowTransactionDeleted',
|
||||||
|
|
||||||
|
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||||
|
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||||
|
|
||||||
|
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||||
|
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||||
|
|
||||||
|
onTransactionCategorizingAsExpense: 'onTransactionCategorizingAsExpense',
|
||||||
|
onTransactionCategorizedAsExpense: 'onTransactionCategorizedAsExpense',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user