mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-24 00:29:49 +00:00
Compare commits
1 Commits
v0.19.1
...
reconcile-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45ad4aa1f1 |
@@ -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,
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
## [v0.18.0] - 10-08-2024
|
|
||||||
|
|
||||||
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
|
||||||
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
|
||||||
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
|
|
||||||
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
|
|
||||||
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
|
|
||||||
|
|
||||||
## [v0.17.5] - 17-06-2024
|
## [v0.17.5] - 17-06-2024
|
||||||
|
|
||||||
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
|
||||||
|
|||||||
@@ -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,18 +1,14 @@
|
|||||||
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 { param } from 'express-validator';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankAccountsController extends BaseController {
|
export class BankAccountsController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private getBankAccountSummaryService: GetBankAccountSummary;
|
private getBankAccountSummaryService: GetBankAccountSummary;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private bankAccountsApp: BankAccountsApplication;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -20,27 +16,6 @@ export class BankAccountsController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
||||||
router.post(
|
|
||||||
'/:bankAccountId/disconnect',
|
|
||||||
this.disconnectBankAccount.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;
|
||||||
}
|
}
|
||||||
@@ -71,112 +46,4 @@ export class BankAccountsController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Disonnect the given bank account.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response|null>}
|
|
||||||
*/
|
|
||||||
async disconnectBankAccount(
|
|
||||||
req: Request<{ bankAccountId: number }>,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { bankAccountId } = req.params;
|
|
||||||
const { tenantId } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.bankAccountsApp.disconnectBankAccount(tenantId, bankAccountId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
id: bankAccountId,
|
|
||||||
message: 'The bank account has been disconnected.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the given bank account.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response|null>}
|
|
||||||
*/
|
|
||||||
async refreshBankAccount(
|
|
||||||
req: Request<{ bankAccountId: number }>,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { bankAccountId } = req.params;
|
|
||||||
const { tenantId } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.bankAccountsApp.refreshBankAccount(tenantId, bankAccountId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
id: bankAccountId,
|
|
||||||
message: 'The bank account has been disconnected.',
|
|
||||||
});
|
|
||||||
} catch (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,24 @@ 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()
|
||||||
|
.isIn([
|
||||||
|
'interest_income',
|
||||||
|
'other_income',
|
||||||
|
'deposit',
|
||||||
|
'expense',
|
||||||
|
'owner_drawings',
|
||||||
|
]),
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { body, param, query } from 'express-validator';
|
import { param } from 'express-validator';
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
import { NextFunction, Request, Response, Router, query } from 'express';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
|
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
|
||||||
import { map, parseInt, trim } from 'lodash';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransactionsController extends BaseController {
|
export class ExcludeBankTransactionsController extends BaseController {
|
||||||
@@ -16,21 +15,9 @@ export class ExcludeBankTransactionsController extends BaseController {
|
|||||||
public router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.put(
|
|
||||||
'/transactions/exclude',
|
|
||||||
[body('ids').exists()],
|
|
||||||
this.validationResult,
|
|
||||||
this.excludeBulkBankTransactions.bind(this)
|
|
||||||
);
|
|
||||||
router.put(
|
|
||||||
'/transactions/unexclude',
|
|
||||||
[body('ids').exists()],
|
|
||||||
this.validationResult,
|
|
||||||
this.unexcludeBulkBankTransactins.bind(this)
|
|
||||||
);
|
|
||||||
router.put(
|
router.put(
|
||||||
'/transactions/:transactionId/exclude',
|
'/transactions/:transactionId/exclude',
|
||||||
[param('transactionId').exists().toInt()],
|
[param('transactionId').exists()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.excludeBankTransaction.bind(this)
|
this.excludeBankTransaction.bind(this)
|
||||||
);
|
);
|
||||||
@@ -42,11 +29,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)
|
||||||
);
|
);
|
||||||
@@ -111,63 +94,6 @@ export class ExcludeBankTransactionsController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude bank transactions in bulk.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
private async excludeBulkBankTransactions(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const { ids } = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.excludeBankTransactionApp.excludeBankTransactions(
|
|
||||||
tenantId,
|
|
||||||
ids
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'The given bank transactions have been excluded',
|
|
||||||
ids,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unexclude the given bank transactions in bulk.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response | null>}
|
|
||||||
*/
|
|
||||||
private async unexcludeBulkBankTransactins(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<Response | null> {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const { ids } = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.excludeBankTransactionApp.unexcludeBankTransactions(
|
|
||||||
tenantId,
|
|
||||||
ids
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'The given bank transactions have been excluded',
|
|
||||||
ids,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the excluded uncategorized bank transactions.
|
* Retrieves the excluded uncategorized bank transactions.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
@@ -181,8 +107,9 @@ 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);
|
||||||
|
|
||||||
|
console.log('123');
|
||||||
try {
|
try {
|
||||||
const data =
|
const data =
|
||||||
await this.excludeBankTransactionApp.getExcludedBankTransactions(
|
await this.excludeBankTransactionApp.getExcludedBankTransactions(
|
||||||
|
|||||||
@@ -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
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('vendor_id').exists().isNumeric().toInt(),
|
check('vendor_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
check('amount').exists().isNumeric().toFloat(),
|
|
||||||
check('payment_account_id').exists().isNumeric().toInt(),
|
check('payment_account_id').exists().isNumeric().toInt(),
|
||||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
@@ -119,7 +118,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
check('reference').optional().trim().escape(),
|
check('reference').optional().trim().escape(),
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').exists().isArray(),
|
check('entries').exists().isArray({ min: 1 }),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
check('entries.*.bill_id').exists().isNumeric().toInt(),
|
||||||
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
check('customer_id').exists().isNumeric().toInt(),
|
check('customer_id').exists().isNumeric().toInt(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
check('amount').exists().isNumeric().toFloat(),
|
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||||
@@ -159,7 +158,8 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').isArray({}),
|
check('entries').isArray({ min: 1 }),
|
||||||
|
|
||||||
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.index').optional().isNumeric().toInt(),
|
check('entries.*.index').optional().isNumeric().toInt(),
|
||||||
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
check('entries.*.invoice_id').exists().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
|||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
||||||
import { SubscriptionApplication } from '@/services/Subscription/SubscriptionApplication';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SubscriptionController extends BaseController {
|
export class SubscriptionController extends BaseController {
|
||||||
@@ -18,9 +17,6 @@ export class SubscriptionController extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private lemonSqueezyService: LemonSqueezyService;
|
private lemonSqueezyService: LemonSqueezyService;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private subscriptionApp: SubscriptionApplication;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -37,14 +33,6 @@ export class SubscriptionController extends BaseController {
|
|||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.getCheckoutUrl.bind(this)
|
this.getCheckoutUrl.bind(this)
|
||||||
);
|
);
|
||||||
router.post('/cancel', asyncMiddleware(this.cancelSubscription.bind(this)));
|
|
||||||
router.post('/resume', asyncMiddleware(this.resumeSubscription.bind(this)));
|
|
||||||
router.post(
|
|
||||||
'/change',
|
|
||||||
[body('variant_id').exists().trim()],
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.changeSubscriptionPlan.bind(this))
|
|
||||||
);
|
|
||||||
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
@@ -97,84 +85,4 @@ export class SubscriptionController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels the subscription of the current organization.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response|null>}
|
|
||||||
*/
|
|
||||||
private async cancelSubscription(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
status: 200,
|
|
||||||
message: 'The organization subscription has been canceled.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resumes the subscription of the current organization.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response | null>}
|
|
||||||
*/
|
|
||||||
private async resumeSubscription(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.subscriptionApp.resumeSubscription(tenantId);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
status: 200,
|
|
||||||
message: 'The organization subscription has been resumed.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changes the main subscription plan of the current organization.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response | null>}
|
|
||||||
*/
|
|
||||||
public async changeSubscriptionPlan(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const body = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.subscriptionApp.changeSubscriptionPlan(
|
|
||||||
tenantId,
|
|
||||||
body.variantId
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'The subscription plan has been changed.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,13 +236,5 @@ module.exports = {
|
|||||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
|
||||||
forcePathStyle: parseBoolean(
|
|
||||||
defaultTo(process.env.S3_FORCE_PATH_STYLE, false),
|
|
||||||
false
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
loops: {
|
|
||||||
apiKey: process.env.LOOPS_API_KEY,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
export const CashflowTransactionTypes = {
|
|
||||||
OtherIncome: 'Other income',
|
|
||||||
OtherExpense: 'Other expense',
|
|
||||||
OwnerDrawing: 'Owner drawing',
|
|
||||||
OwnerContribution: 'Owner contribution',
|
|
||||||
TransferToAccount: 'Transfer to account',
|
|
||||||
TransferFromAccount: 'Transfer from account',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TransactionTypes = {
|
export const TransactionTypes = {
|
||||||
SaleInvoice: 'Sale invoice',
|
SaleInvoice: 'Sale invoice',
|
||||||
SaleReceipt: 'Sale receipt',
|
SaleReceipt: 'Sale receipt',
|
||||||
PaymentReceive: 'Payment received',
|
PaymentReceive: 'Payment receive',
|
||||||
Bill: 'Bill',
|
Bill: 'Bill',
|
||||||
BillPayment: 'Payment made',
|
BillPayment: 'Payment made',
|
||||||
VendorOpeningBalance: 'Vendor opening balance',
|
VendorOpeningBalance: 'Vendor opening balance',
|
||||||
@@ -26,10 +17,12 @@ export const TransactionTypes = {
|
|||||||
OtherExpense: 'Other expense',
|
OtherExpense: 'Other expense',
|
||||||
OwnerDrawing: 'Owner drawing',
|
OwnerDrawing: 'Owner drawing',
|
||||||
InvoiceWriteOff: 'Invoice write-off',
|
InvoiceWriteOff: 'Invoice write-off',
|
||||||
|
|
||||||
CreditNote: 'transaction_type.credit_note',
|
CreditNote: 'transaction_type.credit_note',
|
||||||
VendorCredit: 'transaction_type.vendor_credit',
|
VendorCredit: 'transaction_type.vendor_credit',
|
||||||
|
|
||||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||||
|
|
||||||
LandedCost: 'transaction_type.landed_cost',
|
LandedCost: 'transaction_type.landed_cost',
|
||||||
CashflowTransaction: CashflowTransactionTypes,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ exports.up = function (knex) {
|
|||||||
.integer('uncategorized_transaction_id')
|
.integer('uncategorized_transaction_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
.references('id')
|
.references('id')
|
||||||
.inTable('uncategorized_cashflow_transactions')
|
.inTable('uncategorized_cashflow_transactions');
|
||||||
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
|
||||||
table
|
table
|
||||||
.integer('bank_rule_id')
|
.integer('bank_rule_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
||||||
table
|
table.integer('recognized_transaction_id').unsigned();
|
||||||
.integer('recognized_transaction_id')
|
|
||||||
.unsigned()
|
|
||||||
.references('id')
|
|
||||||
.inTable('recognized_bank_transactions')
|
|
||||||
.withKeyName('uncategorizedCashflowTransRecognizedTranIdForeign');
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||||
table.increments('id');
|
table.increments('id');
|
||||||
table
|
table.integer('uncategorized_transaction_id').unsigned();
|
||||||
.integer('uncategorized_transaction_id')
|
|
||||||
.unsigned()
|
|
||||||
.references('id')
|
|
||||||
.inTable('uncategorized_cashflow_transactions');
|
|
||||||
table.string('reference_type');
|
table.string('reference_type');
|
||||||
table.integer('reference_id').unsigned();
|
table.integer('reference_id').unsigned();
|
||||||
table.decimal('amount');
|
table.decimal('amount');
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
exports.up = function (knex) {
|
|
||||||
return knex('accounts_transactions')
|
|
||||||
.whereIn('referenceType', [
|
|
||||||
'OtherIncome',
|
|
||||||
'OtherExpense',
|
|
||||||
'OwnerDrawing',
|
|
||||||
'OwnerContribution',
|
|
||||||
'TransferToAccount',
|
|
||||||
'TransferFromAccount',
|
|
||||||
])
|
|
||||||
.update({
|
|
||||||
transactionType: knex.raw(`
|
|
||||||
CASE
|
|
||||||
WHEN REFERENCE_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
|
||||||
WHEN REFERENCE_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
|
||||||
WHEN REFERENCE_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
|
||||||
WHEN REFERENCE_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
|
||||||
WHEN REFERENCE_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
|
||||||
WHEN REFERENCE_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
|
||||||
END
|
|
||||||
`),
|
|
||||||
referenceType: knex.raw(`
|
|
||||||
CASE
|
|
||||||
WHEN REFERENCE_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN 'CashflowTransaction'
|
|
||||||
ELSE REFERENCE_TYPE
|
|
||||||
END
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
return knex('accounts_transactions')
|
|
||||||
.whereIn('transactionType', [
|
|
||||||
'OtherIncome',
|
|
||||||
'OtherExpense',
|
|
||||||
'OwnerDrawing',
|
|
||||||
'OwnerContribution',
|
|
||||||
'TransferToAccount',
|
|
||||||
'TransferFromAccount',
|
|
||||||
])
|
|
||||||
.update({
|
|
||||||
referenceType: knex.raw(`
|
|
||||||
CASE
|
|
||||||
WHEN TRANSACTION_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
|
||||||
WHEN TRANSACTION_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
|
||||||
WHEN TRANSACTION_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
|
||||||
WHEN TRANSACTION_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
|
||||||
WHEN TRANSACTION_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
|
||||||
WHEN TRANSACTION_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
|
||||||
ELSE REFERENCE_TYPE
|
|
||||||
END
|
|
||||||
`),
|
|
||||||
transactionType: knex.raw(`
|
|
||||||
CASE
|
|
||||||
WHEN TRANSACTION_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN NULL
|
|
||||||
ELSE TRANSACTION_TYPE
|
|
||||||
END
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
exports.up = function (knex) {
|
|
||||||
return knex.schema.table('accounts', (table) => {
|
|
||||||
table.string('plaid_item_id').nullable();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
return knex.schema.table('accounts', (table) => {
|
|
||||||
table.dropColumn('plaid_item_id');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
exports.up = function (knex) {
|
|
||||||
return knex.schema
|
|
||||||
.table('accounts', (table) => {
|
|
||||||
table
|
|
||||||
.boolean('is_syncing_owner')
|
|
||||||
.defaultTo(false)
|
|
||||||
.after('is_feeds_active');
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return knex('accounts')
|
|
||||||
.whereNotNull('plaid_item_id')
|
|
||||||
.orWhereNotNull('plaid_account_id')
|
|
||||||
.update('is_syncing_owner', true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
table.dropColumn('is_syncing_owner');
|
|
||||||
};
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,8 @@ export default class SeedAccounts extends TenantSeeder {
|
|||||||
description: this.i18n.__(account.description),
|
description: this.i18n.__(account.description),
|
||||||
currencyCode: this.tenant.metadata.baseCurrency,
|
currencyCode: this.tenant.metadata.baseCurrency,
|
||||||
seededAt: new Date(),
|
seededAt: new Date(),
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
return knex('accounts').then(async () => {
|
return knex('accounts').then(async () => {
|
||||||
// Inserts seed entries.
|
// Inserts seed entries.
|
||||||
return knex('accounts').insert(data);
|
return knex('accounts').insert(data);
|
||||||
|
|||||||
@@ -9,28 +9,6 @@ export const TaxPayableAccount = {
|
|||||||
predefined: 1,
|
predefined: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnearnedRevenueAccount = {
|
|
||||||
name: 'Unearned Revenue',
|
|
||||||
slug: 'unearned-revenue',
|
|
||||||
account_type: 'other-current-liability',
|
|
||||||
parent_account_id: null,
|
|
||||||
code: '50005',
|
|
||||||
active: true,
|
|
||||||
index: 1,
|
|
||||||
predefined: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PrepardExpenses = {
|
|
||||||
name: 'Prepaid Expenses',
|
|
||||||
slug: 'prepaid-expenses',
|
|
||||||
account_type: 'other-current-asset',
|
|
||||||
parent_account_id: null,
|
|
||||||
code: '100010',
|
|
||||||
active: true,
|
|
||||||
index: 1,
|
|
||||||
predefined: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: 'Bank Account',
|
name: 'Bank Account',
|
||||||
@@ -345,6 +323,4 @@ export default [
|
|||||||
index: 1,
|
index: 1,
|
||||||
predefined: 0,
|
predefined: 0,
|
||||||
},
|
},
|
||||||
UnearnedRevenueAccount,
|
|
||||||
PrepardExpenses,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface IAccountDTO {
|
|||||||
export interface IAccountCreateDTO extends IAccountDTO {
|
export interface IAccountCreateDTO extends IAccountDTO {
|
||||||
currencyCode?: string;
|
currencyCode?: string;
|
||||||
plaidAccountId?: string;
|
plaidAccountId?: string;
|
||||||
plaidItemId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccountEditDTO extends IAccountDTO {}
|
export interface IAccountEditDTO extends IAccountDTO {}
|
||||||
@@ -38,8 +37,6 @@ export interface IAccount {
|
|||||||
accountNormal: string;
|
accountNormal: string;
|
||||||
accountParentType: string;
|
accountParentType: string;
|
||||||
bankBalance: string;
|
bankBalance: string;
|
||||||
plaidItemId: number | null
|
|
||||||
lastFeedsUpdatedAt: Date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AccountNormal {
|
export enum AccountNormal {
|
||||||
@@ -69,9 +66,7 @@ export interface IAccountTransaction {
|
|||||||
referenceId: number;
|
referenceId: number;
|
||||||
|
|
||||||
referenceNumber?: string;
|
referenceNumber?: string;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
transactionType?: string;
|
|
||||||
|
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import {
|
import {
|
||||||
IFinancialSheetCommonMeta,
|
IFinancialSheetCommonMeta,
|
||||||
INumberFormatQuery,
|
INumberFormatQuery,
|
||||||
@@ -236,7 +235,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;
|
||||||
@@ -259,6 +257,7 @@ export interface IUncategorizedCashflowTransaction {
|
|||||||
categorized: boolean;
|
categorized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CreateUncategorizedTransactionDTO {
|
export interface CreateUncategorizedTransactionDTO {
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
accountId: number;
|
accountId: number;
|
||||||
@@ -270,16 +269,3 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUncategorizedTransactionCreatingEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
|
||||||
trx: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUncategorizedTransactionCreatedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransaction: any;
|
|
||||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
|
||||||
trx: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -130,23 +130,19 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
|
|
||||||
export interface ICashflowTransactionCategorizedPayload {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
cashflowTransactionId: number;
|
||||||
cashflowTransaction: ICashflowTransaction;
|
cashflowTransaction: ICashflowTransaction;
|
||||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,4 @@ export interface ICashflowAccountTransaction {
|
|||||||
|
|
||||||
date: Date;
|
date: Date;
|
||||||
formattedDate: string;
|
formattedDate: string;
|
||||||
|
|
||||||
status: string;
|
|
||||||
formattedStatus: string;
|
|
||||||
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
|
||||||
|
|
||||||
|
|
||||||
export interface IImportFileCommitedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
importId: number;
|
|
||||||
meta: ImportFilePreviewPOJO;
|
|
||||||
}
|
|
||||||
@@ -40,8 +40,6 @@ export interface ILedgerEntry {
|
|||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
transactionSubType?: string;
|
|
||||||
|
|
||||||
transactionId: number;
|
transactionId: number;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
|
|||||||
@@ -1,12 +1,69 @@
|
|||||||
|
import { forEach } from 'lodash';
|
||||||
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
|
||||||
|
import { createPlaidApiEvent } from './PlaidApiEventsDBSync';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
|
||||||
|
const OPTIONS = { clientApp: 'Plaid-Pattern' };
|
||||||
|
|
||||||
|
// We want to log requests to / responses from the Plaid API (via the Plaid client), as this data
|
||||||
|
// can be useful for troubleshooting.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging function for Plaid client methods that use an access_token as an argument. Associates
|
||||||
|
* the Plaid API event log entry with the item and user the request is for.
|
||||||
|
*
|
||||||
|
* @param {string} clientMethod the name of the Plaid client method called.
|
||||||
|
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
|
||||||
|
* @param {Object} response the response from the Plaid client.
|
||||||
|
*/
|
||||||
|
const defaultLogger = async (clientMethod, clientMethodArgs, response) => {
|
||||||
|
const accessToken = clientMethodArgs[0].access_token;
|
||||||
|
// const { id: itemId, user_id: userId } = await retrieveItemByPlaidAccessToken(
|
||||||
|
// accessToken
|
||||||
|
// );
|
||||||
|
// await createPlaidApiEvent(1, 1, clientMethod, clientMethodArgs, response);
|
||||||
|
|
||||||
|
// console.log(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging function for Plaid client methods that do not use access_token as an argument. These
|
||||||
|
* Plaid API event log entries will not be associated with an item or user.
|
||||||
|
*
|
||||||
|
* @param {string} clientMethod the name of the Plaid client method called.
|
||||||
|
* @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
|
||||||
|
* @param {Object} response the response from the Plaid client.
|
||||||
|
*/
|
||||||
|
const noAccessTokenLogger = async (
|
||||||
|
clientMethod,
|
||||||
|
clientMethodArgs,
|
||||||
|
response
|
||||||
|
) => {
|
||||||
|
// console.log(response);
|
||||||
|
|
||||||
|
// await createPlaidApiEvent(
|
||||||
|
// undefined,
|
||||||
|
// undefined,
|
||||||
|
// clientMethod,
|
||||||
|
// clientMethodArgs,
|
||||||
|
// response
|
||||||
|
// );
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plaid client methods used in this app, mapped to their appropriate logging functions.
|
||||||
|
const clientMethodLoggingFns = {
|
||||||
|
accountsGet: defaultLogger,
|
||||||
|
institutionsGet: noAccessTokenLogger,
|
||||||
|
institutionsGetById: noAccessTokenLogger,
|
||||||
|
itemPublicTokenExchange: noAccessTokenLogger,
|
||||||
|
itemRemove: defaultLogger,
|
||||||
|
linkTokenCreate: noAccessTokenLogger,
|
||||||
|
transactionsSync: defaultLogger,
|
||||||
|
sandboxItemResetLogin: defaultLogger,
|
||||||
|
};
|
||||||
// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
|
// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
|
||||||
export class PlaidClientWrapper {
|
export class PlaidClientWrapper {
|
||||||
private static instance: PlaidClientWrapper;
|
constructor() {
|
||||||
private client: PlaidApi;
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
// Initialize the Plaid client.
|
// Initialize the Plaid client.
|
||||||
const configuration = new Configuration({
|
const configuration = new Configuration({
|
||||||
basePath: PlaidEnvironments[config.plaid.env],
|
basePath: PlaidEnvironments[config.plaid.env],
|
||||||
@@ -18,13 +75,26 @@ export class PlaidClientWrapper {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client = new PlaidApi(configuration);
|
this.client = new PlaidApi(configuration);
|
||||||
|
|
||||||
|
// Wrap the Plaid client methods to add a logging function.
|
||||||
|
forEach(clientMethodLoggingFns, (logFn, method) => {
|
||||||
|
this[method] = this.createWrappedClientMethod(method, logFn);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getClient(): PlaidApi {
|
// Allows us to log API request data for troubleshooting purposes.
|
||||||
if (!PlaidClientWrapper.instance) {
|
createWrappedClientMethod(clientMethod, log) {
|
||||||
PlaidClientWrapper.instance = new PlaidClientWrapper();
|
return async (...args) => {
|
||||||
|
try {
|
||||||
|
const res = await this.client[clientMethod](...args);
|
||||||
|
await log(clientMethod, args, res);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
await log(clientMethod, args, err?.response?.data);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
return PlaidClientWrapper.instance.client;
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export const s3 = new S3Client({
|
|||||||
secretAccessKey: config.s3.secretAccessKey,
|
secretAccessKey: config.s3.secretAccessKey,
|
||||||
},
|
},
|
||||||
endpoint: config.s3.endpoint,
|
endpoint: config.s3.endpoint,
|
||||||
forcePathStyle: config.s3.forcePathStyle,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -110,11 +110,6 @@ import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching
|
|||||||
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
||||||
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
||||||
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
||||||
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
|
||||||
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
|
||||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
|
||||||
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
|
||||||
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -263,9 +258,6 @@ export const susbcribers = () => {
|
|||||||
// Bank Rules
|
// Bank Rules
|
||||||
TriggerRecognizedTransactions,
|
TriggerRecognizedTransactions,
|
||||||
UnlinkBankRuleOnDeleteBankRule,
|
UnlinkBankRuleOnDeleteBankRule,
|
||||||
DecrementUncategorizedTransactionOnMatching,
|
|
||||||
DecrementUncategorizedTransactionOnExclude,
|
|
||||||
DecrementUncategorizedTransactionOnCategorize,
|
|
||||||
|
|
||||||
// Validate matching
|
// Validate matching
|
||||||
ValidateMatchingOnCashflowDelete,
|
ValidateMatchingOnCashflowDelete,
|
||||||
@@ -276,9 +268,5 @@ export const susbcribers = () => {
|
|||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
DisconnectPlaidItemOnAccountDeleted,
|
|
||||||
|
|
||||||
// Loops
|
|
||||||
LoopsEventsSubscriber
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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', {});
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
const ExpenseEntry = require('models/ExpenseCategory');
|
const ExpenseEntry = require('models/ExpenseCategory');
|
||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
||||||
const PlaidItem = require('models/PlaidItem');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -322,18 +321,6 @@ export default class Account extends mixin(TenantModel, [
|
|||||||
query.where('categorized', false);
|
query.where('categorized', false);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Account model may belongs to a Plaid item.
|
|
||||||
*/
|
|
||||||
plaidItem: {
|
|
||||||
relation: Model.BelongsToOneRelation,
|
|
||||||
modelClass: PlaidItem.default,
|
|
||||||
join: {
|
|
||||||
from: 'accounts.plaidItemId',
|
|
||||||
to: 'plaid_items.plaidItemId',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
debit: number;
|
debit: number;
|
||||||
exchangeRate: number;
|
exchangeRate: number;
|
||||||
taxRate: number;
|
taxRate: number;
|
||||||
transactionType: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
@@ -54,7 +53,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
get referenceTypeFormatted() {
|
get referenceTypeFormatted() {
|
||||||
return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
return getTransactionTypeLabel(this.referenceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
return notFoundBillsIds;
|
return notFoundBillsIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
static changePaymentAmount(billId, amount, trx) {
|
static changePaymentAmount(billId, amount) {
|
||||||
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
const changeMethod = amount > 0 ? 'increment' : 'decrement';
|
||||||
return this.query(trx)
|
return this.query()
|
||||||
.where('id', billId)
|
.where('id', billId)
|
||||||
[changeMethod]('payment_amount', Math.abs(amount));
|
[changeMethod]('payment_amount', Math.abs(amount));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
getCashflowAccountTransactionsTypes,
|
getCashflowAccountTransactionsTypes,
|
||||||
getCashflowTransactionType,
|
getCashflowTransactionType,
|
||||||
} from '@/services/Cashflow/utils';
|
} from '@/services/Cashflow/utils';
|
||||||
|
import AccountTransaction from './AccountTransaction';
|
||||||
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
||||||
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||||
|
|
||||||
export default class CashflowTransaction extends TenantModel {
|
export default class CashflowTransaction extends TenantModel {
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -64,7 +64,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
get transactionTypeFormatted() {
|
get transactionTypeFormatted() {
|
||||||
return getCashflowTransactionFormattedType(this.transactionType);
|
return getTransactionTypeLabel(this.transactionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
get typeMeta() {
|
get typeMeta() {
|
||||||
@@ -95,34 +95,6 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
return !!this.uncategorizedTransaction;
|
return !!this.uncategorizedTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Model modifiers.
|
|
||||||
*/
|
|
||||||
static get modifiers() {
|
|
||||||
return {
|
|
||||||
/**
|
|
||||||
* Filter the published transactions.
|
|
||||||
*/
|
|
||||||
published(query) {
|
|
||||||
query.whereNot('published_at', null);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the not categorized transactions.
|
|
||||||
*/
|
|
||||||
notCategorized(query) {
|
|
||||||
query.whereNull('cashflowTransactions.uncategorizedTransactionId');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the categorized transactions.
|
|
||||||
*/
|
|
||||||
categorized(query) {
|
|
||||||
query.whereNotNull('cashflowTransactions.uncategorizedTransactionId');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
@@ -159,7 +131,8 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
to: 'accounts_transactions.referenceId',
|
to: 'accounts_transactions.referenceId',
|
||||||
},
|
},
|
||||||
filter(builder) {
|
filter(builder) {
|
||||||
builder.where('reference_type', 'CashflowTransaction');
|
const referenceTypes = getCashflowAccountTransactionsTypes();
|
||||||
|
builder.whereIn('reference_type', referenceTypes);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -115,34 +105,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
* Filters the excluded transactions.
|
* Filters the excluded transactions.
|
||||||
*/
|
*/
|
||||||
excluded(query) {
|
excluded(query) {
|
||||||
query.whereNotNull('excluded_at');
|
query.whereNotNull('excluded_at')
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter out the recognized transactions.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
recognized(query) {
|
|
||||||
query.whereNotNull('recognizedTransactionId');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter out the not recognized transactions.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
notRecognized(query) {
|
|
||||||
query.whereNull('recognizedTransactionId');
|
|
||||||
},
|
|
||||||
|
|
||||||
categorized(query) {
|
|
||||||
query.whereNotNull('categorizeRefType');
|
|
||||||
query.whereNotNull('categorizeRefId');
|
|
||||||
},
|
|
||||||
|
|
||||||
notCategorized(query) {
|
|
||||||
query.whereNull('categorizeRefType');
|
|
||||||
query.whereNull('categorizeRefId');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,4 +158,56 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the count of uncategorized transactions for the associated account
|
||||||
|
* based on the specified operation.
|
||||||
|
* @param {QueryContext} queryContext - The query context for the transaction.
|
||||||
|
* @param {boolean} increment - Indicates whether to increment or decrement the count.
|
||||||
|
*/
|
||||||
|
private async updateUncategorizedTransactionCount(
|
||||||
|
queryContext: QueryContext,
|
||||||
|
increment: boolean,
|
||||||
|
amount: number = 1
|
||||||
|
) {
|
||||||
|
const operation = increment ? 'increment' : 'decrement';
|
||||||
|
|
||||||
|
await Account.query(queryContext.transaction)
|
||||||
|
.findById(this.accountId)
|
||||||
|
[operation]('uncategorized_transactions', amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs after insert.
|
||||||
|
* @param {QueryContext} queryContext
|
||||||
|
*/
|
||||||
|
public async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
await this.updateUncategorizedTransactionCount(queryContext, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs after update.
|
||||||
|
* @param {ModelOptions} opt
|
||||||
|
* @param {QueryContext} queryContext
|
||||||
|
*/
|
||||||
|
public async $afterUpdate(
|
||||||
|
opt: ModelOptions,
|
||||||
|
queryContext: QueryContext
|
||||||
|
): Promise<any> {
|
||||||
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
if (this.id && this.categorized) {
|
||||||
|
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs after delete.
|
||||||
|
* @param {QueryContext} queryContext
|
||||||
|
*/
|
||||||
|
public async $afterDelete(queryContext: QueryContext) {
|
||||||
|
await super.$afterDelete(queryContext);
|
||||||
|
await this.updateUncategorizedTransactionCount(queryContext, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import { Account } from 'models';
|
|||||||
import TenantRepository from '@/repositories/TenantRepository';
|
import TenantRepository from '@/repositories/TenantRepository';
|
||||||
import { IAccount } from '@/interfaces';
|
import { IAccount } from '@/interfaces';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import {
|
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
|
||||||
PrepardExpenses,
|
|
||||||
TaxPayableAccount,
|
|
||||||
UnearnedRevenueAccount,
|
|
||||||
} from '@/database/seeds/data/accounts';
|
|
||||||
import { TenantMetadata } from '@/system/models';
|
|
||||||
|
|
||||||
export default class AccountRepository extends TenantRepository {
|
export default class AccountRepository extends TenantRepository {
|
||||||
/**
|
/**
|
||||||
@@ -184,67 +179,4 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds or creates the unearned revenue.
|
|
||||||
* @param {Record<string, string>} extraAttrs
|
|
||||||
* @param {Knex.Transaction} trx
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public async findOrCreateUnearnedRevenue(
|
|
||||||
extraAttrs: Record<string, string> = {},
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
) {
|
|
||||||
// Retrieves the given tenant metadata.
|
|
||||||
const tenantMeta = await TenantMetadata.query().findOne({
|
|
||||||
tenantId: this.tenantId,
|
|
||||||
});
|
|
||||||
const _extraAttrs = {
|
|
||||||
currencyCode: tenantMeta.baseCurrency,
|
|
||||||
...extraAttrs,
|
|
||||||
};
|
|
||||||
let result = await this.model
|
|
||||||
.query(trx)
|
|
||||||
.findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs });
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
result = await this.model.query(trx).insertAndFetch({
|
|
||||||
...UnearnedRevenueAccount,
|
|
||||||
..._extraAttrs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds or creates the prepard expenses account.
|
|
||||||
* @param {Record<string, string>} extraAttrs
|
|
||||||
* @param {Knex.Transaction} trx
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public async findOrCreatePrepardExpenses(
|
|
||||||
extraAttrs: Record<string, string> = {},
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
) {
|
|
||||||
// Retrieves the given tenant metadata.
|
|
||||||
const tenantMeta = await TenantMetadata.query().findOne({
|
|
||||||
tenantId: this.tenantId,
|
|
||||||
});
|
|
||||||
const _extraAttrs = {
|
|
||||||
currencyCode: tenantMeta.baseCurrency,
|
|
||||||
...extraAttrs,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = await this.model
|
|
||||||
.query(trx)
|
|
||||||
.findOne({ slug: PrepardExpenses.slug, ..._extraAttrs });
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
result = await this.model.query(trx).insertAndFetch({
|
|
||||||
...PrepardExpenses,
|
|
||||||
..._extraAttrs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import CachableRepository from './CachableRepository';
|
|||||||
|
|
||||||
export default class TenantRepository extends CachableRepository {
|
export default class TenantRepository extends CachableRepository {
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
tenantId: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
@@ -13,8 +12,4 @@ export default class TenantRepository extends CachableRepository {
|
|||||||
constructor(knex, cache, i18n) {
|
constructor(knex, cache, i18n) {
|
||||||
super(knex, cache, i18n);
|
super(knex, cache, i18n);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTenantId(tenantId: number) {
|
|
||||||
this.tenantId = tenantId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -19,8 +19,6 @@ export const transformLedgerEntryToTransaction = (
|
|||||||
referenceId: entry.transactionId,
|
referenceId: entry.transactionId,
|
||||||
|
|
||||||
transactionNumber: entry.transactionNumber,
|
transactionNumber: entry.transactionNumber,
|
||||||
transactionType: entry.transactionSubType,
|
|
||||||
|
|
||||||
referenceNumber: entry.referenceNumber,
|
referenceNumber: entry.referenceNumber,
|
||||||
|
|
||||||
note: entry.note,
|
note: entry.note,
|
||||||
|
|||||||
@@ -13,21 +13,7 @@ export class AccountTransformer extends Transformer {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return ['formattedAmount', 'flattenName', 'bankBalanceFormatted'];
|
||||||
'formattedAmount',
|
|
||||||
'flattenName',
|
|
||||||
'bankBalanceFormatted',
|
|
||||||
'lastFeedsUpdatedAtFormatted',
|
|
||||||
'isFeedsPaused',
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude attributes.
|
|
||||||
* @returns {string[]}
|
|
||||||
*/
|
|
||||||
public excludeAttributes = (): string[] => {
|
|
||||||
return ['plaidItem'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,24 +52,6 @@ export class AccountTransformer extends Transformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the formatted last feeds update at.
|
|
||||||
* @param {IAccount} account
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected lastFeedsUpdatedAtFormatted = (account: IAccount): string => {
|
|
||||||
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[]}
|
||||||
|
|||||||
@@ -96,11 +96,6 @@ export class CreateAccount {
|
|||||||
...createAccountDTO,
|
...createAccountDTO,
|
||||||
slug: kebabCase(createAccountDTO.name),
|
slug: kebabCase(createAccountDTO.name),
|
||||||
currencyCode: createAccountDTO.currencyCode || baseCurrency,
|
currencyCode: createAccountDTO.currencyCode || baseCurrency,
|
||||||
|
|
||||||
// Mark the account is Plaid owner since Plaid item/account is defined on creating.
|
|
||||||
isSyncingOwner: Boolean(
|
|
||||||
createAccountDTO.plaidAccountId || createAccountDTO.plaidItemId
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,7 +117,12 @@ export class CreateAccount {
|
|||||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
// Authorize the account creation.
|
// Authorize the account creation.
|
||||||
await this.authorize(tenantId, accountDTO, tenantMeta.baseCurrency, params);
|
await this.authorize(
|
||||||
|
tenantId,
|
||||||
|
accountDTO,
|
||||||
|
tenantMeta.baseCurrency,
|
||||||
|
params
|
||||||
|
);
|
||||||
// Transformes the DTO to model.
|
// Transformes the DTO to model.
|
||||||
const accountInputModel = this.transformDTOToModel(
|
const accountInputModel = this.transformDTOToModel(
|
||||||
accountDTO,
|
accountDTO,
|
||||||
@@ -157,3 +157,4 @@ export class CreateAccount {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,72 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { DisconnectBankAccount } from './DisconnectBankAccount';
|
|
||||||
import { RefreshBankAccountService } from './RefreshBankAccount';
|
|
||||||
import { PauseBankAccountFeeds } from './PauseBankAccountFeeds';
|
|
||||||
import { ResumeBankAccountFeeds } from './ResumeBankAccountFeeds';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class BankAccountsApplication {
|
|
||||||
@Inject()
|
|
||||||
private disconnectBankAccountService: DisconnectBankAccount;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private refreshBankAccountService: RefreshBankAccountService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private pauseBankAccountFeedsService: PauseBankAccountFeeds;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects the given bank account.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankAccountId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async disconnectBankAccount(tenantId: number, bankAccountId: number) {
|
|
||||||
return this.disconnectBankAccountService.disconnectBankAccount(
|
|
||||||
tenantId,
|
|
||||||
bankAccountId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the bank transactions of the given bank account.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankAccountId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async refreshBankAccount(tenantId: number, bankAccountId: number) {
|
|
||||||
return this.refreshBankAccountService.refreshBankAccount(
|
|
||||||
tenantId,
|
|
||||||
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,78 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
|
||||||
import { PlaidClientWrapper } from '@/lib/Plaid';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import {
|
|
||||||
ERRORS,
|
|
||||||
IBankAccountDisconnectedEventPayload,
|
|
||||||
IBankAccountDisconnectingEventPayload,
|
|
||||||
} from './types';
|
|
||||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class DisconnectBankAccount {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private eventPublisher: EventPublisher;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private uow: UnitOfWork;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects the given bank account.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankAccountId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async disconnectBankAccount(tenantId: number, bankAccountId: number) {
|
|
||||||
const { Account, PlaidItem } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
// Retrieve the bank account or throw not found error.
|
|
||||||
const account = await Account.query()
|
|
||||||
.findById(bankAccountId)
|
|
||||||
.whereIn('account_type', [ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.BANK])
|
|
||||||
.withGraphFetched('plaidItem')
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
const oldPlaidItem = account.plaidItem;
|
|
||||||
|
|
||||||
if (!oldPlaidItem) {
|
|
||||||
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
|
|
||||||
}
|
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
|
||||||
// Triggers `onBankAccountDisconnecting` event.
|
|
||||||
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnecting, {
|
|
||||||
tenantId,
|
|
||||||
bankAccountId,
|
|
||||||
} as IBankAccountDisconnectingEventPayload);
|
|
||||||
|
|
||||||
// Remove the Plaid item from the system.
|
|
||||||
await PlaidItem.query(trx).findById(account.plaidItemId).delete();
|
|
||||||
|
|
||||||
// Remove the plaid item association to the bank account.
|
|
||||||
await Account.query(trx).findById(bankAccountId).patch({
|
|
||||||
plaidAccountId: null,
|
|
||||||
plaidItemId: null,
|
|
||||||
isFeedsActive: false,
|
|
||||||
});
|
|
||||||
// Remove the Plaid item.
|
|
||||||
await plaidInstance.itemRemove({
|
|
||||||
access_token: oldPlaidItem.plaidAccessToken,
|
|
||||||
});
|
|
||||||
// Triggers `onBankAccountDisconnected` event.
|
|
||||||
await this.eventPublisher.emitAsync(events.bankAccount.onDisconnected, {
|
|
||||||
tenantId,
|
|
||||||
bankAccountId,
|
|
||||||
trx,
|
|
||||||
} as IBankAccountDisconnectedEventPayload);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { initialize } from 'objection';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
|
import { Server } from 'socket.io';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetBankAccountSummary {
|
export class GetBankAccountSummary {
|
||||||
@@ -15,82 +14,42 @@ export class GetBankAccountSummary {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
const {
|
const {
|
||||||
Account,
|
Account,
|
||||||
UncategorizedCashflowTransaction,
|
UncategorizedCashflowTransaction,
|
||||||
RecognizedBankTransaction,
|
RecognizedBankTransaction,
|
||||||
MatchedBankTransaction,
|
|
||||||
} = this.tenancy.models(tenantId);
|
} = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
await initialize(knex, [
|
|
||||||
UncategorizedCashflowTransaction,
|
|
||||||
RecognizedBankTransaction,
|
|
||||||
MatchedBankTransaction,
|
|
||||||
]);
|
|
||||||
const bankAccount = await Account.query()
|
const bankAccount = await Account.query()
|
||||||
.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()
|
||||||
commonQuery(q);
|
.where('accountId', bankAccountId)
|
||||||
|
.count('id as total')
|
||||||
|
.first();
|
||||||
|
|
||||||
// Only the not matched bank transactions.
|
// Retrieves the recognized transactions count of the given bank account.
|
||||||
q.withGraphJoined('matchedBankTransactions');
|
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||||
q.whereNull('matchedBankTransactions.id');
|
.whereExists(
|
||||||
|
UncategorizedCashflowTransaction.query().where(
|
||||||
// Count the results.
|
'accountId',
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
bankAccountId
|
||||||
q.first();
|
)
|
||||||
});
|
)
|
||||||
|
.count('id as total')
|
||||||
// Retrives the recognized transactions count.
|
.first();
|
||||||
const recognizedTransactionsCount =
|
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
|
||||||
commonQuery(q);
|
|
||||||
|
|
||||||
q.withGraphJoined('recognizedTransaction');
|
|
||||||
q.whereNotNull('recognizedTransaction.id');
|
|
||||||
|
|
||||||
// Count the results.
|
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
|
||||||
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;
|
||||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
const totalRecognizedTransactions = recognizedTransactionsCount?.total;
|
||||||
|
|
||||||
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,36 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { PlaidClientWrapper } from '@/lib/Plaid';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { ERRORS } from './types';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class RefreshBankAccountService {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asks Plaid to trigger syncing the given bank account.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankAccountId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async refreshBankAccount(tenantId: number, bankAccountId: number) {
|
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const bankAccount = await Account.query()
|
|
||||||
.findById(bankAccountId)
|
|
||||||
.withGraphFetched('plaidItem')
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
// Can't continue if the given account is not linked with Plaid item.
|
|
||||||
if (!bankAccount.plaidItem) {
|
|
||||||
throw new ServiceError(ERRORS.BANK_ACCOUNT_NOT_CONNECTED);
|
|
||||||
}
|
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
|
||||||
|
|
||||||
await plaidInstance.transactionsRefresh({
|
|
||||||
access_token: bankAccount.plaidItem.plaidAccessToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { IAccountEventDeletedPayload } from '@/interfaces';
|
|
||||||
import { PlaidClientWrapper } from '@/lib/Plaid';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class DisconnectPlaidItemOnAccountDeleted {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
*/
|
|
||||||
public attach(bus) {
|
|
||||||
bus.subscribe(
|
|
||||||
events.accounts.onDeleted,
|
|
||||||
this.handleDisconnectPlaidItemOnAccountDelete.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes Plaid item from the system and Plaid once the account deleted.
|
|
||||||
* @param {IAccountEventDeletedPayload} payload
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
private async handleDisconnectPlaidItemOnAccountDelete({
|
|
||||||
tenantId,
|
|
||||||
oldAccount,
|
|
||||||
trx,
|
|
||||||
}: IAccountEventDeletedPayload) {
|
|
||||||
const { PlaidItem, Account } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
// Can't continue if the deleted account is not linked to Plaid item.
|
|
||||||
if (!oldAccount.plaidItemId) return;
|
|
||||||
|
|
||||||
// Retrieves the Plaid item that associated to the deleted account.
|
|
||||||
const oldPlaidItem = await PlaidItem.query(trx).findOne(
|
|
||||||
'plaidItemId',
|
|
||||||
oldAccount.plaidItemId
|
|
||||||
);
|
|
||||||
// Unlink the Plaid item from all account before deleting it.
|
|
||||||
await Account.query(trx)
|
|
||||||
.where('plaidItemId', oldAccount.plaidItemId)
|
|
||||||
.patch({
|
|
||||||
plaidAccountId: null,
|
|
||||||
plaidItemId: null,
|
|
||||||
});
|
|
||||||
// Remove the Plaid item from the system.
|
|
||||||
await PlaidItem.query(trx)
|
|
||||||
.findOne('plaidItemId', oldAccount.plaidItemId)
|
|
||||||
.delete();
|
|
||||||
|
|
||||||
if (oldPlaidItem) {
|
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
|
||||||
|
|
||||||
// Remove the Plaid item.
|
|
||||||
await plaidInstance.itemRemove({
|
|
||||||
access_token: oldPlaidItem.plaidAccessToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
export interface IBankAccountDisconnectingEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
bankAccountId: number;
|
|
||||||
trx: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBankAccountDisconnectedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
bankAccountId: number;
|
|
||||||
trx: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ERRORS = {
|
|
||||||
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,17 +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 events from '@/subscribers/events';
|
|
||||||
import {
|
|
||||||
IBankTransactionUnexcludedEventPayload,
|
|
||||||
IBankTransactionUnexcludingEventPayload,
|
|
||||||
} from './_types';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransaction {
|
export class ExcludeBankTransaction {
|
||||||
@@ -21,9 +11,6 @@ export class ExcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private eventPublisher: EventPublisher;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -41,30 +28,14 @@ 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, {
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
|
||||||
} as IBankTransactionUnexcludingEventPayload);
|
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: new Date(),
|
excludedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluded, {
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
|
||||||
} as IBankTransactionUnexcludedEventPayload);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import PromisePool from '@supercharge/promise-pool';
|
|
||||||
import { castArray, uniq } from 'lodash';
|
|
||||||
import { ExcludeBankTransaction } from './ExcludeBankTransaction';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class ExcludeBankTransactions {
|
|
||||||
@Inject()
|
|
||||||
private excludeBankTransaction: ExcludeBankTransaction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude bank transactions in bulk.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankTransactionIds
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public async excludeBankTransactions(
|
|
||||||
tenantId: number,
|
|
||||||
bankTransactionIds: Array<number> | number
|
|
||||||
) {
|
|
||||||
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
|
|
||||||
|
|
||||||
await PromisePool.withConcurrency(1)
|
|
||||||
.for(_bankTransactionIds)
|
|
||||||
.process((bankTransactionId: number) => {
|
|
||||||
return this.excludeBankTransaction.excludeBankTransaction(
|
|
||||||
tenantId,
|
|
||||||
bankTransactionId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ import { ExcludeBankTransaction } from './ExcludeBankTransaction';
|
|||||||
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
||||||
import { GetExcludedBankTransactionsService } from './GetExcludedBankTransactions';
|
import { GetExcludedBankTransactionsService } from './GetExcludedBankTransactions';
|
||||||
import { ExcludedBankTransactionsQuery } from './_types';
|
import { ExcludedBankTransactionsQuery } from './_types';
|
||||||
import { UnexcludeBankTransactions } from './UnexcludeBankTransactions';
|
|
||||||
import { ExcludeBankTransactions } from './ExcludeBankTransactions';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransactionsApplication {
|
export class ExcludeBankTransactionsApplication {
|
||||||
@@ -17,12 +15,6 @@ export class ExcludeBankTransactionsApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getExcludedBankTransactionsService: GetExcludedBankTransactionsService;
|
private getExcludedBankTransactionsService: GetExcludedBankTransactionsService;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private excludeBankTransactionsService: ExcludeBankTransactions;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private unexcludeBankTransactionsService: UnexcludeBankTransactions;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks a bank transaction as excluded.
|
* Marks a bank transaction as excluded.
|
||||||
* @param {number} tenantId - The ID of the tenant.
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
@@ -64,36 +56,4 @@ export class ExcludeBankTransactionsApplication {
|
|||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude the given bank transactions in bulk.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {Array<number> | number} bankTransactionIds
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public excludeBankTransactions(
|
|
||||||
tenantId: number,
|
|
||||||
bankTransactionIds: Array<number> | number
|
|
||||||
): Promise<void> {
|
|
||||||
return this.excludeBankTransactionsService.excludeBankTransactions(
|
|
||||||
tenantId,
|
|
||||||
bankTransactionIds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude the given bank transactions in bulk.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {Array<number> | number} bankTransactionIds
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
public unexcludeBankTransactions(
|
|
||||||
tenantId: number,
|
|
||||||
bankTransactionIds: Array<number> | number
|
|
||||||
): Promise<void> {
|
|
||||||
return this.unexcludeBankTransactionsService.unexcludeBankTransactions(
|
|
||||||
tenantId,
|
|
||||||
bankTransactionIds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +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 events from '@/subscribers/events';
|
|
||||||
import {
|
|
||||||
IBankTransactionExcludedEventPayload,
|
|
||||||
IBankTransactionExcludingEventPayload,
|
|
||||||
} from './_types';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UnexcludeBankTransaction {
|
export class UnexcludeBankTransaction {
|
||||||
@@ -21,9 +11,6 @@ export class UnexcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private eventPublisher: EventPublisher;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -33,7 +20,7 @@ export class UnexcludeBankTransaction {
|
|||||||
public async unexcludeBankTransaction(
|
public async unexcludeBankTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number
|
uncategorizedTransactionId: number
|
||||||
): Promise<void> {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldUncategorizedTransaction =
|
const oldUncategorizedTransaction =
|
||||||
@@ -41,34 +28,14 @@ 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(
|
|
||||||
events.bankTransactions.onUnexcluding,
|
|
||||||
{
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
} as IBankTransactionExcludingEventPayload
|
|
||||||
);
|
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: null,
|
excludedAt: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.eventPublisher.emitAsync(
|
|
||||||
events.bankTransactions.onUnexcluded,
|
|
||||||
{
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
} as IBankTransactionExcludedEventPayload
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import PromisePool from '@supercharge/promise-pool';
|
|
||||||
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
|
||||||
import { castArray, uniq } from 'lodash';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class UnexcludeBankTransactions {
|
|
||||||
@Inject()
|
|
||||||
private unexcludeBankTransaction: UnexcludeBankTransaction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unexclude bank transactions in bulk.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} bankTransactionIds
|
|
||||||
*/
|
|
||||||
public async unexcludeBankTransactions(
|
|
||||||
tenantId: number,
|
|
||||||
bankTransactionIds: Array<number> | number
|
|
||||||
) {
|
|
||||||
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
|
|
||||||
|
|
||||||
await PromisePool.withConcurrency(1)
|
|
||||||
.for(_bankTransactionIds)
|
|
||||||
.process((bankTransactionId: number) => {
|
|
||||||
return this.unexcludeBankTransaction.unexcludeBankTransaction(
|
|
||||||
tenantId,
|
|
||||||
bankTransactionId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,6 @@
|
|||||||
import { Knex } from "knex";
|
|
||||||
|
|
||||||
export interface ExcludedBankTransactionsQuery {
|
export interface ExcludedBankTransactionsQuery {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionUnexcludingEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBankTransactionUnexcludedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBankTransactionExcludingEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
}
|
|
||||||
export interface IBankTransactionExcludedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import {
|
|
||||||
IBankTransactionExcludedEventPayload,
|
|
||||||
IBankTransactionUnexcludedEventPayload,
|
|
||||||
} from '../_types';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class DecrementUncategorizedTransactionOnExclude {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
*/
|
|
||||||
public attach(bus) {
|
|
||||||
bus.subscribe(
|
|
||||||
events.bankTransactions.onExcluded,
|
|
||||||
this.decrementUnCategorizedTransactionsOnExclude.bind(this)
|
|
||||||
);
|
|
||||||
bus.subscribe(
|
|
||||||
events.bankTransactions.onUnexcluded,
|
|
||||||
this.incrementUnCategorizedTransactionsOnUnexclude.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
|
||||||
* @param {IManualJournalDeletingPayload}
|
|
||||||
*/
|
|
||||||
public async decrementUnCategorizedTransactionsOnExclude({
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
|
||||||
}: IBankTransactionExcludedEventPayload) {
|
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const transaction = await UncategorizedCashflowTransaction.query(
|
|
||||||
trx
|
|
||||||
).findById(uncategorizedTransactionId);
|
|
||||||
|
|
||||||
await Account.query(trx)
|
|
||||||
.findById(transaction.accountId)
|
|
||||||
.decrement('uncategorizedTransactions', 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
|
||||||
* @param {IManualJournalDeletingPayload}
|
|
||||||
*/
|
|
||||||
public async incrementUnCategorizedTransactionsOnUnexclude({
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
|
||||||
}: IBankTransactionUnexcludedEventPayload) {
|
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
|
||||||
uncategorizedTransactionId
|
|
||||||
);
|
|
||||||
//
|
|
||||||
await Account.query(trx)
|
|
||||||
.findById(transaction.accountId)
|
|
||||||
.increment('uncategorizedTransactions', 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
'transactionNormal',
|
|
||||||
'referenceId',
|
|
||||||
'referenceType',
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,29 +100,4 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Bill';
|
return 'Bill';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the bill transaction normal (debit or credit).
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNormal() {
|
|
||||||
return 'credit';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the match transaction reference id.
|
|
||||||
* @param bill
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected referenceId(bill) {
|
|
||||||
return bill.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the match transaction referenece type.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected referenceType() {
|
|
||||||
return 'Bill';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
|
||||||
|
|
||||||
export class GetMatchedTransactionCashflowTransformer extends Transformer {
|
|
||||||
/**
|
|
||||||
* Include these attributes to sale credit note object.
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
public includeAttributes = (): string[] => {
|
|
||||||
return [
|
|
||||||
'referenceNo',
|
|
||||||
'amount',
|
|
||||||
'amountFormatted',
|
|
||||||
'transactionNo',
|
|
||||||
'date',
|
|
||||||
'dateFormatted',
|
|
||||||
'transactionId',
|
|
||||||
'transactionNo',
|
|
||||||
'transactionType',
|
|
||||||
'transsactionTypeFormatted',
|
|
||||||
'transactionNormal',
|
|
||||||
'referenceId',
|
|
||||||
'referenceType',
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exclude all attributes.
|
|
||||||
* @returns {Array<string>}
|
|
||||||
*/
|
|
||||||
public excludeAttributes = (): string[] => {
|
|
||||||
return ['*'];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the invoice reference number.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected referenceNo(invoice) {
|
|
||||||
return invoice.referenceNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction amount.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected amount(transaction) {
|
|
||||||
return transaction.amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction formatted amount.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected amountFormatted(transaction) {
|
|
||||||
return this.formatNumber(transaction.amount, {
|
|
||||||
currencyCode: transaction.currencyCode,
|
|
||||||
money: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the date of the invoice.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {Date}
|
|
||||||
*/
|
|
||||||
protected date(transaction) {
|
|
||||||
return transaction.date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format the date of the invoice.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected dateFormatted(transaction) {
|
|
||||||
return this.formatDate(transaction.date);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction ID of the invoice.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected transactionId(transaction) {
|
|
||||||
return transaction.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the invoice transaction number.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNo(transaction) {
|
|
||||||
return transaction.transactionNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the invoice transaction type.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
protected transactionType(transaction) {
|
|
||||||
return transaction.transactionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the invoice formatted transaction type.
|
|
||||||
* @param invoice
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transsactionTypeFormatted(transaction) {
|
|
||||||
return transaction.transactionTypeFormatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the cashflow transaction normal (credit or debit).
|
|
||||||
* @param transaction
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNormal(transaction) {
|
|
||||||
return transaction.isCashCredit ? 'credit' : 'debit';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the cashflow transaction reference id.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected referenceId(transaction) {
|
|
||||||
return transaction.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the cashflow transaction reference type.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected referenceType() {
|
|
||||||
return 'CashflowTransaction';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,9 +17,6 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
'transactionNormal',
|
|
||||||
'referenceType',
|
|
||||||
'referenceId',
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,29 +111,4 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Expense';
|
return 'Expense';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the expense transaction normal (credit or debit).
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNormal() {
|
|
||||||
return 'credit';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction reference type.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected referenceType() {
|
|
||||||
return 'Expense';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction reference id.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected referenceId(transaction) {
|
|
||||||
return transaction.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
'transactionNormal',
|
|
||||||
'referenceType',
|
|
||||||
'referenceId'
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +49,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected amountFormatted(invoice) {
|
protected formatAmount(invoice) {
|
||||||
return this.formatNumber(invoice.dueAmount, {
|
return this.formatNumber(invoice.dueAmount, {
|
||||||
currencyCode: invoice.currencyCode,
|
currencyCode: invoice.currencyCode,
|
||||||
money: true,
|
money: true,
|
||||||
@@ -82,7 +79,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected transactionId(invoice) {
|
protected getTransactionId(invoice) {
|
||||||
return invoice.id;
|
return invoice.id;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -111,28 +108,4 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted(invoice) {
|
protected transsactionTypeFormatted(invoice) {
|
||||||
return 'Sale invoice';
|
return 'Sale invoice';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction normal of invoice (credit or debit).
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNormal() {
|
|
||||||
return 'debit';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction reference type.
|
|
||||||
* @returns {string}
|
|
||||||
*/ protected referenceType() {
|
|
||||||
return 'SaleInvoice';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the transaction reference id.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected referenceId(transaction) {
|
|
||||||
return transaction.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { sumBy } from 'lodash';
|
|
||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
import { AccountNormal } from '@/interfaces';
|
|
||||||
|
|
||||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -19,9 +17,6 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
'transactionNormal',
|
|
||||||
'referenceType',
|
|
||||||
'referenceId',
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,20 +37,13 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
return manualJournal.referenceNo;
|
return manualJournal.referenceNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected total(manualJournal) {
|
|
||||||
const credit = sumBy(manualJournal?.entries, 'credit');
|
|
||||||
const debit = sumBy(manualJournal?.entries, 'debit');
|
|
||||||
|
|
||||||
return debit - credit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the manual journal amount.
|
* Retrieves the manual journal amount.
|
||||||
* @param manualJournal
|
* @param manualJournal
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected amount(manualJournal) {
|
protected amount(manualJournal) {
|
||||||
return Math.abs(this.total(manualJournal));
|
return manualJournal.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,31 +107,5 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Manual Journal';
|
return 'Manual Journal';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the manual journal transaction normal (credit or debit).
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected transactionNormal(transaction) {
|
|
||||||
const amount = this.total(transaction);
|
|
||||||
|
|
||||||
return amount >= 0 ? AccountNormal.DEBIT : AccountNormal.CREDIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the manual journal reference type.
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
protected referenceType() {
|
|
||||||
return 'ManualJournal';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the manual journal reference id.
|
|
||||||
* @param transaction
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
protected referenceId(transaction) {
|
|
||||||
return transaction.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -9,8 +8,6 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
|||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { sortClosestMatchTransactions } from './_utils';
|
import { sortClosestMatchTransactions } from './_utils';
|
||||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
|
||||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetMatchedTransactions {
|
export class GetMatchedTransactions {
|
||||||
@@ -18,7 +15,7 @@ export class GetMatchedTransactions {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedInvoicesService: GetMatchedTransactionsByInvoices;
|
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
||||||
@@ -29,9 +26,6 @@ export class GetMatchedTransactions {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private getMatchedCashflowService: GetMatchedTransactionsByCashflow;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registered matched transactions types.
|
* Registered matched transactions types.
|
||||||
*/
|
*/
|
||||||
@@ -41,31 +35,27 @@ export class GetMatchedTransactions {
|
|||||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||||
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 +65,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 +84,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);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { initialize } from 'objection';
|
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
@@ -23,25 +22,10 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
const { Bill, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { Bill } = this.tenancy.models(tenantId);
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
// Initialize the models metadata.
|
|
||||||
await initialize(knex, [Bill, MatchedBankTransaction]);
|
|
||||||
|
|
||||||
// Retrieves the bill matches.
|
|
||||||
const bills = await Bill.query().onBuild((q) => {
|
const bills = await Bill.query().onBuild((q) => {
|
||||||
q.withGraphJoined('matchedBankTransaction');
|
q.whereNotExists(Bill.relatedQuery('matchedBankTransaction'));
|
||||||
q.whereNull('matchedBankTransaction.id');
|
|
||||||
q.modify('published');
|
|
||||||
|
|
||||||
if (filter.fromDate) {
|
|
||||||
q.where('billDate', '>=', filter.fromDate);
|
|
||||||
}
|
|
||||||
if (filter.toDate) {
|
|
||||||
q.where('billDate', '<=', filter.toDate);
|
|
||||||
}
|
|
||||||
q.orderBy('billDate', 'DESC');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { initialize } from 'objection';
|
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
|
||||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
|
||||||
import { GetMatchedTransactionsFilter } from './types';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
|
|
||||||
@Inject()
|
|
||||||
private transformer: TransformerInjectable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the matched transactions of cash flow.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {GetMatchedTransactionsFilter} filter
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async getMatchedTransactions(
|
|
||||||
tenantId: number,
|
|
||||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
|
||||||
) {
|
|
||||||
const { CashflowTransaction, MatchedBankTransaction } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
// Initialize the ORM models metadata.
|
|
||||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
|
||||||
|
|
||||||
const transactions = await CashflowTransaction.query().onBuild((q) => {
|
|
||||||
// Not matched to bank transaction.
|
|
||||||
q.withGraphJoined('matchedBankTransaction');
|
|
||||||
q.whereNull('matchedBankTransaction.id');
|
|
||||||
|
|
||||||
// Not categorized.
|
|
||||||
q.modify('notCategorized');
|
|
||||||
|
|
||||||
// Published.
|
|
||||||
q.modify('published');
|
|
||||||
|
|
||||||
if (filter.fromDate) {
|
|
||||||
q.where('date', '>=', filter.fromDate);
|
|
||||||
}
|
|
||||||
if (filter.toDate) {
|
|
||||||
q.where('date', '<=', filter.toDate);
|
|
||||||
}
|
|
||||||
q.orderBy('date', 'DESC');
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.transformer.transform(
|
|
||||||
tenantId,
|
|
||||||
transactions,
|
|
||||||
new GetMatchedTransactionCashflowTransformer()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the matched transaction of cash flow.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} transactionId
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async getMatchedTransaction(tenantId: number, transactionId: number) {
|
|
||||||
const { CashflowTransaction, MatchedBankTransaction } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
// Initialize the ORM models metadata.
|
|
||||||
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
|
||||||
|
|
||||||
const transactions = await CashflowTransaction.query()
|
|
||||||
.findById(transactionId)
|
|
||||||
.withGraphJoined('matchedBankTransaction')
|
|
||||||
.whereNull('matchedBankTransaction.id')
|
|
||||||
.modify('notCategorized')
|
|
||||||
.modify('published')
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
return this.transformer.transform(
|
|
||||||
tenantId,
|
|
||||||
transactions,
|
|
||||||
new GetMatchedTransactionCashflowTransformer()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { initialize } from 'objection';
|
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
@@ -24,34 +23,22 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
const { Expense, MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { Expense } = this.tenancy.models(tenantId);
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
// Initialize the models metadata.
|
|
||||||
await initialize(knex, [Expense, MatchedBankTransaction]);
|
|
||||||
|
|
||||||
// Retrieve the expense matches.
|
|
||||||
const expenses = await Expense.query().onBuild((query) => {
|
const expenses = await Expense.query().onBuild((query) => {
|
||||||
// Filter out the not matched to bank transactions.
|
query.whereNotExists(Expense.relatedQuery('matchedBankTransaction'));
|
||||||
query.withGraphJoined('matchedBankTransaction');
|
|
||||||
query.whereNull('matchedBankTransaction.id');
|
|
||||||
|
|
||||||
// Filter the published onyl
|
|
||||||
query.modify('filterByPublished');
|
|
||||||
|
|
||||||
if (filter.fromDate) {
|
if (filter.fromDate) {
|
||||||
query.where('paymentDate', '>=', filter.fromDate);
|
query.where('payment_date', '>=', filter.fromDate);
|
||||||
}
|
}
|
||||||
if (filter.toDate) {
|
if (filter.toDate) {
|
||||||
query.where('paymentDate', '<=', filter.toDate);
|
query.where('payment_date', '<=', filter.toDate);
|
||||||
}
|
}
|
||||||
if (filter.minAmount) {
|
if (filter.minAmount) {
|
||||||
query.where('totalAmount', '>=', filter.minAmount);
|
query.where('total_amount', '>=', filter.minAmount);
|
||||||
}
|
}
|
||||||
if (filter.maxAmount) {
|
if (filter.maxAmount) {
|
||||||
query.where('totalAmount', '<=', filter.maxAmount);
|
query.where('total_amount', '<=', filter.maxAmount);
|
||||||
}
|
}
|
||||||
query.orderBy('paymentDate', 'DESC');
|
|
||||||
});
|
});
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { initialize } from 'objection';
|
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +6,7 @@ import {
|
|||||||
MatchedTransactionsPOJO,
|
MatchedTransactionsPOJO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -28,27 +27,10 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
const { SaleInvoice, MatchedBankTransaction } =
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
// Initialize the models metadata.
|
|
||||||
await initialize(knex, [SaleInvoice, MatchedBankTransaction]);
|
|
||||||
|
|
||||||
// Retrieve the invoices that not matched, unpaid.
|
|
||||||
const invoices = await SaleInvoice.query().onBuild((q) => {
|
const invoices = await SaleInvoice.query().onBuild((q) => {
|
||||||
q.withGraphJoined('matchedBankTransaction');
|
q.whereNotExists(SaleInvoice.relatedQuery('matchedBankTransaction'));
|
||||||
q.whereNull('matchedBankTransaction.id');
|
|
||||||
q.modify('unpaid');
|
|
||||||
q.modify('published');
|
|
||||||
|
|
||||||
if (filter.fromDate) {
|
|
||||||
q.where('invoiceDate', '>=', filter.fromDate);
|
|
||||||
}
|
|
||||||
if (filter.toDate) {
|
|
||||||
q.where('invoiceDate', '<=', filter.toDate);
|
|
||||||
}
|
|
||||||
q.orderBy('invoiceDate', 'DESC');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { initialize } from 'objection';
|
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
@@ -20,26 +19,12 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||||
) {
|
) {
|
||||||
const { ManualJournal, ManualJournalEntry, MatchedBankTransaction } =
|
const { ManualJournal } = this.tenancy.models(tenantId);
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
const knex = this.tenancy.knex(tenantId);
|
|
||||||
|
|
||||||
await initialize(knex, [
|
|
||||||
ManualJournal,
|
|
||||||
ManualJournalEntry,
|
|
||||||
MatchedBankTransaction,
|
|
||||||
]);
|
|
||||||
const accountId = 1000;
|
|
||||||
|
|
||||||
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
||||||
query.withGraphJoined('matchedBankTransaction');
|
query.whereNotExists(
|
||||||
query.whereNull('matchedBankTransaction.id');
|
ManualJournal.relatedQuery('matchedBankTransaction')
|
||||||
|
);
|
||||||
query.withGraphJoined('entries');
|
|
||||||
query.where('entries.accountId', accountId);
|
|
||||||
|
|
||||||
query.modify('filterByPublished');
|
|
||||||
|
|
||||||
if (filter.fromDate) {
|
if (filter.fromDate) {
|
||||||
query.where('date', '>=', filter.fromDate);
|
query.where('date', '>=', filter.fromDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
.for(uncategorizedTransactionIds)
|
|
||||||
.process(async (uncategorizedTransactionId) => {
|
|
||||||
await MatchedBankTransaction.query(trx).insert({
|
await MatchedBankTransaction.query(trx).insert({
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
referenceType: matchTransactionDTO.referenceType,
|
referenceType: matchTransactionDTO.referenceType,
|
||||||
referenceId: matchTransactionDTO.referenceId,
|
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, sumBy } 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,10 @@ 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 {
|
|
||||||
sumMatchTranasctions,
|
|
||||||
sumUncategorizedTransactions,
|
|
||||||
validateUncategorizedTransactionsExcluded,
|
|
||||||
validateUncategorizedTransactionsNotMatched,
|
|
||||||
} from './_utils';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -44,25 +38,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 =
|
||||||
@@ -94,15 +90,13 @@ export class MatchBankTransactions {
|
|||||||
throw new ServiceError(error);
|
throw new ServiceError(error);
|
||||||
}
|
}
|
||||||
// Calculate the total given matching transactions.
|
// Calculate the total given matching transactions.
|
||||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
const totalMatchedTranasctions = sumBy(
|
||||||
validatationResult.results
|
validatationResult.results,
|
||||||
);
|
'amount'
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
|||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||||
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
|
||||||
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchTransactionsTypes {
|
export class MatchTransactionsTypes {
|
||||||
@@ -27,10 +25,6 @@ export class MatchTransactionsTypes {
|
|||||||
type: 'ManualJournal',
|
type: 'ManualJournal',
|
||||||
service: GetMatchedTransactionsByManualJournals,
|
service: GetMatchedTransactionsByManualJournals,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'CashflowTransaction',
|
|
||||||
service: GetMatchedTransactionsByCashflow,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
|
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { ERRORS } from './types';
|
import { ERRORS } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -19,13 +18,12 @@ export class ValidateTransactionMatched {
|
|||||||
public async validateTransactionNoMatchLinking(
|
public async validateTransactionNoMatchLinking(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
referenceType: string,
|
referenceType: string,
|
||||||
referenceId: number,
|
referenceId: number
|
||||||
trx?: Knex.Transaction
|
|
||||||
) {
|
) {
|
||||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const foundMatchedTransaction =
|
const foundMatchedTransaction =
|
||||||
await MatchedBankTransaction.query(trx).findOne({
|
await MatchedBankTransaction.query().findOne({
|
||||||
referenceType,
|
referenceType,
|
||||||
referenceId,
|
referenceId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,65 +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);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
|
||||||
return transactions.reduce(
|
|
||||||
(total, item) =>
|
|
||||||
total +
|
|
||||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import {
|
|
||||||
IBankTransactionMatchedEventPayload,
|
|
||||||
IBankTransactionUnmatchedEventPayload,
|
|
||||||
} from '../types';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import PromisePool from '@supercharge/promise-pool';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class DecrementUncategorizedTransactionOnMatching {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
/**
|
|
||||||
* Constructor method.
|
|
||||||
*/
|
|
||||||
public attach(bus) {
|
|
||||||
bus.subscribe(
|
|
||||||
events.bankMatch.onMatched,
|
|
||||||
this.decrementUnCategorizedTransactionsOnMatching.bind(this)
|
|
||||||
);
|
|
||||||
bus.subscribe(
|
|
||||||
events.bankMatch.onUnmatched,
|
|
||||||
this.incrementUnCategorizedTransactionsOnUnmatching.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
|
||||||
* @param {IManualJournalDeletingPayload}
|
|
||||||
*/
|
|
||||||
public async decrementUnCategorizedTransactionsOnMatching({
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionIds,
|
|
||||||
trx,
|
|
||||||
}: IBankTransactionMatchedEventPayload) {
|
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const uncategorizedTransactions =
|
|
||||||
await UncategorizedCashflowTransaction.query().whereIn(
|
|
||||||
'id',
|
|
||||||
uncategorizedTransactionIds
|
|
||||||
);
|
|
||||||
await PromisePool.withConcurrency(1)
|
|
||||||
.for(uncategorizedTransactions)
|
|
||||||
.process(async (transaction) => {
|
|
||||||
await Account.query(trx)
|
|
||||||
.findById(transaction.accountId)
|
|
||||||
.decrement('uncategorizedTransactions', 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
|
||||||
* @param {IManualJournalDeletingPayload}
|
|
||||||
*/
|
|
||||||
public async incrementUnCategorizedTransactionsOnUnmatching({
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionId,
|
|
||||||
trx,
|
|
||||||
}: IBankTransactionUnmatchedEventPayload) {
|
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
|
||||||
this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
|
||||||
uncategorizedTransactionId
|
|
||||||
);
|
|
||||||
await Account.query(trx)
|
|
||||||
.findById(transaction.accountId)
|
|
||||||
.increment('uncategorizedTransactions', 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ICommandCashflowDeletingPayload, IManualJournalDeletingPayload } from '@/interfaces';
|
import { IManualJournalDeletingPayload } from '@/interfaces';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||||
|
|
||||||
@@ -24,14 +24,13 @@ export class ValidateMatchingOnCashflowDelete {
|
|||||||
*/
|
*/
|
||||||
public async validateMatchingOnCashflowDeleting({
|
public async validateMatchingOnCashflowDeleting({
|
||||||
tenantId,
|
tenantId,
|
||||||
oldCashflowTransaction,
|
oldManualJournal,
|
||||||
trx,
|
trx,
|
||||||
}: ICommandCashflowDeletingPayload) {
|
}: IManualJournalDeletingPayload) {
|
||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'CashflowTransaction',
|
'ManualJournal',
|
||||||
oldCashflowTransaction.id,
|
oldManualJournal.id
|
||||||
trx
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export class ValidateMatchingOnExpenseDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'Expense',
|
'Expense',
|
||||||
oldExpense.id,
|
oldExpense.id
|
||||||
trx
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export class ValidateMatchingOnManualJournalDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'ManualJournal',
|
'ManualJournal',
|
||||||
oldManualJournal.id,
|
oldManualJournal.id
|
||||||
trx
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentMade',
|
'PaymentMade',
|
||||||
oldBillPayment.id,
|
oldBillPayment.id
|
||||||
trx
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentReceive',
|
'PaymentReceive',
|
||||||
oldPaymentReceive.id,
|
oldPaymentReceive.id
|
||||||
trx
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,24 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionUnmatchingEventPayload {
|
export interface IBankTransactionUnmatchingEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionUnmatchedEventPayload {
|
export interface IBankTransactionUnmatchedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
trx?: Knex.Transaction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionDTO {
|
export interface IMatchTransactionDTO {
|
||||||
@@ -32,7 +28,6 @@ export interface IMatchTransactionDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionsDTO {
|
export interface IMatchTransactionsDTO {
|
||||||
uncategorizedTransactionIds: Array<number>;
|
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +53,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 = {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class PlaidItemService {
|
|||||||
const { PlaidItem } = this.tenancy.models(tenantId);
|
const { PlaidItem } = this.tenancy.models(tenantId);
|
||||||
const { publicToken, institutionId } = itemDTO;
|
const { publicToken, institutionId } = itemDTO;
|
||||||
|
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
const plaidInstance = new PlaidClientWrapper();
|
||||||
|
|
||||||
// Exchange the public token for a private access token and store with the item.
|
// Exchange the public token for a private access token and store with the item.
|
||||||
const response = await plaidInstance.itemPublicTokenExchange({
|
const response = await plaidInstance.itemPublicTokenExchange({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class PlaidLinkTokenService {
|
|||||||
webhook: config.plaid.linkWebhook,
|
webhook: config.plaid.linkWebhook,
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
};
|
};
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
const plaidInstance = new PlaidClientWrapper();
|
||||||
const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
|
const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
|
||||||
|
|
||||||
return createResponse.data;
|
return createResponse.data;
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import * as R from 'ramda';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import bluebird from 'bluebird';
|
import bluebird from 'bluebird';
|
||||||
import { entries, groupBy } from 'lodash';
|
import { entries, groupBy } from 'lodash';
|
||||||
import {
|
|
||||||
AccountBase as PlaidAccountBase,
|
|
||||||
Item as PlaidItem,
|
|
||||||
Institution as PlaidInstitution,
|
|
||||||
} from 'plaid';
|
|
||||||
import { CreateAccount } from '@/services/Accounts/CreateAccount';
|
import { CreateAccount } from '@/services/Accounts/CreateAccount';
|
||||||
import {
|
import {
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
@@ -22,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import uniqid from 'uniqid';
|
import { uniqid } from 'uniqid';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
@@ -58,7 +53,6 @@ export class PlaidSyncDb {
|
|||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const plaidAccount = await Account.query().findOne(
|
const plaidAccount = await Account.query().findOne(
|
||||||
'plaidAccountId',
|
'plaidAccountId',
|
||||||
createBankAccountDTO.plaidAccountId
|
createBankAccountDTO.plaidAccountId
|
||||||
@@ -83,15 +77,13 @@ export class PlaidSyncDb {
|
|||||||
*/
|
*/
|
||||||
public async syncBankAccounts(
|
public async syncBankAccounts(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
plaidAccounts: PlaidAccountBase[],
|
plaidAccounts: PlaidAccount[],
|
||||||
institution: PlaidInstitution,
|
institution: any,
|
||||||
item: PlaidItem,
|
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const transformToPlaidAccounts = transformPlaidAccountToCreateAccount(
|
const transformToPlaidAccounts =
|
||||||
item,
|
transformPlaidAccountToCreateAccount(institution);
|
||||||
institution
|
|
||||||
);
|
|
||||||
const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
|
const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
|
||||||
|
|
||||||
await bluebird.map(
|
await bluebird.map(
|
||||||
@@ -156,6 +148,7 @@ export class PlaidSyncDb {
|
|||||||
*/
|
*/
|
||||||
public async syncAccountsTransactions(
|
public async syncAccountsTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
batchNo: string,
|
||||||
plaidAccountsTransactions: PlaidTransaction[],
|
plaidAccountsTransactions: PlaidTransaction[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -168,6 +161,7 @@ export class PlaidSyncDb {
|
|||||||
return this.syncAccountTranactions(
|
return this.syncAccountTranactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
plaidAccountId,
|
plaidAccountId,
|
||||||
|
batchNo,
|
||||||
plaidTransactions,
|
plaidTransactions,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class PlaidUpdateTransactions {
|
|||||||
await this.fetchTransactionUpdates(tenantId, plaidItemId);
|
await this.fetchTransactionUpdates(tenantId, plaidItemId);
|
||||||
|
|
||||||
const request = { access_token: accessToken };
|
const request = { access_token: accessToken };
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
const plaidInstance = new PlaidClientWrapper();
|
||||||
const {
|
const {
|
||||||
data: { accounts, item },
|
data: { accounts, item },
|
||||||
} = await plaidInstance.accountsGet(request);
|
} = await plaidInstance.accountsGet(request);
|
||||||
@@ -66,19 +66,15 @@ export class PlaidUpdateTransactions {
|
|||||||
country_codes: ['US', 'UK'],
|
country_codes: ['US', 'UK'],
|
||||||
});
|
});
|
||||||
// Sync bank accounts.
|
// Sync bank accounts.
|
||||||
await this.plaidSync.syncBankAccounts(
|
await this.plaidSync.syncBankAccounts(tenantId, accounts, institution, trx);
|
||||||
tenantId,
|
|
||||||
accounts,
|
|
||||||
institution,
|
|
||||||
item,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
// Sync bank account transactions.
|
// Sync bank account transactions.
|
||||||
await this.plaidSync.syncAccountsTransactions(
|
await this.plaidSync.syncAccountsTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
added.concat(modified),
|
added.concat(modified),
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
// Sync removed transactions.
|
||||||
|
await this.plaidSync.syncRemoveTransactions(tenantId, removed, trx);
|
||||||
// Sync transactions cursor.
|
// Sync transactions cursor.
|
||||||
await this.plaidSync.syncTransactionsCursor(
|
await this.plaidSync.syncTransactionsCursor(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -147,7 +143,7 @@ export class PlaidUpdateTransactions {
|
|||||||
cursor: cursor,
|
cursor: cursor,
|
||||||
count: batchSize,
|
count: batchSize,
|
||||||
};
|
};
|
||||||
const plaidInstance = PlaidClientWrapper.getClient();
|
const plaidInstance = new PlaidClientWrapper();
|
||||||
const response = await plaidInstance.transactionsSync(request);
|
const response = await plaidInstance.transactionsSync(request);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
// Add this page of results
|
// Add this page of results
|
||||||
|
|||||||
@@ -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,28 +1,18 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import {
|
|
||||||
Item as PlaidItem,
|
|
||||||
Institution as PlaidInstitution,
|
|
||||||
AccountBase as PlaidAccount,
|
|
||||||
} from 'plaid';
|
|
||||||
import {
|
import {
|
||||||
CreateUncategorizedTransactionDTO,
|
CreateUncategorizedTransactionDTO,
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
|
PlaidAccount,
|
||||||
PlaidTransaction,
|
PlaidTransaction,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the Plaid account to create cashflow account DTO.
|
* Transformes the Plaid account to create cashflow account DTO.
|
||||||
* @param {PlaidItem} item -
|
* @param {PlaidAccount} plaidAccount
|
||||||
* @param {PlaidInstitution} institution -
|
|
||||||
* @param {PlaidAccount} plaidAccount -
|
|
||||||
* @returns {IAccountCreateDTO}
|
* @returns {IAccountCreateDTO}
|
||||||
*/
|
*/
|
||||||
export const transformPlaidAccountToCreateAccount = R.curry(
|
export const transformPlaidAccountToCreateAccount = R.curry(
|
||||||
(
|
(institution: any, plaidAccount: PlaidAccount): IAccountCreateDTO => {
|
||||||
item: PlaidItem,
|
|
||||||
institution: PlaidInstitution,
|
|
||||||
plaidAccount: PlaidAccount
|
|
||||||
): IAccountCreateDTO => {
|
|
||||||
return {
|
return {
|
||||||
name: `${institution.name} - ${plaidAccount.name}`,
|
name: `${institution.name} - ${plaidAccount.name}`,
|
||||||
code: '',
|
code: '',
|
||||||
@@ -30,10 +20,9 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
currencyCode: plaidAccount.balances.iso_currency_code,
|
currencyCode: plaidAccount.balances.iso_currency_code,
|
||||||
accountType: 'cash',
|
accountType: 'cash',
|
||||||
active: true,
|
active: true,
|
||||||
|
plaidAccountId: plaidAccount.account_id,
|
||||||
bankBalance: plaidAccount.balances.current,
|
bankBalance: plaidAccount.balances.current,
|
||||||
accountMask: plaidAccount.mask,
|
accountMask: plaidAccount.mask,
|
||||||
plaidAccountId: plaidAccount.account_id,
|
|
||||||
plaidItemId: item.item_id,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -48,6 +37,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||||
(
|
(
|
||||||
cashflowAccountId: number,
|
cashflowAccountId: number,
|
||||||
|
creditAccountId: number,
|
||||||
plaidTranasction: PlaidTransaction
|
plaidTranasction: PlaidTransaction
|
||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user