Compare commits

..

10 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
d0e227ff28 feat: disable auto applying credit payments 2024-07-25 11:52:40 +02:00
Ahmed Bouhuolia
b590d2cb03 fix: excess dialog 2024-07-25 11:01:26 +02:00
Ahmed Bouhuolia
daf1cd38c0 feat: advanced payments 2024-07-25 01:40:48 +02:00
Ahmed Bouhuolia
3e2997d745 feat: logic of excess amount confirmation 2024-07-24 22:33:26 +02:00
Ahmed Bouhuolia
f3af3843dd feat: wip prepard expenses from vendors 2024-07-24 18:57:51 +02:00
Ahmed Bouhuolia
b68d180785 feat: prepard expenses of payment made transactions 2024-07-24 02:18:32 +02:00
Ahmed Bouhuolia
341d47cc7b feat: excess payment alert 2024-07-23 18:54:08 +02:00
Ahmed Bouhuolia
5c3a371e8a feat: wip advanced payment 2024-07-23 15:02:39 +02:00
Ahmed Bouhuolia
1141991e44 feat: advanced payments 2024-07-23 13:52:25 +02:00
Ahmed Bouhuolia
8cd3a6c48d feat: advanced payments 2024-07-22 20:40:15 +02:00
325 changed files with 3129 additions and 7249 deletions

View File

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

View File

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

View File

@@ -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 })

View File

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

View File

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

View File

@@ -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);
}
}
} }

View File

