mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: bulk categorizing 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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
@@ -191,14 +197,18 @@ 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.',
|
||||||
|
|||||||
@@ -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,14 +130,15 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
|
|
||||||
export interface ICashflowTransactionCategorizedPayload {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransaction: any;
|
uncategorizedTransactions: any;
|
||||||
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;
|
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizedPayload {
|
export interface ICashflowTransactionUncategorizedPayload {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -45,22 +46,26 @@ export abstract class GetMatchedTransactionsByType {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @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 {
|
||||||
@@ -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,15 @@ 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,
|
||||||
|
validateUncategorizedTransactionsExcluded,
|
||||||
|
validateUncategorizedTransactionsNotMatched,
|
||||||
|
} from './_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -39,27 +43,24 @@ 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();
|
|
||||||
|
|
||||||
// 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 =
|
||||||
@@ -96,9 +97,9 @@ export class MatchBankTransactions {
|
|||||||
);
|
);
|
||||||
// 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 (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
|
||||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
// throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,23 +110,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 +140,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,7 +1,9 @@
|
|||||||
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 } from 'lodash';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
export const sortClosestMatchTransactions = (
|
export const sortClosestMatchTransactions = (
|
||||||
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
||||||
@@ -29,3 +31,26 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateUncategorizedTransactionsNotMatched = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
) => {
|
||||||
|
const isMatchedTransactions = uncategorizedTransactions.filter(
|
||||||
|
(trans) => !isEmpty(trans.matchedBankTransactions)
|
||||||
|
);
|
||||||
|
//
|
||||||
|
if (isMatchedTransactions.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateUncategorizedTransactionsExcluded = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
) => {
|
||||||
|
const excludedTransactions = uncategorizedTransactions.filter(
|
||||||
|
(trans) => trans.excluded
|
||||||
|
);
|
||||||
|
if (excludedTransactions.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,23 @@ 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 transactions = await UncategorizedCashflowTransaction.query().whereIn(
|
||||||
uncategorizedTransactionId
|
'id',
|
||||||
|
uncategorizedTransactionIds
|
||||||
);
|
);
|
||||||
await Account.query(trx)
|
await PromisePool.withConcurrency(1)
|
||||||
.findById(transaction.accountId)
|
.for(transactions)
|
||||||
.decrement('uncategorizedTransactions', 1);
|
.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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,29 +41,31 @@ 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(
|
||||||
|
oldIncategorizedTransactions
|
||||||
|
);
|
||||||
// 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,
|
// uncategorizedTransactions,
|
||||||
categorizeDTO.transactionType
|
// categorizeDTO.transactionType
|
||||||
);
|
// );
|
||||||
// Edits the cashflow transaction under UOW env.
|
// Edits the cashflow transaction under UOW env.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onTransactionCategorizing` event.
|
// Triggers `onTransactionCategorizing` event.
|
||||||
@@ -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.
|
||||||
@@ -84,22 +89,28 @@ export class CategorizeCashflowTransaction {
|
|||||||
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(
|
||||||
events.cashflow.onTransactionCategorized,
|
events.cashflow.onTransactionCategorized,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransaction,
|
cashflowTransaction,
|
||||||
uncategorizedTransaction,
|
uncategorizedTransactions,
|
||||||
|
oldUncategorizedTransactions,
|
||||||
categorizeDTO,
|
categorizeDTO,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionCategorizedPayload
|
} as ICashflowTransactionCategorizedPayload
|
||||||
|
|||||||
@@ -68,10 +68,12 @@ 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);
|
||||||
|
|
||||||
|
if (categorized?.length > 0) {
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ export class CommandCashflowValidator {
|
|||||||
transactionType: string
|
transactionType: string
|
||||||
) {
|
) {
|
||||||
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 &&
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { upperFirst, camelCase } from 'lodash';
|
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash';
|
||||||
import {
|
import {
|
||||||
|
CASHFLOW_DIRECTION,
|
||||||
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 +11,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 +31,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 +52,35 @@ 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ import {
|
|||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
||||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
import {
|
||||||
|
useAddTransactionsToCategorizeSelected,
|
||||||
|
useRemoveTransactionsToCategorizeSelected,
|
||||||
|
} from '@/hooks/state/banking';
|
||||||
|
|
||||||
export function ActionsMenu({
|
export function ActionsMenu({
|
||||||
payload: { onUncategorize, onUnmatch },
|
payload: { onUncategorize, onUnmatch },
|
||||||
@@ -183,6 +188,20 @@ function statusAccessor(transaction) {
|
|||||||
* Retrieve account uncategorized transctions table columns.
|
* Retrieve account uncategorized transctions table columns.
|
||||||
*/
|
*/
|
||||||
export function useAccountUncategorizedTransactionsColumns() {
|
export function useAccountUncategorizedTransactionsColumns() {
|
||||||
|
const addTransactionsToCategorizeSelected =
|
||||||
|
useAddTransactionsToCategorizeSelected();
|
||||||
|
|
||||||
|
const removeTransactionsToCategorizeSelected =
|
||||||
|
useRemoveTransactionsToCategorizeSelected();
|
||||||
|
|
||||||
|
const handleChange = (value) => (event) => {
|
||||||
|
if (event.currentTarget.checked) {
|
||||||
|
addTransactionsToCategorizeSelected(value.id);
|
||||||
|
} else {
|
||||||
|
removeTransactionsToCategorizeSelected(value.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return React.useMemo(
|
return React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -242,6 +261,15 @@ export function useAccountUncategorizedTransactionsColumns() {
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'categorize_include',
|
||||||
|
Header: 'Include',
|
||||||
|
accessor: (value) => <Checkbox large onChange={handleChange(value)} />,
|
||||||
|
width: 10,
|
||||||
|
minWidth: 10,
|
||||||
|
maxWidth: 10,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
resetUncategorizedTransactionsSelected,
|
resetUncategorizedTransactionsSelected,
|
||||||
resetExcludedTransactionsSelected,
|
resetExcludedTransactionsSelected,
|
||||||
setExcludedTransactionsSelected,
|
setExcludedTransactionsSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
} from '@/store/banking/banking.reducer';
|
} from '@/store/banking/banking.reducer';
|
||||||
|
|
||||||
export interface WithBankingActionsProps {
|
export interface WithBankingActionsProps {
|
||||||
@@ -23,6 +25,9 @@ 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;
|
||||||
|
resetTransactionsToCategorizeSelected: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
||||||
@@ -56,6 +61,11 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
),
|
),
|
||||||
resetExcludedTransactionsSelected: () =>
|
resetExcludedTransactionsSelected: () =>
|
||||||
dispatch(resetExcludedTransactionsSelected()),
|
dispatch(resetExcludedTransactionsSelected()),
|
||||||
|
|
||||||
|
setTransactionsToCategorizeSelected: (ids: Array<string | number>) =>
|
||||||
|
dispatch(setTransactionsToCategorizeSelected({ ids })),
|
||||||
|
resetTransactionsToCategorizeSelected: () =>
|
||||||
|
dispatch(resetTransactionsToCategorizeSelected()),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const withBankingActions = connect<
|
export const withBankingActions = connect<
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
getPlaidToken,
|
getPlaidToken,
|
||||||
setPlaidId,
|
setPlaidId,
|
||||||
resetPlaidId,
|
resetPlaidId,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
|
getTransactionsToCategorizeSelected,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
} 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 +35,50 @@ 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]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { uniq } from 'lodash';
|
||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
interface StorePlaidState {
|
interface StorePlaidState {
|
||||||
@@ -8,6 +9,7 @@ interface StorePlaidState {
|
|||||||
|
|
||||||
uncategorizedTransactionsSelected: Array<number | string>;
|
uncategorizedTransactionsSelected: Array<number | string>;
|
||||||
excludedTransactionsSelected: Array<number | string>;
|
excludedTransactionsSelected: Array<number | string>;
|
||||||
|
transactionsToCategorizeSelected: Array<number | string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaidSlice = createSlice({
|
export const PlaidSlice = createSlice({
|
||||||
@@ -22,6 +24,7 @@ export const PlaidSlice = createSlice({
|
|||||||
},
|
},
|
||||||
uncategorizedTransactionsSelected: [],
|
uncategorizedTransactionsSelected: [],
|
||||||
excludedTransactionsSelected: [],
|
excludedTransactionsSelected: [],
|
||||||
|
transactionsToCategorizeSelected: [],
|
||||||
} as StorePlaidState,
|
} as StorePlaidState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
||||||
@@ -79,6 +82,37 @@ export const PlaidSlice = createSlice({
|
|||||||
resetExcludedTransactionsSelected: (state: StorePlaidState) => {
|
resetExcludedTransactionsSelected: (state: StorePlaidState) => {
|
||||||
state.excludedTransactionsSelected = [];
|
state.excludedTransactionsSelected = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ ids: Array<string | number> }>,
|
||||||
|
) => {
|
||||||
|
state.transactionsToCategorizeSelected = action.payload.ids;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ id: string | number }>,
|
||||||
|
) => {
|
||||||
|
state.transactionsToCategorizeSelected = uniq([
|
||||||
|
...state.transactionsToCategorizeSelected,
|
||||||
|
action.payload.id,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTransactionsToCategorizeSelected: (
|
||||||
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ id: string | number }>,
|
||||||
|
) => {
|
||||||
|
state.transactionsToCategorizeSelected =
|
||||||
|
state.transactionsToCategorizeSelected.filter(
|
||||||
|
(t) => t !== action.payload.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetTransactionsToCategorizeSelected: (state: StorePlaidState) => {
|
||||||
|
state.transactionsToCategorizeSelected = [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,6 +127,12 @@ export const {
|
|||||||
resetUncategorizedTransactionsSelected,
|
resetUncategorizedTransactionsSelected,
|
||||||
setExcludedTransactionsSelected,
|
setExcludedTransactionsSelected,
|
||||||
resetExcludedTransactionsSelected,
|
resetExcludedTransactionsSelected,
|
||||||
|
setTransactionsToCategorizeSelected,
|
||||||
|
addTransactionsToCategorizeSelected,
|
||||||
|
removeTransactionsToCategorizeSelected,
|
||||||
|
resetTransactionsToCategorizeSelected,
|
||||||
} = 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user