Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
8f30c86f5f fizx: remove debug console.log 2024-07-30 21:54:55 +02:00
135 changed files with 902 additions and 2805 deletions

View File

@@ -132,24 +132,6 @@
"contributions": [ "contributions": [
"bug" "bug"
] ]
},
{
"login": "oleynikd",
"name": "Denis",
"avatar_url": "https://avatars.githubusercontent.com/u/3976868?v=4",
"profile": "https://github.com/oleynikd",
"contributions": [
"bug"
]
},
{
"login": "mittalsam98",
"name": "Sachin Mittal",
"avatar_url": "https://avatars.githubusercontent.com/u/42431274?v=4",
"profile": "https://myself.vercel.app/",
"contributions": [
"bug"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -126,10 +126,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
</tr> </tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@@ -250,12 +250,10 @@ export class AttachmentsController extends BaseController {
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<Response | void> { ): Promise<Response | void> {
const { tenantId } = req;
const { id: documentKey } = req.params; const { id: documentKey } = req.params;
try { try {
const presignedUrl = await this.attachmentsApplication.getPresignedUrl( const presignedUrl = await this.attachmentsApplication.getPresignedUrl(
tenantId,
documentKey documentKey
); );
return res.status(200).send({ presignedUrl }); return res.status(200).send({ presignedUrl });

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
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 { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication'; import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
import { param } from 'express-validator';
@Service() @Service()
export class BankAccountsController extends BaseController { export class BankAccountsController extends BaseController {
@@ -25,22 +25,6 @@ export class BankAccountsController extends BaseController {
this.disconnectBankAccount.bind(this) this.disconnectBankAccount.bind(this)
); );
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post(
'/:bankAccountId/pause_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.pauseBankAccountFeeds.bind(this)
);
router.post(
'/:bankAccountId/resume_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
this.validationResult,
this.resumeBankAccountFeeds.bind(this)
);
return router; return router;
} }
@@ -125,58 +109,4 @@ export class BankAccountsController extends BaseController {
next(error); next(error);
} }
} }
/**
* Resumes the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async resumeBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been resumed.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
/**
* Pauses the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async pauseBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been paused.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
} }

View File

@@ -1,8 +1,12 @@
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 {
@@ -16,17 +20,9 @@ export class BankTransactionsMatchingController extends BaseController {
const router = Router(); const router = Router();
router.post( router.post(
'/unmatch/:transactionId', '/:transactionId',
[param('transactionId').exists()],
this.validationResult,
this.unmatchMatchedBankTransaction.bind(this)
);
router.post(
'/match',
[ [
body('uncategorizedTransactions').exists().isArray({ min: 1 }), param('transactionId').exists(),
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(),
@@ -34,6 +30,12 @@ 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;
} }
@@ -48,21 +50,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 bodyData = this.matchedBodyData(req); const { transactionId } = req.params;
const matchTransactionDTO = this.matchedBodyData(
const uncategorizedTransactions = bodyData?.uncategorizedTransactions; req
const matchedTransactions = bodyData?.matchedTransactions; ) as IMatchTransactionsDTO;
try { try {
await this.bankTransactionsMatchingApp.matchTransaction( await this.bankTransactionsMatchingApp.matchTransaction(
tenantId, tenantId,
uncategorizedTransactions, transactionId,
matchedTransactions matchTransactionDTO
); );
return res.status(200).send({ return res.status(200).send({
ids: uncategorizedTransactions, id: transactionId,
message: 'The bank transaction has been matched.', message: 'The bank transaction has been matched.',
}); });
} catch (error) { } catch (error) {

View File

@@ -6,7 +6,6 @@ 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 {
@@ -30,10 +29,6 @@ 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;
} }
} }

View File

@@ -34,15 +34,16 @@ export class BankingRulesController extends BaseController {
body('conditions.*.comparator') body('conditions.*.comparator')
.exists() .exists()
.isIn(['equals', 'contains', 'not_contain']) .isIn(['equals', 'contains', 'not_contain'])
.default('contain') .default('contain'),
.trim(), body('conditions.*.value').exists(),
body('conditions.*.value').exists().trim(),
// Assign // Assign
body('assign_category').isString(), body('assign_category').isString(),
body('assign_account_id').isInt({ min: 0 }), body('assign_account_id').isInt({ min: 0 }),
body('assign_payee').isString().optional({ nullable: true }), body('assign_payee').isString().optional({ nullable: true }),
body('assign_memo').isString().optional({ nullable: true }), body('assign_memo').isString().optional({ nullable: true }),
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
]; ];
} }

View File

@@ -1,57 +0,0 @@
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);
}
}
}

View File

@@ -42,11 +42,7 @@ export class ExcludeBankTransactionsController extends BaseController {
); );
router.get( router.get(
'/excluded', '/excluded',
[ [],
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
],
this.validationResult, this.validationResult,
this.getExcludedBankTransactions.bind(this) this.getExcludedBankTransactions.bind(this)
); );
@@ -181,7 +177,7 @@ export class ExcludeBankTransactionsController extends BaseController {
next: NextFunction next: NextFunction
): Promise<Response | void> { ): Promise<Response | void> {
const { tenantId } = req; const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedBodyData(req);
try { try {
const data = const data =

View File

@@ -1,6 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import { query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@@ -15,16 +14,7 @@ export class RecognizedTransactionsController extends BaseController {
router() { router() {
const router = Router(); const router = Router();
router.get( router.get('/', this.getRecognizedTransactions.bind(this));
'/',
[
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)
);
router.get( router.get(
'/transactions/:uncategorizedTransactionId', '/transactions/:uncategorizedTransactionId',
this.getRecognizedTransaction.bind(this) this.getRecognizedTransaction.bind(this)

View File

@@ -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, query } from 'express-validator'; import { param } from 'express-validator';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
@@ -24,12 +24,7 @@ export default class GetCashflowAccounts extends BaseController {
const router = Router(); const router = Router();
router.get( router.get(
'/transactions/matches', '/transactions/:transactionId/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(
@@ -49,7 +44,7 @@ export default class GetCashflowAccounts extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private getCashflowTransaction = async ( private getCashflowTransaction = async (
req: Request<{ transactionId: number }>, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
@@ -76,24 +71,19 @@ export default class GetCashflowAccounts extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getMatchedTransactions( private async getMatchedTransactions(
req: Request< req: Request<{ transactionId: number }>,
{ transactionId: number },
null,
null,
{ uncategorizeTransactionsIds: Array<number> }
>,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
const uncategorizeTransactionsIds = req.query.uncategorizeTransactionsIds; const { transactionId } = req.params;
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,
uncategorizeTransactionsIds, transactionId,
filter filter
); );
return res.status(200).send(data); return res.status(200).send(data);

View File

@@ -1,15 +1,10 @@
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 { import { AbilitySubject, CashflowAction } from '@/interfaces';
AbilitySubject,
CashflowAction,
ICategorizeCashflowTransactioDTO,
} from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service() @Service()
@@ -49,7 +44,7 @@ export default class NewCashflowTransactionController extends BaseController {
this.catchServiceErrors this.catchServiceErrors
); );
router.post( router.post(
'/transactions/categorize', '/transactions/:id/categorize',
this.categorizeCashflowTransactionValidationSchema, this.categorizeCashflowTransactionValidationSchema,
this.validationResult, this.validationResult,
this.categorizeCashflowTransaction, this.categorizeCashflowTransaction,
@@ -94,7 +89,6 @@ 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(),
@@ -167,7 +161,7 @@ export default class NewCashflowTransactionController extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private revertCategorizedCashflowTransaction = async ( private revertCategorizedCashflowTransaction = async (
req: Request<{ id: number }>, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
@@ -197,19 +191,14 @@ export default class NewCashflowTransactionController extends BaseController {
next: NextFunction next: NextFunction
) => { ) => {
const { tenantId } = req; const { tenantId } = req;
const matchedObject = this.matchedBodyData(req); const { id: cashflowTransactionId } = req.params;
const categorizeDTO = omit(matchedObject, [ const cashflowTransaction = this.matchedBodyData(req);
'uncategorizedTransactionIds',
]) as ICategorizeCashflowTransactioDTO;
const uncategorizedTransactionIds =
matchedObject.uncategorizedTransactionIds;
try { try {
await this.cashflowApplication.categorizeTransaction( await this.cashflowApplication.categorizeTransaction(
tenantId, tenantId,
uncategorizedTransactionIds, cashflowTransactionId,
categorizeDTO cashflowTransaction
); );
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.',
@@ -280,7 +269,7 @@ export default class NewCashflowTransactionController extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public getUncategorizedCashflowTransactions = async ( public getUncategorizedCashflowTransactions = async (
req: Request<{ id: number }>, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {

View File

@@ -1,11 +0,0 @@
exports.up = function (knex) {
return knex.schema.table('plaid_items', (table) => {
table.datetime('paused_at');
});
};
exports.down = function (knex) {
return knex.schema.table('plaid_items', (table) => {
table.dropColumn('paused_at');
});
};

View File

@@ -236,7 +236,6 @@ 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;

View File

@@ -130,23 +130,20 @@ export interface ICommandCashflowDeletedPayload {
export interface ICashflowTransactionCategorizedPayload { export interface ICashflowTransactionCategorizedPayload {
tenantId: number; tenantId: number;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; uncategorizedTransaction: 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;
uncategorizedTransactionId: number; uncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICashflowTransactionUncategorizedPayload { export interface ICashflowTransactionUncategorizedPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransaction: IUncategorizedCashflowTransaction;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction; trx: Knex.Transaction;
} }

View File

@@ -13,9 +13,7 @@ import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentRecei
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob'; import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob'; import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob'; import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
import { ReregonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob'; import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob';
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob';
import { RevertRegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob';
export default ({ agenda }: { agenda: Agenda }) => { export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda); new ResetPasswordMailJob(agenda);
@@ -33,8 +31,6 @@ export default ({ agenda }: { agenda: Agenda }) => {
new ImportDeleteExpiredFilesJobs(agenda); new ImportDeleteExpiredFilesJobs(agenda);
new SendVerifyMailJob(agenda); new SendVerifyMailJob(agenda);
new RegonizeTransactionsJob(agenda); new RegonizeTransactionsJob(agenda);
new ReregonizeTransactionsJob(agenda);
new RevertRegonizeTransactionsJob(agenda);
agenda.start().then(() => { agenda.start().then(() => {
agenda.every('1 hours', 'delete-expired-imported-files', {}); agenda.every('1 hours', 'delete-expired-imported-files', {});

View File

@@ -1,8 +1,6 @@
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
export default class PlaidItem extends TenantModel { export default class PlaidItem extends TenantModel {
pausedAt: Date;
/** /**
* Table name. * Table name.
*/ */
@@ -23,19 +21,4 @@ export default class PlaidItem extends TenantModel {
static get relationMappings() { static get relationMappings() {
return {}; return {};
} }
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isPaused'];
}
/**
* Detarmines whether the Plaid item feeds syncing is paused.
* @return {boolean}
*/
get isPaused() {
return !!this.pausedAt;
}
} }

View File

@@ -20,7 +20,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
description!: string; description!: string;
plaidTransactionId!: string; plaidTransactionId!: string;
recognizedTransactionId!: number; recognizedTransactionId!: number;
excludedAt: Date;
/** /**
* Table name. * Table name.
@@ -32,7 +31,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
/** /**
* Timestamps columns. * Timestamps columns.
*/ */
get timestamps() { static get timestamps() {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
@@ -46,7 +45,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
'isDepositTransaction', 'isDepositTransaction',
'isWithdrawalTransaction', 'isWithdrawalTransaction',
'isRecognized', 'isRecognized',
'isExcluded'
]; ];
} }
@@ -91,14 +89,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
return !!this.recognizedTransactionId; return !!this.recognizedTransactionId;
} }
/**
* Detarmines whether the transaction is excluded.
* @returns {boolean}
*/
public get isExcluded(): boolean {
return !!this.excludedAt;
}
/** /**
* Model modifiers. * Model modifiers.
*/ */

View File