@@ -1,8 +1,12 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { body, param } from 'express-validator';
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication'; import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
import { body, param } from 'express-validator';
import {
GetMatchedTransactionsFilter,
IMatchTransactionsDTO,
} from '@/services/Banking/Matching/types';
@Service() @Service()
export class BankTransactionsMatchingController extends BaseController { export class BankTransactionsMatchingController extends BaseController {
@@ -16,17 +20,9 @@ export class BankTransactionsMatchingController extends BaseController {
const router = Router(); const router = Router();
router.post( router.post(
'/unmatch/:transactionId', '/:transactionId',
[param('transactionId').exists()],
this.validationResult,
this.unmatchMatchedBankTransaction.bind(this)
);
router.post(
'/match',
[ [
body('uncategorizedTransactions').exists().isArray({ min: 1 }), param('transactionId').exists(),
body('uncategorizedTransactions.*').isNumeric().toInt(),
body('matchedTransactions').isArray({ min: 1 }), body('matchedTransactions').isArray({ min: 1 }),
body('matchedTransactions.*.reference_type').exists(), body('matchedTransactions.*.reference_type').exists(),
body('matchedTransactions.*.reference_id').isNumeric().toInt(), body('matchedTransactions.*.reference_id').isNumeric().toInt(),
@@ -34,6 +30,12 @@ export class BankTransactionsMatchingController extends BaseController {
this.validationResult, this.validationResult,
this.matchBankTransaction.bind(this) this.matchBankTransaction.bind(this)
); );
router.post(
'/unmatch/:transactionId',
[param('transactionId').exists()],
this.validationResult,
this.unmatchMatchedBankTransaction.bind(this)
);
return router; return router;
} }
@@ -48,21 +50,21 @@ export class BankTransactionsMatchingController extends BaseController {
req: Request<{ transactionId: number }>, req: Request<{ transactionId: number }>,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<Response | null> { ) {
const { tenantId } = req; const { tenantId } = req;
const bodyData = this.matchedBodyData(req); const { transactionId } = req.params;
const matchTransactionDTO = this.matchedBodyData(
const uncategorizedTransactions = bodyData?.uncategorizedTransactions; req
const matchedTransactions = bodyData?.matchedTransactions; ) as IMatchTransactionsDTO;
try { try {
await this.bankTransactionsMatchingApp.matchTransaction( await this.bankTransactionsMatchingApp.matchTransaction(
tenantId, tenantId,
uncategorizedTransactions, transactionId,
matchedTransactions matchTransactionDTO
); );
return res.status(200).send({ return res.status(200).send({
ids: uncategorizedTransactions, id: transactionId,
message: 'The bank transaction has been matched.', message: 'The bank transaction has been matched.',
}); });
} catch (error) { } catch (error) {

View File

@@ -6,7 +6,6 @@ import { BankingRulesController } from './BankingRulesController';
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController'; import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
import { RecognizedTransactionsController } from './RecognizedTransactionsController'; import { RecognizedTransactionsController } from './RecognizedTransactionsController';
import { BankAccountsController } from './BankAccountsController'; import { BankAccountsController } from './BankAccountsController';
import { BankingUncategorizedController } from './BankingUncategorizedController';
@Service() @Service()
export class BankingController extends BaseController { export class BankingController extends BaseController {
@@ -30,10 +29,6 @@ export class BankingController extends BaseController {
'/bank_accounts', '/bank_accounts',
Container.get(BankAccountsController).router() Container.get(BankAccountsController).router()
); );
router.use(
'/categorize',
Container.get(BankingUncategorizedController).router()
);
return router; return router;
} }
} }

View File

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

View File

@@ -1,57 +0,0 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import { query } from 'express-validator';
import BaseController from '../BaseController';
import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction';
@Service()
export class BankingUncategorizedController extends BaseController {
@Inject()
private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/autofill',
[
query('uncategorizedTransactionIds').isArray({ min: 1 }),
query('uncategorizedTransactionIds.*').isNumeric().toInt(),
],
this.validationResult,
this.getAutofillCategorizeTransaction.bind(this)
);
return router;
}
/**
* Retrieves the autofill values of the categorize form of the given
* uncategorized transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
public async getAutofillCategorizeTransaction(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds;
try {
const data =
await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { param, query } from 'express-validator'; import { param } from 'express-validator';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
@@ -24,12 +24,7 @@ export default class GetCashflowAccounts extends BaseController {
const router = Router(); const router = Router();
router.get( router.get(
'/transactions/matches', '/transactions/:transactionId/matches',
[
query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }),
query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(),
],
this.validationResult,
this.getMatchedTransactions.bind(this) this.getMatchedTransactions.bind(this)
); );
router.get( router.get(
@@ -49,7 +44,7 @@ export default class GetCashflowAccounts extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private getCashflowTransaction = async ( private getCashflowTransaction = async (
req: Request<{ transactionId: number }>, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
@@ -76,24 +71,19 @@ export default class GetCashflowAccounts extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getMatchedTransactions( private async getMatchedTransactions(
req: Request< req: Request<{ transactionId: number }>,
{ transactionId: number },
null,
null,
{ uncategorizeTransactionsIds: Array<number> }
>,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
const uncategorizeTransactionsIds = req.query.uncategorizeTransactionsIds; const { transactionId } = req.params;
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter; const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
try { try {
const data = const data =
await this.bankTransactionsMatchingApp.getMatchedTransactions( await this.bankTransactionsMatchingApp.getMatchedTransactions(
tenantId, tenantId,
uncategorizeTransactionsIds, transactionId,
filter filter
); );
return res.status(200).send(data); return res.status(200).send(data);

View File

@@ -1,15 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ValidationChain, 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
) => { ) => {

View File

@@ -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(),
]; ];

View File

@@ -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(),

View File

@@ -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 }),
]; ];
} }

View File

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

View File

@@ -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')

View File

@@ -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(),
]; ];
} }
} }

View File

@@ -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(),

View File

@@ -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(
[ [

View File

@@ -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(),
]; ];
} }

View File

@@ -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(),

View File

@@ -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(),
]; ];
} }

View File

@@ -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),

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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(),
] ]
} }

View File

@@ -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(),
]; ];
} }

View File

@@ -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(),
]; ];
} }

View File

@@ -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(),
]; ];
} }

View File

@@ -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()

View File

@@ -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);

View File

@@ -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()

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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 })

View File

@@ -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(),
]; ];

View File

@@ -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(),
];
} }
/** /**

View File

@@ -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);
}
}
} }

View File

@@ -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()];
} }
/** /**

View File

@@ -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,
}, },
}; };

View File

@@ -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');
});
};

View File

@@ -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');
});
};

View File

@@ -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');
});
};

View File

@@ -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');
};

View File

@@ -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();
});
};

View File

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

View File

@@ -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');
});
};

View File

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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -236,7 +236,6 @@ export interface ICashflowTransactionSchema {
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {} export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
export interface ICategorizeCashflowTransactioDTO { export interface ICategorizeCashflowTransactioDTO {
date: Date;
creditAccountId: number; creditAccountId: number;
referenceNo: string; referenceNo: string;
transactionNumber: string; transactionNumber: string;
@@ -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;
}

View File

@@ -130,23 +130,20 @@ export interface ICommandCashflowDeletedPayload {
export interface ICashflowTransactionCategorizedPayload { export interface ICashflowTransactionCategorizedPayload {
tenantId: number; tenantId: number;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; uncategorizedTransaction: any;
cashflowTransaction: ICashflowTransaction; cashflowTransaction: ICashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
categorizeDTO: any; categorizeDTO: any;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICashflowTransactionUncategorizingPayload { export interface ICashflowTransactionUncategorizingPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICashflowTransactionUncategorizedPayload { export interface ICashflowTransactionUncategorizedPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransaction: IUncategorizedCashflowTransaction;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction; trx: Knex.Transaction;
} }

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
};
} }
} }

View File

@@ -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,
}); });

View File

@@ -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
]; ];
}; };

View File

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

View File

@@ -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',
},
},
}; };
} }

View File

@@ -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.
*/ */

View File

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

View File

@@ -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',
},
},
};

View File

@@ -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.
*/ */

View File

@@ -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);
},
}; };
} }

