mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
Compare commits
10 Commits
multi-line
...
advanced-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e227ff28 | ||
|
|
b590d2cb03 | ||
|
|
daf1cd38c0 | ||
|
|
3e2997d745 | ||
|
|
f3af3843dd | ||
|
|
b68d180785 | ||
|
|
341d47cc7b | ||
|
|
5c3a371e8a | ||
|
|
1141991e44 | ||
|
|
8cd3a6c48d |
@@ -132,24 +132,6 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"bug"
|
"bug"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "oleynikd",
|
|
||||||
"name": "Denis",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3976868?v=4",
|
|
||||||
"profile": "https://github.com/oleynikd",
|
|
||||||
"contributions": [
|
|
||||||
"bug"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mittalsam98",
|
|
||||||
"name": "Sachin Mittal",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/42431274?v=4",
|
|
||||||
"profile": "https://myself.vercel.app/",
|
|
||||||
"contributions": [
|
|
||||||
"bug"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
@@ -126,10 +126,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
|
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -103,20 +103,24 @@ export default class AccountsController extends BaseController {
|
|||||||
check('name')
|
check('name')
|
||||||
.exists()
|
.exists()
|
||||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('code')
|
check('code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isLength({ min: 3, max: 6 })
|
.isLength({ min: 3, max: 6 })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('currency_code').optional(),
|
check('currency_code').optional(),
|
||||||
check('account_type')
|
check('account_type')
|
||||||
.exists()
|
.exists()
|
||||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('description')
|
check('description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('parent_account_id')
|
check('parent_account_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||||
@@ -132,19 +136,23 @@ export default class AccountsController extends BaseController {
|
|||||||
check('name')
|
check('name')
|
||||||
.exists()
|
.exists()
|
||||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('code')
|
check('code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isLength({ min: 3, max: 6 })
|
.isLength({ min: 3, max: 6 })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('account_type')
|
check('account_type')
|
||||||
.exists()
|
.exists()
|
||||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('description')
|
check('description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('parent_account_id')
|
check('parent_account_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -90,23 +90,27 @@ export default class AuthenticationController extends BaseController {
|
|||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('last_name')
|
check('last_name')
|
||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('email')
|
check('email')
|
||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.isEmail()
|
.isEmail()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('password')
|
check('password')
|
||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 6 })
|
.isLength({ min: 6 })
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,7 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @returns {ValidationChain[]}
|
* @returns {ValidationChain[]}
|
||||||
*/
|
*/
|
||||||
private get sendResetPasswordSchema(): ValidationChain[] {
|
private get sendResetPasswordSchema(): ValidationChain[] {
|
||||||
return [check('email').exists().isEmail().trim()];
|
return [check('email').exists().isEmail().trim().escape()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,11 +158,7 @@ export default class AuthenticationController extends BaseController {
|
|||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
private async login(
|
private async login(req: Request, res: Response, next: Function): Response {
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: Function
|
|
||||||
): Promise<Response | null> {
|
|
||||||
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,22 +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 { param, query } from 'express-validator';
|
|
||||||
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 { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankAccountsController extends BaseController {
|
export class BankAccountsController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private getBankAccountSummaryService: GetBankAccountSummary;
|
private getBankAccountSummaryService: GetBankAccountSummary;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private bankAccountsApp: BankAccountsApplication;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private getPendingTransactionsService: GetPendingBankAccountTransactions;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -24,33 +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.get(
|
|
||||||
'/pending_transactions',
|
|
||||||
[
|
|
||||||
query('account_id').optional().isNumeric().toInt(),
|
|
||||||
query('page').optional().isNumeric().toInt(),
|
|
||||||
query('page_size').optional().isNumeric().toInt(),
|
|
||||||
],
|
|
||||||
this.validationResult,
|
|
||||||
this.getBankAccountsPendingTransactions.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;
|
||||||
}
|
}
|
||||||
@@ -81,138 +46,4 @@ export class BankAccountsController extends BaseController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the bank account pending transactions.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async getBankAccountsPendingTransactions(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const query = this.matchedQueryData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data =
|
|
||||||
await this.getPendingTransactionsService.getPendingTransactions(
|
|
||||||
tenantId,
|
|
||||||
query
|
|
||||||
);
|
|
||||||
return res.status(200).send(data);
|
|
||||||
} catch (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,16 @@ export class BankingRulesController extends BaseController {
|
|||||||
body('conditions.*.comparator')
|
body('conditions.*.comparator')
|
||||||
.exists()
|
.exists()
|
||||||
.isIn(['equals', 'contains', 'not_contain'])
|
.isIn(['equals', 'contains', 'not_contain'])
|
||||||
.default('contain')
|
.default('contain'),
|
||||||
.trim(),
|
body('conditions.*.value').exists(),
|
||||||
body('conditions.*.value').exists().trim(),
|
|
||||||
|
|
||||||
// Assign
|
// Assign
|
||||||
body('assign_category').isString(),
|
body('assign_category').isString(),
|
||||||
body('assign_account_id').isInt({ min: 0 }),
|
body('assign_account_id').isInt({ min: 0 }),
|
||||||
body('assign_payee').isString().optional({ nullable: true }),
|
body('assign_payee').isString().optional({ nullable: true }),
|
||||||
body('assign_memo').isString().optional({ nullable: true }),
|
body('assign_memo').isString().optional({ nullable: true }),
|
||||||
|
|
||||||
|
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
|
||||||
import { query } from 'express-validator';
|
|
||||||
import BaseController from '../BaseController';
|
|
||||||
import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class BankingUncategorizedController extends BaseController {
|
|
||||||
@Inject()
|
|
||||||
private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Router constructor.
|
|
||||||
*/
|
|
||||||
router() {
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/autofill',
|
|
||||||
[
|
|
||||||
query('uncategorizedTransactionIds').isArray({ min: 1 }),
|
|
||||||
query('uncategorizedTransactionIds.*').isNumeric().toInt(),
|
|
||||||
],
|
|
||||||
this.validationResult,
|
|
||||||
this.getAutofillCategorizeTransaction.bind(this)
|
|
||||||
);
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the autofill values of the categorize form of the given
|
|
||||||
* uncategorized transactions.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response | null>}
|
|
||||||
*/
|
|
||||||
public async getAutofillCategorizeTransaction(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data =
|
|
||||||
await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionIds
|
|
||||||
);
|
|
||||||
return res.status(200).send({ data });
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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, body, 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()
|
||||||
@@ -43,23 +38,13 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
this.asyncMiddleware(this.newCashflowTransaction),
|
this.asyncMiddleware(this.newCashflowTransaction),
|
||||||
this.catchServiceErrors
|
this.catchServiceErrors
|
||||||
);
|
);
|
||||||
router.post(
|
|
||||||
'/transactions/uncategorize/bulk',
|
|
||||||
[
|
|
||||||
body('ids').isArray({ min: 1 }),
|
|
||||||
body('ids.*').exists().isNumeric().toInt(),
|
|
||||||
],
|
|
||||||
this.validationResult,
|
|
||||||
this.uncategorizeBulkTransactions.bind(this),
|
|
||||||
this.catchServiceErrors
|
|
||||||
);
|
|
||||||
router.post(
|
router.post(
|
||||||
'/transactions/:id/uncategorize',
|
'/transactions/:id/uncategorize',
|
||||||
this.revertCategorizedCashflowTransaction,
|
this.revertCategorizedCashflowTransaction,
|
||||||
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,
|
||||||
@@ -104,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(),
|
||||||
@@ -122,11 +106,12 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
public get newTransactionValidationSchema() {
|
public get newTransactionValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('date').exists().isISO8601().toDate(),
|
check('date').exists().isISO8601().toDate(),
|
||||||
check('reference_no').optional({ nullable: true }).trim(),
|
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||||
check('description')
|
check('description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isLength({ min: 3 })
|
.isLength({ min: 3 })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('transaction_type').exists(),
|
check('transaction_type').exists(),
|
||||||
|
|
||||||
check('amount').exists().isFloat().toFloat(),
|
check('amount').exists().isFloat().toFloat(),
|
||||||
@@ -176,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
|
||||||
) => {
|
) => {
|
||||||
@@ -194,34 +179,6 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncategorize the given transactions in bulk.
|
|
||||||
* @param {Request<{}>} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
* @returns {Promise<Response | null>}
|
|
||||||
*/
|
|
||||||
private uncategorizeBulkTransactions = async (
|
|
||||||
req: Request<{}>,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) => {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.cashflowApplication.uncategorizeTransactions(
|
|
||||||
tenantId,
|
|
||||||
uncategorizedTransactionIds
|
|
||||||
);
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'The given transactions have been uncategorized successfully.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categorize the cashflow transaction.
|
* Categorize the cashflow transaction.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
@@ -234,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.',
|
||||||
@@ -317,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
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default class ContactsController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
get autocompleteQuerySchema() {
|
get autocompleteQuerySchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('stringified_filter_roles').optional().isJSON(),
|
query('stringified_filter_roles').optional().isJSON(),
|
||||||
@@ -122,27 +122,32 @@ export default class ContactsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('first_name')
|
check('first_name')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('last_name')
|
check('last_name')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('company_name')
|
check('company_name')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
|
||||||
check('display_name')
|
check('display_name')
|
||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
|
||||||
check('email')
|
check('email')
|
||||||
@@ -160,101 +165,120 @@ export default class ContactsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('personal_phone')
|
check('personal_phone')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
|
||||||
check('billing_address_1')
|
check('billing_address_1')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_2')
|
check('billing_address_2')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_city')
|
check('billing_address_city')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_country')
|
check('billing_address_country')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_email')
|
check('billing_address_email')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.isEmail()
|
.isEmail()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_postcode')
|
check('billing_address_postcode')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_phone')
|
check('billing_address_phone')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('billing_address_state')
|
check('billing_address_state')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
|
||||||
check('shipping_address_1')
|
check('shipping_address_1')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_2')
|
check('shipping_address_2')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_city')
|
check('shipping_address_city')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_country')
|
check('shipping_address_country')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_email')
|
check('shipping_address_email')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.isEmail()
|
.isEmail()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_postcode')
|
check('shipping_address_postcode')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_phone')
|
check('shipping_address_phone')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('shipping_address_state')
|
check('shipping_address_state')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
|
|
||||||
check('note')
|
check('note')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('active').optional().isBoolean().toBoolean(),
|
check('active').optional().isBoolean().toBoolean(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ export default class CustomersController extends ContactsController {
|
|||||||
*/
|
*/
|
||||||
get customerDTOSchema() {
|
get customerDTOSchema() {
|
||||||
return [
|
return [
|
||||||
check('customer_type').exists().isIn(['business', 'individual']).trim(),
|
check('customer_type')
|
||||||
|
.exists()
|
||||||
|
.isIn(['business', 'individual'])
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +123,7 @@ export default class CustomersController extends ContactsController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: 3 }),
|
.isLength({ max: 3 }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -128,7 +133,7 @@ export default class CustomersController extends ContactsController {
|
|||||||
*/
|
*/
|
||||||
get validateListQuerySchema() {
|
get validateListQuerySchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('page').optional().isNumeric().toInt(),
|
query('page').optional().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export default class VendorsController extends ContactsController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ min: 3, max: 3 }),
|
.isLength({ min: 3, max: 3 }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default class CurrenciesController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get currencyParamSchema(): ValidationChain[] {
|
get currencyParamSchema(): ValidationChain[] {
|
||||||
return [param('currency_code').exists().trim()];
|
return [param('currency_code').exists().trim().escape()];
|
||||||
}
|
}
|
||||||
|
|
||||||
get listSchema(): ValidationChain[] {
|
get listSchema(): ValidationChain[] {
|
||||||
@@ -187,13 +187,11 @@ export default class CurrenciesController extends BaseController {
|
|||||||
}
|
}
|
||||||
if (error.errorType === 'currency_code_exists') {
|
if (error.errorType === 'currency_code_exists') {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [
|
errors: [{
|
||||||
{
|
type: 'CURRENCY_CODE_EXISTS',
|
||||||
type: 'CURRENCY_CODE_EXISTS',
|
message: 'The given currency code is already exists.',
|
||||||
message: 'The given currency code is already exists.',
|
code: 200,
|
||||||
code: 200,
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
|
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export class ExpensesController extends BaseController {
|
|||||||
check('reference_no')
|
check('reference_no')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('payment_date').exists().isISO8601().toDate(),
|
check('payment_date').exists().isISO8601().toDate(),
|
||||||
check('payment_account_id')
|
check('payment_account_id')
|
||||||
@@ -122,6 +123,7 @@ export class ExpensesController extends BaseController {
|
|||||||
check('categories.*.description')
|
check('categories.*.description')
|
||||||
.optional()
|
.optional()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||||
check('categories.*.project_id')
|
check('categories.*.project_id')
|
||||||
@@ -142,6 +144,7 @@ export class ExpensesController extends BaseController {
|
|||||||
check('reference_no')
|
check('reference_no')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('payment_date').exists().isISO8601().toDate(),
|
check('payment_date').exists().isISO8601().toDate(),
|
||||||
check('payment_account_id')
|
check('payment_account_id')
|
||||||
@@ -176,6 +179,7 @@ export class ExpensesController extends BaseController {
|
|||||||
check('categories.*.description')
|
check('categories.*.description')
|
||||||
.optional()
|
.optional()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||||
check('categories.*.project_id')
|
check('categories.*.project_id')
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { query } from 'express-validator';
|
import { query } from 'express-validator';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from "../BaseController";
|
||||||
|
|
||||||
export default class BaseFinancialReportController extends BaseController {
|
export default class BaseFinancialReportController extends BaseController {
|
||||||
|
|
||||||
|
|
||||||
get sheetNumberFormatValidationSchema() {
|
get sheetNumberFormatValidationSchema() {
|
||||||
return [
|
return [
|
||||||
query('number_format.precision')
|
query('number_format.precision')
|
||||||
@@ -17,7 +19,8 @@ export default class BaseFinancialReportController extends BaseController {
|
|||||||
query('number_format.negative_format')
|
query('number_format.negative_format')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['parentheses', 'mines'])
|
.isIn(['parentheses', 'mines'])
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,8 @@ export default class InventoryDetailsController extends BaseController {
|
|||||||
query('number_format.negative_format')
|
query('number_format.negative_format')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['parentheses', 'mines'])
|
.isIn(['parentheses', 'mines'])
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
query('from_date').optional(),
|
query('from_date').optional(),
|
||||||
query('to_date').optional(),
|
query('to_date').optional(),
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
|||||||
return [
|
return [
|
||||||
query('from_date').optional().isISO8601(),
|
query('from_date').optional().isISO8601(),
|
||||||
query('to_date').optional().isISO8601(),
|
query('to_date').optional().isISO8601(),
|
||||||
query('transaction_type').optional().trim(),
|
query('transaction_type').optional().trim().escape(),
|
||||||
query('transaction_id').optional().isInt().toInt(),
|
query('transaction_id').optional().isInt().toInt(),
|
||||||
oneOf(
|
oneOf(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export default class TransactionsByReferenceController extends BaseController {
|
|||||||
query('number_format.negative_format')
|
query('number_format.negative_format')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['parentheses', 'mines'])
|
.isIn(['parentheses', 'mines'])
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default class InventoryAdjustmentsController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
get validateListQuerySchema() {
|
get validateListQuerySchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('page').optional().isNumeric().toInt(),
|
query('page').optional().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default class InviteUsersController extends BaseController {
|
|||||||
router.post(
|
router.post(
|
||||||
'/send',
|
'/send',
|
||||||
[
|
[
|
||||||
body('email').exists().trim(),
|
body('email').exists().trim().escape(),
|
||||||
body('role_id').exists().isNumeric().toInt(),
|
body('role_id').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
@@ -57,7 +57,7 @@ export default class InviteUsersController extends BaseController {
|
|||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/invited/:token',
|
'/invited/:token',
|
||||||
[param('token').exists().trim()],
|
[param('token').exists().trim().escape()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
asyncMiddleware(this.invited.bind(this)),
|
asyncMiddleware(this.invited.bind(this)),
|
||||||
this.handleServicesError
|
this.handleServicesError
|
||||||
@@ -72,10 +72,10 @@ export default class InviteUsersController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
private get inviteUserDTO() {
|
private get inviteUserDTO() {
|
||||||
return [
|
return [
|
||||||
check('first_name').exists().trim(),
|
check('first_name').exists().trim().escape(),
|
||||||
check('last_name').exists().trim(),
|
check('last_name').exists().trim().escape(),
|
||||||
check('password').exists().trim().isLength({ min: 5 }),
|
check('password').exists().trim().escape().isLength({ min: 5 }),
|
||||||
param('token').exists().trim(),
|
param('token').exists().trim().escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,11 +73,13 @@ export default class ItemsCategoriesController extends BaseController {
|
|||||||
check('name')
|
check('name')
|
||||||
.exists()
|
.exists()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ min: 0, max: DATATYPES_LENGTH.STRING }),
|
.isLength({ min: 0, max: DATATYPES_LENGTH.STRING }),
|
||||||
check('description')
|
check('description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('sell_account_id')
|
check('sell_account_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
@@ -99,8 +101,9 @@ export default class ItemsCategoriesController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
get categoriesListValidationSchema() {
|
get categoriesListValidationSchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().trim().isIn(['desc', 'asc']),
|
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('stringified_filter_roles').optional().isJSON(),
|
query('stringified_filter_roles').optional().isJSON(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -204,12 +207,14 @@ export default class ItemsCategoriesController extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { itemCategories, filterMeta } =
|
const {
|
||||||
await this.itemCategoriesService.getItemCategoriesList(
|
itemCategories,
|
||||||
tenantId,
|
filterMeta,
|
||||||
itemCategoriesFilter,
|
} = await this.itemCategoriesService.getItemCategoriesList(
|
||||||
user
|
tenantId,
|
||||||
);
|
itemCategoriesFilter,
|
||||||
|
user
|
||||||
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
item_categories: itemCategories,
|
item_categories: itemCategories,
|
||||||
filter_meta: this.transfromToResponse(filterMeta),
|
filter_meta: this.transfromToResponse(filterMeta),
|
||||||
|
|||||||
@@ -96,11 +96,13 @@ export default class ItemsController extends BaseController {
|
|||||||
.exists()
|
.exists()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isIn(['service', 'non-inventory', 'inventory']),
|
.isIn(['service', 'non-inventory', 'inventory']),
|
||||||
check('code')
|
check('code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
// Purchase attributes.
|
// Purchase attributes.
|
||||||
check('purchasable').optional().isBoolean().toBoolean(),
|
check('purchasable').optional().isBoolean().toBoolean(),
|
||||||
@@ -139,11 +141,13 @@ export default class ItemsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('purchase_description')
|
check('purchase_description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
|
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
|
||||||
check('purchase_tax_rate_id')
|
check('purchase_tax_rate_id')
|
||||||
@@ -158,6 +162,7 @@ export default class ItemsController extends BaseController {
|
|||||||
.optional()
|
.optional()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('active').optional().isBoolean().toBoolean(),
|
check('active').optional().isBoolean().toBoolean(),
|
||||||
|
|
||||||
@@ -179,7 +184,7 @@ export default class ItemsController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
private get validateListQuerySchema() {
|
private get validateListQuerySchema() {
|
||||||
return [
|
return [
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('page').optional().isNumeric().toInt(),
|
query('page').optional().isNumeric().toInt(),
|
||||||
|
|||||||
@@ -94,21 +94,25 @@ export default class ManualJournalsController extends BaseController {
|
|||||||
.optional()
|
.optional()
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('journal_type')
|
check('journal_type')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('reference')
|
check('reference')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('description')
|
check('description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('publish').optional().isBoolean().toBoolean(),
|
check('publish').optional().isBoolean().toBoolean(),
|
||||||
@@ -159,7 +163,7 @@ export default class ManualJournalsController extends BaseController {
|
|||||||
query('page_size').optional().isNumeric().toInt(),
|
query('page_size').optional().isNumeric().toInt(),
|
||||||
query('custom_view_id').optional().isNumeric().toInt(),
|
query('custom_view_id').optional().isNumeric().toInt(),
|
||||||
|
|
||||||
query('column_sort_by').optional().trim(),
|
query('column_sort_by').optional().trim().escape(),
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||||
|
|
||||||
query('stringified_filter_roles').optional().isJSON(),
|
query('stringified_filter_roles').optional().isJSON(),
|
||||||
|
|||||||
@@ -61,14 +61,15 @@ export default class MediaController extends BaseController {
|
|||||||
|
|
||||||
get uploadValidationSchema() {
|
get uploadValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('model_name').optional().trim(),
|
// check('attachment'),
|
||||||
check('model_id').optional().isNumeric(),
|
check('model_name').optional().trim().escape(),
|
||||||
|
check('model_id').optional().isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
get linkValidationSchema() {
|
get linkValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('model_name').exists().trim(),
|
check('model_name').exists().trim().escape(),
|
||||||
check('model_id').exists().isNumeric().toInt(),
|
check('model_id').exists().isNumeric().toInt(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default class OrganizationController extends BaseController {
|
|||||||
private get commonOrganizationValidationSchema(): ValidationChain[] {
|
private get commonOrganizationValidationSchema(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
check('name').exists().trim(),
|
check('name').exists().trim(),
|
||||||
check('industry').optional({ nullable: true }).isString().trim(),
|
check('industry').optional({ nullable: true }).isString().trim().escape(),
|
||||||
check('location').exists().isString().isISO31661Alpha2(),
|
check('location').exists().isString().isISO31661Alpha2(),
|
||||||
check('base_currency').exists().isISO4217(),
|
check('base_currency').exists().isISO4217(),
|
||||||
check('timezone').exists().isIn(moment.tz.names()),
|
check('timezone').exists().isIn(moment.tz.names()),
|
||||||
@@ -87,7 +87,11 @@ export default class OrganizationController extends BaseController {
|
|||||||
private get updateOrganizationValidationSchema(): ValidationChain[] {
|
private get updateOrganizationValidationSchema(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
...this.commonOrganizationValidationSchema,
|
...this.commonOrganizationValidationSchema,
|
||||||
check('tax_number').optional({ nullable: true }).isString().trim(),
|
check('tax_number')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isString()
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export default class BillsController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
private get billValidationSchema() {
|
private get billValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('bill_number').exists().trim(),
|
check('bill_number').exists().trim().escape(),
|
||||||
check('reference_no').optional().trim(),
|
check('reference_no').optional().trim().escape(),
|
||||||
check('bill_date').exists().isISO8601(),
|
check('bill_date').exists().isISO8601(),
|
||||||
check('due_date').optional().isISO8601(),
|
check('due_date').optional().isISO8601(),
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export default class BillsController extends BaseController {
|
|||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
check('open').default(false).isBoolean().toBoolean(),
|
check('open').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
|
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
|
||||||
@@ -126,7 +126,10 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
check('entries.*.description')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.landed_cost')
|
check('entries.*.landed_cost')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isBoolean()
|
.isBoolean()
|
||||||
@@ -138,6 +141,7 @@ export default class BillsController extends BaseController {
|
|||||||
check('entries.*.tax_code')
|
check('entries.*.tax_code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isString(),
|
.isString(),
|
||||||
check('entries.*.tax_rate_id')
|
check('entries.*.tax_rate_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
@@ -154,8 +158,8 @@ export default class BillsController extends BaseController {
|
|||||||
*/
|
*/
|
||||||
private get billEditValidationSchema() {
|
private get billEditValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('bill_number').optional().trim(),
|
check('bill_number').optional().trim().escape(),
|
||||||
check('reference_no').optional().trim(),
|
check('reference_no').optional().trim().escape(),
|
||||||
check('bill_date').exists().isISO8601(),
|
check('bill_date').exists().isISO8601(),
|
||||||
check('due_date').optional().isISO8601(),
|
check('due_date').optional().isISO8601(),
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ export default class BillsController extends BaseController {
|
|||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
check('open').default(false).isBoolean().toBoolean(),
|
check('open').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('entries').isArray({ min: 1 }),
|
check('entries').isArray({ min: 1 }),
|
||||||
@@ -180,7 +184,10 @@ export default class BillsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
check('entries.*.description')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.landed_cost')
|
check('entries.*.landed_cost')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isBoolean()
|
.isBoolean()
|
||||||
@@ -215,8 +222,8 @@ export default class BillsController extends BaseController {
|
|||||||
|
|
||||||
private get dueBillsListingValidationSchema() {
|
private get dueBillsListingValidationSchema() {
|
||||||
return [
|
return [
|
||||||
query('vendor_id').optional().trim(),
|
query('vendor_id').optional().trim().escape(),
|
||||||
query('payment_made_id').optional().trim(),
|
query('payment_made_id').optional().trim().escape(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ export default class BillsPayments extends BaseController {
|
|||||||
|
|
||||||
check('amount').exists().isNumeric().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(),
|
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||||
check('payment_date').exists(),
|
check('payment_date').exists(),
|
||||||
check('statement').optional().trim(),
|
check('statement').optional().trim().escape(),
|
||||||
check('reference').optional().trim(),
|
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(),
|
||||||
@@ -126,6 +126,8 @@ export default class BillsPayments extends BaseController {
|
|||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
check('prepard_expenses_account_id').optional().isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,10 +156,13 @@ export default class VendorCreditController 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('vendor_credit_number').optional({ nullable: true }).trim(),
|
check('vendor_credit_number')
|
||||||
check('reference_no').optional().trim(),
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
|
check('reference_no').optional().trim().escape(),
|
||||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
check('open').default(false).isBoolean().toBoolean(),
|
check('open').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
@@ -175,7 +178,10 @@ export default class VendorCreditController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
check('entries.*.description')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
@@ -196,10 +202,13 @@ export default class VendorCreditController 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('vendor_credit_number').optional({ nullable: true }).trim(),
|
check('vendor_credit_number')
|
||||||
check('reference_no').optional().trim(),
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
|
check('reference_no').optional().trim().escape(),
|
||||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
|
|
||||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
@@ -214,7 +223,10 @@ export default class VendorCreditController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
check('entries.*.description')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export default class ResourceController extends BaseController {
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:resource_model/meta',
|
'/:resource_model/meta',
|
||||||
[param('resource_model').exists().trim()],
|
[
|
||||||
|
param('resource_model').exists().trim().escape()
|
||||||
|
],
|
||||||
this.asyncMiddleware(this.resourceMeta.bind(this)),
|
this.asyncMiddleware(this.resourceMeta.bind(this)),
|
||||||
this.handleServiceErrors
|
this.handleServiceErrors
|
||||||
);
|
);
|
||||||
@@ -46,7 +48,9 @@ export default class ResourceController extends BaseController {
|
|||||||
resourceModel
|
resourceModel
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
resource_meta: this.transfromToResponse(resourceMeta),
|
resource_meta: this.transfromToResponse(
|
||||||
|
resourceMeta,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -210,9 +210,9 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('credit_note_date').exists().isISO8601().toDate(),
|
check('credit_note_date').exists().isISO8601().toDate(),
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('credit_note_number').optional({ nullable: true }).trim(),
|
check('credit_note_number').optional({ nullable: true }).trim().escape(),
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
check('terms_conditions').optional().trim(),
|
check('terms_conditions').optional().trim().escape(),
|
||||||
check('open').default(false).isBoolean().toBoolean(),
|
check('open').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
@@ -228,7 +228,10 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
check('entries.*.description')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
|
|||||||
@@ -150,16 +150,18 @@ 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('amount').exists().isNumeric().toFloat(),
|
||||||
|
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||||
check('payment_receive_no').optional({ nullable: true }).trim(),
|
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
|
||||||
check('statement').optional().trim(),
|
check('statement').optional().trim().escape(),
|
||||||
|
|
||||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
|
|
||||||
check('entries').isArray({}),
|
check('entries').isArray(),
|
||||||
|
|
||||||
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(),
|
||||||
@@ -167,6 +169,11 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
check('unearned_revenue_account_id')
|
||||||
|
.optional({ nullable: true })
|
||||||
|
.isNumeric()
|
||||||
|
.toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +183,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
private get validatePaymentReceiveList(): ValidationChain[] {
|
private get validatePaymentReceiveList(): ValidationChain[] {
|
||||||
return [
|
return [
|
||||||
query('stringified_filter_roles').optional().isJSON(),
|
query('stringified_filter_roles').optional().isJSON(),
|
||||||
|
|
||||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||||
|
|
||||||
query('column_sort_by').optional(),
|
query('column_sort_by').optional(),
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
check('estimate_date').exists().isISO8601().toDate(),
|
check('estimate_date').exists().isISO8601().toDate(),
|
||||||
check('expiration_date').exists().isISO8601().toDate(),
|
check('expiration_date').exists().isISO8601().toDate(),
|
||||||
check('reference').optional(),
|
check('reference').optional(),
|
||||||
check('estimate_number').optional().trim(),
|
check('estimate_number').optional().trim().escape(),
|
||||||
check('delivered').default(false).isBoolean().toBoolean(),
|
check('delivered').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
@@ -170,7 +170,8 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||||
check('entries.*.description')
|
check('entries.*.description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.discount')
|
check('entries.*.discount')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
@@ -180,9 +181,9 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toInt(),
|
.toInt(),
|
||||||
|
|
||||||
check('note').optional().trim(),
|
check('note').optional().trim().escape(),
|
||||||
check('terms_conditions').optional().trim(),
|
check('terms_conditions').optional().trim().escape(),
|
||||||
check('send_to_email').optional().trim(),
|
check('send_to_email').optional().trim().escape(),
|
||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
check('customer_id').exists().isNumeric().toInt(),
|
check('customer_id').exists().isNumeric().toInt(),
|
||||||
check('invoice_date').exists().isISO8601().toDate(),
|
check('invoice_date').exists().isISO8601().toDate(),
|
||||||
check('due_date').exists().isISO8601().toDate(),
|
check('due_date').exists().isISO8601().toDate(),
|
||||||
check('invoice_no').optional().trim(),
|
check('invoice_no').optional().trim().escape(),
|
||||||
check('reference_no').optional().trim(),
|
check('reference_no').optional().trim().escape(),
|
||||||
check('delivered').default(false).isBoolean().toBoolean(),
|
check('delivered').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('invoice_message').optional().trim(),
|
check('invoice_message').optional().trim().escape(),
|
||||||
check('terms_conditions').optional().trim(),
|
check('terms_conditions').optional().trim().escape(),
|
||||||
|
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
@@ -226,10 +226,12 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description')
|
check('entries.*.description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.tax_code')
|
check('entries.*.tax_code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
|
.escape()
|
||||||
.isString(),
|
.isString(),
|
||||||
check('entries.*.tax_rate_id')
|
check('entries.*.tax_rate_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
|
|
||||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||||
check('receipt_date').exists().isISO8601(),
|
check('receipt_date').exists().isISO8601(),
|
||||||
check('receipt_number').optional().trim(),
|
check('receipt_number').optional().trim().escape(),
|
||||||
check('reference_no').optional().trim(),
|
check('reference_no').optional().trim().escape(),
|
||||||
check('closed').default(false).isBoolean().toBoolean(),
|
check('closed').default(false).isBoolean().toBoolean(),
|
||||||
|
|
||||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
@@ -150,13 +150,14 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
.toInt(),
|
.toInt(),
|
||||||
check('entries.*.description')
|
check('entries.*.description')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim(),
|
.trim()
|
||||||
|
.escape(),
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toInt(),
|
.toInt(),
|
||||||
check('receipt_message').optional().trim(),
|
check('receipt_message').optional().trim().escape(),
|
||||||
check('statement').optional().trim(),
|
check('statement').optional().trim().escape(),
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export default class SettingsController extends BaseController {
|
|||||||
* Retrieve the application options from the storage.
|
* Retrieve the application options from the storage.
|
||||||
*/
|
*/
|
||||||
private get getSettingsSchema() {
|
private get getSettingsSchema() {
|
||||||
return [query('key').optional().trim(), query('group').optional().trim()];
|
return [
|
||||||
|
query('key').optional().trim().escape(),
|
||||||
|
query('group').optional().trim().escape(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default class ViewsController extends BaseController {
|
|||||||
* Custom views list validation schema.
|
* Custom views list validation schema.
|
||||||
*/
|
*/
|
||||||
get viewsListSchemaValidation() {
|
get viewsListSchemaValidation() {
|
||||||
return [param('resource_model').exists().trim()];
|
return [param('resource_model').exists().trim().escape()];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,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');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('payment_receives', (table) => {
|
||||||
|
table.decimal('applied_amount', 13, 3).defaultTo(0);
|
||||||
|
table
|
||||||
|
.integer('unearned_revenue_account_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('payment_receives', (table) => {
|
||||||
|
table.dropColumn('applied_amount');
|
||||||
|
table.dropColumn('unearned_revenue_account_id');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema.table('bills_payments', (table) => {
|
||||||
|
table.decimal('applied_amount', 13, 3).defaultTo(0);
|
||||||
|
table
|
||||||
|
.integer('prepard_expenses_account_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('accounts');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema.table('bills_payments', (table) => {
|
||||||
|
table.dropColumn('applied_amount');
|
||||||
|
table.dropColumn('prepard_expenses_account_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,18 +0,0 @@
|
|||||||
// This migration changes the precision of the tax_amount_withheld column in the bills and sales_invoices tables from 8, 2 to 13, 2.
|
|
||||||
// This migration is necessary to allow tax_amount_withheld filed to store values bigger than 999,999.99.
|
|
||||||
|
|
||||||
exports.up = function(knex) {
|
|
||||||
return knex.schema.alterTable('bills', function (table) {
|
|
||||||
table.decimal('tax_amount_withheld', 13, 2).alter();
|
|
||||||
}).alterTable('sales_invoices', function (table) {
|
|
||||||
table.decimal('tax_amount_withheld', 13, 2).alter();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function(knex) {
|
|
||||||
return knex.schema.alterTable('bills', function (table) {
|
|
||||||
table.decimal('tax_amount_withheld', 8, 2).alter();
|
|
||||||
}).alterTable('sales_invoices', function (table) {
|
|
||||||
table.decimal('tax_amount_withheld', 8, 2).alter();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
exports.up = function (knex) {
|
|
||||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
|
||||||
table.boolean('pending').defaultTo(false);
|
|
||||||
table.string('pending_plaid_transaction_id').nullable();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.down = function (knex) {
|
|
||||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
|
||||||
table.dropColumn('pending');
|
|
||||||
table.dropColumn('pending_plaid_transaction_id');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -166,3 +166,10 @@ export interface IBillOpenedPayload {
|
|||||||
oldBill: IBill;
|
oldBill: IBill;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface IBillPrepardExpensesAppliedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
billId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export interface IBillPayment {
|
|||||||
|
|
||||||
localAmount?: number;
|
localAmount?: number;
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
|
|
||||||
|
prepardExpensesAccountId?: number;
|
||||||
|
isPrepardExpense: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillPaymentEntryDTO {
|
export interface IBillPaymentEntryDTO {
|
||||||
@@ -38,6 +41,7 @@ export interface IBillPaymentEntryDTO {
|
|||||||
|
|
||||||
export interface IBillPaymentDTO {
|
export interface IBillPaymentDTO {
|
||||||
vendorId: number;
|
vendorId: number;
|
||||||
|
amount: number;
|
||||||
paymentAccountId: number;
|
paymentAccountId: number;
|
||||||
paymentNumber?: string;
|
paymentNumber?: string;
|
||||||
paymentDate: Date;
|
paymentDate: Date;
|
||||||
@@ -47,6 +51,7 @@ export interface IBillPaymentDTO {
|
|||||||
entries: IBillPaymentEntryDTO[];
|
entries: IBillPaymentEntryDTO[];
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
attachments?: AttachmentLinkDTO[];
|
attachments?: AttachmentLinkDTO[];
|
||||||
|
prepardExpensesAccountId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillReceivePageEntry {
|
export interface IBillReceivePageEntry {
|
||||||
@@ -119,3 +124,11 @@ export enum IPaymentMadeAction {
|
|||||||
Delete = 'Delete',
|
Delete = 'Delete',
|
||||||
View = 'View',
|
View = 'View',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPaymentPrepardExpensesAppliedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
billPaymentId: number;
|
||||||
|
billId: number;
|
||||||
|
appliedAmount: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ export interface ICashflowTransactionSchema {
|
|||||||
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
||||||
|
|
||||||
export interface ICategorizeCashflowTransactioDTO {
|
export interface ICategorizeCashflowTransactioDTO {
|
||||||
date: Date;
|
|
||||||
creditAccountId: number;
|
creditAccountId: number;
|
||||||
referenceNo: string;
|
referenceNo: string;
|
||||||
transactionNumber: string;
|
transactionNumber: string;
|
||||||
@@ -268,8 +267,6 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
description?: string;
|
description?: string;
|
||||||
referenceNo?: string | null;
|
referenceNo?: string | null;
|
||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
pending?: boolean;
|
|
||||||
pendingPlaidTransactionId?: string | null;
|
|
||||||
batch?: string;
|
batch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,17 +282,3 @@ export interface IUncategorizedTransactionCreatedEventPayload {
|
|||||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPendingTransactionRemovingEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
|
||||||
trx?: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IPendingTransactionRemovedEventPayload {
|
|
||||||
tenantId: number;
|
|
||||||
uncategorizedTransactionId: number;
|
|
||||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
|
||||||
trx?: Knex.Transaction;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -130,23 +130,20 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
|
|
||||||
export interface ICashflowTransactionCategorizedPayload {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
uncategorizedTransaction: any;
|
||||||
cashflowTransaction: ICashflowTransaction;
|
cashflowTransaction: ICashflowTransaction;
|
||||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
|
||||||
categorizeDTO: any;
|
categorizeDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizingPayload {
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizedPayload {
|
export interface ICashflowTransactionUncategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionId: number;
|
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ export interface IPaymentReceive {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
localAmount?: number;
|
localAmount?: number;
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
|
unearnedRevenueAccountId?: number;
|
||||||
}
|
}
|
||||||
export interface IPaymentReceiveCreateDTO {
|
|
||||||
|
interface IPaymentReceivedCommonDTO {
|
||||||
|
unearnedRevenueAccountId?: number;
|
||||||
|
}
|
||||||
|
export interface IPaymentReceiveCreateDTO extends IPaymentReceivedCommonDTO {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
paymentDate: Date;
|
paymentDate: Date;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -41,7 +46,7 @@ export interface IPaymentReceiveCreateDTO {
|
|||||||
attachments?: AttachmentLinkDTO[];
|
attachments?: AttachmentLinkDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaymentReceiveEditDTO {
|
export interface IPaymentReceiveEditDTO extends IPaymentReceivedCommonDTO {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
paymentDate: Date;
|
paymentDate: Date;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -184,3 +189,11 @@ export interface PaymentReceiveMailPresendEvent {
|
|||||||
paymentReceiveId: number;
|
paymentReceiveId: number;
|
||||||
messageOptions: PaymentReceiveMailOptsDTO;
|
messageOptions: PaymentReceiveMailOptsDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaymentReceiveUnearnedRevenueAppliedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
paymentReceiveId: number;
|
||||||
|
saleInvoiceId: number;
|
||||||
|
appliedAmount: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -216,3 +216,9 @@ export interface ISaleInvoiceMailSent {
|
|||||||
saleInvoiceId: number;
|
saleInvoiceId: number;
|
||||||
messageOptions: SendInvoiceMailDTO;
|
messageOptions: SendInvoiceMailDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
saleInvoiceId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
return PlaidClientWrapper.instance.client;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/
|
|||||||
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
||||||
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
||||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||||
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
import { AutoApplyUnearnedRevenueOnInvoiceCreated } from '@/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated';
|
||||||
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
import { AutoApplyPrepardExpensesOnBillCreated } from '@/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -276,9 +276,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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export default class BillPayment extends mixin(TenantModel, [
|
|||||||
CustomViewBaseModel,
|
CustomViewBaseModel,
|
||||||
ModelSearchable,
|
ModelSearchable,
|
||||||
]) {
|
]) {
|
||||||
|
prepardExpensesAccountId: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -47,6 +49,14 @@ export default class BillPayment extends mixin(TenantModel, [
|
|||||||
return BillPaymentSettings;
|
return BillPaymentSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the payment is prepard expense.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isPrepardExpense() {
|
||||||
|
return !!this.prepardExpensesAccountId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
export default {
|
|
||||||
defaultSort: {
|
|
||||||
sortOrder: 'DESC',
|
|
||||||
sortField: 'created_at',
|
|
||||||
},
|
|
||||||
exportable: true,
|
|
||||||
importable: true,
|
|
||||||
print: {
|
|
||||||
pageTitle: 'Tax Rates',
|
|
||||||
},
|
|
||||||
columns: {
|
|
||||||
name: {
|
|
||||||
name: 'Tax Rate Name',
|
|
||||||
type: 'text',
|
|
||||||
accessor: 'name',
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
name: 'Code',
|
|
||||||
type: 'text',
|
|
||||||
accessor: 'code',
|
|
||||||
},
|
|
||||||
rate: {
|
|
||||||
name: 'Rate',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
name: 'Description',
|
|
||||||
type: 'text',
|
|
||||||
},
|
|
||||||
isNonRecoverable: {
|
|
||||||
name: 'Is Non Recoverable',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
name: 'Active',
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
field: {},
|
|
||||||
fields2: {
|
|
||||||
name: {
|
|
||||||
name: 'Tax name',
|
|
||||||
fieldType: 'name',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
name: 'Code',
|
|
||||||
fieldType: 'code',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
rate: {
|
|
||||||
name: 'Rate',
|
|
||||||
fieldType: 'number',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
name: 'Description',
|
|
||||||
fieldType: 'text',
|
|
||||||
},
|
|
||||||
isNonRecoverable: {
|
|
||||||
name: 'Is Non Recoverable',
|
|
||||||
fieldType: 'boolean',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
name: 'Active',
|
|
||||||
fieldType: 'boolean',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -2,13 +2,8 @@ import { mixin, Model, raw } from 'objection';
|
|||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
import ModelSearchable from './ModelSearchable';
|
import ModelSearchable from './ModelSearchable';
|
||||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||||
import TaxRateMeta from './TaxRate.settings';
|
|
||||||
import ModelSetting from './ModelSetting';
|
|
||||||
|
|
||||||
export default class TaxRate extends mixin(TenantModel, [
|
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
|
||||||
ModelSetting,
|
|
||||||
ModelSearchable,
|
|
||||||
]) {
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -30,13 +25,6 @@ export default class TaxRate extends mixin(TenantModel, [
|
|||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the tax rate meta.
|
|
||||||
*/
|
|
||||||
static get meta() {
|
|
||||||
return TaxRateMeta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
description!: string;
|
description!: string;
|
||||||
plaidTransactionId!: string;
|
plaidTransactionId!: string;
|
||||||
recognizedTransactionId!: number;
|
recognizedTransactionId!: number;
|
||||||
excludedAt: Date;
|
|
||||||
pending: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
@@ -33,7 +31,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
/**
|
/**
|
||||||
* Timestamps columns.
|
* Timestamps columns.
|
||||||
*/
|
*/
|
||||||
get timestamps() {
|
static get timestamps() {
|
||||||
return ['createdAt', 'updatedAt'];
|
return ['createdAt', 'updatedAt'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +45,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
'isDepositTransaction',
|
'isDepositTransaction',
|
||||||
'isWithdrawalTransaction',
|
'isWithdrawalTransaction',
|
||||||
'isRecognized',
|
'isRecognized',
|
||||||
'isExcluded',
|
|
||||||
'isPending',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,22 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detarmines whether the transaction is pending.
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
public get isPending(): boolean {
|
|
||||||
return !!this.pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -153,20 +133,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
query.whereNull('categorizeRefType');
|
query.whereNull('categorizeRefType');
|
||||||
query.whereNull('categorizeRefId');
|
query.whereNull('categorizeRefId');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the not pending transactions.
|
|
||||||
*/
|
|
||||||
notPending(query) {
|
|
||||||
query.where('pending', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the pending transactions.
|
|
||||||
*/
|
|
||||||
pending(query) {
|
|
||||||
query.where('pending', true);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Inject, Service } from 'typedi';
|
||||||
import { initialize } from 'objection';
|
import { initialize } from 'objection';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetBankAccountSummary {
|
export class GetBankAccountSummary {
|
||||||
@@ -32,85 +31,46 @@ export class GetBankAccountSummary {
|
|||||||
.findById(bankAccountId)
|
.findById(bankAccountId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const commonQuery = (q) => {
|
|
||||||
// Include just the given account.
|
|
||||||
q.where('accountId', bankAccountId);
|
|
||||||
|
|
||||||
// Only the not excluded.
|
|
||||||
q.modify('notExcluded');
|
|
||||||
|
|
||||||
// Only the not categorized.
|
|
||||||
q.modify('notCategorized');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieves the uncategorized transactions count of the given bank account.
|
// Retrieves the uncategorized transactions count of the given bank account.
|
||||||
const uncategorizedTranasctionsCount =
|
const uncategorizedTranasctionsCount =
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||||
commonQuery(q);
|
// Include just the given account.
|
||||||
|
q.where('accountId', bankAccountId);
|
||||||
|
|
||||||
|
// Only the not excluded.
|
||||||
|
q.modify('notExcluded');
|
||||||
|
|
||||||
|
// Only the not categorized.
|
||||||
|
q.modify('notCategorized');
|
||||||
|
|
||||||
// Only the not matched bank transactions.
|
// Only the not matched bank transactions.
|
||||||
q.withGraphJoined('matchedBankTransactions');
|
q.withGraphJoined('matchedBankTransactions');
|
||||||
q.whereNull('matchedBankTransactions.id');
|
q.whereNull('matchedBankTransactions.id');
|
||||||
|
|
||||||
// Exclude the pending transactions.
|
|
||||||
q.modify('notPending');
|
|
||||||
|
|
||||||
// Count the results.
|
// Count the results.
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
q.count('uncategorized_cashflow_transactions.id as total');
|
||||||
q.first();
|
q.first();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retrives the recognized transactions count.
|
// Retrieves the recognized transactions count of the given bank account.
|
||||||
const recognizedTransactionsCount =
|
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
.whereExists(
|
||||||
commonQuery(q);
|
UncategorizedCashflowTransaction.query().where(
|
||||||
|
'accountId',
|
||||||
q.withGraphJoined('recognizedTransaction');
|
bankAccountId
|
||||||
q.whereNotNull('recognizedTransaction.id');
|
)
|
||||||
|
)
|
||||||
// Exclude the pending transactions.
|
.count('id as total')
|
||||||
q.modify('notPending');
|
.first();
|
||||||
|
|
||||||
// Count the results.
|
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
|
||||||
q.first();
|
|
||||||
});
|
|
||||||
// Retrieves excluded transactions count.
|
|
||||||
const excludedTransactionsCount =
|
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
|
||||||
q.where('accountId', bankAccountId);
|
|
||||||
q.modify('excluded');
|
|
||||||
|
|
||||||
// Exclude the pending transactions.
|
|
||||||
q.modify('notPending');
|
|
||||||
|
|
||||||
// Count the results.
|
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
|
||||||
q.first();
|
|
||||||
});
|
|
||||||
// Retrieves the pending transactions count.
|
|
||||||
const pendingTransactionsCount =
|
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
|
||||||
q.where('accountId', bankAccountId);
|
|
||||||
q.modify('pending');
|
|
||||||
|
|
||||||
// Count the results.
|
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
|
||||||
q.first();
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalUncategorizedTransactions =
|
const totalUncategorizedTransactions =
|
||||||
uncategorizedTranasctionsCount?.total || 0;
|
uncategorizedTranasctionsCount?.total || 0;
|
||||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||||
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
|
|
||||||
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: bankAccount.name,
|
name: bankAccount.name,
|
||||||
totalUncategorizedTransactions,
|
totalUncategorizedTransactions,
|
||||||
totalRecognizedTransactions,
|
totalRecognizedTransactions,
|
||||||
totalExcludedTransactions,
|
|
||||||
totalPendingTransactions,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +1,7 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import {
|
import { Inject, Service } from 'typedi';
|
||||||
validateTransactionNotCategorized,
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
validateTransactionNotExcluded,
|
|
||||||
} from './utils';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import {
|
import {
|
||||||
@@ -41,13 +37,9 @@ export class ExcludeBankTransaction {
|
|||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validate the transaction shouldn't be excluded.
|
|
||||||
validateTransactionNotExcluded(oldUncategorizedTransaction);
|
|
||||||
|
|
||||||
// Validate the transaction shouldn't be categorized.
|
|
||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
|
|||||||
@@ -1,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,11 +1,7 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import {
|
import { Inject, Service } from 'typedi';
|
||||||
validateTransactionNotCategorized,
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
validateTransactionShouldBeExcluded,
|
|
||||||
} from './utils';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import {
|
import {
|
||||||
@@ -41,13 +37,9 @@ export class UnexcludeBankTransaction {
|
|||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validate the transaction should be excludded.
|
|
||||||
validateTransactionShouldBeExcluded(oldUncategorizedTransaction);
|
|
||||||
|
|
||||||
// Validate the transaction shouldn't be categorized.
|
|
||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.bankTransactions.onUnexcluding,
|
events.bankTransactions.onUnexcluding,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran
|
|||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
||||||
TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED',
|
|
||||||
TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateTransactionNotCategorized = (
|
export const validateTransactionNotCategorized = (
|
||||||
@@ -14,19 +12,3 @@ export const validateTransactionNotCategorized = (
|
|||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateTransactionNotExcluded = (
|
|
||||||
transaction: UncategorizedCashflowTransaction
|
|
||||||
) => {
|
|
||||||
if (transaction.isExcluded) {
|
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_EXCLUDED);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateTransactionShouldBeExcluded = (
|
|
||||||
transaction: UncategorizedCashflowTransaction
|
|
||||||
) => {
|
|
||||||
if (!transaction.isExcluded) {
|
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_NOT_EXCLUDED);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { first, sumBy } from 'lodash';
|
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
||||||
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
||||||
@@ -48,24 +47,21 @@ export class GetMatchedTransactions {
|
|||||||
/**
|
/**
|
||||||
* Retrieves the matched transactions.
|
* Retrieves the matched transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
|
|
||||||
* @param {GetMatchedTransactionsFilter} filter -
|
* @param {GetMatchedTransactionsFilter} filter -
|
||||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||||
*/
|
*/
|
||||||
public async getMatchedTransactions(
|
public async getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionIds: Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const uncategorizedTransactions =
|
const uncategorizedTransaction =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.whereIn('id', uncategorizedTransactionIds)
|
.findById(uncategorizedTransactionId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const totalPending = sumBy(uncategorizedTransactions, 'amount');
|
|
||||||
|
|
||||||
const filtered = filter.transactionType
|
const filtered = filter.transactionType
|
||||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||||
: this.registered;
|
: this.registered;
|
||||||
@@ -75,14 +71,14 @@ export class GetMatchedTransactions {
|
|||||||
.process(async ({ type, service }) => {
|
.process(async ({ type, service }) => {
|
||||||
return service.getMatchedTransactions(tenantId, filter);
|
return service.getMatchedTransactions(tenantId, filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
||||||
uncategorizedTransactions,
|
uncategorizedTransaction,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
perfectMatches,
|
perfectMatches,
|
||||||
possibleMatches,
|
possibleMatches,
|
||||||
totalPending,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,20 +90,20 @@ export class GetMatchedTransactions {
|
|||||||
* @returns {MatchedTransactionsPOJO}
|
* @returns {MatchedTransactionsPOJO}
|
||||||
*/
|
*/
|
||||||
private groupMatchedResults(
|
private groupMatchedResults(
|
||||||
uncategorizedTransactions: Array<any>,
|
uncategorizedTransaction,
|
||||||
matchedTransactions
|
matchedTransactions
|
||||||
): MatchedTransactionsPOJO {
|
): MatchedTransactionsPOJO {
|
||||||
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
const results = R.compose(R.flatten)(matchedTransactions?.results);
|
||||||
|
|
||||||
const firstUncategorized = first(uncategorizedTransactions);
|
|
||||||
const amount = sumBy(uncategorizedTransactions, 'amount');
|
|
||||||
const date = firstUncategorized.date;
|
|
||||||
|
|
||||||
// Sort the results based on amount, date, and transaction type
|
// Sort the results based on amount, date, and transaction type
|
||||||
const closestResullts = sortClosestMatchTransactions(amount, date, results);
|
const closestResullts = sortClosestMatchTransactions(
|
||||||
|
uncategorizedTransaction,
|
||||||
|
results
|
||||||
|
);
|
||||||
const perfectMatches = R.filter(
|
const perfectMatches = R.filter(
|
||||||
(match) =>
|
(match) =>
|
||||||
match.amount === amount && moment(match.date).isSame(date, 'day'),
|
match.amount === uncategorizedTransaction.amount &&
|
||||||
|
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
|
||||||
closestResullts
|
closestResullts
|
||||||
);
|
);
|
||||||
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
MatchedTransactionsPOJO,
|
MatchedTransactionsPOJO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import PromisePool from '@supercharge/promise-pool';
|
|
||||||
|
|
||||||
export abstract class GetMatchedTransactionsByType {
|
export abstract class GetMatchedTransactionsByType {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -44,28 +43,24 @@ export abstract class GetMatchedTransactionsByType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the common matched transaction.
|
*
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {Array<number>} uncategorizedTransactionIds
|
* @param {number} uncategorizedTransactionId
|
||||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||||
* @param {Knex.Transaction} trx
|
* @param {Knex.Transaction} trx
|
||||||
*/
|
*/
|
||||||
public async createMatchedTransaction(
|
public async createMatchedTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionIds: Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
matchTransactionDTO: IMatchTransactionDTO,
|
matchTransactionDTO: IMatchTransactionDTO,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
await PromisePool.withConcurrency(2)
|
await MatchedBankTransaction.query(trx).insert({
|
||||||
.for(uncategorizedTransactionIds)
|
uncategorizedTransactionId,
|
||||||
.process(async (uncategorizedTransactionId) => {
|
referenceType: matchTransactionDTO.referenceType,
|
||||||
await MatchedBankTransaction.query(trx).insert({
|
referenceId: matchTransactionDTO.referenceId,
|
||||||
uncategorizedTransactionId,
|
});
|
||||||
referenceType: matchTransactionDTO.referenceType,
|
|
||||||
referenceId: matchTransactionDTO.referenceId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
||||||
import { MatchBankTransactions } from './MatchTransactions';
|
import { MatchBankTransactions } from './MatchTransactions';
|
||||||
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
||||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactionsApplication {
|
export class MatchBankTransactionsApplication {
|
||||||
@@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication {
|
|||||||
*/
|
*/
|
||||||
public getMatchedTransactions(
|
public getMatchedTransactions(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionsIds: Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
) {
|
) {
|
||||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionsIds,
|
uncategorizedTransactionId,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -42,13 +42,13 @@ export class MatchBankTransactionsApplication {
|
|||||||
*/
|
*/
|
||||||
public matchTransaction(
|
public matchTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number | Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>
|
matchTransactionsDTO: IMatchTransactionsDTO
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.matchTransactionService.matchTransaction(
|
return this.matchTransactionService.matchTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
uncategorizedTransactionId,
|
||||||
matchedTransactions
|
matchTransactionsDTO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { castArray } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
@@ -10,16 +10,11 @@ import {
|
|||||||
ERRORS,
|
ERRORS,
|
||||||
IBankTransactionMatchedEventPayload,
|
IBankTransactionMatchedEventPayload,
|
||||||
IBankTransactionMatchingEventPayload,
|
IBankTransactionMatchingEventPayload,
|
||||||
IMatchTransactionDTO,
|
IMatchTransactionsDTO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import { sumMatchTranasctions } from './_utils';
|
||||||
sumMatchTranasctions,
|
|
||||||
sumUncategorizedTransactions,
|
|
||||||
validateUncategorizedTransactionsExcluded,
|
|
||||||
validateUncategorizedTransactionsNotMatched,
|
|
||||||
} from './_utils';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -44,25 +39,27 @@ export class MatchBankTransactions {
|
|||||||
*/
|
*/
|
||||||
async validate(
|
async validate(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number | Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>
|
matchTransactionsDTO: IMatchTransactionsDTO
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
const { matchedTransactions } = matchTransactionsDTO;
|
||||||
|
|
||||||
// Validates the uncategorized transaction existance.
|
// Validates the uncategorized transaction existance.
|
||||||
const uncategorizedTransactions =
|
const uncategorizedTransaction =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query()
|
||||||
.whereIn('id', uncategorizedTransactionIds)
|
.findById(uncategorizedTransactionId)
|
||||||
.withGraphFetched('matchedBankTransactions')
|
.withGraphFetched('matchedBankTransactions')
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validates the uncategorized transaction is not already matched.
|
// Validates the uncategorized transaction is not already matched.
|
||||||
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions);
|
if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
|
||||||
|
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
|
||||||
|
}
|
||||||
// Validate the uncategorized transaction is not excluded.
|
// Validate the uncategorized transaction is not excluded.
|
||||||
validateUncategorizedTransactionsExcluded(uncategorizedTransactions);
|
if (uncategorizedTransaction.excluded) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
|
||||||
|
}
|
||||||
// Validates the given matched transaction.
|
// Validates the given matched transaction.
|
||||||
const validateMatchedTransaction = async (matchedTransaction) => {
|
const validateMatchedTransaction = async (matchedTransaction) => {
|
||||||
const getMatchedTransactionsService =
|
const getMatchedTransactionsService =
|
||||||
@@ -97,12 +94,9 @@ export class MatchBankTransactions {
|
|||||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||||
validatationResult.results
|
validatationResult.results
|
||||||
);
|
);
|
||||||
const totalUncategorizedTransactions = sumUncategorizedTransactions(
|
|
||||||
uncategorizedTransactions
|
|
||||||
);
|
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
|
||||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,23 +109,23 @@ export class MatchBankTransactions {
|
|||||||
*/
|
*/
|
||||||
public async matchTransaction(
|
public async matchTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number | Array<number>,
|
uncategorizedTransactionId: number,
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>
|
matchTransactionsDTO: IMatchTransactionsDTO
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
const { matchedTransactions } = matchTransactionsDTO;
|
||||||
|
|
||||||
// Validates the given matching transactions DTO.
|
// Validates the given matching transactions DTO.
|
||||||
await this.validate(
|
await this.validate(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionIds,
|
uncategorizedTransactionId,
|
||||||
matchedTransactions
|
matchTransactionsDTO
|
||||||
);
|
);
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers the event `onBankTransactionMatching`.
|
// Triggers the event `onBankTransactionMatching`.
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionIds,
|
uncategorizedTransactionId,
|
||||||
matchedTransactions,
|
matchTransactionsDTO,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionMatchingEventPayload);
|
} as IBankTransactionMatchingEventPayload);
|
||||||
|
|
||||||
@@ -145,16 +139,17 @@ export class MatchBankTransactions {
|
|||||||
);
|
);
|
||||||
await getMatchedTransactionsService.createMatchedTransaction(
|
await getMatchedTransactionsService.createMatchedTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionIds,
|
uncategorizedTransactionId,
|
||||||
matchedTransaction,
|
matchedTransaction,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Triggers the event `onBankTransactionMatched`.
|
// Triggers the event `onBankTransactionMatched`.
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionIds,
|
uncategorizedTransactionId,
|
||||||
matchedTransactions,
|
matchTransactionsDTO,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionMatchedEventPayload);
|
} as IBankTransactionMatchedEventPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
import { MatchedTransactionPOJO } from './types';
|
||||||
import { isEmpty, sumBy } from 'lodash';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
|
|
||||||
export const sortClosestMatchTransactions = (
|
export const sortClosestMatchTransactions = (
|
||||||
amount: number,
|
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
||||||
date: Date,
|
|
||||||
matches: MatchedTransactionPOJO[]
|
matches: MatchedTransactionPOJO[]
|
||||||
) => {
|
) => {
|
||||||
return R.sortWith([
|
return R.sortWith([
|
||||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(match.amount - amount)
|
Math.abs(match.amount - uncategorizedTransaction.amount)
|
||||||
),
|
),
|
||||||
// Sort by date difference (closest to uncategorized transaction date first)
|
// Sort by date difference (closest to uncategorized transaction date first)
|
||||||
R.ascend((match: MatchedTransactionPOJO) =>
|
R.ascend((match: MatchedTransactionPOJO) =>
|
||||||
Math.abs(moment(match.date).diff(moment(date), 'days'))
|
Math.abs(
|
||||||
|
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
|
||||||
|
)
|
||||||
),
|
),
|
||||||
])(matches);
|
])(matches);
|
||||||
};
|
};
|
||||||
@@ -30,36 +29,3 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sumUncategorizedTransactions = (
|
|
||||||
uncategorizedTransactions: Array<any>
|
|
||||||
) => {
|
|
||||||
return sumBy(uncategorizedTransactions, 'amount');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateUncategorizedTransactionsNotMatched = (
|
|
||||||
uncategorizedTransactions: any
|
|
||||||
) => {
|
|
||||||
const matchedTransactions = uncategorizedTransactions.filter(
|
|
||||||
(trans) => !isEmpty(trans.matchedBankTransactions)
|
|
||||||
);
|
|
||||||
//
|
|
||||||
if (matchedTransactions.length > 0) {
|
|
||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
|
|
||||||
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateUncategorizedTransactionsExcluded = (
|
|
||||||
uncategorizedTransactions: any
|
|
||||||
) => {
|
|
||||||
const excludedTransactions = uncategorizedTransactions.filter(
|
|
||||||
(trans) => trans.excluded
|
|
||||||
);
|
|
||||||
if (excludedTransactions.length > 0) {
|
|
||||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
|
|
||||||
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
IBankTransactionUnmatchedEventPayload,
|
IBankTransactionUnmatchedEventPayload,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import PromisePool from '@supercharge/promise-pool';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class DecrementUncategorizedTransactionOnMatching {
|
export class DecrementUncategorizedTransactionOnMatching {
|
||||||
@@ -31,24 +30,18 @@ export class DecrementUncategorizedTransactionOnMatching {
|
|||||||
*/
|
*/
|
||||||
public async decrementUnCategorizedTransactionsOnMatching({
|
public async decrementUnCategorizedTransactionsOnMatching({
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionIds,
|
uncategorizedTransactionId,
|
||||||
trx,
|
trx,
|
||||||
}: IBankTransactionMatchedEventPayload) {
|
}: IBankTransactionMatchedEventPayload) {
|
||||||
const { UncategorizedCashflowTransaction, Account } =
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
this.tenancy.models(tenantId);
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const uncategorizedTransactions =
|
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||||
await UncategorizedCashflowTransaction.query().whereIn(
|
uncategorizedTransactionId
|
||||||
'id',
|
);
|
||||||
uncategorizedTransactionIds
|
await Account.query(trx)
|
||||||
);
|
.findById(transaction.accountId)
|
||||||
await PromisePool.withConcurrency(1)
|
.decrement('uncategorizedTransactions', 1);
|
||||||
.for(uncategorizedTransactions)
|
|
||||||
.process(async (transaction) => {
|
|
||||||
await Account.query(trx)
|
|
||||||
.findById(transaction.accountId)
|
|
||||||
.decrement('uncategorizedTransactions', 1);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { Knex } from 'knex';
|
|||||||
|
|
||||||
export interface IBankTransactionMatchingEventPayload {
|
export interface IBankTransactionMatchingEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionIds: Array<number>;
|
uncategorizedTransactionId: number;
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
matchTransactionsDTO: IMatchTransactionsDTO;
|
||||||
trx?: Knex.Transaction;
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionMatchedEventPayload {
|
export interface IBankTransactionMatchedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
uncategorizedTransactionIds: Array<number>;
|
uncategorizedTransactionId: number;
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
matchTransactionsDTO: IMatchTransactionsDTO;
|
||||||
trx?: Knex.Transaction;
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ export interface IMatchTransactionDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionsDTO {
|
export interface IMatchTransactionsDTO {
|
||||||
uncategorizedTransactionIds: Array<number>;
|
|
||||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +57,6 @@ export interface MatchedTransactionPOJO {
|
|||||||
export type MatchedTransactionsPOJO = {
|
export type MatchedTransactionsPOJO = {
|
||||||
perfectMatches: Array<MatchedTransactionPOJO>;
|
perfectMatches: Array<MatchedTransactionPOJO>;
|
||||||
possibleMatches: Array<MatchedTransactionPOJO>;
|
possibleMatches: Array<MatchedTransactionPOJO>;
|
||||||
totalPending: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -25,7 +20,6 @@ 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';
|
||||||
import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction';
|
|
||||||
|
|
||||||
const CONCURRENCY_ASYNC = 10;
|
const CONCURRENCY_ASYNC = 10;
|
||||||
|
|
||||||
@@ -41,7 +35,7 @@ export class PlaidSyncDb {
|
|||||||
private cashflowApp: CashflowApplication;
|
private cashflowApp: CashflowApplication;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private removePendingTransaction: RemovePendingUncategorizedTransaction;
|
private deleteCashflowTransactionService: DeleteCashflowTransaction;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
@@ -59,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
|
||||||
@@ -84,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(
|
||||||
@@ -186,22 +177,21 @@ export class PlaidSyncDb {
|
|||||||
plaidTransactionsIds: string[],
|
plaidTransactionsIds: string[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { CashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const uncategorizedTransactions =
|
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
|
||||||
await UncategorizedCashflowTransaction.query(trx).whereIn(
|
'plaidTransactionId',
|
||||||
'plaidTransactionId',
|
plaidTransactionsIds
|
||||||
plaidTransactionsIds
|
);
|
||||||
);
|
const cashflowTransactionsIds = cashflowTransactions.map(
|
||||||
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
|
|
||||||
(trans) => trans.id
|
(trans) => trans.id
|
||||||
);
|
);
|
||||||
await bluebird.map(
|
await bluebird.map(
|
||||||
uncategorizedTransactionsIds,
|
cashflowTransactionsIds,
|
||||||
(uncategorizedTransactionId: number) =>
|
(transactionId: number) =>
|
||||||
this.removePendingTransaction.removePendingTransaction(
|
this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
uncategorizedTransactionId,
|
transactionId,
|
||||||
trx
|
trx
|
||||||
),
|
),
|
||||||
{ concurrency: CONCURRENCY_ASYNC }
|
{ concurrency: CONCURRENCY_ASYNC }
|
||||||
|
|||||||
@@ -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,7 @@ 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 removed transactions.
|
|
||||||
await this.plaidSync.syncRemoveTransactions(
|
|
||||||
tenantId,
|
|
||||||
removed?.map((r) => r.transaction_id),
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
// Sync bank account transactions.
|
// Sync bank account transactions.
|
||||||
await this.plaidSync.syncAccountsTransactions(
|
await this.plaidSync.syncAccountsTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -153,7 +141,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,22 +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()
|
|
||||||
.findOne({ 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,
|
|
||||||
TransactionBase as PlaidTransactionBase,
|
|
||||||
} from 'plaid';
|
|
||||||
import {
|
import {
|
||||||
CreateUncategorizedTransactionDTO,
|
CreateUncategorizedTransactionDTO,
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
|
PlaidAccount,
|
||||||
|
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,7 +37,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||||
(
|
(
|
||||||
cashflowAccountId: number,
|
cashflowAccountId: number,
|
||||||
plaidTranasction: PlaidTransactionBase
|
plaidTranasction: PlaidTransaction
|
||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
date: plaidTranasction.date,
|
date: plaidTranasction.date,
|
||||||
@@ -64,8 +53,6 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
|
|||||||
accountId: cashflowAccountId,
|
accountId: cashflowAccountId,
|
||||||
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
||||||
plaidTransactionId: plaidTranasction.transaction_id,
|
plaidTransactionId: plaidTranasction.transaction_id,
|
||||||
pending: plaidTranasction.pending,
|
|
||||||
pendingPlaidTransactionId: plaidTranasction.pending_transaction_id,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user