@@ -18,18 +18,9 @@ export class AccountTransformer extends Transformer {
'flattenName', 'flattenName',
'bankBalanceFormatted', 'bankBalanceFormatted',
'lastFeedsUpdatedAtFormatted', 'lastFeedsUpdatedAtFormatted',
'isFeedsPaused',
]; ];
}; };
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['plaidItem'];
};
/** /**
* Retrieves the flatten name with all dependants accounts names. * Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account - * @param {IAccount} account -
@@ -75,15 +66,6 @@ export class AccountTransformer extends Transformer {
return this.formatDate(account.lastFeedsUpdatedAt); return this.formatDate(account.lastFeedsUpdatedAt);
}; };
/**
* Detarmines whether the bank account connection is paused.
* @param account
* @returns {boolean}
*/
protected isFeedsPaused = (account: any): boolean => {
return account.plaidItem?.isPaused || false;
};
/** /**
* Transformes the accounts collection to flat or nested array. * Transformes the accounts collection to flat or nested array.
* @param {IAccount[]} * @param {IAccount[]}

View File

@@ -25,10 +25,7 @@ export class GetAccount {
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error. // Find the given account or throw not found error.
const account = await Account.query() const account = await Account.query().findById(accountId).throwIfNotFound();
.findById(accountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();

View File

@@ -96,11 +96,10 @@ export class AttachmentsApplication {
/** /**
* Retrieves the presigned url of the given attachment key. * Retrieves the presigned url of the given attachment key.
* @param {number} tenantId
* @param {string} key * @param {string} key
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
public getPresignedUrl(tenantId: number, key: string): Promise<string> { public getPresignedUrl(key: string): Promise<string> {
return this.getPresignedUrlService.getPresignedUrl(tenantId, key); return this.getPresignedUrlService.getPresignedUrl(key);
} }
} }

View File

@@ -1,34 +1,20 @@
import { Inject, Service } from 'typedi'; import { Service } from 'typedi';
import { GetObjectCommand } from '@aws-sdk/client-s3'; import { GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { s3 } from '@/lib/S3/S3'; import { s3 } from '@/lib/S3/S3';
import config from '@/config'; import config from '@/config';
import HasTenancyService from '../Tenancy/TenancyService';
@Service() @Service()
export class getAttachmentPresignedUrl { export class getAttachmentPresignedUrl {
@Inject()
private tenancy: HasTenancyService;
/** /**
* Retrieves the presigned url of the given attachment key with the original filename. * Retrieves the presigned url of the given attachment key.
* @param {number} tenantId
* @param {string} key * @param {string} key
* @returns {string} * @returns {Promise<string?>}
*/ */
async getPresignedUrl(tenantId: number, key: string) { async getPresignedUrl(key: string) {
const { Document } = this.tenancy.models(tenantId);
const foundDocument = await Document.query().findOne({ key });
let ResponseContentDisposition = 'attachment';
if (foundDocument && foundDocument.originName) {
ResponseContentDisposition += `; filename="${foundDocument.originName}"`;
}
const command = new GetObjectCommand({ const command = new GetObjectCommand({
Bucket: config.s3.bucket, Bucket: config.s3.bucket,
Key: key, Key: key,
ResponseContentDisposition,
}); });
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

View File

@@ -1,8 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { DisconnectBankAccount } from './DisconnectBankAccount'; import { DisconnectBankAccount } from './DisconnectBankAccount';
import { RefreshBankAccountService } from './RefreshBankAccount'; import { RefreshBankAccountService } from './RefreshBankAccount';
import { PauseBankAccountFeeds } from './PauseBankAccountFeeds';
import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds';
@Service() @Service()
export class BankAccountsApplication { export class BankAccountsApplication {
@@ -12,12 +10,6 @@ export class BankAccountsApplication {
@Inject() @Inject()
private refreshBankAccountService: RefreshBankAccountService; private refreshBankAccountService: RefreshBankAccountService;
@Inject()
private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
@Inject()
private pauseBankAccountFeedsService: PauseBankAccountFeeds;
/** /**
* Disconnects the given bank account. * Disconnects the given bank account.
* @param {number} tenantId * @param {number} tenantId
@@ -43,30 +35,4 @@ export class BankAccountsApplication {
bankAccountId bankAccountId
); );
} }
/**
* Pauses the feeds sync of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async pauseBankAccount(tenantId: number, bankAccountId: number) {
return this.pauseBankAccountFeedsService.pauseBankAccountFeeds(
tenantId,
bankAccountId
);
}
/**
* Resumes the feeds sync of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
async resumeBankAccount(tenantId: number, bankAccountId: number) {
return this.resumeBankAccountFeedsService.resumeBankAccountFeeds(
tenantId,
bankAccountId
);
}
} }

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { initialize } from 'objection'; import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
@Service() @Service()
export class GetBankAccountSummary { export class GetBankAccountSummary {
@@ -32,21 +31,17 @@ export class GetBankAccountSummary {
.findById(bankAccountId) .findById(bankAccountId)
.throwIfNotFound(); .throwIfNotFound();
const commonQuery = (q) => {
// Include just the given account.
q.where('accountId', bankAccountId);
// Only the not excluded.
q.modify('notExcluded');
// Only the not categorized.
q.modify('notCategorized');
};
// Retrieves the uncategorized transactions count of the given bank account. // Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount = const uncategorizedTranasctionsCount =
await UncategorizedCashflowTransaction.query().onBuild((q) => { await UncategorizedCashflowTransaction.query().onBuild((q) => {
commonQuery(q); // Include just the given account.
q.where('accountId', bankAccountId);
// Only the not excluded.
q.modify('notExcluded');
// Only the not categorized.
q.modify('notCategorized');
// Only the not matched bank transactions. // Only the not matched bank transactions.
q.withGraphJoined('matchedBankTransactions'); q.withGraphJoined('matchedBankTransactions');
@@ -57,40 +52,25 @@ export class GetBankAccountSummary {
q.first(); q.first();
}); });
// Retrives the recognized transactions count. // Retrieves the recognized transactions count of the given bank account.
const recognizedTransactionsCount = const recognizedTransactionsCount = await RecognizedBankTransaction.query()
await UncategorizedCashflowTransaction.query().onBuild((q) => { .whereExists(
commonQuery(q); UncategorizedCashflowTransaction.query().where(
'accountId',
q.withGraphJoined('recognizedTransaction'); bankAccountId
q.whereNotNull('recognizedTransaction.id'); )
)
// Count the results. .count('id as total')
q.count('uncategorized_cashflow_transactions.id as total'); .first();
q.first();
});
const excludedTransactionsCount =
await UncategorizedCashflowTransaction.query().onBuild((q) => {
q.where('accountId', bankAccountId);
q.modify('excluded');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
const totalUncategorizedTransactions = const totalUncategorizedTransactions =
uncategorizedTranasctionsCount?.total || 0; uncategorizedTranasctionsCount?.total || 0;
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0; const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
return { return {
name: bankAccount.name, name: bankAccount.name,
totalUncategorizedTransactions, totalUncategorizedTransactions,
totalRecognizedTransactions, totalRecognizedTransactions,
totalExcludedTransactions,
}; };
} }
} }

View File

@@ -1,44 +0,0 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './types';
@Service()
export class PauseBankAccountFeeds {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Pauses the bankfeed syncing of the given bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
public async pauseBankAccountFeeds(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const oldAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
// Can't continue if the bank account is not connected.
if (!oldAccount.plaidItem) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
}
// Cannot continue if the bank account feeds is already paused.
if (oldAccount.plaidItem.isPaused) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_PAUSED);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({
pausedAt: new Date(),
});
});
}
}

View File

@@ -1,43 +0,0 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './types';
@Service()
export class ResumeBankAccountFeeds {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Resumes the bank feeds syncing of the bank account.
* @param {number} tenantId
* @param {number} bankAccountId
* @returns {Promise<void>}
*/
public async resumeBankAccountFeeds(tenantId: number, bankAccountId: number) {
const { Account, PlaidItem } = this.tenancy.models(tenantId);
const oldAccount = await Account.query()
.findById(bankAccountId)
.withGraphFetched('plaidItem');
// Can't continue if the bank account is not connected.
if (!oldAccount.plaidItem) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
}
// Cannot continue if the bank account feeds is already paused.
if (!oldAccount.plaidItem.isPaused) {
throw new ServiceError(ERRORS.BANK_ACCOUNT_FEEDS_ALREADY_RESUMED);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await PlaidItem.query(trx).findById(oldAccount.plaidItem.id).patch({
pausedAt: null,
});
});
}
}

View File

@@ -14,6 +14,4 @@ export interface IBankAccountDisconnectedEventPayload {
export const ERRORS = { export const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED', BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED',
BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED',
}; };

View File

@@ -1,11 +1,7 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { import { Inject, Service } from 'typedi';
validateTransactionNotCategorized, import { validateTransactionNotCategorized } from './utils';
validateTransactionNotExcluded,
} from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
@@ -41,13 +37,9 @@ export class ExcludeBankTransaction {
.findById(uncategorizedTransactionId) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
// Validate the transaction shouldn't be excluded.
validateTransactionNotExcluded(oldUncategorizedTransaction);
// Validate the transaction shouldn't be categorized.
validateTransactionNotCategorized(oldUncategorizedTransaction); validateTransactionNotCategorized(oldUncategorizedTransaction);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, { await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool'; import PromisePool from '@supercharge/promise-pool';
import { castArray, uniq } from 'lodash'; import { castArray } from 'lodash';
import { ExcludeBankTransaction } from './ExcludeBankTransaction'; import { ExcludeBankTransaction } from './ExcludeBankTransaction';
@Service() @Service()
@@ -18,7 +18,7 @@ export class ExcludeBankTransactions {
tenantId: number, tenantId: number,
bankTransactionIds: Array<number> | number bankTransactionIds: Array<number> | number
) { ) {
const _bankTransactionIds = uniq(castArray(bankTransactionIds)); const _bankTransactionIds = castArray(bankTransactionIds);
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(_bankTransactionIds) .for(_bankTransactionIds)

View File

@@ -1,11 +1,7 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { import { Inject, Service } from 'typedi';
validateTransactionNotCategorized, import { validateTransactionNotCategorized } from './utils';
validateTransactionShouldBeExcluded,
} from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
@@ -41,13 +37,9 @@ export class UnexcludeBankTransaction {
.findById(uncategorizedTransactionId) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
// Validate the transaction should be excludded.
validateTransactionShouldBeExcluded(oldUncategorizedTransaction);
// Validate the transaction shouldn't be categorized.
validateTransactionNotCategorized(oldUncategorizedTransaction); validateTransactionNotCategorized(oldUncategorizedTransaction);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync( await this.eventPublisher.emitAsync(
events.bankTransactions.onUnexcluding, events.bankTransactions.onUnexcluding,
{ {

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool'; import PromisePool from '@supercharge/promise-pool';
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
import { castArray, uniq } from 'lodash'; import { castArray } from 'lodash';
@Service() @Service()
export class UnexcludeBankTransactions { export class UnexcludeBankTransactions {
@@ -17,7 +17,7 @@ export class UnexcludeBankTransactions {
tenantId: number, tenantId: number,
bankTransactionIds: Array<number> | number bankTransactionIds: Array<number> | number
) { ) {
const _bankTransactionIds = uniq(castArray(bankTransactionIds)); const _bankTransactionIds = castArray(bankTransactionIds);
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(_bankTransactionIds) .for(_bankTransactionIds)

View File

@@ -3,8 +3,6 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran
const ERRORS = { const ERRORS = {
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED',
TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED',
}; };
export const validateTransactionNotCategorized = ( export const validateTransactionNotCategorized = (
@@ -14,19 +12,3 @@ export const validateTransactionNotCategorized = (
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
} }
}; };
export const validateTransactionNotExcluded = (
transaction: UncategorizedCashflowTransaction
) => {
if (transaction.isExcluded) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_EXCLUDED);
}
};
export const validateTransactionShouldBeExcluded = (
transaction: UncategorizedCashflowTransaction
) => {
if (!transaction.isExcluded) {
throw new ServiceError(ERRORS.TRANSACTION_NOT_EXCLUDED);
}
};

View File

@@ -1,7 +1,6 @@
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';
@@ -48,24 +47,21 @@ 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,
uncategorizedTransactionIds: Array<number>, uncategorizedTransactionId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
): Promise<MatchedTransactionsPOJO> { ): Promise<MatchedTransactionsPOJO> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactions = const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds) .findById(uncategorizedTransactionId)
.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;
@@ -75,14 +71,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(
uncategorizedTransactions, uncategorizedTransaction,
matchedTransactions matchedTransactions
); );
return { return {
perfectMatches, perfectMatches,
possibleMatches, possibleMatches,
totalPending,
}; };
} }
@@ -94,20 +90,20 @@ export class GetMatchedTransactions {
* @returns {MatchedTransactionsPOJO} * @returns {MatchedTransactionsPOJO}
*/ */
private groupMatchedResults( private groupMatchedResults(
uncategorizedTransactions: Array<any>, uncategorizedTransaction,
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(amount, date, results); const closestResullts = sortClosestMatchTransactions(
uncategorizedTransaction,
results
);
const perfectMatches = R.filter( const perfectMatches = R.filter(
(match) => (match) =>
match.amount === amount && moment(match.date).isSame(date, 'day'), match.amount === uncategorizedTransaction.amount &&
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
closestResullts closestResullts
); );
const possibleMatches = R.difference(closestResullts, perfectMatches); const possibleMatches = R.difference(closestResullts, perfectMatches);

View File

@@ -7,7 +7,6 @@ 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()
@@ -44,28 +43,24 @@ export abstract class GetMatchedTransactionsByType {
} }
/** /**
* Creates the common matched transaction. *
* @param {number} tenantId * @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds * @param {number} uncategorizedTransactionId
* @param {IMatchTransactionDTO} matchTransactionDTO * @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx
*/ */
public async createMatchedTransaction( public async createMatchedTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionIds: Array<number>, uncategorizedTransactionId: number,
matchTransactionDTO: IMatchTransactionDTO, matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction trx?: Knex.Transaction
) { ) {
const { MatchedBankTransaction } = this.tenancy.models(tenantId); const { MatchedBankTransaction } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(2) await MatchedBankTransaction.query(trx).insert({
.for(uncategorizedTransactionIds) uncategorizedTransactionId,
.process(async (uncategorizedTransactionId) => { referenceType: matchTransactionDTO.referenceType,
await MatchedBankTransaction.query(trx).insert({ referenceId: matchTransactionDTO.referenceId,
uncategorizedTransactionId, });
referenceType: matchTransactionDTO.referenceType,
referenceId: matchTransactionDTO.referenceId,
});
});
} }
} }

View File

@@ -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, IMatchTransactionDTO } from './types'; import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } 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,
uncategorizedTransactionsIds: Array<number>, uncategorizedTransactionId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
) { ) {
return this.getMatchedTransactionsService.getMatchedTransactions( return this.getMatchedTransactionsService.getMatchedTransactions(
tenantId, tenantId,
uncategorizedTransactionsIds, uncategorizedTransactionId,
filter filter
); );
} }
@@ -42,13 +42,13 @@ export class MatchBankTransactionsApplication {
*/ */
public matchTransaction( public matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> { ): Promise<void> {
return this.matchTransactionService.matchTransaction( return this.matchTransactionService.matchTransaction(
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,
matchedTransactions matchTransactionsDTO
); );
} }

View File

@@ -1,4 +1,4 @@
import { castArray } from 'lodash'; import { isEmpty } 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,16 +10,11 @@ import {
ERRORS, ERRORS,
IBankTransactionMatchedEventPayload, IBankTransactionMatchedEventPayload,
IBankTransactionMatchingEventPayload, IBankTransactionMatchingEventPayload,
IMatchTransactionDTO, IMatchTransactionsDTO,
} from './types'; } from './types';
import { MatchTransactionsTypes } from './MatchTransactionsTypes'; import { MatchTransactionsTypes } from './MatchTransactionsTypes';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import { sumMatchTranasctions } from './_utils';
sumMatchTranasctions,
sumUncategorizedTransactions,
validateUncategorizedTransactionsExcluded,
validateUncategorizedTransactionsNotMatched,
} from './_utils';
@Service() @Service()
export class MatchBankTransactions { export class MatchBankTransactions {
@@ -44,25 +39,27 @@ export class MatchBankTransactions {
*/ */
async validate( async validate(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
) { ) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); const { matchedTransactions } = matchTransactionsDTO;
// Validates the uncategorized transaction existance. // Validates the uncategorized transaction existance.
const uncategorizedTransactions = const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds) .findById(uncategorizedTransactionId)
.withGraphFetched('matchedBankTransactions') .withGraphFetched('matchedBankTransactions')
.throwIfNotFound(); .throwIfNotFound();
// Validates the uncategorized transaction is not already matched. // Validates the uncategorized transaction is not already matched.
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions); if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
}
// Validate the uncategorized transaction is not excluded. // Validate the uncategorized transaction is not excluded.
validateUncategorizedTransactionsExcluded(uncategorizedTransactions); if (uncategorizedTransaction.excluded) {
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 =
@@ -97,12 +94,9 @@ 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 (totalUncategorizedTransactions !== totalMatchedTranasctions) { if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
} }
} }
@@ -115,23 +109,23 @@ export class MatchBankTransactions {
*/ */
public async matchTransaction( public async matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> { ): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); const { matchedTransactions } = matchTransactionsDTO;
// Validates the given matching transactions DTO. // Validates the given matching transactions DTO.
await this.validate( await this.validate(
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions matchTransactionsDTO
); );
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,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions, matchTransactionsDTO,
trx, trx,
} as IBankTransactionMatchingEventPayload); } as IBankTransactionMatchingEventPayload);
@@ -145,16 +139,17 @@ export class MatchBankTransactions {
); );
await getMatchedTransactionsService.createMatchedTransaction( await getMatchedTransactionsService.createMatchedTransaction(
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
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,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions, matchTransactionsDTO,
trx, trx,
} as IBankTransactionMatchedEventPayload); } as IBankTransactionMatchedEventPayload);
}); });