View File

@@ -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[]}

View File

@@ -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 {
); );
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
}
}

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { initialize } from 'objection'; import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
@Service() @Service()
export class GetBankAccountSummary { export class GetBankAccountSummary {
@@ -32,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,
}; };
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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,
});
}
}
}

View File

@@ -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',
};

View File

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

View File

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

View File

@@ -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
);
}
} }

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import moment from 'moment'; import moment from 'moment';
import { first, sumBy } from 'lodash';
import { PromisePool } from '@supercharge/promise-pool'; import { PromisePool } from '@supercharge/promise-pool';
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types'; import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses'; import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
@@ -48,24 +47,21 @@ export class GetMatchedTransactions {
/** /**
* Retrieves the matched transactions. * Retrieves the matched transactions.
* @param {number} tenantId - * @param {number} tenantId -
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
* @param {GetMatchedTransactionsFilter} filter - * @param {GetMatchedTransactionsFilter} filter -
* @returns {Promise<MatchedTransactionsPOJO>} * @returns {Promise<MatchedTransactionsPOJO>}
*/ */
public async getMatchedTransactions( public async getMatchedTransactions(
tenantId: number, tenantId: number,
uncategorizedTransactionIds: Array<number>, uncategorizedTransactionId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
): Promise<MatchedTransactionsPOJO> { ): Promise<MatchedTransactionsPOJO> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactions = const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
const totalPending = sumBy(uncategorizedTransactions, 'amount');
const filtered = filter.transactionType const filtered = filter.transactionType
? this.registered.filter((item) => item.type === filter.transactionType) ? this.registered.filter((item) => item.type === filter.transactionType)
: this.registered; : this.registered;
@@ -75,14 +71,14 @@ export class GetMatchedTransactions {
.process(async ({ type, service }) => { .process(async ({ type, service }) => {
return service.getMatchedTransactions(tenantId, filter); return service.getMatchedTransactions(tenantId, filter);
}); });
const { perfectMatches, possibleMatches } = this.groupMatchedResults( const { perfectMatches, possibleMatches } = this.groupMatchedResults(
uncategorizedTransactions, uncategorizedTransaction,
matchedTransactions matchedTransactions
); );
return { return {
perfectMatches, perfectMatches,
possibleMatches, possibleMatches,
totalPending,
}; };
} }
@@ -94,20 +90,20 @@ export class GetMatchedTransactions {
* @returns {MatchedTransactionsPOJO} * @returns {MatchedTransactionsPOJO}
*/ */
private groupMatchedResults( private groupMatchedResults(
uncategorizedTransactions: Array<any>, uncategorizedTransaction,
matchedTransactions matchedTransactions
): MatchedTransactionsPOJO { ): MatchedTransactionsPOJO {
const results = R.compose(R.flatten)(matchedTransactions?.results); const results = R.compose(R.flatten)(matchedTransactions?.results);
const firstUncategorized = first(uncategorizedTransactions);
const amount = sumBy(uncategorizedTransactions, 'amount');
const date = firstUncategorized.date;
// Sort the results based on amount, date, and transaction type // Sort the results based on amount, date, and transaction type
const closestResullts = sortClosestMatchTransactions(amount, date, results); const closestResullts = sortClosestMatchTransactions(
uncategorizedTransaction,
results
);
const perfectMatches = R.filter( const perfectMatches = R.filter(
(match) => (match) =>
match.amount === amount && moment(match.date).isSame(date, 'day'), match.amount === uncategorizedTransaction.amount &&
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
closestResullts closestResullts
); );
const possibleMatches = R.difference(closestResullts, perfectMatches); const possibleMatches = R.difference(closestResullts, perfectMatches);

View File

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

View File

@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
import { GetMatchedTransactions } from './GetMatchedTransactions'; import { GetMatchedTransactions } from './GetMatchedTransactions';
import { MatchBankTransactions } from './MatchTransactions'; import { MatchBankTransactions } from './MatchTransactions';
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction'; import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types'; import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
@Service() @Service()
export class MatchBankTransactionsApplication { export class MatchBankTransactionsApplication {
@@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication {
*/ */
public getMatchedTransactions( public getMatchedTransactions(
tenantId: number, tenantId: number,
uncategorizedTransactionsIds: Array<number>, uncategorizedTransactionId: number,
filter: GetMatchedTransactionsFilter filter: GetMatchedTransactionsFilter
) { ) {
return this.getMatchedTransactionsService.getMatchedTransactions( return this.getMatchedTransactionsService.getMatchedTransactions(
tenantId, tenantId,
uncategorizedTransactionsIds, uncategorizedTransactionId,
filter filter
); );
} }
@@ -42,13 +42,13 @@ export class MatchBankTransactionsApplication {
*/ */
public matchTransaction( public matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> { ): Promise<void> {
return this.matchTransactionService.matchTransaction( return this.matchTransactionService.matchTransaction(
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,
matchedTransactions matchTransactionsDTO
); );
} }

View File

@@ -1,4 +1,4 @@
import { castArray } from 'lodash'; import { isEmpty } from 'lodash';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { PromisePool } from '@supercharge/promise-pool'; import { PromisePool } from '@supercharge/promise-pool';
@@ -10,16 +10,11 @@ import {
ERRORS, ERRORS,
IBankTransactionMatchedEventPayload, IBankTransactionMatchedEventPayload,
IBankTransactionMatchingEventPayload, IBankTransactionMatchingEventPayload,
IMatchTransactionDTO, IMatchTransactionsDTO,
} from './types'; } from './types';
import { MatchTransactionsTypes } from './MatchTransactionsTypes'; import { MatchTransactionsTypes } from './MatchTransactionsTypes';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import { sumMatchTranasctions } from './_utils';
sumMatchTranasctions,
sumUncategorizedTransactions,
validateUncategorizedTransactionsExcluded,
validateUncategorizedTransactionsNotMatched,
} from './_utils';
@Service() @Service()
export class MatchBankTransactions { export class MatchBankTransactions {
@@ -44,25 +39,27 @@ export class MatchBankTransactions {
*/ */
async validate( async validate(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
) { ) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); const { matchedTransactions } = matchTransactionsDTO;
// Validates the uncategorized transaction existance. // Validates the uncategorized transaction existance.
const uncategorizedTransactions = const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds) .findById(uncategorizedTransactionId)
.withGraphFetched('matchedBankTransactions') .withGraphFetched('matchedBankTransactions')
.throwIfNotFound(); .throwIfNotFound();
// Validates the uncategorized transaction is not already matched. // Validates the uncategorized transaction is not already matched.
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions); if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
}
// Validate the uncategorized transaction is not excluded. // Validate the uncategorized transaction is not excluded.
validateUncategorizedTransactionsExcluded(uncategorizedTransactions); if (uncategorizedTransaction.excluded) {
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
}
// Validates the given matched transaction. // Validates the given matched transaction.
const validateMatchedTransaction = async (matchedTransaction) => { const validateMatchedTransaction = async (matchedTransaction) => {
const getMatchedTransactionsService = const getMatchedTransactionsService =
@@ -97,12 +94,9 @@ export class MatchBankTransactions {
const totalMatchedTranasctions = sumMatchTranasctions( const totalMatchedTranasctions = sumMatchTranasctions(
validatationResult.results validatationResult.results
); );
const totalUncategorizedTransactions = sumUncategorizedTransactions(
uncategorizedTransactions
);
// Validates the total given matching transcations whether is not equal // Validates the total given matching transcations whether is not equal
// uncategorized transaction amount. // uncategorized transaction amount.
if (totalUncategorizedTransactions !== totalMatchedTranasctions) { if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
} }
} }
@@ -115,23 +109,23 @@ export class MatchBankTransactions {
*/ */
public async matchTransaction( public async matchTransaction(
tenantId: number, tenantId: number,
uncategorizedTransactionId: number | Array<number>, uncategorizedTransactionId: number,
matchedTransactions: Array<IMatchTransactionDTO> matchTransactionsDTO: IMatchTransactionsDTO
): Promise<void> { ): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId); const { matchedTransactions } = matchTransactionsDTO;
// Validates the given matching transactions DTO. // Validates the given matching transactions DTO.
await this.validate( await this.validate(
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions matchTransactionsDTO
); );
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers the event `onBankTransactionMatching`. // Triggers the event `onBankTransactionMatching`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, { await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions, matchTransactionsDTO,
trx, trx,
} as IBankTransactionMatchingEventPayload); } as IBankTransactionMatchingEventPayload);
@@ -145,16 +139,17 @@ export class MatchBankTransactions {
); );
await getMatchedTransactionsService.createMatchedTransaction( await getMatchedTransactionsService.createMatchedTransaction(
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransaction, matchedTransaction,
trx trx
); );
}); });
// Triggers the event `onBankTransactionMatched`. // Triggers the event `onBankTransactionMatched`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, { await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
matchedTransactions, matchTransactionsDTO,
trx, trx,
} as IBankTransactionMatchedEventPayload); } as IBankTransactionMatchedEventPayload);
}); });

