mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
Compare commits
138 Commits
all-contri
...
v0.19.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1989887b25 | ||
|
|
e4fb126d39 | ||
|
|
5fcb2d9cc9 | ||
|
|
06ea631732 | ||
|
|
2f21107a43 | ||
|
|
fb8118bea8 | ||
|
|
4ba1c0aa22 | ||
|
|
169f115fa0 | ||
|
|
61ab2b78d9 | ||
|
|
93732430fc | ||
|
|
0215206220 | ||
|
|
3c8956fedf | ||
|
|
4477ada1ad | ||
|
|
fde9ccc5ca | ||
|
|
bbbd96f159 | ||
|
|
9f4de8115f | ||
|
|
d9f241a2f8 | ||
|
|
ee96dc68cc | ||
|
|
b12f090d13 | ||
|
|
f6ce761a27 | ||
|
|
8c89e04f54 | ||
|
|
f97b127a69 | ||
|
|
5c1fa8f5cd | ||
|
|
64c4d7b5a4 | ||
|
|
ea2fad648b | ||
|
|
9b8b51cb91 | ||
|
|
09b7e74d65 | ||
|
|
7137e06d99 | ||
|
|
fc29b765f7 | ||
|
|
2946475f89 | ||
|
|
d2193fdac0 | ||
|
|
ec2b7e332e | ||
|
|
a3704df6dd | ||
|
|
c3c784e52c | ||
|
|
bbcf695a6c | ||
|
|
038d4dd5a7 | ||
|
|
961e4b99e8 | ||
|
|
9991eebaaf | ||
|
|
cd90fede54 | ||
|
|
a2d28648bd | ||
|
|
3097d05eda | ||
|
|
ff94d8d9b2 | ||
|
|
79cc09fad9 | ||
|
|
c1b29c3f23 | ||
|
|
cf4bb3007e | ||
|
|
193a86cf30 | ||
|
|
7a81f14eb2 | ||
|
|
14d1f0bd1d | ||
|
|
82f8648c59 | ||
|
|
c928940d32 | ||
|
|
0a78d56015 | ||
|
|
1a5716873e | ||
|
|
01b7c86ab9 | ||
|
|
0ca209b195 | ||
|
|
be6f6e3c73 | ||
|
|
cb016be78c | ||
|
|
fc085f2328 | ||
|
|
9a34f3e283 | ||
|
|
7054e862d5 | ||
|
|
faa81abee4 | ||
|
|
6d01f2a323 | ||
|
|
72678bb936 | ||
|
|
9ae5644af9 | ||
|
|
e8830c5911 | ||
|
|
7699889bd6 | ||
|
|
35a061d188 | ||
|
|
c7c021c969 | ||
|
|
be8352654e | ||
|
|
fb58ab8cc1 | ||
|
|
8da89ebe8b | ||
|
|
d43d46ebec | ||
|
|
ac3a514795 | ||
|
|
f67c63a4fa | ||
|
|
0025dcf8d4 | ||
|
|
81995dc94f | ||
|
|
3fcc70c1d8 | ||
|
|
a986c7a250 | ||
|
|
37e25a8061 | ||
|
|
cfba628465 | ||
|
|
3d200f4d7d | ||
|
|
f21b01b1d6 | ||
|
|
3cbcfac333 | ||
|
|
cc21e1856f | ||
|
|
efd0e1e225 | ||
|
|
521b083ed7 | ||
|
|
a09fe26df7 | ||
|
|
c7a85c4cf8 | ||
|
|
f6350d3d61 | ||
|
|
64c0732e5f | ||
|
|
8e99a31455 | ||
|
|
6d0d0689e1 | ||
|
|
9836129e49 | ||
|
|
86631ea8c3 | ||
|
|
475ccd4903 | ||
|
|
8608144ec1 | ||
|
|
f9cf6d325a | ||
|
|
fc0240c692 | ||
|
|
b84675325f | ||
|
|
647bed5c67 | ||
|
|
00f5bb1d73 | ||
|
|
208800b411 | ||
|
|
5e12a4cea4 | ||
|
|
fdf3e34f1c | ||
|
|
d74337fb94 | ||
|
|
8cab012324 | ||
|
|
940b4f9175 | ||
|
|
5d0dd1fe3f | ||
|
|
ded4e2bb59 | ||
|
|
219e6fb466 | ||
|
|
7147e230de | ||
|
|
5ce11f192f | ||
|
|
71e865e9b7 | ||
|
|
590506f183 | ||
|
|
bed281a637 | ||
|
|
47dd767b3a | ||
|
|
8623b69991 | ||
|
|
a1ddc81dac | ||
|
|
832cdacebf | ||
|
|
9f979080b6 | ||
|
|
7f7301b31e | ||
|
|
6affbedef4 | ||
|
|
ba7f32c1bf | ||
|
|
305ce29ebb | ||
|
|
4cd0405078 | ||
|
|
783102449f | ||
|
|
ae617b2e1d | ||
|
|
07c57ed539 | ||
|
|
f5e18fc1fe | ||
|
|
f64cd32985 | ||
|
|
333b6f5a4b | ||
|
|
1660df20af | ||
|
|
14a9c4ba28 | ||
|
|
383be111fa | ||
|
|
7720b1cc34 | ||
|
|
db634cbb79 | ||
|
|
998e6de211 | ||
|
|
6fb02f9869 | ||
|
|
449390143d |
@@ -141,6 +141,24 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mittalsam98",
|
||||
"name": "Sachin Mittal",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/42431274?v=4",
|
||||
"profile": "https://myself.vercel.app/",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Champetaman",
|
||||
"name": "Camilo Oviedo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/64604272?v=4",
|
||||
"profile": "https://www.camilooviedo.com/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -128,6 +128,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -69,9 +69,8 @@
|
||||
"is-my-json-valid": "^2.20.5",
|
||||
"js-money": "^0.6.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.95.15",
|
||||
"knex": "^3.1.0",
|
||||
"knex-cleaner": "^1.3.0",
|
||||
"knex-db-manager": "^0.6.1",
|
||||
"libphonenumber-js": "^1.9.6",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^6.0.0",
|
||||
|
||||
@@ -103,24 +103,20 @@ export default class AccountsController extends BaseController {
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('currency_code').optional(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
@@ -136,23 +132,19 @@ export default class AccountsController extends BaseController {
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
|
||||
@@ -250,10 +250,12 @@ export class AttachmentsController extends BaseController {
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const { id: documentKey } = req.params;
|
||||
|
||||
try {
|
||||
const presignedUrl = await this.attachmentsApplication.getPresignedUrl(
|
||||
tenantId,
|
||||
documentKey
|
||||
);
|
||||
return res.status(200).send({ presignedUrl });
|
||||
|
||||
@@ -90,27 +90,23 @@ export default class AuthenticationController extends BaseController {
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('email')
|
||||
.exists()
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.isLength({ min: 6 })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
];
|
||||
}
|
||||
@@ -150,7 +146,7 @@ export default class AuthenticationController extends BaseController {
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
private get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [check('email').exists().isEmail().trim().escape()];
|
||||
return [check('email').exists().isEmail().trim()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +154,11 @@ export default class AuthenticationController extends BaseController {
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
private async login(req: Request, res: Response, next: Function): Response {
|
||||
private async login(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
): Promise<Response | null> {
|
||||
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { param, query } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
||||
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
|
||||
import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
|
||||
|
||||
@Service()
|
||||
export class BankAccountsController extends BaseController {
|
||||
@@ -13,6 +14,9 @@ export class BankAccountsController extends BaseController {
|
||||
@Inject()
|
||||
private bankAccountsApp: BankAccountsApplication;
|
||||
|
||||
@Inject()
|
||||
private getPendingTransactionsService: GetPendingBankAccountTransactions;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -20,11 +24,33 @@ export class BankAccountsController extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -56,6 +82,32 @@ export class BankAccountsController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -109,4 +161,58 @@ export class BankAccountsController extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the bank account feeds sync.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response | void>}
|
||||
*/
|
||||
async resumeBankAccountFeeds(
|
||||
req: Request<{ bankAccountId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { bankAccountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The bank account feeds syncing has been resumed.',
|
||||
id: bankAccountId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the bank account feeds sync.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response | void>}
|
||||
*/
|
||||
async pauseBankAccountFeeds(
|
||||
req: Request<{ bankAccountId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { bankAccountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The bank account feeds syncing has been paused.',
|
||||
id: bankAccountId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { body, param } from 'express-validator';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionsDTO,
|
||||
} from '@/services/Banking/Matching/types';
|
||||
|
||||
@Service()
|
||||
export class BankTransactionsMatchingController extends BaseController {
|
||||
@@ -20,9 +16,17 @@ export class BankTransactionsMatchingController extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:transactionId',
|
||||
'/unmatch/:transactionId',
|
||||
[param('transactionId').exists()],
|
||||
this.validationResult,
|
||||
this.unmatchMatchedBankTransaction.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/match',
|
||||
[
|
||||
param('transactionId').exists(),
|
||||
body('uncategorizedTransactions').exists().isArray({ min: 1 }),
|
||||
body('uncategorizedTransactions.*').isNumeric().toInt(),
|
||||
|
||||
body('matchedTransactions').isArray({ min: 1 }),
|
||||
body('matchedTransactions.*.reference_type').exists(),
|
||||
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
|
||||
@@ -30,12 +34,6 @@ export class BankTransactionsMatchingController extends BaseController {
|
||||
this.validationResult,
|
||||
this.matchBankTransaction.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/unmatch/:transactionId',
|
||||
[param('transactionId').exists()],
|
||||
this.validationResult,
|
||||
this.unmatchMatchedBankTransaction.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -50,21 +48,21 @@ export class BankTransactionsMatchingController extends BaseController {
|
||||
req: Request<{ transactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
): Promise<Response | null> {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
const matchTransactionDTO = this.matchedBodyData(
|
||||
req
|
||||
) as IMatchTransactionsDTO;
|
||||
const bodyData = this.matchedBodyData(req);
|
||||
|
||||
const uncategorizedTransactions = bodyData?.uncategorizedTransactions;
|
||||
const matchedTransactions = bodyData?.matchedTransactions;
|
||||
|
||||
try {
|
||||
await this.bankTransactionsMatchingApp.matchTransaction(
|
||||
tenantId,
|
||||
transactionId,
|
||||
matchTransactionDTO
|
||||
uncategorizedTransactions,
|
||||
matchedTransactions
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: transactionId,
|
||||
ids: uncategorizedTransactions,
|
||||
message: 'The bank transaction has been matched.',
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BankingRulesController } from './BankingRulesController';
|
||||
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
|
||||
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
|
||||
import { BankAccountsController } from './BankAccountsController';
|
||||
import { BankingUncategorizedController } from './BankingUncategorizedController';
|
||||
|
||||
@Service()
|
||||
export class BankingController extends BaseController {
|
||||
@@ -29,6 +30,10 @@ export class BankingController extends BaseController {
|
||||
'/bank_accounts',
|
||||
Container.get(BankAccountsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/categorize',
|
||||
Container.get(BankingUncategorizedController).router()
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,25 @@ export class BankingRulesController extends BaseController {
|
||||
body('conditions.*.field').exists().isIn(['description', 'amount']),
|
||||
body('conditions.*.comparator')
|
||||
.exists()
|
||||
.isIn(['equals', 'contains', 'not_contain'])
|
||||
.default('contain'),
|
||||
body('conditions.*.value').exists(),
|
||||
.isIn([
|
||||
'equals',
|
||||
'equal',
|
||||
'contains',
|
||||
'not_contain',
|
||||
'bigger',
|
||||
'bigger_or_equal',
|
||||
'smaller',
|
||||
'smaller_or_equal',
|
||||
])
|
||||
.default('contain')
|
||||
.trim(),
|
||||
body('conditions.*.value').exists().trim(),
|
||||
|
||||
// Assign
|
||||
body('assign_category').isString(),
|
||||
body('assign_account_id').isInt({ min: 0 }),
|
||||
body('assign_payee').isString().optional({ nullable: true }),
|
||||
body('assign_memo').isString().optional({ nullable: true }),
|
||||
|
||||
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction';
|
||||
|
||||
@Service()
|
||||
export class BankingUncategorizedController extends BaseController {
|
||||
@Inject()
|
||||
private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/autofill',
|
||||
[
|
||||
query('uncategorizedTransactionIds').isArray({ min: 1 }),
|
||||
query('uncategorizedTransactionIds.*').isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.getAutofillCategorizeTransaction.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the autofill values of the categorize form of the given
|
||||
* uncategorized transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response | null>}
|
||||
*/
|
||||
public async getAutofillCategorizeTransaction(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionIds
|
||||
);
|
||||
return res.status(200).send({ data });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,11 @@ export class ExcludeBankTransactionsController extends BaseController {
|
||||
);
|
||||
router.get(
|
||||
'/excluded',
|
||||
[],
|
||||
[
|
||||
query('account_id').optional().isNumeric().toInt(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.getExcludedBankTransactions.bind(this)
|
||||
);
|
||||
@@ -177,7 +181,7 @@ export class ExcludeBankTransactionsController extends BaseController {
|
||||
next: NextFunction
|
||||
): Promise<Response | void> {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedBodyData(req);
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const data =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@@ -14,7 +15,16 @@ export class RecognizedTransactionsController extends BaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', this.getRecognizedTransactions.bind(this));
|
||||
router.get(
|
||||
'/',
|
||||
[
|
||||
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(
|
||||
'/transactions/:uncategorizedTransactionId',
|
||||
this.getRecognizedTransaction.bind(this)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import { param, query } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
@@ -24,7 +24,12 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions/:transactionId/matches',
|
||||
'/transactions/matches',
|
||||
[
|
||||
query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }),
|
||||
query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.getMatchedTransactions.bind(this)
|
||||
);
|
||||
router.get(
|
||||
@@ -44,7 +49,7 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCashflowTransaction = async (
|
||||
req: Request,
|
||||
req: Request<{ transactionId: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
@@ -71,19 +76,24 @@ export default class GetCashflowAccounts extends BaseController {
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getMatchedTransactions(
|
||||
req: Request<{ transactionId: number }>,
|
||||
req: Request<
|
||||
{ transactionId: number },
|
||||
null,
|
||||
null,
|
||||
{ uncategorizeTransactionsIds: Array<number> }
|
||||
>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
const uncategorizeTransactionsIds = req.query.uncategorizeTransactionsIds;
|
||||
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.bankTransactionsMatchingApp.getMatchedTransactions(
|
||||
tenantId,
|
||||
transactionId,
|
||||
uncategorizeTransactionsIds,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ValidationChain, check, param, query } from 'express-validator';
|
||||
import { ValidationChain, body, check, param, query } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { omit } from 'lodash';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
import {
|
||||
AbilitySubject,
|
||||
CashflowAction,
|
||||
ICategorizeCashflowTransactioDTO,
|
||||
} from '@/interfaces';
|
||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||
|
||||
@Service()
|
||||
@@ -38,13 +43,23 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
this.asyncMiddleware(this.newCashflowTransaction),
|
||||
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(
|
||||
'/transactions/:id/uncategorize',
|
||||
this.revertCategorizedCashflowTransaction,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/transactions/:id/categorize',
|
||||
'/transactions/categorize',
|
||||
this.categorizeCashflowTransactionValidationSchema,
|
||||
this.validationResult,
|
||||
this.categorizeCashflowTransaction,
|
||||
@@ -89,6 +104,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
*/
|
||||
public get categorizeCashflowTransactionValidationSchema() {
|
||||
return [
|
||||
check('uncategorized_transaction_ids').exists().isArray({ min: 1 }),
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('credit_account_id').exists().isInt().toInt(),
|
||||
check('transaction_number').optional(),
|
||||
@@ -106,12 +122,11 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
public get newTransactionValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||
check('reference_no').optional({ nullable: true }).trim(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('transaction_type').exists(),
|
||||
|
||||
check('amount').exists().isFloat().toFloat(),
|
||||
@@ -161,7 +176,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private revertCategorizedCashflowTransaction = async (
|
||||
req: Request,
|
||||
req: Request<{ id: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
@@ -179,6 +194,34 @@ 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.
|
||||
* @param {Request} req
|
||||
@@ -191,14 +234,19 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: cashflowTransactionId } = req.params;
|
||||
const cashflowTransaction = this.matchedBodyData(req);
|
||||
const matchedObject = this.matchedBodyData(req);
|
||||
const categorizeDTO = omit(matchedObject, [
|
||||
'uncategorizedTransactionIds',
|
||||
]) as ICategorizeCashflowTransactioDTO;
|
||||
|
||||
const uncategorizedTransactionIds =
|
||||
matchedObject.uncategorizedTransactionIds;
|
||||
|
||||
try {
|
||||
await this.cashflowApplication.categorizeTransaction(
|
||||
tenantId,
|
||||
cashflowTransactionId,
|
||||
cashflowTransaction
|
||||
uncategorizedTransactionIds,
|
||||
categorizeDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The cashflow transaction has been created successfully.',
|
||||
@@ -269,7 +317,7 @@ export default class NewCashflowTransactionController extends BaseController {
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getUncategorizedCashflowTransactions = async (
|
||||
req: Request,
|
||||
req: Request<{ id: number }>,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class ContactsController extends BaseController {
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
@@ -122,32 +122,27 @@ export default class ContactsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('first_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('company_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('display_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('email')
|
||||
@@ -165,120 +160,101 @@ export default class ContactsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('personal_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('billing_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('shipping_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('note')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
|
||||
@@ -106,11 +106,7 @@ export default class CustomersController extends ContactsController {
|
||||
*/
|
||||
get customerDTOSchema() {
|
||||
return [
|
||||
check('customer_type')
|
||||
.exists()
|
||||
.isIn(['business', 'individual'])
|
||||
.trim()
|
||||
.escape(),
|
||||
check('customer_type').exists().isIn(['business', 'individual']).trim(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -123,7 +119,6 @@ export default class CustomersController extends ContactsController {
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 3 }),
|
||||
];
|
||||
}
|
||||
@@ -133,7 +128,7 @@ export default class CustomersController extends ContactsController {
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
|
||||
@@ -106,7 +106,6 @@ export default class VendorsController extends ContactsController {
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 3, max: 3 }),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default class CurrenciesController extends BaseController {
|
||||
}
|
||||
|
||||
get currencyParamSchema(): ValidationChain[] {
|
||||
return [param('currency_code').exists().trim().escape()];
|
||||
return [param('currency_code').exists().trim()];
|
||||
}
|
||||
|
||||
get listSchema(): ValidationChain[] {
|
||||
@@ -187,11 +187,13 @@ export default class CurrenciesController extends BaseController {
|
||||
}
|
||||
if (error.errorType === 'currency_code_exists') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{
|
||||
type: 'CURRENCY_CODE_EXISTS',
|
||||
message: 'The given currency code is already exists.',
|
||||
code: 200,
|
||||
}],
|
||||
errors: [
|
||||
{
|
||||
type: 'CURRENCY_CODE_EXISTS',
|
||||
message: 'The given currency code is already exists.',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
|
||||
|
||||
@@ -89,7 +89,6 @@ export class ExpensesController extends BaseController {
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
@@ -123,7 +122,6 @@ export class ExpensesController extends BaseController {
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
@@ -144,7 +142,6 @@ export class ExpensesController extends BaseController {
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
@@ -179,7 +176,6 @@ export class ExpensesController extends BaseController {
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from "../BaseController";
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
export default class BaseFinancialReportController extends BaseController {
|
||||
|
||||
|
||||
get sheetNumberFormatValidationSchema() {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
@@ -19,8 +17,7 @@ export default class BaseFinancialReportController extends BaseController {
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ export default class InventoryDetailsController extends BaseController {
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('transaction_type').optional().trim().escape(),
|
||||
query('transaction_type').optional().trim(),
|
||||
query('transaction_id').optional().isInt().toInt(),
|
||||
oneOf(
|
||||
[
|
||||
|
||||
@@ -40,8 +40,7 @@ export default class TransactionsByReferenceController extends BaseController {
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class InventoryAdjustmentsController extends BaseController {
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class InviteUsersController extends BaseController {
|
||||
router.post(
|
||||
'/send',
|
||||
[
|
||||
body('email').exists().trim().escape(),
|
||||
body('email').exists().trim(),
|
||||
body('role_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
@@ -57,7 +57,7 @@ export default class InviteUsersController extends BaseController {
|
||||
);
|
||||
router.get(
|
||||
'/invited/:token',
|
||||
[param('token').exists().trim().escape()],
|
||||
[param('token').exists().trim()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.invited.bind(this)),
|
||||
this.handleServicesError
|
||||
@@ -72,10 +72,10 @@ export default class InviteUsersController extends BaseController {
|
||||
*/
|
||||
private get inviteUserDTO() {
|
||||
return [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('password').exists().trim().escape().isLength({ min: 5 }),
|
||||
param('token').exists().trim().escape(),
|
||||
check('first_name').exists().trim(),
|
||||
check('last_name').exists().trim(),
|
||||
check('password').exists().trim().isLength({ min: 5 }),
|
||||
param('token').exists().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -73,13 +73,11 @@ export default class ItemsCategoriesController extends BaseController {
|
||||
check('name')
|
||||
.exists()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 0, max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_account_id')
|
||||
.optional({ nullable: true })
|
||||
@@ -101,9 +99,8 @@ export default class ItemsCategoriesController extends BaseController {
|
||||
*/
|
||||
get categoriesListValidationSchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
|
||||
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().trim().isIn(['desc', 'asc']),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
];
|
||||
}
|
||||
@@ -207,14 +204,12 @@ export default class ItemsCategoriesController extends BaseController {
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
itemCategories,
|
||||
filterMeta,
|
||||
} = await this.itemCategoriesService.getItemCategoriesList(
|
||||
tenantId,
|
||||
itemCategoriesFilter,
|
||||
user
|
||||
);
|
||||
const { itemCategories, filterMeta } =
|
||||
await this.itemCategoriesService.getItemCategoriesList(
|
||||
tenantId,
|
||||
itemCategoriesFilter,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
item_categories: itemCategories,
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
|
||||
@@ -96,13 +96,11 @@ export default class ItemsController extends BaseController {
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isIn(['service', 'non-inventory', 'inventory']),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
// Purchase attributes.
|
||||
check('purchasable').optional().isBoolean().toBoolean(),
|
||||
@@ -141,13 +139,11 @@ export default class ItemsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('purchase_description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_tax_rate_id').optional({ nullable: true }).isInt().toInt(),
|
||||
check('purchase_tax_rate_id')
|
||||
@@ -162,7 +158,6 @@ export default class ItemsController extends BaseController {
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
|
||||
@@ -184,7 +179,7 @@ export default class ItemsController extends BaseController {
|
||||
*/
|
||||
private get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
|
||||
@@ -94,25 +94,21 @@ export default class ManualJournalsController extends BaseController {
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('journal_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('reference')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
@@ -163,7 +159,7 @@ export default class ManualJournalsController extends BaseController {
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('column_sort_by').optional().trim(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
@@ -61,15 +61,14 @@ export default class MediaController extends BaseController {
|
||||
|
||||
get uploadValidationSchema() {
|
||||
return [
|
||||
// check('attachment'),
|
||||
check('model_name').optional().trim().escape(),
|
||||
check('model_id').optional().isNumeric().toInt(),
|
||||
check('model_name').optional().trim(),
|
||||
check('model_id').optional().isNumeric(),
|
||||
];
|
||||
}
|
||||
|
||||
get linkValidationSchema() {
|
||||
return [
|
||||
check('model_name').exists().trim().escape(),
|
||||
check('model_name').exists().trim(),
|
||||
check('model_id').exists().isNumeric().toInt(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class OrganizationController extends BaseController {
|
||||
private get commonOrganizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional({ nullable: true }).isString().trim().escape(),
|
||||
check('industry').optional({ nullable: true }).isString().trim(),
|
||||
check('location').exists().isString().isISO31661Alpha2(),
|
||||
check('base_currency').exists().isISO4217(),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
@@ -87,11 +87,7 @@ export default class OrganizationController extends BaseController {
|
||||
private get updateOrganizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.commonOrganizationValidationSchema,
|
||||
check('tax_number')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape(),
|
||||
check('tax_number').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ export default class BillsController extends BaseController {
|
||||
*/
|
||||
private get billValidationSchema() {
|
||||
return [
|
||||
check('bill_number').exists().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('bill_number').exists().trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('bill_date').exists().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('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('note').optional().trim(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
|
||||
@@ -126,10 +126,7 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
@@ -141,7 +138,6 @@ export default class BillsController extends BaseController {
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isString(),
|
||||
check('entries.*.tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
@@ -158,8 +154,8 @@ export default class BillsController extends BaseController {
|
||||
*/
|
||||
private get billEditValidationSchema() {
|
||||
return [
|
||||
check('bill_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('bill_number').optional().trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('bill_date').exists().isISO8601(),
|
||||
check('due_date').optional().isISO8601(),
|
||||
|
||||
@@ -170,7 +166,7 @@ export default class BillsController extends BaseController {
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('note').optional().trim(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('entries').isArray({ min: 1 }),
|
||||
@@ -184,10 +180,7 @@ export default class BillsController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.landed_cost')
|
||||
.optional({ nullable: true })
|
||||
.isBoolean()
|
||||
@@ -222,8 +215,8 @@ export default class BillsController extends BaseController {
|
||||
|
||||
private get dueBillsListingValidationSchema() {
|
||||
return [
|
||||
query('vendor_id').optional().trim().escape(),
|
||||
query('payment_made_id').optional().trim().escape(),
|
||||
query('vendor_id').optional().trim(),
|
||||
query('payment_made_id').optional().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -113,10 +113,10 @@ export default class BillsPayments extends BaseController {
|
||||
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_number').optional({ nullable: true }).trim().escape(),
|
||||
check('payment_number').optional({ nullable: true }).trim(),
|
||||
check('payment_date').exists(),
|
||||
check('statement').optional().trim().escape(),
|
||||
check('reference').optional().trim().escape(),
|
||||
check('statement').optional().trim(),
|
||||
check('reference').optional().trim(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray(),
|
||||
|
||||
@@ -156,13 +156,10 @@ export default class VendorCreditController extends BaseController {
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('vendor_credit_number')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('vendor_credit_number').optional({ nullable: true }).trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('note').optional().trim(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
@@ -178,10 +175,7 @@ export default class VendorCreditController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -202,13 +196,10 @@ export default class VendorCreditController extends BaseController {
|
||||
check('vendor_id').exists().isNumeric().toInt(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('vendor_credit_number')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('vendor_credit_number').optional({ nullable: true }).trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('vendor_credit_date').exists().isISO8601().toDate(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('note').optional().trim(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
@@ -223,10 +214,7 @@ export default class VendorCreditController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
|
||||
@@ -18,9 +18,7 @@ export default class ResourceController extends BaseController {
|
||||
|
||||
router.get(
|
||||
'/:resource_model/meta',
|
||||
[
|
||||
param('resource_model').exists().trim().escape()
|
||||
],
|
||||
[param('resource_model').exists().trim()],
|
||||
this.asyncMiddleware(this.resourceMeta.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
@@ -48,9 +46,7 @@ export default class ResourceController extends BaseController {
|
||||
resourceModel
|
||||
);
|
||||
return res.status(200).send({
|
||||
resource_meta: this.transfromToResponse(
|
||||
resourceMeta,
|
||||
),
|
||||
resource_meta: this.transfromToResponse(resourceMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -210,9 +210,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
check('credit_note_date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional(),
|
||||
check('credit_note_number').optional({ nullable: true }).trim().escape(),
|
||||
check('note').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
check('credit_note_number').optional({ nullable: true }).trim(),
|
||||
check('note').optional().trim(),
|
||||
check('terms_conditions').optional().trim(),
|
||||
check('open').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
@@ -228,10 +228,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from '@/interfaces';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages';
|
||||
import PaymentsReceivedPages from '@/services/Sales/PaymentReceived/PaymentsReceivedPages';
|
||||
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceived/PaymentReceivedApplication';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
@@ -22,7 +22,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
private paymentReceiveApplication: PaymentReceivesApplication;
|
||||
|
||||
@Inject()
|
||||
private PaymentReceivesPages: PaymentReceivesPages;
|
||||
private PaymentsReceivedPages: PaymentsReceivedPages;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
@@ -154,8 +154,8 @@ export default class PaymentReceivesController extends BaseController {
|
||||
check('payment_date').exists(),
|
||||
check('reference_no').optional(),
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
check('payment_receive_no').optional({ nullable: true }).trim(),
|
||||
check('statement').optional().trim(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
@@ -176,7 +176,6 @@ export default class PaymentReceivesController extends BaseController {
|
||||
private get validatePaymentReceiveList(): ValidationChain[] {
|
||||
return [
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
@@ -230,7 +229,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
try {
|
||||
const storedPaymentReceive =
|
||||
await this.paymentReceiveApplication.createPaymentReceive(
|
||||
await this.paymentReceiveApplication.createPaymentReceived(
|
||||
tenantId,
|
||||
paymentReceive,
|
||||
user
|
||||
@@ -377,7 +376,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
const { customerId } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const entries = await this.PaymentReceivesPages.getNewPageEntries(
|
||||
const entries = await this.PaymentsReceivedPages.getNewPageEntries(
|
||||
tenantId,
|
||||
customerId
|
||||
);
|
||||
@@ -405,7 +404,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
|
||||
try {
|
||||
const { paymentReceive, entries } =
|
||||
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
|
||||
await this.PaymentsReceivedPages.getPaymentReceiveEditPage(
|
||||
tenantId,
|
||||
paymentReceiveId,
|
||||
user
|
||||
|
||||
@@ -155,7 +155,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
check('estimate_date').exists().isISO8601().toDate(),
|
||||
check('expiration_date').exists().isISO8601().toDate(),
|
||||
check('reference').optional(),
|
||||
check('estimate_number').optional().trim().escape(),
|
||||
check('estimate_number').optional().trim(),
|
||||
check('delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
@@ -170,8 +170,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('entries.*.discount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -181,9 +180,9 @@ export default class SalesEstimatesController extends BaseController {
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
|
||||
check('note').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
check('send_to_email').optional().trim().escape(),
|
||||
check('note').optional().trim(),
|
||||
check('terms_conditions').optional().trim(),
|
||||
check('send_to_email').optional().trim(),
|
||||
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
|
||||
@@ -200,12 +200,12 @@ export default class SaleInvoicesController extends BaseController {
|
||||
check('customer_id').exists().isNumeric().toInt(),
|
||||
check('invoice_date').exists().isISO8601().toDate(),
|
||||
check('due_date').exists().isISO8601().toDate(),
|
||||
check('invoice_no').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('invoice_no').optional().trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('delivered').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('invoice_message').optional().trim().escape(),
|
||||
check('terms_conditions').optional().trim().escape(),
|
||||
check('invoice_message').optional().trim(),
|
||||
check('terms_conditions').optional().trim(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
@@ -226,12 +226,10 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.toFloat(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isString(),
|
||||
check('entries.*.tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
|
||||
@@ -130,8 +130,8 @@ export default class SalesReceiptsController extends BaseController {
|
||||
|
||||
check('deposit_account_id').exists().isNumeric().toInt(),
|
||||
check('receipt_date').exists().isISO8601(),
|
||||
check('receipt_number').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('receipt_number').optional().trim(),
|
||||
check('reference_no').optional().trim(),
|
||||
check('closed').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
@@ -150,14 +150,13 @@ export default class SalesReceiptsController extends BaseController {
|
||||
.toInt(),
|
||||
check('entries.*.description')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
.trim(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('receipt_message').optional().trim().escape(),
|
||||
check('statement').optional().trim().escape(),
|
||||
check('receipt_message').optional().trim(),
|
||||
check('statement').optional().trim(),
|
||||
check('attachments').isArray().optional(),
|
||||
check('attachments.*.key').exists().isString(),
|
||||
];
|
||||
|
||||
@@ -52,10 +52,7 @@ export default class SettingsController extends BaseController {
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
private get getSettingsSchema() {
|
||||
return [
|
||||
query('key').optional().trim().escape(),
|
||||
query('group').optional().trim().escape(),
|
||||
];
|
||||
return [query('key').optional().trim(), query('group').optional().trim()];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '../BaseController';
|
||||
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
|
||||
import { SubscriptionApplication } from '@/services/Subscription/SubscriptionApplication';
|
||||
|
||||
@Service()
|
||||
export class SubscriptionController extends BaseController {
|
||||
@@ -17,6 +18,9 @@ export class SubscriptionController extends BaseController {
|
||||
@Inject()
|
||||
private lemonSqueezyService: LemonSqueezyService;
|
||||
|
||||
@Inject()
|
||||
private subscriptionApp: SubscriptionApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -33,6 +37,14 @@ export class SubscriptionController extends BaseController {
|
||||
this.validationResult,
|
||||
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)));
|
||||
|
||||
return router;
|
||||
@@ -85,4 +97,84 @@ export class SubscriptionController extends BaseController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the subscription of the current organization.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response|null>}
|
||||
*/
|
||||
private async cancelSubscription(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The organization subscription has been canceled.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the subscription of the current organization.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response | null>}
|
||||
*/
|
||||
private async resumeSubscription(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.subscriptionApp.resumeSubscription(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 200,
|
||||
message: 'The organization subscription has been resumed.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the main subscription plan of the current organization.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Promise<Response | null>}
|
||||
*/
|
||||
public async changeSubscriptionPlan(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const body = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.subscriptionApp.changeSubscriptionPlan(
|
||||
tenantId,
|
||||
body.variantId
|
||||
);
|
||||
return res.status(200).send({
|
||||
message: 'The subscription plan has been changed.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class ViewsController extends BaseController {
|
||||
* Custom views list validation schema.
|
||||
*/
|
||||
get viewsListSchemaValidation() {
|
||||
return [param('resource_model').exists().trim().escape()];
|
||||
return [param('resource_model').exists().trim()];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,7 @@ module.exports = {
|
||||
*/
|
||||
tenant: {
|
||||
db_client: process.env.TENANT_DB_CLIENT || process.env.DB_CLIENT || 'mysql',
|
||||
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX,
|
||||
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX || 'bigcapital_tenant_',
|
||||
db_host: process.env.TENANT_DB_HOST || process.env.DB_HOST,
|
||||
db_user: process.env.TENANT_DB_USER || process.env.DB_USER,
|
||||
db_password: process.env.TENANT_DB_PASSWORD || process.env.DB_PASSWORD,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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();
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
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');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
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');
|
||||
});
|
||||
};
|
||||
@@ -236,6 +236,7 @@ export interface ICashflowTransactionSchema {
|
||||
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
|
||||
|
||||
export interface ICategorizeCashflowTransactioDTO {
|
||||
date: Date;
|
||||
creditAccountId: number;
|
||||
referenceNo: string;
|
||||
transactionNumber: string;
|
||||
@@ -267,6 +268,8 @@ export interface CreateUncategorizedTransactionDTO {
|
||||
description?: string;
|
||||
referenceNo?: string | null;
|
||||
plaidTransactionId?: string | null;
|
||||
pending?: boolean;
|
||||
pendingPlaidTransactionId?: string | null;
|
||||
batch?: string;
|
||||
}
|
||||
|
||||
@@ -282,3 +285,17 @@ export interface IUncategorizedTransactionCreatedEventPayload {
|
||||
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPendingTransactionRemovingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPendingTransactionRemovedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
pendingTransaction: IUncategorizedCashflowTransaction;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -130,20 +130,23 @@ export interface ICommandCashflowDeletedPayload {
|
||||
|
||||
export interface ICashflowTransactionCategorizedPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: any;
|
||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
cashflowTransaction: ICashflowTransaction;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
categorizeDTO: any;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizingPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
uncategorizedTransactionId: number;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICashflowTransactionUncategorizedPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
|
||||
uncategorizedTransactionId: number;
|
||||
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
|
||||
currencyCode: string;
|
||||
note?: string;
|
||||
|
||||
transactionType?: string;
|
||||
transactionTypeFormatted: string;
|
||||
transactionNumber: string;
|
||||
|
||||
referenceId?: number;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ILedgerEntry } from './Ledger';
|
||||
import { ISaleInvoice } from './SaleInvoice';
|
||||
import { AttachmentLinkDTO } from './Attachments';
|
||||
|
||||
export interface IPaymentReceive {
|
||||
export interface IPaymentReceived {
|
||||
id?: number;
|
||||
customerId: number;
|
||||
paymentDate: Date;
|
||||
@@ -19,14 +19,14 @@ export interface IPaymentReceive {
|
||||
depositAccountId: number;
|
||||
paymentReceiveNo: string;
|
||||
statement: string;
|
||||
entries: IPaymentReceiveEntry[];
|
||||
entries: IPaymentReceivedEntry[];
|
||||
userId: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
localAmount?: number;
|
||||
branchId?: number;
|
||||
}
|
||||
export interface IPaymentReceiveCreateDTO {
|
||||
export interface IPaymentReceivedCreateDTO {
|
||||
customerId: number;
|
||||
paymentDate: Date;
|
||||
amount: number;
|
||||
@@ -35,13 +35,13 @@ export interface IPaymentReceiveCreateDTO {
|
||||
depositAccountId: number;
|
||||
paymentReceiveNo?: string;
|
||||
statement: string;
|
||||
entries: IPaymentReceiveEntryDTO[];
|
||||
entries: IPaymentReceivedEntryDTO[];
|
||||
|
||||
branchId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEditDTO {
|
||||
export interface IPaymentReceivedEditDTO {
|
||||
customerId: number;
|
||||
paymentDate: Date;
|
||||
amount: number;
|
||||
@@ -50,12 +50,12 @@ export interface IPaymentReceiveEditDTO {
|
||||
depositAccountId: number;
|
||||
paymentReceiveNo?: string;
|
||||
statement: string;
|
||||
entries: IPaymentReceiveEntryDTO[];
|
||||
entries: IPaymentReceivedEntryDTO[];
|
||||
branchId?: number;
|
||||
attachments?: AttachmentLinkDTO[];
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEntry {
|
||||
export interface IPaymentReceivedEntry {
|
||||
id?: number;
|
||||
paymentReceiveId: number;
|
||||
invoiceId: number;
|
||||
@@ -64,15 +64,15 @@ export interface IPaymentReceiveEntry {
|
||||
invoice?: ISaleInvoice;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEntryDTO {
|
||||
export interface IPaymentReceivedEntryDTO {
|
||||
id?: number;
|
||||
index: number;
|
||||
paymentReceiveId: number;
|
||||
paymentReceiveId?: number;
|
||||
invoiceId: number;
|
||||
paymentAmount: number;
|
||||
}
|
||||
|
||||
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
|
||||
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string;
|
||||
}
|
||||
|
||||
@@ -88,65 +88,65 @@ export interface IPaymentReceivePageEntry {
|
||||
date: Date | string;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEditPage {
|
||||
paymentReceive: IPaymentReceive;
|
||||
export interface IPaymentReceivedEditPage {
|
||||
paymentReceive: IPaymentReceived;
|
||||
entries: IPaymentReceivePageEntry[];
|
||||
}
|
||||
|
||||
export interface IPaymentsReceiveService {
|
||||
export interface IPaymentsReceivedService {
|
||||
validateCustomerHasNoPayments(
|
||||
tenantId: number,
|
||||
customerId: number
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveSmsDetails {
|
||||
export interface IPaymentReceivedSmsDetails {
|
||||
customerName: string;
|
||||
customerPhoneNumber: string;
|
||||
smsMessage: string;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveCreatingPayload {
|
||||
export interface IPaymentReceivedCreatingPayload {
|
||||
tenantId: number;
|
||||
paymentReceiveDTO: IPaymentReceiveCreateDTO;
|
||||
paymentReceiveDTO: IPaymentReceivedCreateDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveCreatedPayload {
|
||||
export interface IPaymentReceivedCreatedPayload {
|
||||
tenantId: number;
|
||||
paymentReceive: IPaymentReceive;
|
||||
paymentReceive: IPaymentReceived;
|
||||
paymentReceiveId: number;
|
||||
authorizedUser: ISystemUser;
|
||||
paymentReceiveDTO: IPaymentReceiveCreateDTO;
|
||||
paymentReceiveDTO: IPaymentReceivedCreateDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEditedPayload {
|
||||
export interface IPaymentReceivedEditedPayload {
|
||||
tenantId: number;
|
||||
paymentReceiveId: number;
|
||||
paymentReceive: IPaymentReceive;
|
||||
oldPaymentReceive: IPaymentReceive;
|
||||
paymentReceiveDTO: IPaymentReceiveEditDTO;
|
||||
paymentReceive: IPaymentReceived;
|
||||
oldPaymentReceive: IPaymentReceived;
|
||||
paymentReceiveDTO: IPaymentReceivedEditDTO;
|
||||
authorizedUser: ISystemUser;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveEditingPayload {
|
||||
export interface IPaymentReceivedEditingPayload {
|
||||
tenantId: number;
|
||||
oldPaymentReceive: IPaymentReceive;
|
||||
paymentReceiveDTO: IPaymentReceiveEditDTO;
|
||||
oldPaymentReceive: IPaymentReceived;
|
||||
paymentReceiveDTO: IPaymentReceivedEditDTO;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IPaymentReceiveDeletingPayload {
|
||||
export interface IPaymentReceivedDeletingPayload {
|
||||
tenantId: number;
|
||||
oldPaymentReceive: IPaymentReceive;
|
||||
oldPaymentReceive: IPaymentReceived;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface IPaymentReceiveDeletedPayload {
|
||||
export interface IPaymentReceivedDeletedPayload {
|
||||
tenantId: number;
|
||||
paymentReceiveId: number;
|
||||
oldPaymentReceive: IPaymentReceive;
|
||||
oldPaymentReceive: IPaymentReceived;
|
||||
authorizedUser: ISystemUser;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -51,5 +51,4 @@ export interface ISystemService {
|
||||
cache();
|
||||
repositories();
|
||||
knex();
|
||||
dbManager();
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default class ComputeItemCostJob {
|
||||
|
||||
agenda.define(
|
||||
'compute-item-cost',
|
||||
{ priority: 'high', concurrency: 1 },
|
||||
{ priority: 'high', concurrency: 20 },
|
||||
this.handler.bind(this)
|
||||
);
|
||||
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));
|
||||
|
||||
@@ -8,7 +8,7 @@ export default class OrganizationSetupJob {
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'organization-setup',
|
||||
{ priority: 'high', concurrency: 1 },
|
||||
{ priority: 'high', concurrency: 20 },
|
||||
this.handler
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default class WriteInvoicesJournalEntries {
|
||||
|
||||
agenda.define(
|
||||
eventName,
|
||||
{ priority: 'normal', concurrency: 1 },
|
||||
{ priority: 'normal', concurrency: 20 },
|
||||
this.handler.bind(this)
|
||||
);
|
||||
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import knexManager from 'knex-db-manager';
|
||||
import { systemKnexConfig, systemDbManager } from 'config/knexConfig';
|
||||
|
||||
export default () => knexManager.databaseManagerFactory({
|
||||
knex: systemKnexConfig,
|
||||
dbManager: systemDbManager,
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import LoggerInstance from '@/loaders/logger';
|
||||
import agendaFactory from '@/loaders/agenda';
|
||||
import SmsClientLoader from '@/loaders/smsClient';
|
||||
import mailInstance from '@/loaders/mail';
|
||||
import dbManagerFactory from '@/loaders/dbManager';
|
||||
import i18n from '@/loaders/i18n';
|
||||
import repositoriesLoader from '@/loaders/systemRepositories';
|
||||
import Cache from '@/services/Cache';
|
||||
@@ -16,7 +15,6 @@ export default ({ mongoConnection, knex }) => {
|
||||
try {
|
||||
const agendaInstance = agendaFactory({ mongoConnection });
|
||||
const smsClientInstance = SmsClientLoader(config.easySMSGateway.api_key);
|
||||
const dbManager = dbManagerFactory(knex);
|
||||
const cacheInstance = new Cache();
|
||||
|
||||
Container.set('logger', LoggerInstance);
|
||||
@@ -24,7 +22,6 @@ export default ({ mongoConnection, knex }) => {
|
||||
Container.set('SMSClient', smsClientInstance);
|
||||
Container.set('mail', mailInstance);
|
||||
|
||||
Container.set('dbManager', dbManager);
|
||||
LoggerInstance.info(
|
||||
'[DI] Database manager has been injected into container.'
|
||||
);
|
||||
|
||||
@@ -115,6 +115,7 @@ import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/E
|
||||
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
|
||||
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
|
||||
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -277,6 +278,7 @@ export const susbcribers = () => {
|
||||
// Plaid
|
||||
RecognizeSyncedBankTranasctions,
|
||||
DisconnectPlaidItemOnAccountDeleted,
|
||||
DeleteUncategorizedTransactionsOnAccountDeleting,
|
||||
|
||||
// Loops
|
||||
LoopsEventsSubscriber
|
||||
|
||||
@@ -34,4 +34,4 @@
|
||||
// import 'services/Sales/SaleInvoiceWriteoffSubscriber';
|
||||
// import 'subscribers/SaleInvoices/SendSmsNotificationToCustomer';
|
||||
// import 'subscribers/SaleReceipt/SendNotificationToCustomer';
|
||||
// import 'services/Sales/PaymentReceives/PaymentReceiveSmsSubscriber';
|
||||
// import 'services/Sales/PaymentReceived/PaymentReceiveSmsSubscriber';
|
||||
@@ -9,11 +9,13 @@ import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoic
|
||||
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
|
||||
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
|
||||
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
|
||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||
import { PaymentReceivedMailNotificationJob } from '@/services/Sales/PaymentReceived/PaymentReceivedMailNotificationJob';
|
||||
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
||||
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
||||
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
|
||||
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob';
|
||||
import { ReregonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob';
|
||||
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob';
|
||||
import { RevertRegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new ResetPasswordMailJob(agenda);
|
||||
@@ -26,11 +28,13 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new SendSaleInvoiceReminderMailJob(agenda);
|
||||
new SendSaleEstimateMailJob(agenda);
|
||||
new SaleReceiptMailNotificationJob(agenda);
|
||||
new PaymentReceiveMailNotificationJob(agenda);
|
||||
new PaymentReceivedMailNotificationJob(agenda);
|
||||
new PlaidFetchTransactionsJob(agenda);
|
||||
new ImportDeleteExpiredFilesJobs(agenda);
|
||||
new SendVerifyMailJob(agenda);
|
||||
new RegonizeTransactionsJob(agenda);
|
||||
new ReregonizeTransactionsJob(agenda);
|
||||
new RevertRegonizeTransactionsJob(agenda);
|
||||
|
||||
agenda.start().then(() => {
|
||||
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
||||
|
||||
@@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
import BillPaymentSettings from './BillPayment.Settings';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
|
||||
export default class BillPayment extends mixin(TenantModel, [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Model } from 'objection';
|
||||
import { castArray, omit, pick } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@@ -16,7 +17,15 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
queryAndThrowIfHasRelations = ({ type, message }) => {
|
||||
queryAndThrowIfHasRelations = ({
|
||||
type,
|
||||
message,
|
||||
excludeRelations = [],
|
||||
includedRelations = [],
|
||||
}) => {
|
||||
const _excludeRelations = castArray(excludeRelations);
|
||||
const _includedRelations = castArray(includedRelations);
|
||||
|
||||
const model = this.modelClass();
|
||||
const modelRelations = Object.keys(model.relationMappings).filter(
|
||||
(relation) =>
|
||||
@@ -25,9 +34,20 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
|
||||
) !== -1
|
||||
);
|
||||
const relations = model.secureDeleteRelations || modelRelations;
|
||||
const filteredByIncluded = relations.filter((r) =>
|
||||
_includedRelations.includes(r)
|
||||
);
|
||||
const filteredByExcluded = relations.filter(
|
||||
(r) => !excludeRelations.includes(r)
|
||||
);
|
||||
const filteredRelations = !isEmpty(_includedRelations)
|
||||
? filteredByIncluded
|
||||
: !isEmpty(_excludeRelations)
|
||||
? filteredByExcluded
|
||||
: relations;
|
||||
|
||||
this.runAfter((model, query) => {
|
||||
const nonEmptyRelations = relations.filter(
|
||||
const nonEmptyRelations = filteredRelations.filter(
|
||||
(relation) => !isEmpty(model[relation])
|
||||
);
|
||||
if (nonEmptyRelations.length > 0) {
|
||||
@@ -36,7 +56,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
|
||||
return model;
|
||||
});
|
||||
return this.onBuild((query) => {
|
||||
relations.forEach((relation) => {
|
||||
filteredRelations.forEach((relation) => {
|
||||
query.withGraphFetched(`${relation}(selectId)`).modifiers({
|
||||
selectId(builder) {
|
||||
builder.select('id');
|
||||
|
||||
@@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
import PaymentReceiveSettings from './PaymentReceive.Settings';
|
||||
import CustomViewBaseModel from './CustomViewBaseModel';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
|
||||
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
|
||||
export default class PaymentReceive extends mixin(TenantModel, [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
export default class PlaidItem extends TenantModel {
|
||||
pausedAt: Date;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -21,4 +23,19 @@ export default class PlaidItem extends TenantModel {
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isPaused'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the Plaid item feeds syncing is paused.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isPaused() {
|
||||
return !!this.pausedAt;
|
||||
}
|
||||
}
|
||||
|
||||
69
packages/server/src/models/TaxRate.settings.ts
Normal file
69
packages/server/src/models/TaxRate.settings.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export default {
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
exportable: true,
|
||||
importable: true,
|
||||
print: {
|
||||
pageTitle: 'Tax Rates',
|
||||
},
|
||||
columns: {
|
||||
name: {
|
||||
name: 'Tax Rate Name',
|
||||
type: 'text',
|
||||
accessor: 'name',
|
||||
},
|
||||
code: {
|
||||
name: 'Code',
|
||||
type: 'text',
|
||||
accessor: 'code',
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
type: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
type: 'text',
|
||||
},
|
||||
isNonRecoverable: {
|
||||
name: 'Is Non Recoverable',
|
||||
type: 'boolean',
|
||||
},
|
||||
active: {
|
||||
name: 'Active',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
field: {},
|
||||
fields2: {
|
||||
name: {
|
||||
name: 'Tax name',
|
||||
fieldType: 'name',
|
||||
required: true,
|
||||
},
|
||||
code: {
|
||||
name: 'Code',
|
||||
fieldType: 'code',
|
||||
required: true,
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
isNonRecoverable: {
|
||||
name: 'Is Non Recoverable',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
active: {
|
||||
name: 'Active',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,8 +2,13 @@ import { mixin, Model, raw } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||
import TaxRateMeta from './TaxRate.settings';
|
||||
import ModelSetting from './ModelSetting';
|
||||
|
||||
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
|
||||
export default class TaxRate extends mixin(TenantModel, [
|
||||
ModelSetting,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -25,6 +30,13 @@ export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rate meta.
|
||||
*/
|
||||
static get meta() {
|
||||
return TaxRateMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable global-require */
|
||||
import * as R from 'ramda';
|
||||
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSettings from './ModelSetting';
|
||||
import Account from './Account';
|
||||
@@ -20,6 +19,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
description!: string;
|
||||
plaidTransactionId!: string;
|
||||
recognizedTransactionId!: number;
|
||||
excludedAt: Date;
|
||||
pending: boolean;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
@@ -31,7 +32,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
@@ -45,6 +46,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
'isDepositTransaction',
|
||||
'isWithdrawalTransaction',
|
||||
'isRecognized',
|
||||
'isExcluded',
|
||||
'isPending',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -89,6 +92,22 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
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.
|
||||
*/
|
||||
@@ -133,6 +152,20 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
query.whereNull('categorizeRefType');
|
||||
query.whereNull('categorizeRefId');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the not pending transactions.
|
||||
*/
|
||||
notPending(query) {
|
||||
query.where('pending', false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the pending transactions.
|
||||
*/
|
||||
pending(query) {
|
||||
query.where('pending', true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ export default class Ledger implements ILedger {
|
||||
|
||||
transactionId: entry.referenceId,
|
||||
transactionType: entry.referenceType,
|
||||
transactionSubType: entry.transactionType,
|
||||
|
||||
transactionNumber: entry.transactionNumber,
|
||||
referenceNumber: entry.referenceNumber,
|
||||
@@ -262,6 +263,8 @@ export default class Ledger implements ILedger {
|
||||
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
|
||||
note: entry.note,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,18 @@ export class AccountTransformer extends Transformer {
|
||||
'flattenName',
|
||||
'bankBalanceFormatted',
|
||||
'lastFeedsUpdatedAtFormatted',
|
||||
'isFeedsPaused',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['plaidItem'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the flatten name with all dependants accounts names.
|
||||
* @param {IAccount} account -
|
||||
@@ -66,6 +75,15 @@ export class AccountTransformer extends Transformer {
|
||||
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.
|
||||
* @param {IAccount[]}
|
||||
|
||||
@@ -43,8 +43,8 @@ export class AccountsApplication {
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public createAccount = (
|
||||
@@ -108,8 +108,8 @@ export class AccountsApplication {
|
||||
|
||||
/**
|
||||
* Retrieves the account details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public getAccount = (tenantId: number, accountId: number) => {
|
||||
|
||||
@@ -73,6 +73,7 @@ export class DeleteAccount {
|
||||
.throwIfNotFound()
|
||||
.queryAndThrowIfHasRelations({
|
||||
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
|
||||
excludeRelations: ['uncategorizedTransactions', 'plaidItem']
|
||||
});
|
||||
// Authorize before delete account.
|
||||
await this.authorize(tenantId, accountId, oldAccount);
|
||||
|
||||
@@ -25,7 +25,10 @@ export class GetAccount {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Find the given account or throw not found error.
|
||||
const account = await Account.query().findById(accountId).throwIfNotFound();
|
||||
const account = await Account.query()
|
||||
.findById(accountId)
|
||||
.withGraphFetched('plaidItem')
|
||||
.throwIfNotFound();
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
|
||||
@@ -96,10 +96,11 @@ export class AttachmentsApplication {
|
||||
|
||||
/**
|
||||
* Retrieves the presigned url of the given attachment key.
|
||||
* @param {number} tenantId
|
||||
* @param {string} key
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public getPresignedUrl(key: string): Promise<string> {
|
||||
return this.getPresignedUrlService.getPresignedUrl(key);
|
||||
public getPresignedUrl(tenantId: number, key: string): Promise<string> {
|
||||
return this.getPresignedUrlService.getPresignedUrl(tenantId, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
import { Service } from 'typedi';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { s3 } from '@/lib/S3/S3';
|
||||
import config from '@/config';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class getAttachmentPresignedUrl {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieves the presigned url of the given attachment key.
|
||||
* Retrieves the presigned url of the given attachment key with the original filename.
|
||||
* @param {number} tenantId
|
||||
* @param {string} key
|
||||
* @returns {Promise<string?>}
|
||||
* @returns {string}
|
||||
*/
|
||||
async getPresignedUrl(key: string) {
|
||||
async getPresignedUrl(tenantId: number, 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({
|
||||
Bucket: config.s3.bucket,
|
||||
Key: key,
|
||||
ResponseContentDisposition,
|
||||
});
|
||||
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
IPaymentReceiveCreatedPayload,
|
||||
IPaymentReceiveCreatingPayload,
|
||||
IPaymentReceiveDeletingPayload,
|
||||
IPaymentReceiveEditedPayload,
|
||||
IPaymentReceivedCreatedPayload,
|
||||
IPaymentReceivedCreatingPayload,
|
||||
IPaymentReceivedDeletingPayload,
|
||||
IPaymentReceivedEditedPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { LinkAttachment } from '../LinkAttachment';
|
||||
@@ -50,13 +50,13 @@ export class AttachmentsOnPaymentsReceived {
|
||||
|
||||
/**
|
||||
* Validates the attachment keys on creating payment.
|
||||
* @param {IPaymentReceiveCreatingPayload}
|
||||
* @param {IPaymentReceivedCreatingPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async validateAttachmentsOnPaymentCreate({
|
||||
paymentReceiveDTO,
|
||||
tenantId,
|
||||
}: IPaymentReceiveCreatingPayload): Promise<void> {
|
||||
}: IPaymentReceivedCreatingPayload): Promise<void> {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) {
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
|
||||
/**
|
||||
* Handles linking the attachments of the created payment.
|
||||
* @param {IPaymentReceiveCreatedPayload}
|
||||
* @param {IPaymentReceivedCreatedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async handleAttachmentsOnPaymentCreated({
|
||||
@@ -75,7 +75,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
paymentReceiveDTO,
|
||||
paymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceiveCreatedPayload): Promise<void> {
|
||||
}: IPaymentReceivedCreatedPayload): Promise<void> {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) return;
|
||||
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
@@ -92,14 +92,14 @@ export class AttachmentsOnPaymentsReceived {
|
||||
|
||||
/**
|
||||
* Handles unlinking all the unpresented keys of the edited payment.
|
||||
* @param {IPaymentReceiveEditedPayload}
|
||||
* @param {IPaymentReceivedEditedPayload}
|
||||
*/
|
||||
private async handleUnlinkUnpresentedKeysOnPaymentEdited({
|
||||
tenantId,
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceiveEditedPayload) {
|
||||
}: IPaymentReceivedEditedPayload) {
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
(attachment) => attachment.key
|
||||
);
|
||||
@@ -114,7 +114,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
|
||||
/**
|
||||
* Handles linking all the presented keys of the edited payment.
|
||||
* @param {IPaymentReceiveEditedPayload}
|
||||
* @param {IPaymentReceivedEditedPayload}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async handleLinkPresentedKeysOnPaymentEdited({
|
||||
@@ -122,7 +122,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
paymentReceiveDTO,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceiveEditedPayload) {
|
||||
}: IPaymentReceivedEditedPayload) {
|
||||
if (isEmpty(paymentReceiveDTO.attachments)) return;
|
||||
|
||||
const keys = paymentReceiveDTO.attachments?.map(
|
||||
@@ -146,7 +146,7 @@ export class AttachmentsOnPaymentsReceived {
|
||||
tenantId,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceiveDeletingPayload) {
|
||||
}: IPaymentReceivedDeletingPayload) {
|
||||
await this.unlinkAttachmentService.unlinkAllModelKeys(
|
||||
tenantId,
|
||||
'PaymentReceive',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 {
|
||||
@@ -10,6 +12,12 @@ export class BankAccountsApplication {
|
||||
@Inject()
|
||||
private refreshBankAccountService: RefreshBankAccountService;
|
||||
|
||||
@Inject()
|
||||
private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
|
||||
|
||||
@Inject()
|
||||
private pauseBankAccountFeedsService: PauseBankAccountFeeds;
|
||||
|
||||
/**
|
||||
* Disconnects the given bank account.
|
||||
* @param {number} tenantId
|
||||
@@ -27,7 +35,7 @@ export class BankAccountsApplication {
|
||||
* Refresh the bank transactions of the given bank account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} bankAccountId
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async refreshBankAccount(tenantId: number, bankAccountId: number) {
|
||||
return this.refreshBankAccountService.refreshBankAccount(
|
||||
@@ -35,4 +43,30 @@ export class BankAccountsApplication {
|
||||
bankAccountId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the feeds sync of the given bank account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} bankAccountId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async pauseBankAccount(tenantId: number, bankAccountId: number) {
|
||||
return this.pauseBankAccountFeedsService.pauseBankAccountFeeds(
|
||||
tenantId,
|
||||
bankAccountId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the feeds sync of the given bank account.
|
||||
* @param {number} tenantId
|
||||
* @param {number} bankAccountId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resumeBankAccount(tenantId: number, bankAccountId: number) {
|
||||
return this.resumeBankAccountFeedsService.resumeBankAccountFeeds(
|
||||
tenantId,
|
||||
bankAccountId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetBankAccountSummary {
|
||||
@@ -31,46 +32,85 @@ export class GetBankAccountSummary {
|
||||
.findById(bankAccountId)
|
||||
.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.
|
||||
const uncategorizedTranasctionsCount =
|
||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||
// Include just the given account.
|
||||
q.where('accountId', bankAccountId);
|
||||
|
||||
// Only the not excluded.
|
||||
q.modify('notExcluded');
|
||||
|
||||
// Only the not categorized.
|
||||
q.modify('notCategorized');
|
||||
commonQuery(q);
|
||||
|
||||
// Only the not matched bank transactions.
|
||||
q.withGraphJoined('matchedBankTransactions');
|
||||
q.whereNull('matchedBankTransactions.id');
|
||||
|
||||
// Exclude the pending transactions.
|
||||
q.modify('notPending');
|
||||
|
||||
// Count the results.
|
||||
q.count('uncategorized_cashflow_transactions.id as total');
|
||||
q.first();
|
||||
});
|
||||
|
||||
// Retrieves the recognized transactions count of the given bank account.
|
||||
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||
.whereExists(
|
||||
UncategorizedCashflowTransaction.query().where(
|
||||
'accountId',
|
||||
bankAccountId
|
||||
)
|
||||
)
|
||||
.count('id as total')
|
||||
.first();
|
||||
// Retrives the recognized transactions count.
|
||||
const recognizedTransactionsCount =
|
||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||
commonQuery(q);
|
||||
|
||||
q.withGraphJoined('recognizedTransaction');
|
||||
q.whereNotNull('recognizedTransaction.id');
|
||||
|
||||
// Exclude the pending transactions.
|
||||
q.modify('notPending');
|
||||
|
||||
// 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 =
|
||||
uncategorizedTranasctionsCount?.total || 0;
|
||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
|
||||
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
|
||||
|
||||
return {
|
||||
name: bankAccount.name,
|
||||
totalUncategorizedTransactions,
|
||||
totalRecognizedTransactions,
|
||||
totalExcludedTransactions,
|
||||
totalPendingTransactions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import events from '@/subscribers/events';
|
||||
import { IAccountEventDeletePayload } from '@/interfaces';
|
||||
import { DeleteBankRulesService } from '../../Rules/DeleteBankRules';
|
||||
import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions';
|
||||
|
||||
@Service()
|
||||
export class DeleteUncategorizedTransactionsOnAccountDeleting {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private deleteBankRules: DeleteBankRulesService;
|
||||
|
||||
@Inject()
|
||||
private revertRecognizedTransactins: RevertRecognizedTransactions;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.accounts.onDelete,
|
||||
this.handleDeleteBankRulesOnAccountDeleting.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles revert the recognized transactions and delete all the bank rules
|
||||
* associated to the deleted bank account.
|
||||
* @param {IAccountEventDeletePayload}
|
||||
*/
|
||||
private async handleDeleteBankRulesOnAccountDeleting({
|
||||
tenantId,
|
||||
oldAccount,
|
||||
trx,
|
||||
}: IAccountEventDeletePayload) {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const {
|
||||
BankRule,
|
||||
UncategorizedCashflowTransaction,
|
||||
MatchedBankTransaction,
|
||||
RecognizedBankTransaction,
|
||||
} = this.tenancy.models(tenantId);
|
||||
|
||||
const foundAssociatedRules = await BankRule.query(trx).where(
|
||||
'applyIfAccountId',
|
||||
oldAccount.id
|
||||
);
|
||||
const foundAssociatedRulesIds = foundAssociatedRules.map((rule) => rule.id);
|
||||
|
||||
await initialize(knex, [
|
||||
UncategorizedCashflowTransaction,
|
||||
RecognizedBankTransaction,
|
||||
MatchedBankTransaction,
|
||||
]);
|
||||
// Revert the recognized transactions of the given bank rules.
|
||||
await this.revertRecognizedTransactins.revertRecognizedTransactions(
|
||||
tenantId,
|
||||
foundAssociatedRulesIds,
|
||||
null,
|
||||
trx
|
||||
);
|
||||
// Delete the associated uncategorized transactions.
|
||||
await UncategorizedCashflowTransaction.query(trx)
|
||||
.where('accountId', oldAccount.id)
|
||||
.delete();
|
||||
|
||||
// Delete the given bank rules.
|
||||
await this.deleteBankRules.deleteBankRules(
|
||||
tenantId,
|
||||
foundAssociatedRulesIds,
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ export class DisconnectPlaidItemOnAccountDeleted {
|
||||
.findOne('plaidItemId', oldAccount.plaidItemId)
|
||||
.delete();
|
||||
|
||||
// Remove Plaid item once the transaction resolve.
|
||||
if (oldPlaidItem) {
|
||||
const plaidInstance = PlaidClientWrapper.getClient();
|
||||
|
||||
|
||||
@@ -14,4 +14,6 @@ export interface IBankAccountDisconnectedEventPayload {
|
||||
|
||||
export const ERRORS = {
|
||||
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
|
||||
BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED',
|
||||
BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED',
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { validateTransactionNotCategorized } from './utils';
|
||||
import {
|
||||
validateTransactionNotCategorized,
|
||||
validateTransactionNotExcluded,
|
||||
} from './utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
@@ -37,9 +41,13 @@ export class ExcludeBankTransaction {
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the transaction shouldn't be excluded.
|
||||
validateTransactionNotExcluded(oldUncategorizedTransaction);
|
||||
|
||||
// Validate the transaction shouldn't be categorized.
|
||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { castArray } from 'lodash';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
import { ExcludeBankTransaction } from './ExcludeBankTransaction';
|
||||
|
||||
@Service()
|
||||
@@ -18,7 +18,7 @@ export class ExcludeBankTransactions {
|
||||
tenantId: number,
|
||||
bankTransactionIds: Array<number> | number
|
||||
) {
|
||||
const _bankTransactionIds = castArray(bankTransactionIds);
|
||||
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
|
||||
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(_bankTransactionIds)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { validateTransactionNotCategorized } from './utils';
|
||||
import {
|
||||
validateTransactionNotCategorized,
|
||||
validateTransactionShouldBeExcluded,
|
||||
} from './utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import {
|
||||
@@ -37,9 +41,13 @@ export class UnexcludeBankTransaction {
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate the transaction should be excludded.
|
||||
validateTransactionShouldBeExcluded(oldUncategorizedTransaction);
|
||||
|
||||
// Validate the transaction shouldn't be categorized.
|
||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.bankTransactions.onUnexcluding,
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
|
||||
import { castArray } from 'lodash';
|
||||
import { castArray, uniq } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class UnexcludeBankTransactions {
|
||||
@@ -17,7 +17,7 @@ export class UnexcludeBankTransactions {
|
||||
tenantId: number,
|
||||
bankTransactionIds: Array<number> | number
|
||||
) {
|
||||
const _bankTransactionIds = castArray(bankTransactionIds);
|
||||
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
|
||||
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(_bankTransactionIds)
|
||||
|
||||
@@ -3,6 +3,8 @@ import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTran
|
||||
|
||||
const ERRORS = {
|
||||
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
||||
TRANSACTION_ALREADY_EXCLUDED: 'TRANSACTION_ALREADY_EXCLUDED',
|
||||
TRANSACTION_NOT_EXCLUDED: 'TRANSACTION_NOT_EXCLUDED',
|
||||
};
|
||||
|
||||
export const validateTransactionNotCategorized = (
|
||||
@@ -12,3 +14,19 @@ export const validateTransactionNotCategorized = (
|
||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateTransactionNotExcluded = (
|
||||
transaction: UncategorizedCashflowTransaction
|
||||
) => {
|
||||
if (transaction.isExcluded) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_EXCLUDED);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateTransactionShouldBeExcluded = (
|
||||
transaction: UncategorizedCashflowTransaction
|
||||
) => {
|
||||
if (!transaction.isExcluded) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_NOT_EXCLUDED);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { first, sumBy } from 'lodash';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionsPOJO } from './types';
|
||||
import { GetMatchedTransactionsByExpenses } from './GetMatchedTransactionsByExpenses';
|
||||
@@ -47,21 +48,24 @@ export class GetMatchedTransactions {
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {Array<number>} uncategorizedTransactionIds - Uncategorized transactions ids.
|
||||
* @param {GetMatchedTransactionsFilter} filter -
|
||||
* @returns {Promise<MatchedTransactionsPOJO>}
|
||||
*/
|
||||
public async getMatchedTransactions(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
): Promise<MatchedTransactionsPOJO> {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const uncategorizedTransaction =
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.findById(uncategorizedTransactionId)
|
||||
.whereIn('id', uncategorizedTransactionIds)
|
||||
.throwIfNotFound();
|
||||
|
||||
const totalPending = sumBy(uncategorizedTransactions, 'amount');
|
||||
|
||||
const filtered = filter.transactionType
|
||||
? this.registered.filter((item) => item.type === filter.transactionType)
|
||||
: this.registered;
|
||||
@@ -71,14 +75,14 @@ export class GetMatchedTransactions {
|
||||
.process(async ({ type, service }) => {
|
||||
return service.getMatchedTransactions(tenantId, filter);
|
||||
});
|
||||
|
||||
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
|
||||
uncategorizedTransaction,
|
||||
uncategorizedTransactions,
|
||||
matchedTransactions
|
||||
);
|
||||
return {
|
||||
perfectMatches,
|
||||
possibleMatches,
|
||||
totalPending,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,20 +94,20 @@ export class GetMatchedTransactions {
|
||||
* @returns {MatchedTransactionsPOJO}
|
||||
*/
|
||||
private groupMatchedResults(
|
||||
uncategorizedTransaction,
|
||||
uncategorizedTransactions: Array<any>,
|
||||
matchedTransactions
|
||||
): MatchedTransactionsPOJO {
|
||||
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
|
||||
const closestResullts = sortClosestMatchTransactions(
|
||||
uncategorizedTransaction,
|
||||
results
|
||||
);
|
||||
const closestResullts = sortClosestMatchTransactions(amount, date, results);
|
||||
const perfectMatches = R.filter(
|
||||
(match) =>
|
||||
match.amount === uncategorizedTransaction.amount &&
|
||||
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
|
||||
match.amount === amount && moment(match.date).isSame(date, 'day'),
|
||||
closestResullts
|
||||
);
|
||||
const possibleMatches = R.difference(closestResullts, perfectMatches);
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { Knex } from 'knex';
|
||||
import { first } from 'lodash';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionDTO,
|
||||
MatchedTransactionPOJO,
|
||||
} from './types';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { CreateBillPayment } from '@/services/Purchases/BillPayments/CreateBillPayment';
|
||||
import { IBillPaymentDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
private createPaymentMadeService: CreateBillPayment;
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
@@ -71,4 +78,62 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
||||
new GetMatchedTransactionBillsTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<void> {
|
||||
await super.createMatchedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionIds,
|
||||
matchTransactionDTO,
|
||||
trx
|
||||
);
|
||||
const { Bill, UncategorizedCashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const bill = await Bill.query(trx)
|
||||
.findById(matchTransactionDTO.referenceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const createPaymentMadeDTO: IBillPaymentDTO = {
|
||||
vendorId: bill.vendorId,
|
||||
paymentAccountId: uncategorizedTransaction.accountId,
|
||||
paymentDate: uncategorizedTransaction.date,
|
||||
exchangeRate: 1,
|
||||
entries: [
|
||||
{
|
||||
paymentAmount: bill.dueAmount,
|
||||
billId: bill.id,
|
||||
},
|
||||
],
|
||||
branchId: bill.branchId,
|
||||
};
|
||||
// Create a new bill payment associated to the matched bill.
|
||||
const billPayment = await this.createPaymentMadeService.createBillPayment(
|
||||
tenantId,
|
||||
createPaymentMadeDTO,
|
||||
trx
|
||||
);
|
||||
// Link the create bill payment with matched transaction.
|
||||
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
|
||||
referenceType: 'BillPayment',
|
||||
referenceId: billPayment.id,
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { initialize } from 'objection';
|
||||
import { Knex } from 'knex';
|
||||
import { first } from 'lodash';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||
import {
|
||||
GetMatchedTransactionsFilter,
|
||||
IMatchTransactionDTO,
|
||||
MatchedTransactionPOJO,
|
||||
MatchedTransactionsPOJO,
|
||||
} from './types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||
import { CreatePaymentReceived } from '@/services/Sales/PaymentReceived/CreatePaymentReceived';
|
||||
import { IPaymentReceivedCreateDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
|
||||
@Inject()
|
||||
protected tenancy: HasTenancyService;
|
||||
protected transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
protected transformer: TransformerInjectable;
|
||||
protected createPaymentReceivedService: CreatePaymentReceived;
|
||||
|
||||
/**
|
||||
* Retrieves the matched transactions.
|
||||
@@ -78,4 +82,64 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
||||
new GetMatchedTransactionInvoicesTransformer()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
await super.createMatchedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionIds,
|
||||
matchTransactionDTO,
|
||||
trx
|
||||
);
|
||||
const { SaleInvoice, UncategorizedCashflowTransaction, MatchedBankTransaction } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
|
||||
const uncategorizedTransaction =
|
||||
await UncategorizedCashflowTransaction.query(trx)
|
||||
.findById(uncategorizedTransactionId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const invoice = await SaleInvoice.query(trx)
|
||||
.findById(matchTransactionDTO.referenceId)
|
||||
.throwIfNotFound();
|
||||
|
||||
const createPaymentReceivedDTO: IPaymentReceivedCreateDTO = {
|
||||
customerId: invoice.customerId,
|
||||
paymentDate: uncategorizedTransaction.date,
|
||||
amount: invoice.dueAmount,
|
||||
depositAccountId: uncategorizedTransaction.accountId,
|
||||
entries: [
|
||||
{
|
||||
index: 1,
|
||||
invoiceId: invoice.id,
|
||||
paymentAmount: invoice.dueAmount,
|
||||
},
|
||||
],
|
||||
branchId: invoice.branchId,
|
||||
};
|
||||
// Create a payment received associated to the matched invoice.
|
||||
const paymentReceived = await this.createPaymentReceivedService.createPaymentReceived(
|
||||
tenantId,
|
||||
createPaymentReceivedDTO,
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Link the create payment received with matched invoice transaction.
|
||||
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
|
||||
referenceType: 'PaymentReceive',
|
||||
referenceId: paymentReceived.id,
|
||||
}, trx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MatchedTransactionsPOJO,
|
||||
} from './types';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
|
||||
export abstract class GetMatchedTransactionsByType {
|
||||
@Inject()
|
||||
@@ -43,24 +44,28 @@ export abstract class GetMatchedTransactionsByType {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates the common matched transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} uncategorizedTransactionId
|
||||
* @param {Array<number>} uncategorizedTransactionIds
|
||||
* @param {IMatchTransactionDTO} matchTransactionDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async createMatchedTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
uncategorizedTransactionIds: Array<number>,
|
||||
matchTransactionDTO: IMatchTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
await MatchedBankTransaction.query(trx).insert({
|
||||
uncategorizedTransactionId,
|
||||
referenceType: matchTransactionDTO.referenceType,
|
||||
referenceId: matchTransactionDTO.referenceId,
|
||||
});
|
||||
await PromisePool.withConcurrency(2)
|
||||
.for(uncategorizedTransactionIds)
|
||||
.process(async (uncategorizedTransactionId) => {
|
||||
await MatchedBankTransaction.query(trx).insert({
|
||||
uncategorizedTransactionId,
|
||||
referenceType: matchTransactionDTO.referenceType,
|
||||
referenceId: matchTransactionDTO.referenceId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { GetMatchedTransactions } from './GetMatchedTransactions';
|
||||
import { MatchBankTransactions } from './MatchTransactions';
|
||||
import { UnmatchMatchedBankTransaction } from './UnmatchMatchedTransaction';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionsDTO } from './types';
|
||||
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
|
||||
|
||||
@Service()
|
||||
export class MatchBankTransactionsApplication {
|
||||
@@ -23,12 +23,12 @@ export class MatchBankTransactionsApplication {
|
||||
*/
|
||||
public getMatchedTransactions(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
uncategorizedTransactionsIds: Array<number>,
|
||||
filter: GetMatchedTransactionsFilter
|
||||
) {
|
||||
return this.getMatchedTransactionsService.getMatchedTransactions(
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
uncategorizedTransactionsIds,
|
||||
filter
|
||||
);
|
||||
}
|
||||
@@ -42,13 +42,13 @@ export class MatchBankTransactionsApplication {
|
||||
*/
|
||||
public matchTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
matchTransactionsDTO: IMatchTransactionsDTO
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: Array<IMatchTransactionDTO>
|
||||
): Promise<void> {
|
||||
return this.matchTransactionService.matchTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
matchTransactionsDTO
|
||||
matchedTransactions
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { castArray } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
ERRORS,
|
||||
IBankTransactionMatchedEventPayload,
|
||||
IBankTransactionMatchingEventPayload,
|
||||
IMatchTransactionsDTO,
|
||||
IMatchTransactionDTO,
|
||||
} from './types';
|
||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { sumMatchTranasctions } from './_utils';
|
||||
import {
|
||||
sumMatchTranasctions,
|
||||
sumUncategorizedTransactions,
|
||||
validateUncategorizedTransactionsExcluded,
|
||||
validateUncategorizedTransactionsNotMatched,
|
||||
} from './_utils';
|
||||
|
||||
@Service()
|
||||
export class MatchBankTransactions {
|
||||
@@ -39,27 +44,25 @@ export class MatchBankTransactions {
|
||||
*/
|
||||
async validate(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
matchTransactionsDTO: IMatchTransactionsDTO
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: Array<IMatchTransactionDTO>
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
const { matchedTransactions } = matchTransactionsDTO;
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
|
||||
// Validates the uncategorized transaction existance.
|
||||
const uncategorizedTransaction =
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query()
|
||||
.findById(uncategorizedTransactionId)
|
||||
.whereIn('id', uncategorizedTransactionIds)
|
||||
.withGraphFetched('matchedBankTransactions')
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validates the uncategorized transaction is not already matched.
|
||||
if (!isEmpty(uncategorizedTransaction.matchedBankTransactions)) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED);
|
||||
}
|
||||
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions);
|
||||
|
||||
// Validate the uncategorized transaction is not excluded.
|
||||
if (uncategorizedTransaction.excluded) {
|
||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION);
|
||||
}
|
||||
validateUncategorizedTransactionsExcluded(uncategorizedTransactions);
|
||||
|
||||
// Validates the given matched transaction.
|
||||
const validateMatchedTransaction = async (matchedTransaction) => {
|
||||
const getMatchedTransactionsService =
|
||||
@@ -94,9 +97,12 @@ export class MatchBankTransactions {
|
||||
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||
validatationResult.results
|
||||
);
|
||||
const totalUncategorizedTransactions = sumUncategorizedTransactions(
|
||||
uncategorizedTransactions
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
if (totalMatchedTranasctions !== uncategorizedTransaction.amount) {
|
||||
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||
}
|
||||
}
|
||||
@@ -109,23 +115,23 @@ export class MatchBankTransactions {
|
||||
*/
|
||||
public async matchTransaction(
|
||||
tenantId: number,
|
||||
uncategorizedTransactionId: number,
|
||||
matchTransactionsDTO: IMatchTransactionsDTO
|
||||
uncategorizedTransactionId: number | Array<number>,
|
||||
matchedTransactions: Array<IMatchTransactionDTO>
|
||||
): Promise<void> {
|
||||
const { matchedTransactions } = matchTransactionsDTO;
|
||||
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
|
||||
|
||||
// Validates the given matching transactions DTO.
|
||||
await this.validate(
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
matchTransactionsDTO
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransactions
|
||||
);
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers the event `onBankTransactionMatching`.
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
matchTransactionsDTO,
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransactions,
|
||||
trx,
|
||||
} as IBankTransactionMatchingEventPayload);
|
||||
|
||||
@@ -139,17 +145,16 @@ export class MatchBankTransactions {
|
||||
);
|
||||
await getMatchedTransactionsService.createMatchedTransaction(
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransaction,
|
||||
trx
|
||||
);
|
||||
});
|
||||
|
||||
// Triggers the event `onBankTransactionMatched`.
|
||||
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
matchTransactionsDTO,
|
||||
uncategorizedTransactionIds,
|
||||
matchedTransactions,
|
||||
trx,
|
||||
} as IBankTransactionMatchedEventPayload);
|
||||
});
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
|
||||
import { MatchedTransactionPOJO } from './types';
|
||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
export const sortClosestMatchTransactions = (
|
||||
uncategorizedTransaction: UncategorizedCashflowTransaction,
|
||||
amount: number,
|
||||
date: Date,
|
||||
matches: MatchedTransactionPOJO[]
|
||||
) => {
|
||||
return R.sortWith([
|
||||
// Sort by amount difference (closest to uncategorized transaction amount first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(match.amount - uncategorizedTransaction.amount)
|
||||
Math.abs(match.amount - amount)
|
||||
),
|
||||
// Sort by date difference (closest to uncategorized transaction date first)
|
||||
R.ascend((match: MatchedTransactionPOJO) =>
|
||||
Math.abs(
|
||||
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
|
||||
)
|
||||
Math.abs(moment(match.date).diff(moment(date), 'days'))
|
||||
),
|
||||
])(matches);
|
||||
};
|
||||
@@ -29,3 +30,36 @@ export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
export const sumUncategorizedTransactions = (
|
||||
uncategorizedTransactions: Array<any>
|
||||
) => {
|
||||
return sumBy(uncategorizedTransactions, 'amount');
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsNotMatched = (
|
||||
uncategorizedTransactions: any
|
||||
) => {
|
||||
const matchedTransactions = uncategorizedTransactions.filter(
|
||||
(trans) => !isEmpty(trans.matchedBankTransactions)
|
||||
);
|
||||
//
|
||||
if (matchedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
|
||||
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsExcluded = (
|
||||
uncategorizedTransactions: any
|
||||
) => {
|
||||
const excludedTransactions = uncategorizedTransactions.filter(
|
||||
(trans) => trans.excluded
|
||||
);
|
||||
if (excludedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
|
||||
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IBankTransactionUnmatchedEventPayload,
|
||||
} from '../types';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
|
||||
@Service()
|
||||
export class DecrementUncategorizedTransactionOnMatching {
|
||||
@@ -30,18 +31,24 @@ export class DecrementUncategorizedTransactionOnMatching {
|
||||
*/
|
||||
public async decrementUnCategorizedTransactionsOnMatching({
|
||||
tenantId,
|
||||
uncategorizedTransactionId,
|
||||
uncategorizedTransactionIds,
|
||||
trx,
|
||||
}: IBankTransactionMatchedEventPayload) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
|
||||
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||
uncategorizedTransactionId
|
||||
);
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query().whereIn(
|
||||
'id',
|
||||
uncategorizedTransactionIds
|
||||
);
|
||||
await PromisePool.withConcurrency(1)
|
||||
.for(uncategorizedTransactions)
|
||||
.process(async (transaction) => {
|
||||
await Account.query(trx)
|
||||
.findById(transaction.accountId)
|
||||
.decrement('uncategorizedTransactions', 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
IBillPaymentEventDeletedPayload,
|
||||
IPaymentReceiveDeletedPayload,
|
||||
IPaymentReceivedDeletedPayload,
|
||||
} from '@/interfaces';
|
||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||
import events from '@/subscribers/events';
|
||||
@@ -23,7 +23,7 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
||||
|
||||
/**
|
||||
* Validates the payment made transaction whether matched with bank transaction on deleting.
|
||||
* @param {IPaymentReceiveDeletedPayload}
|
||||
* @param {IPaymentReceivedDeletedPayload}
|
||||
*/
|
||||
public async validateMatchingOnPaymentMadeDeleting({
|
||||
tenantId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IPaymentReceiveDeletedPayload } from '@/interfaces';
|
||||
import { IPaymentReceivedDeletedPayload } from '@/interfaces';
|
||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@@ -20,13 +20,13 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
||||
|
||||
/**
|
||||
* Validates the payment received transaction whether matched with bank transaction on deleting.
|
||||
* @param {IPaymentReceiveDeletedPayload}
|
||||
* @param {IPaymentReceivedDeletedPayload}
|
||||
*/
|
||||
public async validateMatchingOnPaymentReceivedDeleting({
|
||||
tenantId,
|
||||
oldPaymentReceive,
|
||||
trx,
|
||||
}: IPaymentReceiveDeletedPayload) {
|
||||
}: IPaymentReceivedDeletedPayload) {
|
||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||
tenantId,
|
||||
'PaymentReceive',
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Knex } from 'knex';
|
||||
|
||||
export interface IBankTransactionMatchingEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
matchTransactionsDTO: IMatchTransactionsDTO;
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface IBankTransactionMatchedEventPayload {
|
||||
tenantId: number;
|
||||
uncategorizedTransactionId: number;
|
||||
matchTransactionsDTO: IMatchTransactionsDTO;
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IMatchTransactionDTO {
|
||||
}
|
||||
|
||||
export interface IMatchTransactionsDTO {
|
||||
uncategorizedTransactionIds: Array<number>;
|
||||
matchedTransactions: Array<IMatchTransactionDTO>;
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ export interface MatchedTransactionPOJO {
|
||||
export type MatchedTransactionsPOJO = {
|
||||
perfectMatches: Array<MatchedTransactionPOJO>;
|
||||
possibleMatches: Array<MatchedTransactionPOJO>;
|
||||
totalPending: number;
|
||||
};
|
||||
|
||||
export const ERRORS = {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Knex } from 'knex';
|
||||
import uniqid from 'uniqid';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction';
|
||||
|
||||
const CONCURRENCY_ASYNC = 10;
|
||||
|
||||
@@ -40,7 +41,7 @@ export class PlaidSyncDb {
|
||||
private cashflowApp: CashflowApplication;
|
||||
|
||||
@Inject()
|
||||
private deleteCashflowTransactionService: DeleteCashflowTransaction;
|
||||
private removePendingTransaction: RemovePendingUncategorizedTransaction;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
@@ -185,21 +186,22 @@ export class PlaidSyncDb {
|
||||
plaidTransactionsIds: string[],
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { CashflowTransaction } = this.tenancy.models(tenantId);
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
|
||||
'plaidTransactionId',
|
||||
plaidTransactionsIds
|
||||
);
|
||||
const cashflowTransactionsIds = cashflowTransactions.map(
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query(trx).whereIn(
|
||||
'plaidTransactionId',
|
||||
plaidTransactionsIds
|
||||
);
|
||||
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
|
||||
(trans) => trans.id
|
||||
);
|
||||
await bluebird.map(
|
||||
cashflowTransactionsIds,
|
||||
(transactionId: number) =>
|
||||
this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||
uncategorizedTransactionsIds,
|
||||
(uncategorizedTransactionId: number) =>
|
||||
this.removePendingTransaction.removePendingTransaction(
|
||||
tenantId,
|
||||
transactionId,
|
||||
uncategorizedTransactionId,
|
||||
trx
|
||||
),
|
||||
{ concurrency: CONCURRENCY_ASYNC }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
|
||||
import { PlaidSyncDb } from './PlaidSyncDB';
|
||||
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class PlaidUpdateTransactions {
|
||||
@@ -19,9 +19,9 @@ export class PlaidUpdateTransactions {
|
||||
|
||||
/**
|
||||
* Handles sync the Plaid item to Bigcaptial under UOW.
|
||||
* @param {number} tenantId
|
||||
* @param {number} plaidItemId
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} plaidItemId - Plaid item id.
|
||||
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
|
||||
*/
|
||||
public async updateTransactions(tenantId: number, plaidItemId: string) {
|
||||
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
|
||||
@@ -73,6 +73,12 @@ export class PlaidUpdateTransactions {
|
||||
item,
|
||||
trx
|
||||
);
|
||||
// Sync removed transactions.
|
||||
await this.plaidSync.syncRemoveTransactions(
|
||||
tenantId,
|
||||
removed?.map((r) => r.transaction_id),
|
||||
trx
|
||||
);
|
||||
// Sync bank account transactions.
|
||||
await this.plaidSync.syncAccountsTransactions(
|
||||
tenantId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user