mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
Merge pull request #533 from bigcapitalhq/bulk-categorize-bank-transactions
feat: Bulk categorize bank transactions
This commit is contained in:
@@ -1,12 +1,8 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { body, param } from 'express-validator';
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||||
import { body, param } from 'express-validator';
|
|
||||||
import {
|
|
||||||
GetMatchedTransactionsFilter,
|
|
||||||
IMatchTransactionsDTO,
|
|
||||||
} from '@/services/Banking/Matching/types';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankTransactionsMatchingController extends BaseController {
|
export class BankTransactionsMatchingController extends BaseController {
|
||||||
@@ -20,9 +16,17 @@ export class BankTransactionsMatchingController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:transactionId',
|
'/unmatch/:transactionId',
|
||||||
|
[param('transactionId').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.unmatchMatchedBankTransaction.bind(this)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/match',
|
||||||
[
|
[
|
||||||
param('transactionId').exists(),
|
body('uncategorizedTransactions').exists().isArray({ min: 1 }),
|
||||||
|
body('uncategorizedTransactions.*').isNumeric().toInt(),
|
||||||
|
|
||||||
body('matchedTransactions').isArray({ min: 1 }),
|
body('matchedTransactions').isArray({ min: 1 }),
|
||||||
body('matchedTransactions.*.reference_type').exists(),
|
body('matchedTransactions.*.reference_type').exists(),
|
||||||
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
|
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
|
||||||
@@ -30,12 +34,6 @@ export class BankTransactionsMatchingController extends BaseController {
|
|||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.matchBankTransaction.bind(this)
|
this.matchBankTransaction.bind(this)
|
||||||
);
|
);
|
||||||
router.post(
|
|
||||||
'/unmatch/:transactionId',
|
|
||||||
[param('transactionId').exists()],
|
|
||||||
this.validationResult,
|
|
||||||
this.unmatchMatchedBankTransaction.bind(this)
|
|
||||||
);
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,21 +48,21 @@ export class BankTransactionsMatchingController extends BaseController {
|
|||||||
req: Request<{ transactionId: number }>,
|
req: Request<{ transactionId: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
): Promise<Response | null> {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { transactionId } = req.params;
|
const bodyData = this.matchedBodyData(req);
|
||||||
const matchTransactionDTO = this.matchedBodyData(
|
|
||||||
req
|
const uncategorizedTransactions = bodyData?.uncategorizedTransactions;
|
||||||
) as IMatchTransactionsDTO;
|
const matchedTransactions = bodyData?.matchedTransactions;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.bankTransactionsMatchingApp.matchTransaction(
|
await this.bankTransactionsMatchingApp.matchTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
transactionId,
|
uncategorizedTransactions,
|
||||||
matchTransactionDTO
|
matchedTransactions
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: transactionId,
|
ids: uncategorizedTransactions,
|
||||||
message: 'The bank transaction has been matched.',
|
message: 'The bank transaction has been matched.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BankingRulesController } from './BankingRulesController';
|
|||||||
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||||
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
||||||
import { BankAccountsController } from './BankAccountsController';
|
import { BankAccountsController } from './BankAccountsController';
|
||||||
|
import { BankingUncategorizedController } from './BankingUncategorizedController';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankingController extends BaseController {
|
export class BankingController extends BaseController {
|
||||||
@@ -29,6 +30,10 @@ export class BankingController extends BaseController {
|
|||||||
'/bank_accounts',
|
'/bank_accounts',
|
||||||
Container.get(BankAccountsController).router()
|
Container.get(BankAccountsController).router()
|
||||||
);
|
);
|
||||||
|
router.use(
|
||||||
|
'/categorize',
|
||||||
|
Container.get(BankingUncategorizedController).router()
|
||||||
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
|
import { query } from 'express-validator';
|
||||||
|
import BaseController from '../BaseController';
|
||||||
|
import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BankingUncategorizedController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/autofill',
|
||||||
|
[
|
||||||
|
query('uncategorizedTransactionIds').isArray({ min: 1 }),
|
||||||
|
query('uncategorizedTransactionIds.*').isNumeric().toInt(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
|
this.getAutofillCategorizeTransaction.bind(this)
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the autofill values of the categorize form of the given
|
||||||
|
* uncategorized transactions.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
* @returns {Promise<Response | null>}
|
||||||
|
*/
|
||||||
|
public async getAutofillCategorizeTransaction(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data =
|
||||||
|
await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionIds
|
||||||
|
);
|
||||||
|
return res.status(200).send({ data });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { param } from 'express-validator';
|
import { param, query } from 'express-validator';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
@@ -24,7 +24,12 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/transactions/:transactionId/matches',
|
'/transactions/matches',
|
||||||
|
[
|
||||||
|
query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }),
|
||||||
|
query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
this.getMatchedTransactions.bind(this)
|
this.getMatchedTransactions.bind(this)
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
@@ -44,7 +49,7 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
private getCashflowTransaction = async (
|
private getCashflowTransaction = async (
|
||||||
req: Request,
|
req: Request<{ transactionId: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
@@ -71,19 +76,24 @@ export default class GetCashflowAccounts extends BaseController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
private async getMatchedTransactions(
|
private async getMatchedTransactions(
|
||||||
req: Request<{ transactionId: number }>,
|
req: Request<
|
||||||
|
{ transactionId: number },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ uncategorizeTransactionsIds: Array<number> }
|
||||||
|
>,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { transactionId } = req.params;
|
const uncategorizeTransactionsIds = req.query.uncategorizeTransactionsIds;
|
||||||
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data =
|
const data =
|
||||||
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
transactionId,
|
uncategorizeTransactionsIds,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { ValidationChain, check, param, query } from 'express-validator';
|
import { ValidationChain, check, param, query } from 'express-validator';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { omit } from 'lodash';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
import {
|
||||||
|
AbilitySubject,
|
||||||
|
CashflowAction,
|
||||||
|
ICategorizeCashflowTransactioDTO,
|
||||||
|
} from '@/interfaces';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -44,7 +49,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
this.catchServiceErrors
|
this.catchServiceErrors
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/transactions/:id/categorize',
|
'/transactions/categorize',
|
||||||
this.categorizeCashflowTransactionValidationSchema,
|
this.categorizeCashflowTransactionValidationSchema,
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.categorizeCashflowTransaction,
|
this.categorizeCashflowTransaction,
|
||||||
@@ -89,6 +94,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
public get categorizeCashflowTransactionValidationSchema() {
|
public get categorizeCashflowTransactionValidationSchema() {
|
||||||
return [
|
return [
|
||||||
|
check('uncategorized_transaction_ids').exists().isArray({ min: 1 }),
|
||||||
check('date').exists().isISO8601().toDate(),
|
check('date').exists().isISO8601().toDate(),
|
||||||
check('credit_account_id').exists().isInt().toInt(),
|
check('credit_account_id').exists().isInt().toInt(),
|
||||||
check('transaction_number').optional(),
|
check('transaction_number').optional(),
|
||||||
@@ -161,7 +167,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
private revertCategorizedCashflowTransaction = async (
|
private revertCategorizedCashflowTransaction = async (
|
||||||
req: Request,
|
req: Request<{ id: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
@@ -191,14 +197,19 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { id: cashflowTransactionId } = req.params;
|
const matchedObject = this.matchedBodyData(req);
|
||||||
const cashflowTransaction = this.matchedBodyData(req);
|
const categorizeDTO = omit(matchedObject, [
|
||||||
|
'uncategorizedTransactionIds',
|
||||||
|
]) as ICategorizeCashflowTransactioDTO;
|
||||||
|
|
||||||
|
const uncategorizedTransactionIds =
|
||||||
|
matchedObject.uncategorizedTransactionIds;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.cashflowApplication.categorizeTransaction(
|
await this.cashflowApplication.categorizeTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionId,
|
uncategorizedTransactionIds,
|
||||||
cashflowTransaction
|
categorizeDTO
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'The cashflow transaction has been created successfully.',
|
message: 'The cashflow transaction has been created successfully.',
|
||||||
@@ -269,7 +280,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
public getUncategorizedCashflowTransactions = async (
|
public getUncategorizedCashflowTransactions = async (
|
||||||
req: Request,
|
req: Request<{ id: number }>,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ export interface ICashflowTransactionSchema {
|
|||||||
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
||||||
|
|
||||||
export interface ICategorizeCashflowTransactioDTO {
|
export interface ICategorizeCashflowTransactioDTO {
|
||||||
|
date: Date;
|
||||||
creditAccountId: number;
|
creditAccountId: number;
|
||||||
referenceNo: string;
|
referenceNo: string;
|
||||||
transactionNumber: string;
|
transactionNumber: string;
|
||||||
|
|||||||
@@ -130,20 +130,23 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
|
|
||||||
export interface ICashflowTransactionCategorizedPayload {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransaction: any;
|
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
cashflowTransaction: ICashflowTransaction;
|
cashflowTransaction: ICashflowTransaction;
|
||||||
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
categorizeDTO: any;
|
categorizeDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizingPayload {
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
uncategorizedTransactionId: number;
|
||||||
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizedPayload {
|
export interface ICashflowTransactionUncategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
uncategorizedTransactionId: number;
|
||||||
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
/**
|
/**
|
||||||
* Timestamps columns.
|
* Timestamps columns.
|
||||||
*/
|
*/
|
||||||
static get timestamps() {
|
get timestamps() {
|
||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { first, sumBy } from 'lodash';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
||||||
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
||||||
@@ -47,21 +48,24 @@ export class GetMatchedTransactions {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the matched transactions.
|
* Retrieves the matched transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
|
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
|
||||||
* @param {GetMatchedTransactionsFilter} filter -
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||||
*/
|
*/
|
||||||
public async getMatchedTransactions(
|
public async getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionIds: Array<number>,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const uncategorizedTransaction =
|
const uncategorizedTransactions =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.findById(uncategorizedTransactionId)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const totalPending = Math.abs(sumBy(uncategorizedTransactions, 'amount'));
|
||||||
|
|
||||||
const filtered = filter.transactionType
|
const filtered = filter.transactionType
|
||||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||||
: this.registered;
|
: this.registered;
|
||||||
@@ -71,14 +75,14 @@ export class GetMatchedTransactions {
|
|||||||
.process(async ({ type, service }) => {
|
.process(async ({ type, service }) => {
|
||||||
return service.getMatchedTransactions(tenantId, filter);
|
return service.getMatchedTransactions(tenantId, filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
perfectMatches,
|
perfectMatches,
|
||||||
possibleMatches,
|
possibleMatches,
|
||||||
|
totalPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,20 +94,20 @@ export class GetMatchedTransactions {
|
|||||||
* @returns {MatchedTransactionsPOJO}
|
* @returns {MatchedTransactionsPOJO}
|
||||||
*/
|
*/
|
||||||
private groupMatchedResults(
|
private groupMatchedResults(
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions: Array<any>,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
): MatchedTransactionsPOJO {
|
): MatchedTransactionsPOJO {
|
||||||
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
||||||
|
|
||||||
|
const firstUncategorized = first(uncategorizedTransactions);
|
||||||
|
const amount = sumBy(uncategorizedTransactions, 'amount');
|
||||||
|
const date = firstUncategorized.date;
|
||||||
|
|
||||||
// Sort the results based on amount, date, and transaction type
|
// Sort the results based on amount, date, and transaction type
|
||||||
const closestResullts = sortClosestMatchTransactions(
|
const closestResullts = sortClosestMatchTransactions(amount, date, results);
|
||||||
uncategorizedTransaction,
|
|
||||||
results
|
|
||||||
);
|
|
||||||
const perfectMatches = R.filter(
|
const perfectMatches = R.filter(
|
||||||
(match) =>
|
(match) =>
|
||||||
match.amount === uncategorizedTransaction.amount &&
|
match.amount === amount && moment(match.date).isSame(date, 'day'),
|
||||||
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
|
|
||||||
closestResullts
|
closestResullts
|
||||||
);
|
);
|
||||||
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
MatchedTransactionsPOJO,
|
MatchedTransactionsPOJO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import PromisePool from '@supercharge/promise-pool';
|
||||||
|
|
||||||
export abstract class GetMatchedTransactionsByType {
|
export abstract class GetMatchedTransactionsByType {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -43,24 +44,28 @@ export abstract class GetMatchedTransactionsByType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Creates the common matched transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} uncategorizedTransactionId
|
* @param {Array<number>} uncategorizedTransactionIds
|
||||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||||
* @param {Knex.Transaction} trx
|
* @param {Knex.Transaction} trx
|
||||||
*/
|
*/
|
||||||
public async createMatchedTransaction(
|
public async createMatchedTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionIds: Array<number>,
|
||||||
matchTransactionDTO: IMatchTransactionDTO,
|
matchTransactionDTO: IMatchTransactionDTO,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
await MatchedBankTransaction.query(trx).insert({
|
await PromisePool.withConcurrency(2)
|
||||||
uncategorizedTransactionId,
|
.for(uncategorizedTransactionIds)
|
||||||
referenceType: matchTransactionDTO.referenceType,
|
.process(async (uncategorizedTransactionId) => {
|
||||||
referenceId: matchTransactionDTO.referenceId,
|
await MatchedBankTransaction.query(trx).insert({
|
||||||
});
|
uncategorizedTransactionId,
|
||||||
|
referenceType: matchTransactionDTO.referenceType,
|
||||||
|
referenceId: matchTransactionDTO.referenceId,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
||||||
import { MatchBankTransactions } from './MatchTransactions';
|
import { MatchBankTransactions } from './MatchTransactions';
|
||||||
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
||||||
import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
|
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactionsApplication {
|
export class MatchBankTransactionsApplication {
|
||||||
@@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication {
|
|||||||
*/
|
*/
|
||||||
public getMatchedTransactions(
|
public getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionsIds: Array<number>,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionsIds,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -42,13 +42,13 @@ export class MatchBankTransactionsApplication {
|
|||||||
*/
|
*/
|
||||||
public matchTransaction(
|
public matchTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionId: number | Array<number>,
|
||||||
matchTransactionsDTO: IMatchTransactionsDTO
|
matchedTransactions: Array<IMatchTransactionDTO>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.matchTransactionService.matchTransaction(
|
return this.matchTransactionService.matchTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
matchTransactionsDTO
|
matchedTransactions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isEmpty } from 'lodash';
|
import { castArray } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
@@ -10,11 +10,16 @@ import {
|
|||||||
ERRORS,
|
ERRORS,
|
||||||
IBankTransactionMatchedEventPayload,
|
IBankTransactionMatchedEventPayload,
|
||||||
IBankTransactionMatchingEventPayload,
|
IBankTransactionMatchingEventPayload,
|
||||||
IMatchTransactionsDTO,
|
IMatchTransactionDTO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { sumMatchTranasctions } from './_utils';
|
import {
|
||||||
|
sumMatchTranasctions,
|
||||||
|
sumUncategorizedTransactions,
|
||||||
|
validateUncategorizedTransactionsExcluded,
|
||||||
|
validateUncategorizedTransactionsNotMatched,
|
||||||
|
} from './_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -39,27 +44,25 @@ export class MatchBankTransactions {
|
|||||||
*/
|
*/
|
||||||
async validate(
|
async validate(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionId: number | Array<number>,
|
||||||
matchTransactionsDTO: IMatchTransactionsDTO
|
matchedTransactions: Array<IMatchTransactionDTO>
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
const { matchedTransactions } = matchTransactionsDTO;
|
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||||
|
|
||||||
// Validates the uncategorized transaction existance.
|
// Validates the uncategorized transaction existance.
|
||||||
const uncategorizedTransaction =
|
const uncategorizedTransactions =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.findById(uncategorizedTransactionId)
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
.withGraphFetched('matchedBankTransactions')
|
.withGraphFetched('matchedBankTransactions')
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validates the uncategorized transaction is not already matched.
|
// Validates the uncategorized transaction is not already matched.
|
||||||
if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
|
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions);
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
|
|
||||||
}
|
|
||||||
// Validate the uncategorized transaction is not excluded.
|
// Validate the uncategorized transaction is not excluded.
|
||||||
if (uncategorizedTransaction.excluded) {
|
validateUncategorizedTransactionsExcluded(uncategorizedTransactions);
|
||||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
|
|
||||||
}
|
|
||||||
// Validates the given matched transaction.
|
// Validates the given matched transaction.
|
||||||
const validateMatchedTransaction = async (matchedTransaction) => {
|
const validateMatchedTransaction = async (matchedTransaction) => {
|
||||||
const getMatchedTransactionsService =
|
const getMatchedTransactionsService =
|
||||||
@@ -94,9 +97,12 @@ export class MatchBankTransactions {
|
|||||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||||
validatationResult.results
|
validatationResult.results
|
||||||
);
|
);
|
||||||
|
const totalUncategorizedTransactions = sumUncategorizedTransactions(
|
||||||
|
uncategorizedTransactions
|
||||||
|
);
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
|
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,23 +115,23 @@ export class MatchBankTransactions {
|
|||||||
*/
|
*/
|
||||||
public async matchTransaction(
|
public async matchTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionId: number | Array<number>,
|
||||||
matchTransactionsDTO: IMatchTransactionsDTO
|
matchedTransactions: Array<IMatchTransactionDTO>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { matchedTransactions } = matchTransactionsDTO;
|
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||||
|
|
||||||
// Validates the given matching transactions DTO.
|
// Validates the given matching transactions DTO.
|
||||||
await this.validate(
|
await this.validate(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
matchTransactionsDTO
|
matchedTransactions
|
||||||
);
|
);
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers the event `onBankTransactionMatching`.
|
// Triggers the event `onBankTransactionMatching`.
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
matchTransactionsDTO,
|
matchedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionMatchingEventPayload);
|
} as IBankTransactionMatchingEventPayload);
|
||||||
|
|
||||||
@@ -139,17 +145,16 @@ export class MatchBankTransactions {
|
|||||||
);
|
);
|
||||||
await getMatchedTransactionsService.createMatchedTransaction(
|
await getMatchedTransactionsService.createMatchedTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
matchedTransaction,
|
matchedTransaction,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Triggers the event `onBankTransactionMatched`.
|
// Triggers the event `onBankTransactionMatched`.
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
matchTransactionsDTO,
|
matchedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionMatchedEventPayload);
|
} as IBankTransactionMatchedEventPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||||
import { MatchedTransactionPOJO } from './types';
|
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||||
|
import { isEmpty, sumBy } from 'lodash';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
export const sortClosestMatchTransactions = (
|
export const sortClosestMatchTransactions = (
|
||||||
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
amount: number,
|
||||||
|
date: Date,
|
||||||
matches: MatchedTransactionPOJO[]
|
matches: MatchedTransactionPOJO[]
|
||||||
) => {
|
) => {
|
||||||
return R.sortWith([
|
return R.sortWith([
|
||||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(match.amount - uncategorizedTransaction.amount)
|
Math.abs(match.amount - amount)
|
||||||
),
|
),
|
||||||
// Sort by date difference (closest to uncategorized transaction date first)
|
// Sort by date difference (closest to uncategorized transaction date first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(
|
Math.abs(moment(match.date).diff(moment(date), 'days'))
|
||||||
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
])(matches);
|
])(matches);
|
||||||
};
|
};
|
||||||
@@ -29,3 +30,36 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sumUncategorizedTransactions = (
|
||||||
|
uncategorizedTransactions: Array<any>
|
||||||
|
) => {
|
||||||
|
return sumBy(uncategorizedTransactions, 'amount');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateUncategorizedTransactionsNotMatched = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
) => {
|
||||||
|
const matchedTransactions = uncategorizedTransactions.filter(
|
||||||
|
(trans) => !isEmpty(trans.matchedBankTransactions)
|
||||||
|
);
|
||||||
|
//
|
||||||
|
if (matchedTransactions.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
|
||||||
|
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateUncategorizedTransactionsExcluded = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
) => {
|
||||||
|
const excludedTransactions = uncategorizedTransactions.filter(
|
||||||
|
(trans) => trans.excluded
|
||||||
|
);
|
||||||
|
if (excludedTransactions.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
|
||||||
|
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
IBankTransactionUnmatchedEventPayload,
|
IBankTransactionUnmatchedEventPayload,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import PromisePool from '@supercharge/promise-pool';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DecrementUncategorizedTransactionOnMatching {
|
export class DecrementUncategorizedTransactionOnMatching {
|
||||||
@@ -30,18 +31,24 @@ export class DecrementUncategorizedTransactionOnMatching {
|
|||||||
*/
|
*/
|
||||||
public async decrementUnCategorizedTransactionsOnMatching({
|
public async decrementUnCategorizedTransactionsOnMatching({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
trx,
|
trx,
|
||||||
}: IBankTransactionMatchedEventPayload) {
|
}: IBankTransactionMatchedEventPayload) {
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
this.tenancy.models(tenantId);
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
const uncategorizedTransactions =
|
||||||
uncategorizedTransactionId
|
await UncategorizedCashflowTransaction.query().whereIn(
|
||||||
);
|
'id',
|
||||||
await Account.query(trx)
|
uncategorizedTransactionIds
|
||||||
.findById(transaction.accountId)
|
);
|
||||||
.decrement('uncategorizedTransactions', 1);
|
await PromisePool.withConcurrency(1)
|
||||||
|
.for(uncategorizedTransactions)
|
||||||
|
.process(async (transaction) => {
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(transaction.accountId)
|
||||||
|
.decrement('uncategorizedTransactions', 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { Knex } from 'knex';
|
|||||||
|
|
||||||
export interface IBankTransactionMatchingEventPayload {
|
export interface IBankTransactionMatchingEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: Array<number>;
|
||||||
matchTransactionsDTO: IMatchTransactionsDTO;
|
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||||
trx?: Knex.Transaction;
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionMatchedEventPayload {
|
export interface IBankTransactionMatchedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: Array<number>;
|
||||||
matchTransactionsDTO: IMatchTransactionsDTO;
|
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||||
trx?: Knex.Transaction;
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ export interface IMatchTransactionDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionsDTO {
|
export interface IMatchTransactionsDTO {
|
||||||
|
uncategorizedTransactionIds: Array<number>;
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ export interface MatchedTransactionPOJO {
|
|||||||
export type MatchedTransactionsPOJO = {
|
export type MatchedTransactionsPOJO = {
|
||||||
perfectMatches: Array<MatchedTransactionPOJO>;
|
perfectMatches: Array<MatchedTransactionPOJO>;
|
||||||
possibleMatches: Array<MatchedTransactionPOJO>;
|
possibleMatches: Array<MatchedTransactionPOJO>;
|
||||||
|
totalPending: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { castArray, first, uniq } from 'lodash';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { GetAutofillCategorizeTransctionTransformer } from './GetAutofillCategorizeTransactionTransformer';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetAutofillCategorizeTransaction {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the autofill values of categorize transactions form.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {Array<number> | number} uncategorizeTransactionsId - Uncategorized transactions ids.
|
||||||
|
*/
|
||||||
|
public async getAutofillCategorizeTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizeTransactionsId: Array<number> | number
|
||||||
|
) {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
const uncategorizeTransactionsIds = uniq(
|
||||||
|
castArray(uncategorizeTransactionsId)
|
||||||
|
);
|
||||||
|
const uncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query()
|
||||||
|
.whereIn('id', uncategorizeTransactionsIds)
|
||||||
|
.withGraphFetched('recognizedTransaction.assignAccount')
|
||||||
|
.withGraphFetched('recognizedTransaction.bankRule')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
{},
|
||||||
|
new GetAutofillCategorizeTransctionTransformer(),
|
||||||
|
{
|
||||||
|
uncategorizedTransactions,
|
||||||
|
firstUncategorizedTransaction: first(uncategorizedTransactions),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { sumBy } from 'lodash';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public transactionType() {
|
||||||
|
const assignCategory =
|
||||||
|
this.options.firstUncategorizedTransaction?.recognizedTransaction
|
||||||
|
?.assignCategory || null;
|
||||||
|
|
||||||
|
return assignCategory || 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { transformToMapBy } from '@/utils';
|
import { transformToMapBy } from '@/utils';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { BankRule } from '@/models/BankRule';
|
import { BankRule } from '@/models/BankRule';
|
||||||
import { bankRulesMatchTransaction } from './_utils';
|
import { bankRulesMatchTransaction } from './_utils';
|
||||||
|
|||||||
@@ -164,12 +164,12 @@ export class CashflowApplication {
|
|||||||
*/
|
*/
|
||||||
public categorizeTransaction(
|
public categorizeTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
cashflowTransactionId: number,
|
uncategorizeTransactionIds: Array<number>,
|
||||||
categorizeDTO: ICategorizeCashflowTransactioDTO
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
) {
|
) {
|
||||||
return this.categorizeTransactionService.categorize(
|
return this.categorizeTransactionService.categorize(
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionId,
|
uncategorizeTransactionIds,
|
||||||
categorizeDTO
|
categorizeDTO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { castArray } from 'lodash';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
@@ -8,12 +10,12 @@ import {
|
|||||||
ICashflowTransactionUncategorizingPayload,
|
ICashflowTransactionUncategorizingPayload,
|
||||||
ICategorizeCashflowTransactioDTO,
|
ICategorizeCashflowTransactioDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { Knex } from 'knex';
|
import {
|
||||||
import { transformCategorizeTransToCashflow } from './utils';
|
transformCategorizeTransToCashflow,
|
||||||
|
validateUncategorizedTransactionsNotExcluded,
|
||||||
|
} from './utils';
|
||||||
import { CommandCashflowValidator } from './CommandCasflowValidator';
|
import { CommandCashflowValidator } from './CommandCasflowValidator';
|
||||||
import NewCashflowTransactionService from './NewCashflowTransactionService';
|
import NewCashflowTransactionService from './NewCashflowTransactionService';
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { ERRORS } from './constants';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CategorizeCashflowTransaction {
|
export class CategorizeCashflowTransaction {
|
||||||
@@ -39,27 +41,29 @@ export class CategorizeCashflowTransaction {
|
|||||||
*/
|
*/
|
||||||
public async categorize(
|
public async categorize(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionId: number | Array<number>,
|
||||||
categorizeDTO: ICategorizeCashflowTransactioDTO
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||||
|
|
||||||
// Retrieves the uncategorized transaction or throw an error.
|
// Retrieves the uncategorized transaction or throw an error.
|
||||||
const transaction = await UncategorizedCashflowTransaction.query()
|
const oldUncategorizedTransactions =
|
||||||
.findById(uncategorizedTransactionId)
|
await UncategorizedCashflowTransaction.query()
|
||||||
.throwIfNotFound();
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validate cannot categorize excluded transaction.
|
// Validate cannot categorize excluded transaction.
|
||||||
if (transaction.excluded) {
|
validateUncategorizedTransactionsNotExcluded(oldUncategorizedTransactions);
|
||||||
throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION);
|
|
||||||
}
|
|
||||||
// Validates the transaction shouldn't be categorized before.
|
|
||||||
this.commandValidators.validateTransactionShouldNotCategorized(transaction);
|
|
||||||
|
|
||||||
|
// Validates the transaction shouldn't be categorized before.
|
||||||
|
this.commandValidators.validateTransactionsShouldNotCategorized(
|
||||||
|
oldUncategorizedTransactions
|
||||||
|
);
|
||||||
// Validate the uncateogirzed transaction if it's deposit the transaction direction
|
// Validate the uncateogirzed transaction if it's deposit the transaction direction
|
||||||
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
|
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
|
||||||
this.commandValidators.validateUncategorizeTransactionType(
|
this.commandValidators.validateUncategorizeTransactionType(
|
||||||
transaction,
|
oldUncategorizedTransactions,
|
||||||
categorizeDTO.transactionType
|
categorizeDTO.transactionType
|
||||||
);
|
);
|
||||||
// Edits the cashflow transaction under UOW env.
|
// Edits the cashflow transaction under UOW env.
|
||||||
@@ -69,12 +73,13 @@ export class CategorizeCashflowTransaction {
|
|||||||
events.cashflow.onTransactionCategorizing,
|
events.cashflow.onTransactionCategorizing,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionUncategorizingPayload
|
} as ICashflowTransactionUncategorizingPayload
|
||||||
);
|
);
|
||||||
// Transformes the categorize DTO to the cashflow transaction.
|
// Transformes the categorize DTO to the cashflow transaction.
|
||||||
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
|
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
|
||||||
transaction,
|
oldUncategorizedTransactions,
|
||||||
categorizeDTO
|
categorizeDTO
|
||||||
);
|
);
|
||||||
// Creates a new cashflow transaction.
|
// Creates a new cashflow transaction.
|
||||||
@@ -83,15 +88,20 @@ export class CategorizeCashflowTransaction {
|
|||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionDTO
|
cashflowTransactionDTO
|
||||||
);
|
);
|
||||||
|
|
||||||
// Updates the uncategorized transaction as categorized.
|
// Updates the uncategorized transaction as categorized.
|
||||||
const uncategorizedTransaction =
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
.whereIn('id', uncategorizedTransactionIds)
|
||||||
uncategorizedTransactionId,
|
.patch({
|
||||||
{
|
categorized: true,
|
||||||
categorized: true,
|
categorizeRefType: 'CashflowTransaction',
|
||||||
categorizeRefType: 'CashflowTransaction',
|
categorizeRefId: cashflowTransaction.id,
|
||||||
categorizeRefId: cashflowTransaction.id,
|
});
|
||||||
}
|
// Fetch the new updated uncategorized transactions.
|
||||||
|
const uncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query(trx).whereIn(
|
||||||
|
'id',
|
||||||
|
uncategorizedTransactionIds
|
||||||
);
|
);
|
||||||
// Triggers `onCashflowTransactionCategorized` event.
|
// Triggers `onCashflowTransactionCategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
@@ -99,7 +109,8 @@ export class CategorizeCashflowTransaction {
|
|||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransaction,
|
cashflowTransaction,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
categorizeDTO,
|
categorizeDTO,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionCategorizedPayload
|
} as ICashflowTransactionCategorizedPayload
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { includes, camelCase, upperFirst } from 'lodash';
|
import { includes, camelCase, upperFirst, sumBy } from 'lodash';
|
||||||
import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
|
import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
|
||||||
import { getCashflowTransactionType } from './utils';
|
import { getCashflowTransactionType } from './utils';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
@@ -68,11 +68,15 @@ export class CommandCashflowValidator {
|
|||||||
* Validate the given transcation shouldn't be categorized.
|
* Validate the given transcation shouldn't be categorized.
|
||||||
* @param {CashflowTransaction} cashflowTransaction
|
* @param {CashflowTransaction} cashflowTransaction
|
||||||
*/
|
*/
|
||||||
public validateTransactionShouldNotCategorized(
|
public validateTransactionsShouldNotCategorized(
|
||||||
cashflowTransaction: CashflowTransaction
|
cashflowTransactions: Array<IUncategorizedCashflowTransaction>
|
||||||
) {
|
) {
|
||||||
if (cashflowTransaction.uncategorize) {
|
const categorized = cashflowTransactions.filter((t) => t.categorized);
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
|
||||||
|
if (categorized?.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED, '', {
|
||||||
|
ids: categorized.map((t) => t.id),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,17 +87,19 @@ export class CommandCashflowValidator {
|
|||||||
* @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
|
* @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
|
||||||
*/
|
*/
|
||||||
public validateUncategorizeTransactionType(
|
public validateUncategorizeTransactionType(
|
||||||
uncategorizeTransaction: IUncategorizedCashflowTransaction,
|
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>,
|
||||||
transactionType: string
|
transactionType: string
|
||||||
) {
|
) {
|
||||||
|
const amount = sumBy(uncategorizeTransactions, 'amount');
|
||||||
|
const isDepositTransaction = amount > 0;
|
||||||
|
const isWithdrawalTransaction = amount <= 0;
|
||||||
|
|
||||||
const type = getCashflowTransactionType(
|
const type = getCashflowTransactionType(
|
||||||
upperFirst(camelCase(transactionType)) as CASHFLOW_TRANSACTION_TYPE
|
transactionType as CASHFLOW_TRANSACTION_TYPE
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
(type.direction === CASHFLOW_DIRECTION.IN &&
|
(type.direction === CASHFLOW_DIRECTION.IN && isDepositTransaction) ||
|
||||||
uncategorizeTransaction.isDepositTransaction) ||
|
(type.direction === CASHFLOW_DIRECTION.OUT && isWithdrawalTransaction)
|
||||||
(type.direction === CASHFLOW_DIRECTION.OUT &&
|
|
||||||
uncategorizeTransaction.isWithdrawalTransaction)
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ICashflowTransactionUncategorizedPayload,
|
ICashflowTransactionUncategorizedPayload,
|
||||||
ICashflowTransactionUncategorizingPayload,
|
ICashflowTransactionUncategorizingPayload,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
import { validateTransactionShouldBeCategorized } from './utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UncategorizeCashflowTransaction {
|
export class UncategorizeCashflowTransaction {
|
||||||
@@ -24,11 +25,12 @@ export class UncategorizeCashflowTransaction {
|
|||||||
* Uncategorizes the given cashflow transaction.
|
* Uncategorizes the given cashflow transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} cashflowTransactionId
|
* @param {number} cashflowTransactionId
|
||||||
|
* @returns {Promise<Array<number>>}
|
||||||
*/
|
*/
|
||||||
public async uncategorize(
|
public async uncategorize(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number
|
uncategorizedTransactionId: number
|
||||||
) {
|
): Promise<Array<number>> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldUncategorizedTransaction =
|
const oldUncategorizedTransaction =
|
||||||
@@ -36,6 +38,22 @@ export class UncategorizeCashflowTransaction {
|
|||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
validateTransactionShouldBeCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
|
const associatedUncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query()
|
||||||
|
.where('categorizeRefId', oldUncategorizedTransaction.categorizeRefId)
|
||||||
|
.where(
|
||||||
|
'categorizeRefType',
|
||||||
|
oldUncategorizedTransaction.categorizeRefType
|
||||||
|
);
|
||||||
|
const oldUncategorizedTransactions = [
|
||||||
|
oldUncategorizedTransaction,
|
||||||
|
...associatedUncategorizedTransactions,
|
||||||
|
];
|
||||||
|
const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map(
|
||||||
|
(t) => t.id
|
||||||
|
);
|
||||||
// Updates the transaction under UOW.
|
// Updates the transaction under UOW.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onTransactionUncategorizing` event.
|
// Triggers `onTransactionUncategorizing` event.
|
||||||
@@ -43,30 +61,36 @@ export class UncategorizeCashflowTransaction {
|
|||||||
events.cashflow.onTransactionUncategorizing,
|
events.cashflow.onTransactionUncategorizing,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionUncategorizingPayload
|
} as ICashflowTransactionUncategorizingPayload
|
||||||
);
|
);
|
||||||
// Removes the ref relation with the related transaction.
|
// Removes the ref relation with the related transaction.
|
||||||
const uncategorizedTransaction =
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
await UncategorizedCashflowTransaction.query(trx).updateAndFetchById(
|
.whereIn('id', oldUncategoirzedTransactionsIds)
|
||||||
uncategorizedTransactionId,
|
.patch({
|
||||||
{
|
categorized: false,
|
||||||
categorized: false,
|
categorizeRefId: null,
|
||||||
categorizeRefId: null,
|
categorizeRefType: null,
|
||||||
categorizeRefType: null,
|
});
|
||||||
}
|
const uncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query(trx).whereIn(
|
||||||
|
'id',
|
||||||
|
oldUncategoirzedTransactionsIds
|
||||||
);
|
);
|
||||||
// Triggers `onTransactionUncategorized` event.
|
// Triggers `onTransactionUncategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionUncategorized,
|
events.cashflow.onTransactionUncategorized,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactionId,
|
||||||
oldUncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionUncategorizedPayload
|
} as ICashflowTransactionUncategorizedPayload
|
||||||
);
|
);
|
||||||
return uncategorizedTransaction;
|
return oldUncategoirzedTransactionsIds;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export const ERRORS = {
|
|||||||
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
|
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
|
||||||
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
||||||
|
|
||||||
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION'
|
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
|
||||||
|
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED'
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CASHFLOW_DIRECTION {
|
export enum CASHFLOW_DIRECTION {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ICashflowTransactionCategorizedPayload,
|
ICashflowTransactionCategorizedPayload,
|
||||||
ICashflowTransactionUncategorizedPayload,
|
ICashflowTransactionUncategorizedPayload,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
import PromisePool from '@supercharge/promise-pool';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DecrementUncategorizedTransactionOnCategorize {
|
export class DecrementUncategorizedTransactionOnCategorize {
|
||||||
@@ -34,13 +35,18 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
*/
|
*/
|
||||||
public async decrementUnCategorizedTransactionsOnCategorized({
|
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
trx
|
||||||
}: ICashflowTransactionCategorizedPayload) {
|
}: ICashflowTransactionCategorizedPayload) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
await Account.query()
|
await PromisePool.withConcurrency(1)
|
||||||
.findById(uncategorizedTransaction.accountId)
|
.for(uncategorizedTransactions)
|
||||||
.decrement('uncategorizedTransactions', 1);
|
.process(async (uncategorizedTransaction) => {
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(uncategorizedTransaction.accountId)
|
||||||
|
.decrement('uncategorizedTransactions', 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,13 +55,18 @@ export class DecrementUncategorizedTransactionOnCategorize {
|
|||||||
*/
|
*/
|
||||||
public async incrementUnCategorizedTransactionsOnUncategorized({
|
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
trx
|
||||||
}: ICashflowTransactionUncategorizedPayload) {
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
await Account.query()
|
await PromisePool.withConcurrency(1)
|
||||||
.findById(uncategorizedTransaction.accountId)
|
.for(uncategorizedTransactions)
|
||||||
.increment('uncategorizedTransactions', 1);
|
.process(async (uncategorizedTransaction) => {
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(uncategorizedTransaction.accountId)
|
||||||
|
.increment('uncategorizedTransactions', 1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
||||||
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DeleteCashflowTransactionOnUncategorize {
|
export class DeleteCashflowTransactionOnUncategorize {
|
||||||
@@ -25,18 +27,27 @@ export class DeleteCashflowTransactionOnUncategorize {
|
|||||||
*/
|
*/
|
||||||
public async deleteCashflowTransactionOnUncategorize({
|
public async deleteCashflowTransactionOnUncategorize({
|
||||||
tenantId,
|
tenantId,
|
||||||
oldUncategorizedTransaction,
|
oldUncategorizedTransactions,
|
||||||
trx,
|
trx,
|
||||||
}: ICashflowTransactionUncategorizedPayload) {
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
// Deletes the cashflow transaction.
|
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter(
|
||||||
if (
|
(transaction) => transaction.categorizeRefType === 'CashflowTransaction'
|
||||||
oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
|
);
|
||||||
) {
|
|
||||||
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
|
||||||
tenantId,
|
|
||||||
|
|
||||||
oldUncategorizedTransaction.categorizeRefId
|
// Deletes the cashflow transaction.
|
||||||
);
|
if (_oldUncategorizedTransactions.length > 0) {
|
||||||
|
const result = await PromisePool.withConcurrency(1)
|
||||||
|
.for(_oldUncategorizedTransactions)
|
||||||
|
.process(async (oldUncategorizedTransaction) => {
|
||||||
|
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||||
|
tenantId,
|
||||||
|
oldUncategorizedTransaction.categorizeRefId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
throw new ServiceError('SOMETHING_WRONG');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { upperFirst, camelCase } from 'lodash';
|
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash';
|
||||||
import {
|
import {
|
||||||
CASHFLOW_TRANSACTION_TYPE,
|
CASHFLOW_TRANSACTION_TYPE,
|
||||||
CASHFLOW_TRANSACTION_TYPE_META,
|
CASHFLOW_TRANSACTION_TYPE_META,
|
||||||
|
ERRORS,
|
||||||
ICashflowTransactionTypeMeta,
|
ICashflowTransactionTypeMeta,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +10,8 @@ import {
|
|||||||
ICategorizeCashflowTransactioDTO,
|
ICategorizeCashflowTransactioDTO,
|
||||||
IUncategorizedCashflowTransaction,
|
IUncategorizedCashflowTransaction,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures the given transaction type to transformed to appropriate format.
|
* Ensures the given transaction type to transformed to appropriate format.
|
||||||
@@ -27,7 +30,9 @@ export const transformCashflowTransactionType = (type) => {
|
|||||||
export function getCashflowTransactionType(
|
export function getCashflowTransactionType(
|
||||||
transactionType: CASHFLOW_TRANSACTION_TYPE
|
transactionType: CASHFLOW_TRANSACTION_TYPE
|
||||||
): ICashflowTransactionTypeMeta {
|
): ICashflowTransactionTypeMeta {
|
||||||
return CASHFLOW_TRANSACTION_TYPE_META[transactionType];
|
const _transactionType = transformCashflowTransactionType(transactionType);
|
||||||
|
|
||||||
|
return CASHFLOW_TRANSACTION_TYPE_META[_transactionType];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,22 +51,46 @@ export const getCashflowAccountTransactionsTypes = () => {
|
|||||||
* @returns {ICashflowNewCommandDTO}
|
* @returns {ICashflowNewCommandDTO}
|
||||||
*/
|
*/
|
||||||
export const transformCategorizeTransToCashflow = (
|
export const transformCategorizeTransToCashflow = (
|
||||||
uncategorizeModel: IUncategorizedCashflowTransaction,
|
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>,
|
||||||
categorizeDTO: ICategorizeCashflowTransactioDTO
|
categorizeDTO: ICategorizeCashflowTransactioDTO
|
||||||
): ICashflowNewCommandDTO => {
|
): ICashflowNewCommandDTO => {
|
||||||
|
const uncategorizeTransaction = first(uncategorizeTransactions);
|
||||||
|
const amount = sumBy(uncategorizeTransactions, 'amount');
|
||||||
|
const amountAbs = Math.abs(amount);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: uncategorizeModel.date,
|
date: categorizeDTO.date,
|
||||||
referenceNo: categorizeDTO.referenceNo || uncategorizeModel.referenceNo,
|
referenceNo: categorizeDTO.referenceNo,
|
||||||
description: categorizeDTO.description || uncategorizeModel.description,
|
description: categorizeDTO.description,
|
||||||
cashflowAccountId: uncategorizeModel.accountId,
|
cashflowAccountId: uncategorizeTransaction.accountId,
|
||||||
creditAccountId: categorizeDTO.creditAccountId,
|
creditAccountId: categorizeDTO.creditAccountId,
|
||||||
exchangeRate: categorizeDTO.exchangeRate || 1,
|
exchangeRate: categorizeDTO.exchangeRate || 1,
|
||||||
currencyCode: uncategorizeModel.currencyCode,
|
currencyCode: categorizeDTO.currencyCode,
|
||||||
amount: uncategorizeModel.amount,
|
amount: amountAbs,
|
||||||
transactionNumber: categorizeDTO.transactionNumber,
|
transactionNumber: categorizeDTO.transactionNumber,
|
||||||
transactionType: categorizeDTO.transactionType,
|
transactionType: categorizeDTO.transactionType,
|
||||||
uncategorizedTransactionId: uncategorizeModel.id,
|
|
||||||
branchId: categorizeDTO?.branchId,
|
branchId: categorizeDTO?.branchId,
|
||||||
publish: true,
|
publish: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateUncategorizedTransactionsNotExcluded = (
|
||||||
|
transactions: Array<UncategorizeCashflowTransaction>
|
||||||
|
) => {
|
||||||
|
const excluded = transactions.filter((tran) => tran.excluded);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
Intent,
|
Intent,
|
||||||
|
Switch,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
useExcludeUncategorizedTransactions,
|
useExcludeUncategorizedTransactions,
|
||||||
useUnexcludeUncategorizedTransactions,
|
useUnexcludeUncategorizedTransactions,
|
||||||
} from '@/hooks/query/bank-rules';
|
} from '@/hooks/query/bank-rules';
|
||||||
|
import { withBankingActions } from '../withBankingActions';
|
||||||
import { withBanking } from '../withBanking';
|
import { withBanking } from '../withBanking';
|
||||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
@@ -61,6 +63,10 @@ function AccountTransactionsActionsBar({
|
|||||||
// #withBanking
|
// #withBanking
|
||||||
uncategorizedTransationsIdsSelected,
|
uncategorizedTransationsIdsSelected,
|
||||||
excludedTransactionsIdsSelected,
|
excludedTransactionsIdsSelected,
|
||||||
|
openMatchingTransactionAside,
|
||||||
|
|
||||||
|
// #withBankingActions
|
||||||
|
enableMultipleCategorization,
|
||||||
|
|
||||||
// #withAlerts
|
// #withAlerts
|
||||||
openAlert,
|
openAlert,
|
||||||
@@ -185,6 +191,10 @@ function AccountTransactionsActionsBar({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle multi select transactions for categorization or matching.
|
||||||
|
const handleMultipleCategorizingSwitch = (event) => {
|
||||||
|
enableMultipleCategorization(event.currentTarget.checked);
|
||||||
|
}
|
||||||
// Handle resume bank feeds syncing.
|
// Handle resume bank feeds syncing.
|
||||||
const handleResumeFeedsSyncing = () => {
|
const handleResumeFeedsSyncing = () => {
|
||||||
openAlert('resume-feeds-syncing-bank-accounnt', {
|
openAlert('resume-feeds-syncing-bank-accounnt', {
|
||||||
@@ -290,6 +300,22 @@ function AccountTransactionsActionsBar({
|
|||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
|
|
||||||
<NavbarGroup align={Alignment.RIGHT}>
|
<NavbarGroup align={Alignment.RIGHT}>
|
||||||
|
{openMatchingTransactionAside && (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
'Enables to categorize or matching multiple bank transactions into one transaction.'
|
||||||
|
}
|
||||||
|
position={Position.BOTTOM}
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
label={'Multi Select'}
|
||||||
|
inline
|
||||||
|
onChange={handleMultipleCategorizingSwitch}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<NavbarDivider />
|
||||||
<Popover
|
<Popover
|
||||||
minimal={true}
|
minimal={true}
|
||||||
interactionKind={PopoverInteractionKind.CLICK}
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
@@ -352,9 +378,12 @@ export default compose(
|
|||||||
({
|
({
|
||||||
uncategorizedTransationsIdsSelected,
|
uncategorizedTransationsIdsSelected,
|
||||||
excludedTransactionsIdsSelected,
|
excludedTransactionsIdsSelected,
|
||||||
|
openMatchingTransactionAside,
|
||||||
}) => ({
|
}) => ({
|
||||||
uncategorizedTransationsIdsSelected,
|
uncategorizedTransationsIdsSelected,
|
||||||
excludedTransactionsIdsSelected,
|
excludedTransactionsIdsSelected,
|
||||||
|
openMatchingTransactionAside,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
withBankingActions,
|
||||||
)(AccountTransactionsActionsBar);
|
)(AccountTransactionsActionsBar);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.table :global .td.categorize_include,
|
||||||
|
.table :global .th.categorize_include {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table.showCategorizeColumn :global .td.categorize_include,
|
||||||
|
.table.showCategorizeColumn :global .th.categorize_include {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categorizeCheckbox:global(.bp4-checkbox) :global .bp4-control-indicator {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import {
|
import {
|
||||||
@@ -12,17 +13,19 @@ import {
|
|||||||
AppToaster,
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
import { ActionsMenu } from './UncategorizedTransactions/components';
|
import { ActionsMenu } from './components';
|
||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import { withBankingActions } from '../withBankingActions';
|
import { withBankingActions } from '../../withBankingActions';
|
||||||
|
|
||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import { useAccountUncategorizedTransactionsColumns } from './components';
|
import { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot';
|
||||||
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
|
||||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
import { useAccountUncategorizedTransactionsColumns } from './hooks';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { withBanking } from '../../withBanking';
|
||||||
|
import styles from './AccountTransactionsUncategorizedTable.module.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
@@ -31,9 +34,16 @@ function AccountTransactionsDataTable({
|
|||||||
// #withSettings
|
// #withSettings
|
||||||
cashflowTansactionsTableSize,
|
cashflowTansactionsTableSize,
|
||||||
|
|
||||||
|
// #withBanking
|
||||||
|
openMatchingTransactionAside,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
setUncategorizedTransactionIdForMatching,
|
setUncategorizedTransactionIdForMatching,
|
||||||
setUncategorizedTransactionsSelected,
|
setUncategorizedTransactionsSelected,
|
||||||
|
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
}) {
|
}) {
|
||||||
// Retrieve table columns.
|
// Retrieve table columns.
|
||||||
const columns = useAccountUncategorizedTransactionsColumns();
|
const columns = useAccountUncategorizedTransactionsColumns();
|
||||||
@@ -51,7 +61,11 @@ function AccountTransactionsDataTable({
|
|||||||
|
|
||||||
// Handle cell click.
|
// Handle cell click.
|
||||||
const handleCellClick = (cell) => {
|
const handleCellClick = (cell) => {
|
||||||
setUncategorizedTransactionIdForMatching(cell.row.original.id);
|
if (enableMultipleCategorization) {
|
||||||
|
addTransactionsToCategorizeSelected(cell.row.original.id);
|
||||||
|
} else {
|
||||||
|
setTransactionsToCategorizeSelected(cell.row.original.id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Handles categorize button click.
|
// Handles categorize button click.
|
||||||
const handleCategorizeBtnClick = (transaction) => {
|
const handleCategorizeBtnClick = (transaction) => {
|
||||||
@@ -66,7 +80,7 @@ function AccountTransactionsDataTable({
|
|||||||
message: 'The bank transaction has been excluded successfully.',
|
message: 'The bank transaction has been excluded successfully.',
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
message: 'Something went wrong.',
|
message: 'Something went wrong.',
|
||||||
@@ -74,12 +88,6 @@ function AccountTransactionsDataTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle selected rows change.
|
|
||||||
const handleSelectedRowsChange = (selected) => {
|
|
||||||
const _selectedIds = selected?.map((row) => row.original.id);
|
|
||||||
setUncategorizedTransactionsSelected(_selectedIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
@@ -106,12 +114,13 @@ function AccountTransactionsDataTable({
|
|||||||
noResults={
|
noResults={
|
||||||
'There is no uncategorized transactions in the current account.'
|
'There is no uncategorized transactions in the current account.'
|
||||||
}
|
}
|
||||||
className="table-constrant"
|
|
||||||
onSelectedRowsChange={handleSelectedRowsChange}
|
|
||||||
payload={{
|
payload={{
|
||||||
onExclude: handleExcludeTransaction,
|
onExclude: handleExcludeTransaction,
|
||||||
onCategorize: handleCategorizeBtnClick,
|
onCategorize: handleCategorizeBtnClick,
|
||||||
}}
|
}}
|
||||||
|
className={clsx('table-constrant', styles.table, {
|
||||||
|
[styles.showCategorizeColumn]: enableMultipleCategorization,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -121,6 +130,12 @@ export default compose(
|
|||||||
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
|
||||||
})),
|
})),
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
|
withBanking(
|
||||||
|
({ openMatchingTransactionAside, enableMultipleCategorization }) => ({
|
||||||
|
openMatchingTransactionAside,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
}),
|
||||||
|
),
|
||||||
)(AccountTransactionsDataTable);
|
)(AccountTransactionsDataTable);
|
||||||
|
|
||||||
const DashboardConstrantTable = styled(DataTable)`
|
const DashboardConstrantTable = styled(DataTable)`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import AccountTransactionsUncategorizedTable from '../AccountTransactionsUncategorizedTable';
|
import AccountTransactionsUncategorizedTable from './AccountTransactionsUncategorizedTable';
|
||||||
import { AccountUncategorizedTransactionsBoot } from '../AllTransactionsUncategorizedBoot';
|
import { AccountUncategorizedTransactionsBoot } from '../AllTransactionsUncategorizedBoot';
|
||||||
import { AccountTransactionsCard } from './AccountTransactionsCard';
|
import { AccountTransactionsCard } from './AccountTransactionsCard';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Intent,
|
||||||
|
PopoverInteractionKind,
|
||||||
|
Position,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
useAddTransactionsToCategorizeSelected,
|
||||||
|
useIsTransactionToCategorizeSelected,
|
||||||
|
useRemoveTransactionsToCategorizeSelected,
|
||||||
|
} from '@/hooks/state/banking';
|
||||||
|
import { Box, Icon } from '@/components';
|
||||||
|
import styles from './AccountTransactionsUncategorizedTable.module.scss';
|
||||||
|
|
||||||
|
function statusAccessor(transaction) {
|
||||||
|
return transaction.is_recognized ? (
|
||||||
|
<Tooltip
|
||||||
|
interactionKind={PopoverInteractionKind.HOVER}
|
||||||
|
position={Position.RIGHT}
|
||||||
|
content={
|
||||||
|
<Box>
|
||||||
|
<span>{transaction.assigned_category_formatted}</span>
|
||||||
|
<Icon
|
||||||
|
icon={'arrowRight'}
|
||||||
|
color={'#8F99A8'}
|
||||||
|
iconSize={12}
|
||||||
|
style={{ marginLeft: 8, marginRight: 8 }}
|
||||||
|
/>
|
||||||
|
<span>{transaction.assigned_account_name}</span>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Tag intent={Intent.SUCCESS} interactive>
|
||||||
|
Recognized
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionSelectCheckboxProps {
|
||||||
|
transactionId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionSelectCheckbox({
|
||||||
|
transactionId,
|
||||||
|
}: TransactionSelectCheckboxProps) {
|
||||||
|
const addTransactionsToCategorizeSelected =
|
||||||
|
useAddTransactionsToCategorizeSelected();
|
||||||
|
|
||||||
|
const removeTransactionsToCategorizeSelected =
|
||||||
|
useRemoveTransactionsToCategorizeSelected();
|
||||||
|
|
||||||
|
const isTransactionSelected =
|
||||||
|
useIsTransactionToCategorizeSelected(transactionId);
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
isTransactionSelected
|
||||||
|
? removeTransactionsToCategorizeSelected(transactionId)
|
||||||
|
: addTransactionsToCategorizeSelected(transactionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
large
|
||||||
|
checked={isTransactionSelected}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.categorizeCheckbox}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account uncategorized transctions table columns.
|
||||||
|
*/
|
||||||
|
export function useAccountUncategorizedTransactionsColumns() {
|
||||||
|
return React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
Header: intl.get('date'),
|
||||||
|
accessor: 'formatted_date',
|
||||||
|
width: 40,
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'description',
|
||||||
|
Header: 'Description',
|
||||||
|
accessor: 'description',
|
||||||
|
width: 160,
|
||||||
|
textOverview: true,
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'payee',
|
||||||
|
Header: 'Payee',
|
||||||
|
accessor: 'payee',
|
||||||
|
width: 60,
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reference_number',
|
||||||
|
Header: 'Ref.#',
|
||||||
|
accessor: 'reference_no',
|
||||||
|
width: 50,
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: statusAccessor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deposit',
|
||||||
|
Header: intl.get('cash_flow.label.deposit'),
|
||||||
|
accessor: 'formatted_deposit_amount',
|
||||||
|
width: 40,
|
||||||
|
className: 'deposit',
|
||||||
|
textOverview: true,
|
||||||
|
align: 'right',
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'withdrawal',
|
||||||
|
Header: intl.get('cash_flow.label.withdrawal'),
|
||||||
|
accessor: 'formatted_withdrawal_amount',
|
||||||
|
className: 'withdrawal',
|
||||||
|
width: 40,
|
||||||
|
textOverview: true,
|
||||||
|
align: 'right',
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'categorize_include',
|
||||||
|
Header: '',
|
||||||
|
accessor: (value) => (
|
||||||
|
<TransactionSelectCheckbox transactionId={value.id} />
|
||||||
|
),
|
||||||
|
width: 20,
|
||||||
|
minWidth: 20,
|
||||||
|
maxWidth: 20,
|
||||||
|
align: 'right',
|
||||||
|
className: 'categorize_include selection-checkbox',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -150,99 +150,3 @@ export function AccountTransactionsProgressBar() {
|
|||||||
<MaterialProgressBar />
|
<MaterialProgressBar />
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusAccessor(transaction) {
|
|
||||||
return transaction.is_recognized ? (
|
|
||||||
<Tooltip
|
|
||||||
compact
|
|
||||||
interactionKind={PopoverInteractionKind.HOVER}
|
|
||||||
position={Position.RIGHT}
|
|
||||||
content={
|
|
||||||
<Box>
|
|
||||||
<span>{transaction.assigned_category_formatted}</span>
|
|
||||||
<Icon
|
|
||||||
icon={'arrowRight'}
|
|
||||||
color={'#8F99A8'}
|
|
||||||
iconSize={12}
|
|
||||||
style={{ marginLeft: 8, marginRight: 8 }}
|
|
||||||
/>
|
|
||||||
<span>{transaction.assigned_account_name}</span>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<Tag intent={Intent.SUCCESS} interactive>
|
|
||||||
Recognized
|
|
||||||
</Tag>
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve account uncategorized transctions table columns.
|
|
||||||
*/
|
|
||||||
export function useAccountUncategorizedTransactionsColumns() {
|
|
||||||
return React.useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 'date',
|
|
||||||
Header: intl.get('date'),
|
|
||||||
accessor: 'formatted_date',
|
|
||||||
width: 40,
|
|
||||||
clickable: true,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'description',
|
|
||||||
Header: 'Description',
|
|
||||||
accessor: 'description',
|
|
||||||
width: 160,
|
|
||||||
textOverview: true,
|
|
||||||
clickable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payee',
|
|
||||||
Header: 'Payee',
|
|
||||||
accessor: 'payee',
|
|
||||||
width: 60,
|
|
||||||
clickable: true,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'reference_number',
|
|
||||||
Header: 'Ref.#',
|
|
||||||
accessor: 'reference_no',
|
|
||||||
width: 50,
|
|
||||||
clickable: true,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
Header: 'Status',
|
|
||||||
accessor: statusAccessor,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deposit',
|
|
||||||
Header: intl.get('cash_flow.label.deposit'),
|
|
||||||
accessor: 'formatted_deposit_amount',
|
|
||||||
width: 40,
|
|
||||||
className: 'deposit',
|
|
||||||
textOverview: true,
|
|
||||||
align: 'right',
|
|
||||||
clickable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'withdrawal',
|
|
||||||
Header: intl.get('cash_flow.label.withdrawal'),
|
|
||||||
accessor: 'formatted_withdrawal_amount',
|
|
||||||
className: 'withdrawal',
|
|
||||||
width: 40,
|
|
||||||
textOverview: true,
|
|
||||||
align: 'right',
|
|
||||||
clickable: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ import { useAccounts, useBranches } from '@/hooks/query';
|
|||||||
import { useFeatureCan } from '@/hooks/state';
|
import { useFeatureCan } from '@/hooks/state';
|
||||||
import { Features } from '@/constants';
|
import { Features } from '@/constants';
|
||||||
import { Spinner } from '@blueprintjs/core';
|
import { Spinner } from '@blueprintjs/core';
|
||||||
import { useGetRecognizedBankTransaction } from '@/hooks/query/bank-rules';
|
import {
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
GetAutofillCategorizeTransaction,
|
||||||
|
useGetAutofillCategorizeTransaction,
|
||||||
|
} from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
interface CategorizeTransactionBootProps {
|
interface CategorizeTransactionBootProps {
|
||||||
|
uncategorizedTransactionsIds: Array<number>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +22,8 @@ interface CategorizeTransactionBootValue {
|
|||||||
isBranchesLoading: boolean;
|
isBranchesLoading: boolean;
|
||||||
isAccountsLoading: boolean;
|
isAccountsLoading: boolean;
|
||||||
primaryBranch: any;
|
primaryBranch: any;
|
||||||
recognizedTranasction: any;
|
autofillCategorizeValues: null | GetAutofillCategorizeTransaction;
|
||||||
isRecognizedTransactionLoading: boolean;
|
isAutofillCategorizeValuesLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CategorizeTransactionBootContext =
|
const CategorizeTransactionBootContext =
|
||||||
@@ -32,11 +35,9 @@ const CategorizeTransactionBootContext =
|
|||||||
* Categorize transcation boot.
|
* Categorize transcation boot.
|
||||||
*/
|
*/
|
||||||
function CategorizeTransactionBoot({
|
function CategorizeTransactionBoot({
|
||||||
|
uncategorizedTransactionsIds,
|
||||||
...props
|
...props
|
||||||
}: CategorizeTransactionBootProps) {
|
}: CategorizeTransactionBootProps) {
|
||||||
const { uncategorizedTransaction, uncategorizedTransactionId } =
|
|
||||||
useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
// Detarmines whether the feature is enabled.
|
// Detarmines whether the feature is enabled.
|
||||||
const { featureCan } = useFeatureCan();
|
const { featureCan } = useFeatureCan();
|
||||||
const isBranchFeatureCan = featureCan(Features.Branches);
|
const isBranchFeatureCan = featureCan(Features.Branches);
|
||||||
@@ -49,13 +50,11 @@ function CategorizeTransactionBoot({
|
|||||||
{},
|
{},
|
||||||
{ enabled: isBranchFeatureCan },
|
{ enabled: isBranchFeatureCan },
|
||||||
);
|
);
|
||||||
// Fetches the recognized transaction.
|
// Fetches the autofill values of categorize transaction.
|
||||||
const {
|
const {
|
||||||
data: recognizedTranasction,
|
data: autofillCategorizeValues,
|
||||||
isLoading: isRecognizedTransactionLoading,
|
isLoading: isAutofillCategorizeValuesLoading,
|
||||||
} = useGetRecognizedBankTransaction(uncategorizedTransactionId, {
|
} = useGetAutofillCategorizeTransaction(uncategorizedTransactionsIds, {});
|
||||||
enabled: !!uncategorizedTransaction.is_recognized,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieves the primary branch.
|
// Retrieves the primary branch.
|
||||||
const primaryBranch = useMemo(
|
const primaryBranch = useMemo(
|
||||||
@@ -69,11 +68,11 @@ function CategorizeTransactionBoot({
|
|||||||
isBranchesLoading,
|
isBranchesLoading,
|
||||||
isAccountsLoading,
|
isAccountsLoading,
|
||||||
primaryBranch,
|
primaryBranch,
|
||||||
recognizedTranasction,
|
autofillCategorizeValues,
|
||||||
isRecognizedTransactionLoading,
|
isAutofillCategorizeValuesLoading,
|
||||||
};
|
};
|
||||||
const isLoading =
|
const isLoading =
|
||||||
isBranchesLoading || isAccountsLoading || isRecognizedTransactionLoading;
|
isBranchesLoading || isAccountsLoading || isAutofillCategorizeValuesLoading;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
<Spinner size={30} />;
|
<Spinner size={30} />;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { CategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
import { CategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionForm } from './CategorizeTransactionForm';
|
import { CategorizeTransactionForm } from './CategorizeTransactionForm';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
import { withBanking } from '@/containers/CashFlow/withBanking';
|
||||||
|
|
||||||
export function CategorizeTransactionContent() {
|
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
|
function CategorizeTransactionContentRoot({
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<CategorizeTransactionBoot
|
<CategorizeTransactionBoot
|
||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionsIds={transactionsToCategorizeIdsSelected}
|
||||||
>
|
>
|
||||||
<CategorizeTransactionDrawerBody>
|
<CategorizeTransactionDrawerBody>
|
||||||
<CategorizeTransactionForm />
|
<CategorizeTransactionForm />
|
||||||
@@ -18,6 +19,12 @@ export function CategorizeTransactionContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CategorizeTransactionContent = R.compose(
|
||||||
|
withBanking(({ transactionsToCategorizeIdsSelected }) => ({
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
|
})),
|
||||||
|
)(CategorizeTransactionContentRoot);
|
||||||
|
|
||||||
const CategorizeTransactionDrawerBody = styled.div`
|
const CategorizeTransactionDrawerBody = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function CategorizeTransactionFormRoot({
|
|||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
closeMatchingTransactionAside,
|
closeMatchingTransactionAside,
|
||||||
}) {
|
}) {
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot();
|
||||||
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
|
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
|
||||||
|
|
||||||
// Form initial values in create and edit mode.
|
// Form initial values in create and edit mode.
|
||||||
@@ -30,10 +30,10 @@ function CategorizeTransactionFormRoot({
|
|||||||
|
|
||||||
// Callbacks handles form submit.
|
// Callbacks handles form submit.
|
||||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
const transformedValues = tranformToRequest(values);
|
const _values = tranformToRequest(values, uncategorizedTransactionIds);
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
categorizeTransaction([uncategorizedTransactionId, transformedValues])
|
categorizeTransaction(_values)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Box, FFormGroup, FSelect } from '@/components';
|
|||||||
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
||||||
|
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
|
|
||||||
// Retrieves the add money in button options.
|
// Retrieves the add money in button options.
|
||||||
const MoneyInOptions = getAddMoneyInOptions();
|
const MoneyInOptions = getAddMoneyInOptions();
|
||||||
@@ -18,16 +19,18 @@ const Title = styled('h3')`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function CategorizeTransactionFormContent() {
|
export function CategorizeTransactionFormContent() {
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
const { autofillCategorizeValues } = useCategorizeTransactionBoot();
|
||||||
|
|
||||||
const transactionTypes = uncategorizedTransaction?.is_deposit_transaction
|
const transactionTypes = autofillCategorizeValues?.isDepositTransaction
|
||||||
? MoneyInOptions
|
? MoneyInOptions
|
||||||
: MoneyOutOptions;
|
: MoneyOutOptions;
|
||||||
|
|
||||||
|
const formattedAmount = autofillCategorizeValues?.formattedAmount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ flex: 1, margin: 20 }}>
|
<Box style={{ flex: 1, margin: 20 }}>
|
||||||
<FormGroup label={'Amount'} inline>
|
<FormGroup label={'Amount'} inline>
|
||||||
<Title>{uncategorizedTransaction.formatted_amount}</Title>
|
<Title>{formattedAmount}</Title>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FFormGroup name={'category'} label={'Category'} fastField inline>
|
<FFormGroup name={'category'} label={'Category'} fastField inline>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { transformToForm, transfromToSnakeCase } from '@/utils';
|
import { transformToForm, transfromToSnakeCase } from '@/utils';
|
||||||
import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
|
|
||||||
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
|
||||||
|
import { GetAutofillCategorizeTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
// Default initial form values.
|
// Default initial form values.
|
||||||
export const defaultInitialValues = {
|
export const defaultInitialValues = {
|
||||||
@@ -18,48 +18,28 @@ export const defaultInitialValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const transformToCategorizeForm = (
|
export const transformToCategorizeForm = (
|
||||||
uncategorizedTransaction: any,
|
autofillCategorizeTransaction: GetAutofillCategorizeTransaction,
|
||||||
recognizedTransaction?: any,
|
|
||||||
) => {
|
) => {
|
||||||
let defaultValues = {
|
return transformToForm(autofillCategorizeTransaction, defaultInitialValues);
|
||||||
debitAccountId: uncategorizedTransaction.account_id,
|
|
||||||
transactionType: uncategorizedTransaction.is_deposit_transaction
|
|
||||||
? 'other_income'
|
|
||||||
: 'other_expense',
|
|
||||||
amount: uncategorizedTransaction.amount,
|
|
||||||
date: uncategorizedTransaction.date,
|
|
||||||
};
|
|
||||||
if (recognizedTransaction) {
|
|
||||||
const recognizedDefaults = getRecognizedTransactionDefaultValues(
|
|
||||||
recognizedTransaction,
|
|
||||||
);
|
|
||||||
defaultValues = R.merge(defaultValues, recognizedDefaults);
|
|
||||||
}
|
|
||||||
return transformToForm(defaultValues, defaultInitialValues);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRecognizedTransactionDefaultValues = (
|
export const tranformToRequest = (
|
||||||
recognizedTransaction: any,
|
formValues: Record<string, any>,
|
||||||
|
uncategorizedTransactionIds: Array<number>,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
creditAccountId: recognizedTransaction.assignedAccountId || '',
|
uncategorized_transaction_ids: uncategorizedTransactionIds,
|
||||||
// transactionType: recognizedTransaction.assignCategory,
|
...transfromToSnakeCase(formValues),
|
||||||
referenceNo: recognizedTransaction.referenceNo || '',
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tranformToRequest = (formValues: Record<string, any>) => {
|
|
||||||
return transfromToSnakeCase(formValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categorize transaction form initial values.
|
* Categorize transaction form initial values.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useCategorizeTransactionFormInitialValues = () => {
|
export const useCategorizeTransactionFormInitialValues = () => {
|
||||||
const { primaryBranch, recognizedTranasction } =
|
const { primaryBranch, autofillCategorizeValues } =
|
||||||
useCategorizeTransactionBoot();
|
useCategorizeTransactionBoot();
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultInitialValues,
|
...defaultInitialValues,
|
||||||
@@ -68,10 +48,7 @@ export const useCategorizeTransactionFormInitialValues = () => {
|
|||||||
* values such as `notes` come back from the API as null, so remove those
|
* values such as `notes` come back from the API as null, so remove those
|
||||||
* as well.
|
* as well.
|
||||||
*/
|
*/
|
||||||
...transformToCategorizeForm(
|
...transformToCategorizeForm(autofillCategorizeValues),
|
||||||
uncategorizedTransaction,
|
|
||||||
recognizedTranasction,
|
|
||||||
),
|
|
||||||
|
|
||||||
/** Assign the primary branch id as default value. */
|
/** Assign the primary branch id as default value. */
|
||||||
branchId: primaryBranch?.id || null,
|
branchId: primaryBranch?.id || null,
|
||||||
|
|||||||
@@ -19,20 +19,32 @@ function CategorizeTransactionAsideRoot({
|
|||||||
|
|
||||||
// #withBanking
|
// #withBanking
|
||||||
selectedUncategorizedTransactionId,
|
selectedUncategorizedTransactionId,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
enableMultipleCategorization,
|
||||||
}: CategorizeTransactionAsideProps) {
|
}: CategorizeTransactionAsideProps) {
|
||||||
//
|
//
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
|
// Close the reconcile matching form.
|
||||||
closeReconcileMatchingTransaction();
|
closeReconcileMatchingTransaction();
|
||||||
|
|
||||||
|
// Reset the selected transactions to categorize.
|
||||||
|
resetTransactionsToCategorizeSelected();
|
||||||
|
|
||||||
|
// Disable multi matching.
|
||||||
|
enableMultipleCategorization(false);
|
||||||
},
|
},
|
||||||
[closeReconcileMatchingTransaction],
|
[
|
||||||
|
closeReconcileMatchingTransaction,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
closeMatchingTransactionAside();
|
closeMatchingTransactionAside();
|
||||||
};
|
}
|
||||||
const uncategorizedTransactionId = selectedUncategorizedTransactionId;
|
// Cannot continue if there is no selected transactions.;
|
||||||
|
|
||||||
if (!selectedUncategorizedTransactionId) {
|
if (!selectedUncategorizedTransactionId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -40,7 +52,7 @@ function CategorizeTransactionAsideRoot({
|
|||||||
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
|
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
|
||||||
<Aside.Body>
|
<Aside.Body>
|
||||||
<CategorizeTransactionTabsBoot
|
<CategorizeTransactionTabsBoot
|
||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionId={selectedUncategorizedTransactionId}
|
||||||
>
|
>
|
||||||
<CategorizeTransactionTabs />
|
<CategorizeTransactionTabs />
|
||||||
</CategorizeTransactionTabsBoot>
|
</CategorizeTransactionTabsBoot>
|
||||||
@@ -51,7 +63,7 @@ function CategorizeTransactionAsideRoot({
|
|||||||
|
|
||||||
export const CategorizeTransactionAside = R.compose(
|
export const CategorizeTransactionAside = R.compose(
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
withBanking(({ selectedUncategorizedTransactionId }) => ({
|
withBanking(({ transactionsToCategorizeIdsSelected }) => ({
|
||||||
selectedUncategorizedTransactionId,
|
selectedUncategorizedTransactionId: transactionsToCategorizeIdsSelected,
|
||||||
})),
|
})),
|
||||||
)(CategorizeTransactionAsideRoot);
|
)(CategorizeTransactionAsideRoot);
|
||||||
|
|||||||
@@ -2,14 +2,10 @@
|
|||||||
import { Tab, Tabs } from '@blueprintjs/core';
|
import { Tab, Tabs } from '@blueprintjs/core';
|
||||||
import { MatchingBankTransaction } from './MatchingTransaction';
|
import { MatchingBankTransaction } from './MatchingTransaction';
|
||||||
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
import { CategorizeTransactionContent } from '../CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionContent';
|
||||||
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
|
||||||
import styles from './CategorizeTransactionTabs.module.scss';
|
import styles from './CategorizeTransactionTabs.module.scss';
|
||||||
|
|
||||||
export function CategorizeTransactionTabs() {
|
export function CategorizeTransactionTabs() {
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
const defaultSelectedTabId = 'categorize';
|
||||||
const defaultSelectedTabId = uncategorizedTransaction?.is_recognized
|
|
||||||
? 'categorize'
|
|
||||||
: 'matching';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Spinner } from '@blueprintjs/core';
|
import { castArray, uniq } from 'lodash';
|
||||||
import { useUncategorizedTransaction } from '@/hooks/query';
|
|
||||||
|
|
||||||
interface CategorizeTransactionTabsValue {
|
interface CategorizeTransactionTabsValue {
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: Array<number>;
|
||||||
isUncategorizedTransactionLoading: boolean;
|
|
||||||
uncategorizedTransaction: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategorizeTransactionTabsBootProps {
|
interface CategorizeTransactionTabsBootProps {
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionIds: number | Array<number>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,28 +23,23 @@ export function CategorizeTransactionTabsBoot({
|
|||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
children,
|
children,
|
||||||
}: CategorizeTransactionTabsBootProps) {
|
}: CategorizeTransactionTabsBootProps) {
|
||||||
const {
|
const uncategorizedTransactionIds = useMemo(
|
||||||
data: uncategorizedTransaction,
|
() => uniq(castArray(uncategorizedTransactionId)),
|
||||||
isLoading: isUncategorizedTransactionLoading,
|
[uncategorizedTransactionId],
|
||||||
} = useUncategorizedTransaction(uncategorizedTransactionId);
|
);
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionIds,
|
||||||
uncategorizedTransaction,
|
|
||||||
isUncategorizedTransactionLoading,
|
|
||||||
};
|
};
|
||||||
const isLoading = isUncategorizedTransactionLoading;
|
// Use a key prop to force re-render of children when `uncategorizedTransactionIds` changes
|
||||||
|
|
||||||
// Use a key prop to force re-render of children when uncategorizedTransactionId changes
|
|
||||||
const childrenPerKey = React.useMemo(() => {
|
const childrenPerKey = React.useMemo(() => {
|
||||||
return React.Children.map(children, (child) =>
|
return React.Children.map(children, (child) =>
|
||||||
React.cloneElement(child, { key: uncategorizedTransactionId }),
|
React.cloneElement(child, {
|
||||||
|
key: uncategorizedTransactionIds?.join(','),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}, [children, uncategorizedTransactionId]);
|
}, [children, uncategorizedTransactionIds]);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner size={30} />;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<CategorizeTransactionTabsBootContext.Provider value={provider}>
|
<CategorizeTransactionTabsBootContext.Provider value={provider}>
|
||||||
{childrenPerKey}
|
{childrenPerKey}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import {
|
|||||||
WithBankingActionsProps,
|
WithBankingActionsProps,
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
} from '../withBankingActions';
|
} from '../withBankingActions';
|
||||||
import styles from './CategorizeTransactionAside.module.scss';
|
|
||||||
import { MatchingReconcileTransactionForm } from './MatchingReconcileTransactionAside/MatchingReconcileTransactionForm';
|
|
||||||
import { withBanking } from '../withBanking';
|
import { withBanking } from '../withBanking';
|
||||||
|
import { MatchingReconcileTransactionForm } from './MatchingReconcileTransactionAside/MatchingReconcileTransactionForm';
|
||||||
|
import styles from './CategorizeTransactionAside.module.scss';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
matched: {},
|
matched: {},
|
||||||
@@ -40,8 +40,11 @@ const initialValues = {
|
|||||||
function MatchingBankTransactionRoot({
|
function MatchingBankTransactionRoot({
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
closeMatchingTransactionAside,
|
closeMatchingTransactionAside,
|
||||||
|
|
||||||
|
// #withBanking
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
}) {
|
}) {
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot();
|
||||||
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
||||||
|
|
||||||
// Handles the form submitting.
|
// Handles the form submitting.
|
||||||
@@ -49,7 +52,7 @@ function MatchingBankTransactionRoot({
|
|||||||
values: MatchingTransactionFormValues,
|
values: MatchingTransactionFormValues,
|
||||||
{ setSubmitting }: FormikHelpers<MatchingTransactionFormValues>,
|
{ setSubmitting }: FormikHelpers<MatchingTransactionFormValues>,
|
||||||
) => {
|
) => {
|
||||||
const _values = transformToReq(values);
|
const _values = transformToReq(values, uncategorizedTransactionIds);
|
||||||
|
|
||||||
if (_values.matchedTransactions?.length === 0) {
|
if (_values.matchedTransactions?.length === 0) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -59,7 +62,7 @@ function MatchingBankTransactionRoot({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
matchTransaction({ id: uncategorizedTransactionId, value: _values })
|
matchTransaction(_values)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
@@ -78,7 +81,7 @@ function MatchingBankTransactionRoot({
|
|||||||
message: `The total amount does not equal the uncategorized transaction.`,
|
message: `The total amount does not equal the uncategorized transaction.`,
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
|
setSubmitting(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -91,7 +94,7 @@ function MatchingBankTransactionRoot({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MatchingTransactionBoot
|
<MatchingTransactionBoot
|
||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionsIds={uncategorizedTransactionIds}
|
||||||
>
|
>
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
<MatchingBankTransactionFormContent />
|
<MatchingBankTransactionFormContent />
|
||||||
@@ -100,9 +103,12 @@ function MatchingBankTransactionRoot({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchingBankTransaction = R.compose(withBankingActions)(
|
export const MatchingBankTransaction = R.compose(
|
||||||
MatchingBankTransactionRoot,
|
withBankingActions,
|
||||||
);
|
withBanking(({ transactionsToCategorizeIdsSelected }) => ({
|
||||||
|
transactionsToCategorizeIdsSelected,
|
||||||
|
})),
|
||||||
|
)(MatchingBankTransactionRoot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matching bank transaction form content.
|
* Matching bank transaction form content.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface MatchingTransactionBootValues {
|
|||||||
possibleMatches: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
perfectMatchesCount: number;
|
perfectMatchesCount: number;
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
|
totalPending: number;
|
||||||
matches: Array<any>;
|
matches: Array<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,12 +19,12 @@ const RuleFormBootContext = createContext<MatchingTransactionBootValues>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface RuleFormBootProps {
|
interface RuleFormBootProps {
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransactionsIds: Array<number>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MatchingTransactionBoot({
|
function MatchingTransactionBoot({
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionsIds,
|
||||||
...props
|
...props
|
||||||
}: RuleFormBootProps) {
|
}: RuleFormBootProps) {
|
||||||
const {
|
const {
|
||||||
@@ -31,11 +32,12 @@ function MatchingTransactionBoot({
|
|||||||
isLoading: isMatchingTransactionsLoading,
|
isLoading: isMatchingTransactionsLoading,
|
||||||
isFetching: isMatchingTransactionsFetching,
|
isFetching: isMatchingTransactionsFetching,
|
||||||
isSuccess: isMatchingTransactionsSuccess,
|
isSuccess: isMatchingTransactionsSuccess,
|
||||||
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
|
} = useGetBankTransactionsMatches(uncategorizedTransactionsIds);
|
||||||
|
|
||||||
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
||||||
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
||||||
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
||||||
|
const totalPending = defaultTo(matchingTransactions?.totalPending, 0);
|
||||||
|
|
||||||
const matches = R.concat(perfectMatches, possibleMatches);
|
const matches = R.concat(perfectMatches, possibleMatches);
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ function MatchingTransactionBoot({
|
|||||||
possibleMatches,
|
possibleMatches,
|
||||||
perfectMatchesCount,
|
perfectMatchesCount,
|
||||||
perfectMatches,
|
perfectMatches,
|
||||||
|
totalPending,
|
||||||
matches,
|
matches,
|
||||||
} as MatchingTransactionBootValues;
|
} as MatchingTransactionBootValues;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { useMatchingTransactionBoot } from './MatchingTransactionBoot';
|
|||||||
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
import { useCategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export const transformToReq = (values: MatchingTransactionFormValues) => {
|
export const transformToReq = (
|
||||||
|
values: MatchingTransactionFormValues,
|
||||||
|
uncategorizedTransactions: Array<number>,
|
||||||
|
) => {
|
||||||
const matchedTransactions = Object.entries(values.matched)
|
const matchedTransactions = Object.entries(values.matched)
|
||||||
.filter(([key, value]) => value)
|
.filter(([key, value]) => value)
|
||||||
.map(([key]) => {
|
.map(([key]) => {
|
||||||
@@ -12,14 +15,13 @@ export const transformToReq = (values: MatchingTransactionFormValues) => {
|
|||||||
|
|
||||||
return { reference_type, reference_id: parseInt(reference_id, 10) };
|
return { reference_type, reference_id: parseInt(reference_id, 10) };
|
||||||
});
|
});
|
||||||
|
return { matchedTransactions, uncategorizedTransactions };
|
||||||
return { matchedTransactions };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetPendingAmountMatched = () => {
|
export const useGetPendingAmountMatched = () => {
|
||||||
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
const { values } = useFormikContext<MatchingTransactionFormValues>();
|
||||||
const { perfectMatches, possibleMatches } = useMatchingTransactionBoot();
|
const { perfectMatches, possibleMatches, totalPending } =
|
||||||
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
|
useMatchingTransactionBoot();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||||
@@ -34,11 +36,10 @@ export const useGetPendingAmountMatched = () => {
|
|||||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const amount = uncategorizedTransaction.amount;
|
const pendingAmount = totalPending - totalMatchedAmount;
|
||||||
const pendingAmount = amount - totalMatchedAmount;
|
|
||||||
|
|
||||||
return pendingAmount;
|
return pendingAmount;
|
||||||
}, [uncategorizedTransaction, perfectMatches, possibleMatches, values]);
|
}, [totalPending, perfectMatches, possibleMatches, values]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAtleastOneMatchedSelected = () => {
|
export const useAtleastOneMatchedSelected = () => {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export const withBanking = (mapState) => {
|
|||||||
state.plaid.uncategorizedTransactionsSelected,
|
state.plaid.uncategorizedTransactionsSelected,
|
||||||
|
|
||||||
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
|
excludedTransactionsIdsSelected: state.plaid.excludedTransactionsSelected,
|
||||||
|
enableMultipleCategorization: state.plaid.enableMultipleCategorization,
|
||||||
|
|
||||||
|
transactionsToCategorizeIdsSelected:
|
||||||
|
state.plaid.transactionsToCategorizeSelected,
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import {
|
|||||||
resetUncategorizedTransactionsSelected,
|
resetUncategorizedTransactionsSelected,
|
||||||
resetExcludedTransactionsSelected,
|
resetExcludedTransactionsSelected,
|
||||||
setExcludedTransactionsSelected,
|
setExcludedTransactionsSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
|
enableMultipleCategorization,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
} from '@/store/banking/banking.reducer';
|
} from '@/store/banking/banking.reducer';
|
||||||
|
|
||||||
export interface WithBankingActionsProps {
|
export interface WithBankingActionsProps {
|
||||||
@@ -23,6 +28,13 @@ export interface WithBankingActionsProps {
|
|||||||
|
|
||||||
setExcludedTransactionsSelected: (ids: Array<string | number>) => void;
|
setExcludedTransactionsSelected: (ids: Array<string | number>) => void;
|
||||||
resetExcludedTransactionsSelected: () => void;
|
resetExcludedTransactionsSelected: () => void;
|
||||||
|
|
||||||
|
setTransactionsToCategorizeSelected: (ids: Array<string | number>) => void;
|
||||||
|
addTransactionsToCategorizeSelected: (id: string | number) => void;
|
||||||
|
removeTransactionsToCategorizeSelected: (id: string | number) => void;
|
||||||
|
resetTransactionsToCategorizeSelected: () => void;
|
||||||
|
|
||||||
|
enableMultipleCategorization: (enable: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
||||||
@@ -41,7 +53,7 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected uncategorized transactions.
|
* Sets the selected uncategorized transactions.
|
||||||
* @param {Array<string | number>} ids
|
* @param {Array<string | number>} ids
|
||||||
*/
|
*/
|
||||||
setUncategorizedTransactionsSelected: (ids: Array<string | number>) =>
|
setUncategorizedTransactionsSelected: (ids: Array<string | number>) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -68,10 +80,46 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
),
|
),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the excluded selected transactions
|
* Resets the excluded selected transactions.
|
||||||
*/
|
*/
|
||||||
resetExcludedTransactionsSelected: () =>
|
resetExcludedTransactionsSelected: () =>
|
||||||
dispatch(resetExcludedTransactionsSelected()),
|
dispatch(resetExcludedTransactionsSelected()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected transactions to categorize or match.
|
||||||
|
* @param {Array<string | number>} ids
|
||||||
|
*/
|
||||||
|
setTransactionsToCategorizeSelected: (ids: Array<string | number>) =>
|
||||||
|
dispatch(setTransactionsToCategorizeSelected({ ids })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds selected transactions to categorize.
|
||||||
|
* @param {string | number} id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
addTransactionsToCategorizeSelected: (id: string | number) =>
|
||||||
|
dispatch(addTransactionsToCategorizeSelected({ id })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the selected transactions.
|
||||||
|
* @param {string | number} id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
removeTransactionsToCategorizeSelected: (id: string | number) =>
|
||||||
|
dispatch(removeTransactionsToCategorizeSelected({ id })),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the selected transactions to categorize or match.
|
||||||
|
*/
|
||||||
|
resetTransactionsToCategorizeSelected: () =>
|
||||||
|
dispatch(resetTransactionsToCategorizeSelected()),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables/Disables the multiple selection to categorize or match.
|
||||||
|
* @param {boolean} enable
|
||||||
|
*/
|
||||||
|
enableMultipleCategorization: (enable: boolean) =>
|
||||||
|
dispatch(enableMultipleCategorization({ enable })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withBankingActions = connect<
|
export const withBankingActions = connect<
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const QUERY_KEY = {
|
|||||||
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
|
||||||
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
||||||
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
||||||
|
AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
|
||||||
};
|
};
|
||||||
|
|
||||||
const commonInvalidateQueries = (query: QueryClient) => {
|
const commonInvalidateQueries = (query: QueryClient) => {
|
||||||
@@ -238,10 +239,13 @@ export function useBankRule(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetBankTransactionsMatchesValue = number;
|
interface GetBankTransactionsMatchesValue {
|
||||||
|
uncategorizeTransactionsIds: Array<number>;
|
||||||
|
}
|
||||||
interface GetBankTransactionsMatchesResponse {
|
interface GetBankTransactionsMatchesResponse {
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
possibleMatches: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
|
totalPending: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -250,16 +254,18 @@ interface GetBankTransactionsMatchesResponse {
|
|||||||
* @returns {UseQueryResult<GetBankTransactionsMatchesResponse, Error>}
|
* @returns {UseQueryResult<GetBankTransactionsMatchesResponse, Error>}
|
||||||
*/
|
*/
|
||||||
export function useGetBankTransactionsMatches(
|
export function useGetBankTransactionsMatches(
|
||||||
uncategorizedTransactionId: number,
|
uncategorizeTransactionsIds: Array<number>,
|
||||||
options?: UseQueryOptions<GetBankTransactionsMatchesResponse, Error>,
|
options?: UseQueryOptions<GetBankTransactionsMatchesResponse, Error>,
|
||||||
): UseQueryResult<GetBankTransactionsMatchesResponse, Error> {
|
): UseQueryResult<GetBankTransactionsMatchesResponse, Error> {
|
||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useQuery<GetBankTransactionsMatchesResponse, Error>(
|
return useQuery<GetBankTransactionsMatchesResponse, Error>(
|
||||||
[QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizedTransactionId],
|
[QUERY_KEY.BANK_TRANSACTION_MATCHES, uncategorizeTransactionsIds],
|
||||||
() =>
|
() =>
|
||||||
apiRequest
|
apiRequest
|
||||||
.get(`/cashflow/transactions/${uncategorizedTransactionId}/matches`)
|
.get(`/cashflow/transactions/matches`, {
|
||||||
|
params: { uncategorizeTransactionsIds },
|
||||||
|
})
|
||||||
.then((res) => transformToCamelCase(res.data)),
|
.then((res) => transformToCamelCase(res.data)),
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
@@ -437,8 +443,8 @@ export function useUnexcludeUncategorizedTransactions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MatchUncategorizedTransactionValues {
|
interface MatchUncategorizedTransactionValues {
|
||||||
id: number;
|
uncategorizedTransactions: Array<number>;
|
||||||
value: any;
|
matchedTransactions: Array<{ reference_type: string; reference_id: number }>;
|
||||||
}
|
}
|
||||||
interface MatchUncategorizedTransactionRes {}
|
interface MatchUncategorizedTransactionRes {}
|
||||||
|
|
||||||
@@ -465,7 +471,7 @@ export function useMatchUncategorizedTransaction(
|
|||||||
MatchUncategorizedTransactionRes,
|
MatchUncategorizedTransactionRes,
|
||||||
Error,
|
Error,
|
||||||
MatchUncategorizedTransactionValues
|
MatchUncategorizedTransactionValues
|
||||||
>(({ id, value }) => apiRequest.post(`/banking/matches/${id}`, value), {
|
>((value) => apiRequest.post('/banking/matches/match', value), {
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
@@ -581,6 +587,42 @@ export function useGetBankAccountSummaryMeta(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetAutofillCategorizeTransaction {
|
||||||
|
accountId: number | null;
|
||||||
|
amount: number;
|
||||||
|
category: string | null;
|
||||||
|
date: Date;
|
||||||
|
formattedAmount: string;
|
||||||
|
formattedDate: string;
|
||||||
|
isRecognized: boolean;
|
||||||
|
recognizedByRuleId: number | null;
|
||||||
|
recognizedByRuleName: string | null;
|
||||||
|
referenceNo: null | string;
|
||||||
|
isDepositTransaction: boolean;
|
||||||
|
isWithdrawalTransaction: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAutofillCategorizeTransaction(
|
||||||
|
uncategorizedTransactionIds: number[],
|
||||||
|
options: any,
|
||||||
|
) {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetAutofillCategorizeTransaction, Error>(
|
||||||
|
[
|
||||||
|
QUERY_KEY.AUTOFILL_CATEGORIZE_BANK_TRANSACTION,
|
||||||
|
uncategorizedTransactionIds,
|
||||||
|
],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/banking/categorize/autofill`, {
|
||||||
|
params: { uncategorizedTransactionIds },
|
||||||
|
})
|
||||||
|
.then((res) => transformToCamelCase(res.data?.data)),
|
||||||
|
{ ...options },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -243,8 +243,7 @@ export function useCategorizeTransaction(props) {
|
|||||||
const apiRequest = useApiRequest();
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
([id, values]) =>
|
(values) => apiRequest.post(`cashflow/transactions/categorize`, values),
|
||||||
apiRequest.post(`cashflow/transactions/${id}/categorize`, values),
|
|
||||||
{
|
{
|
||||||
onSuccess: (res, id) => {
|
onSuccess: (res, id) => {
|
||||||
// Invalidate queries.
|
// Invalidate queries.
|
||||||
@@ -279,7 +278,6 @@ export function useUncategorizeTransaction(props) {
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invalidate bank account summary.
|
// Invalidate bank account summary.
|
||||||
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
getPlaidToken,
|
getPlaidToken,
|
||||||
setPlaidId,
|
setPlaidId,
|
||||||
resetPlaidId,
|
resetPlaidId,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
getTransactionsToCategorizeSelected,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
|
getOpenMatchingTransactionAside,
|
||||||
|
getTransactionsToCategorizeIdsSelected,
|
||||||
} from '@/store/banking/banking.reducer';
|
} from '@/store/banking/banking.reducer';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
export const useSetBankingPlaidToken = () => {
|
export const useSetBankingPlaidToken = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -30,3 +37,76 @@ export const useResetBankingPlaidToken = () => {
|
|||||||
dispatch(resetPlaidId());
|
dispatch(resetPlaidId());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetTransactionsToCategorizeSelected = () => {
|
||||||
|
const selectedTransactions = useSelector(getTransactionsToCategorizeSelected);
|
||||||
|
|
||||||
|
return useMemo(() => selectedTransactions, [selectedTransactions]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetTransactionsToCategorizeSelected = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(ids: Array<string | number>) => {
|
||||||
|
return dispatch(setTransactionsToCategorizeSelected({ ids }));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddTransactionsToCategorizeSelected = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(id: string | number) => {
|
||||||
|
return dispatch(addTransactionsToCategorizeSelected({ id }));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveTransactionsToCategorizeSelected = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(id: string | number) => {
|
||||||
|
return dispatch(removeTransactionsToCategorizeSelected({ id }));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetTransactionsToCategorizeSelected = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return useCallback(() => {
|
||||||
|
dispatch(resetTransactionsToCategorizeSelected());
|
||||||
|
}, [dispatch]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetOpenMatchingTransactionAside = () => {
|
||||||
|
const openMatchingTransactionAside = useSelector(
|
||||||
|
getOpenMatchingTransactionAside,
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => openMatchingTransactionAside,
|
||||||
|
[openMatchingTransactionAside],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the detarmined value whether the given transaction id is checked.
|
||||||
|
* @param {number} transactionId
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const useIsTransactionToCategorizeSelected = (transactionId: number) => {
|
||||||
|
const transactionsToCategorizeIdsSelected = useSelector(
|
||||||
|
getTransactionsToCategorizeIdsSelected,
|
||||||
|
);
|
||||||
|
return useMemo(
|
||||||
|
() => transactionsToCategorizeIdsSelected.indexOf(transactionId) !== -1,
|
||||||
|
[transactionsToCategorizeIdsSelected, transactionId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { castArray, uniq } from 'lodash';
|
||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
interface StorePlaidState {
|
interface StorePlaidState {
|
||||||
@@ -8,6 +9,9 @@ interface StorePlaidState {
|
|||||||
|
|
||||||
uncategorizedTransactionsSelected: Array<number | string>;
|
uncategorizedTransactionsSelected: Array<number | string>;
|
||||||
excludedTransactionsSelected: Array<number | string>;
|
excludedTransactionsSelected: Array<number | string>;
|
||||||
|
transactionsToCategorizeSelected: Array<number | string>;
|
||||||
|
|
||||||
|
enableMultipleCategorization: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaidSlice = createSlice({
|
export const PlaidSlice = createSlice({
|
||||||
@@ -22,6 +26,8 @@ export const PlaidSlice = createSlice({
|
|||||||
},
|
},
|
||||||
uncategorizedTransactionsSelected: [],
|
uncategorizedTransactionsSelected: [],
|
||||||
excludedTransactionsSelected: [],
|
excludedTransactionsSelected: [],
|
||||||
|
transactionsToCategorizeSelected: [],
|
||||||
|
enableMultipleCategorization: false,
|
||||||
} as StorePlaidState,
|
} as StorePlaidState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
||||||
@@ -97,6 +103,79 @@ export const PlaidSlice = createSlice({
|
|||||||
resetExcludedTransactionsSelected: (state: StorePlaidState) => {
|
resetExcludedTransactionsSelected: (state: StorePlaidState) => {
|
||||||
state.excludedTransactionsSelected = [];
|
state.excludedTransactionsSelected = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected transactions to categorize or match.
|
||||||
|
* @param {StorePlaidState} state
|
||||||
|
* @param {PayloadAction<{ ids: Array<string | number> }>} action
|
||||||
|
*/
|
||||||
|
setTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ ids: Array<string | number> }>,
|
||||||
|
) => {
|
||||||
|
const ids = castArray(action.payload.ids);
|
||||||
|
|
||||||
|
state.transactionsToCategorizeSelected = ids;
|
||||||
|
state.openMatchingTransactionAside = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a transaction to selected transactions to categorize or match.
|
||||||
|
* @param {StorePlaidState} state
|
||||||
|
* @param {PayloadAction<{ id: string | number }>} action
|
||||||
|
*/
|
||||||
|
addTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ id: string | number }>,
|
||||||
|
) => {
|
||||||
|
state.transactionsToCategorizeSelected = uniq([
|
||||||
|
...state.transactionsToCategorizeSelected,
|
||||||
|
action.payload.id,
|
||||||
|
]);
|
||||||
|
state.openMatchingTransactionAside = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a transaction from the selected transactions to categorize or match.
|
||||||
|
* @param {StorePlaidState} state
|
||||||
|
* @param {PayloadAction<{ id: string | number }>} action
|
||||||
|
*/
|
||||||
|
removeTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ id: string | number }>,
|
||||||
|
) => {
|
||||||
|
state.transactionsToCategorizeSelected =
|
||||||
|
state.transactionsToCategorizeSelected.filter(
|
||||||
|
(t) => t !== action.payload.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state.transactionsToCategorizeSelected.length === 0) {
|
||||||
|
state.openMatchingTransactionAside = false;
|
||||||
|
} else {
|
||||||
|
state.openMatchingTransactionAside = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the selected transactions to categorize or match.
|
||||||
|
* @param {StorePlaidState} state
|
||||||
|
*/
|
||||||
|
resetTransactionsToCategorizeSelected: (state: StorePlaidState) => {
|
||||||
|
state.transactionsToCategorizeSelected = [];
|
||||||
|
state.openMatchingTransactionAside = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables/Disables the multiple selection to categorize or match.
|
||||||
|
* @param {StorePlaidState} state
|
||||||
|
* @param {PayloadAction<{ enable: boolean }>} action
|
||||||
|
*/
|
||||||
|
enableMultipleCategorization: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ enable: boolean }>,
|
||||||
|
) => {
|
||||||
|
state.enableMultipleCategorization = action.payload.enable;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,6 +190,22 @@ export const {
|
|||||||
resetUncategorizedTransactionsSelected,
|
resetUncategorizedTransactionsSelected,
|
||||||
setExcludedTransactionsSelected,
|
setExcludedTransactionsSelected,
|
||||||
resetExcludedTransactionsSelected,
|
resetExcludedTransactionsSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
enableMultipleCategorization,
|
||||||
} = PlaidSlice.actions;
|
} = PlaidSlice.actions;
|
||||||
|
|
||||||
export const getPlaidToken = (state: any) => state.plaid.plaidToken;
|
export const getPlaidToken = (state: any) => state.plaid.plaidToken;
|
||||||
|
export const getTransactionsToCategorizeSelected = (state: any) =>
|
||||||
|
state.plaid.transactionsToCategorizeSelected;
|
||||||
|
|
||||||
|
export const getOpenMatchingTransactionAside = (state: any) =>
|
||||||
|
state.plaid.openMatchingTransactionAside;
|
||||||
|
|
||||||
|
export const isMultipleCategorization = (state: any) =>
|
||||||
|
state.plaid.enableMultipleCategorization;
|
||||||
|
|
||||||
|
export const getTransactionsToCategorizeIdsSelected = (state: any) =>
|
||||||
|
state.plaid.transactionsToCategorizeSelected;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp4-control.bp4-checkbox .bp4-control-indicator {
|
.bp4-control.bp4-checkbox:not(.bp4-large) .bp4-control-indicator {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
|
||||||
&,
|
&,
|
||||||
|
|||||||
Reference in New Issue
Block a user