View File

@@ -1,23 +1,22 @@
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
import { ERRORS, MatchedTransactionPOJO } from './types'; import { MatchedTransactionPOJO } from './types';
import { isEmpty, sumBy } from 'lodash';
import { ServiceError } from '@/exceptions';
export const sortClosestMatchTransactions = ( export const sortClosestMatchTransactions = (
amount: number, uncategorizedTransaction: UncategorizedCashflowTransaction,
date: Date,
matches: MatchedTransactionPOJO[] matches: MatchedTransactionPOJO[]
) => { ) => {
return R.sortWith([ return R.sortWith([
// Sort by amount difference (closest to uncategorized transaction amount first) // Sort by amount difference (closest to uncategorized transaction amount first)
R.ascend((match: MatchedTransactionPOJO) => R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(match.amount - amount) Math.abs(match.amount - uncategorizedTransaction.amount)
), ),
// Sort by date difference (closest to uncategorized transaction date first) // Sort by date difference (closest to uncategorized transaction date first)
R.ascend((match: MatchedTransactionPOJO) => R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(moment(match.date).diff(moment(date), 'days')) Math.abs(
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
)
), ),
])(matches); ])(matches);
}; };
@@ -30,36 +29,3 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
0 0
); );
}; };
export const sumUncategorizedTransactions = (
uncategorizedTransactions: Array<any>
) => {
return sumBy(uncategorizedTransactions, 'amount');
};
export const validateUncategorizedTransactionsNotMatched = (
uncategorizedTransactions: any
) => {
const matchedTransactions = uncategorizedTransactions.filter(
(trans) => !isEmpty(trans.matchedBankTransactions)
);
//
if (matchedTransactions.length > 0) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
});
}
};
export const validateUncategorizedTransactionsExcluded = (
uncategorizedTransactions: any
) => {
const excludedTransactions = uncategorizedTransactions.filter(
(trans) => trans.excluded
);
if (excludedTransactions.length > 0) {
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
});
}
};