View File

@@ -1,23 +1,22 @@
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 { ERRORS, MatchedTransactionPOJO } from './types'; import { MatchedTransactionPOJO } from './types';
import { isEmpty, sumBy } from 'lodash';
import { ServiceError } from '@/exceptions';
export const sortClosestMatchTransactions = ( export const sortClosestMatchTransactions = (
amount: number, uncategorizedTransaction: UncategorizedCashflowTransaction,
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 - amount) Math.abs(match.amount - uncategorizedTransaction.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(moment(match.date).diff(moment(date), 'days')) Math.abs(
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
)
), ),
])(matches); ])(matches);
}; };
@@ -30,36 +29,3 @@ 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),
});
}
};

View File

@@ -5,7 +5,6 @@ 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 {
@@ -31,24 +30,18 @@ export class DecrementUncategorizedTransactionOnMatching {
*/ */
public async decrementUnCategorizedTransactionsOnMatching({ public async decrementUnCategorizedTransactionsOnMatching({
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
trx, trx,
}: IBankTransactionMatchedEventPayload) { }: IBankTransactionMatchedEventPayload) {
const { UncategorizedCashflowTransaction, Account } = const { UncategorizedCashflowTransaction, Account } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
const uncategorizedTransactions = const transaction = await UncategorizedCashflowTransaction.query().findById(
await UncategorizedCashflowTransaction.query().whereIn( uncategorizedTransactionId
'id', );
uncategorizedTransactionIds await Account.query(trx)
); .findById(transaction.accountId)
await PromisePool.withConcurrency(1) .decrement('uncategorizedTransactions', 1);
.for(uncategorizedTransactions)
.process(async (transaction) => {
await Account.query(trx)
.findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1);
});
} }
/** /**

View File

@@ -2,15 +2,15 @@ import { Knex } from 'knex';
export interface IBankTransactionMatchingEventPayload { export interface IBankTransactionMatchingEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionIds: Array<number>; uncategorizedTransactionId: number;
matchedTransactions: Array<IMatchTransactionDTO>; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
export interface IBankTransactionMatchedEventPayload { export interface IBankTransactionMatchedEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionIds: Array<number>; uncategorizedTransactionId: number;
matchedTransactions: Array<IMatchTransactionDTO>; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
@@ -32,7 +32,6 @@ export interface IMatchTransactionDTO {
} }
export interface IMatchTransactionsDTO { export interface IMatchTransactionsDTO {
uncategorizedTransactionIds: Array<number>;
matchedTransactions: Array<IMatchTransactionDTO>; matchedTransactions: Array<IMatchTransactionDTO>;
} }
@@ -58,7 +57,6 @@ 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 = {

View File

@@ -1,15 +1,11 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions'; import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class PlaidWebooks { export class PlaidWebooks {
@Inject() @Inject()
private updateTransactionsService: PlaidUpdateTransactions; private updateTransactionsService: PlaidUpdateTransactions;
@Inject()
private tenancy: HasTenancyService;
/** /**
* Listens to Plaid webhooks * Listens to Plaid webhooks
* @param {number} tenantId - Tenant Id. * @param {number} tenantId - Tenant Id.
@@ -65,7 +61,7 @@ export class PlaidWebooks {
plaidItemId: string plaidItemId: string
): void { ): void {
console.log( console.log(
`PLAID WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}` `WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`
); );
} }
@@ -82,21 +78,8 @@ export class PlaidWebooks {
plaidItemId: string, plaidItemId: string,
webhookCode: string webhookCode: string
): Promise<void> { ): Promise<void> {
const { PlaidItem } = this.tenancy.models(tenantId);
const plaidItem = await PlaidItem.query()
.findById(plaidItemId)
.throwIfNotFound();
switch (webhookCode) { switch (webhookCode) {
case 'SYNC_UPDATES_AVAILABLE': { case 'SYNC_UPDATES_AVAILABLE': {
if (plaidItem.isPaused) {
this.serverLogAndEmitSocket(
'Plaid item syncing is paused.',
webhookCode,
plaidItemId
);
return;
}
// Fired when new transactions data becomes available. // Fired when new transactions data becomes available.
const { addedCount, modifiedCount, removedCount } = const { addedCount, modifiedCount, removedCount } =
await this.updateTransactionsService.updateTransactions( await this.updateTransactionsService.updateTransactions(

View File

@@ -35,8 +35,7 @@ export class RecognizeSyncedBankTranasctions extends EventSubscriber {
runAfterTransaction(trx, async () => { runAfterTransaction(trx, async () => {
await this.recognizeTranasctionsService.recognizeTransactions( await this.recognizeTranasctionsService.recognizeTransactions(
tenantId, tenantId,
null, batch
{ batch }
); );
}); });
}; };

View File

@@ -1,45 +0,0 @@
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),
}
);
}
}

View File

@@ -1,176 +0,0 @@
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;
}
/**
* Retrieves the assigned category of recognized transaction, if is not recognized
* returns the default transaction type depends on the transaction normal.
* @returns {string}
*/
public transactionType() {
const assignedCategory =
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedCategory;
return (
assignedCategory ||
(this.isDepositTransaction() ? 'other_income' : 'other_expense')
);
}
/**
*
* @returns {string}
*/
public payee() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedPayee || null
);
}
/**
*
* @returns {string}
*/
public memo() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.assignedMemo || null
);
}
/**
* Retrieves the rule id the transaction recongized by.
* @returns {string}
*/
public recognizedByRuleId() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.bankRuleId || null
);
}
/**
* Retrieves the rule name the transaction recongized by.
* @returns {string}
*/
public recognizedByRuleName() {
return (
this.options.firstUncategorizedTransaction?.recognizedTransaction
?.bankRule?.name || null
);
}
}

View File

