Compare commits

..

2 Commits

Author SHA1 Message Date
allcontributors[bot]
6d5da42695 docs: update .all-contributorsrc [skip ci] 2024-07-30 09:13:59 +00:00
allcontributors[bot]
a298d2dd97 docs: update README.md [skip ci] 2024-07-30 09:13:58 +00:00
466 changed files with 2964 additions and 7440 deletions

View File

@@ -141,24 +141,6 @@
"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,

View File

@@ -128,8 +128,6 @@ 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>

View File

@@ -69,8 +69,9 @@
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"jsonwebtoken": "^8.5.1",
"knex": "^3.1.0",
"knex": "^0.95.15",
"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",

View File

@@ -103,20 +103,24 @@ export default class AccountsController extends BaseController {
check('name')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(),
.trim()
.escape(),
check('code')
.optional({ nullable: true })
.isLength({ min: 3, max: 6 })
.trim(),
.trim()
.escape(),
check('currency_code').optional(),
check('account_type')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(),
.trim()
.escape(),
check('description')
.optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT })
.trim(),
.trim()
.escape(),
check('parent_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
@@ -132,19 +136,23 @@ export default class AccountsController extends BaseController {
check('name')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(),
.trim()
.escape(),
check('code')
.optional({ nullable: true })
.isLength({ min: 3, max: 6 })
.trim(),
.trim()
.escape(),
check('account_type')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(),
.trim()
.escape(),
check('description')
.optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT })
.trim(),
.trim()
.escape(),
check('parent_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })

View File

@@ -250,12 +250,10 @@ 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 });

View File

@@ -90,23 +90,27 @@ 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 }),
];
}
@@ -146,7 +150,7 @@ export default class AuthenticationController extends BaseController {
* @returns {ValidationChain[]}
*/
private get sendResetPasswordSchema(): ValidationChain[] {
return [check('email').exists().isEmail().trim()];
return [check('email').exists().isEmail().trim().escape()];
}
/**
@@ -154,11 +158,7 @@ export default class AuthenticationController extends BaseController {
* @param {Request} req
* @param {Response} res
*/
private async login(
req: Request,
res: Response,
next: Function
): Promise<Response | null> {
private async login(req: Request, res: Response, next: Function): Response {
const userDTO: ILoginDTO = this.matchedBodyData(req);
try {

View File

@@ -1,10 +1,9 @@
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 {
@@ -14,9 +13,6 @@ export class BankAccountsController extends BaseController {
@Inject()
private bankAccountsApp: BankAccountsApplication;
@Inject()
private getPendingTransactionsService: GetPendingBankAccountTransactions;
/**
* Router constructor.
*/
@@ -24,33 +20,11 @@ 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;
}
@@ -82,32 +56,6 @@ 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
@@ -161,58 +109,4 @@ 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);
}
}
}

View File

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

View File

@@ -6,7 +6,6 @@ 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 {
@@ -30,10 +29,6 @@ export class BankingController extends BaseController {
'/bank_accounts',
Container.get(BankAccountsController).router()
);
router.use(
'/categorize',
Container.get(BankingUncategorizedController).router()
);
return router;
}
}

View File

@@ -33,25 +33,17 @@ export class BankingRulesController extends BaseController {
body('conditions.*.field').exists().isIn(['description', 'amount']),
body('conditions.*.comparator')
.exists()
.isIn([
'equals',
'equal',
'contains',
'not_contain',
'bigger',
'bigger_or_equal',
'smaller',
'smaller_or_equal',
])
.default('contain')
.trim(),
body('conditions.*.value').exists().trim(),
.isIn(['equals', 'contains', 'not_contain'])
.default('contain'),
body('conditions.*.value').exists(),
// 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 }),
];
}

View File

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

View File