View File

@@ -5,7 +5,6 @@ import {
IBankTransactionUnmatchedEventPayload, IBankTransactionUnmatchedEventPayload,
} from '../types'; } from '../types';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import PromisePool from '@supercharge/promise-pool';
@Service() @Service()
export class DecrementUncategorizedTransactionOnMatching { export class DecrementUncategorizedTransactionOnMatching {
@@ -31,24 +30,18 @@ export class DecrementUncategorizedTransactionOnMatching {
*/ */
public async decrementUnCategorizedTransactionsOnMatching({ public async decrementUnCategorizedTransactionsOnMatching({
tenantId, tenantId,
uncategorizedTransactionIds, uncategorizedTransactionId,
trx, trx,
}: IBankTransactionMatchedEventPayload) { }: IBankTransactionMatchedEventPayload) {
const { UncategorizedCashflowTransaction, Account } = const { UncategorizedCashflowTransaction, Account } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
const uncategorizedTransactions = const transaction = await UncategorizedCashflowTransaction.query().findById(
await UncategorizedCashflowTransaction.query().whereIn( uncategorizedTransactionId
'id', );
uncategorizedTransactionIds await Account.query(trx)
); .findById(transaction.accountId)
await PromisePool.withConcurrency(1) .decrement('uncategorizedTransactions', 1);
.for(uncategorizedTransactions)
.process(async (transaction) => {
await Account.query(trx)
.findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1);
});
} }
/** /**

View File

@@ -2,15 +2,15 @@ import { Knex } from 'knex';
export interface IBankTransactionMatchingEventPayload { export interface IBankTransactionMatchingEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionIds: Array<number>; uncategorizedTransactionId: number;
matchedTransactions: Array<IMatchTransactionDTO>; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
export interface IBankTransactionMatchedEventPayload { export interface IBankTransactionMatchedEventPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionIds: Array<number>; uncategorizedTransactionId: number;
matchedTransactions: Array<IMatchTransactionDTO>; matchTransactionsDTO: IMatchTransactionsDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
@@ -32,7 +32,6 @@ export interface IMatchTransactionDTO {
} }
export interface IMatchTransactionsDTO { export interface IMatchTransactionsDTO {
uncategorizedTransactionIds: Array<number>;
matchedTransactions: Array<IMatchTransactionDTO>; matchedTransactions: Array<IMatchTransactionDTO>;
} }
@@ -58,7 +57,6 @@ export interface MatchedTransactionPOJO {
export type MatchedTransactionsPOJO = { export type MatchedTransactionsPOJO = {
perfectMatches: Array<MatchedTransactionPOJO>; perfectMatches: Array<MatchedTransactionPOJO>;
possibleMatches: Array<MatchedTransactionPOJO>; possibleMatches: Array<MatchedTransactionPOJO>;
totalPending: number;
}; };
export const ERRORS = { export const ERRORS = {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,11 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions'; import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class PlaidWebooks { export class PlaidWebooks {
@Inject() @Inject()
private updateTransactionsService: PlaidUpdateTransactions; private updateTransactionsService: PlaidUpdateTransactions;
@Inject()
private tenancy: HasTenancyService;
/** /**
* Listens to Plaid webhooks * Listens to Plaid webhooks
* @param {number} tenantId - Tenant Id. * @param {number} tenantId - Tenant Id.
@@ -65,7 +61,7 @@ export class PlaidWebooks {
plaidItemId: string plaidItemId: string
): void { ): void {
console.log( console.log(
`PLAID WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}` `WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`
); );
} }
@@ -82,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(

View File

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

View File

@@ -1,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