@@ -1,13 +1,11 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { castArray, isEmpty } from 'lodash';
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';
import { RecognizeTransactionsCriteria } from './_types';
@Service() @Service()
export class RecognizeTranasctionsService { export class RecognizeTranasctionsService {
@@ -50,42 +48,24 @@ export class RecognizeTranasctionsService {
/** /**
* Regonized the uncategorized transactions. * Regonized the uncategorized transactions.
* @param {number} tenantId - * @param {number} tenantId -
* @param {number|Array<number>} ruleId - The target rule id/ids.
* @param {RecognizeTransactionsCriteria}
* @param {Knex.Transaction} trx - * @param {Knex.Transaction} trx -
*/ */
public async recognizeTransactions( public async recognizeTransactions(
tenantId: number, tenantId: number,
ruleId?: number | Array<number>, batch: string = '',
transactionsCriteria?: RecognizeTransactionsCriteria,
trx?: Knex.Transaction trx?: Knex.Transaction
) { ) {
const { UncategorizedCashflowTransaction, BankRule } = const { UncategorizedCashflowTransaction, BankRule } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
const uncategorizedTranasctions = const uncategorizedTranasctions =
await UncategorizedCashflowTransaction.query(trx).onBuild((query) => { await UncategorizedCashflowTransaction.query().onBuild((query) => {
query.modify('notRecognized'); query.where('recognized_transaction_id', null);
query.modify('notCategorized'); query.where('categorized', false);
// Filter the transactions based on the given criteria. if (batch) query.where('batch', batch);
if (transactionsCriteria?.batch) {
query.where('batch', transactionsCriteria.batch);
}
if (transactionsCriteria?.accountId) {
query.where('accountId', transactionsCriteria.accountId);
}
}); });
const bankRules = await BankRule.query().withGraphFetched('conditions');
const bankRules = await BankRule.query(trx).onBuild((q) => {
const rulesIds = castArray(ruleId);
if (!isEmpty(rulesIds)) {
q.whereIn('id', rulesIds);
}
q.withGraphFetched('conditions');
});
const bankRulesByAccountId = transformToMapBy( const bankRulesByAccountId = transformToMapBy(
bankRules, bankRules,
'applyIfAccountId' 'applyIfAccountId'

View File

@@ -1,5 +1,5 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService'; import { RecognizeTranasctionsService } from './RecognizeTranasctionsService';
@Service() @Service()
export class RegonizeTransactionsJob { export class RegonizeTransactionsJob {
@@ -18,15 +18,11 @@ export class RegonizeTransactionsJob {
* Triggers sending invoice mail. * Triggers sending invoice mail.
*/ */
private handler = async (job, done: Function) => { private handler = async (job, done: Function) => {
const { tenantId, ruleId, transactionsCriteria } = job.attrs.data; const { tenantId, batch } = job.attrs.data;
const regonizeTransactions = Container.get(RecognizeTranasctionsService); const regonizeTransactions = Container.get(RecognizeTranasctionsService);
try { try {
await regonizeTransactions.recognizeTransactions( await regonizeTransactions.recognizeTransactions(tenantId, batch);
tenantId,
ruleId,
transactionsCriteria
);
done(); done();
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -1,72 +0,0 @@
import { Inject, Service } from 'typedi';
import { castArray } from 'lodash';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { RevertRecognizedTransactionsCriteria } from './_types';
@Service()
export class RevertRecognizedTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Revert and unlinks the recognized transactions based on the given bank rule
* and transactions criteria..
* @param {number} tenantId - Tenant id.
* @param {number|Array<number>} bankRuleId - Bank rule id.
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria -
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async revertRecognizedTransactions(
tenantId: number,
ruleId?: number | Array<number>,
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction, RecognizedBankTransaction } =
this.tenancy.models(tenantId);
const rulesIds = castArray(ruleId);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Retrieves all the recognized transactions of the banbk rule.
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query(trx).onBuild((q) => {
q.withGraphJoined('recognizedTransaction');
q.whereNotNull('recognizedTransaction.id');
if (rulesIds.length > 0) {
q.whereIn('recognizedTransaction.bankRuleId', rulesIds);
}
if (transactionsCriteria?.accountId) {
q.where('accountId', transactionsCriteria.accountId);
}
if (transactionsCriteria?.batch) {
q.where('batch', transactionsCriteria.batch);
}
});
const uncategorizedTransactionIds = uncategorizedTransactions.map(
(r) => r.id
);
// Unlink the recongized transactions out of uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
recognizedTransactionId: null,
});
// Delete the recognized bank transactions that assocaited to bank rule.
await RecognizedBankTransaction.query(trx)
.whereIn('uncategorizedTransactionId', uncategorizedTransactionIds)
.delete();
},
trx
);
}
}

View File

@@ -1,11 +0,0 @@
export interface RevertRecognizedTransactionsCriteria {
batch?: string;
accountId?: number;
}
export interface RecognizeTransactionsCriteria {
batch?: string;
accountId?: number;
}

View File

@@ -1,4 +1,3 @@
import { lowerCase } from 'lodash';
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
import { import {
BankRuleApplyIfTransactionType, BankRuleApplyIfTransactionType,
@@ -52,15 +51,12 @@ const matchNumberCondition = (
const matchTextCondition = ( const matchTextCondition = (
transaction: UncategorizedCashflowTransaction, transaction: UncategorizedCashflowTransaction,
condition: IBankRuleCondition condition: IBankRuleCondition
): boolean => { ) => {
switch (condition.comparator) { switch (condition.comparator) {
case BankRuleConditionComparator.Equals: case BankRuleConditionComparator.Equals:
return transaction[condition.field] === condition.value; return transaction[condition.field] === condition.value;
case BankRuleConditionComparator.Contains: case BankRuleConditionComparator.Contains:
const fieldValue = lowerCase(transaction[condition.field]); return transaction[condition.field]?.includes(condition.value.toString());
const conditionValue = lowerCase(condition.value);
return fieldValue.includes(conditionValue);
case BankRuleConditionComparator.NotContain: case BankRuleConditionComparator.NotContain:
return !transaction[condition.field]?.includes( return !transaction[condition.field]?.includes(
condition.value.toString() condition.value.toString()

View File

@@ -41,10 +41,14 @@ export class TriggerRecognizedTransactions {
*/ */
private async recognizedTransactionsOnRuleCreated({ private async recognizedTransactionsOnRuleCreated({
tenantId, tenantId,
bankRule, createRuleDTO,
}: IBankRuleEventCreatedPayload) { }: IBankRuleEventCreatedPayload) {
const payload = { tenantId, ruleId: bankRule.id }; const payload = { tenantId };
// Cannot run recognition if the option is not enabled.
if (createRuleDTO.recognition) {
return;
}
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
@@ -55,14 +59,14 @@ export class TriggerRecognizedTransactions {
private async recognizedTransactionsOnRuleEdited({ private async recognizedTransactionsOnRuleEdited({
tenantId, tenantId,
editRuleDTO, editRuleDTO,
ruleId,
}: IBankRuleEventEditedPayload) { }: IBankRuleEventEditedPayload) {
const payload = { tenantId, ruleId }; const payload = { tenantId };
await this.agenda.now( // Cannot run recognition if the option is not enabled.
'rerecognize-uncategorized-transactions-job', if (!editRuleDTO.recognition) {
payload return;
); }
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
/** /**
@@ -71,13 +75,9 @@ export class TriggerRecognizedTransactions {
*/ */
private async recognizedTransactionsOnRuleDeleted({ private async recognizedTransactionsOnRuleDeleted({
tenantId, tenantId,
ruleId,
}: IBankRuleEventDeletedPayload) { }: IBankRuleEventDeletedPayload) {
const payload = { tenantId, ruleId }; const payload = { tenantId };
await this.agenda.now( await this.agenda.now('recognize-uncategorized-transactions-job', payload);
'revert-recognized-uncategorized-transactions-job',
payload
);
} }
/** /**
@@ -91,7 +91,7 @@ export class TriggerRecognizedTransactions {
}: IImportFileCommitedEventPayload) { }: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId }); const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch; const batch = importFile.paramsParsed.batch;
const payload = { tenantId, transactionsCriteria: { batch } }; const payload = { tenantId, batch };
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }

View File

@@ -1,45 +0,0 @@
import Container, { Service } from 'typedi';
import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService';
import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions';
@Service()
export class ReregonizeTransactionsJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'rerecognize-uncategorized-transactions-job',
{ priority: 'high', concurrency: 2 },
this.handler
);
}
/**
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
const revertRegonizedTransactions = Container.get(
RevertRecognizedTransactions
);
try {
await revertRegonizedTransactions.revertRecognizedTransactions(
tenantId,
ruleId,
transactionsCriteria
);
await regonizeTransactions.recognizeTransactions(
tenantId,
ruleId,
transactionsCriteria
);
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -1,38 +0,0 @@
import Container, { Service } from 'typedi';
import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions';
@Service()
export class RevertRegonizeTransactionsJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'revert-recognized-uncategorized-transactions-job',
{ priority: 'high', concurrency: 2 },
this.handler
);
}
/**
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
const revertRegonizedTransactions = Container.get(
RevertRecognizedTransactions
);
try {
await revertRegonizedTransactions.revertRecognizedTransactions(
tenantId,
ruleId,
transactionsCriteria
);
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -62,7 +62,6 @@ export class CreateBankRuleService {
await this.eventPublisher.emitAsync(events.bankRules.onCreated, { await this.eventPublisher.emitAsync(events.bankRules.onCreated, {
tenantId, tenantId,
createRuleDTO, createRuleDTO,
bankRule,
trx, trx,
} as IBankRuleEventCreatedPayload); } as IBankRuleEventCreatedPayload);

View File

@@ -64,10 +64,9 @@ export class EditBankRuleService {
} as IBankRuleEventEditingPayload); } as IBankRuleEventEditingPayload);
// Updates the given bank rule. // Updates the given bank rule.
await BankRule.query(trx).upsertGraphAndFetch({ await BankRule.query(trx)
...tranformDTO, .findById(ruleId)
id: ruleId, .patch({ ...tranformDTO });
});
// Triggers `onBankRuleEdited` event. // Triggers `onBankRuleEdited` event.
await this.eventPublisher.emitAsync(events.bankRules.onEdited, { await this.eventPublisher.emitAsync(events.bankRules.onEdited, {

View File

@@ -0,0 +1,54 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
@Service()
export class UnlinkBankRuleRecognizedTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Unlinks the given bank rule out of recognized transactions.
* @param {number} tenantId - Tenant id.
* @param {number} bankRuleId - Bank rule id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async unlinkBankRuleOutRecognizedTransactions(
tenantId: number,
bankRuleId: number,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction, RecognizedBankTransaction } =
this.tenancy.models(tenantId);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Retrieves all the recognized transactions of the banbk rule.
const recognizedTransactions = await RecognizedBankTransaction.query(
trx
).where('bankRuleId', bankRuleId);
const uncategorizedTransactionIds = recognizedTransactions.map(
(r) => r.uncategorizedTransactionId
);
// Unlink the recongized transactions out of uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
recognizedTransactionId: null,
});
// Delete the recognized bank transactions that assocaited to bank rule.
await RecognizedBankTransaction.query(trx)
.where({ bankRuleId })
.delete();
},
trx
);
}
}

View File

@@ -1,12 +1,12 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { UnlinkBankRuleRecognizedTransactions } from '../UnlinkBankRuleRecognizedTransactions';
import { IBankRuleEventDeletingPayload } from '../types'; import { IBankRuleEventDeletingPayload } from '../types';
import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions';
@Service() @Service()
export class UnlinkBankRuleOnDeleteBankRule { export class UnlinkBankRuleOnDeleteBankRule {
@Inject() @Inject()
private revertRecognizedTransactionsService: RevertRecognizedTransactions; private unlinkBankRule: UnlinkBankRuleRecognizedTransactions;
/** /**
* Constructor method. * Constructor method.
@@ -26,7 +26,7 @@ export class UnlinkBankRuleOnDeleteBankRule {
tenantId, tenantId,
ruleId, ruleId,
}: IBankRuleEventDeletingPayload) { }: IBankRuleEventDeletingPayload) {
await this.revertRecognizedTransactionsService.revertRecognizedTransactions( await this.unlinkBankRule.unlinkBankRuleOutRecognizedTransactions(
tenantId, tenantId,
ruleId ruleId
); );

View File

@@ -16,7 +16,7 @@ export interface IBankRuleCondition {
id?: number; id?: number;
field: BankRuleConditionField; field: BankRuleConditionField;
comparator: BankRuleConditionComparator; comparator: BankRuleConditionComparator;
value: string; value: number;
} }
export enum BankRuleConditionType { export enum BankRuleConditionType {
@@ -30,7 +30,6 @@ export enum BankRuleApplyIfTransactionType {
} }
export interface IBankRule { export interface IBankRule {
id?: number;
name: string; name: string;
order?: number; order?: number;
applyIfAccountId: number; applyIfAccountId: number;
@@ -72,6 +71,8 @@ export interface IBankRuleCommonDTO {
assignAccountId: number; assignAccountId: number;
assignPayee?: string; assignPayee?: string;
assignMemo?: string; assignMemo?: string;
recognition?: boolean;
} }
export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {} export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {}
@@ -85,7 +86,6 @@ export interface IBankRuleEventCreatingPayload {
export interface IBankRuleEventCreatedPayload { export interface IBankRuleEventCreatedPayload {
tenantId: number; tenantId: number;
createRuleDTO: ICreateBankRuleDTO; createRuleDTO: ICreateBankRuleDTO;
bankRule: IBankRule;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }

View File

@@ -164,12 +164,12 @@ export class CashflowApplication {
*/ */
public categorizeTransaction( public categorizeTransaction(
tenantId: number, tenantId: number,
uncategorizeTransactionIds: Array<number>, cashflowTransactionId: number,
categorizeDTO: ICategorizeCashflowTransactioDTO categorizeDTO: ICategorizeCashflowTransactioDTO
) { ) {
return this.categorizeTransactionService.categorize( return this.categorizeTransactionService.categorize(
tenantId, tenantId,
uncategorizeTransactionIds, cashflowTransactionId,
categorizeDTO categorizeDTO
); );
} }

View File

@@ -1,6 +1,4 @@
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';
@@ -10,12 +8,12 @@ import {
ICashflowTransactionUncategorizingPayload, ICashflowTransactionUncategorizingPayload,
ICategorizeCashflowTransactioDTO, ICategorizeCashflowTransactioDTO,
} from '@/interfaces'; } from '@/interfaces';
import { import { Knex } from 'knex';
transformCategorizeTransToCashflow, import { transformCategorizeTransToCashflow } from './utils';
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 {
@@ -41,29 +39,27 @@ export class CategorizeCashflowTransaction {
*/ */
public async categorize( public async categorize(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: 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 oldUncategorizedTransactions = const transaction = await UncategorizedCashflowTransaction.query()
await UncategorizedCashflowTransaction.query() .findById(uncategorizedTransactionId)
.whereIn('id', uncategorizedTransactionIds) .throwIfNotFound();
.throwIfNotFound();
// Validate cannot categorize excluded transaction. // Validate cannot categorize excluded transaction.
validateUncategorizedTransactionsNotExcluded(oldUncategorizedTransactions); if (transaction.excluded) {
throw new ServiceError(ERRORS.CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION);
}
// Validates the transaction shouldn't be categorized before. // Validates the transaction shouldn't be categorized before.
this.commandValidators.validateTransactionsShouldNotCategorized( this.commandValidators.validateTransactionShouldNotCategorized(transaction);
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(
oldUncategorizedTransactions, transaction,
categorizeDTO.transactionType categorizeDTO.transactionType
); );
// Edits the cashflow transaction under UOW env. // Edits the cashflow transaction under UOW env.
@@ -73,13 +69,12 @@ 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(
oldUncategorizedTransactions, transaction,
categorizeDTO categorizeDTO
); );
// Creates a new cashflow transaction. // Creates a new cashflow transaction.
@@ -88,20 +83,15 @@ export class CategorizeCashflowTransaction {
tenantId, tenantId,
cashflowTransactionDTO cashflowTransactionDTO
); );
// Updates the uncategorized transaction as categorized. // Updates the uncategorized transaction as categorized.
await UncategorizedCashflowTransaction.query(trx) const uncategorizedTransaction =
.whereIn('id', uncategorizedTransactionIds) await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
.patch({ uncategorizedTransactionId,
categorized: true, {
categorizeRefType: 'CashflowTransaction', categorized: true,
categorizeRefId: cashflowTransaction.id, categorizeRefType: 'CashflowTransaction',
}); 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(
@@ -109,8 +99,7 @@ export class CategorizeCashflowTransaction {
{ {
tenantId, tenantId,
cashflowTransaction, cashflowTransaction,
uncategorizedTransactions, uncategorizedTransaction,
oldUncategorizedTransactions,
categorizeDTO, categorizeDTO,
trx, trx,
} as ICashflowTransactionCategorizedPayload } as ICashflowTransactionCategorizedPayload

View File

@@ -1,5 +1,5 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { includes, camelCase, upperFirst, sumBy } from 'lodash'; import { includes, camelCase, upperFirst } 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,15 +68,11 @@ 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 validateTransactionsShouldNotCategorized( public validateTransactionShouldNotCategorized(
cashflowTransactions: Array<IUncategorizedCashflowTransaction> cashflowTransaction: CashflowTransaction
) { ) {
const categorized = cashflowTransactions.filter((t) => t.categorized); if (cashflowTransaction.uncategorize) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
if (categorized?.length > 0) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED, '', {
ids: categorized.map((t) => t.id),
});
} }
} }
@@ -87,19 +83,17 @@ export class CommandCashflowValidator {
* @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)} * @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
*/ */
public validateUncategorizeTransactionType( public validateUncategorizeTransactionType(
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>, uncategorizeTransaction: IUncategorizedCashflowTransaction,
transactionType: string transactionType: string
) { ) {
const amount = sumBy(uncategorizeTransactions, 'amount');
const isDepositTransaction = amount > 0;
const isWithdrawalTransaction = amount <= 0;
const type = getCashflowTransactionType( const type = getCashflowTransactionType(
transactionType as CASHFLOW_TRANSACTION_TYPE upperFirst(camelCase(transactionType)) as CASHFLOW_TRANSACTION_TYPE
); );
if ( if (
(type.direction === CASHFLOW_DIRECTION.IN && isDepositTransaction) || (type.direction === CASHFLOW_DIRECTION.IN &&
(type.direction === CASHFLOW_DIRECTION.OUT && isWithdrawalTransaction) uncategorizeTransaction.isDepositTransaction) ||
(type.direction === CASHFLOW_DIRECTION.OUT &&
uncategorizeTransaction.isWithdrawalTransaction)
) { ) {
return; return;
} }

View File

@@ -34,7 +34,6 @@ export class GetRecognizedTransactionsService {
q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule'); q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId'); q.whereNotNull('recognizedTransactionId');
q.modify('notExcluded');
if (_filter.accountId) { if (_filter.accountId) {
q.where('accountId', _filter.accountId); q.where('accountId', _filter.accountId);

View File

@@ -8,7 +8,6 @@ import {
ICashflowTransactionUncategorizedPayload, ICashflowTransactionUncategorizedPayload,
ICashflowTransactionUncategorizingPayload, ICashflowTransactionUncategorizingPayload,
} from '@/interfaces'; } from '@/interfaces';
import { validateTransactionShouldBeCategorized } from './utils';
@Service() @Service()
export class UncategorizeCashflowTransaction { export class UncategorizeCashflowTransaction {
@@ -25,12 +24,11 @@ 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 =
@@ -38,22 +36,6 @@ 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.
@@ -61,36 +43,30 @@ 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.
await UncategorizedCashflowTransaction.query(trx) const uncategorizedTransaction =
.whereIn('id', oldUncategoirzedTransactionsIds) await UncategorizedCashflowTransaction.query(trx).updateAndFetchById(
.patch({ uncategorizedTransactionId,
categorized: false, {
categorizeRefId: null, categorized: false,
categorizeRefType: null, categorizeRefId: 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,
uncategorizedTransactionId, uncategorizedTransaction,
uncategorizedTransactions, oldUncategorizedTransaction,
oldUncategorizedTransactions,
trx, trx,
} as ICashflowTransactionUncategorizedPayload } as ICashflowTransactionUncategorizedPayload
); );
return oldUncategoirzedTransactionsIds; return uncategorizedTransaction;
}); });
} }
} }

View File

@@ -16,9 +16,7 @@ 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 {

View File

@@ -5,7 +5,6 @@ import {
ICashflowTransactionCategorizedPayload, ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload, ICashflowTransactionUncategorizedPayload,
} from '@/interfaces'; } from '@/interfaces';
import PromisePool from '@supercharge/promise-pool';
@Service() @Service()
export class DecrementUncategorizedTransactionOnCategorize { export class DecrementUncategorizedTransactionOnCategorize {
@@ -35,18 +34,13 @@ export class DecrementUncategorizedTransactionOnCategorize {
*/ */
public async decrementUnCategorizedTransactionsOnCategorized({ public async decrementUnCategorizedTransactionsOnCategorized({
tenantId, tenantId,
uncategorizedTransactions, uncategorizedTransaction,
trx
}: ICashflowTransactionCategorizedPayload) { }: ICashflowTransactionCategorizedPayload) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1) await Account.query()
.for(uncategorizedTransactions) .findById(uncategorizedTransaction.accountId)
.process(async (uncategorizedTransaction) => { .decrement('uncategorizedTransactions', 1);
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1);
});
} }
/** /**
@@ -55,18 +49,13 @@ export class DecrementUncategorizedTransactionOnCategorize {
*/ */
public async incrementUnCategorizedTransactionsOnUncategorized({ public async incrementUnCategorizedTransactionsOnUncategorized({
tenantId, tenantId,
uncategorizedTransactions, uncategorizedTransaction,
trx
}: ICashflowTransactionUncategorizedPayload) { }: ICashflowTransactionUncategorizedPayload) {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1) await Account.query()
.for(uncategorizedTransactions) .findById(uncategorizedTransaction.accountId)
.process(async (uncategorizedTransaction) => { .increment('uncategorizedTransactions', 1);
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
});
} }
/** /**

View File

@@ -1,10 +1,8 @@
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 {
@@ -27,27 +25,18 @@ export class DeleteCashflowTransactionOnUncategorize {
*/ */
public async deleteCashflowTransactionOnUncategorize({ public async deleteCashflowTransactionOnUncategorize({
tenantId, tenantId,
oldUncategorizedTransactions, oldUncategorizedTransaction,
trx, trx,
}: ICashflowTransactionUncategorizedPayload) { }: ICashflowTransactionUncategorizedPayload) {
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter(
(transaction) => transaction.categorizeRefType === 'CashflowTransaction'
);
// Deletes the cashflow transaction. // Deletes the cashflow transaction.
if (_oldUncategorizedTransactions.length > 0) { if (
const result = await PromisePool.withConcurrency(1) oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
.for(_oldUncategorizedTransactions) ) {
.process(async (oldUncategorizedTransaction) => { await this.deleteCashflowTransactionService.deleteCashflowTransaction(
await this.deleteCashflowTransactionService.deleteCashflowTransaction( tenantId,
tenantId,
oldUncategorizedTransaction.categorizeRefId, oldUncategorizedTransaction.categorizeRefId
trx );
);
});
if (result.errors.length > 0) {
throw new ServiceError('SOMETHING_WRONG');
}
} }
} }
} }

View File

@@ -1,8 +1,7 @@
import { upperFirst, camelCase, first, sum, sumBy } from 'lodash'; import { upperFirst, camelCase } 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 {
@@ -10,8 +9,6 @@ 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.
@@ -30,9 +27,7 @@ export const transformCashflowTransactionType = (type) => {
export function getCashflowTransactionType( export function getCashflowTransactionType(
transactionType: CASHFLOW_TRANSACTION_TYPE transactionType: CASHFLOW_TRANSACTION_TYPE
): ICashflowTransactionTypeMeta { ): ICashflowTransactionTypeMeta {
const _transactionType = transformCashflowTransactionType(transactionType); return CASHFLOW_TRANSACTION_TYPE_META[transactionType];
return CASHFLOW_TRANSACTION_TYPE_META[_transactionType];
} }
/** /**
@@ -51,46 +46,22 @@ export const getCashflowAccountTransactionsTypes = () => {
* @returns {ICashflowNewCommandDTO} * @returns {ICashflowNewCommandDTO}
*/ */
export const transformCategorizeTransToCashflow = ( export const transformCategorizeTransToCashflow = (
uncategorizeTransactions: Array<IUncategorizedCashflowTransaction>, uncategorizeModel: IUncategorizedCashflowTransaction,
categorizeDTO: ICategorizeCashflowTransactioDTO categorizeDTO: ICategorizeCashflowTransactioDTO
): ICashflowNewCommandDTO => { ): ICashflowNewCommandDTO => {
const uncategorizeTransaction = first(uncategorizeTransactions);
const amount = sumBy(uncategorizeTransactions, 'amount');
const amountAbs = Math.abs(amount);
return { return {
date: categorizeDTO.date, date: uncategorizeModel.date,
referenceNo: categorizeDTO.referenceNo, referenceNo: categorizeDTO.referenceNo || uncategorizeModel.referenceNo,
description: categorizeDTO.description, description: categorizeDTO.description || uncategorizeModel.description,
cashflowAccountId: uncategorizeTransaction.accountId, cashflowAccountId: uncategorizeModel.accountId,
creditAccountId: categorizeDTO.creditAccountId, creditAccountId: categorizeDTO.creditAccountId,
exchangeRate: categorizeDTO.exchangeRate || 1, exchangeRate: categorizeDTO.exchangeRate || 1,
currencyCode: categorizeDTO.currencyCode, currencyCode: uncategorizeModel.currencyCode,
amount: amountAbs, amount: uncategorizeModel.amount,
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);
}
};

View File

@@ -1,9 +1,5 @@
// @ts-nocheck import React from 'react';
import React, { forwardRef, Ref } from 'react'; import { AppShellProvider, useAppShellContext } from './AppContentShellProvider';
import {
AppShellProvider,
useAppShellContext,
} from './AppContentShellProvider';
import { Box, BoxProps } from '../../Layout'; import { Box, BoxProps } from '../../Layout';
import styles from './AppContentShell.module.scss'; import styles from './AppContentShell.module.scss';
@@ -16,73 +12,50 @@ interface AppContentShellProps {
hideMain?: boolean; hideMain?: boolean;
} }
export const AppContentShell = forwardRef( export function AppContentShell({
( asideProps,
{ mainProps,
asideProps, topbarOffset = 0,
mainProps, hideAside = false,
topbarOffset = 0, hideMain = false,
hideAside = false, ...restProps
hideMain = false, }: AppContentShellProps) {
...restProps return (
}: AppContentShellProps, <AppShellProvider
ref: Ref<HTMLDivElement>, mainProps={mainProps}
) => { asideProps={asideProps}
return ( topbarOffset={topbarOffset}
<AppShellProvider hideAside={hideAside}
mainProps={mainProps} hideMain={hideMain}
asideProps={asideProps} >
topbarOffset={topbarOffset} <Box {...restProps} className={styles.root} />
hideAside={hideAside} </AppShellProvider>
hideMain={hideMain} );
> }
<Box {...restProps} className={styles.root} ref={ref} />
</AppShellProvider>
);
},
);
AppContentShell.displayName = 'AppContentShell';
interface AppContentShellMainProps extends BoxProps {} interface AppContentShellMainProps extends BoxProps {}
/** function AppContentShellMain({ ...props }: AppContentShellMainProps) {
* Main content of the app shell. const { hideMain } = useAppShellContext();
* @param {AppContentShellMainProps} props -
* @returns {React.ReactNode}
*/
const AppContentShellMain = forwardRef(
({ ...props }: AppContentShellMainProps, ref: Ref<HTMLDivElement>) => {
const { hideMain } = useAppShellContext();
if (hideMain === true) { if (hideMain === true) {
return null; return null;
} }
return <Box {...props} className={styles.main} ref={ref} />; return <Box {...props} className={styles.main} />;
}, }
);
AppContentShellMain.displayName = 'AppContentShellMain';
interface AppContentShellAsideProps extends BoxProps { interface AppContentShellAsideProps extends BoxProps {
children: React.ReactNode; children: React.ReactNode;
} }
/** function AppContentShellAside({ ...props }: AppContentShellAsideProps) {
* Aside content of the app shell. const { hideAside } = useAppShellContext();
* @param {AppContentShellAsideProps} props
* @returns {React.ReactNode}
*/
const AppContentShellAside = forwardRef(
({ ...props }: AppContentShellAsideProps, ref: Ref<HTMLDivElement>) => {
const { hideAside } = useAppShellContext();
if (hideAside === true) { if (hideAside === true) {
return null; return null;
} }
return <Box {...props} className={styles.aside} ref={ref} />; return <Box {...props} className={styles.aside} />;
}, }
);
AppContentShellAside.displayName = 'AppContentShellAside';
AppContentShell.Main = AppContentShellMain; AppContentShell.Main = AppContentShellMain;
AppContentShell.Aside = AppContentShellAside; AppContentShell.Aside = AppContentShellAside;

View File

@@ -15,7 +15,7 @@ import { useAuthActions } from '@/hooks/state';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useAuthenticatedAccount } from '@/hooks/query'; import { useAuthenticatedAccount, useDashboardMeta } from '@/hooks/query';
import { firstLettersArgs, compose } from '@/utils'; import { firstLettersArgs, compose } from '@/utils';
/** /**
@@ -31,6 +31,9 @@ function DashboardTopbarUser({
// Retrieve authenticated user information. // Retrieve authenticated user information.
const { data: user } = useAuthenticatedAccount(); const { data: user } = useAuthenticatedAccount();
const { data: dashboardMeta } = useDashboardMeta({
keepPreviousData: true,
});
const onClickLogout = () => { const onClickLogout = () => {
setLogout(); setLogout();
}; };
@@ -58,6 +61,12 @@ function DashboardTopbarUser({
} }
/> />
<MenuDivider /> <MenuDivider />
{dashboardMeta.is_bigcapital_cloud && (
<MenuItem
text={'Billing'}
onClick={() => history.push('/billing')}
/>
)}
<MenuItem <MenuItem
text={<T id={'keyboard_shortcuts'} />} text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut} onClick={onKeyboardShortcut}

View File

@@ -25,13 +25,14 @@ function TableVirtualizedListRow({ index, isScrolling, isVisible, style }) {
export function TableVirtualizedListRows() { export function TableVirtualizedListRows() {
const { const {
table: { page }, table: { page },
props: { vListrowHeight, vListOverscanRowCount, windowScrollerProps }, props: { vListrowHeight, vListOverscanRowCount },
} = useContext(TableContext); } = useContext(TableContext);
// Dashboard content pane. // Dashboard content pane.
const scrollElement = const dashboardContentPane = React.useMemo(
windowScrollerProps?.scrollElement || () => document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`),
document.querySelector(`.${CLASSES.DASHBOARD_CONTENT_PANE}`); [],
);
const rowRenderer = React.useCallback( const rowRenderer = React.useCallback(
({ key, ...args }) => <TableVirtualizedListRow {...args} key={key} />, ({ key, ...args }) => <TableVirtualizedListRow {...args} key={key} />,
@@ -39,7 +40,7 @@ export function TableVirtualizedListRows() {
); );
return ( return (
<WindowScroller scrollElement={scrollElement}> <WindowScroller scrollElement={dashboardContentPane}>
{({ height, isScrolling, onChildScroll, scrollTop }) => ( {({ height, isScrolling, onChildScroll, scrollTop }) => (
<AutoSizer disableHeight> <AutoSizer disableHeight>
{({ width }) => ( {({ width }) => (

View File

@@ -52,7 +52,6 @@ import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/Rec
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog'; import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
import { ExportDialog } from '@/containers/Dialogs/ExportDialog'; import { ExportDialog } from '@/containers/Dialogs/ExportDialog';
import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog'; import { RuleFormDialog } from '@/containers/Banking/Rules/RuleFormDialog/RuleFormDialog';
import { DisconnectBankAccountDialog } from '@/containers/CashFlow/AccountTransactions/dialogs/DisconnectBankAccountDialog/DisconnectBankAccountDialog';
/** /**
* Dialogs container. * Dialogs container.
@@ -150,9 +149,6 @@ export default function DialogsContainer() {
<PaymentMailDialog dialogName={DialogsName.PaymentMail} /> <PaymentMailDialog dialogName={DialogsName.PaymentMail} />
<ExportDialog dialogName={DialogsName.Export} /> <ExportDialog dialogName={DialogsName.Export} />
<RuleFormDialog dialogName={DialogsName.BankRuleForm} /> <RuleFormDialog dialogName={DialogsName.BankRuleForm} />
<DisconnectBankAccountDialog
dialogName={DialogsName.DisconnectBankAccountConfirmation}
/>
</div> </div>
); );
} }

View File

@@ -1,15 +1,12 @@
import React, { forwardRef, Ref } from 'react'; import React from 'react';
import { HTMLDivProps, Props } from '@blueprintjs/core'; import { HTMLDivProps, Props } from '@blueprintjs/core';
export interface BoxProps extends Props, HTMLDivProps { export interface BoxProps extends Props, HTMLDivProps {
className?: string; className?: string;
} }
export const Box = forwardRef( export function Box({ className, ...rest }: BoxProps) {
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => { const Element = 'div';
const Element = 'div';
return <Element className={className} ref={ref} {...rest} />; return <Element className={className} {...rest} />;
}, }
);
Box.displayName = '@bigcapital/Box';

View File

@@ -75,6 +75,5 @@ export enum DialogsName {
GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview', GeneralLedgerPdfPreview = 'GeneralLedgerPdfPreview',
SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview', SalesTaxLiabilitySummaryPdfPreview = 'SalesTaxLiabilitySummaryPdfPreview',
Export = 'Export', Export = 'Export',
BankRuleForm = 'BankRuleForm', BankRuleForm = 'BankRuleForm'
DisconnectBankAccountConfirmation = 'DisconnectBankAccountConfirmation',
} }

View File

@@ -8,10 +8,6 @@ export default [
disabled: false, disabled: false,
href: '/preferences/general', href: '/preferences/general',
}, },
{
text: 'Billing',
href: '/preferences/billing',
},
{ {
text: <T id={'users'} />, text: <T id={'users'} />,
href: '/preferences/users', href: '/preferences/users',

View File

@@ -1,10 +0,0 @@
export const BANK_QUERY_KEY = {
BANK_RULES: 'BANK_RULE',
BANK_TRANSACTION_MATCHES: 'BANK_TRANSACTION_MATCHES',
RECOGNIZED_BANK_TRANSACTION: 'RECOGNIZED_BANK_TRANSACTION',
EXCLUDED_BANK_TRANSACTIONS_INFINITY: 'EXCLUDED_BANK_TRANSACTIONS_INFINITY',
RECOGNIZED_BANK_TRANSACTIONS_INFINITY:
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
};

View File

@@ -28,7 +28,6 @@ import TaxRatesAlerts from '@/containers/TaxRates/alerts';
import { CashflowAlerts } from '../CashFlow/CashflowAlerts'; import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts'; import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts'; import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
export default [ export default [
...AccountsAlerts, ...AccountsAlerts,
@@ -59,6 +58,5 @@ export default [
...TaxRatesAlerts, ...TaxRatesAlerts,
...CashflowAlerts, ...CashflowAlerts,
...BankRulesAlerts, ...BankRulesAlerts,
...SubscriptionAlerts, ...SubscriptionAlerts
...BankAccountAlerts,
]; ];

View File

@@ -12,7 +12,6 @@ import {
PopoverInteractionKind, PopoverInteractionKind,
Position, Position,
Intent, Intent,
Switch,
Tooltip, Tooltip,
MenuDivider, MenuDivider,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
@@ -41,14 +40,12 @@ import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { import {
useDisconnectBankAccount,
useUpdateBankAccount, useUpdateBankAccount,
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 { DialogsName } from '@/constants/dialogs';
function AccountTransactionsActionsBar({ function AccountTransactionsActionsBar({
// #withDialogActions // #withDialogActions
@@ -63,13 +60,6 @@ function AccountTransactionsActionsBar({
// #withBanking // #withBanking
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside,
// #withBankingActions
enableMultipleCategorization,
// #withAlerts
openAlert,
}) { }) {
const history = useHistory(); const history = useHistory();
const { accountId, currentAccount } = useAccountTransactionsContext(); const { accountId, currentAccount } = useAccountTransactionsContext();
@@ -77,6 +67,7 @@ function AccountTransactionsActionsBar({
// Refresh cashflow infinity transactions hook. // Refresh cashflow infinity transactions hook.
const { refresh } = useRefreshCashflowTransactionsInfinity(); const { refresh } = useRefreshCashflowTransactionsInfinity();
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
const { mutateAsync: updateBankAccount } = useUpdateBankAccount(); const { mutateAsync: updateBankAccount } = useUpdateBankAccount();
// Retrieves the money in/out buttons options. // Retrieves the money in/out buttons options.
@@ -84,7 +75,6 @@ function AccountTransactionsActionsBar({
const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []); const addMoneyOutOptions = useMemo(() => getAddMoneyOutOptions(), []);
const isFeedsActive = !!currentAccount.is_feeds_active; const isFeedsActive = !!currentAccount.is_feeds_active;
const isFeedsPaused = currentAccount.is_feeds_paused;
const isSyncingOwner = currentAccount.is_syncing_owner; const isSyncingOwner = currentAccount.is_syncing_owner;
// Handle table row size change. // Handle table row size change.
@@ -118,9 +108,19 @@ function AccountTransactionsActionsBar({
// Handles the bank account disconnect click. // Handles the bank account disconnect click.
const handleDisconnectClick = () => { const handleDisconnectClick = () => {
openDialog(DialogsName.DisconnectBankAccountConfirmation, { disconnectBankAccount({ bankAccountId: accountId })
bankAccountId: accountId, .then(() => {
}); AppToaster.show({
message: 'The bank account has been disconnected.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
}; };
// handles the bank update button click. // handles the bank update button click.
const handleBankUpdateClick = () => { const handleBankUpdateClick = () => {
@@ -191,23 +191,6 @@ function AccountTransactionsActionsBar({
}); });
}; };
// Handle multi select transactions for categorization or matching.
const handleMultipleCategorizingSwitch = (event) => {
enableMultipleCategorization(event.currentTarget.checked);
}
// Handle resume bank feeds syncing.
const handleResumeFeedsSyncing = () => {
openAlert('resume-feeds-syncing-bank-accounnt', {
bankAccountId: accountId,
});
};
// Handles pause bank feeds syncing.
const handlePauseFeedsSyncing = () => {
openAlert('pause-feeds-syncing-bank-accounnt', {
bankAccountId: accountId,
});
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -255,9 +238,7 @@ function AccountTransactionsActionsBar({
<Tooltip <Tooltip
content={ content={
isFeedsActive isFeedsActive
? isFeedsPaused ? 'The bank syncing is active'
? 'The bank syncing is paused'
: 'The bank syncing is active'
: 'The bank syncing is disconnected' : 'The bank syncing is disconnected'
} }
minimal={true} minimal={true}
@@ -266,13 +247,7 @@ function AccountTransactionsActionsBar({
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="feed" iconSize={16} />} icon={<Icon icon="feed" iconSize={16} />}
intent={ intent={isFeedsActive ? Intent.SUCCESS : Intent.DANGER}
isFeedsActive
? isFeedsPaused
? Intent.WARNING
: Intent.SUCCESS
: Intent.DANGER
}
/> />
</Tooltip> </Tooltip>
</If> </If>
@@ -300,22 +275,6 @@ 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}
@@ -329,23 +288,6 @@ function AccountTransactionsActionsBar({
<MenuItem onClick={handleBankUpdateClick} text={'Update'} /> <MenuItem onClick={handleBankUpdateClick} text={'Update'} />
<MenuDivider /> <MenuDivider />
</If> </If>
<If condition={isSyncingOwner && isFeedsActive && !isFeedsPaused}>
<MenuItem
onClick={handlePauseFeedsSyncing}
text={'Pause bank feeds'}
/>
<MenuDivider />
</If>
<If condition={isSyncingOwner && isFeedsActive && isFeedsPaused}>
<MenuItem
onClick={handleResumeFeedsSyncing}
text={'Resume bank feeds'}
/>
<MenuDivider />
</If>
<MenuItem onClick={handleBankRulesClick} text={'Bank rules'} /> <MenuItem onClick={handleBankRulesClick} text={'Bank rules'} />
<If condition={isSyncingOwner && isFeedsActive}> <If condition={isSyncingOwner && isFeedsActive}>
@@ -369,7 +311,6 @@ function AccountTransactionsActionsBar({
export default compose( export default compose(
withDialogActions, withDialogActions,
withAlertActions,
withSettingsActions, withSettingsActions,
withSettings(({ cashflowTransactionsSettings }) => ({ withSettings(({ cashflowTransactionsSettings }) => ({
cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize,
@@ -378,12 +319,9 @@ export default compose(
({ ({
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside,
}) => ({ }) => ({
uncategorizedTransationsIdsSelected, uncategorizedTransationsIdsSelected,
excludedTransactionsIdsSelected, excludedTransactionsIdsSelected,
openMatchingTransactionAside,
}), }),
), ),
withBankingActions,
)(AccountTransactionsActionsBar); )(AccountTransactionsActionsBar);

View File

@@ -1,5 +1,4 @@
// @ts-nocheck // @ts-nocheck
import { useMemo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ContentTabs } from '@/components/ContentTabs/ContentTabs'; import { ContentTabs } from '@/components/ContentTabs/ContentTabs';
import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { useAccountTransactionsContext } from './AccountTransactionsProvider';
@@ -9,19 +8,15 @@ const AccountContentTabs = styled(ContentTabs)`
`; `;
export function AccountTransactionsFilterTabs() { export function AccountTransactionsFilterTabs() {
const { filterTab, setFilterTab, bankAccountMetaSummary, currentAccount } = const { filterTab, setFilterTab, currentAccount } =
useAccountTransactionsContext(); useAccountTransactionsContext();
const handleChange = (value) => { const handleChange = (value) => {
setFilterTab(value); setFilterTab(value);
}; };
// Detarmines whether show the uncategorized transactions tab. const hasUncategorizedTransx = Boolean(
const hasUncategorizedTransx = useMemo( currentAccount.uncategorized_transactions,
() =>
bankAccountMetaSummary?.totalUncategorizedTransactions > 0 ||
bankAccountMetaSummary?.totalExcludedTransactions > 0,
[bankAccountMetaSummary],
); );
return ( return (

View File

@@ -29,41 +29,28 @@ function AccountTransactionsListRoot({
return ( return (
<AccountTransactionsProvider> <AccountTransactionsProvider>
<AppContentShell hideAside={!openMatchingTransactionAside}> <AppContentShell hideAside={!openMatchingTransactionAside}>
<AccountTransactionsMain /> <AppContentShell.Main>
<AccountTransactionsAside /> <AccountTransactionsActionsBar />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AppContentShell.Main>
<AppContentShell.Aside>
<CategorizeTransactionAside />
</AppContentShell.Aside>
</AppContentShell> </AppContentShell>
</AccountTransactionsProvider> </AccountTransactionsProvider>
); );
} }
function AccountTransactionsMain() {
const { setScrollableRef } = useAccountTransactionsContext();
return (
<AppContentShell.Main ref={(e) => setScrollableRef(e)}>
<AccountTransactionsActionsBar />
<AccountTransactionsDetailsBar />
<AccountTransactionsProgressBar />
<DashboardPageContent>
<AccountTransactionsFilterTabs />
<Suspense fallback={<Spinner size={30} />}>
<AccountTransactionsContent />
</Suspense>
</DashboardPageContent>
</AppContentShell.Main>
);
}
function AccountTransactionsAside() {
return (
<AppContentShell.Aside>
<CategorizeTransactionAside />
</AppContentShell.Aside>
);
}
export default R.compose( export default R.compose(
withBanking( withBanking(
({ selectedUncategorizedTransactionId, openMatchingTransactionAside }) => ({ ({ selectedUncategorizedTransactionId, openMatchingTransactionAside }) => ({

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { useRef, useState } from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { DashboardInsider } from '@/components'; import { DashboardInsider } from '@/components';
import { useCashflowAccounts, useAccount } from '@/hooks/query'; import { useCashflowAccounts, useAccount } from '@/hooks/query';
@@ -41,8 +41,6 @@ function AccountTransactionsProvider({ query, ...props }) {
isLoading: isBankAccountMetaSummaryLoading, isLoading: isBankAccountMetaSummaryLoading,
} = useGetBankAccountSummaryMeta(accountId); } = useGetBankAccountSummaryMeta(accountId);
const [scrollableRef, setScrollableRef] = useState();
// Provider payload. // Provider payload.
const provider = { const provider = {
accountId, accountId,
@@ -58,9 +56,6 @@ function AccountTransactionsProvider({ query, ...props }) {
filterTab, filterTab,
setFilterTab, setFilterTab,
scrollableRef,
setScrollableRef
}; };
return ( return (

View File

@@ -1,6 +1,5 @@
// @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 {
@@ -13,20 +12,17 @@ import {
AppToaster, AppToaster,
} from '@/components'; } from '@/components';
import { TABLES } from '@/constants/tables'; import { TABLES } from '@/constants/tables';
import { ActionsMenu } from './components'; import { ActionsMenu } from './UncategorizedTransactions/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 { useAccountUncategorizedTransactionsContext } from '../AllTransactionsUncategorizedBoot'; import { useAccountUncategorizedTransactionsColumns } from './components';
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { useAccountUncategorizedTransactionsColumns } from './hooks';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
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.
@@ -35,22 +31,13 @@ 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();
const { scrollableRef } = useAccountTransactionsContext();
// Retrieve list context. // Retrieve list context.
const { uncategorizedTransactions, isUncategorizedTransactionsLoading } = const { uncategorizedTransactions, isUncategorizedTransactionsLoading } =
useAccountUncategorizedTransactionsContext(); useAccountUncategorizedTransactionsContext();
@@ -64,21 +51,12 @@ function AccountTransactionsDataTable({
// Handle cell click. // Handle cell click.
const handleCellClick = (cell) => { const handleCellClick = (cell) => {
if (enableMultipleCategorization) { setUncategorizedTransactionIdForMatching(cell.row.original.id);
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) => {
setUncategorizedTransactionIdForMatching(transaction.id); setUncategorizedTransactionIdForMatching(transaction.id);
}; };
// handles table selected rows change.
const handleSelectedRowsChange = (selected) => {
const transactionIds = selected.map((r) => r.original.id);
setUncategorizedTransactionsSelected(transactionIds);
};
// Handle exclude transaction. // Handle exclude transaction.
const handleExcludeTransaction = (transaction) => { const handleExcludeTransaction = (transaction) => {
excludeTransaction(transaction.id) excludeTransaction(transaction.id)
@@ -88,7 +66,7 @@ function AccountTransactionsDataTable({
message: 'The bank transaction has been excluded successfully.', message: 'The bank transaction has been excluded successfully.',
}); });
}) })
.catch(() => { .catch((error) => {
AppToaster.show({ AppToaster.show({
intent: Intent.DANGER, intent: Intent.DANGER,
message: 'Something went wrong.', message: 'Something went wrong.',
@@ -96,6 +74,12 @@ 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}
@@ -122,15 +106,12 @@ 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,
}} }}
onSelectedRowsChange={handleSelectedRowsChange}
windowScrollerProps={{ scrollElement: scrollableRef }}
className={clsx('table-constrant', styles.table, {
[styles.showCategorizeColumn]: enableMultipleCategorization,
})}
/> />
); );
} }
@@ -140,12 +121,6 @@ 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)`

View File

@@ -16,7 +16,6 @@ import { TABLES } from '@/constants/tables';
import { useMemorizedColumnsWidths } from '@/hooks'; import { useMemorizedColumnsWidths } from '@/hooks';
import { useExcludedTransactionsColumns } from './_utils'; import { useExcludedTransactionsColumns } from './_utils';
import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot'; import { useExcludedTransactionsBoot } from './ExcludedTransactionsTableBoot';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { ActionsMenu } from './_components'; import { ActionsMenu } from './_components';
import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useUnexcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
@@ -38,8 +37,6 @@ function ExcludedTransactionsTableRoot({
const { mutateAsync: unexcludeBankTransaction } = const { mutateAsync: unexcludeBankTransaction } =
useUnexcludeUncategorizedTransaction(); useUnexcludeUncategorizedTransaction();
const { scrollableRef } = useAccountTransactionsContext();
// Retrieve table columns. // Retrieve table columns.
const columns = useExcludedTransactionsColumns(); const columns = useExcludedTransactionsColumns();
@@ -100,7 +97,6 @@ function ExcludedTransactionsTableRoot({
className="table-constrant" className="table-constrant"
selectionColumn={true} selectionColumn={true}
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
windowScrollerProps={{ scrollElement: scrollableRef }}
payload={{ payload={{
onRestore: handleRestoreClick, onRestore: handleRestoreClick,
}} }}

View File

@@ -20,7 +20,6 @@ import { useRecognizedTransactionsBoot } from './RecognizedTransactionsTableBoot
import { ActionsMenu } from './_components'; import { ActionsMenu } from './_components';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { useAccountTransactionsContext } from '../AccountTransactionsProvider';
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules'; import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
import { import {
WithBankingActionsProps, WithBankingActionsProps,
@@ -34,8 +33,8 @@ interface RecognizedTransactionsTableProps extends WithBankingActionsProps {}
* Renders the recognized account transactions datatable. * Renders the recognized account transactions datatable.
*/ */
function RecognizedTransactionsTableRoot({ function RecognizedTransactionsTableRoot({
// #withBankingActions // #withBanking
setTransactionsToCategorizeSelected, setUncategorizedTransactionIdForMatching,
}: RecognizedTransactionsTableProps) { }: RecognizedTransactionsTableProps) {
const { mutateAsync: excludeBankTransaction } = const { mutateAsync: excludeBankTransaction } =
useExcludeUncategorizedTransaction(); useExcludeUncategorizedTransaction();
@@ -50,11 +49,9 @@ function RecognizedTransactionsTableRoot({
const [initialColumnsWidths, , handleColumnResizing] = const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_ACCOUNT_TRANSACTIONS); useMemorizedColumnsWidths(TABLES.UNCATEGORIZED_ACCOUNT_TRANSACTIONS);
const { scrollableRef } = useAccountTransactionsContext();
// Handle cell click. // Handle cell click.
const handleCellClick = (cell, event) => { const handleCellClick = (cell, event) => {
setTransactionsToCategorizeSelected( setUncategorizedTransactionIdForMatching(
cell.row.original.uncategorized_transaction_id, cell.row.original.uncategorized_transaction_id,
); );
}; };
@@ -77,7 +74,7 @@ function RecognizedTransactionsTableRoot({
// Handles categorize button click. // Handles categorize button click.
const handleCategorizeClick = (transaction) => { const handleCategorizeClick = (transaction) => {
setTransactionsToCategorizeSelected( setUncategorizedTransactionIdForMatching(
transaction.uncategorized_transaction_id, transaction.uncategorized_transaction_id,
); );
}; };
@@ -105,7 +102,6 @@ function RecognizedTransactionsTableRoot({
vListOverscanRowCount={0} vListOverscanRowCount={0}
initialColumnsWidths={initialColumnsWidths} initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing} onColumnResizing={handleColumnResizing}
windowScrollerProps={{ scrollElement: scrollableRef }}
noResults={<RecognizedTransactionsTableNoResults />} noResults={<RecognizedTransactionsTableNoResults />}
className="table-constrant" className="table-constrant"
payload={{ payload={{

View File

@@ -43,7 +43,6 @@ function RecognizedTransactionsTableBoot({
hasNextPage: hasUncategorizedTransactionsNextPage, hasNextPage: hasUncategorizedTransactionsNextPage,
} = useRecognizedBankTransactionsInfinity({ } = useRecognizedBankTransactionsInfinity({
page_size: 50, page_size: 50,
account_id: accountId,
}); });
// Memorized the cashflow account transactions. // Memorized the cashflow account transactions.
const recognizedTransactions = React.useMemo( const recognizedTransactions = React.useMemo(

View File

@@ -1,15 +0,0 @@
.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;
}

View File

@@ -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 {

View File

@@ -1,157 +0,0 @@
// @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',
},
],
[],
);
}

View File

@@ -1,68 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster, FormattedMessage as T } from '@/components';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { usePauseFeedsBankAccount } from '@/hooks/query/bank-accounts';
import { compose } from '@/utils';
/**
* Pause feeds of the bank account alert.
*/
function PauseFeedsBankAccountAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { bankAccountId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: pauseBankAccountFeeds, isLoading } =
usePauseFeedsBankAccount();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
closeAlert(name);
};
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
pauseBankAccountFeeds({ bankAccountId })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been paused.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Pause bank feeds'}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
Are you sure want to pause bank feeds syncing of this bank account, you
can always resume it again?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(PauseFeedsBankAccountAlert);

View File

@@ -1,69 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster, FormattedMessage as T } from '@/components';
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { useResumeFeedsBankAccount } from '@/hooks/query/bank-accounts';
import { compose } from '@/utils';
/**
* Resume bank account feeds alert.
*/
function ResumeFeedsBankAccountAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { bankAccountId },
// #withAlertActions
closeAlert,
}) {
const { mutateAsync: resumeFeedsBankAccount, isLoading } =
useResumeFeedsBankAccount();
// Handle activate item alert cancel.
const handleCancelActivateItem = () => {
closeAlert(name);
};
// Handle confirm item activated.
const handleConfirmItemActivate = () => {
resumeFeedsBankAccount({ bankAccountId })
.then(() => {
AppToaster.show({
message: 'The bank feeds of the bank account has been resumed.',
intent: Intent.SUCCESS,
});
})
.catch((error) => {})
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Resume bank feeds'}
intent={Intent.SUCCESS}
isOpen={isOpen}
onCancel={handleCancelActivateItem}
loading={isLoading}
onConfirm={handleConfirmItemActivate}
>
<p>
Are you sure want to resume bank feeds syncing of this bank account, you
can always pause it again?
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ResumeFeedsBankAccountAlert);

View File

@@ -1,24 +0,0 @@
// @ts-nocheck
import React from 'react';
const ResumeFeedsBankAccountAlert = React.lazy(
() => import('./ResumeFeedsBankAccount'),
);
const PauseFeedsBankAccountAlert = React.lazy(
() => import('./PauseFeedsBankAccount'),
);
/**
* Bank account alerts.
*/
export const BankAccountAlerts = [
{
name: 'resume-feeds-syncing-bank-accounnt',
component: ResumeFeedsBankAccountAlert,
},
{
name: 'pause-feeds-syncing-bank-accounnt',
component: PauseFeedsBankAccountAlert,
},
];

View File

@@ -150,3 +150,99 @@ 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,
},
],
[],
);
}

View File

@@ -1,42 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const DisconnectBankAccountDialogContent = React.lazy(
() => import('./DisconnectBankAccountDialogContent'),
);
/**
* Disconnect bank account confirmation dialog.
*/
function DisconnectBankAccountDialogRoot({
dialogName,
payload: { bankAccountId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Disconnect Bank Account'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 400 }}
>
<DialogSuspense>
<DisconnectBankAccountDialogContent
dialogName={dialogName}
bankAccountId={bankAccountId}
/>
</DialogSuspense>
</Dialog>
);
}
export const DisconnectBankAccountDialog = compose(withDialogRedux())(
DisconnectBankAccountDialogRoot,
);
DisconnectBankAccountDialog.displayName = 'DisconnectBankAccountDialog';

View File

@@ -1,104 +0,0 @@
// @ts-nocheck
import * as Yup from 'yup';
import { Button, Intent, Classes } from '@blueprintjs/core';
import * as R from 'ramda';
import { Form, Formik, FormikHelpers } from 'formik';
import { AppToaster, FFormGroup, FInputGroup } from '@/components';
import { useDisconnectBankAccount } from '@/hooks/query/bank-rules';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs';
interface DisconnectFormValues {
label: string;
}
const initialValues = {
label: '',
};
const Schema = Yup.object().shape({
label: Yup.string().required().label('Confirmation'),
});
interface DisconnectBankAccountDialogContentProps {
bankAccountId: number;
}
function DisconnectBankAccountDialogContent({
bankAccountId,
// #withDialogActions
closeDialog,
}: DisconnectBankAccountDialogContentProps) {
const { mutateAsync: disconnectBankAccount } = useDisconnectBankAccount();
const handleSubmit = (
values: DisconnectFormValues,
{ setErrors, setSubmitting }: FormikHelpers<DisconnectFormValues>,
) => {
debugger;
setSubmitting(true);
if (values.label !== 'DISCONNECT ACCOUNT') {
setErrors({
label: 'The entered value is incorrect.',
});
setSubmitting(false);
return;
}
disconnectBankAccount({ bankAccountId })
.then(() => {
setSubmitting(false);
AppToaster.show({
message: 'The bank account has been disconnected.',
intent: Intent.SUCCESS,
});
closeDialog(DialogsName.DisconnectBankAccountConfirmation);
})
.catch((error) => {
setSubmitting(false);
AppToaster.show({
message: 'Something went wrong.',
intent: Intent.DANGER,
});
});
};
const handleCancelBtnClick = () => {
closeDialog(DialogsName.DisconnectBankAccountConfirmation);
};
return (
<Formik
onSubmit={handleSubmit}
validationSchema={Schema}
initialValues={initialValues}
>
<Form>
<div className={Classes.DIALOG_BODY}>
<FFormGroup
label={`Type "DISCONNECT ACCOUNT"`}
name={'label'}
fastField
>
<FInputGroup name={'label'} fastField />
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button type="submit" intent={Intent.DANGER}>
Disconnect Bank Account
</Button>
<Button intent={Intent.NONE} onClick={handleCancelBtnClick}>
Cancel
</Button>
</div>
</div>
</Form>
</Formik>
);
}
export default R.compose(withDialogActions)(DisconnectBankAccountDialogContent);

View File

@@ -6,13 +6,10 @@ 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 { import { useGetRecognizedBankTransaction } from '@/hooks/query/bank-rules';
GetAutofillCategorizeTransaction, import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
useGetAutofillCategorizeTransaction,
} from '@/hooks/query/bank-rules';
interface CategorizeTransactionBootProps { interface CategorizeTransactionBootProps {
uncategorizedTransactionsIds: Array<number>;
children: React.ReactNode; children: React.ReactNode;
} }
@@ -22,8 +19,8 @@ interface CategorizeTransactionBootValue {
isBranchesLoading: boolean; isBranchesLoading: boolean;
isAccountsLoading: boolean; isAccountsLoading: boolean;
primaryBranch: any; primaryBranch: any;
autofillCategorizeValues: null | GetAutofillCategorizeTransaction; recognizedTranasction: any;
isAutofillCategorizeValuesLoading: boolean; isRecognizedTransactionLoading: boolean;
} }
const CategorizeTransactionBootContext = const CategorizeTransactionBootContext =
@@ -35,9 +32,11 @@ 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);
@@ -50,11 +49,13 @@ function CategorizeTransactionBoot({
{}, {},
{ enabled: isBranchFeatureCan }, { enabled: isBranchFeatureCan },
); );
// Fetches the autofill values of categorize transaction. // Fetches the recognized transaction.
const { const {
data: autofillCategorizeValues, data: recognizedTranasction,
isLoading: isAutofillCategorizeValuesLoading, isLoading: isRecognizedTransactionLoading,
} = useGetAutofillCategorizeTransaction(uncategorizedTransactionsIds, {}); } = useGetRecognizedBankTransaction(uncategorizedTransactionId, {
enabled: !!uncategorizedTransaction.is_recognized,
});
// Retrieves the primary branch. // Retrieves the primary branch.
const primaryBranch = useMemo( const primaryBranch = useMemo(
@@ -68,11 +69,11 @@ function CategorizeTransactionBoot({
isBranchesLoading, isBranchesLoading,
isAccountsLoading, isAccountsLoading,
primaryBranch, primaryBranch,
autofillCategorizeValues, recognizedTranasction,
isAutofillCategorizeValuesLoading, isRecognizedTransactionLoading,
}; };
const isLoading = const isLoading =
isBranchesLoading || isAccountsLoading || isAutofillCategorizeValuesLoading; isBranchesLoading || isAccountsLoading || isRecognizedTransactionLoading;
if (isLoading) { if (isLoading) {
<Spinner size={30} />; <Spinner size={30} />;

View File

@@ -8,15 +8,15 @@ export function CategorizeTransactionBranchField() {
const { branches } = useCategorizeTransactionBoot(); const { branches } = useCategorizeTransactionBoot();
return ( return (
<FeatureCan feature={Features.Branches}> <FFormGroup name={'branchId'} label={'Branch'} fastField inline>
<FFormGroup name={'branchId'} label={'Branch'} fastField inline> <FeatureCan feature={Features.Branches}>
<BranchSuggestField <BranchSuggestField
name={'branchId'} name={'branchId'}
items={branches} items={branches}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
fill fill
/> />
</FFormGroup> </FeatureCan>
</FeatureCan> </FFormGroup>
); );
} }

View File

@@ -1,16 +1,15 @@
// @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 { withBanking } from '@/containers/CashFlow/withBanking'; import { useCategorizeTransactionTabsBoot } from '@/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionTabsBoot';
export function CategorizeTransactionContent() {
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
function CategorizeTransactionContentRoot({
transactionsToCategorizeIdsSelected,
}) {
return ( return (
<CategorizeTransactionBoot <CategorizeTransactionBoot
uncategorizedTransactionsIds={transactionsToCategorizeIdsSelected} uncategorizedTransactionId={uncategorizedTransactionId}
> >
<CategorizeTransactionDrawerBody> <CategorizeTransactionDrawerBody>
<CategorizeTransactionForm /> <CategorizeTransactionForm />
@@ -19,12 +18,6 @@ function CategorizeTransactionContentRoot({
); );
} }
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;

View File

@@ -22,7 +22,7 @@ function CategorizeTransactionFormRoot({
// #withBankingActions // #withBankingActions
closeMatchingTransactionAside, closeMatchingTransactionAside,
}) { }) {
const { uncategorizedTransactionIds } = useCategorizeTransactionTabsBoot(); const { uncategorizedTransactionId } = 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 _values = tranformToRequest(values, uncategorizedTransactionIds); const transformedValues = tranformToRequest(values);
setSubmitting(true); setSubmitting(true);
categorizeTransaction(_values) categorizeTransaction([uncategorizedTransactionId, transformedValues])
.then(() => { .then(() => {
setSubmitting(false); setSubmitting(false);

View File

@@ -6,7 +6,6 @@ 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();
@@ -19,18 +18,16 @@ const Title = styled('h3')`
`; `;
export function CategorizeTransactionFormContent() { export function CategorizeTransactionFormContent() {
const { autofillCategorizeValues } = useCategorizeTransactionBoot(); const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
const transactionTypes = autofillCategorizeValues?.isDepositTransaction const transactionTypes = uncategorizedTransaction?.is_deposit_transaction
? 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>{formattedAmount}</Title> <Title>{uncategorizedTransaction.formatted_amount}</Title>
</FormGroup> </FormGroup>
<FFormGroup name={'category'} label={'Category'} fastField inline> <FFormGroup name={'category'} label={'Category'} fastField inline>

View File

@@ -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,28 +18,48 @@ export const defaultInitialValues = {
}; };
export const transformToCategorizeForm = ( export const transformToCategorizeForm = (
autofillCategorizeTransaction: GetAutofillCategorizeTransaction, uncategorizedTransaction: any,
recognizedTransaction?: any,
) => { ) => {
return transformToForm(autofillCategorizeTransaction, defaultInitialValues); let defaultValues = {
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 tranformToRequest = ( export const getRecognizedTransactionDefaultValues = (
formValues: Record<string, any>, recognizedTransaction: any,
uncategorizedTransactionIds: Array<number>,
) => { ) => {
return { return {
uncategorized_transaction_ids: uncategorizedTransactionIds, creditAccountId: recognizedTransaction.assignedAccountId || '',
...transfromToSnakeCase(formValues), // transactionType: recognizedTransaction.assignCategory,
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, autofillCategorizeValues } = const { primaryBranch, recognizedTranasction } =
useCategorizeTransactionBoot(); useCategorizeTransactionBoot();
const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
return { return {
...defaultInitialValues, ...defaultInitialValues,
@@ -48,7 +68,10 @@ 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(autofillCategorizeValues), ...transformToCategorizeForm(
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,

View File

@@ -19,32 +19,20 @@ 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();
} };
// Cannot continue if there is no selected transactions.; const uncategorizedTransactionId = selectedUncategorizedTransactionId;
if (!selectedUncategorizedTransactionId) { if (!selectedUncategorizedTransactionId) {
return null; return null;
} }
@@ -52,7 +40,7 @@ function CategorizeTransactionAsideRoot({
<Aside title={'Categorize Bank Transaction'} onClose={handleClose}> <Aside title={'Categorize Bank Transaction'} onClose={handleClose}>
<Aside.Body> <Aside.Body>
<CategorizeTransactionTabsBoot <CategorizeTransactionTabsBoot
uncategorizedTransactionId={selectedUncategorizedTransactionId} uncategorizedTransactionId={uncategorizedTransactionId}
> >
<CategorizeTransactionTabs /> <CategorizeTransactionTabs />
</CategorizeTransactionTabsBoot> </CategorizeTransactionTabsBoot>
@@ -63,7 +51,7 @@ function CategorizeTransactionAsideRoot({
export const CategorizeTransactionAside = R.compose( export const CategorizeTransactionAside = R.compose(
withBankingActions, withBankingActions,
withBanking(({ transactionsToCategorizeIdsSelected }) => ({ withBanking(({ selectedUncategorizedTransactionId }) => ({
selectedUncategorizedTransactionId: transactionsToCategorizeIdsSelected, selectedUncategorizedTransactionId,
})), })),
)(CategorizeTransactionAsideRoot); )(CategorizeTransactionAsideRoot);

View File

@@ -2,10 +2,14 @@
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 defaultSelectedTabId = 'categorize'; const { uncategorizedTransaction } = useCategorizeTransactionTabsBoot();
const defaultSelectedTabId = uncategorizedTransaction?.is_recognized
? 'categorize'
: 'matching';
return ( return (
<Tabs <Tabs

View File

@@ -1,13 +1,16 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import React from 'react';
import { castArray, uniq } from 'lodash'; import { Spinner } from '@blueprintjs/core';
import { useUncategorizedTransaction } from '@/hooks/query';
interface CategorizeTransactionTabsValue { interface CategorizeTransactionTabsValue {
uncategorizedTransactionIds: Array<number>; uncategorizedTransactionId: number;
isUncategorizedTransactionLoading: boolean;
uncategorizedTransaction: any;
} }
interface CategorizeTransactionTabsBootProps { interface CategorizeTransactionTabsBootProps {
uncategorizedTransactionIds: number | Array<number>; uncategorizedTransactionId: number;
children: React.ReactNode; children: React.ReactNode;
} }
@@ -23,23 +26,28 @@ export function CategorizeTransactionTabsBoot({
uncategorizedTransactionId, uncategorizedTransactionId,
children, children,
}: CategorizeTransactionTabsBootProps) { }: CategorizeTransactionTabsBootProps) {
const uncategorizedTransactionIds = useMemo( const {
() => uniq(castArray(uncategorizedTransactionId)), data: uncategorizedTransaction,
[uncategorizedTransactionId], isLoading: isUncategorizedTransactionLoading,
); } = useUncategorizedTransaction(uncategorizedTransactionId);
const provider = { const provider = {
uncategorizedTransactionIds, uncategorizedTransactionId,
uncategorizedTransaction,
isUncategorizedTransactionLoading,
}; };
// Use a key prop to force re-render of children when `uncategorizedTransactionIds` changes const isLoading = isUncategorizedTransactionLoading;
// 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, { React.cloneElement(child, { key: uncategorizedTransactionId }),
key: uncategorizedTransactionIds?.join(','),
}),
); );
}, [children, uncategorizedTransactionIds]); }, [children, uncategorizedTransactionId]);
if (isLoading) {
return <Spinner size={30} />;
}
return ( return (
<CategorizeTransactionTabsBootContext.Provider value={provider}> <CategorizeTransactionTabsBootContext.Provider value={provider}>
{childrenPerKey} {childrenPerKey}

Some files were not shown because too many files have changed in this diff Show More