@@ -42,11 +42,7 @@ 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)
);
@@ -181,7 +177,7 @@ export class ExcludeBankTransactionsController extends BaseController {
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
const filter = this.matchedBodyData(req);
try {
const data =

View File

@@ -1,6 +1,5 @@
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';
@@ -15,16 +14,7 @@ export class RecognizedTransactionsController extends BaseController {
router() {
const router = Router();
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('/', this.getRecognizedTransactions.bind(this));
router.get(
'/transactions/:uncategorizedTransactionId',
this.getRecognizedTransaction.bind(this)

View File

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

View File

@@ -1,15 +1,10 @@
import { Service, Inject } from 'typedi';
import { ValidationChain, body, check, param, query } from 'express-validator';
import { ValidationChain, check, param, query } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { omit } from 'lodash';
import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import {
AbilitySubject,
CashflowAction,
ICategorizeCashflowTransactioDTO,
} from '@/interfaces';
import { AbilitySubject, CashflowAction } from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
@@ -43,23 +38,13 @@ 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/categorize',
'/transactions/:id/categorize',
this.categorizeCashflowTransactionValidationSchema,
this.validationResult,
this.categorizeCashflowTransaction,
@@ -104,7 +89,6 @@ 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(),
@@ -122,11 +106,12 @@ export default class NewCashflowTransactionController extends BaseController {
public get newTransactionValidationSchema() {
return [
check('date').exists().isISO8601().toDate(),
check('reference_no').optional({ nullable: true }).trim(),
check('reference_no').optional({ nullable: true }).trim().escape(),
check('description')
.optional({ nullable: true })
.isLength({ min: 3 })
.trim(),
.trim()
.escape(),
check('transaction_type').exists(),
check('amount').exists().isFloat().toFloat(),
@@ -176,7 +161,7 @@ export default class NewCashflowTransactionController extends BaseController {
* @param {NextFunction} next
*/
private revertCategorizedCashflowTransaction = async (
req: Request<{ id: number }>,
req: Request,
res: Response,
next: NextFunction
) => {
@@ -194,34 +179,6 @@ export default class NewCashflowTransactionController extends BaseController {
}
};
/**
* Uncategorize the given transactions in bulk.
* @param {Request<{}>} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
private uncategorizeBulkTransactions = async (
req: Request<{}>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req);
try {
await this.cashflowApplication.uncategorizeTransactions(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({
message: 'The given transactions have been uncategorized successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Categorize the cashflow transaction.
* @param {Request} req
@@ -234,19 +191,14 @@ export default class NewCashflowTransactionController extends BaseController {
next: NextFunction
) => {
const { tenantId } = req;
const matchedObject = this.matchedBodyData(req);
const categorizeDTO = omit(matchedObject, [
'uncategorizedTransactionIds',
]) as ICategorizeCashflowTransactioDTO;
const uncategorizedTransactionIds =
matchedObject.uncategorizedTransactionIds;
const { id: cashflowTransactionId } = req.params;
const cashflowTransaction = this.matchedBodyData(req);
try {
await this.cashflowApplication.categorizeTransaction(
tenantId,
uncategorizedTransactionIds,
categorizeDTO
cashflowTransactionId,
cashflowTransaction
);
return res.status(200).send({
message: 'The cashflow transaction has been created successfully.',
@@ -317,7 +269,7 @@ export default class NewCashflowTransactionController extends BaseController {
* @param {NextFunction} next
*/
public getUncategorizedCashflowTransactions = async (
req: Request<{ id: number }>,
req: Request,
res: Response,
next: NextFunction
) => {

View File

@@ -56,7 +56,7 @@ export default class ContactsController extends BaseController {
*/
get autocompleteQuerySchema() {
return [
query('column_sort_by').optional().trim(),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
@@ -122,27 +122,32 @@ 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')
@@ -160,101 +165,120 @@ 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(),
];

View File

@@ -106,7 +106,11 @@ export default class CustomersController extends ContactsController {
*/
get customerDTOSchema() {
return [
check('customer_type').exists().isIn(['business', 'individual']).trim(),
check('customer_type')
.exists()
.isIn(['business', 'individual'])
.trim()
.escape(),
];
}
@@ -119,6 +123,7 @@ export default class CustomersController extends ContactsController {
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: 3 }),
];
}
@@ -128,7 +133,7 @@ export default class CustomersController extends ContactsController {
*/
get validateListQuerySchema() {
return [
query('column_sort_by').optional().trim(),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),

View File

@@ -106,6 +106,7 @@ export default class VendorsController extends ContactsController {
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ min: 3, max: 3 }),
];
}

View File

@@ -67,7 +67,7 @@ export default class CurrenciesController extends BaseController {
}
get currencyParamSchema(): ValidationChain[] {
return [param('currency_code').exists().trim()];
return [param('currency_code').exists().trim().escape()];
}
get listSchema(): ValidationChain[] {
@@ -187,13 +187,11 @@ 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') {

View File

@@ -89,6 +89,7 @@ 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')
@@ -122,6 +123,7 @@ 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')
@@ -142,6 +144,7 @@ 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')
@@ -176,6 +179,7 @@ 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')

View File

@@ -1,7 +1,9 @@
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')
@@ -17,7 +19,8 @@ export default class BaseFinancialReportController extends BaseController {
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim(),
.trim()
.escape(),
];
}
}
}

View File

@@ -51,7 +51,8 @@ export default class InventoryDetailsController extends BaseController {
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim(),
.trim()
.escape(),
query('from_date').optional(),
query('to_date').optional(),

View File

@@ -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(),
query('transaction_type').optional().trim().escape(),
query('transaction_id').optional().isInt().toInt(),
oneOf(
[

View File

@@ -40,7 +40,8 @@ export default class TransactionsByReferenceController extends BaseController {
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim(),
.trim()
.escape(),
];
}

View File

@@ -86,7 +86,7 @@ export default class InventoryAdjustmentsController extends BaseController {
*/
get validateListQuerySchema() {
return [
query('column_sort_by').optional().trim(),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),

View File

@@ -25,7 +25,7 @@ export default class InviteUsersController extends BaseController {
router.post(
'/send',
[
body('email').exists().trim(),
body('email').exists().trim().escape(),
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()],
[param('token').exists().trim().escape()],
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(),
check('last_name').exists().trim(),
check('password').exists().trim().isLength({ min: 5 }),
param('token').exists().trim(),
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(),
];
}

View File

@@ -73,11 +73,13 @@ 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 })
@@ -99,8 +101,9 @@ export default class ItemsCategoriesController extends BaseController {
*/
get categoriesListValidationSchema() {
return [
query('column_sort_by').optional().trim(),
query('sort_order').optional().trim().isIn(['desc', 'asc']),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
];
}
@@ -204,12 +207,14 @@ 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),

View File

@@ -96,11 +96,13 @@ 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(),
@@ -139,11 +141,13 @@ 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')
@@ -158,6 +162,7 @@ export default class ItemsController extends BaseController {
.optional()
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('active').optional().isBoolean().toBoolean(),
@@ -179,7 +184,7 @@ export default class ItemsController extends BaseController {
*/
private get validateListQuerySchema() {
return [
query('column_sort_by').optional().trim(),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),

View File

@@ -94,21 +94,25 @@ 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(),
@@ -159,7 +163,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(),
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),

View File

@@ -61,14 +61,15 @@ export default class MediaController extends BaseController {
get uploadValidationSchema() {
return [
check('model_name').optional().trim(),
check('model_id').optional().isNumeric(),
// check('attachment'),
check('model_name').optional().trim().escape(),
check('model_id').optional().isNumeric().toInt(),
];
}
get linkValidationSchema() {
return [
check('model_name').exists().trim(),
check('model_name').exists().trim().escape(),
check('model_id').exists().isNumeric().toInt(),
]
}

View File

@@ -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(),
check('industry').optional({ nullable: true }).isString().trim().escape(),
check('location').exists().isString().isISO31661Alpha2(),
check('base_currency').exists().isISO4217(),
check('timezone').exists().isIn(moment.tz.names()),
@@ -87,7 +87,11 @@ export default class OrganizationController extends BaseController {
private get updateOrganizationValidationSchema(): ValidationChain[] {
return [
...this.commonOrganizationValidationSchema,
check('tax_number').optional({ nullable: true }).isString().trim(),
check('tax_number')
.optional({ nullable: true })
.isString()
.trim()
.escape(),
];
}

View File

@@ -100,8 +100,8 @@ export default class BillsController extends BaseController {
*/
private get billValidationSchema() {
return [
check('bill_number').exists().trim(),
check('reference_no').optional().trim(),
check('bill_number').exists().trim().escape(),
check('reference_no').optional().trim().escape(),
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(),
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),
@@ -126,7 +126,10 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.landed_cost')
.optional({ nullable: true })
.isBoolean()
@@ -138,6 +141,7 @@ export default class BillsController extends BaseController {
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
.escape()
.isString(),
check('entries.*.tax_rate_id')
.optional({ nullable: true })
@@ -154,8 +158,8 @@ export default class BillsController extends BaseController {
*/
private get billEditValidationSchema() {
return [
check('bill_number').optional().trim(),
check('reference_no').optional().trim(),
check('bill_number').optional().trim().escape(),
check('reference_no').optional().trim().escape(),
check('bill_date').exists().isISO8601(),
check('due_date').optional().isISO8601(),
@@ -166,7 +170,7 @@ export default class BillsController extends BaseController {
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
check('note').optional().trim(),
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }),
@@ -180,7 +184,10 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.landed_cost')
.optional({ nullable: true })
.isBoolean()
@@ -215,8 +222,8 @@ export default class BillsController extends BaseController {
private get dueBillsListingValidationSchema() {
return [
query('vendor_id').optional().trim(),
query('payment_made_id').optional().trim(),
query('vendor_id').optional().trim().escape(),
query('payment_made_id').optional().trim().escape(),
];
}

View File

@@ -113,10 +113,10 @@ export default class BillsPayments extends BaseController {
check('amount').exists().isNumeric().toFloat(),
check('payment_account_id').exists().isNumeric().toInt(),
check('payment_number').optional({ nullable: true }).trim(),
check('payment_number').optional({ nullable: true }).trim().escape(),
check('payment_date').exists(),
check('statement').optional().trim(),
check('reference').optional().trim(),
check('statement').optional().trim().escape(),
check('reference').optional().trim().escape(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('entries').exists().isArray(),

View File

@@ -156,10 +156,13 @@ 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(),
check('reference_no').optional().trim(),
check('vendor_credit_number')
.optional({ nullable: true })
.trim()
.escape(),
check('reference_no').optional().trim().escape(),
check('vendor_credit_date').exists().isISO8601().toDate(),
check('note').optional().trim(),
check('note').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
@@ -175,7 +178,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
@@ -196,10 +202,13 @@ 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(),
check('reference_no').optional().trim(),
check('vendor_credit_number')
.optional({ nullable: true })
.trim()
.escape(),
check('reference_no').optional().trim().escape(),
check('vendor_credit_date').exists().isISO8601().toDate(),
check('note').optional().trim(),
check('note').optional().trim().escape(),
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
@@ -214,7 +223,10 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()

View File

@@ -18,7 +18,9 @@ export default class ResourceController extends BaseController {
router.get(
'/:resource_model/meta',
[param('resource_model').exists().trim()],
[
param('resource_model').exists().trim().escape()
],
this.asyncMiddleware(this.resourceMeta.bind(this)),
this.handleServiceErrors
);
@@ -46,7 +48,9 @@ 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);

View File

@@ -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(),
check('note').optional().trim(),
check('terms_conditions').optional().trim(),
check('credit_note_number').optional({ nullable: true }).trim().escape(),
check('note').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(),
check('open').default(false).isBoolean().toBoolean(),
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
@@ -228,7 +228,10 @@ export default class PaymentReceivesController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.description').optional({ nullable: true }).trim(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()

View File

@@ -9,9 +9,9 @@ import {
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import PaymentsReceivedPages from '@/services/Sales/PaymentReceived/PaymentsReceivedPages';
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceived/PaymentReceivedApplication';
import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages';
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 PaymentsReceivedPages: PaymentsReceivedPages;
private PaymentReceivesPages: PaymentReceivesPages;
@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(),
check('statement').optional().trim(),
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
check('statement').optional().trim().escape(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
@@ -176,6 +176,7 @@ 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(),
@@ -229,7 +230,7 @@ export default class PaymentReceivesController extends BaseController {
try {
const storedPaymentReceive =
await this.paymentReceiveApplication.createPaymentReceived(
await this.paymentReceiveApplication.createPaymentReceive(
tenantId,
paymentReceive,
user
@@ -376,7 +377,7 @@ export default class PaymentReceivesController extends BaseController {
const { customerId } = this.matchedQueryData(req);
try {
const entries = await this.PaymentsReceivedPages.getNewPageEntries(
const entries = await this.PaymentReceivesPages.getNewPageEntries(
tenantId,
customerId
);
@@ -404,7 +405,7 @@ export default class PaymentReceivesController extends BaseController {
try {
const { paymentReceive, entries } =
await this.PaymentsReceivedPages.getPaymentReceiveEditPage(
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user

View File

@@ -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(),
check('estimate_number').optional().trim().escape(),
check('delivered').default(false).isBoolean().toBoolean(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -170,7 +170,8 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
.trim()
.escape(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
@@ -180,9 +181,9 @@ export default class SalesEstimatesController extends BaseController {
.isNumeric()
.toInt(),
check('note').optional().trim(),
check('terms_conditions').optional().trim(),
check('send_to_email').optional().trim(),
check('note').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(),
check('send_to_email').optional().trim().escape(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),

View File

@@ -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(),
check('reference_no').optional().trim(),
check('invoice_no').optional().trim().escape(),
check('reference_no').optional().trim().escape(),
check('delivered').default(false).isBoolean().toBoolean(),
check('invoice_message').optional().trim(),
check('terms_conditions').optional().trim(),
check('invoice_message').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -226,10 +226,12 @@ export default class SaleInvoicesController extends BaseController {
.toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
.trim()
.escape(),
check('entries.*.tax_code')
.optional({ nullable: true })
.trim()
.escape()
.isString(),
check('entries.*.tax_rate_id')
.optional({ nullable: true })

View File

@@ -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(),
check('reference_no').optional().trim(),
check('receipt_number').optional().trim().escape(),
check('reference_no').optional().trim().escape(),
check('closed').default(false).isBoolean().toBoolean(),
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
@@ -150,13 +150,14 @@ export default class SalesReceiptsController extends BaseController {
.toInt(),
check('entries.*.description')
.optional({ nullable: true })
.trim(),
.trim()
.escape(),
check('entries.*.warehouse_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('receipt_message').optional().trim(),
check('statement').optional().trim(),
check('receipt_message').optional().trim().escape(),
check('statement').optional().trim().escape(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];

View File

@@ -52,7 +52,10 @@ export default class SettingsController extends BaseController {
* Retrieve the application options from the storage.
*/
private get getSettingsSchema() {
return [query('key').optional().trim(), query('group').optional().trim()];
return [
query('key').optional().trim().escape(),
query('group').optional().trim().escape(),
];
}
/**

View File

@@ -8,7 +8,6 @@ import SubscriptionService from '@/services/Subscription/SubscriptionService';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '../BaseController';
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
import { SubscriptionApplication } from '@/services/Subscription/SubscriptionApplication';
@Service()
export class SubscriptionController extends BaseController {
@@ -18,9 +17,6 @@ export class SubscriptionController extends BaseController {
@Inject()
private lemonSqueezyService: LemonSqueezyService;
@Inject()
private subscriptionApp: SubscriptionApplication;
/**
* Router constructor.
*/
@@ -37,14 +33,6 @@ 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;
@@ -97,84 +85,4 @@ 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);
}
}
}

View File

@@ -32,7 +32,7 @@ export default class ViewsController extends BaseController {
* Custom views list validation schema.
*/
get viewsListSchemaValidation() {
return [param('resource_model').exists().trim()];
return [param('resource_model').exists().trim().escape()];
}
/**

View File

@@ -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 || 'bigcapital_tenant_',
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX,
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,

View File

@@ -1,18 +0,0 @@
// This migration changes the precision of the tax_amount_withheld column in the bills and sales_invoices tables from 8, 2 to 13, 2.
// This migration is necessary to allow tax_amount_withheld filed to store values bigger than 999,999.99.
exports.up = function(knex) {
return knex.schema.alterTable('bills', function (table) {
table.decimal('tax_amount_withheld', 13, 2).alter();
}).alterTable('sales_invoices', function (table) {
table.decimal('tax_amount_withheld', 13, 2).alter();
});
};
exports.down = function(knex) {
return knex.schema.alterTable('bills', function (table) {
table.decimal('tax_amount_withheld', 8, 2).alter();
}).alterTable('sales_invoices', function (table) {
table.decimal('tax_amount_withheld', 8, 2).alter();
});
};

View File

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

View File

@@ -1,13 +0,0 @@
exports.up = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.boolean('pending').defaultTo(false);
table.string('pending_plaid_transaction_id').nullable();
});
};
exports.down = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.dropColumn('pending');
table.dropColumn('pending_plaid_transaction_id');
});
};

View File

@@ -236,7 +236,6 @@ export interface ICashflowTransactionSchema {
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
export interface ICategorizeCashflowTransactioDTO {
date: Date;
creditAccountId: number;
referenceNo: string;
transactionNumber: string;
@@ -268,8 +267,6 @@ export interface CreateUncategorizedTransactionDTO {
description?: string;
referenceNo?: string | null;
plaidTransactionId?: string | null;
pending?: boolean;
pendingPlaidTransactionId?: string | null;
batch?: string;
}
@@ -285,17 +282,3 @@ 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;
}

View File

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

View File

@@ -30,7 +30,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
currencyCode: string;
note?: string;
transactionTypeFormatted: string;
transactionType?: string;
transactionNumber: string;
referenceId?: number;

View File

@@ -8,7 +8,7 @@ import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice';
import { AttachmentLinkDTO } from './Attachments';
export interface IPaymentReceived {
export interface IPaymentReceive {
id?: number;
customerId: number;
paymentDate: Date;
@@ -19,14 +19,14 @@ export interface IPaymentReceived {
depositAccountId: number;
paymentReceiveNo: string;
statement: string;
entries: IPaymentReceivedEntry[];
entries: IPaymentReceiveEntry[];
userId: number;
createdAt: Date;
updatedAt: Date;
localAmount?: number;
branchId?: number;
}
export interface IPaymentReceivedCreateDTO {
export interface IPaymentReceiveCreateDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -35,13 +35,13 @@ export interface IPaymentReceivedCreateDTO {
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceivedEntryDTO[];
entries: IPaymentReceiveEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceivedEditDTO {
export interface IPaymentReceiveEditDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -50,12 +50,12 @@ export interface IPaymentReceivedEditDTO {
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceivedEntryDTO[];
entries: IPaymentReceiveEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceivedEntry {
export interface IPaymentReceiveEntry {
id?: number;
paymentReceiveId: number;
invoiceId: number;
@@ -64,15 +64,15 @@ export interface IPaymentReceivedEntry {
invoice?: ISaleInvoice;
}
export interface IPaymentReceivedEntryDTO {
export interface IPaymentReceiveEntryDTO {
id?: number;
index: number;
paymentReceiveId?: number;
paymentReceiveId: number;
invoiceId: number;
paymentAmount: number;
}
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
}
@@ -88,65 +88,65 @@ export interface IPaymentReceivePageEntry {
date: Date | string;
}
export interface IPaymentReceivedEditPage {
paymentReceive: IPaymentReceived;
export interface IPaymentReceiveEditPage {
paymentReceive: IPaymentReceive;
entries: IPaymentReceivePageEntry[];
}
export interface IPaymentsReceivedService {
export interface IPaymentsReceiveService {
validateCustomerHasNoPayments(
tenantId: number,
customerId: number
): Promise<void>;
}
export interface IPaymentReceivedSmsDetails {
export interface IPaymentReceiveSmsDetails {
customerName: string;
customerPhoneNumber: string;
smsMessage: string;
}
export interface IPaymentReceivedCreatingPayload {
export interface IPaymentReceiveCreatingPayload {
tenantId: number;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
paymentReceiveDTO: IPaymentReceiveCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedCreatedPayload {
export interface IPaymentReceiveCreatedPayload {
tenantId: number;
paymentReceive: IPaymentReceived;
paymentReceive: IPaymentReceive;
paymentReceiveId: number;
authorizedUser: ISystemUser;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
paymentReceiveDTO: IPaymentReceiveCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedEditedPayload {
export interface IPaymentReceiveEditedPayload {
tenantId: number;
paymentReceiveId: number;
paymentReceive: IPaymentReceived;
oldPaymentReceive: IPaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
paymentReceive: IPaymentReceive;
oldPaymentReceive: IPaymentReceive;
paymentReceiveDTO: IPaymentReceiveEditDTO;
authorizedUser: ISystemUser;
trx: Knex.Transaction;
}
export interface IPaymentReceivedEditingPayload {
export interface IPaymentReceiveEditingPayload {
tenantId: number;
oldPaymentReceive: IPaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
oldPaymentReceive: IPaymentReceive;
paymentReceiveDTO: IPaymentReceiveEditDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceivedDeletingPayload {
export interface IPaymentReceiveDeletingPayload {
tenantId: number;
oldPaymentReceive: IPaymentReceived;
oldPaymentReceive: IPaymentReceive;
trx: Knex.Transaction;
}
export interface IPaymentReceivedDeletedPayload {
export interface IPaymentReceiveDeletedPayload {
tenantId: number;
paymentReceiveId: number;
oldPaymentReceive: IPaymentReceived;
oldPaymentReceive: IPaymentReceive;
authorizedUser: ISystemUser;
trx: Knex.Transaction;
}

View File

@@ -51,4 +51,5 @@ export interface ISystemService {
cache();
repositories();
knex();
dbManager();
}

View File

@@ -21,7 +21,7 @@ export default class ComputeItemCostJob {
agenda.define(
'compute-item-cost',
{ priority: 'high', concurrency: 20 },
{ priority: 'high', concurrency: 1 },
this.handler.bind(this)
);
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));

View File

@@ -8,7 +8,7 @@ export default class OrganizationSetupJob {
constructor(agenda) {
agenda.define(
'organization-setup',
{ priority: 'high', concurrency: 20 },
{ priority: 'high', concurrency: 1 },
this.handler
);
}

View File

@@ -15,7 +15,7 @@ export default class WriteInvoicesJournalEntries {
agenda.define(
eventName,
{ priority: 'normal', concurrency: 20 },
{ priority: 'normal', concurrency: 1 },
this.handler.bind(this)
);
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));

View File

@@ -0,0 +1,7 @@
import knexManager from 'knex-db-manager';
import { systemKnexConfig, systemDbManager } from 'config/knexConfig';
export default () => knexManager.databaseManagerFactory({
knex: systemKnexConfig,
dbManager: systemDbManager,
});

View File

@@ -3,6 +3,7 @@ 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';
@@ -15,6 +16,7 @@ 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);
@@ -22,6 +24,7 @@ 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.'
);

View File

@@ -115,7 +115,6 @@ 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();
@@ -278,7 +277,6 @@ export const susbcribers = () => {
// Plaid
RecognizeSyncedBankTranasctions,
DisconnectPlaidItemOnAccountDeleted,
DeleteUncategorizedTransactionsOnAccountDeleting,
// Loops
LoopsEventsSubscriber

View File

@@ -34,4 +34,4 @@
// import 'services/Sales/SaleInvoiceWriteoffSubscriber';
// import 'subscribers/SaleInvoices/SendSmsNotificationToCustomer';
// import 'subscribers/SaleReceipt/SendNotificationToCustomer';
// import 'services/Sales/PaymentReceived/PaymentReceiveSmsSubscriber';
// import 'services/Sales/PaymentReceives/PaymentReceiveSmsSubscriber';

View File

@@ -9,13 +9,11 @@ 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 { PaymentReceivedMailNotificationJob } from '@/services/Sales/PaymentReceived/PaymentReceivedMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
import { ReregonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob';
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob';
import { RevertRegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob';
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -28,13 +26,11 @@ export default ({ agenda }: { agenda: Agenda }) => {
new SendSaleInvoiceReminderMailJob(agenda);
new SendSaleEstimateMailJob(agenda);
new SaleReceiptMailNotificationJob(agenda);
new PaymentReceivedMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(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', {});

View File

@@ -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/PaymentReceived/constants';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import ModelSearchable from './ModelSearchable';
export default class BillPayment extends mixin(TenantModel, [

View File

@@ -1,5 +1,4 @@
import { Model } from 'objection';
import { castArray, omit, pick } from 'lodash';
import { isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
@@ -17,15 +16,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
});
}
queryAndThrowIfHasRelations = ({
type,
message,
excludeRelations = [],
includedRelations = [],
}) => {
const _excludeRelations = castArray(excludeRelations);
const _includedRelations = castArray(includedRelations);
queryAndThrowIfHasRelations = ({ type, message }) => {
const model = this.modelClass();
const modelRelations = Object.keys(model.relationMappings).filter(
(relation) =>
@@ -34,20 +25,9 @@ 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 = filteredRelations.filter(
const nonEmptyRelations = relations.filter(
(relation) => !isEmpty(model[relation])
);
if (nonEmptyRelations.length > 0) {
@@ -56,7 +36,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
return model;
});
return this.onBuild((query) => {
filteredRelations.forEach((relation) => {
relations.forEach((relation) => {
query.withGraphFetched(`${relation}(selectId)`).modifiers({
selectId(builder) {
builder.select('id');

View File

@@ -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/PaymentReceived/constants';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import ModelSearchable from './ModelSearchable';
export default class PaymentReceive extends mixin(TenantModel, [

View File

@@ -1,8 +1,6 @@
import TenantModel from 'models/TenantModel';
export default class PlaidItem extends TenantModel {
pausedAt: Date;
/**
* Table name.
*/
@@ -23,19 +21,4 @@ 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;
}
}

View File

@@ -1,69 +0,0 @@
export default {
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
importable: true,
print: {
pageTitle: 'Tax Rates',
},
columns: {
name: {
name: 'Tax Rate Name',
type: 'text',
accessor: 'name',
},
code: {
name: 'Code',
type: 'text',
accessor: 'code',
},
rate: {
name: 'Rate',
type: 'text',
},
description: {
name: 'Description',
type: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
type: 'boolean',
},
active: {
name: 'Active',
type: 'boolean',
},
},
field: {},
fields2: {
name: {
name: 'Tax name',
fieldType: 'name',
required: true,
},
code: {
name: 'Code',
fieldType: 'code',
required: true,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
fieldType: 'boolean',
},
active: {
name: 'Active',
fieldType: 'boolean',
},
},
};

View File

@@ -2,13 +2,8 @@ import { mixin, Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSearchable from './ModelSearchable';
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
import TaxRateMeta from './TaxRate.settings';
import ModelSetting from './ModelSetting';
export default class TaxRate extends mixin(TenantModel, [
ModelSetting,
ModelSearchable,
]) {
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
/**
* Table name
*/
@@ -30,13 +25,6 @@ export default class TaxRate extends mixin(TenantModel, [
return ['createdAt', 'updatedAt'];
}
/**
* Retrieves the tax rate meta.
*/
static get meta() {
return TaxRateMeta;
}
/**
* Virtual attributes.
*/

View File

@@ -1,5 +1,6 @@
/* eslint-disable global-require */
import { Model, mixin } from 'objection';
import * as R from 'ramda';
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSettings from './ModelSetting';
import Account from './Account';
@@ -19,8 +20,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
description!: string;
plaidTransactionId!: string;
recognizedTransactionId!: number;
excludedAt: Date;
pending: boolean;
/**
* Table name.
@@ -32,7 +31,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
/**
* Timestamps columns.
*/
get timestamps() {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
@@ -46,8 +45,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
'isDepositTransaction',
'isWithdrawalTransaction',
'isRecognized',
'isExcluded',
'isPending',
];
}
@@ -92,22 +89,6 @@ 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.
*/
@@ -152,20 +133,6 @@ 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);
},
};
}

View File

@@ -249,7 +249,6 @@ export default class Ledger implements ILedger {
transactionId: entry.referenceId,
transactionType: entry.referenceType,
transactionSubType: entry.transactionType,
transactionNumber: entry.transactionNumber,
referenceNumber: entry.referenceNumber,
@@ -263,8 +262,6 @@ export default class Ledger implements ILedger {
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
note: entry.note,
};
}

View File

@@ -18,18 +18,9 @@ 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 -
@@ -75,15 +66,6 @@ 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[]}

View File

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

View File

@@ -73,7 +73,6 @@ export class DeleteAccount {
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
excludeRelations: ['uncategorizedTransactions', 'plaidItem']
});
// Authorize before delete account.
await this.authorize(tenantId, accountId, oldAccount);

View File

@@ -25,10 +25,7 @@ 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)
.withGraphFetched('plaidItem')
.throwIfNotFound();
const account = await Account.query().findById(accountId).throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();

View File

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

View File

@@ -1,34 +1,20 @@
import { Inject, Service } from 'typedi';
import { Service } from 'typedi';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { 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 with the original filename.
* @param {number} tenantId
* Retrieves the presigned url of the given attachment key.
* @param {string} key
* @returns {string}
* @returns {Promise<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}"`;
}
async getPresignedUrl(key: string) {
const command = new GetObjectCommand({
Bucket: config.s3.bucket,
Key: key,
ResponseContentDisposition,
});
const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

View File

@@ -1,10 +1,10 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
import {
IPaymentReceivedCreatedPayload,
IPaymentReceivedCreatingPayload,
IPaymentReceivedDeletingPayload,
IPaymentReceivedEditedPayload,
IPaymentReceiveCreatedPayload,
IPaymentReceiveCreatingPayload,
IPaymentReceiveDeletingPayload,
IPaymentReceiveEditedPayload,
} 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 {IPaymentReceivedCreatingPayload}
* @param {IPaymentReceiveCreatingPayload}
* @returns {Promise<void>}
*/
private async validateAttachmentsOnPaymentCreate({
paymentReceiveDTO,
tenantId,
}: IPaymentReceivedCreatingPayload): Promise<void> {
}: IPaymentReceiveCreatingPayload): Promise<void> {
if (isEmpty(paymentReceiveDTO.attachments)) {
return;
}
@@ -67,7 +67,7 @@ export class AttachmentsOnPaymentsReceived {
/**
* Handles linking the attachments of the created payment.
* @param {IPaymentReceivedCreatedPayload}
* @param {IPaymentReceiveCreatedPayload}
* @returns {Promise<void>}
*/
private async handleAttachmentsOnPaymentCreated({
@@ -75,7 +75,7 @@ export class AttachmentsOnPaymentsReceived {
paymentReceiveDTO,
paymentReceive,
trx,
}: IPaymentReceivedCreatedPayload): Promise<void> {
}: IPaymentReceiveCreatedPayload): 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 {IPaymentReceivedEditedPayload}
* @param {IPaymentReceiveEditedPayload}
*/
private async handleUnlinkUnpresentedKeysOnPaymentEdited({
tenantId,
paymentReceiveDTO,
oldPaymentReceive,
trx,
}: IPaymentReceivedEditedPayload) {
}: IPaymentReceiveEditedPayload) {
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 {IPaymentReceivedEditedPayload}
* @param {IPaymentReceiveEditedPayload}
* @returns {Promise<void>}
*/
private async handleLinkPresentedKeysOnPaymentEdited({
@@ -122,7 +122,7 @@ export class AttachmentsOnPaymentsReceived {
paymentReceiveDTO,
oldPaymentReceive,
trx,
}: IPaymentReceivedEditedPayload) {
}: IPaymentReceiveEditedPayload) {
if (isEmpty(paymentReceiveDTO.attachments)) return;
const keys = paymentReceiveDTO.attachments?.map(
@@ -146,7 +146,7 @@ export class AttachmentsOnPaymentsReceived {
tenantId,
oldPaymentReceive,
trx,
}: IPaymentReceivedDeletingPayload) {
}: IPaymentReceiveDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'PaymentReceive',

View File

@@ -1,8 +1,6 @@
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 {
@@ -12,12 +10,6 @@ export class BankAccountsApplication {
@Inject()
private refreshBankAccountService: RefreshBankAccountService;
@Inject()
private resumeBankAccountFeedsService: ResumeBankAccountFeeds;
@Inject()
private pauseBankAccountFeedsService: PauseBankAccountFeeds;
/**
* Disconnects the given bank account.
* @param {number} tenantId
@@ -35,7 +27,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(
@@ -43,30 +35,4 @@ 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
);
}
}

View File

@@ -1,7 +1,6 @@
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 {
@@ -32,85 +31,46 @@ 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) => {
commonQuery(q);
// Include just the given account.
q.where('accountId', bankAccountId);
// Only the not excluded.
q.modify('notExcluded');
// Only the not categorized.
q.modify('notCategorized');
// Only the not matched bank transactions.
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();
});
// 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();
});
// 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();
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,
};
}
}

View File

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

View File

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

View File

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

View File

@@ -51,7 +51,6 @@ export class DisconnectPlaidItemOnAccountDeleted {
.findOne('plaidItemId', oldAccount.plaidItemId)
.delete();
// Remove Plaid item once the transaction resolve.
if (oldPlaidItem) {
const plaidInstance = PlaidClientWrapper.getClient();

View File

@@ -14,6 +14,4 @@ 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',
};

View File

@@ -1,11 +1,7 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import {
validateTransactionNotCategorized,
validateTransactionNotExcluded,
} from './utils';
import { Inject, Service } from 'typedi';
import { validateTransactionNotCategorized } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
@@ -41,13 +37,9 @@ 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: Knex.Transaction) => {
return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
tenantId,
uncategorizedTransactionId,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import { castArray, uniq } from 'lodash';
import { castArray } from 'lodash';
import { ExcludeBankTransaction } from './ExcludeBankTransaction';
@Service()
@@ -18,7 +18,7 @@ export class ExcludeBankTransactions {
tenantId: number,
bankTransactionIds: Array<number> | number
) {
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
const _bankTransactionIds = castArray(bankTransactionIds);
await PromisePool.withConcurrency(1)
.for(_bankTransactionIds)

View File

@@ -1,11 +1,7 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import {
validateTransactionNotCategorized,
validateTransactionShouldBeExcluded,
} from './utils';
import { Inject, Service } from 'typedi';
import { validateTransactionNotCategorized } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
@@ -41,13 +37,9 @@ 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: Knex.Transaction) => {
return this.uow.withTransaction(tenantId, async (trx) => {
await this.eventPublisher.emitAsync(
events.bankTransactions.onUnexcluding,
{

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import { UnexcludeBankTransaction } from './UnexcludeBankTransaction';
import { castArray, uniq } from 'lodash';
import { castArray } from 'lodash';
@Service()
export class UnexcludeBankTransactions {
@@ -17,7 +17,7 @@ export class UnexcludeBankTransactions {
tenantId: number,
bankTransactionIds: Array<number> | number
) {
const _bankTransactionIds = uniq(castArray(bankTransactionIds));
const _bankTransactionIds = castArray(bankTransactionIds);
await PromisePool.withConcurrency(1)
.for(_bankTransactionIds)

View File

@@ -3,8 +3,6 @@ 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 = (
@@ -14,19 +12,3 @@ 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);
}
};

View File

@@ -1,7 +1,6 @@
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';
@@ -48,24 +47,21 @@ 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,
uncategorizedTransactionIds: Array<number>,
uncategorizedTransactionId: number,
filter: GetMatchedTransactionsFilter
): Promise<MatchedTransactionsPOJO> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactions =
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query()
.whereIn('id', uncategorizedTransactionIds)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const totalPending = sumBy(uncategorizedTransactions, 'amount');
const filtered = filter.transactionType
? this.registered.filter((item) => item.type === filter.transactionType)
: this.registered;
@@ -75,14 +71,14 @@ export class GetMatchedTransactions {
.process(async ({ type, service }) => {
return service.getMatchedTransactions(tenantId, filter);
});
const { perfectMatches, possibleMatches } = this.groupMatchedResults(
uncategorizedTransactions,
uncategorizedTransaction,
matchedTransactions
);
return {
perfectMatches,
possibleMatches,
totalPending,
};
}
@@ -94,20 +90,20 @@ export class GetMatchedTransactions {
* @returns {MatchedTransactionsPOJO}
*/
private groupMatchedResults(
uncategorizedTransactions: Array<any>,
uncategorizedTransaction,
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(amount, date, results);
const closestResullts = sortClosestMatchTransactions(
uncategorizedTransaction,
results
);
const perfectMatches = R.filter(
(match) =>
match.amount === amount && moment(match.date).isSame(date, 'day'),
match.amount === uncategorizedTransaction.amount &&
moment(match.date).isSame(uncategorizedTransaction.date, 'day'),
closestResullts
);
const possibleMatches = R.difference(closestResullts, perfectMatches);

View File

@@ -1,25 +1,18 @@
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,
IMatchTransactionDTO,
MatchedTransactionPOJO,
} from './types';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreateBillPayment } from '@/services/Purchases/BillPayments/CreateBillPayment';
import { IBillPaymentDTO } from '@/interfaces';
@Service()
export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
@Inject()
private transformer: TransformerInjectable;
private tenancy: HasTenancyService;
@Inject()
private createPaymentMadeService: CreateBillPayment;
private transformer: TransformerInjectable;
/**
* Retrieves the matched transactions.
@@ -78,62 +71,4 @@ 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);
}
}

View File

@@ -1,26 +1,22 @@
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 transformer: TransformerInjectable;
protected tenancy: HasTenancyService;
@Inject()
protected createPaymentReceivedService: CreatePaymentReceived;
protected transformer: TransformerInjectable;
/**
* Retrieves the matched transactions.
@@ -82,64 +78,4 @@ 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)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,22 @@
import moment from 'moment';
import * as R from 'ramda';
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
import { ERRORS, MatchedTransactionPOJO } from './types';
import { isEmpty, sumBy } from 'lodash';
import { ServiceError } from '@/exceptions';
import { MatchedTransactionPOJO } from './types';
export const sortClosestMatchTransactions = (
amount: number,
date: Date,
uncategorizedTransaction: UncategorizedCashflowTransaction,
matches: MatchedTransactionPOJO[]
) => {
return R.sortWith([
// Sort by amount difference (closest to uncategorized transaction amount first)
R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(match.amount - amount)
Math.abs(match.amount - uncategorizedTransaction.amount)
),
// Sort by date difference (closest to uncategorized transaction date first)
R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(moment(match.date).diff(moment(date), 'days'))
Math.abs(
moment(match.date).diff(moment(uncategorizedTransaction.date), 'days')
)
),
])(matches);
};
@@ -30,36 +29,3 @@ 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),
});
}
};

View File

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

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import {
IBillPaymentEventDeletedPayload,
IPaymentReceivedDeletedPayload,
IPaymentReceiveDeletedPayload,
} 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 {IPaymentReceivedDeletedPayload}
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentMadeDeleting({
tenantId,

View File

@@ -1,5 +1,5 @@
import { Inject, Service } from 'typedi';
import { IPaymentReceivedDeletedPayload } from '@/interfaces';
import { IPaymentReceiveDeletedPayload } 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 {IPaymentReceivedDeletedPayload}
* @param {IPaymentReceiveDeletedPayload}
*/
public async validateMatchingOnPaymentReceivedDeleting({
tenantId,
oldPaymentReceive,
trx,
}: IPaymentReceivedDeletedPayload) {
}: IPaymentReceiveDeletedPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'PaymentReceive',

View File

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

View File

@@ -25,7 +25,6 @@ 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;
@@ -41,7 +40,7 @@ export class PlaidSyncDb {
private cashflowApp: CashflowApplication;
@Inject()
private removePendingTransaction: RemovePendingUncategorizedTransaction;
private deleteCashflowTransactionService: DeleteCashflowTransaction;
@Inject()
private eventPublisher: EventPublisher;
@@ -186,22 +185,21 @@ export class PlaidSyncDb {
plaidTransactionsIds: string[],
trx?: Knex.Transaction
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const { CashflowTransaction } = this.tenancy.models(tenantId);
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query(trx).whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
const cashflowTransactionsIds = cashflowTransactions.map(
(trans) => trans.id
);
await bluebird.map(
uncategorizedTransactionsIds,
(uncategorizedTransactionId: number) =>
this.removePendingTransaction.removePendingTransaction(
cashflowTransactionsIds,
(transactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
uncategorizedTransactionId,
transactionId,
trx
),
{ concurrency: CONCURRENCY_ASYNC }

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
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 - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
* @param {number} tenantId
* @param {number} plaidItemId
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactions(tenantId: number, plaidItemId: string) {
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
@@ -73,12 +73,6 @@ 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