mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-24 00:29:49 +00:00
Compare commits
1 Commits
v0.19.1
...
billing-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f30c86f5f |
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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', {});
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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[]}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export interface RevertRecognizedTransactionsCriteria {
|
|
||||||
batch?: string;
|
|
||||||
accountId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface RecognizeTransactionsCriteria {
|
|
||||||
batch?: string;
|
|
||||||
accountId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 }) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
|
||||||
|
|||||||
@@ -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',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
|
||||||
};
|
|
||||||
@@ -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,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 }) => ({
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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)`
|
||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
@@ -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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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);
|
|
||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user