diff --git a/client/src/containers/Setup/WizardSetupSteps.js b/client/src/containers/Setup/WizardSetupSteps.js index aaa39d6b7..d29cb4abe 100644 --- a/client/src/containers/Setup/WizardSetupSteps.js +++ b/client/src/containers/Setup/WizardSetupSteps.js @@ -24,7 +24,7 @@ function WizardSetupSteps({ {registerWizardSteps.map((step, index) => ( ))} diff --git a/client/src/style/pages/authentication.scss b/client/src/style/pages/authentication.scss index 05379fe82..81e1c4e75 100644 --- a/client/src/style/pages/authentication.scss +++ b/client/src/style/pages/authentication.scss @@ -145,32 +145,33 @@ } } - // Register Form - .register-form { - width: 690px; - margin: 0px auto; - padding: 85px 50px; + // // Register Form + // .register-form { + // // width: 690px; + // // margin: 0px auto; + // // padding: 85px 50px; + - &__agreement-section { - margin-top: -10px; + // &__agreement-section { + // margin-top: -10px; - p { - font-size: 13px; - margin-top: -10px; - margin-bottom: 24px; - line-height: 1.65; - } - } + // p { + // font-size: 13px; + // margin-top: -10px; + // margin-bottom: 24px; + // line-height: 1.65; + // } + // } - &__submit-button-wrap { - margin: 25px 0px 25px 0px; + // &__submit-button-wrap { + // margin: 25px 0px 25px 0px; - .bp3-button { - min-height: 45px; - background-color: #0052cc; - } - } - } + // .bp3-button { + // min-height: 45px; + // background-color: #0052cc; + // } + // } + // } .send-reset-password { .form-group--crediential { diff --git a/client/src/style/pages/register-wizard-page.scss b/client/src/style/pages/register-wizard-page.scss index a53e1add5..67ebe31fa 100644 --- a/client/src/style/pages/register-wizard-page.scss +++ b/client/src/style/pages/register-wizard-page.scss @@ -1,11 +1,136 @@ +.register-page { + .bp3-input { + min-height: 40px; + border: 1px solid #ced4da; + } + .bp3-form-group { + margin-bottom: 23px; + + &.bp3-intent-danger { + .bp3-input { + border-color: #eea9a9; + } + } + } + .bp3-form-group.has-password-revealer { + .bp3-label { + display: flex; + justify-content: space-between; + } + + .password-revealer { + .text { + font-size: 12px; + } + } + } + + .bp3-button.bp3-fill.bp3-intent-primary { + font-size: 16px; + } + + &__label-section { + margin-bottom: 29px; + color: #555; + + h3 { + // font-weight: 500; + font-weight: 400; + // font-size: 28px; + font-size: 22px; + // color: #444; + color: #555555; + margin: 0 0 12px; + } + + a { + text-decoration: underline; + color: #0040bd; + } + } + + &__form-wrapper { + width: 100%; + // max-width: 415px; + // padding: 15px; + margin: 0 auto; + } + + &__footer-links { + padding: 9px; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + text-align: center; + margin-bottom: 1.2rem; + + a { + color: #0052cc; + } + } + + &__loading-overlay { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(252, 253, 255, 0.5); + display: flex; + justify-content: center; + } + + &__submit-button-wrap { + margin: 0px 0px 24px 0px; + + .bp3-button { + background-color: #0052cc; + min-height: 45px; + } + } + + &-form { + width: 800px; + margin: 0 auto; + + // width: 690px; + // padding: 85px 60px; + // padding: 85px 105px; + + // Register Form + .register-form { + padding: 85px 105px; + + &__agreement-section { + margin-top: -10px; + + p { + font-size: 13px; + margin-top: -10px; + margin-bottom: 24px; + line-height: 1.65; + } + } + + &__submit-button-wrap { + margin: 25px 0px 25px 0px; + + .bp3-button { + min-height: 45px; + background-color: #0052cc; + } + } + } + } +} + .setup-page { - + &__right-section { padding-left: 25%; } - &__left-section{ + &__left-section { position: fixed; background: #778cab; overflow: auto; @@ -59,6 +184,8 @@ } } } + + .setup-page-steps { @@ -122,9 +249,6 @@ } } - -// @import './billing.scss'; - //Register Subscription form .register-subscription-form { padding-top: 50px; diff --git a/server/.env.example b/server/.env.example index ea99a35cc..437019d76 100644 --- a/server/.env.example +++ b/server/.env.example @@ -20,7 +20,7 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_ TENANT_DB_HOST=127.0.0.1 TENANT_DB_PASSWORD=root TEANNT_DB_USER=root -TENANT_DB_CHARSET=charset +TENANT_DB_CHARSET=utf8 TENANT_MIGRATIONS_DIR=src/database/migrations TENANT_SEEDS_DIR=src/database/seeds/core diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index fb3670580..a3126f202 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -43,6 +43,15 @@ export default class AccountsController extends BaseController{ asyncMiddleware(this.inactivateAccount.bind(this)), this.catchServiceErrors, ); + router.post( + '/:id/close', [ + ...this.accountParamSchema, + ...this.closingAccountSchema, + ], + this.validationResult, + asyncMiddleware(this.closeAccount.bind(this)), + this.catchServiceErrors, + ) router.post( '/:id', [ ...this.accountDTOSchema, @@ -127,18 +136,12 @@ export default class AccountsController extends BaseController{ ]; } - /** - * Account param schema validation. - */ get accountParamSchema() { return [ param('id').exists().isNumeric().toInt() ]; } - /** - * Accounts list schema validation. - */ get accountsListSchema() { return [ query('custom_view_id').optional().isNumeric().toInt(), @@ -149,9 +152,6 @@ export default class AccountsController extends BaseController{ ]; } - /** - * - */ get bulkSelectIdsQuerySchema() { return [ query('ids').isArray({ min: 2 }), @@ -159,6 +159,13 @@ export default class AccountsController extends BaseController{ ]; } + get closingAccountSchema() { + return [ + check('to_account_id').exists().isNumeric().toInt(), + check('delete_after_closing').exists().isBoolean(), + ] + } + /** * Creates a new account. * @param {Request} req - @@ -328,8 +335,36 @@ export default class AccountsController extends BaseController{ filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const accounts = await this.accountsService.getAccountsList(tenantId, filter); - return res.status(200).send({ accounts }); + const { accounts, filterMeta } = await this.accountsService.getAccountsList(tenantId, filter); + + return res.status(200).send({ + accounts, + filter_meta: this.transfromToResponse(filterMeta) + }); + } catch (error) { + next(error); + } + } + + /** + * Closes the given account. + * @param {Request} req + * @param {Response} res + * @param next + */ + async closeAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: accountId } = req.params; + const closeAccountQuery = this.matchedBodyData(req); + + try { + await this.accountsService.closeAccount( + tenantId, + accountId, + closeAccountQuery.toAccountId, + closeAccountQuery.deleteAfterClosing + ); + return res.status(200).send({ id: accountId }); } catch (error) { next(error); } @@ -358,9 +393,8 @@ export default class AccountsController extends BaseController{ } if (error.errorType === 'account_type_not_found') { return res.boom.badRequest( - 'The given account type not found.', { - errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] - } + 'The given account type not found.', + { errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] } ); } if (error.errorType === 'account_type_not_allowed_to_changed') { @@ -417,6 +451,12 @@ export default class AccountsController extends BaseController{ { errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] } ); } + if (error.errorType === 'close_account_and_to_account_not_same_type') { + return res.boom.badRequest( + 'The close account has different root type with to account.', + { errors: [{ type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE', code: 1200 }] }, + ); + } } next(error) } diff --git a/server/src/api/controllers/Authentication.ts b/server/src/api/controllers/Authentication.ts index e5d162333..e219e5a81 100644 --- a/server/src/api/controllers/Authentication.ts +++ b/server/src/api/controllers/Authentication.ts @@ -2,10 +2,9 @@ import { Request, Response, Router } from 'express'; import { check, ValidationChain } from 'express-validator'; import { Service, Inject } from 'typedi'; import BaseController from 'api/controllers/BaseController'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import AuthenticationService from 'services/Authentication'; -import { IUserOTD, ISystemUser, IRegisterOTD } from 'interfaces'; +import { ILoginDTO, ISystemUser, IRegisterOTD } from 'interfaces'; import { ServiceError, ServiceErrors } from "exceptions"; @Service() @@ -61,7 +60,6 @@ export default class AuthenticationController extends BaseController{ */ get registerSchema(): ValidationChain[] { return [ - check('organization_name').exists().trim().escape(), check('first_name').exists().trim().escape(), check('last_name').exists().trim().escape(), check('email').exists().isEmail().trim().escape(), @@ -102,7 +100,7 @@ export default class AuthenticationController extends BaseController{ * @param {Response} res */ async login(req: Request, res: Response, next: Function): Response { - const userDTO: IUserOTD = this.matchedBodyData(req); + const userDTO: ILoginDTO = this.matchedBodyData(req); try { const { token, user, tenant } = await this.authService.signIn( diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index 1ff423c00..6299a9ac0 100644 --- a/server/src/api/controllers/BaseController.ts +++ b/server/src/api/controllers/BaseController.ts @@ -1,6 +1,6 @@ import { Response, Request, NextFunction } from 'express'; import { matchedData, validationResult } from "express-validator"; -import { camelCase, omit } from "lodash"; +import { camelCase, snakeCase, omit } from "lodash"; import { mapKeysDeep } from 'utils' export default class BaseController { @@ -55,4 +55,12 @@ export default class BaseController { } next(); } + + /** + * Transform the given data to response. + * @param {any} data + */ + transfromToResponse(data: any) { + return mapKeysDeep(data, (v, k) => snakeCase(k)); + } } \ No newline at end of file diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index b271cf7f9..0ee03b2bc 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -46,6 +46,12 @@ export default class CustomersController extends ContactsController { this.validationResult, asyncMiddleware(this.deleteBulkCustomers.bind(this)) ); + router.get('/', [ + + ], + this.validationResult, + asyncMiddleware(this.getCustomersList.bind(this)) + ); router.get('/:id', [ ...this.specificContactSchema, ], @@ -193,4 +199,15 @@ export default class CustomersController extends ContactsController { next(error); } } + + + async getCustomersList(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + + try { + await this.customersService.getCustomersList(tenantId) + } catch (error) { + next(error); + } + } } \ No newline at end of file diff --git a/server/src/api/controllers/Currencies.ts b/server/src/api/controllers/Currencies.ts index 06f0fbb5e..357083e34 100644 --- a/server/src/api/controllers/Currencies.ts +++ b/server/src/api/controllers/Currencies.ts @@ -162,7 +162,7 @@ export default class CurrenciesController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handlerServiceError(error, req, res, next) { + handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'currency_not_found') { return res.boom.badRequest(null, { diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index 668dbba45..5d51cc68a 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -7,6 +7,7 @@ import ExpensesService from "services/Expenses/ExpensesService"; import { IExpenseDTO } from 'interfaces'; import { ServiceError } from "exceptions"; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { takeWhile } from "lodash"; @Service() export default class ExpensesController extends BaseController { @@ -30,7 +31,8 @@ export default class ExpensesController extends BaseController { asyncMiddleware(this.newExpense.bind(this)), this.catchServiceErrors, ); - router.post('/publish', [ + router.post( + '/publish', [ ...this.bulkSelectSchema, ], this.bulkPublishExpenses.bind(this), @@ -69,11 +71,22 @@ export default class ExpensesController extends BaseController { this.catchServiceErrors, ); router.get( - '/', + '/', [ + ...this.expensesListSchema, + ], + this.validationResult, asyncMiddleware(this.getExpensesList.bind(this)), this.dynamicListService.handlerErrorsToResponse, this.catchServiceErrors, ); + router.get( + '/:id', [ + this.expenseParamSchema, + ], + this.validationResult, + asyncMiddleware(this.getExpense.bind(this)), + this.catchServiceErrors, + ); return router; } @@ -89,6 +102,7 @@ export default class ExpensesController extends BaseController { check('currency_code').optional(), check('exchange_rate').optional().isNumeric().toFloat(), check('publish').optional().isBoolean().toBoolean(), + check('categories').exists().isArray({ min: 1 }), check('categories.*.index').exists().isNumeric().toInt(), check('categories.*.expense_account_id').exists().isNumeric().toInt(), @@ -120,6 +134,20 @@ export default class ExpensesController extends BaseController { ]; } + + get expensesListSchema() { + return [ + query('custom_view_id').optional().isNumeric().toInt(), + query('stringified_filter_roles').optional().isJSON(), + + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + /** * Creates a new expense on * @param {Request} req @@ -240,12 +268,41 @@ export default class ExpensesController extends BaseController { const filter = { filterRoles: [], sortOrder: 'asc', + columnSortBy: 'created_at', + page: 1, + pageSize: 12, ...this.matchedQueryData(req), }; + if (filter.stringifiedFilterRoles) { + filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); + } try { - const expenses = await this.expensesService.getExpensesList(tenantId, filter); - return res.status(200).send({ expenses }); + const { expenses, pagination, filterMeta } = await this.expensesService.getExpensesList(tenantId, filter); + + return res.status(200).send({ + expenses, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { + next(error); + } + } + + /** + * Retrieve expense details. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getExpense(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: expenseId } = req.params; + + try { + const expense = await this.expensesService.getExpense(tenantId, expenseId); + return res.status(200).send({ expense }); } catch (error) { next(error); } @@ -259,29 +316,34 @@ export default class ExpensesController extends BaseController { catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'expense_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }], - }); + return res.boom.badRequest( + 'Expense not found.', + { errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }] } + ); } if (error.errorType === 'total_amount_equals_zero') { - return res.boom.badRequest(null, { - errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }], - }); + return res.boom.badRequest( + 'Expense total should not equal zero.', + { errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }] }, + ); } if (error.errorType === 'payment_account_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }], - }); + return res.boom.badRequest( + 'Payment account not found.', + { errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }] }, + ); } if (error.errorType === 'some_expenses_not_found') { - return res.boom.badRequest(null, { - errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] - }) + return res.boom.badRequest( + 'Some expense accounts not found.', + { errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] }, + ); } if (error.errorType === 'payment_account_has_invalid_type') { - return res.boom.badRequest(null, { - errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], - }); + return res.boom.badRequest( + 'Payment account has invalid type.', + { errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], }, + ); } if (error.errorType === 'expenses_account_has_invalid_type') { return res.boom.badRequest(null, { diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index 0d54a3c0c..8eb5d28e8 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -7,16 +7,19 @@ import { import ItemCategoriesService from 'services/ItemCategories/ItemCategoriesService'; import { Inject, Service } from 'typedi'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import { IItemCategoryOTD } from 'interfaces'; import { ServiceError } from 'exceptions'; import BaseController from 'api/controllers/BaseController'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class ItemsCategoriesController extends BaseController { @Inject() itemCategoriesService: ItemCategoriesService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor method. */ @@ -27,44 +30,45 @@ export default class ItemsCategoriesController extends BaseController { ...this.categoryValidationSchema, ...this.specificCategoryValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.editCategory.bind(this)), this.handlerServiceError, ); router.post('/', [ ...this.categoryValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.newCategory.bind(this)), this.handlerServiceError, ); router.delete('/', [ ...this.categoriesBulkValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.bulkDeleteCategories.bind(this)), this.handlerServiceError, ); router.delete('/:id', [ ...this.specificCategoryValidationSchema ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.deleteItem.bind(this)), this.handlerServiceError, ); router.get('/:id', [ ...this.specificCategoryValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.getCategory.bind(this)), this.handlerServiceError, ); router.get('/', [ ...this.categoriesListValidationSchema ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.getList.bind(this)), this.handlerServiceError, + this.dynamicListService.handlerErrorsToResponse, ); return router; } @@ -113,8 +117,9 @@ export default class ItemsCategoriesController extends BaseController { */ get categoriesListValidationSchema() { return [ - query('column_sort_order').optional().trim().escape(), + query('column_sort_by').optional().trim().escape(), query('sort_order').optional().trim().escape().isIn(['desc', 'asc']), + query('stringified_filter_roles').optional().isJSON(), ]; } @@ -190,13 +195,21 @@ export default class ItemsCategoriesController extends BaseController { */ async getList(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; - const itemCategoriesFilter = this.matchedQueryData(req); + const itemCategoriesFilter = { + filterRoles: [], + sortOrder: 'asc', + columnSortBy: 'created_at', + ...this.matchedQueryData(req), + }; try { - const itemCategories = await this.itemCategoriesService.getItemCategoriesList( + const { itemCategories, filterMeta } = await this.itemCategoriesService.getItemCategoriesList( tenantId, itemCategoriesFilter, user, ); - return res.status(200).send({ item_categories: itemCategories }); + return res.status(200).send({ + item_categories: itemCategories, + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 672c09bb5..a063f01ac 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -23,8 +23,7 @@ export default class ItemsController extends BaseController { router() { const router = Router(); - router.post( - '/', [ + router.post('/', [ ...this.validateItemSchema, ], this.validationResult, @@ -150,10 +149,12 @@ export default class ItemsController extends BaseController { */ get validateListQuerySchema() { return [ - query('column_sort_order').optional().trim().escape(), + query('column_sort_by').optional().trim().escape(), query('sort_order').optional().isIn(['desc', 'asc']), + query('page').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(), + query('custom_view_id').optional().isNumeric().toInt(), query('stringified_filter_roles').optional().isJSON(), ] @@ -240,14 +241,22 @@ export default class ItemsController extends BaseController { const filter = { filterRoles: [], sortOrder: 'asc', + columnSortBy: 'created_at', + page: 1, + pageSize: 12, ...this.matchedQueryData(req), }; if (filter.stringifiedFilterRoles) { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const items = await this.itemsService.itemsList(tenantId, filter); - return res.status(200).send({ items }); + const { items, pagination, filterMeta } = await this.itemsService.itemsList(tenantId, filter); + + return res.status(200).send({ + items, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); } catch (error) { next(error); } @@ -345,6 +354,16 @@ export default class ItemsController extends BaseController { errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], }); } + if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') { + return res.status(400).send({ + errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }], + }); + } + if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') { + return res.status(400).send({ + errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }], + }) + } } } } \ No newline at end of file diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index 486d681f8..b4ad00dc8 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -299,17 +299,22 @@ export default class ManualJournalsController extends BaseController { sortOrder: 'asc', columnSortBy: 'created_at', filterRoles: [], + page: 1, + pageSize: 12, ...this.matchedQueryData(req), } if (filter.stringifiedFilterRoles) { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const manualJournals = await this.manualJournalsService.getManualJournals(tenantId, filter); - return res.status(200).send({ manualJournals }); - } catch (error) { - console.log(error); + const { manualJournals, pagination, filterMeta } = await this.manualJournalsService.getManualJournals(tenantId, filter); + return res.status(200).send({ + manual_journals: manualJournals, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { next(error); } } diff --git a/server/src/api/controllers/Media.js b/server/src/api/controllers/Media.js deleted file mode 100644 index 94631e804..000000000 --- a/server/src/api/controllers/Media.js +++ /dev/null @@ -1,163 +0,0 @@ - -import express from 'express'; -import { - param, - query, - validationResult, -} from 'express-validator'; -import Container from 'typedi'; -import fs from 'fs'; -import { difference } from 'lodash'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; - -const fsPromises = fs.promises; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.post('/upload', - this.upload.validation, - asyncMiddleware(this.upload.handler)); - - router.delete('/', - this.delete.validation, - asyncMiddleware(this.delete.handler)); - - router.get('/', - this.get.validation, - asyncMiddleware(this.get.handler)); - - return router; - }, - - /** - * Retrieve all or the given attachment ids. - */ - get: { - validation: [ - query('ids'), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Media } = req.models; - const media = await Media.query().onBuild((builder) => { - - if (req.query.ids) { - const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids]; - builder.whereIn('id', ids); - } - }); - - return res.status(200).send({ media }); - }, - }, - - /** - * Uploads the given attachment file. - */ - upload: { - validation: [ - // check('attachment').exists(), - ], - async handler(req, res) { - const Logger = Container.get('logger'); - - if (!req.files.attachment) { - return res.status(400).send({ - errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }], - }); - } - const publicPath = 'storage/app/public/'; - const attachmentsMimes = ['image/png', 'image/jpeg']; - const { attachment } = req.files; - const { Media } = req.models; - - const errorReasons = []; - - // Validate the attachment. - if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) { - errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 }); - } - // Catch all error reasons to response 400. - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - - try { - await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`); - Logger.info('[attachment] uploaded successfully'); - } catch (error) { - Logger.info('[attachment] uploading failed.', { error }); - } - - const media = await Media.query().insert({ - attachment_file: `${attachment.md5}.png`, - }); - return res.status(200).send({ media }); - }, - }, - - /** - * Deletes the given attachment ids from file system and database. - */ - delete: { - validation: [ - query('ids').exists().isArray(), - query('ids.*').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const Logger = Container.get('logger'); - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Media, MediaLink } = req.models; - const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids]; - const media = await Media.query().whereIn('id', ids); - const mediaIds = media.map((m) => m.id); - const notFoundMedia = difference(ids, mediaIds); - - if (notFoundMedia.length) { - return res.status(400).send({ - errors: [{ type: 'MEDIA.IDS.NOT.FOUND', code: 200, ids: notFoundMedia }], - }); - } - const publicPath = 'storage/app/public/'; - const tenantPath = `${publicPath}${req.organizationId}`; - const unlinkOpers = []; - - media.forEach((mediaModel) => { - const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`); - unlinkOpers.push(oper); - }); - await Promise.all(unlinkOpers).then((resolved) => { - resolved.forEach(() => { - Logger.info('[attachment] file has been deleted.'); - }); - }) - .catch((errors) => { - errors.forEach((error) => { - Logger.info('[attachment] Delete item attachment file delete failed.', { error }); - }) - }); - - await MediaLink.query().whereIn('media_id', mediaIds).delete(); - await Media.query().whereIn('id', mediaIds).delete(); - - return res.status(200).send(); - }, - }, -}; diff --git a/server/src/api/controllers/Media.ts b/server/src/api/controllers/Media.ts new file mode 100644 index 000000000..469d6564b --- /dev/null +++ b/server/src/api/controllers/Media.ts @@ -0,0 +1,212 @@ + +import { Router, Request, Response, NextFunction } from 'express'; +import { + param, + query, + check, +} from 'express-validator'; +import { camelCase, upperFirst } from 'lodash'; +import { Inject, Service } from 'typedi'; +import { IMediaLinkDTO } from 'interfaces'; +import fs from 'fs'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from './BaseController'; +import MediaService from 'services/Media/MediaService'; +import { ServiceError } from 'exceptions'; + +const fsPromises = fs.promises; + +@Service() +export default class MediaController extends BaseController { + @Inject() + mediaService: MediaService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.post('/upload', [ + ...this.uploadValidationSchema, + ], + this.validationResult, + asyncMiddleware(this.uploadMedia.bind(this)), + this.handlerServiceErrors, + ); + router.post('/:id/link', [ + ...this.mediaIdParamSchema, + ...this.linkValidationSchema, + ], + this.validationResult, + asyncMiddleware(this.linkMedia.bind(this)), + this.handlerServiceErrors, + ); + router.delete('/', [ + ...this.deleteValidationSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteMedia.bind(this)), + this.handlerServiceErrors, + ); + router.get('/:id', [ + ...this.mediaIdParamSchema, + ], + this.validationResult, + asyncMiddleware(this.getMedia.bind(this)), + this.handlerServiceErrors, + ); + return router; + } + + get uploadValidationSchema() { + return [ + // check('attachment'), + check('model_name').optional().trim().escape(), + check('model_id').optional().isNumeric().toInt(), + ]; + } + + get linkValidationSchema() { + return [ + check('model_name').exists().trim().escape(), + check('model_id').exists().isNumeric().toInt(), + ] + } + + get deleteValidationSchema() { + return [ + query('ids').exists().isArray(), + query('ids.*').exists().isNumeric().toInt(), + ]; + } + + get mediaIdParamSchema() { + return [ + param('id').exists().isNumeric().toInt(), + ]; + } + + /** + * Retrieve all or the given attachment ids. + * @param {Request} req - + * @param {Response} req - + * @param {NextFunction} req - + */ + async getMedia(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: mediaId } = req.params; + + try { + const media = await this.mediaService.getMedia(tenantId, mediaId); + return res.status(200).send({ media }); + } catch (error) { + next(error); + } + } + + /** + * Uploads media. + * @param {Request} req - + * @param {Response} req - + * @param {NextFunction} req - + */ + async uploadMedia(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { attachment } = req.files + + const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req); + const modelName = linkMediaDTO.modelName + ? upperFirst(camelCase(linkMediaDTO.modelName)) : ''; + + try { + const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId); + return res.status(200).send({ media_id: media.id }); + } catch (error) { + next(error); + } + } + + /** + * Deletes the given attachment ids from file system and database. + * @param {Request} req - + * @param {Response} req - + * @param {NextFunction} req - + */ + async deleteMedia(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { ids: mediaIds } = req.query; + + try { + await this.mediaService.deleteMedia(tenantId, mediaIds); + return res.status(200).send({ + media_ids: mediaIds + }); + } catch (error) { + next(error); + } + } + + /** + * Links the given media to the specific resource model. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async linkMedia(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: mediaId } = req.params; + const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req); + const modelName = upperFirst(camelCase(linkMediaDTO.modelName)); + + try { + await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName); + return res.status(200).send({ media_id: mediaId }); + } catch (error) { + next(error); + } + } + + /** + * Handler service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'MINETYPE_NOT_SUPPORTED') { + return res.boom.badRequest(null, { + errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }] + }); + } + if (error.errorType === 'MEDIA_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }] + }); + } + if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') { + return res.boom.badRequest(null, { + errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }] + }); + } + if (error.errorType === 'MODEL_ID_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }] + }); + } + if (error.errorType === 'MEDIA_IDS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }], + }); + } + if (error.errorType === 'MEDIA_LINK_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }], + }); + } + } + next(error); + } +}; diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 9f7618323..83168506e 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator'; import { Service, Inject } from 'typedi'; import { difference } from 'lodash'; import { BillOTD } from 'interfaces'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import BillsService from 'services/Purchases/Bills'; import BaseController from 'api/controllers/BaseController'; @@ -30,7 +29,7 @@ export default class BillsController extends BaseController { router.post( '/', [...this.billValidationSchema], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateVendorExistance.bind(this)), asyncMiddleware(this.validateItemsIds.bind(this)), asyncMiddleware(this.validateBillNumberExists.bind(this)), @@ -40,7 +39,7 @@ export default class BillsController extends BaseController { router.post( '/:id', [...this.billValidationSchema, ...this.specificBillValidationSchema], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillExistance.bind(this)), asyncMiddleware(this.validateVendorExistance.bind(this)), asyncMiddleware(this.validateItemsIds.bind(this)), @@ -51,20 +50,20 @@ export default class BillsController extends BaseController { router.get( '/:id', [...this.specificBillValidationSchema], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillExistance.bind(this)), asyncMiddleware(this.getBill.bind(this)) ); router.get( '/', [...this.billsListingValidationSchema], - validateMiddleware, + this.validationResult, asyncMiddleware(this.listingBills.bind(this)) ); router.delete( '/:id', [...this.specificBillValidationSchema], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillExistance.bind(this)), asyncMiddleware(this.deleteBill.bind(this)) ); diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts index bd80c6903..1b72a99d6 100644 --- a/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/server/src/api/controllers/Purchases/BillsPayments.ts @@ -30,7 +30,7 @@ export default class BillsPayments extends BaseController { router.post('/', [ ...this.billPaymentSchemaValidation, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)), asyncMiddleware(this.validatePaymentAccount.bind(this)), asyncMiddleware(this.validatePaymentNumber.bind(this)), @@ -42,7 +42,7 @@ export default class BillsPayments extends BaseController { ...this.billPaymentSchemaValidation, ...this.specificBillPaymentValidateSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)), asyncMiddleware(this.validatePaymentAccount.bind(this)), asyncMiddleware(this.validatePaymentNumber.bind(this)), @@ -53,19 +53,19 @@ export default class BillsPayments extends BaseController { ) router.delete('/:id', this.specificBillPaymentValidateSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillPaymentExistance.bind(this)), asyncMiddleware(this.deleteBillPayment.bind(this)), ); router.get('/:id', this.specificBillPaymentValidateSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateBillPaymentExistance.bind(this)), asyncMiddleware(this.getBillPayment.bind(this)), ); router.get('/', this.listingValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.getBillsPayments.bind(this)) ); return router; @@ -381,6 +381,20 @@ export default class BillsPayments extends BaseController { * @return {Response} */ async getBillsPayments(req: Request, res: Response) { + const { tenantId } = req.params; + const billPaymentsFilter = this.matchedQueryData(req); + try { + const { billPayments, pagination, filterMeta } = await this.billPaymentService + .listBillPayments(tenantId, billPaymentsFilter); + + return res.status(200).send({ + bill_payments: billPayments, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta) + }); + } catch (error) { + next(error); + } } } \ No newline at end of file diff --git a/server/src/api/controllers/Resources.js b/server/src/api/controllers/Resources.js deleted file mode 100644 index bf1063a41..000000000 --- a/server/src/api/controllers/Resources.js +++ /dev/null @@ -1,115 +0,0 @@ -import express from 'express'; -import { - param, - query, -} from 'express-validator'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/:resource_slug/data', - this.resourceData.validation, - asyncMiddleware(this.resourceData.handler)); - - router.get('/:resource_slug/columns', - this.resourceColumns.validation, - asyncMiddleware(this.resourceColumns.handler)); - - router.get('/:resource_slug/fields', - this.resourceFields.validation, - asyncMiddleware(this.resourceFields.handler)); - - return router; - }, - - /** - * Retrieve resource data of the given resource key/slug. - */ - resourceData: { - validation: [ - param('resource_slug').trim().escape().exists(), - ], - async handler(req, res) { - const { AccountType } = req.models; - const { resource_slug: resourceSlug } = req.params; - - const data = await AccountType.query(); - - return res.status(200).send({ - data, - resource_slug: resourceSlug, - }); - }, - }, - - /** - * Retrieve resource columns of the given resource. - */ - resourceColumns: { - validation: [ - param('resource_slug').trim().escape().exists(), - ], - async handler(req, res) { - const { resource_slug: resourceSlug } = req.params; - const { Resource } = req.models; - - const resource = await Resource.query() - .where('name', resourceSlug) - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }], - }); - } - const resourceFields = resource.fields - .filter((field) => field.columnable) - .map((field) => ({ - id: field.id, - label: field.labelName, - key: field.key, - })); - - return res.status(200).send({ - resource_columns: resourceFields, - resource_slug: resourceSlug, - }); - }, - }, - - /** - * Retrieve resource fields of the given resource. - */ - resourceFields: { - validation: [ - param('resource_slug').trim().escape().exists(), - query('predefined').optional().isBoolean().toBoolean(), - query('builtin').optional().isBoolean().toBoolean(), - ], - async handler(req, res) { - const { resource_slug: resourceSlug } = req.params; - const { Resource } = req.models; - - const resource = await Resource.query() - .where('name', resourceSlug) - .withGraphFetched('fields') - .first(); - - if (!resource) { - return res.status(400).send({ - errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }], - }); - } - return res.status(200).send({ - resource_fields: resource.fields, - resource_slug: resourceSlug, - }); - }, - }, -}; diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts new file mode 100644 index 000000000..91f0d8955 --- /dev/null +++ b/server/src/api/controllers/Resources.ts @@ -0,0 +1,44 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { + param, + query, +} from 'express-validator'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from './BaseController'; +import { Service } from 'typedi'; +import ResourceFieldsKeys from 'data/ResourceFieldsKeys'; + +@Service() +export default class ResourceController extends BaseController{ + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get('/:resource_model/fields', + this.resourceModelParamSchema, + asyncMiddleware(this.resourceFields.bind(this)) + ); + return router; + } + + get resourceModelParamSchema() { + return [ + param('resource_model').exists().trim().escape(), + ]; + } + + /** + * Retrieve resource fields of the given resource. + */ + resourceFields(req: Request, res: Response, next: NextFunction) { + const { resource_model: resourceModel } = req.params; + + try { + + } catch (error) { + next(error); + } + } +}; diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index d2693a174..0d2d1fac7 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -4,7 +4,6 @@ import { difference } from 'lodash'; import { Inject, Service } from 'typedi'; import { IPaymentReceive, IPaymentReceiveOTD } from 'interfaces'; import BaseController from 'api/controllers/BaseController'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import PaymentReceiveService from 'services/Sales/PaymentsReceives'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; @@ -34,7 +33,7 @@ export default class PaymentReceivesController extends BaseController { router.post( '/:id', this.editPaymentReceiveValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)), asyncMiddleware(this.validateCustomerExistance.bind(this)), @@ -47,7 +46,7 @@ export default class PaymentReceivesController extends BaseController { router.post( '/', this.newPaymentReceiveValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)), asyncMiddleware(this.validateCustomerExistance.bind(this)), asyncMiddleware(this.validateDepositAccount.bind(this)), @@ -58,20 +57,20 @@ export default class PaymentReceivesController extends BaseController { router.get( '/:id', this.paymentReceiveValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), asyncMiddleware(this.getPaymentReceive.bind(this)) ); router.get( '/', this.validatePaymentReceiveList, - validateMiddleware, + this.validationResult, asyncMiddleware(this.getPaymentReceiveList.bind(this)), ); router.delete( '/:id', this.paymentReceiveValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)), asyncMiddleware(this.deletePaymentReceive.bind(this)), ); diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts index 9b397071c..dd5f589ec 100644 --- a/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/server/src/api/controllers/Sales/SalesEstimates.ts @@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator'; import { Inject, Service } from 'typedi'; import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces'; import BaseController from 'api/controllers/BaseController' -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import ItemsService from 'services/Items/ItemsService'; @@ -25,7 +24,7 @@ export default class SalesEstimatesController extends BaseController { router.post( '/', this.estimateValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)), @@ -36,7 +35,7 @@ export default class SalesEstimatesController extends BaseController { ...this.validateSpecificEstimateSchema, ...this.estimateValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateEstimateIdExistance.bind(this)), asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)), asyncMiddleware(this.validateEstimateNumberExistance.bind(this)), @@ -48,21 +47,21 @@ export default class SalesEstimatesController extends BaseController { '/:id', [ this.validateSpecificEstimateSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateEstimateIdExistance.bind(this)), asyncMiddleware(this.deleteEstimate.bind(this)) ); router.get( '/:id', this.validateSpecificEstimateSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateEstimateIdExistance.bind(this)), asyncMiddleware(this.getEstimate.bind(this)) ); router.get( '/', this.validateEstimateListSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.getEstimates.bind(this)) ); return router; @@ -298,7 +297,21 @@ export default class SalesEstimatesController extends BaseController { * @param {Request} req * @param {Response} res */ - async getEstimates(req: Request, res: Response) { - + async getEstimates(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const estimatesFilter: ISalesEstimatesFilter = this.matchedQueryData(req); + + try { + const { salesEstimates, pagination, filterMeta } = await this.saleEstimateService + .estimatesList(tenantId, estimatesFilter); + + return res.status(200).send({ + sales_estimates: this.transfromToResponse(salesEstimates), + pagination, + filter_meta: this.transfromToResponse(filterMeta), + }) + } catch (error) { + next(error); + } } }; diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 96c0ef719..db4ce0a16 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -1,22 +1,26 @@ -import { Router, Request, Response } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { difference } from 'lodash'; import { raw } from 'objection'; import { Service, Inject } from 'typedi'; -import validateMiddleware from 'api/middleware/validateMiddleware'; +import BaseController from '../BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import SaleInvoiceService from 'services/Sales/SalesInvoices'; import ItemsService from 'services/Items/ItemsService'; -import { ISaleInvoiceOTD } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces'; @Service() -export default class SaleInvoicesController { +export default class SaleInvoicesController extends BaseController{ @Inject() itemsService: ItemsService; @Inject() saleInvoiceService: SaleInvoiceService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor. */ @@ -26,7 +30,7 @@ export default class SaleInvoicesController { router.post( '/', this.saleInvoiceValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)), @@ -39,7 +43,7 @@ export default class SaleInvoicesController { ...this.saleInvoiceValidationSchema, ...this.specificSaleInvoiceValidation, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)), asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)), @@ -52,7 +56,7 @@ export default class SaleInvoicesController { router.delete( '/:id', this.specificSaleInvoiceValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.deleteSaleInvoice.bind(this)) ); @@ -64,13 +68,14 @@ export default class SaleInvoicesController { router.get( '/:id', this.specificSaleInvoiceValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateInvoiceExistance.bind(this)), asyncMiddleware(this.getSaleInvoice.bind(this)) ); router.get( '/', this.saleInvoiceListValidationSchema, + this.validationResult, asyncMiddleware(this.getSalesInvoices.bind(this)) ) return router; @@ -411,7 +416,21 @@ export default class SaleInvoicesController { * @param {Response} res * @param {Function} next */ - async getSalesInvoices(req, res) { - + public async getSalesInvoices(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req.params; + const salesInvoicesFilter: ISalesInvoicesFilter = req.query; + + try { + const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList( + tenantId, salesInvoicesFilter, + ); + return res.status(200).send({ + sales_invoices: salesInvoices, + pagination: this.transfromToResponse(pagination), + filter_meta: this.transfromToResponse(filterMeta), + }); + } catch (error) { + next(error); + } } } diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts index 7d5e7c8e8..53823def1 100644 --- a/server/src/api/controllers/Sales/SalesReceipts.ts +++ b/server/src/api/controllers/Sales/SalesReceipts.ts @@ -1,14 +1,14 @@ import { Router, Request, Response } from 'express'; import { check, param, query, matchedData } from 'express-validator'; import { Inject, Service } from 'typedi'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import AccountsService from 'services/Accounts/AccountsService'; import ItemsService from 'services/Items/ItemsService'; import SaleReceiptService from 'services/Sales/SalesReceipts'; +import BaseController from '../BaseController'; @Service() -export default class SalesReceiptsController { +export default class SalesReceiptsController extends BaseController{ @Inject() saleReceiptService: SaleReceiptService; @@ -29,7 +29,7 @@ export default class SalesReceiptsController { ...this.specificReceiptValidationSchema, ...this.salesReceiptsValidationSchema, ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateSaleReceiptExistance.bind(this)), asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)), asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)), @@ -40,7 +40,7 @@ export default class SalesReceiptsController { router.post( '/', this.salesReceiptsValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)), asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)), asyncMiddleware(this.validateReceiptItemsIdsExistance.bind(this)), @@ -49,14 +49,14 @@ export default class SalesReceiptsController { router.delete( '/:id', this.specificReceiptValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateSaleReceiptExistance.bind(this)), asyncMiddleware(this.deleteSaleReceipt.bind(this)) ); router.get( '/', this.listSalesReceiptsValidationSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.listingSalesReceipts.bind(this)) ); return router; @@ -274,7 +274,6 @@ export default class SalesReceiptsController { const { id: saleReceiptId } = req.params; const saleReceipt = { ...req.body }; - const errorReasons = []; // Handle all errors with reasons messages. @@ -296,7 +295,19 @@ export default class SalesReceiptsController { * @param {Request} req * @param {Response} res */ - async listingSalesReceipts(req: Request, res: Response) { + async getSalesReceipts(req: Request, res: Response) { + const { tenantId } = req; + const filter = { + sortOrder: 'asc', + page: 1, + pageSize: 12, + ...this.matchedBodyData(req), + }; + try { + + } catch (error) { + next(error); + } } }; diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts index eddd3eae9..23b5b1136 100644 --- a/server/src/api/controllers/Settings.ts +++ b/server/src/api/controllers/Settings.ts @@ -1,3 +1,4 @@ +import { Service } from 'typedi'; import { Router, Request, Response } from 'express'; import { body, query } from 'express-validator'; import { pick } from 'lodash'; @@ -9,6 +10,7 @@ import { isDefinedOptionConfigurable, } from 'utils'; +@Service() export default class SettingsController extends BaseController{ /** * Router constructor. diff --git a/server/src/api/controllers/Subscription/Licenses.ts b/server/src/api/controllers/Subscription/Licenses.ts index 781c8710f..5639d9b7c 100644 --- a/server/src/api/controllers/Subscription/Licenses.ts +++ b/server/src/api/controllers/Subscription/Licenses.ts @@ -6,7 +6,6 @@ import config from 'config'; import { License, Plan } from 'system/models'; import BaseController from 'api/controllers/BaseController'; import LicenseService from 'services/Payment/License'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import { ILicensesFilter } from 'interfaces'; @@ -31,13 +30,13 @@ export default class LicensesController extends BaseController { router.post( '/generate', this.generateLicenseSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePlanExistance.bind(this)), asyncMiddleware(this.generateLicense.bind(this)), ); router.post( '/disable/:licenseId', - validateMiddleware, + this.validationResult, asyncMiddleware(this.validateLicenseExistance.bind(this)), asyncMiddleware(this.validateNotDisabledLicense.bind(this)), asyncMiddleware(this.disableLicense.bind(this)), @@ -45,7 +44,7 @@ export default class LicensesController extends BaseController { router.post( '/send', this.sendLicenseSchemaValidation, - validateMiddleware, + this.validationResult, asyncMiddleware(this.sendLicense.bind(this)), ); router.delete( diff --git a/server/src/api/controllers/Subscription/PaymentViaLicense.ts b/server/src/api/controllers/Subscription/PaymentViaLicense.ts index 56ceb1345..45b219c53 100644 --- a/server/src/api/controllers/Subscription/PaymentViaLicense.ts +++ b/server/src/api/controllers/Subscription/PaymentViaLicense.ts @@ -1,7 +1,6 @@ import { Inject, Service } from 'typedi'; import { Router, Request, Response } from 'express'; import { check } from 'express-validator'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import PaymentMethodController from 'api/controllers/Subscription/PaymentMethod'; import { @@ -26,7 +25,7 @@ export default class PaymentViaLicenseController extends PaymentMethodController router.post( '/payment', this.paymentViaLicenseSchema, - validateMiddleware, + this.validationResult, asyncMiddleware(this.validatePlanSlugExistance.bind(this)), asyncMiddleware(this.paymentViaLicense.bind(this)), ); diff --git a/server/src/api/controllers/Users.ts b/server/src/api/controllers/Users.ts index 0dc47c8ae..1c2bae9b8 100644 --- a/server/src/api/controllers/Users.ts +++ b/server/src/api/controllers/Users.ts @@ -222,19 +222,22 @@ export default class UsersController extends BaseController{ } if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { - return res.status(404).send({ - errors: [{ type: 'USER.NOT.FOUND', code: 100 }], - }); + return res.boom.badRequest( + 'User not found.', + { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] } + ); } if (error.errorType === 'user_already_active') { - return res.status(404).send({ - errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }], - }); + return res.boom.badRequest( + 'User is already active.', + { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] }, + ); } if (error.errorType === 'user_already_inactive') { - return res.status(404).send({ - errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }], - }); + return res.boom.badRequest( + 'User is already inactive.', + { errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] }, + ); } if (error.errorType === 'user_same_the_authorized_user') { return res.boom.badRequest( diff --git a/server/src/api/controllers/Views.js b/server/src/api/controllers/Views.js deleted file mode 100644 index 68063e77f..000000000 --- a/server/src/api/controllers/Views.js +++ /dev/null @@ -1,473 +0,0 @@ -import { difference, pick } from 'lodash'; -import express from 'express'; -import { - check, - query, - param, - oneOf, - validationResult, -} from 'express-validator'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import { - validateViewRoles, -} from 'lib/ViewRolesBuilder'; - -export default { - resource: 'items', - - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/', - this.listViews.validation, - asyncMiddleware(this.listViews.handler)); - - router.post('/', - this.createView.validation, - asyncMiddleware(this.createView.handler)); - - router.post('/:view_id', - this.editView.validation, - asyncMiddleware(this.editView.handler)); - - router.delete('/:view_id', - this.deleteView.validation, - asyncMiddleware(this.deleteView.handler)); - - router.get('/:view_id', - asyncMiddleware(this.getView.handler)); - - router.get('/:view_id/resource', - this.getViewResource.validation, - asyncMiddleware(this.getViewResource.handler)); - - return router; - }, - - /** - * List all views that associated with the given resource. - */ - listViews: { - validation: [ - oneOf([ - query('resource_name').exists().trim().escape(), - ], [ - query('resource_id').exists().isNumeric().toInt(), - ]), - ], - async handler(req, res) { - const { Resource, View } = req.models; - const filter = { ...req.query }; - - const resource = await Resource.query().onBuild((builder) => { - if (filter.resource_id) { - builder.where('id', filter.resource_id); - } - if (filter.resource_name) { - builder.where('name', filter.resource_name); - } - builder.first(); - }); - - const views = await View.query().where('resource_id', resource.id); - - return res.status(200).send({ views }); - }, - }, - - /** - * Retrieve view details of the given view id. - */ - getView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const { View } = req.models; - - const view = await View.query() - .where('id', viewId) - .withGraphFetched('resource') - .withGraphFetched('columns') - .withGraphFetched('roles.field') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }], - }); - } - return res.status(200).send({ view: view.toJSON() }); - }, - }, - - /** - * Delete the given view of the resource. - */ - deleteView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { View } = req.models; - const { view_id: viewId } = req.params; - const view = await View.query().findById(viewId); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }], - }); - } - if (view.predefined) { - return res.boom.badRequest(null, { - errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], - }); - } - await Promise.all([ - view.$relatedQuery('roles').delete(), - view.$relatedQuery('columns').delete(), - ]); - await View.query().where('id', view.id).delete(); - - return res.status(200).send({ id: view.id }); - }, - }, - - /** - * Creates a new view. - */ - createView: { - validation: [ - check('resource_name').exists().escape().trim(), - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - check('roles').isArray({ min: 1 }), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - check('columns').exists().isArray({ min: 1 }), - check('columns.*.key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { - Resource, - View, - ViewColumn, - ViewRole, - } = req.models; - const form = { roles: [], ...req.body }; - const resource = await Resource.query().where('name', form.resource_name).first(); - - if (!resource) { - return res.boom.notFound(null, { - errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }], - }); - } - const errorReasons = []; - const fieldsSlugs = form.roles.map((role) => role.field_key); - - const resourceFields = await resource.$relatedQuery('fields'); - const resourceFieldsKeys = resourceFields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resourceFields.map((field) => [field.key, field])); - const columnsKeys = form.columns.map((c) => c.key); - - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); - - if (notFoundFields.length > 0) { - errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); - - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } - // Validates the view conditional logic expression. - if (!validateViewRoles(form.roles, form.logic_expression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - if (errorReasons.length > 0) { - return res.boom.badRequest(null, { errors: errorReasons }); - } - - // Save view details. - const view = await View.query().insert({ - name: form.name, - predefined: false, - resource_id: resource.id, - roles_logic_expression: form.logic_expression, - }); - // Save view roles async operations. - const saveViewRolesOpers = []; - - form.roles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - - const saveViewRoleOper = ViewRole.query().insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - saveViewRolesOpers.push(saveViewRoleOper); - }); - - form.columns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - - const saveViewColumnOper = ViewColumn.query().insert({ - field_id: fieldModel.id, - view_id: view.id, - index: column.index, - }); - saveViewRolesOpers.push(saveViewColumnOper); - }); - await Promise.all(saveViewRolesOpers); - - return res.status(200).send({ id: view.id }); - }, - }, - - /** - * Edit the given custom view metadata. - */ - editView: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - - check('columns').exists().isArray({ min: 1 }), - - check('columns.*.id').optional().isNumeric().toInt(), - check('columns.*.key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - - check('roles').isArray(), - check('roles.*.id').optional().isNumeric().toInt(), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { - View, ViewRole, ViewColumn, Resource, - } = req.models; - const view = await View.query().where('id', viewId) - .withGraphFetched('roles.field') - .withGraphFetched('columns') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }], - }); - } - const form = { ...req.body }; - const resource = await Resource.query() - .where('id', view.resourceId) - .withGraphFetched('fields') - .withGraphFetched('views') - .first(); - - const errorReasons = []; - const fieldsSlugs = form.roles.map((role) => role.field_key); - const resourceFieldsKeys = resource.fields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field])); - const columnsKeys = form.columns.map((c) => c.key); - - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); - - // Validate not found resource fields keys. - if (notFoundFields.length > 0) { - errorReasons.push({ - type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields, - }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); - - // Validate not found view columns. - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } - // Validates the view conditional logic expression. - if (!validateViewRoles(form.roles, form.logic_expression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - - const viewRolesIds = view.roles.map((r) => r.id); - const viewColumnsIds = view.columns.map((c) => c.id); - - const formUpdatedRoles = form.roles.filter((r) => r.id); - const formInsertRoles = form.roles.filter((r) => !r.id); - - const formRolesIds = formUpdatedRoles.map((r) => r.id); - - const formUpdatedColumns = form.columns.filter((r) => r.id); - const formInsertedColumns = form.columns.filter((r) => !r.id); - const formColumnsIds = formUpdatedColumns.map((r) => r.id); - - const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds); - const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds); - - const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds); - const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds); - - // Validate the not found view roles ids. - if (notFoundViewRolesIds.length) { - errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds }); - } - // Validate the not found view columns ids. - if (notFoundViewColumnsIds.length) { - errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const asyncOpers = []; - - // Save view details. - await View.query() - .where('id', view.id) - .patch({ - name: form.name, - roles_logic_expression: form.logic_expression, - }); - - // Update view roles. - if (formUpdatedRoles.length > 0) { - formUpdatedRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const updateOper = ViewRole.query() - .where('id', role.id) - .update({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - }); - asyncOpers.push(updateOper); - }); - } - // Insert a new view roles. - if (formInsertRoles.length > 0) { - formInsertRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const insertOper = ViewRole.query() - .insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Delete view roles. - if (rolesIdsShouldDeleted.length > 0) { - const deleteOper = ViewRole.query() - .whereIn('id', rolesIdsShouldDeleted) - .delete(); - asyncOpers.push(deleteOper); - } - // Insert a new view columns to the storage. - if (formInsertedColumns.length > 0) { - formInsertedColumns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - const insertOper = ViewColumn.query() - .insert({ - field_id: fieldModel.id, - index: column.index, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Update the view columns on the storage. - if (formUpdatedColumns.length > 0) { - formUpdatedColumns.forEach((column) => { - const updateOper = ViewColumn.query() - .where('id', column.id) - .update({ - index: column.index, - }); - asyncOpers.push(updateOper); - }); - } - // Delete the view columns from the storage. - if (columnsIdsShouldDelete.length > 0) { - const deleteOper = ViewColumn.query() - .whereIn('id', columnsIdsShouldDelete) - .delete(); - asyncOpers.push(deleteOper); - } - await Promise.all(asyncOpers); - - return res.status(200).send(); - }, - }, - - /** - * Retrieve resource columns that associated to the given custom view. - */ - getViewResource: { - validation: [ - param('view_id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const { view_id: viewId } = req.params; - const { View } = req.models; - - const view = await View.query() - .where('id', viewId) - .withGraphFetched('resource.fields') - .first(); - - if (!view) { - return res.boom.notFound(null, { - errors: [{ type: 'VIEW.NOT.FOUND', code: 100 }], - }); - } - if (!view.resource) { - return res.boom.badData(null, { - errors: [{ type: 'VIEW.HAS.NOT.ASSOCIATED.RESOURCE', code: 200 }], - }); - } - - const resourceColumns = view.resource.fields - .filter((field) => field.columnable) - .map((field) => ({ - id: field.id, - label: field.labelName, - key: field.key, - })); - - return res.status(200).send({ - resource_slug: view.resource.name, - resource_columns: resourceColumns, - resource_fields: view.resource.fields, - }); - } - }, -}; diff --git a/server/src/api/controllers/Views.ts b/server/src/api/controllers/Views.ts index e82a8ae64..238d01b6b 100644 --- a/server/src/api/controllers/Views.ts +++ b/server/src/api/controllers/Views.ts @@ -1,19 +1,10 @@ import { Inject, Service } from 'typedi'; import { Router, Request, NextFunction, Response } from 'express'; -import { - check, - query, - param, - oneOf, - validationResult, -} from 'express-validator'; +import { check, param } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import { - validateViewRoles, -} from 'lib/ViewRolesBuilder'; import ViewsService from 'services/Views/ViewsService'; -import BaseController from './BaseController'; -import { IViewDTO } from 'interfaces'; +import BaseController from 'api/controllers/BaseController'; +import { IViewDTO, IViewEditDTO } from 'interfaces'; import { ServiceError } from 'exceptions'; @Service() @@ -27,66 +18,96 @@ export default class ViewsController extends BaseController{ router() { const router = Router(); - router.get('/', [ - ...this.viewDTOSchemaValidation, + router.get('/resource/:resource_model', [ + ...this.viewsListSchemaValidation, ], - asyncMiddleware(this.listViews) + this.validationResult, + asyncMiddleware(this.listResourceViews.bind(this)), + this.handlerServiceErrors, ); router.post('/', [ ...this.viewDTOSchemaValidation, ], - asyncMiddleware(this.createView) + this.validationResult, + asyncMiddleware(this.createView.bind(this)), + this.handlerServiceErrors ); - - router.post('/:view_id', [ - ...this.viewDTOSchemaValidation, + router.post('/:id', [ + ...this.viewParamSchemaValidation, + ...this.viewEditDTOSchemaValidation, ], - asyncMiddleware(this.editView) + this.validationResult, + asyncMiddleware(this.editView.bind(this)), + this.handlerServiceErrors, ); - - router.delete('/:view_id', [ + router.delete('/:id', [ ...this.viewParamSchemaValidation ], - asyncMiddleware(this.deleteView)); - - router.get('/:view_id', [ - ...this.viewParamSchemaValidation - ] - asyncMiddleware(this.getView) + this.validationResult, + asyncMiddleware(this.deleteView.bind(this)), + this.handlerServiceErrors, ); - - router.get('/:view_id/resource', [ + router.get('/:id', [ ...this.viewParamSchemaValidation ], - asyncMiddleware(this.getViewResource) + this.validationResult, + asyncMiddleware(this.getView.bind(this)), ); - return router; } + /** + * New view DTO schema validation. + */ get viewDTOSchemaValidation() { return [ - check('resource_name').exists().escape().trim(), + check('resource_model').exists().escape().trim(), check('name').exists().escape().trim(), check('logic_expression').exists().trim().escape(), + check('roles').isArray({ min: 1 }), check('roles.*.field_key').exists().escape().trim(), check('roles.*.comparator').exists(), check('roles.*.value').exists(), check('roles.*.index').exists().isNumeric().toInt(), + check('columns').exists().isArray({ min: 1 }), - check('columns.*.key').exists().escape().trim(), + check('columns.*.field_key').exists().escape().trim(), + check('columns.*.index').exists().isNumeric().toInt(), + ]; + } + + /** + * Edit view DTO schema validation. + */ + get viewEditDTOSchemaValidation() { + return [ + check('name').exists().escape().trim(), + check('logic_expression').exists().trim().escape(), + + check('roles').isArray({ min: 1 }), + check('roles.*.field_key').exists().escape().trim(), + check('roles.*.comparator').exists(), + check('roles.*.value').exists(), + check('roles.*.index').exists().isNumeric().toInt(), + + check('columns').exists().isArray({ min: 1 }), + check('columns.*.field_key').exists().escape().trim(), check('columns.*.index').exists().isNumeric().toInt(), ]; } get viewParamSchemaValidation() { return [ - - ] + param('id').exists().isNumeric().toInt(), + ]; } - + get viewsListSchemaValidation() { + return [ + param('resource_model').exists().trim().escape(), + ] + } /** * List all views that associated with the given resource. @@ -94,12 +115,12 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - listViews(req: Request, res: Response, next: NextFunction) { + async listResourceViews(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const filter = req.query; + const { resource_model: resourceModel } = req.params; try { - const views = this.viewsService.listViews(tenantId, filter); + const views = await this.viewsService.listResourceViews(tenantId, resourceModel); return res.status(200).send({ views }); } catch (error) { next(error); @@ -107,16 +128,17 @@ export default class ViewsController extends BaseController{ } /** - * + * Retrieve view details with assocaited roles and columns. * @param {Request} req * @param {Response} res * @param {NextFunction} next */ - getView(req: Request, res: Response, next: NextFunction) { + async getView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const { id: viewId } = req.params; try { - const view = this.viewsService.getView(tenantId, viewId); + const view = await this.viewsService.getView(tenantId, viewId); return res.status(200).send({ view }); } catch (error) { next(error); @@ -129,13 +151,13 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - createView(req: Request, res: Response, next: NextFunction) { + async createView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const viewDTO: IViewDTO = this.matchedBodyData(req); try { - await this.viewsService.newView(tenantId, viewDTO); - return res.status(200).send({ id: 1 }); + const view = await this.viewsService.newView(tenantId, viewDTO); + return res.status(200).send({ id: view.id }); } catch (error) { next(error); } @@ -147,10 +169,10 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - editView(req: Request, res: Response, next: NextFunction) { + async editView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: viewId } = req.params; - const { body: viewEditDTO } = req; + const viewEditDTO: IViewEditDTO = this.matchedBodyData(req); try { await this.viewsService.editView(tenantId, viewId, viewEditDTO); @@ -166,7 +188,7 @@ export default class ViewsController extends BaseController{ * @param {Response} res - * @param {NextFunction} next - */ - deleteView(req: Request, res: Response, next: NextFunction) { + async deleteView(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: viewId } = req.params; @@ -187,6 +209,16 @@ export default class ViewsController extends BaseController{ */ handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { + if (error.errorType === 'VIEW_NAME_NOT_UNIQUE') { + return res.boom.badRequest(null, { + errors: [{ type: 'VIEW_NAME_NOT_UNIQUE', code: 110 }], + }); + } + if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150, }], + }); + } if (error.errorType === 'INVALID_LOGIC_EXPRESSION') { return res.boom.badRequest(null, { errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }], @@ -212,6 +244,17 @@ export default class ViewsController extends BaseController{ errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], }); } + if (error.errorType === 'RESOURCE_FIELDS_KEYS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', code: 300 }], + }) + } + if (error.errorType === 'RESOURCE_COLUMNS_KEYS_NOT_FOUND') { + return res.boom.badRequest(null, { + errors: [{ type: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', code: 310 }], + }) + } } + next(error); } }; diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 3cfa75f3e..12dc5138b 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -87,18 +87,18 @@ export default () => { dashboard.use('/accounts', Container.get(Accounts).router()); dashboard.use('/account_types', Container.get(AccountTypes).router()); dashboard.use('/manual-journals', Container.get(ManualJournals).router()); - dashboard.use('/views', Views.router()); + dashboard.use('/views', Container.get(Views).router()); dashboard.use('/items', Container.get(Items).router()); dashboard.use('/item_categories', Container.get(ItemCategories).router()); dashboard.use('/expenses', Container.get(Expenses).router()); dashboard.use('/financial_statements', FinancialStatements.router()); - dashboard.use('/sales', Container.get(Sales).router()); dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/vendors', Container.get(Vendors).router()); - dashboard.use('/purchases', Container.get(Purchases).router()); - dashboard.use('/resources', Resources.router()); + // dashboard.use('/sales', Container.get(Sales).router()); + // dashboard.use('/purchases', Container.get(Purchases).router()); + dashboard.use('/resources', Container.get(Resources).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); - dashboard.use('/media', Media.router()); + dashboard.use('/media', Container.get(Media).router()); app.use('/', dashboard); diff --git a/server/src/api/middleware/TenantDependencyInjection.ts b/server/src/api/middleware/TenantDependencyInjection.ts index fed2b4219..c8c447bca 100644 --- a/server/src/api/middleware/TenantDependencyInjection.ts +++ b/server/src/api/middleware/TenantDependencyInjection.ts @@ -17,6 +17,8 @@ export default (req: Request, tenant: ITenant) => { const repositories = tenantServices.repositories(tenantId) const cacheInstance = tenantServices.cache(tenantId); + tenantServices.setI18nLocals(tenantId, { __: req.__ }); + req.knex = knexInstance; req.organizationId = organizationId; req.tenant = tenant; diff --git a/server/src/api/middleware/validateMiddleware.js b/server/src/api/middleware/validateMiddleware.js deleted file mode 100644 index 266680e9a..000000000 --- a/server/src/api/middleware/validateMiddleware.js +++ /dev/null @@ -1,13 +0,0 @@ -import { validationResult } from 'express-validator'; - -export default (req, res, next) => { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - next(); -} \ No newline at end of file diff --git a/server/src/config/index.js b/server/src/config/index.js index 1258c04ce..7ce065268 100644 --- a/server/src/config/index.js +++ b/server/src/config/index.js @@ -24,6 +24,7 @@ export default { db_user: process.env.SYSTEM_DB_USER, db_password: process.env.SYSTEM_DB_PASSWORD, db_name: process.env.SYSTEM_DB_NAME, + charset: process.env.SYSTEM_DB_CHARSET, migrations_dir: process.env.SYSTEM_MIGRATIONS_DIR, seeds_dir: process.env.SYSTEM_SEEDS_DIR, }, diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js index 462142dfd..8504d3fdf 100644 --- a/server/src/data/ResourceFieldsKeys.js +++ b/server/src/data/ResourceFieldsKeys.js @@ -2,7 +2,7 @@ export default { // Expenses. - 'expenses': { + expense: { payment_date: { column: 'payment_date', }, @@ -10,9 +10,12 @@ export default { column: 'payment_account_id', relation: 'accounts.id', }, - total_amount: { + amount: { column: 'total_amount', }, + currency_code: { + column: 'currency_code', + }, reference_no: { column: 'reference_no' }, @@ -30,7 +33,7 @@ export default { }, // Accounts - 'accounts': { + Account: { name: { column: 'name', }, @@ -72,23 +75,106 @@ export default { }, // Items - 'items': { - 'type': { + item: { + type: { column: 'type', }, - 'name': { + name: { column: 'name', }, + sellable: { + column: 'sellable', + }, + purchasable: { + column: 'purchasable', + }, + sell_price: { + column: 'sell_price' + }, + cost_price: { + column: 'cost_price', + }, + currency_code: { + column: 'currency_code', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + sell_description: { + column: 'sell_description', + }, + purchase_description: { + column: 'purchase_description', + }, + quantity_on_hand: { + column: 'quantity_on_hand', + }, + note: { + column: 'note', + }, + category: { + column: 'category_id', + relation: 'categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + } + }, + + // Item category. + item_category: { + name: { + column: 'name', + }, + description: { + column: 'description', + }, + parent_category_id: { + column: 'parent_category_id', + relation: 'items_categories.id', + relationColumn: 'items_categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + cost_method: { + column: 'cost_method', + }, }, // Manual Journals - manual_journals: { + manual_journal: { date: { column: 'date', }, - created_at: { - column: 'created_at', - }, journal_number: { column: 'journal_number', }, @@ -112,5 +198,8 @@ export default { journal_type: { column: 'journal_type', }, + created_at: { + column: 'created_at', + }, } }; diff --git a/server/src/database/migrations/20190822214242_create_users_table.js b/server/src/database/migrations/20190822214242_create_users_table.js deleted file mode 100644 index fb9ee35f6..000000000 --- a/server/src/database/migrations/20190822214242_create_users_table.js +++ /dev/null @@ -1,21 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('users', (table) => { - table.increments(); - table.string('first_name'); - table.string('last_name'); - table.string('email').unique(); - table.string('phone_number').unique(); - table.boolean('active'); - table.integer('role_id').unique(); - table.string('language'); - table.date('last_login_at'); - - table.date('invite_accepted_at'); - table.timestamps(); - }).raw('ALTER TABLE `USERS` AUTO_INCREMENT = 1000');; -}; - -exports.down = function (knex) { - return knex.schema.dropTableIfExists('users'); -}; diff --git a/server/src/database/migrations/20190822214904_create_account_types_table.js b/server/src/database/migrations/20190822214302_create_account_types_table.js similarity index 76% rename from server/src/database/migrations/20190822214904_create_account_types_table.js rename to server/src/database/migrations/20190822214302_create_account_types_table.js index c580efe83..4515e3338 100644 --- a/server/src/database/migrations/20190822214904_create_account_types_table.js +++ b/server/src/database/migrations/20190822214302_create_account_types_table.js @@ -2,10 +2,9 @@ exports.up = (knex) => { return knex.schema.createTable('account_types', (table) => { table.increments(); - table.string('name'); - table.string('key'); - table.string('normal'); - table.string('root_type'); + table.string('key').index(); + table.string('normal').index(); + table.string('root_type').index(); table.string('child_type'); table.boolean('balance_sheet'); table.boolean('income_sheet'); diff --git a/server/src/database/migrations/20190822214303_create_accounts_table.js b/server/src/database/migrations/20190822214303_create_accounts_table.js new file mode 100644 index 000000000..d2dd03580 --- /dev/null +++ b/server/src/database/migrations/20190822214303_create_accounts_table.js @@ -0,0 +1,20 @@ + +exports.up = function (knex) { + return knex.schema.createTable('accounts', (table) => { + table.increments('id').comment('Auto-generated id');; + table.string('name').index(); + table.string('slug'); + table.integer('account_type_id').unsigned().references('id').inTable('account_types'); + table.integer('parent_account_id').unsigned().references('id').inTable('accounts'); + table.string('code', 10).index(); + table.text('description'); + table.boolean('active').defaultTo(true).index(); + table.integer('index').unsigned(); + table.boolean('predefined').defaultTo(false).index(); + table.decimal('amount', 15, 5); + table.string('currency_code', 3).index(); + table.timestamps(); + }).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000'); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('accounts'); diff --git a/server/src/database/migrations/20190822214303_create_items_table.js b/server/src/database/migrations/20190822214303_create_items_table.js deleted file mode 100644 index 09516bfc1..000000000 --- a/server/src/database/migrations/20190822214303_create_items_table.js +++ /dev/null @@ -1,27 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('items', (table) => { - table.increments(); - table.string('name'); - table.string('type'); - table.string('sku'); - table.boolean('sellable'); - table.boolean('purchasable'); - table.decimal('sell_price', 13, 3).unsigned(); - table.decimal('cost_price', 13, 3).unsigned(); - table.string('currency_code', 3); - table.string('picture_uri'); - table.integer('cost_account_id').unsigned(); - table.integer('sell_account_id').unsigned(); - table.integer('inventory_account_id').unsigned(); - table.text('sell_description').nullable(); - table.text('purchase_description').nullable(); - table.integer('quantity_on_hand'); - table.text('note').nullable(); - table.integer('category_id').unsigned(); - table.integer('user_id').unsigned(); - table.timestamps(); - }).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000');; -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('items'); diff --git a/server/src/database/migrations/20190822214304_create_accounts_table.js b/server/src/database/migrations/20190822214304_create_accounts_table.js deleted file mode 100644 index 038116275..000000000 --- a/server/src/database/migrations/20190822214304_create_accounts_table.js +++ /dev/null @@ -1,20 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('accounts', (table) => { - table.bigIncrements('id').comment('Auto-generated id');; - table.string('name'); - table.string('slug'); - table.integer('account_type_id').unsigned(); - table.integer('parent_account_id').unsigned(); - table.string('code', 10); - table.text('description'); - table.boolean('active').defaultTo(true); - table.integer('index').unsigned(); - table.boolean('predefined').defaultTo(false); - table.decimal('amount', 15, 5); - table.string('currency_code', 3); - table.timestamps(); - }).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000'); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('accounts'); diff --git a/server/src/database/migrations/20190822214304_create_items_categories_table.js b/server/src/database/migrations/20190822214304_create_items_categories_table.js new file mode 100644 index 000000000..c807352f5 --- /dev/null +++ b/server/src/database/migrations/20190822214304_create_items_categories_table.js @@ -0,0 +1,19 @@ + +exports.up = function (knex) { + return knex.schema.createTable('items_categories', (table) => { + table.increments(); + table.string('name').index(); + table.integer('parent_category_id').unsigned().references('id').inTable('items_categories'); + table.text('description'); + table.integer('user_id').unsigned().index(); + + table.integer('cost_account_id').unsigned().references('id').inTable('accounts'); + table.integer('sell_account_id').unsigned().references('id').inTable('accounts'); + table.integer('inventory_account_id').unsigned().references('id').inTable('accounts'); + + table.string('cost_method'); + table.timestamps(); + }); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('items_categories'); diff --git a/server/src/database/migrations/20190822214306_create_items_categories_table.js b/server/src/database/migrations/20190822214306_create_items_categories_table.js deleted file mode 100644 index a8b189137..000000000 --- a/server/src/database/migrations/20190822214306_create_items_categories_table.js +++ /dev/null @@ -1,19 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('items_categories', (table) => { - table.increments(); - table.string('name'); - table.integer('parent_category_id').unsigned(); - table.text('description'); - table.integer('user_id').unsigned(); - - table.integer('cost_account_id').unsigned(); - table.integer('sell_account_id').unsigned(); - table.integer('inventory_account_id').unsigned(); - - table.string('cost_method'); - table.timestamps(); - }); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('items_categories'); diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js new file mode 100644 index 000000000..ff79afd56 --- /dev/null +++ b/server/src/database/migrations/20190822214306_create_items_table.js @@ -0,0 +1,27 @@ + +exports.up = function (knex) { + return knex.schema.createTable('items', (table) => { + table.increments(); + table.string('name').index(); + table.string('type').index(); + table.string('sku'); + table.boolean('sellable').index(); + table.boolean('purchasable').index(); + table.decimal('sell_price', 13, 3).unsigned(); + table.decimal('cost_price', 13, 3).unsigned(); + table.string('currency_code', 3); + table.string('picture_uri'); + table.integer('cost_account_id').nullable().unsigned().references('id').inTable('accounts'); + table.integer('sell_account_id').nullable().unsigned().references('id').inTable('accounts'); + table.integer('inventory_account_id').unsigned().references('id').inTable('accounts'); + table.text('sell_description').nullable(); + table.text('purchase_description').nullable(); + table.integer('quantity_on_hand'); + table.text('note').nullable(); + table.integer('category_id').unsigned().index().references('id').inTable('items_categories'); + table.integer('user_id').unsigned().index(); + table.timestamps(); + }).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000'); +}; + +exports.down = (knex) => knex.schema.dropTableIfExists('items'); diff --git a/server/src/database/migrations/20190822214905_create_views_table.js b/server/src/database/migrations/20190822214903_create_views_table.js similarity index 82% rename from server/src/database/migrations/20190822214905_create_views_table.js rename to server/src/database/migrations/20190822214903_create_views_table.js index e238d7ccb..53099e854 100644 --- a/server/src/database/migrations/20190822214905_create_views_table.js +++ b/server/src/database/migrations/20190822214903_create_views_table.js @@ -2,9 +2,9 @@ exports.up = function (knex) { return knex.schema.createTable('views', (table) => { table.increments(); - table.string('name'); + table.string('name').index(); table.boolean('predefined'); - table.string('resource_model'); + table.string('resource_model').index(); table.boolean('favourite'); table.string('roles_logic_expression'); table.timestamps(); diff --git a/server/src/database/migrations/20190822214302_create_settings_table.js b/server/src/database/migrations/20190822214904_create_settings_table.js similarity index 71% rename from server/src/database/migrations/20190822214302_create_settings_table.js rename to server/src/database/migrations/20190822214904_create_settings_table.js index b6aba4bef..65f3f4fdc 100644 --- a/server/src/database/migrations/20190822214302_create_settings_table.js +++ b/server/src/database/migrations/20190822214904_create_settings_table.js @@ -2,10 +2,10 @@ exports.up = function (knex) { return knex.schema.createTable('settings', (table) => { table.increments(); - table.integer('user_id').unsigned(); - table.string('group'); + table.integer('user_id').unsigned().index(); + table.string('group').index(); table.string('type'); - table.string('key'); + table.string('key').index(); table.string('value'); }).raw('ALTER TABLE `SETTINGS` AUTO_INCREMENT = 2000'); }; diff --git a/server/src/database/migrations/20190822214905_create_views_columns.js b/server/src/database/migrations/20190822214905_create_views_columns.js index 5b902fbf3..4fc76e399 100644 --- a/server/src/database/migrations/20190822214905_create_views_columns.js +++ b/server/src/database/migrations/20190822214905_create_views_columns.js @@ -2,7 +2,7 @@ exports.up = function (knex) { return knex.schema.createTable('view_has_columns', (table) => { table.increments(); - table.integer('view_id').unsigned(); + table.integer('view_id').unsigned().index().references('id').inTable('views'); table.string('field_key'); table.integer('index').unsigned(); }).raw('ALTER TABLE `ITEMS_CATEGORIES` AUTO_INCREMENT = 1000'); diff --git a/server/src/database/migrations/20190822214905_create_views_roles_table.js b/server/src/database/migrations/20190822214905_create_views_roles_table.js index f96a15cbe..e9add11eb 100644 --- a/server/src/database/migrations/20190822214905_create_views_roles_table.js +++ b/server/src/database/migrations/20190822214905_create_views_roles_table.js @@ -3,10 +3,10 @@ exports.up = function (knex) { return knex.schema.createTable('view_roles', (table) => { table.increments(); table.integer('index'); - table.string('field_key'); + table.string('field_key').index(); table.string('comparator'); table.string('value'); - table.integer('view_id').unsigned(); + table.integer('view_id').unsigned().index().references('id').inTable('views'); }).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200104232644_create_contacts_table.js b/server/src/database/migrations/20200104232644_create_contacts_table.js new file mode 100644 index 000000000..ccc7e13f0 --- /dev/null +++ b/server/src/database/migrations/20200104232644_create_contacts_table.js @@ -0,0 +1,49 @@ + +exports.up = function(knex) { + return knex.schema.createTable('contacts', table => { + table.increments(); + + table.string('contact_service'); + table.string('contact_type'); + + table.decimal('balance', 13, 3).defaultTo(0); + table.decimal('opening_balance', 13, 3).defaultTo(0); + + table.string('first_name').nullable(); + table.string('last_name').nullable(); + table.string('company_name').nullable(); + + table.string('display_name'); + + table.string('email').nullable(); + table.string('work_phone').nullable(); + table.string('personal_phone').nullable(); + + table.string('billing_address_1').nullable(); + table.string('billing_address_2').nullable(); + table.string('billing_address_city').nullable(); + table.string('billing_address_country').nullable(); + table.string('billing_address_email').nullable(); + table.string('billing_address_zipcode').nullable(); + table.string('billing_address_phone').nullable(); + table.string('billing_address_state').nullable(), + + table.string('shipping_address_1').nullable(); + table.string('shipping_address_2').nullable(); + table.string('shipping_address_city').nullable(); + table.string('shipping_address_country').nullable(); + table.string('shipping_address_email').nullable(); + table.string('shipping_address_zipcode').nullable(); + table.string('shipping_address_phone').nullable(); + table.string('shipping_address_state').nullable(); + + table.text('note'); + table.boolean('active').defaultTo(true); + + table.timestamps(); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('contacts'); +}; diff --git a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js index bc02d6b4a..42adc70fb 100644 --- a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js +++ b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js @@ -4,17 +4,17 @@ exports.up = function(knex) { table.increments(); table.decimal('credit', 13, 3); table.decimal('debit', 13, 3); - table.string('transaction_type'); - table.string('reference_type'); - table.integer('reference_id'); - table.integer('account_id').unsigned(); - table.string('contact_type').nullable(); - table.integer('contact_id').unsigned().nullable(); + table.string('transaction_type').index(); + table.string('reference_type').index(); + table.integer('reference_id').index(); + table.integer('account_id').unsigned().index().references('id').inTable('accounts'); + table.string('contact_type').nullable().index(); + table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts'); table.string('note'); table.boolean('draft').defaultTo(false); - table.integer('user_id').unsigned(); + table.integer('user_id').unsigned().index(); table.integer('index').unsigned(); - table.date('date'); + table.date('date').index(); table.timestamps(); }).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200105013334_create_options_table.js b/server/src/database/migrations/20200105013334_create_options_table.js deleted file mode 100644 index 2003c6c9b..000000000 --- a/server/src/database/migrations/20200105013334_create_options_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = function(knex) { - return knex.schema.createTable('options', (table) => { - table.increments(); - table.string('key'); - table.string('value'); - table.string('group'); - table.string('type'); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('options'); -}; diff --git a/server/src/database/migrations/20200105014405_create_expenses_table.js b/server/src/database/migrations/20200105014405_create_expenses_table.js index 40030dce3..5f4382e35 100644 --- a/server/src/database/migrations/20200105014405_create_expenses_table.js +++ b/server/src/database/migrations/20200105014405_create_expenses_table.js @@ -5,12 +5,12 @@ exports.up = function(knex) { table.decimal('total_amount', 13, 3); table.string('currency_code', 3); table.text('description'); - table.integer('payment_account_id').unsigned(); - table.integer('payee_id').unsigned(); + table.integer('payment_account_id').unsigned().references('id').inTable('accounts'); + table.integer('payee_id').unsigned().references('id').inTable('contacts');; table.string('reference_no'); - table.date('published_at'); - table.integer('user_id').unsigned(); - table.date('payment_date'); + table.date('published_at').index(); + table.integer('user_id').unsigned().index(); + table.date('payment_date').index(); table.timestamps(); }).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200105195823_create_manual_journals_table.js b/server/src/database/migrations/20200105195823_create_manual_journals_table.js index 69ba8a965..b4200c29a 100644 --- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js +++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js @@ -2,15 +2,15 @@ exports.up = function(knex) { return knex.schema.createTable('manual_journals', (table) => { table.increments(); - table.string('journal_number'); - table.string('reference'); - table.string('journal_type'); + table.string('journal_number').index(); + table.string('reference').index(); + table.string('journal_type').index(); table.decimal('amount', 13, 3); - table.date('date'); - table.boolean('status').defaultTo(false); + table.date('date').index(); + table.boolean('status').defaultTo(false).index(); table.string('description'); table.string('attachment_file'); - table.integer('user_id').unsigned(); + table.integer('user_id').unsigned().index(); table.timestamps(); }).raw('ALTER TABLE `MANUAL_JOURNALS` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200419171451_create_currencies_table.js b/server/src/database/migrations/20200419171451_create_currencies_table.js index de6201bb7..c2d847140 100644 --- a/server/src/database/migrations/20200419171451_create_currencies_table.js +++ b/server/src/database/migrations/20200419171451_create_currencies_table.js @@ -2,8 +2,8 @@ exports.up = function(knex) { return knex.schema.createTable('currencies', table => { table.increments(); - table.string('currency_name'); - table.string('currency_code', 4); + table.string('currency_name').index(); + table.string('currency_code', 4).index(); table.timestamps(); }).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200419191832_create_exchange_rates_table.js b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js index e347c42f7..99db76530 100644 --- a/server/src/database/migrations/20200419191832_create_exchange_rates_table.js +++ b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js @@ -2,9 +2,9 @@ exports.up = function(knex) { return knex.schema.createTable('exchange_rates', table => { table.increments(); - table.string('currency_code', 4); + table.string('currency_code', 4).index(); table.decimal('exchange_rate'); - table.date('date'); + table.date('date').index(); table.timestamps(); }).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000'); }; diff --git a/server/src/database/migrations/20200503032011_create_media_links_table.js b/server/src/database/migrations/20200503032011_create_media_links_table.js index 52d5b4ca7..31d26be4b 100644 --- a/server/src/database/migrations/20200503032011_create_media_links_table.js +++ b/server/src/database/migrations/20200503032011_create_media_links_table.js @@ -2,9 +2,9 @@ exports.up = function(knex) { return knex.schema.createTable('media_links', table => { table.increments(); - table.string('model_name'); - table.integer('media_id').unsigned(); - table.integer('model_id').unsigned(); + table.string('model_name').index(); + table.integer('media_id').unsigned().references('id').inTable('media'); + table.integer('model_id').unsigned().index(); }) }; diff --git a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js index 58568503b..b383bd668 100644 --- a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js +++ b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js @@ -2,11 +2,11 @@ exports.up = function(knex) { return knex.schema.createTable('expense_transaction_categories', table => { table.increments(); - table.integer('expense_account_id').unsigned(); + table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts'); table.integer('index').unsigned(); table.text('description'); table.decimal('amount', 13, 3); - table.integer('expense_id').unsigned(); + table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions'); table.timestamps(); }).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');; }; diff --git a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js index 2dbce39ec..82d2d3ebe 100644 --- a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js +++ b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js @@ -3,15 +3,15 @@ exports.up = function(knex) { return knex.schema.createTable('sales_estimates', (table) => { table.increments(); table.decimal('amount', 13, 3); - table.integer('customer_id').unsigned(); - table.date('estimate_date'); - table.date('expiration_date'); + table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); + table.date('estimate_date').index(); + table.date('expiration_date').index(); table.string('reference'); - table.string('estimate_number'); + table.string('estimate_number').index(); table.text('note'); table.text('terms_conditions'); - table.integer('user_id').unsigned(); + table.integer('user_id').unsigned().index(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js index 3adb04ba1..6b53bdd4d 100644 --- a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js +++ b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js @@ -3,9 +3,9 @@ exports.up = function(knex) { return knex.schema.createTable('sales_receipts', table => { table.increments(); table.decimal('amount', 13, 3); - table.integer('deposit_account_id').unsigned(); - table.integer('customer_id').unsigned(); - table.date('receipt_date'); + table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts'); + table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); + table.date('receipt_date').index(); table.string('reference_no'); table.string('email_send_to'); table.text('receipt_message'); diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js index 7f5b394b8..9fac2e9f2 100644 --- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js +++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js @@ -2,12 +2,12 @@ exports.up = function(knex) { return knex.schema.createTable('sales_invoices', table => { table.increments(); - table.integer('customer_id'); - table.date('invoice_date'); + table.integer('customer_id').unsigned().index().references('id').inTable('contacts') + table.date('invoice_date').index(); table.date('due_date'); - table.string('invoice_no'); + table.string('invoice_no').index(); table.string('reference_no'); - table.string('status'); + table.string('status').index(); table.text('invoice_message'); table.text('terms_conditions'); @@ -15,7 +15,7 @@ exports.up = function(knex) { table.decimal('balance', 13, 3); table.decimal('payment_amount', 13, 3); - table.string('inv_lot_number'); + table.string('inv_lot_number').index(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200715194514_create_payment_receives_table.js b/server/src/database/migrations/20200715194514_create_payment_receives_table.js index bf299c4ee..758b32869 100644 --- a/server/src/database/migrations/20200715194514_create_payment_receives_table.js +++ b/server/src/database/migrations/20200715194514_create_payment_receives_table.js @@ -3,14 +3,14 @@ const { knexSnakeCaseMappers } = require("objection"); exports.up = function(knex) { return knex.schema.createTable('payment_receives', (table) => { table.increments(); - table.integer('customer_id').unsigned(); - table.date('payment_date'); + table.integer('customer_id').unsigned().index().references('id').inTable('contacts'); + table.date('payment_date').index(); table.decimal('amount', 13, 3).defaultTo(0); - table.string('reference_no'); - table.integer('deposit_account_id').unsigned(); + table.string('reference_no').index(); + table.integer('deposit_account_id').unsigned().references('id').inTable('accounts'); table.string('payment_receive_no'); table.text('description'); - table.integer('user_id').unsigned(); + table.integer('user_id').unsigned().index(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js b/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js index 627e46cbe..ffc9c665e 100644 --- a/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js +++ b/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js @@ -2,8 +2,8 @@ exports.up = function(knex) { return knex.schema.createTable('payment_receives_entries', table => { table.increments(); - table.integer('payment_receive_id').unsigned(); - table.integer('invoice_id').unsigned(); + table.integer('payment_receive_id').unsigned().index().references('id').inTable('payment_receives'); + table.integer('invoice_id').unsigned().index().references('id').inTable('sales_invoices'); table.decimal('payment_amount').unsigned(); }) }; diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js index d6f3c2f7a..efc81c7d6 100644 --- a/server/src/database/migrations/20200719152005_create_bills_table.js +++ b/server/src/database/migrations/20200719152005_create_bills_table.js @@ -2,18 +2,18 @@ exports.up = function(knex) { return knex.schema.createTable('bills', (table) => { table.increments(); - table.integer('vendor_id').unsigned(); + table.integer('vendor_id').unsigned().index().references('id').inTable('contacts'); table.string('bill_number'); - table.date('bill_date'); - table.date('due_date'); + table.date('bill_date').index(); + table.date('due_date').index(); table.string('reference_no'); - table.string('status'); + table.string('status').index(); table.text('note'); table.decimal('amount', 13, 3).defaultTo(0); table.decimal('payment_amount', 13, 3).defaultTo(0); - table.string('inv_lot_number'); + table.string('inv_lot_number').index(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200719153909_create_bills_payments_table.js b/server/src/database/migrations/20200719153909_create_bills_payments_table.js index e069d0472..065f2bae6 100644 --- a/server/src/database/migrations/20200719153909_create_bills_payments_table.js +++ b/server/src/database/migrations/20200719153909_create_bills_payments_table.js @@ -2,14 +2,14 @@ exports.up = function(knex) { return knex.schema.createTable('bills_payments', table => { table.increments(); - table.integer('vendor_id').unsigned(); + table.integer('vendor_id').unsigned().index().references('id').inTable('contacts'); table.decimal('amount', 13, 3).defaultTo(0); - table.integer('payment_account_id'); - table.string('payment_number'); - table.date('payment_date'); + table.integer('payment_account_id').unsigned().references('id').inTable('accounts'); + table.string('payment_number').index(); + table.date('payment_date').index(); table.string('payment_method'); table.string('reference'); - table.integer('user_id').unsigned(); + table.integer('user_id').unsigned().index(); table.text('description'); table.timestamps(); }); diff --git a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js index ede2040c4..a62b4d905 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -2,20 +2,20 @@ exports.up = function(knex) { return knex.schema.createTable('inventory_transactions', table => { table.increments('id'); - table.date('date'); + table.date('date').index(); - table.string('direction'); + table.string('direction').index(); - table.integer('item_id').unsigned(); + table.integer('item_id').unsigned().index().references('id').inTable('items'); table.integer('quantity').unsigned(); table.decimal('rate', 13, 3).unsigned(); - table.integer('lot_number'); + table.integer('lot_number').index(); - table.string('transaction_type'); - table.integer('transaction_id').unsigned(); + table.string('transaction_type').index(); + table.integer('transaction_id').unsigned().index(); - table.integer('entry_id').unsigned(); + table.integer('entry_id').unsigned().index(); table.timestamps(); }); }; diff --git a/server/src/database/migrations/20200722173423_create_items_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js index 902ef253d..dc36d5b72 100644 --- a/server/src/database/migrations/20200722173423_create_items_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -2,11 +2,11 @@ exports.up = function(knex) { return knex.schema.createTable('items_entries', (table) => { table.increments(); - table.string('reference_type'); - table.string('reference_id'); + table.string('reference_type').index(); + table.string('reference_id').index(); table.integer('index').unsigned(); - table.integer('item_id'); + table.integer('item_id').unsigned().index().references('id').inTable('items'); table.text('description'); table.integer('discount').unsigned(); table.integer('quantity').unsigned(); diff --git a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js index 7dac8c241..30af9d50f 100644 --- a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js +++ b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js @@ -3,8 +3,8 @@ exports.up = function(knex) { return knex.schema.createTable('bills_payments_entries', table => { table.increments(); - table.integer('bill_payment_id').unsigned(); - table.integer('bill_id').unsigned(); + table.integer('bill_payment_id').unsigned().index().references('id').inTable('bills_payments'); + table.integer('bill_id').unsigned().index(); table.decimal('payment_amount', 13, 3).unsigned(); }) }; diff --git a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js index 51b31cf06..344bf30e4 100644 --- a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js +++ b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js @@ -2,19 +2,19 @@ exports.up = function(knex) { return knex.schema.createTable('inventory_cost_lot_tracker', table => { table.increments(); - table.date('date'); - table.string('direction'); + table.date('date').index(); + table.string('direction').index(); - table.integer('item_id').unsigned(); - table.integer('quantity').unsigned(); + table.integer('item_id').unsigned().index(); + table.integer('quantity').unsigned().index(); table.decimal('rate', 13, 3); table.integer('remaining'); table.integer('cost'); - table.integer('lot_number'); + table.integer('lot_number').index(); - table.string('transaction_type'); - table.integer('transaction_id').unsigned(); - table.integer('entry_id').unsigned(); + table.string('transaction_type').index(); + table.integer('transaction_id').unsigned().index(); + table.integer('entry_id').unsigned().index(); }); }; diff --git a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js index fd03d2f7d..ffe6b9b77 100644 --- a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js +++ b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js @@ -1,8 +1,14 @@ +import Container from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService' +import I18nMiddleware from 'api/middleware/I18nMiddleware'; + exports.up = function (knex) { + const tenancyService = Container.get(TenancyService); + const i18n = tenancyService.i18n(knex.userParams.tenantId); + return knex('account_types').insert([ { id: 1, - name: 'Fixed Asset', key: 'fixed_asset', normal: 'debit', root_type: 'asset', @@ -12,7 +18,6 @@ exports.up = function (knex) { }, { id: 2, - name: 'Current Asset', key: 'current_asset', normal: 'debit', root_type: 'asset', @@ -22,7 +27,6 @@ exports.up = function (knex) { }, { id: 14, - name: 'Other Asset', key: 'other_asset', normal: 'debit', root_type: 'asset', @@ -32,7 +36,6 @@ exports.up = function (knex) { }, { id: 3, - name: 'Long Term Liability', key: 'long_term_liability', normal: 'credit', root_type: 'liability', @@ -42,7 +45,6 @@ exports.up = function (knex) { }, { id: 4, - name: 'Current Liability', key: 'current_liability', normal: 'credit', root_type: 'liability', @@ -52,7 +54,6 @@ exports.up = function (knex) { }, { id: 13, - name: 'Other Liability', key: 'other_liability', normal: 'credit', root_type: 'liability', @@ -62,7 +63,6 @@ exports.up = function (knex) { }, { id: 5, - name: 'Equity', key: 'equity', normal: 'credit', root_type: 'equity', @@ -72,7 +72,6 @@ exports.up = function (knex) { }, { id: 6, - name: 'Expense', key: 'expense', normal: 'debit', root_type: 'expense', @@ -82,7 +81,6 @@ exports.up = function (knex) { }, { id: 10, - name: 'Other Expense', key: 'other_expense', normal: 'debit', root_type: 'expense', @@ -91,7 +89,6 @@ exports.up = function (knex) { }, { id: 7, - name: 'Income', key: 'income', normal: 'credit', root_type: 'income', @@ -101,7 +98,6 @@ exports.up = function (knex) { }, { id: 11, - name: 'Other Income', key: 'other_income', normal: 'credit', root_type: 'income', @@ -111,7 +107,6 @@ exports.up = function (knex) { }, { id: 12, - name: 'Cost of Goods Sold (COGS)', key: 'cost_of_goods_sold', normal: 'debit', root_type: 'expenses', @@ -121,7 +116,6 @@ exports.up = function (knex) { }, { id: 8, - name: 'Accounts Receivable (A/R)', key: 'accounts_receivable', normal: 'debit', root_type: 'asset', @@ -131,7 +125,6 @@ exports.up = function (knex) { }, { id: 9, - name: 'Accounts Payable (A/P)', key: 'accounts_payable', normal: 'credit', root_type: 'liability', diff --git a/server/src/database/seeds/core/20190423085240_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js similarity index 85% rename from server/src/database/seeds/core/20190423085240_seed_accounts.js rename to server/src/database/seeds/core/20190423085242_seed_accounts.js index cce8e6d27..58c29f3cc 100644 --- a/server/src/database/seeds/core/20190423085240_seed_accounts.js +++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js @@ -1,16 +1,18 @@ -import TenancyService from 'services/Tenancy/TenancyService' import Container from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService' exports.up = function (knex) { const tenancyService = Container.get(TenancyService); const i18n = tenancyService.i18n(knex.userParams.tenantId); + console.log(i18n); + return knex('accounts').then(() => { // Inserts seed entries return knex('accounts').insert([ { id: 1, - name: 'Petty Cash', + name: i18n.__('Petty Cash'), slug: 'petty-cash', account_type_id: 2, parent_account_id: null, @@ -22,7 +24,7 @@ exports.up = function (knex) { }, { id: 2, - name: 'Bank', + name: i18n.__('Bank'), slug: 'bank', account_type_id: 2, parent_account_id: null, @@ -34,7 +36,7 @@ exports.up = function (knex) { }, { id: 3, - name: 'Other Income', + name: i18n.__('Other Income'), slug: 'other-income', account_type_id: 7, parent_account_id: null, @@ -46,7 +48,7 @@ exports.up = function (knex) { }, { id: 4, - name: 'Interest Income', + name: i18n.__('Interest Income'), slug: 'interest-income', account_type_id: 7, parent_account_id: null, @@ -58,7 +60,7 @@ exports.up = function (knex) { }, { id: 5, - name: 'Opening Balance', + name: i18n.__('Opening Balance'), slug: 'opening-balance', account_type_id: 5, parent_account_id: null, @@ -70,7 +72,7 @@ exports.up = function (knex) { }, { id: 6, - name: 'Depreciation Expense', + name: i18n.__('Depreciation Expense'), slug: 'depreciation-expense', account_type_id: 6, parent_account_id: null, @@ -82,7 +84,7 @@ exports.up = function (knex) { }, { id: 7, - name: 'Interest Expense', + name: i18n.__('Interest Expense'), slug: 'interest-expense', account_type_id: 6, parent_account_id: null, @@ -94,7 +96,7 @@ exports.up = function (knex) { }, { id: 8, - name: 'Payroll Expenses', + name: i18n.__('Payroll Expenses'), slug: 'payroll-expenses', account_type_id: 6, parent_account_id: null, @@ -106,7 +108,7 @@ exports.up = function (knex) { }, { id: 9, - name: 'Other Expenses', + name: i18n.__('Other Expenses'), slug: 'other-expenses', account_type_id: 6, parent_account_id: null, @@ -118,7 +120,7 @@ exports.up = function (knex) { }, { id: 10, - name: 'Accounts Receivable', + name: i18n.__('Accounts Receivable'), slug: 'accounts-receivable', account_type_id: 8, parent_account_id: null, @@ -130,7 +132,7 @@ exports.up = function (knex) { }, { id: 11, - name: 'Accounts Payable', + name: i18n.__('Accounts Payable'), slug: 'accounts-payable', account_type_id: 9, parent_account_id: null, @@ -142,7 +144,7 @@ exports.up = function (knex) { }, { id: 12, - name: 'Cost of Goods Sold (COGS)', + name: i18n.__('Cost of Goods Sold (COGS)'), slug: 'cost-of-goods-sold', account_type_id: 12, predefined: 1, @@ -153,7 +155,7 @@ exports.up = function (knex) { }, { id: 13, - name: 'Inventory Asset', + name: i18n.__('Inventory Asset'), slug: 'inventory-asset', account_type_id: 14, predefined: 1, @@ -164,7 +166,7 @@ exports.up = function (knex) { }, { id: 14, - name: 'Sales of Product Income', + name: i18n.__('Sales of Product Income'), slug: 'sales-of-product-income', account_type_id: 7, predefined: 1, diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js index 87d383888..5e945978c 100644 --- a/server/src/database/seeds/core/20200810121807_seed_views.js +++ b/server/src/database/seeds/core/20200810121807_seed_views.js @@ -1,17 +1,20 @@ exports.up = (knex) => { + const tenancyService = Container.get(TenancyService); + const i18n = tenancyService.i18n(knex.userParams.tenantId); + // Deletes ALL existing entries return knex('views').del() .then(() => { // Inserts seed entries return knex('views').insert([ // Accounts - { id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, - { id: 5, name: 'Expenses', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 15, name: i18n.__('Inactive'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 1, name: i18n.__('Assets'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 2, name: i18n.__('Liabilities'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 3, name: i18n.__('Equity'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 4, name: i18n.__('Income'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, + { id: 5, name: i18n.__('Expenses'), roles_logic_expression: '1', resource_model: 'Account', predefined: true }, // Items // { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true }, diff --git a/server/src/interfaces/Register.ts b/server/src/interfaces/Authentication.ts similarity index 89% rename from server/src/interfaces/Register.ts rename to server/src/interfaces/Authentication.ts index 375c800ac..be86dfdd3 100644 --- a/server/src/interfaces/Register.ts +++ b/server/src/interfaces/Authentication.ts @@ -9,6 +9,11 @@ export interface IRegisterDTO { organizationName: string, }; +export interface ILoginDTO { + crediential: string, + password: string, +}; + export interface IPasswordReset { id: number, email: string, diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts index e0183cd8e..c2be84a73 100644 --- a/server/src/interfaces/Contact.ts +++ b/server/src/interfaces/Contact.ts @@ -178,8 +178,9 @@ export interface IVendorsFilter extends IDynamicListFilter { pageSize?: number, }; -export interface ICustomerFilter extends IDynamicListFilter { +export interface ICustomersFilter extends IDynamicListFilter { stringifiedFilterRoles?: string, page?: number, pageSize?: number, -}; \ No newline at end of file +}; + diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts index 8ed67b362..4a3263f0f 100644 --- a/server/src/interfaces/Expenses.ts +++ b/server/src/interfaces/Expenses.ts @@ -1,5 +1,16 @@ import { ISystemUser } from "./User"; +export interface IPaginationMeta { + total: number, + page: number, + pageSize: number, +}; + +export interface IExpensesFilter{ + page: number, + pageSize: number, +}; + export interface IExpense { id: number, totalAmount: number, @@ -53,4 +64,7 @@ export interface IExpensesService { deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise; publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise; + + getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>; + getExpense(tenantId: number, expenseId: number): Promise; } \ No newline at end of file diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index d0824056c..2887481ec 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -70,6 +70,6 @@ export interface IItemsService { export interface IItemsFilter extends IDynamicListFilter { stringifiedFilterRoles?: string, - page?: number, - pageSize?: number, + page: number, + pageSize: number, }; \ No newline at end of file diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index cc40c3611..2a1e0fc05 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -37,8 +37,8 @@ export interface IManualJournalDTO { export interface IManualJournalsFilter extends IDynamicListFilterDTO { stringifiedFilterRoles?: string, - page?: number, - pageSize?: number, + page: number, + pageSize: number, } export interface IManuaLJournalsService { @@ -48,5 +48,6 @@ export interface IManuaLJournalsService { deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; publishManualJournal(tenantId: number, manualJournalId: number): Promise; - getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise; + + getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>; } \ No newline at end of file diff --git a/server/src/interfaces/Media.ts b/server/src/interfaces/Media.ts new file mode 100644 index 000000000..6cd338583 --- /dev/null +++ b/server/src/interfaces/Media.ts @@ -0,0 +1,25 @@ + + +export interface IMedia { + id?: number, + attachmentFile: string, + createdAt?: Date, +}; + +export interface IMediaLink { + mediaId: number, + modelName: string, + modelId: number, +}; + +export interface IMediaLinkDTO { + modelName: string, + modelId: number, +}; + +export interface IMediaService { + linkMedia(tenantId: number, mediaId: number, modelId?: number, modelName?: string): Promise; + getMedia(tenantId: number, mediaId: number): Promise; + deleteMedia(tenantId: number, mediaId: number | number[]): Promise; + upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise; +} \ No newline at end of file diff --git a/server/src/interfaces/Model.ts b/server/src/interfaces/Model.ts new file mode 100644 index 000000000..91f20c15f --- /dev/null +++ b/server/src/interfaces/Model.ts @@ -0,0 +1,17 @@ + + +export interface IModel { + name: string, + tableName: string, + fields: { [key: string]: any, }, +}; + +export interface IFilterMeta { + sortOrder: string, + sortBy: string, +}; + +export interface IPaginationMeta { + pageSize: number, + page: number, +}; \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 1628c9da0..ac8850243 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -15,4 +15,9 @@ export interface ISaleInvoiceOTD { invoiceMessage: string, termsConditions: string, entries: any[], -} \ No newline at end of file +} + +export interface ISalesInvoicesFilter{ + page: number, + pageSize: number, +}; \ No newline at end of file diff --git a/server/src/interfaces/SaleReceipt.ts b/server/src/interfaces/SaleReceipt.ts new file mode 100644 index 000000000..1866a6cfd --- /dev/null +++ b/server/src/interfaces/SaleReceipt.ts @@ -0,0 +1,37 @@ +import { ISalesInvoicesFilter } from "./SaleInvoice"; + + +export interface ISaleReceipt { + id?: number, + customerId: number, + depositAccountId: number, + receiptDate: Date, + sendToEmail: string, + referenceNo: string, + receiptMessage: string, + statement: string, + entries: any[], +}; + +export interface ISalesReceiptsFilter { + +}; + +export interface ISaleReceiptDTO { + customerId: number, + depositAccountId: number, + receiptDate: Date, + sendToEmail: string, + referenceNo: string, + receiptMessage: string, + statement: string, + entries: any[], +}; + +export interface ISalesReceiptService { + createSaleReceipt(tenantId: number, saleReceiptDTO: ISaleReceiptDTO): Promise; + editSaleReceipt(tenantId: number, saleReceiptId: number): Promise; + + deleteSaleReceipt(tenantId: number, saleReceiptId: number): Promise; + salesReceiptsList(tennatid: number, salesReceiptsFilter: ISalesReceiptsFilter): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>; +}; \ No newline at end of file diff --git a/server/src/interfaces/View.ts b/server/src/interfaces/View.ts index 5e7ccbcf3..06197cae8 100644 --- a/server/src/interfaces/View.ts +++ b/server/src/interfaces/View.ts @@ -5,7 +5,10 @@ export interface IView { predefined: boolean, resourceModel: string, favourite: boolean, - rolesLogicRxpression: string, + rolesLogicExpression: string, + + roles: IViewRole[], + columns: IViewHasColumn[], }; export interface IViewRole { @@ -42,6 +45,8 @@ export interface IViewColumnDTO { export interface IViewDTO { name: string, logicExpression: string, + resourceModel: string, + roles: IViewRoleDTO[], columns: IViewColumnDTO[], }; @@ -49,12 +54,13 @@ export interface IViewDTO { export interface IViewEditDTO { name: string, logicExpression: string, + roles: IViewRoleDTO[], columns: IViewColumnDTO[], }; export interface IViewsService { - listViews(tenantId: number, resourceModel: string): Promise; + listResourceViews(tenantId: number, resourceModel: string): Promise; newView(tenantId: number, viewDTO: IViewDTO): Promise; editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise; deleteView(tenantId: number, viewId: number): Promise; diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 17852dea8..61d02b690 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -1,3 +1,5 @@ + +export * from './Model'; export * from './InventoryTransaction'; export * from './BillPayment'; export * from './InventoryCostMethod'; @@ -9,7 +11,7 @@ export * from './Payment'; export * from './SaleInvoice'; export * from './PaymentReceive'; export * from './SaleEstimate'; -export * from './Register'; +export * from './Authentication'; export * from './User'; export * from './Metable'; export * from './Options'; @@ -22,4 +24,5 @@ export * from './Tenancy'; export * from './View'; export * from './ManualJournal'; export * from './Currency'; -export * from './ExchangeRate'; \ No newline at end of file +export * from './ExchangeRate'; +export * from './Media'; \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilter.js b/server/src/lib/DynamicFilter/DynamicFilter.ts similarity index 54% rename from server/src/lib/DynamicFilter/DynamicFilter.js rename to server/src/lib/DynamicFilter/DynamicFilter.ts index 62000f11f..cb795ed88 100644 --- a/server/src/lib/DynamicFilter/DynamicFilter.js +++ b/server/src/lib/DynamicFilter/DynamicFilter.ts @@ -1,15 +1,20 @@ -import { uniqBy } from 'lodash'; +import { forEach, uniqBy } from 'lodash'; import { buildFilterRolesJoins, } from 'lib/ViewRolesBuilder'; +import { IModel } from 'interfaces'; export default class DynamicFilter { + model: IModel; + tableName: string; + /** * Constructor. * @param {String} tableName - */ - constructor(tableName) { - this.tableName = tableName; + constructor(model) { + this.model = model; + this.tableName = model.tableName; this.filters = []; } @@ -18,7 +23,7 @@ export default class DynamicFilter { * @param {*} filterRole - */ setFilter(filterRole) { - filterRole.setTableName(this.tableName); + filterRole.setModel(this.model); this.filters.push(filterRole); } @@ -38,7 +43,23 @@ export default class DynamicFilter { buildersCallbacks.forEach((builderCallback) => { builderCallback(builder); }); - buildFilterRolesJoins(this.tableName, uniqBy(tableColumns, 'columnKey'))(builder); + buildFilterRolesJoins(this.model, uniqBy(tableColumns, 'columnKey'))(builder); }; } + + /** + * Retrieve response metadata from all filters adapters. + */ + getResponseMeta() { + const responseMeta = {}; + + this.filters.forEach((filter) => { + const { responseMeta: filterMeta } = filter; + + forEach(filterMeta, (value, key) => { + responseMeta[key] = value; + }); + }); + return responseMeta; + } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts index 4a8366a8d..0a64d5a43 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts @@ -14,14 +14,14 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { constructor(filterRoles: IFilterRole[]) { super(); this.filterRoles = filterRoles; + this.setResponseMeta(); } private buildLogicExpression(): string { let expression = ''; this.filterRoles.forEach((role, index) => { expression += (index === 0) ? - `${role.index} ` : - `${role.condition} ${role.index} `; + `${role.index} ` : `${role.condition} ${role.index} `; }); return expression.trim(); } @@ -32,7 +32,16 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { buildQuery() { return (builder) => { const logicExpression = this.buildLogicExpression(); - buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder); + buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder); + }; + } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + filterRoles: this.filterRoles }; } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts index 5ccfe111b..006008ddd 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts @@ -1,10 +1,13 @@ -import { IFilterRole, IDynamicFilter } from "interfaces"; +import { IFilterRole, IDynamicFilter, IModel } from "interfaces"; export default class DynamicFilterAbstructor implements IDynamicFilter { filterRoles: IFilterRole[] = []; tableName: string; + model: IModel; + responseMeta: { [key: string]: any } = {}; - setTableName(tableName) { - this.tableName = tableName; + setModel(model: IModel) { + this.model = model; + this.tableName = model.tableName; } } \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts index 32b35f555..3d413aa5e 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts @@ -16,10 +16,11 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { fieldKey: sortByFieldKey, order: sortDirection, }; + this.setResponseMeta(); } validate() { - validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey); + validateFieldKeyExistance(this.model, this.sortRole.fieldKey); } /** @@ -27,7 +28,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { */ buildQuery() { return (builder) => { - const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey); + const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey); const comparatorColumn = fieldRelation.relationColumn || `${this.tableName}.${fieldRelation.column}`; @@ -37,4 +38,14 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { } }; } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + sortOrder: this.sortRole.fieldKey, + sortBy: this.sortRole.order, + }; + } } diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.ts b/server/src/lib/DynamicFilter/DynamicFilterViews.ts index b02a86aa1..d8f2bfc84 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterViews.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterViews.ts @@ -1,25 +1,29 @@ -import { IFilterRole } from 'interfaces'; +import { omit } from 'lodash'; +import { IView, IViewRole } from 'interfaces'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import { - validateViewRoles, buildFilterQuery, } from 'lib/ViewRolesBuilder'; export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { + viewId: number; logicExpression: string; + filterRoles: IViewRole[]; /** * Constructor method. - * @param {*} filterRoles - Filter roles. - * @param {*} logicExpression - Logic expression. + * @param {IView} view - */ - constructor(filterRoles: IFilterRole[], logicExpression: string) { + constructor(view: IView) { super(); - this.filterRoles = filterRoles; - this.logicExpression = logicExpression + this.viewId = view.id; + this.filterRoles = view.roles; + this.logicExpression = view.rolesLogicExpression .replace('AND', '&&') .replace('OR', '||'); + + this.setResponseMeta(); } /** @@ -28,20 +32,27 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { buildLogicExpression() { return this.logicExpression; } - - /** - * Validates filter roles. - */ - validate() { - return validateViewRoles(this.filterRoles, this.logicExpression); - } - + /** * Builds database query of view roles. */ buildQuery() { return (builder) => { - buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder); + buildFilterQuery(this.model, this.filterRoles, this.logicExpression)(builder); + }; + } + + /** + * Sets response meta. + */ + setResponseMeta() { + this.responseMeta = { + view: { + logicExpression: this.logicExpression, + filterRoles: this.filterRoles + .map((filterRole) => ({ ...omit(filterRole, ['id', 'viewId']) })), + customViewId: this.viewId, + } }; } } \ No newline at end of file diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index 39af35931..f8eeff373 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -1,10 +1,9 @@ -import { difference, filter } from 'lodash'; +import { difference } from 'lodash'; import moment from 'moment'; import { Lexer } from 'lib/LogicEvaluation/Lexer'; import Parser from 'lib/LogicEvaluation/Parser'; import QueryParser from 'lib/LogicEvaluation/QueryParser'; -import resourceFieldsKeys from 'data/ResourceFieldsKeys'; -import { IFilterRole } from 'interfaces'; +import { IFilterRole, IModel } from 'interfaces'; const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { switch (role.comparator) { @@ -93,7 +92,7 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { if (hasTimeFormat) { const targetDateTime = moment(role.value).format(dateFormat); builder.where(comparatorColumn, '=', targetDateTime); - } else { + } else { const startDate = moment(role.value).startOf('day'); const endDate = moment(role.value).endOf('day'); @@ -109,19 +108,19 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { * @param {String} tableName - Table name of target column. * @param {String} fieldKey - Target column key that stored in resource field. */ -export function getRoleFieldColumn(tableName: string, fieldKey: string) { - const tableFields = resourceFieldsKeys[tableName]; +export function getRoleFieldColumn(model: IModel, fieldKey: string) { + const tableFields = model.fields; return (tableFields[fieldKey]) ? tableFields[fieldKey] : null; } /** * Builds roles queries. - * @param {String} tableName - + * @param {IModel} model - * @param {Object} role - */ -export function buildRoleQuery(tableName: string, role: IFilterRole) { - const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey); - const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`; +export function buildRoleQuery(model: IModel, role: IFilterRole) { + const fieldRelation = getRoleFieldColumn(model, role.fieldKey); + const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`; switch (fieldRelation.columnType) { case 'number': @@ -150,26 +149,26 @@ export const getTableFromRelationColumn = (column: string) => { * @param {String} tableName - * @param {Array} roles - */ -export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) { +export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { return (builder) => { roles.forEach((role) => { - const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey); + const fieldColumn = getRoleFieldColumn(model, role.fieldKey); if (fieldColumn.relation) { const joinTable = getTableFromRelationColumn(fieldColumn.relation); - builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); + builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); } }); }; } -export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { +export function buildSortColumnJoin(model: IModel, sortColumnKey: string) { return (builder) => { - const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey); + const fieldColumn = getRoleFieldColumn(model, sortColumnKey); if (fieldColumn.relation) { const joinTable = getTableFromRelationColumn(fieldColumn.relation); - builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); + builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); } }; } @@ -180,11 +179,11 @@ export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { * @param {Array} roles - * @return {Function} */ -export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') { +export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') { const rolesIndexSet = {}; roles.forEach((role) => { - rolesIndexSet[role.index] = buildRoleQuery(tableName, role); + rolesIndexSet[role.index] = buildRoleQuery(model, role); }); // Lexer for logic expression. const lexer = new Lexer(logicExpression); @@ -204,9 +203,9 @@ export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], l * @param {Array} roles - * @param {String} logicExpression - */ -export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => { +export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => { return (builder) => { - buildFilterRolesQuery(tableName, roles, logicExpression)(builder); + buildFilterRolesQuery(model, roles, logicExpression)(builder); }; }; @@ -240,35 +239,33 @@ export function mapFilterRolesToDynamicFilter(roles) { * @param {String} columnKey - * @param {String} sortDirection - */ -export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) { - const fieldRelation = getRoleFieldColumn(tableName, columnKey); - const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`; +export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirection: string) { + const fieldRelation = getRoleFieldColumn(model, columnKey); + const sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`; return (builder) => { builder.orderBy(sortColumn, sortDirection); - buildSortColumnJoin(tableName, columnKey)(builder); + buildSortColumnJoin(model, columnKey)(builder); }; } export function validateFilterLogicExpression(logicExpression: string, indexes) { const logicExpIndexes = logicExpression.match(/\d+/g) || []; - const diff = !difference(logicExpIndexes.map(Number), indexes).length; + const diff = difference(logicExpIndexes.map(Number), indexes); + return (diff.length > 0) ? false : true; } export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) { return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); } -export function validateFieldKeyExistance(tableName: string, fieldKey: string) { - if (!resourceFieldsKeys?.[tableName]?.[fieldKey]) - return fieldKey; - else - return false; +export function validateFieldKeyExistance(model: any, fieldKey: string) { + return model?.fields?.[fieldKey] || false; } -export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) { +export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) { return filterRoles.filter((filterRole: IFilterRole) => { - return validateFieldKeyExistance(tableName, filterRole.fieldKey); + return !validateFieldKeyExistance(model, filterRole.fieldKey); }); } \ No newline at end of file diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index fed18f4b3..e81cf6659 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -1,4 +1,7 @@ +import { Container } from 'typedi'; + // Here we import all events. import 'subscribers/authentication'; import 'subscribers/organization'; import 'subscribers/manualJournals'; +import 'subscribers/expenses'; \ No newline at end of file diff --git a/server/src/loaders/i18n.ts b/server/src/loaders/i18n.ts index b87823dd2..1c27f8238 100644 --- a/server/src/loaders/i18n.ts +++ b/server/src/loaders/i18n.ts @@ -4,6 +4,6 @@ import path from 'path'; export default () => i18n.configure({ locales: ['en', 'ar'], register: global, - directory: path.join(global.__root, 'src/locales'), + directory: path.join(global.__root, 'locales'), updateFiles: false }) \ No newline at end of file diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index d5c124dce..e5f32d033 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -10,9 +10,7 @@ import Bill from 'models/Bill'; import BillPayment from 'models/BillPayment'; import BillPaymentEntry from 'models/BillPaymentEntry'; import Currency from 'models/Currency'; -import Customer from 'models/Customer'; import Contact from 'models/Contact'; -import Vendor from 'models/Vendor'; import ExchangeRate from 'models/ExchangeRate'; import Expense from 'models/Expense'; import ExpenseCategory from 'models/ExpenseCategory'; @@ -49,8 +47,6 @@ export default (knex) => { BillPayment, BillPaymentEntry, Currency, - Customer, - Vendor, ExchangeRate, Expense, ExpenseCategory, diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 48c4c5fdc..13a8df5e2 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -1,4 +1,33 @@ { "Empty": "", - "Hello": "Hello" + "Hello": "Hello", + "Petty Cash": "Petty Cash 2", + "Bank": "Bank", + "Other Income": "Other Income", + "Interest Income": "Interest Income", + "Opening Balance": "Opening Balance", + "Depreciation Expense": "Depreciation Expense", + "Interest Expense": "Interest Expense", + "Sales of Product Income": "Sales of Product Income", + "Inventory Asset": "Inventory Asset", + "Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)", + "Accounts Payable": "Accounts Payable", + "Other Expenses": "Other Expenses", + "Payroll Expenses": "Payroll Expenses", + "Fixed Asset": "Fixed Asset", + "Current Asset": "Current Asset", + "Other Asset": "Other Asset", + "Long Term Liability": "Long Term Liability", + "Current Liability": "Current Liability", + "Other Liability": "Other Liability", + "Equity": "Equity", + "Expense": "Expense", + "Other Expense": "Other Expense", + "Income": "Income", + "Accounts Receivable (A/R)": "Accounts Receivable (A/R)", + "Accounts Payable (A/P)": "Accounts Payable (A/P)", + "Inactive": "Inactive", + "Assets": "Assets", + "Liabilities": "Liabilities", + "Expenses": "Expenses", } \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index a70b0623a..6fa7625ac 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -8,6 +8,7 @@ import { } from 'lib/ViewRolesBuilder'; import { flatToNestedArray } from 'utils'; import DependencyGraph from 'lib/DependencyGraph'; +import TenantManagerSubscriber from 'subscribers/tenantManager'; export default class Account extends TenantModel { /** @@ -24,6 +25,13 @@ export default class Account extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * + */ + static get resourceable() { + return true; + } + /** * Model modifiers. */ @@ -106,4 +114,55 @@ export default class Account extends TenantModel { accounts, { itemId: 'id', parentItemId: 'parentAccountId' } ); } + + /** + * Model defined fields. + */ + static get fields() { + return { + name: { + label: 'Name', + column: 'name', + }, + type: { + label: 'Account type', + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.key', + }, + description: { + label: 'Description', + column: 'description', + }, + code: { + label: 'Account code', + column: 'code', + }, + root_type: { + label: 'Type', + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.root_type', + }, + created_at: { + column: 'created_at', + columnType: 'date', + }, + active: { + column: 'active', + }, + balance: { + column: 'amount', + columnType: 'number' + }, + currency: { + column: 'currency_code', + }, + normal: { + column: 'account_type_id', + relation: 'account_types.id', + relationColumn: 'account_types.normal' + }, + }; + } } diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index bace2321a..2e7429492 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -36,17 +36,20 @@ export default class Bill extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const Vendor = require('models/Vendor'); + const Contact = require('models/Contact'); const ItemEntry = require('models/ItemEntry'); return { vendor: { relation: Model.BelongsToOneRelation, - modelClass: Vendor.default, + modelClass: Contact.default, join: { from: 'bills.vendorId', - to: 'vendors.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Vendor'); + } }, entries: { diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 71f598615..282722a35 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -22,7 +22,7 @@ export default class BillPayment extends TenantModel { static get relationMappings() { const BillPaymentEntry = require('models/BillPaymentEntry'); const AccountTransaction = require('models/AccountTransaction'); - const Vendor = require('models/Vendor'); + const Contact = require('models/Contact'); const Account = require('models/Account'); return { @@ -37,11 +37,14 @@ export default class BillPayment extends TenantModel { vendor: { relation: Model.BelongsToOneRelation, - modelClass: Vendor.default, + modelClass: Contact.default, join: { from: 'bills_payments.vendorId', - to: 'vendors.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Vendor'); + } }, paymentAccount: { diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js deleted file mode 100644 index 5d0590667..000000000 --- a/server/src/models/Customer.js +++ /dev/null @@ -1,81 +0,0 @@ -import { Model } from 'objection'; -import TenantModel from 'models/TenantModel'; - -export default class Customer extends TenantModel { - /** - * Table name - */ - static get tableName() { - return 'customers'; - } - - /** - * Model timestamps. - */ - get timestamps() { - return ['createdAt', 'updatedAt']; - } - - /** - * Model modifiers. - */ - static get modifiers() { - return { - filterCustomerIds(query, customerIds) { - query.whereIn('id', customerIds); - }, - }; - } - - /** - * Change vendor balance. - * @param {Integer} customerId - * @param {Numeric} amount - */ - static async changeBalance(customerId, amount) { - const changeMethod = (amount > 0) ? 'increment' : 'decrement'; - - return this.query() - .where('id', customerId) - [changeMethod]('balance', Math.abs(amount)); - } - - /** - * Increment the given customer balance. - * @param {Integer} customerId - * @param {Integer} amount - */ - static async incrementBalance(customerId, amount) { - return this.query() - .where('id', customerId) - .increment('balance', amount); - } - - /** - * Decrement the given customer balance. - * @param {integer} customerId - - * @param {integer} amount - - */ - static async decrementBalance(customerId, amount) { - await this.query() - .where('id', customerId) - .decrement('balance', amount); - } - - static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) { - const diffAmount = amount - oldAmount; - const asyncOpers = []; - - if (customerId != oldCustomerId) { - const oldCustomerOper = this.changeBalance(oldCustomerId, (oldAmount * -1)); - const customerOper = this.changeBalance(customerId, amount); - - asyncOpers.push(customerOper); - asyncOpers.push(oldCustomerOper); - } else { - const balanceChangeOper = this.changeBalance(customerId, diffAmount); - asyncOpers.push(balanceChangeOper); - } - return Promise.all(asyncOpers); - } -} diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index c422a6065..eb1ae2c01 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -1,6 +1,7 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; import { viewRolesBuilder } from 'lib/ViewRolesBuilder'; +import Media from './Media'; export default class Expense extends TenantModel { /** @@ -24,6 +25,11 @@ export default class Expense extends TenantModel { return ['createdAt', 'updatedAt']; } + + static get media () { + return true; + } + /** * Model modifiers. */ @@ -55,14 +61,9 @@ export default class Expense extends TenantModel { query.where('payment_account_id', accountId); } }, - viewRolesBuilder(query, conditionals, expression) { viewRolesBuilder(conditionals, expression)(query); }, - - orderBy(query) { - - } }; } @@ -72,7 +73,7 @@ export default class Expense extends TenantModel { static get relationMappings() { const Account = require('models/Account'); const ExpenseCategory = require('models/ExpenseCategory'); - const SystemUser = require('system/models/SystemUser'); + const Media = require('models/Media'); return { paymentAccount: { @@ -91,14 +92,59 @@ export default class Expense extends TenantModel { to: 'expense_transaction_categories.expenseId', }, }, - user: { - relation: Model.BelongsToOneRelation, - modelClass: SystemUser.default, + media: { + relation: Model.ManyToManyRelation, + modelClass: Media.default, join: { - from: 'expenses_transactions.userId', - to: 'users.id', + from: 'expenses_transactions.id', + through: { + from: 'media_links.model_id', + to: 'media_links.media_id', + }, + to: 'media.id', + }, + filter(query) { + query.where('model_name', 'Expense'); } - } + }, + }; + } + + /** + * Model defined fields. + */ + static get fields() { + return { + payment_date: { + column: 'payment_date', + }, + payment_account: { + column: 'payment_account_id', + relation: 'accounts.id', + }, + amount: { + column: 'total_amount', + }, + currency_code: { + column: 'currency_code', + }, + reference_no: { + column: 'reference_no' + }, + description: { + column: 'description', + }, + published: { + column: 'published', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + }, }; } } diff --git a/server/src/models/Item.js b/server/src/models/Item.js index ea500c8cc..739342c66 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -95,4 +95,67 @@ export default class Item extends TenantModel { }, }; } + + + static get fields() { + return { + type: { + column: 'type', + }, + name: { + column: 'name', + }, + sellable: { + column: 'sellable', + }, + purchasable: { + column: 'purchasable', + }, + sell_price: { + column: 'sell_price' + }, + cost_price: { + column: 'cost_price', + }, + currency_code: { + column: 'currency_code', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + sell_description: { + column: 'sell_description', + }, + purchase_description: { + column: 'purchase_description', + }, + quantity_on_hand: { + column: 'quantity_on_hand', + }, + note: { + column: 'note', + }, + category: { + column: 'category_id', + relation: 'categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + created_at: { + column: 'created_at', + } + }; + } } diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js index 26e7f2684..48b763efa 100644 --- a/server/src/models/ItemCategory.js +++ b/server/src/models/ItemCategory.js @@ -10,6 +10,10 @@ export default class ItemCategory extends TenantModel { return 'items_categories'; } + static get resourceable() { + return true; + } + /** * Timestamps columns. */ @@ -37,4 +41,43 @@ export default class ItemCategory extends TenantModel { }, }; } + + static get fields() { + return { + name: { + column: 'name', + }, + description: { + column: 'description', + }, + parent_category_id: { + column: 'parent_category_id', + relation: 'items_categories.id', + relationColumn: 'items_categories.id', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + cost_account: { + column: 'cost_account_id', + relation: 'accounts.id', + }, + sell_account: { + column: 'sell_account_id', + relation: 'accounts.id', + }, + inventory_account: { + column: 'inventory_account_id', + relation: 'accounts.id', + }, + cost_method: { + column: 'cost_method', + }, + created_at: { + column: 'created_at', + }, + }; + } } diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 71c285825..d3f0bf408 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -46,8 +46,48 @@ export default class ManualJournal extends TenantModel { to: 'media_links.media_id', }, to: 'media.id', + }, + filter(query) { + query.where('model_name', 'ManualJournal'); } } }; } + + /** + * Model defined fields. + */ + static get fields() { + return { + date: { + column: 'date', + }, + journal_number: { + column: 'journal_number', + }, + reference: { + column: 'reference', + }, + status: { + column: 'status', + }, + amount: { + column: 'amount', + }, + description: { + column: 'description', + }, + user: { + column: 'user_id', + relation: 'users.id', + relationColumn: 'users.id', + }, + journal_type: { + column: 'journal_type', + }, + created_at: { + column: 'created_at', + }, + }; + } } diff --git a/server/src/models/Media.js b/server/src/models/Media.js index acf421451..aab3aa227 100644 --- a/server/src/models/Media.js +++ b/server/src/models/Media.js @@ -1,3 +1,4 @@ +import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class Media extends TenantModel { @@ -7,4 +8,29 @@ export default class Media extends TenantModel { static get tableName() { return 'media'; } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const MediaLink = require('models/MediaLink'); + + return { + links: { + relation: Model.HasManyRelation, + modelClass: MediaLink.default, + join: { + from: 'media.id', + to: 'media_links.media_id', + }, + }, + }; + } } diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index d992978a9..db1df4b0a 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -22,17 +22,20 @@ export default class PaymentReceive extends TenantModel { static get relationMappings() { const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); const AccountTransaction = require('models/AccountTransaction'); - const Customer = require('models/Customer'); + const Contact = require('models/Contact'); const Account = require('models/Account'); return { customer: { relation: Model.BelongsToOneRelation, - modelClass: Customer.default, + modelClass: Contact.default, join: { from: 'payment_receives.customerId', - to: 'customers.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Customer'); + } }, depositAccount: { diff --git a/server/src/models/ResourcableModel.js b/server/src/models/ResourcableModel.js new file mode 100644 index 000000000..289c2dfa3 --- /dev/null +++ b/server/src/models/ResourcableModel.js @@ -0,0 +1,8 @@ + + +export default class ResourceableModel { + + static get resourceable() { + return true; + } +} \ No newline at end of file diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index fcbe46cb0..375682193 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -21,16 +21,19 @@ export default class SaleEstimate extends TenantModel { */ static get relationMappings() { const ItemEntry = require('models/ItemEntry'); - const Customer = require('models/Customer'); + const Contact = require('models/Contact'); return { customer: { relation: Model.BelongsToOneRelation, - modelClass: Customer.default, + modelClass: Contact.default, join: { from: 'sales_estimates.customerId', - to: 'customers.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Customer'); + } }, entries: { diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 0fae868fa..4070dbcf9 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -57,7 +57,7 @@ export default class SaleInvoice extends TenantModel { static get relationMappings() { const AccountTransaction = require('models/AccountTransaction'); const ItemEntry = require('models/ItemEntry'); - const Customer = require('models/Customer'); + const Contact = require('models/Contact'); const InventoryCostLotTracker = require('models/InventoryCostLotTracker'); return { @@ -75,11 +75,14 @@ export default class SaleInvoice extends TenantModel { customer: { relation: Model.BelongsToOneRelation, - modelClass: Customer.default, + modelClass: Contact.default, join: { from: 'sales_invoices.customerId', - to: 'customers.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Customer'); + } }, transactions: { diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index 9b05cda29..4fe84b9f0 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -20,7 +20,7 @@ export default class SaleReceipt extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const Customer = require('models/Customer'); + const Contact = require('models/Contact'); const Account = require('models/Account'); const AccountTransaction = require('models/AccountTransaction'); const ItemEntry = require('models/ItemEntry'); @@ -28,11 +28,14 @@ export default class SaleReceipt extends TenantModel { return { customer: { relation: Model.BelongsToOneRelation, - modelClass: Customer.default, + modelClass: Contact.default, join: { from: 'sales_receipts.customerId', - to: 'customers.id', + to: 'contacts.id', }, + filter(query) { + query.where('contact_type', 'Customer'); + } }, depositAccount: { diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js deleted file mode 100644 index 636121624..000000000 --- a/server/src/models/Vendor.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Model } from 'objection'; -import TenantModel from 'models/TenantModel'; - -export default class Vendor extends TenantModel { - /** - * Table name - */ - static get tableName() { - return 'vendors'; - } - - /** - * Model timestamps. - */ - get timestamps() { - return ['createdAt', 'updatedAt']; - } - - /** - * Changes the vendor balance. - * @param {Integer} customerId - * @param {Number} amount - * @return {Promise} - */ - static async changeBalance(vendorId, amount) { - const changeMethod = amount > 0 ? 'increment' : 'decrement'; - - return this.query() - .where('id', vendorId) - [changeMethod]('balance', Math.abs(amount)); - } - - /** - * - * @param {number} vendorId - Specific vendor id. - * @param {number} oldVendorId - The given old vendor id. - * @param {number} amount - The new change amount. - * @param {number} oldAmount - The old stored amount. - */ - static changeDiffBalance(vendorId, oldVendorId, amount, oldAmount) { - const diffAmount = (amount - oldAmount); - const asyncOpers = []; - - if (vendorId != oldVendorId) { - const oldVendorOper = Vendor.changeBalance( - oldVendorId, - (oldAmount * -1) - ); - const vendorOper = Vendor.changeBalance( - vendorId, - amount, - ); - asyncOpers.push(vendorOper); - asyncOpers.push(oldVendorOper); - } else { - const balanceChangeOper = Vendor.changeBalance(vendorId, diffAmount); - asyncOpers.push(balanceChangeOper); - } - return Promise.all(asyncOpers); - } -} diff --git a/server/src/models/index.js b/server/src/models/index.js index bd91c577d..a980b2156 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -1,5 +1,3 @@ -import Customer from './Customer'; -import Vendor from './Vendor'; import Option from './Option'; import SaleEstimate from './SaleEstimate'; import SaleEstimateEntry from './SaleEstimateEntry'; @@ -22,8 +20,6 @@ import AccountType from './AccountType'; import InventoryLotCostTracker from './InventoryCostLotTracker'; export { - Customer, - Vendor, SaleEstimate, SaleEstimateEntry, SaleReceipt, diff --git a/server/src/repositories/AccountRepository.ts b/server/src/repositories/AccountRepository.ts index 2452cffe4..d8b0ba3c6 100644 --- a/server/src/repositories/AccountRepository.ts +++ b/server/src/repositories/AccountRepository.ts @@ -1,5 +1,6 @@ import TenantRepository from 'repositories/TenantRepository'; import { IAccount } from 'interfaces'; +import { Account } from 'models'; export default class AccountRepository extends TenantRepository { models: any; @@ -57,14 +58,89 @@ export default class AccountRepository extends TenantRepository { /** * Retrieve the account by the given id. - * @param {number} id - Account id. + * @param {number} id - Account id. * @return {IAccount} */ - getById(id: number): IAccount { + findById(id: number): IAccount { const { Account } = this.models; return this.cache.get(`accounts.id.${id}`, () => { return Account.query().findById(id); }); } + /** + * Retrieve accounts by the given ids. + * @param {number[]} ids - + * @return {IAccount[]} + */ + findByIds(accountsIds: number[]) { + const { Account } = this.models; + return Account.query().whereIn('id', accountsIds); + } + + /** + * Activate the given account. + * @param {number} accountId - + * @return {void} + */ + async activate(accountId: number): Promise { + const { Account } = this.models; + await Account.query().findById(accountId).patch({ active: 1 }) + this.flushCache(); + } + + /** + * Inserts a new accounts to the storage. + * @param {IAccount} account + */ + async insert(accountInput: IAccount): Promise { + const { Account } = this.models; + const account = await Account.query().insertAndFetch({ ...accountInput }); + this.flushCache(); + + return account; + } + + /** + * Updates account of the given account. + * @param {number} accountId - Account id. + * @param {IAccount} account + * @return {void} + */ + async edit(accountId: number, account: IAccount): Promise { + const { Account } = this.models; + await Account.query().findById(accountId).patch({ ...account }); + this.flushCache(); + } + + /** + * Deletes the given account by id. + * @param {number} accountId - Account id. + */ + async deleteById(accountId: number): Promise { + const { Account } = this.models; + await Account.query().deleteById(accountId); + this.flushCache(); + } + + /** + * Changes account balance. + * @param {number} accountId + * @param {number} amount + * @return {Promise} + */ + async balanceChange(accountId: number, amount: number): Promise { + const { Account } = this.models; + const method: string = (amount < 0) ? 'decrement' : 'increment'; + + await Account.query().where('id', accountId)[method]('amount', amount); + this.flushCache(); + } + + /** + * Flush repository cache. + */ + flushCache(): void { + this.cache.delStartWith('accounts'); + } } \ No newline at end of file diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts index 7b273076e..21c93ead5 100644 --- a/server/src/repositories/AccountTypeRepository.ts +++ b/server/src/repositories/AccountTypeRepository.ts @@ -76,4 +76,11 @@ export default class AccountTypeRepository extends TenantRepository { return AccountType.query().where('root_type', rootType); }); } + + /** + * Flush repository cache. + */ + flushCache() { + this.cache.delStartWith('accountType'); + } } \ No newline at end of file diff --git a/server/src/repositories/ContactRepository.ts b/server/src/repositories/ContactRepository.ts index b25942ec4..712e357e1 100644 --- a/server/src/repositories/ContactRepository.ts +++ b/server/src/repositories/ContactRepository.ts @@ -1,4 +1,6 @@ import TenantRepository from 'repositories/TenantRepository'; +import { IContact } from 'interfaces'; +import Contact from 'models/Contact'; export default class ContactRepository extends TenantRepository { cache: any; @@ -17,21 +19,70 @@ export default class ContactRepository extends TenantRepository { this.cache = this.tenancy.cache(tenantId); } - findById(contactId: number) { + /** + * Retrieve the given contact model. + * @param {number} contactId + */ + findById(contactId: number): IContact { const { Contact } = this.models; - return this.cache.get(`contact.id.${contactId}`, () => { + return this.cache.get(`contacts.id.${contactId}`, () => { return Contact.query().findById(contactId); }) } - findByIds(contactIds: number[]) { + /** + * Retrieve the given contacts model. + * @param {number[]} contactIds - Contacts ids. + */ + findByIds(contactIds: number[]): IContact[] { const { Contact } = this.models; - return this.cache.get(`contact.ids.${contactIds.join(',')}`, () => { + return this.cache.get(`contacts.ids.${contactIds.join(',')}`, () => { return Contact.query().whereIn('id', contactIds); }); } - insert(contact) { + /** + * Inserts a new contact model. + * @param contact + */ + async insert(contact) { + await Contact.query().insert({ ...contact }) + this.flushCache(); + } + /** + * Updates the contact details. + * @param {number} contactId - Contact id. + * @param {IContact} contact - Contact input. + */ + async update(contactId: number, contact: IContact) { + await Contact.query().findById(contactId).patch({ ...contact }); + this.flushCache(); + } + + /** + * Deletes contact of the given id. + * @param {number} contactId - + * @return {Promise} + */ + async deleteById(contactId: number): Promise { + await Contact.query().where('id', contactId).delete(); + this.flushCache(); + } + + /** + * Deletes contacts in bulk. + * @param {number[]} contactsIds + */ + async bulkDelete(contactsIds: number[]) { + await Contact.query().whereIn('id', contactsIds); + this.flushCache(); + } + + /** + * Flush contact repository cache. + */ + flushCache() { + this.cache.delStartWith(`contacts`); } } \ No newline at end of file diff --git a/server/src/repositories/CustomerRepository.js b/server/src/repositories/CustomerRepository.js deleted file mode 100644 index 166476a45..000000000 --- a/server/src/repositories/CustomerRepository.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Customer } from 'models'; - -export default class CustomerRepository { - - static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) { - const diffAmount = amount - oldAmount; - const asyncOpers = []; - - if (customerId != oldCustomerId) { - const oldCustomerOper = Customer.changeBalance( - oldCustomerId, - (oldAmount * -1) - ); - const customerOper = Customer.changeBalance( - customerId, - amount, - ); - asyncOpers.push(customerOper); - asyncOpers.push(oldCustomerOper); - } else { - const balanceChangeOper = Customer.changeBalance(customerId, diffAmount); - asyncOpers.push(balanceChangeOper); - } - return Promise.all(asyncOpers); - } -} diff --git a/server/src/repositories/CustomerRepository.ts b/server/src/repositories/CustomerRepository.ts index ce019acc6..12c41cd93 100644 --- a/server/src/repositories/CustomerRepository.ts +++ b/server/src/repositories/CustomerRepository.ts @@ -17,7 +17,7 @@ export default class CustomerRepository extends TenantRepository { /** * Retrieve customer details of the given id. - * @param {number} customerId - + * @param {number} customerId - Customer id. */ getById(customerId: number) { const { Contact } = this.models; diff --git a/server/src/repositories/ExpenseRepository.ts b/server/src/repositories/ExpenseRepository.ts index e83836ee0..a25c308e0 100644 --- a/server/src/repositories/ExpenseRepository.ts +++ b/server/src/repositories/ExpenseRepository.ts @@ -7,6 +7,10 @@ export default class ExpenseRepository extends TenantRepository { repositories: any; cache: any; + /** + * Constructor method. + * @param {number} tenantId + */ constructor(tenantId: number) { super(tenantId); @@ -14,38 +18,98 @@ export default class ExpenseRepository extends TenantRepository { this.cache = this.tenancy.cache(tenantId); } + /** + * Retrieve the given expense by id. + * @param {number} expenseId + * @return {Promise} + */ getById(expenseId: number) { const { Expense } = this.models; return this.cache.get(`expense.id.${expenseId}`, () => { - return Expense.query().findById(expenseId); - }) - } - - create(expense: IExpense) { - const { Expense } = this.models; - return Expense.query().insert({ ...expense }); - } - - update(expenseId: number, expense: IExpense) { - const { Expense } = this.models; - return Expense.query().patchAndFetchById(expenseId, { ...expense }); - } - - publish(expenseId: number) { - const { Expense } = this.models; - - return Expense.query().findById(expenseId).patch({ - publishedAt: moment().toMySqlDateTime(), + return Expense.query().findById(expenseId).withGraphFetched('categories'); }); } - delete(expenseId: number) { + /** + * Inserts a new expense object. + * @param {IExpense} expense - + */ + async create(expenseInput: IExpense): Promise { const { Expense } = this.models; - return Expense.query().findById(expenseId).delete(); + const expense = await Expense.query().insertGraph({ ...expenseInput }); + this.flushCache(); + + return expense; } - bulkDelete(expensesIds: number[]) { + /** + * Updates the given expense details. + * @param {number} expenseId + * @param {IExpense} expense + */ + async update(expenseId: number, expense: IExpense) { const { Expense } = this.models; - return Expense.query().whereIn('id', expensesIds).delete(); + + await Expense.query().findById(expenseId).patch({ ...expense }); + this.flushCache(); + } + + /** + * Publish the given expense. + * @param {number} expenseId + */ + async publish(expenseId: number): Promise { + const { Expense } = this.models; + + await Expense.query().findById(expenseId).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + this.flushCache(); + } + + /** + * Deletes the given expense. + * @param {number} expenseId + */ + async delete(expenseId: number): Promise { + const { Expense, ExpenseCategory } = this.models; + + await ExpenseCategory.query().where('expense_id', expenseId).delete(); + await Expense.query().where('id', expenseId).delete(); + + this.flushCache(); + } + + /** + * Deletes expenses in bulk. + * @param {number[]} expensesIds + */ + async bulkDelete(expensesIds: number[]): Promise { + const { Expense } = this.models; + + await Expense.query().whereIn('expense_id', expensesIds).delete(); + await Expense.query().whereIn('id', expensesIds).delete(); + + this.flushCache(); + } + + /** + * Publishes the given expenses in bulk. + * @param {number[]} expensesIds + * @return {Promise} + */ + async bulkPublish(expensesIds: number): Promise { + const { Expense } = this.models; + await Expense.query().whereIn('id', expensesIds).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + this.flushCache(); + } + + /** + * Flushes repository cache. + */ + flushCache() { + this.cache.delStartWith(`expense`); } } \ No newline at end of file diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts index a02443a0b..264c158c8 100644 --- a/server/src/repositories/VendorRepository.ts +++ b/server/src/repositories/VendorRepository.ts @@ -1,3 +1,4 @@ +import { IVendor } from "interfaces"; import TenantRepository from "./TenantRepository"; @@ -18,7 +19,7 @@ export default class VendorRepository extends TenantRepository { /** * Retrieve the bill that associated to the given vendor id. - * @param {number} vendorId + * @param {number} vendorId - Vendor id. */ getBills(vendorId: number) { const { Bill } = this.models; @@ -29,16 +30,17 @@ export default class VendorRepository extends TenantRepository { } /** - * + * Retrieve all the given vendors. * @param {numner[]} vendorsIds + * @return {IVendor} */ - vendors(vendorsIds: number[]) { + vendors(vendorsIds: number[]): IVendor[] { const { Contact } = this.models; return Contact.query().modifier('vendor').whereIn('id', vendorsIds); } /** - * + * Retrieve vendors with associated bills. * @param {number[]} vendorIds */ vendorsWithBills(vendorIds: number[]) { diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts index ab4b0b8d9..0d124d24b 100644 --- a/server/src/repositories/ViewRepository.ts +++ b/server/src/repositories/ViewRepository.ts @@ -1,3 +1,4 @@ +import { IView } from 'interfaces'; import { View } from 'models'; import TenantRepository from 'repositories/TenantRepository'; @@ -17,7 +18,6 @@ export default class ViewRepository extends TenantRepository { this.models = this.tenancy.models(tenantId); this.cache = this.tenancy.cache(tenantId); - this.repositories = this.tenancy.cache(tenantId); } /** @@ -27,17 +27,41 @@ export default class ViewRepository extends TenantRepository { getById(id: number) { const { View } = this.models; return this.cache.get(`customView.id.${id}`, () => { - return View.query().findById(id); + return View.query().findById(id) + .withGraphFetched('columns') + .withGraphFetched('roles'); }); } /** * Retrieve all views of the given resource id. */ - allByResource() { - const resourceId = 1; - return this.cache.get(`customView.resource.id.${resourceId}`, () => { - return View.query().where('resource_id', resourceId); + allByResource(resourceModel: string) { + const { View } = this.models; + return this.cache.get(`customView.resourceModel.${resourceModel}`, () => { + return View.query().where('resource_model', resourceModel) + .withGraphFetched('columns') + .withGraphFetched('roles'); }); } + + /** + * Inserts a new view to the storage. + * @param {IView} view + */ + async insert(view: IView): Promise { + const insertedView = await View.query().insertGraph({ ...view }); + this.flushCache(); + + return insertedView; + } + + + + /** + * Flushes repository cache. + */ + flushCache() { + this.cache.delStartWith('customView'); + } } \ No newline at end of file diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 9e07d306b..322180adb 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -2,9 +2,12 @@ import { sumBy, chain } from 'lodash'; import JournalPoster from "./JournalPoster"; import JournalEntry from "./JournalEntry"; import { AccountTransaction } from 'models'; -import { IInventoryTransaction, IManualJournal } from 'interfaces'; -import AccountsService from '../Accounts/AccountsService'; -import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces'; +import { + IInventoryTransaction, + IManualJournal, + IExpense, + IExpenseCategory, +} from 'interfaces'; interface IInventoryCostEntity { date: Date, @@ -120,6 +123,38 @@ export default class JournalCommands{ this.journal.credit(creditEntry); } + /** + * Writes journal entries of expense model object. + * @param {IExpense} expense + */ + expense(expense: IExpense) { + const mixinEntry = { + referenceType: 'Expense', + referenceId: expense.id, + date: expense.paymentDate, + userId: expense.userId, + draft: !expense.publishedAt, + }; + const paymentJournalEntry = new JournalEntry({ + credit: expense.totalAmount, + account: expense.paymentAccountId, + index: 1, + ...mixinEntry, + }); + this.journal.credit(paymentJournalEntry); + + expense.categories.forEach((category: IExpenseCategory, index) => { + const expenseJournalEntry = new JournalEntry({ + account: category.expenseAccountId, + debit: category.amount, + note: category.description, + ...mixinEntry, + index: index + 2, + }); + this.journal.debit(expenseJournalEntry); + }); + } + /** * * @param {number|number[]} referenceId diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 825835b7e..2c5635073 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -1,10 +1,18 @@ import { Inject, Service } from 'typedi'; +import { difference } from 'lodash'; import { kebabCase } from 'lodash' import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; -import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces'; -import { difference } from 'lodash'; +import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import events from 'subscribers/events'; +import JournalPoster from 'services/Accounting/JournalPoster'; +import { Account } from 'models'; +import AccountRepository from 'repositories/AccountRepository'; @Service() export default class AccountsService { @@ -17,6 +25,9 @@ export default class AccountsService { @Inject('logger') logger: any; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Retrieve account type or throws service error. * @param {number} tenantId - @@ -104,10 +115,10 @@ export default class AccountsService { * @return {IAccount} */ private async getAccountOrThrowError(tenantId: number, accountId: number) { - const { Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[accounts] validating the account existance.', { tenantId, accountId }); - const account = await Account.query().findById(accountId); + const account = await accountRepository.findById(accountId); if (!account) { this.logger.info('[accounts] the given account not found.', { accountId }); @@ -159,8 +170,8 @@ export default class AccountsService { * @returns {IAccount} */ public async newAccount(tenantId: number, accountDTO: IAccountDTO) { - const { Account } = this.tenancy.models(tenantId); - + const { accountRepository } = this.tenancy.repositories(tenantId); + // Validate account name uniquiness. await this.validateAccountNameUniquiness(tenantId, accountDTO.name); @@ -176,11 +187,15 @@ export default class AccountsService { ); this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); } - const account = await Account.query().insertAndFetch({ + const account = await accountRepository.insert({ ...accountDTO, slug: kebabCase(accountDTO.name), }); this.logger.info('[account] account created successfully.', { account, accountDTO }); + + // Triggers `onAccountCreated` event. + this.eventDispatcher.dispatch(events.accounts.onCreated); + return account; } @@ -191,7 +206,7 @@ export default class AccountsService { * @param {IAccountDTO} accountDTO */ public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) { - const { Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); const oldAccount = await this.getAccountOrThrowError(tenantId, accountId); // Validate account name uniquiness. @@ -214,12 +229,13 @@ export default class AccountsService { this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); } // Update the account on the storage. - const account = await Account.query().patchAndFetchById( - oldAccount.id, { ...accountDTO } - ); + const account = await accountRepository.edit(oldAccount.id, accountDTO); this.logger.info('[account] account edited successfully.', { account, accountDTO, tenantId }); + // Triggers `onAccountEdited` event. + this.eventDispatcher.dispatch(events.accounts.onEdited); + return account; } @@ -247,17 +263,6 @@ export default class AccountsService { return foundAccounts.length > 0; } - public async getAccountByType(tenantId: number, accountTypeKey: string) { - const { AccountType, Account } = this.tenancy.models(tenantId); - const accountType = await AccountType.query() - .findOne('key', accountTypeKey); - - const account = await Account.query() - .findOne('account_type_id', accountType.id); - - return account; - } - /** * Throws error if the account was prefined. * @param {IAccount} account @@ -309,7 +314,7 @@ export default class AccountsService { * @param {number} accountId */ public async deleteAccount(tenantId: number, accountId: number) { - const { Account } = this.tenancy.models(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); const account = await this.getAccountOrThrowError(tenantId, accountId); this.throwErrorIfAccountPredefined(account); @@ -317,10 +322,13 @@ export default class AccountsService { await this.throwErrorIfAccountHasChildren(tenantId, accountId); await this.throwErrorIfAccountHasTransactions(tenantId, accountId); - await Account.query().deleteById(account.id); + await accountRepository.deleteById(account.id); this.logger.info('[account] account has been deleted successfully.', { tenantId, accountId, - }) + }); + + // Triggers `onAccountDeleted` event. + this.eventDispatcher.dispatch(events.accounts.onDeleted); } /** @@ -400,6 +408,9 @@ export default class AccountsService { this.logger.info('[account] given accounts deleted in bulk successfully.', { tenantId, accountsIds }); + + // Triggers `onBulkDeleted` event. + this.eventDispatcher.dispatch(events.accounts.onBulkDeleted); } /** @@ -418,6 +429,9 @@ export default class AccountsService { active: activate ? 1 : 0, }); this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds }); + + // Triggers `onAccountBulkActivated` event. + this.eventDispatcher.dispatch(events.accounts.onActivated); } /** @@ -436,6 +450,9 @@ export default class AccountsService { active: activate ? 1 : 0, }) this.logger.info('[account] account have been activated successfully.', { tenantId, accountId }); + + // Triggers `onAccountActivated` event. + this.eventDispatcher.dispatch(events.accounts.onActivated); } /** @@ -443,9 +460,11 @@ export default class AccountsService { * @param {number} tenantId * @param {IAccountsFilter} accountsFilter */ - public async getAccountsList(tenantId: number, filter: IAccountsFilter) { + public async getAccountsList( + tenantId: number, + filter: IAccountsFilter, + ): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> { const { Account } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter); this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter }); @@ -453,6 +472,60 @@ export default class AccountsService { builder.withGraphFetched('type'); dynamicList.buildQuery()(builder); }); - return accounts; + + return { + accounts, + filterMeta: dynamicList.getResponseMeta(), + }; + } + + /** + * Closes the given account. + * ----------- + * Precedures. + * ----------- + * - Transfer the given account transactions to another account + * with the same root type. + * - Delete the given account. + * ------- + * @param {number} tenantId - + * @param {number} accountId - + * @param {number} toAccountId - + * @param {boolean} deleteAfterClosing - + */ + public async closeAccount( + tenantId: number, + accountId: number, + toAccountId: number, + deleteAfterClosing: boolean, + ) { + this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing }); + + const { AccountTransaction } = this.tenancy.models(tenantId); + const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + + const account = await this.getAccountOrThrowError(tenantId, accountId); + const toAccount = await this.getAccountOrThrowError(tenantId, toAccountId); + + this.throwErrorIfAccountPredefined(account); + + const accountType = await accountTypeRepository.getTypeMeta(account.accountTypeId); + const toAccountType = await accountTypeRepository.getTypeMeta(toAccount.accountTypeId); + + if (accountType.rootType !== toAccountType.rootType) { + throw new ServiceError('close_account_and_to_account_not_same_type'); + } + const updateAccountBalanceOper = await accountRepository.balanceChange(accountId, account.balance || 0); + + // Move transactiosn operation. + const moveTransactionsOper = await AccountTransaction.query() + .where('account_id', accountId) + .patch({ accountId: toAccountId }); + + await Promise.all([ moveTransactionsOper, updateAccountBalanceOper ]); + + if (deleteAfterClosing) { + await accountRepository.deleteById(accountId); + } } } diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 90344d9cf..12ccab4ec 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -7,12 +7,13 @@ import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; -import { SystemUser, PasswordReset } from 'system/models'; +import { PasswordReset } from 'system/models'; import { IRegisterDTO, ITenant, ISystemUser, IPasswordReset, + IAuthenticationService, } from 'interfaces'; import { hashPassword } from 'utils'; import { ServiceError, ServiceErrors } from 'exceptions'; @@ -134,7 +135,7 @@ export default class AuthenticationService implements IAuthenticationService { const { systemUserRepository } = this.sysRepositories; const registeredUser = await systemUserRepository.create({ - ...omit(registerDTO, 'country', 'organizationName'), + ...omit(registerDTO, 'country'), active: true, password: hashedPassword, tenant_id: tenant.id, diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 0577dd74f..6c98474ec 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -45,10 +45,10 @@ export default class ContactsService { * @param {IContactDTO} contactDTO */ async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) { - const { Contact } = this.tenancy.models(tenantId); + const { contactRepository } = this.tenancy.repositories(tenantId); this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO }); - const contact = await Contact.query().insert({ contactService, ...contactDTO }); + const contact = await contactRepository.insert({ contactService, ...contactDTO }); this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact }); return contact; @@ -77,11 +77,11 @@ export default class ContactsService { * @return {Promise} */ async deleteContact(tenantId: number, contactId: number, contactService: TContactService) { - const { Contact } = this.tenancy.models(tenantId); + const { contactRepository } = this.tenancy.repositories(tenantId); const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId }); - await Contact.query().findById(contactId).delete(); + await contactRepository.deleteById(contactId); } /** @@ -124,10 +124,10 @@ export default class ContactsService { * @return {Promise} */ async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) { - const { Contact } = this.tenancy.models(tenantId); + const { contactRepository } = this.tenancy.repositories(tenantId); this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService); - await Contact.query().whereIn('id', contactsIds).delete(); + await contactRepository.bulkDelete(contactsIds); } /** diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 97f85c927..be63d6f93 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -6,10 +6,13 @@ import ContactsService from 'services/Contacts/ContactsService'; import { ICustomerNewDTO, ICustomerEditDTO, + ICustomer, + IPaginationMeta, + ICustomersFilter } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; -import { ICustomer } from 'src/interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class CustomersService { @@ -19,12 +22,15 @@ export default class CustomersService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + /** * Converts customer to contact DTO. * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @returns {IContactDTO} */ - customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) { + private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) { return { ...omit(customerDTO, ['customerType']), contactType: customerDTO.customerType, @@ -39,7 +45,7 @@ export default class CustomersService { * @param {ICustomerNewDTO} customerDTO * @return {Promise} */ - async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { + public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { const contactDTO = this.customerToContactDTO(customerDTO) const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); @@ -59,7 +65,7 @@ export default class CustomersService { * @param {number} tenantId * @param {ICustomerEditDTO} customerDTO */ - async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { + public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { const contactDTO = this.customerToContactDTO(customerDTO); return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); } @@ -70,7 +76,7 @@ export default class CustomersService { * @param {number} customerId * @return {Promise} */ - async deleteCustomer(tenantId: number, customerId: number) { + public async deleteCustomer(tenantId: number, customerId: number) { const { Contact } = this.tenancy.models(tenantId); await this.getCustomerByIdOrThrowError(tenantId, customerId); @@ -88,10 +94,34 @@ export default class CustomersService { * @param {number} tenantId * @param {number} customerId */ - async getCustomer(tenantId: number, customerId: number) { + public async getCustomer(tenantId: number, customerId: number) { return this.contactService.getContact(tenantId, customerId, 'customer'); } + /** + * Retrieve customers paginated list. + * @param {number} tenantId - Tenant id. + * @param {ICustomersFilter} filter - Cusotmers filter. + */ + public async getCustomersList( + tenantId: number, + filter: ICustomersFilter + ): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { Contact } = this.tenancy.models(tenantId); + const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter); + + const { results, pagination } = await Contact.query().onBuild((query) => { + query.modify('customer'); + dynamicList.buildQuery()(query); + }); + + return { + customers: results, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; + } + /** * Writes customer opening balance journal entries. * @param {number} tenantId diff --git a/server/src/services/CustomFields/ResourceCustomFieldRepository.js b/server/src/services/CustomFields/ResourceCustomFieldRepository.js deleted file mode 100644 index 9c99dd48c..000000000 --- a/server/src/services/CustomFields/ResourceCustomFieldRepository.js +++ /dev/null @@ -1,154 +0,0 @@ -import Resource from 'models/Resource'; -import ResourceField from 'models/ResourceField'; -import ResourceFieldMetadata from 'models/ResourceFieldMetadata'; -import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection'; - -export default class ResourceCustomFieldRepository { - /** - * Class constructor. - */ - constructor(model) { - if (typeof model === 'function') { - this.resourceName = model.name; - } else if (typeof model === 'string') { - this.resourceName = model; - } - // Custom fields of the given resource. - this.customFields = []; - this.filledCustomFields = {}; - - // metadata of custom fields of the given resource. - this.fieldsMetadata = {}; - this.resource = {}; - } - - /** - * Fetches metadata of custom fields of the given resource. - * @param {Integer} id - Resource item id. - */ - async fetchCustomFieldsMetadata(id) { - if (typeof id === 'undefined') { - throw new Error('Please define the resource item id.'); - } - if (!this.resource) { - throw new Error('Target resource model is not found.'); - } - const metadata = await ResourceFieldMetadata.query() - .where('resource_id', this.resource.id) - .where('resource_item_id', id); - - this.fieldsMetadata[id] = metadata; - } - - /** - * Load resource. - */ - async loadResource() { - const resource = await Resource.query().where('name', this.resourceName).first(); - - if (!resource) { - throw new Error('There is no stored resource in the storage with the given model name.'); - } - this.setResource(resource); - } - - /** - * Load metadata of the resource. - */ - async loadResourceCustomFields() { - if (typeof this.resource.id === 'undefined') { - throw new Error('Please fetch resource details before fetch custom fields of the resource.'); - } - const customFields = await ResourceField.query() - .where('resource_id', this.resource.id) - .modify('whereNotPredefined'); - - this.setResourceCustomFields(customFields); - } - - /** - * Sets resource model. - * @param {Resource} resource - - */ - setResource(resource) { - this.resource = resource; - } - - /** - * Sets resource custom fields collection. - * @param {Array} customFields - - */ - setResourceCustomFields(customFields) { - this.customFields = customFields; - } - - /** - * Retrieve metadata of the resource custom fields. - * @param {Integer} itemId - - */ - getMetadata(itemId) { - return this.fieldsMetadata[itemId] || this.fieldsMetadata; - } - - /** - * Fill metadata of the custom fields that associated to the resource. - * @param {Inter} id - Resource item id. - * @param {Array} attributes - - */ - fillCustomFields(id, attributes) { - if (typeof this.filledCustomFields[id] === 'undefined') { - this.filledCustomFields[id] = []; - } - attributes.forEach((attr) => { - this.filledCustomFields[id].push(attr); - - if (!this.fieldsMetadata[id]) { - this.fieldsMetadata[id] = new ResourceFieldMetadataCollection(); - } - this.fieldsMetadata[id].setMeta(attr.key, attr.value, { - resource_id: this.resource.id, - resource_item_id: id, - }); - }); - } - - /** - * Saves the instered, updated and deleted custom fields metadata. - * @param {Integer} id - Optional resource item id. - */ - async saveCustomFields(id) { - if (id) { - if (typeof this.fieldsMetadata[id] === 'undefined') { - throw new Error('There is no resource item with the given id.'); - } - await this.fieldsMetadata[id].saveMeta(); - } else { - const opers = []; - this.fieldsMetadata.forEach((metadata) => { - const oper = metadata.saveMeta(); - opers.push(oper); - }); - await Promise.all(opers); - } - } - - /** - * Validates the exist custom fields. - */ - validateExistCustomFields() { - - } - - toArray() { - return this.fieldsMetadata.toArray(); - } - - async load() { - await this.loadResource(); - await this.loadResourceCustomFields(); - } - - static forgeMetadataCollection() { - - } -} diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts index 54ee80db5..60fe3af38 100644 --- a/server/src/services/DynamicListing/DynamicListService.ts +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -1,6 +1,6 @@ import { Service, Inject } from "typedi"; import validator from 'is-my-json-valid'; -import { Router, Request, Response, NextFunction } from 'express'; +import { Request, Response, NextFunction } from 'express'; import { ServiceError } from 'exceptions'; import { DynamicFilter, @@ -12,8 +12,13 @@ import { validateFieldKeyExistance, validateFilterRolesFieldsExistance, } from 'lib/ViewRolesBuilder'; +import { + IDynamicListFilterDTO, + IFilterRole, + IDynamicListService, + IModel, +} from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; -import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces'; const ERRORS = { VIEW_NOT_FOUND: 'view_not_found', @@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService { * @param {number} viewId * @return {Promise} */ - private async getCustomViewOrThrowError(tenantId: number, viewId: number) { + private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) { const { viewRepository } = this.tenancy.repositories(tenantId); const view = await viewRepository.getById(viewId); - if (!view || view.resourceModel !== 'Account') { + if (!view || view.resourceModel !== model.name) { throw new ServiceError(ERRORS.VIEW_NOT_FOUND); } return view; @@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService { * @throws {ServiceError} */ private validateSortColumnExistance(model: any, columnSortBy: string) { - const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy); + const notExistsField = validateFieldKeyExistance(model, columnSortBy); - if (notExistsField) { + if (!notExistsField) { throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); } } @@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService { * @param {IFilterRole[]} filterRoles * @throws {ServiceError} */ - private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) { - const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles); + private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) { + const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles); + + console.log(invalidFieldsKeys); if (invalidFieldsKeys.length > 0) { throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); @@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService { * Dynamic listing. * @param {number} tenantId * @param {IModel} model - * @param {IAccountsFilter} filter + * @param {IDynamicListFilterDTO} filter */ - async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) { - const { viewRoleRepository } = this.tenancy.repositories(tenantId); - const dynamicFilter = new DynamicFilter(model.tableName); + public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) { + const dynamicFilter = new DynamicFilter(model); // Custom view filter roles. if (filter.customViewId) { - const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId); - const viewRoles = await viewRoleRepository.allByView(view.id); + const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model); - const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression); + const viewFilter = new DynamicFilterViews(view); dynamicFilter.setFilter(viewFilter); } // Sort by the given column. if (filter.columnSortBy) { - this.validateSortColumnExistance(model, filter.columnSortBy);; + this.validateSortColumnExistance(model, filter.columnSortBy); const sortByFilter = new DynamicFilterSortBy( filter.columnSortBy, filter.sortOrder @@ -124,7 +129,7 @@ export default class DynamicListService implements IDynamicListService { this.validateFilterRolesSchema(filter.filterRoles); this.validateRolesFieldsExistance(model, filter.filterRoles); - // Validate the accounts resource fields. + // Validate the model resource fields. const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles); dynamicFilter.setFilter(filterRoles); } @@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService { * @param {Response} res * @param {NextFunction} next */ - handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) { + public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'sort_column_not_found') { return res.boom.badRequest(null, { @@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService { } if (error.errorType === 'view_not_found') { return res.boom.badRequest(null, { - errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }] - }) + errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }], + }); } if (error.errorType === 'filter_roles_fields_not_found') { return res.boom.badRequest(null, { diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 617e88d48..cc9b083a2 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -1,13 +1,17 @@ import { Service, Inject } from "typedi"; import { difference, sumBy, omit } from 'lodash'; import moment from "moment"; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import { ServiceError } from "exceptions"; import TenancyService from 'services/Tenancy/TenancyService'; import JournalPoster from 'services/Accounting/JournalPoster'; -import JournalEntry from 'services/Accounting/JournalEntry'; import JournalCommands from 'services/Accounting/JournalCommands'; -import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces'; +import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import events from 'subscribers/events'; const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -30,6 +34,9 @@ export default class ExpensesService implements IExpensesService { @Inject('logger') logger: any; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Retrieve the payment account details or returns not found server error in case the * given account not found on the storage. @@ -41,7 +48,7 @@ export default class ExpensesService implements IExpensesService { this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId }); const { accountRepository } = this.tenancy.repositories(tenantId); - const paymentAccount = await accountRepository.getById(paymentAccountId) + const paymentAccount = await accountRepository.findById(paymentAccountId) if (!paymentAccount) { this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId }); @@ -136,16 +143,15 @@ export default class ExpensesService implements IExpensesService { } } - private async revertJournalEntries( + public async revertJournalEntries( tenantId: number, expenseId: number|number[], ) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); - - if (revertOld) { - await journalCommands.revertJournalEntries(expenseId, 'Expense'); - } + + await journalCommands.revertJournalEntries(expenseId, 'Expense'); + return Promise.all([ journal.saveBalance(), journal.deleteEntries(), @@ -158,11 +164,10 @@ export default class ExpensesService implements IExpensesService { * @param {IExpense} expense * @param {IUser} authorizedUser */ - private async writeJournalEntries( + public async writeJournalEntries( tenantId: number, expense: IExpense, revertOld: boolean, - authorizedUser: ISystemUser ) { this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense }); const journal = new JournalPoster(tenantId); @@ -171,29 +176,8 @@ export default class ExpensesService implements IExpensesService { if (revertOld) { await journalCommands.revertJournalEntries(expense.id, 'Expense'); } - const mixinEntry = { - referenceType: 'Expense', - referenceId: expense.id, - date: expense.paymentDate, - userId: authorizedUser.id, - draft: !expense.publish, - }; - const paymentJournalEntry = new JournalEntry({ - credit: expense.totalAmount, - account: expense.paymentAccountId, - ...mixinEntry, - }); - journal.credit(paymentJournalEntry); - - expense.categories.forEach((category: IExpenseCategory) => { - const expenseJournalEntry = new JournalEntry({ - account: category.expenseAccountId, - debit: category.amount, - note: category.description, - ...mixinEntry, - }); - journal.debit(expenseJournalEntry); - }); + journalCommands.expense(expense); + return Promise.all([ journal.saveBalance(), journal.saveEntries(), @@ -229,7 +213,7 @@ export default class ExpensesService implements IExpensesService { * @param {IExpense} expense */ private validateExpenseIsNotPublished(expense: IExpense) { - if (expense.published) { + if (expense.publishedAt) { throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED); } } @@ -291,33 +275,29 @@ export default class ExpensesService implements IExpensesService { const { expenseRepository } = this.tenancy.repositories(tenantId); const expense = await this.getExpenseOrThrowError(tenantId, expenseId); - // 1. Validate payment account existance on the storage. + // - Validate payment account existance on the storage. const paymentAccount = await this.getPaymentAccountOrThrowError( tenantId, expenseDTO.paymentAccountId, ); - // 2. Validate expense accounts exist on the storage. + // - Validate expense accounts exist on the storage. const expensesAccounts = await this.getExpensesAccountsOrThrowError( tenantId, this.mapExpensesAccountsIdsFromDTO(expenseDTO), ); - // 3. Validate payment account type. + // - Validate payment account type. await this.validatePaymentAccountType(tenantId, paymentAccount); - // 4. Validate expenses accounts type. + // - Validate expenses accounts type. await this.validateExpensesAccountsType(tenantId, expensesAccounts); - // 5. Validate the given expense categories not equal zero. + // - Validate the given expense categories not equal zero. this.validateCategoriesNotEqualZero(expenseDTO); - // 6. Update the expense on the storage. + // - Update the expense on the storage. const expenseObj = this.expenseDTOToModel(expenseDTO); const expenseModel = await expenseRepository.update(expenseId, expenseObj, null); - // 7. In case expense published, write journal entries. - if (expenseObj.published) { - await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser); - } this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO }); return expenseModel; } @@ -364,13 +344,12 @@ export default class ExpensesService implements IExpensesService { // 6. Save the expense to the storage. const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser); const expenseModel = await expenseRepository.create(expenseObj); - - // 7. In case expense published, write journal entries. - if (expenseObj.published) { - await this.writeJournalEntries(tenantId, expenseModel, false, authorizedUser); - } + this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO }); + // Triggers `onExpenseCreated` event. + this.eventDispatcher.dispatch(events.expenses.onCreated, { tenantId, expenseId: expenseModel.id }); + return expenseModel; } @@ -394,6 +373,9 @@ export default class ExpensesService implements IExpensesService { await expenseRepository.publish(expenseId); this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId }); + + // Triggers `onExpensePublished` event. + this.eventDispatcher.dispatch(events.expenses.onPublished, { tenantId, expenseId }); } /** @@ -409,10 +391,10 @@ export default class ExpensesService implements IExpensesService { this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId }); await expenseRepository.delete(expenseId); - if (expense.published) { - await this.revertJournalEntries(tenantId, expenseId); - } this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId }); + + // Triggers `onExpenseDeleted` event. + this.eventDispatcher.dispatch(events.expenses.onDeleted, { tenantId, expenseId }); } /** @@ -427,9 +409,11 @@ export default class ExpensesService implements IExpensesService { this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds }); await expenseRepository.bulkDelete(expensesIds); - await this.revertJournalEntries(tenantId, expensesIds); this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds }); + + // Triggers `onExpenseBulkDeleted` event. + this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, { tenantId, expensesIds }); } /** @@ -443,9 +427,12 @@ export default class ExpensesService implements IExpensesService { const { expenseRepository } = this.tenancy.repositories(tenantId); this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds }); - await expenseRepository.publishBulk(expensesIds); + await expenseRepository.bulkPublish(expensesIds); this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds }); + + // Triggers `onExpenseBulkDeleted` event. + this.eventDispatcher.dispatch(events.expenses.onBulkPublished, { tenantId, expensesIds }); } /** @@ -454,17 +441,43 @@ export default class ExpensesService implements IExpensesService { * @param {IExpensesFilter} expensesFilter * @return {IExpense[]} */ - public async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) { + public async getExpensesList( + tenantId: number, + expensesFilter: IExpensesFilter + ): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { Expense } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter); this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter }); - const expenses = await Expense.query().onBuild((builder) => { + const { results, pagination } = await Expense.query().onBuild((builder) => { builder.withGraphFetched('paymentAccount'); - builder.withGraphFetched('user'); - dynamicFilter.buildQuery()(builder); - }); - return expenses; + }).pagination(expensesFilter.page - 1, expensesFilter.pageSize); + + return { + expenses: results, + pagination, filterMeta: + dynamicFilter.getResponseMeta(), + }; + } + + /** + * Retrieve expense details. + * @param {number} tenantId + * @param {number} expenseId + * @return {Promise} + */ + public async getExpense(tenantId: number, expenseId: number): Promise { + const { Expense } = this.tenancy.models(tenantId); + + const expense = await Expense.query().findById(expenseId) + .withGraphFetched('paymentAccount') + .withGraphFetched('media') + .withGraphFetched('categories'); + + if (!expense) { + throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND); + } + return expense; } } \ No newline at end of file diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index cbc8a0ff2..cacd7a52e 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -8,7 +8,6 @@ import { IItemCategoriesFilter, ISystemUser, } from "interfaces"; -import ItemCategory from "models/ItemCategory"; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -21,6 +20,7 @@ const ERRORS = { SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', + CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS' }; export default class ItemCategoriesService implements IItemCategoriesService { @@ -108,7 +108,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); const incomeType = await accountTypeRepository.getByKey('income'); - const foundAccount = await accountRepository.getById(sellAccountId); + const foundAccount = await accountRepository.findById(sellAccountId); if (!foundAccount) { this.logger.info('[items] sell account not found.', { tenantId, sellAccountId }); @@ -130,7 +130,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); - const foundAccount = await accountRepository.getById(costAccountId) + const foundAccount = await accountRepository.findById(costAccountId) if (!foundAccount) { this.logger.info('[items] cost account not found.', { tenantId, costAccountId }); @@ -152,7 +152,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); const otherAsset = await accountTypeRepository.getByKey('other_asset'); - const foundAccount = await accountRepository.getById(inventoryAccountId); + const foundAccount = await accountRepository.findById(inventoryAccountId); if (!foundAccount) { this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId }); @@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) { this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId }); await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); + await this.unassociateItemsWithCategories(tenantId, itemCategoryId); const { ItemCategory } = this.tenancy.models(tenantId); await ItemCategory.query().findById(itemCategoryId).delete(); @@ -214,7 +215,9 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @param {number[]} itemCategoriesIds */ private async getItemCategoriesOrThrowError(tenantId: number, itemCategoriesIds: number[]) { - const itemCategories = await ItemCategory.query().whereIn('id', ids); + const { ItemCategory } = this.tenancy.models(tenantId); + const itemCategories = await ItemCategory.query().whereIn('id', itemCategoriesIds); + const storedItemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id); const notFoundCategories = difference(itemCategoriesIds, storedItemCategoriesIds); @@ -233,10 +236,22 @@ export default class ItemCategoriesService implements IItemCategoriesService { const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter); const itemCategories = await ItemCategory.query().onBuild((query) => { - query.orderBy('createdAt', 'ASC'); dynamicList.buildQuery()(query); }); - return itemCategories; + return { itemCategories, filterMeta: dynamicList.getResponseMeta() }; + } + + /** + * Unlink items relations with item categories. + * @param {number} tenantId + * @param {number|number[]} itemCategoryId - + * @return {Promise} + */ + private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]): Promise { + const { Item } = this.tenancy.models(tenantId); + const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId]; + + await Item.query().whereIn('id', ids).patch({ category_id: null }); } /** @@ -246,8 +261,11 @@ export default class ItemCategoriesService implements IItemCategoriesService { */ public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) { this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds }); - await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds); + const { ItemCategory } = this.tenancy.models(tenantId); + await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds); + await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds); + await ItemCategory.query().whereIn('id', itemCategoriesIds).delete(); this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds }); } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 8428826e2..3597219c4 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from "exceptions"; -import { Item } from "models"; const ERRORS = { NOT_FOUND: 'NOT_FOUND', @@ -17,6 +16,9 @@ const ERRORS = { INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', + + ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', + ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS' } @Service() @@ -83,7 +85,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); - const foundAccount = await accountRepository.getById(costAccountId) + const foundAccount = await accountRepository.findById(costAccountId) if (!foundAccount) { this.logger.info('[items] cost account not found.', { tenantId, costAccountId }); @@ -104,7 +106,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); const incomeType = await accountTypeRepository.getByKey('income'); - const foundAccount = await accountRepository.getById(sellAccountId); + const foundAccount = await accountRepository.findById(sellAccountId); if (!foundAccount) { this.logger.info('[items] sell account not found.', { tenantId, sellAccountId }); @@ -125,7 +127,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); const otherAsset = await accountTypeRepository.getByKey('other_asset'); - const foundAccount = await accountRepository.getById(inventoryAccountId); + const foundAccount = await accountRepository.findById(inventoryAccountId); if (!foundAccount) { this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId }); @@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] trying to delete item.', { tenantId, itemId }); await this.getItemOrThrowError(tenantId, itemId); + await this.validateHasNoInvoicesOrBills(tenantId, itemId); await Item.query().findById(itemId).delete(); this.logger.info('[items] deleted successfully.', { tenantId, itemId }); @@ -269,6 +272,7 @@ export default class ItemsService implements IItemsService { this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds }); await this.validateItemsIdsExists(tenantId, itemsIds); + await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); await Item.query().whereIn('id', itemsIds).delete(); this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds }); @@ -283,14 +287,39 @@ export default class ItemsService implements IItemsService { const { Item } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter); - const items = await Item.query().onBuild((builder) => { + const { results, pagination } = await Item.query().onBuild((builder) => { builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('sellAccount'); builder.withGraphFetched('costAccount'); builder.withGraphFetched('category'); dynamicFilter.buildQuery()(builder); - }); - return items; + }).pagination( + itemsFilter.page - 1, + itemsFilter.pageSize, + ); + return { items: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; + } + + /** + * Validates the given item or items have no associated invoices or bills. + * @param {number} tenantId - Tenant id. + * @param {number|number[]} itemId - Item id. + * @throws {ServiceError} + */ + private async validateHasNoInvoicesOrBills(tenantId: number, itemId: number[]|number) { + const { ItemEntry } = this.tenancy.models(tenantId); + + const ids = Array.isArray(itemId) ? itemId : [itemId]; + const foundItemEntries = await ItemEntry.query() + .whereIn('item_id', ids) + .whereIn('reference_type', ['SaleInvoice', 'Bill']); + + if (foundItemEntries.length > 0) { + throw new ServiceError(ids.length > 1 ? + ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS : + ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS + ); + } } } \ No newline at end of file diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index 614107497..91864a82f 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -9,6 +9,7 @@ import { ISystemUser, IManualJournal, IManualJournalEntryDTO, + IPaginationMeta, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; @@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService { } /** - * + * Transform DTO to model. * @param {IManualJournalEntryDTO[]} entries */ private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) { @@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService { * @param {number} tenantId * @param {IManualJournalsFilter} filter */ - public async getManualJournals(tenantId: number, filter: IManualJournalsFilter) { + public async getManualJournals( + tenantId: number, + filter: IManualJournalsFilter + ): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { const { ManualJournal } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter); this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter }); - const manualJournal = await ManualJournal.query().onBuild((builder) => { + const { results, pagination } = await ManualJournal.query().onBuild((builder) => { dynamicList.buildQuery()(builder); - }); - return manualJournal; + }).pagination(filter.page - 1, filter.pageSize); + + return { + manualJournals: results, + pagination, + filterMeta: dynamicList.getResponseMeta(), + }; } /** @@ -421,7 +429,8 @@ export default class ManualJournalsService implements IManuaLJournalsService { this.logger.info('[manual_journals] trying to get specific manual journal.', { tenantId, manualJournalId }); const manualJournal = await ManualJournal.query() .findById(manualJournalId) - .withGraphFetched('entries'); + .withGraphFetched('entries') + .withGraphFetched('media'); return manualJournal; } diff --git a/server/src/services/Media/MediaService.ts b/server/src/services/Media/MediaService.ts new file mode 100644 index 000000000..eb8da6f70 --- /dev/null +++ b/server/src/services/Media/MediaService.ts @@ -0,0 +1,223 @@ +import fs from 'fs'; +import { Service, Inject } from 'typedi'; +import TenancyService from 'services/Tenancy/TenancyService'; +import { ServiceError } from "exceptions"; +import { IMedia, IMediaService } from 'interfaces'; +import { difference } from 'lodash'; + +const fsPromises = fs.promises; + +const ERRORS = { + MINETYPE_NOT_SUPPORTED: 'MINETYPE_NOT_SUPPORTED', + MEDIA_NOT_FOUND: 'MEDIA_NOT_FOUND', + MODEL_NAME_HAS_NO_MEDIA: 'MODEL_NAME_HAS_NO_MEDIA', + MODEL_ID_NOT_FOUND: 'MODEL_ID_NOT_FOUND', + MEDIA_IDS_NOT_FOUND: 'MEDIA_IDS_NOT_FOUND', + MEDIA_LINK_EXISTS: 'MEDIA_LINK_EXISTS' +} +const publicPath = 'storage/app/public/'; +const attachmentsMimes = ['image/png', 'image/jpeg']; + +@Service() +export default class MediaService implements IMediaService { + @Inject('logger') + logger: any; + + @Inject() + tenancy: TenancyService; + + @Inject('repositories') + sysRepositories: any; + + /** + * Retrieve media model or throw not found error + * @param tenantId + * @param mediaId + */ + async getMediaOrThrowError(tenantId: number, mediaId: number) { + const { Media } = this.tenancy.models(tenantId); + const foundMedia = await Media.query().findById(mediaId); + + if (!foundMedia) { + throw new ServiceError(ERRORS.MEDIA_NOT_FOUND); + } + return foundMedia; + } + + /** + * Retreive media models by the given ids or throw not found error. + * @param {number} tenantId + * @param {number[]} mediaIds + */ + async getMediaByIdsOrThrowError(tenantId: number, mediaIds: number[]) { + const { Media } = this.tenancy.models(tenantId); + const foundMedia = await Media.query().whereIn('id', mediaIds); + + const storedMediaIds = foundMedia.map((m) => m.id); + const notFoundMedia = difference(mediaIds, storedMediaIds); + + if (notFoundMedia.length > 0) { + throw new ServiceError(ERRORS.MEDIA_IDS_NOT_FOUND); + } + return foundMedia; + } + + /** + * Validates the model name and id. + * @param {number} tenantId + * @param {string} modelName + * @param {number} modelId + */ + async validateModelNameAndIdExistance(tenantId: number, modelName: string, modelId: number) { + const models = this.tenancy.models(tenantId); + this.logger.info('[media] trying to validate model name and id.', { tenantId, modelName, modelId }); + + if (!models[modelName]) { + this.logger.info('[media] model name not found.', { tenantId, modelName, modelId }); + throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA); + } + if (!models[modelName].media) { + this.logger.info('[media] model is not media-able.', { tenantId, modelName, modelId }); + throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA); + } + + const foundModel = await models[modelName].query().findById(modelId); + + if (!foundModel) { + this.logger.info('[media] model is not found.', { tenantId, modelName, modelId }); + throw new ServiceError(ERRORS.MODEL_ID_NOT_FOUND); + } + } + + /** + * Validates the media existance. + * @param {number} tenantId + * @param {number} mediaId + * @param {number} modelId + * @param {string} modelName + */ + async validateMediaLinkExistance( + tenantId: number, + mediaId: number, + modelId: number, + modelName: string + ) { + const { MediaLink } = this.tenancy.models(tenantId); + + const foundMediaLinks = await MediaLink.query() + .where('media_id', mediaId) + .where('model_id', modelId) + .where('model_name', modelName); + + if (foundMediaLinks.length > 0) { + throw new ServiceError(ERRORS.MEDIA_LINK_EXISTS); + } + } + + /** + * Links the given media to the specific media-able model resource. + * @param {number} tenantId + * @param {number} mediaId + * @param {number} modelId + * @param {string} modelType + */ + async linkMedia(tenantId: number, mediaId: number, modelId: number, modelName: string) { + this.logger.info('[media] trying to link media.', { tenantId, mediaId, modelId, modelName }); + const { MediaLink } = this.tenancy.models(tenantId); + await this.validateMediaLinkExistance(tenantId, mediaId, modelId, modelName); + + const media = await this.getMediaOrThrowError(tenantId, mediaId); + await this.validateModelNameAndIdExistance(tenantId, modelName, modelId); + + await MediaLink.query().insert({ mediaId, modelId, modelName }); + } + + /** + * Retrieve media metadata. + * @param {number} tenantId - Tenant id. + * @param {number} mediaId - Media id. + * @return {Promise} + */ + public async getMedia(tenantId: number, mediaId: number): Promise { + this.logger.info('[media] try to get media.', { tenantId, mediaId }); + return this.getMediaOrThrowError(tenantId, mediaId); + } + + /** + * Deletes the given media. + * @param {number} tenantId + * @param {number} mediaId + * @return {Promise} + */ + public async deleteMedia(tenantId: number, mediaId: number|number[]): Promise { + const { Media, MediaLink } = this.tenancy.models(tenantId); + const { tenantRepository } = this.sysRepositories; + + this.logger.info('[media] trying to delete media.', { tenantId, mediaId }); + + const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId]; + + const tenant = await tenantRepository.getById(tenantId); + const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds); + + const tenantPath = `${publicPath}${tenant.organizationId}`; + const unlinkOpers = []; + + media.forEach((mediaModel) => { + const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`); + unlinkOpers.push(oper); + }); + await Promise.all(unlinkOpers) + .then((resolved) => { + resolved.forEach(() => { + this.logger.info('[attachment] file has been deleted.'); + }); + }) + .catch((errors) => { + this.logger.info('[attachment] Delete item attachment file delete failed.', { errors }); + }); + await MediaLink.query().whereIn('media_id', mediaIds).delete(); + await Media.query().whereIn('id', mediaIds).delete(); + } + + /** + * Uploads the given attachment. + * @param {number} tenantId - + * @param {any} attachment - + * @return {Promise} + */ + public async upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise { + const { tenantRepository } = this.sysRepositories; + const { Media } = this.tenancy.models(tenantId); + + this.logger.info('[media] trying to upload media.', { tenantId }); + + const tenant = await tenantRepository.getById(tenantId); + const fileName = `${attachment.md5}.png`; + + // Validate the attachment. + if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) { + throw new ServiceError(ERRORS.MINETYPE_NOT_SUPPORTED); + } + if (modelName && modelId) { + await this.validateModelNameAndIdExistance(tenantId, modelName, modelId); + } + try { + await attachment.mv(`${publicPath}${tenant.organizationId}/${fileName}`); + this.logger.info('[attachment] uploaded successfully'); + } catch (error) { + this.logger.info('[attachment] uploading failed.', { error }); + } + const media = await Media.query().insertGraph({ + attachmentFile: `${fileName}`, + ...(modelName && modelId) ? { + links: [{ + modelName, + modelId, + }] + } : {}, + }); + this.logger.info('[media] uploaded successfully.', { tenantId, fileName, modelName, modelId }); + return media; + } +} \ No newline at end of file diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 4389f2267..f3825488c 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -1,13 +1,14 @@ import { Inject, Service } from 'typedi'; import { omit, sumBy } from 'lodash'; import moment from 'moment'; -import { IBillPaymentOTD, IBillPayment } from 'interfaces'; +import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import AccountsService from 'services/Accounts/AccountsService'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalEntry from 'services/Accounting/JournalEntry'; import JournalPosterService from 'services/Sales/JournalPosterService'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -25,6 +26,9 @@ export default class BillPaymentsService { @Inject() journalService: JournalPosterService; + @Inject() + dynamicListService: DynamicListingService; + /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. @@ -39,7 +43,7 @@ export default class BillPaymentsService { * @param {number} tenantId - Tenant id. * @param {BillPaymentDTO} billPayment - Bill payment object. */ - async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { + public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const billPayment = { @@ -102,7 +106,7 @@ export default class BillPaymentsService { * @param {BillPaymentDTO} billPayment * @param {IBillPayment} oldBillPayment */ - async editBillPayment( + public async editBillPayment( tenantId: number, billPaymentId: number, billPaymentDTO, @@ -171,7 +175,7 @@ export default class BillPaymentsService { * @param {Integer} billPaymentId - The given bill payment id. * @return {Promise} */ - async deleteBillPayment(tenantId: number, billPaymentId: number) { + public async deleteBillPayment(tenantId: number, billPaymentId: number) { const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query().where('id', billPaymentId).first(); @@ -203,7 +207,7 @@ export default class BillPaymentsService { * @param {BillPayment} billPayment * @param {Integer} billPaymentId */ - async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { + private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { const { AccountTransaction, Account } = this.tenancy.models(tenantId); const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); @@ -252,6 +256,35 @@ export default class BillPaymentsService { ]); } + /** + * Retrieve bill payment paginted and filterable list. + * @param {number} tenantId + * @param {IBillPaymentsFilter} billPaymentsFilter + */ + public async listBillPayments( + tenantId: number, + billPaymentsFilter: IBillPaymentsFilter, + ): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { BillPayment } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, BillPayment, billPaymentsFilter); + + this.logger.info('[bill_payment] try to get bill payments list.', { tenantId }); + const { results, pagination } = await BillPayment.query().onBuild(builder => { + builder.withGraphFetched('vendor'); + builder.withGraphFetched('paymentAccount'); + dynamicFilter.buildQuery()(builder); + }).pagination( + billPaymentsFilter.page - 1, + billPaymentsFilter.pageSize, + ); + + return { + billPayments: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } + /** * Retrieve bill payment with associated metadata. * @param {number} billPaymentId - The bill payment id. diff --git a/server/src/services/Resource/ResourceService.js b/server/src/services/Resource/ResourceService.js deleted file mode 100644 index 21f15beff..000000000 --- a/server/src/services/Resource/ResourceService.js +++ /dev/null @@ -1,5 +0,0 @@ - - -export default class ResourceService { - -} \ No newline at end of file diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts new file mode 100644 index 000000000..cd2364b46 --- /dev/null +++ b/server/src/services/Resource/ResourceService.ts @@ -0,0 +1,78 @@ +import { Service, Inject } from 'typedi'; +import { camelCase, upperFirst } from 'lodash' +import { IModel } from 'interfaces'; +import resourceFieldsKeys from 'data/ResourceFieldsKeys'; +import TenancyService from 'services/Tenancy/TenancyService'; + +@Service() +export default class ResourceService { + @Inject() + tenancy: TenancyService; + + /** + * + * @param {string} resourceName + */ + getResourceFieldsRelations(modelName: string) { + const fieldsRelations = resourceFieldsKeys[modelName]; + + if (!fieldsRelations) { + throw new Error('Fields relation not found in thte given resource model.'); + } + return fieldsRelations; + } + + /** + * Transform resource to model name. + * @param {string} resourceName + */ + private resourceToModelName(resourceName: string): string { + return upperFirst(camelCase(resourceName)); + } + + /** + * Retrieve model from resource name in specific tenant. + * @param {number} tenantId + * @param {string} resourceName + */ + public getModel(tenantId: number, resourceName: string) { + const models = this.tenancy.models(tenantId); + const modelName = this.resourceToModelName(resourceName); + + return models[modelName]; + } + + getModelFields(Model: IModel) { + const fields = Object.keys(Model.fields); + + return fields.sort((a, b) => { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; + }); + } + + /** + * + * @param {string} resourceName + */ + getResourceFields(Model: IModel) { + console.log(Model); + + if (Model.resourceable) { + return this.getModelFields(Model); + } + return []; + } + + /** + * + * @param {string} resourceName + */ + getResourceColumns(Model: IModel) { + if (Model.resourceable) { + return this.getModelFields(Model); + } + return []; + } +} \ No newline at end of file diff --git a/server/src/services/Sales/JournalPosterService.ts b/server/src/services/Sales/JournalPosterService.ts index 000bb17a4..d0e4d95b8 100644 --- a/server/src/services/Sales/JournalPosterService.ts +++ b/server/src/services/Sales/JournalPosterService.ts @@ -14,7 +14,7 @@ export default class JournalPosterService { * @param {string} referenceType - The transaction reference type. * @return {Promise} */ - async deleteJournalTransactions( + async revertJournalTransactions( tenantId: number, referenceId: number, referenceType: string diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 57d85c3b7..f0f27a262 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository'; import CustomerRepository from 'repositories/CustomerRepository'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -27,6 +28,9 @@ export default class PaymentReceiveService { @Inject() journalService: JournalPosterService; + @Inject() + dynamicListService: DynamicListingService; + @Inject('logger') logger: any; @@ -37,7 +41,7 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {IPaymentReceive} paymentReceive */ - async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { + public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { const { PaymentReceive, PaymentReceiveEntry, @@ -107,7 +111,7 @@ export default class PaymentReceiveService { * @param {IPaymentReceive} paymentReceive - * @param {IPaymentReceive} oldPaymentReceive - */ - async editPaymentReceive( + public async editPaymentReceive( tenantId: number, paymentReceiveId: number, paymentReceive: any, @@ -242,7 +246,7 @@ export default class PaymentReceiveService { * @param {number} tenantId - Tenant id. * @param {Integer} paymentReceiveId - Payment receive id. */ - async getPaymentReceive(tenantId: number, paymentReceiveId: number) { + public async getPaymentReceive(tenantId: number, paymentReceiveId: number) { const { PaymentReceive } = this.tenancy.models(tenantId); const paymentReceive = await PaymentReceive.query() .where('id', paymentReceiveId) @@ -250,6 +254,30 @@ export default class PaymentReceiveService { .first(); return paymentReceive; } + + /** + * Retrieve payment receives paginated and filterable list. + * @param {number} tenantId + * @param {IPaymentReceivesFilter} paymentReceivesFilter + */ + public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) { + const { PaymentReceive } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter); + + const { results, pagination } = await PaymentReceive.query().onBuild((builder) => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('depositAccount'); + dynamicFilter.buildQuery()(builder); + }).pagination( + paymentReceivesFilter.page - 1, + paymentReceivesFilter.pageSize, + ); + return { + paymentReceives: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } /** * Retrieve the payment receive details with associated invoices. @@ -310,7 +338,7 @@ export default class PaymentReceiveService { * @param {IPaymentReceive} paymentReceive * @param {Number} paymentReceiveId */ - async recordPaymentReceiveJournalEntries( + private async recordPaymentReceiveJournalEntries( tenantId: number, paymentReceive: any, paymentReceiveId?: number @@ -370,7 +398,7 @@ export default class PaymentReceiveService { * @param {Array} revertInvoices * @return {Promise} */ - async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { + private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { const { SaleInvoice } = this.tenancy.models(tenantId); const opers: Promise[] = []; @@ -392,7 +420,7 @@ export default class PaymentReceiveService { * @param {Array} newPaymentReceiveEntries * @return */ - async saveChangeInvoicePaymentAmount( + private async saveChangeInvoicePaymentAmount( tenantId: number, paymentReceiveEntries: [], newPaymentReceiveEntries: [], diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts index ac00708c1..8ec782d8f 100644 --- a/server/src/services/Sales/SalesEstimate.ts +++ b/server/src/services/Sales/SalesEstimate.ts @@ -1,8 +1,10 @@ import { omit, difference, sumBy, mixin } from 'lodash'; import { Service, Inject } from 'typedi'; +import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import { formatDateFields } from 'utils'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; /** * Sale estimate service. @@ -19,6 +21,9 @@ export default class SaleEstimateService { @Inject('logger') logger: any; + @Inject() + dynamicListService: DynamicListingService; + /** * Creates a new estimate with associated entries. * @async @@ -208,4 +213,32 @@ export default class SaleEstimateService { }); return foundEstimates.length > 0; } + + /** + * Retrieves estimates filterable and paginated list. + * @param {number} tenantId + * @param {IEstimatesFilter} estimatesFilter + */ + public async estimatesList( + tenantId: number, + estimatesFilter: IEstimatesFilter + ): Promise<{ salesEstimates: ISaleEstimate[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleEstimate } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleEstimate, estimatesFilter); + + const { results, pagination } = await SaleEstimate.query().onBuild(builder => { + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + dynamicFilter.buildQuery()(builder); + }).pagination( + estimatesFilter.page - 1, + estimatesFilter.pageSize, + ); + + return { + salesEstimates: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } } \ No newline at end of file diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 877b0bf01..1dc960e5b 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -1,11 +1,16 @@ import { Service, Inject } from 'typedi'; import { omit, sumBy, difference, pick, chain } from 'lodash'; -import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces'; import JournalPoster from 'services/Accounting/JournalPoster'; import HasItemsEntries from 'services/Sales/HasItemsEntries'; import InventoryService from 'services/Inventory/Inventory'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; /** @@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @Inject('logger') logger: any; + @Inject() + dynamicListService: DynamicListingService; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Creates a new sale invoices and store it to the storage * with associated to entries and journal transactions. @@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {ISaleInvoice} saleInvoiceDTO - * @return {ISaleInvoice} */ - async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { + public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); @@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {Number} saleInvoiceId - * @param {ISaleInvoice} saleInvoice - */ - async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { + public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); @@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @async * @param {Number} saleInvoiceId - The given sale invoice id. */ - async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { + public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { const { SaleInvoice, ItemEntry, @@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {number} saleInvoiceId - * @param {boolean} override - */ - recordInventoryTranscactions( + private recordInventoryTranscactions( tenantId: number, saleInvoice, saleInvoiceId: number, @@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @param {string} transactionType * @param {number} transactionId */ - async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { + private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { const { InventoryTransaction } = this.tenancy.models(tenantId); const opers: Promise<[]>[] = []; @@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { * @async * @param {Number} saleInvoiceId */ - async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) { + public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) { const { SaleInvoice } = this.tenancy.models(tenantId); return SaleInvoice.query() .where('id', saleInvoiceId) @@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost { journal.saveBalance(), ]); } + + /** + * Retrieve sales invoices filterable and paginated list. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter): + Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleInvoice } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter); + + this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter }); + const { results, pagination } = await SaleInvoice.query().onBuild((builder) => { + builder.withGraphFetched('entries'); + builder.withGraphFetched('customer'); + dynamicFilter.buildQuery()(builder); + }).pagination( + salesInvoicesFilter.page - 1, + salesInvoicesFilter.pageSize, + ); + return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() }; + } } diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index df726b1d2..96a481136 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService'; import HasItemEntries from 'services/Sales/HasItemsEntries'; import TenancyService from 'services/Tenancy/TenancyService'; import { formatDateFields } from 'utils'; +import { IFilterMeta, IPaginationMeta } from 'interfaces'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class SalesReceiptService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + @Inject() journalService: JournalPosterService; @@ -22,7 +27,7 @@ export default class SalesReceiptService { * @param {ISaleReceipt} saleReceipt * @return {Object} */ - async createSaleReceipt(tenantId: number, saleReceiptDTO: any) { + public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); @@ -55,7 +60,7 @@ export default class SalesReceiptService { * @param {ISaleReceipt} saleReceipt * @return {void} */ - async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) { + public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); @@ -88,7 +93,7 @@ export default class SalesReceiptService { * @param {Integer} saleReceiptId * @return {void} */ - async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { + public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const deleteSaleReceiptOper = SaleReceipt.query() .where('id', saleReceiptId) @@ -160,4 +165,35 @@ export default class SalesReceiptService { return saleReceipt; } + + /** + * Retrieve sales receipts paginated and filterable list. + * @param {number} tenantId + * @param {ISaleReceiptFilter} salesReceiptsFilter + */ + public async salesReceiptsList( + tenantId: number, + salesReceiptsFilter: ISaleReceiptFilter, + ): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + const { SaleReceipt } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleReceipt, salesReceiptsFilter); + + this.logger.info('[sale_receipt] try to get sales receipts list.', { tenantId }); + const { results, pagination } = await SaleReceipt.query().onBuild((builder) => { + builder.withGraphFetched('depositAccount'); + builder.withGraphFetched('customer'); + builder.withGraphFetched('entries'); + + dynamicFilter.buildQuery()(builder); + }).pagination( + salesReceiptsFilter.page - 1, + salesReceiptsFilter.pageSize, + ); + + return { + salesReceipts: results, + pagination, + filterMeta: dynamicFilter.getResponseMeta(), + }; + } } diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 8c3b2479d..2d4dfe160 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -27,7 +27,6 @@ export default class HasTenancyService { singletonService(tenantId: number, key: string, callback: Function) { const container = this.tenantContainer(tenantId); const Logger = Container.get('logger'); - const hasServiceInstnace = container.has(key); if (!hasServiceInstnace) { @@ -74,12 +73,24 @@ export default class HasTenancyService { }); } + /** + * Sets i18n locals function. + * @param {number} tenantId + * @param locals + */ + setI18nLocals(tenantId: number, locals: any) { + return this.singletonService(tenantId, 'i18n', () => { + return locals; + }) + } + /** * Retrieve i18n locales methods. * @param {number} tenantId - Tenant id. */ i18n(tenantId: number) { return this.singletonService(tenantId, 'i18n', () => { + throw new Error('I18n locals is not set yet.'); }); } diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index 6af82e033..cec769496 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -1,20 +1,24 @@ import { Service, Inject } from "typedi"; -import { pick, difference } from 'lodash'; +import { difference } from 'lodash'; import { ServiceError } from 'exceptions'; import { IViewsService, IViewDTO, IView, - IViewRole, - IViewHasColumn, + IViewEditDTO, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; +import ResourceService from "services/Resource/ResourceService"; import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; const ERRORS = { VIEW_NOT_FOUND: 'VIEW_NOT_FOUND', VIEW_PREDEFINED: 'VIEW_PREDEFINED', - INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION', + VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE', + LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION', + RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', + RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', + RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND' }; @Service() @@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService { @Inject('logger') logger: any; + @Inject() + resourceService: ResourceService; + /** * Listing resource views. - * @param {number} tenantId - * @param {string} resourceModel + * @param {number} tenantId - + * @param {string} resourceModel - */ - public async listViews(tenantId: number, resourceModel: string) { - const { View } = this.tenancy.models(tenantId); - return View.query().where('resource_model', resourceModel); - } + public async listResourceViews(tenantId: number, resourceModel: string): Promise { + this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel }); - validateResourceFieldsExistance() { - - } - - validateResourceColumnsExistance() { - - } - - getView(tenantId: number, viewId: number) { + // Validate the resource model name is valid. + this.getResourceModelOrThrowError(tenantId, resourceModel); + const { viewRepository } = this.tenancy.repositories(tenantId); + return viewRepository.allByResource(resourceModel); } /** + * Validate model resource conditions fields existance. + * @param {string} resourceName + * @param {IViewRoleDTO[]} viewRoles + */ + private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) { + const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel); + + const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey); + const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); + + if (notFoundFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND); + } + return notFoundFieldsKeys; + } + + /** + * Validates model resource columns existance. + * @param {string} resourceName + * @param {IViewColumnDTO[]} viewColumns + */ + private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) { + const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel); + + const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey); + const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); + + if (notFoundFieldsKeys.length > 0) { + throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND); + } + return notFoundFieldsKeys; + } + + /** + * Retrieve the given view details with associated conditions and columns. + * @param {number} tenantId - Tenant id. + * @param {number} viewId - View id. + */ + public getView(tenantId: number, viewId: number): Promise { + this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); + return this.getViewOrThrowError(tenantId, viewId); + } + + /** + * Retrieve view or throw not found error. + * @param {number} tenantId - Tenant id. + * @param {number} viewId - View id. + */ + private async getViewOrThrowError(tenantId: number, viewId: number): Promise { + const { viewRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); + const view = await viewRepository.getById(viewId); + + if (!view) { + this.logger.info('[view] view not found.', { tenantId, viewId }); + throw new ServiceError(ERRORS.VIEW_NOT_FOUND); + } + return view; + } + + /** + * Retrieve resource model from resource name or throw not found error. + * @param {number} tenantId + * @param {number} resourceModel + */ + private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel { + const ResourceModel = this.resourceService.getModel(tenantId, resourceModel); + + if (!ResourceModel || !ResourceModel.resourceable) { + throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); + } + return ResourceModel; + } + + /** + * Validates view name uniqiness in the given resource. + * @param {number} tenantId + * @param {stirng} resourceModel + * @param {string} viewName + * @param {number} notViewId + */ + private async validateViewNameUniquiness( + tenantId: number, + resourceModel: string, + viewName: string, + notViewId?: number + ): void { + const { View } = this.tenancy.models(tenantId); + const foundViews = await View.query() + .where('resource_model', resourceModel) + .where('name', viewName) + .onBuild((builder) => { + if (notViewId) { + builder.whereNot('id', notViewId); + } + }); + + if (foundViews.length > 0) { + throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE); + } + } + + /** + * Creates a new custom view to specific resource. + * ----–––––– * Precedures. * ----–––––– * - Validate resource fields existance. @@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService { * @param {number} tenantId - Tenant id. * @param {IViewDTO} viewDTO - View DTO. */ - async newView(tenantId: number, viewDTO: IViewDTO): Promise { - const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId); - + public async newView(tenantId: number, viewDTO: IViewDTO): Promise { + const { viewRepository } = this.tenancy.repositories(tenantId); this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO }); + + // Validate the resource name is exists and resourcable. + const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel); + + // Validate view name uniquiness. + await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name); + + // Validate the given fields keys exist on the storage. + this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles); + + // Validate the given columnable fields keys exists on the storage. + this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns); + // Validates the view conditional logic expression. if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) { - throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION); + throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } // Save view details. - const view = await View.query().insert({ - name: viewDTO.name, + const view = await viewRepository.insert({ predefined: false, + name: viewDTO.name, rolesLogicExpression: viewDTO.logicExpression, + resourceModel: viewDTO.resourceModel, + roles: viewDTO.roles, + columns: viewDTO.columns, }); - this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO }); - - // Save view roles async operations. - const saveViewRolesOpers = []; - - viewDTO.roles.forEach((role) => { - const saveViewRoleOper = ViewRole.query().insert({ - ...pick(role, ['fieldKey', 'comparator', 'value', 'index']), - viewId: view.id, - }); - saveViewRolesOpers.push(saveViewRoleOper); - }); - - viewDTO.columns.forEach((column) => { - const saveViewColumnOper = ViewColumn.query().insert({ - viewId: view.id, - index: column.index, - }); - saveViewRolesOpers.push(saveViewColumnOper); - }); - this.logger.info('[views] roles and columns inserted to the storage.', { tenantId, viewDTO }); - - await Promise.all(saveViewRolesOpers); + this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO }); + return view; } /** + * Edits view details, roles and columns on the storage. + * -------- + * Precedures. + * -------- + * - Validate view existance. + * - Validate view resource fields existance. + * - Validate view resource columns existance. + * - Validate view logic expression. + * - Delete old view columns and roles. + * - Re-save view columns and roles. * * @param {number} tenantId * @param {number} viewId * @param {IViewEditDTO} */ - async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) { - const { View, ViewRole, ViewColumn } = req.models; - const view = await View.query().where('id', viewId) - .withGraphFetched('roles.field') - .withGraphFetched('columns') - .first(); + public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise { + const { View } = this.tenancy.models(tenantId); + this.logger.info('[view] trying to edit custom view.', { tenantId, viewId }); - const errorReasons = []; - const fieldsSlugs = viewEditDTO.roles.map((role) => role.field_key); - const resourceFieldsKeys = resource.fields.map((f) => f.key); - const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field])); - const columnsKeys = viewEditDTO.columns.map((c) => c.key); + // Retrieve view details or throw not found error. + const view = await this.getViewOrThrowError(tenantId, viewId); - // The difference between the stored resource fields and submit fields keys. - const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); + // Validate the resource name is exists and resourcable. + const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel); - // Validate not found resource fields keys. - if (notFoundFields.length > 0) { - errorReasons.push({ - type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields, - }); - } - // The difference between the stored resource fields and the submit columns keys. - const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); + // Validate view name uniquiness. + await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId); + + // Validate the given fields keys exist on the storage. + this.validateResourceRolesFieldsExistance(ResourceModel, view.roles); + + // Validate the given columnable fields keys exists on the storage. + this.validateResourceColumnsExistance(ResourceModel, view.columns); - // Validate not found view columns. - if (notFoundColumns.length > 0) { - errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns }); - } // Validates the view conditional logic expression. - if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) { - errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); + if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) { + throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); } - - const viewRolesIds = view.roles.map((r) => r.id); - const viewColumnsIds = view.columns.map((c) => c.id); - - const formUpdatedRoles = viewEditDTO.roles.filter((r) => r.id); - const formInsertRoles = viewEditDTO.roles.filter((r) => !r.id); - - const formRolesIds = formUpdatedRoles.map((r) => r.id); - - const formUpdatedColumns = viewEditDTO.columns.filter((r) => r.id); - const formInsertedColumns = viewEditDTO.columns.filter((r) => !r.id); - const formColumnsIds = formUpdatedColumns.map((r) => r.id); - - const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds); - const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds); - - const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds); - const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds); - - // Validate the not found view roles ids. - if (notFoundViewRolesIds.length) { - errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds }); - } - // Validate the not found view columns ids. - if (notFoundViewColumnsIds.length) { - errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - const asyncOpers = []; - // Save view details. await View.query() .where('id', view.id) @@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService { name: viewEditDTO.name, roles_logic_expression: viewEditDTO.logicExpression, }); - - // Update view roles. - if (formUpdatedRoles.length > 0) { - formUpdatedRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const updateOper = ViewRole.query() - .where('id', role.id) - .update({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - }); - asyncOpers.push(updateOper); - }); - } - // Insert a new view roles. - if (formInsertRoles.length > 0) { - formInsertRoles.forEach((role) => { - const fieldModel = resourceFieldsKeysMap.get(role.field_key); - const insertOper = ViewRole.query() - .insert({ - ...pick(role, ['comparator', 'value', 'index']), - field_id: fieldModel.id, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Delete view roles. - if (rolesIdsShouldDeleted.length > 0) { - const deleteOper = ViewRole.query() - .whereIn('id', rolesIdsShouldDeleted) - .delete(); - asyncOpers.push(deleteOper); - } - // Insert a new view columns to the storage. - if (formInsertedColumns.length > 0) { - formInsertedColumns.forEach((column) => { - const fieldModel = resourceFieldsKeysMap.get(column.key); - const insertOper = ViewColumn.query() - .insert({ - field_id: fieldModel.id, - index: column.index, - view_id: view.id, - }); - asyncOpers.push(insertOper); - }); - } - // Update the view columns on the storage. - if (formUpdatedColumns.length > 0) { - formUpdatedColumns.forEach((column) => { - const updateOper = ViewColumn.query() - .where('id', column.id) - .update({ - index: column.index, - }); - asyncOpers.push(updateOper); - }); - } - // Delete the view columns from the storage. - if (columnsIdsShouldDelete.length > 0) { - const deleteOper = ViewColumn.query() - .whereIn('id', columnsIdsShouldDelete) - .delete(); - asyncOpers.push(deleteOper); - } - await Promise.all(asyncOpers); + this.logger.info('[view] edited successfully.', { tenantId, viewId }); } /** * Retrieve views details of the given id or throw not found error. + * @private * @param {number} tenantId * @param {number} viewId + * @return {Promise} */ private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise { const { View } = this.tenancy.models(tenantId); @@ -267,6 +272,7 @@ export default class ViewsService implements IViewsService { * Deletes the given view with associated roles and columns. * @param {number} tenantId - Tenant id. * @param {number} viewId - View id. + * @return {Promise} */ public async deleteView(tenantId: number, viewId: number): Promise { const { View } = this.tenancy.models(tenantId); diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 975036766..13a990f23 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -37,6 +37,18 @@ export default { tenantSeeded: 'onTenantSeeded', }, + /** + * Accounts service. + */ + accounts: { + onCreated: 'onAccountCreated', + onEdited: 'onAccountEdited', + onDeleted: 'onAccountDeleted', + onBulkDeleted: 'onBulkDeleted', + onBulkActivated: 'onAccountBulkActivated', + onActivated: 'onAccountActivated' + }, + /** * Manual journals service. */ @@ -47,5 +59,18 @@ export default { onDeletedBulk: 'onManualJournalCreatedBulk', onPublished: 'onManualJournalPublished', onPublishedBulk: 'onManualJournalPublishedBulk', + }, + + /** + * Expenses service. + */ + expenses: { + onCreated: 'onExpenseCreated', + onEdited: 'onExpenseEdited', + onDeleted: 'onExpenseDelted', + onPublished: 'onExpensePublished', + + onBulkDeleted: 'onExpenseBulkDeleted', + onBulkPublished: 'onBulkPublished', } } diff --git a/server/src/subscribers/expenses.ts b/server/src/subscribers/expenses.ts new file mode 100644 index 000000000..3425509bf --- /dev/null +++ b/server/src/subscribers/expenses.ts @@ -0,0 +1,87 @@ +import { Container, Inject, Service } from 'typedi'; +import { EventSubscriber, On } from 'event-dispatch'; +import events from 'subscribers/events'; +import ExpensesService from 'services/Expenses/ExpensesService'; +import TenancyService from 'services/Tenancy/TenancyService'; +import ExpenseRepository from 'repositories/ExpenseRepository'; + +@EventSubscriber() +export default class ExpensesSubscriber { + tenancy: TenancyService; + expensesService: ExpensesService; + + constructor() { + this.tenancy = Container.get(TenancyService); + this.expensesService = Container.get(ExpensesService); + } + + /** + * On expense created. + */ + @On(events.expenses.onCreated) + public async onExpenseCreated({ expenseId, tenantId }) { + const { expenseRepository } = this.tenancy.repositories(tenantId); + const expense = await expenseRepository.getById(expenseId); + + // In case expense published, write journal entries. + if (expense.publishedAt) { + await this.expensesService.writeJournalEntries(tenantId, expense, false); + } + } + + /** + * On expense edited. + */ + @On(events.expenses.onEdited) + public async onExpenseEdited({ expenseId, tenantId }) { + const { expenseRepository } = this.tenancy.repositories(tenantId); + const expense = await expenseRepository.getById(expenseId); + + // In case expense published, write journal entries. + if (expense.publishedAt) { + await this.expensesService.writeJournalEntries(tenantId, expense, true); + } + } + + /** + * + * @param param0 + */ + @On(events.expenses.onDeleted) + public async onExpenseDeleted({ expenseId, tenantId }) { + await this.expensesService.revertJournalEntries(tenantId, expenseId); + } + + /** + * + * @param param0 + */ + @On(events.expenses.onPublished) + public async onExpensePublished({ expenseId, tenantId }) { + const { expenseRepository } = this.tenancy.repositories(tenantId); + const expense = await expenseRepository.getById(expenseId); + + // In case expense published, write journal entries. + if (expense.publishedAt) { + await this.expensesService.writeJournalEntries(tenantId, expense, false); + } + } + + /** + * + * @param param0 + */ + @On(events.expenses.onBulkDeleted) + public onExpenseBulkDeleted({ expensesIds, tenantId }) { + + } + + /** + * + * @param param0 + */ + @On(events.expenses.onBulkPublished) + public onExpenseBulkPublished({ expensesIds, tenantId }) { + + } +} \ No newline at end of file diff --git a/server/src/system/migrations/20190104195900_create_password_resets_table.js b/server/src/system/migrations/20190104195900_create_password_resets_table.js index bd274950e..9337949c7 100644 --- a/server/src/system/migrations/20190104195900_create_password_resets_table.js +++ b/server/src/system/migrations/20190104195900_create_password_resets_table.js @@ -1,8 +1,8 @@ exports.up = (knex) => knex.schema.createTable('password_resets', (table) => { table.increments(); - table.string('email'); - table.string('token'); + table.string('email').index(); + table.string('token').index(); table.timestamp('created_at'); }); diff --git a/server/src/system/migrations/20200420134631_create_tenants_table.js b/server/src/system/migrations/20200420134631_create_tenants_table.js index 1d73550fb..ef968abf9 100644 --- a/server/src/system/migrations/20200420134631_create_tenants_table.js +++ b/server/src/system/migrations/20200420134631_create_tenants_table.js @@ -2,7 +2,7 @@ exports.up = function(knex) { return knex.schema.createTable('tenants', (table) => { table.bigIncrements(); - table.string('organization_id'); + table.string('organization_id').index(); table.dateTime('under_maintenance_since').nullable(); table.dateTime('initialized_at').nullable(); diff --git a/server/src/system/migrations/20190822214242_create_users_table.js b/server/src/system/migrations/20200420134633_create_users_table.js similarity index 50% rename from server/src/system/migrations/20190822214242_create_users_table.js rename to server/src/system/migrations/20200420134633_create_users_table.js index 50fa5e42c..d4a08a226 100644 --- a/server/src/system/migrations/20190822214242_create_users_table.js +++ b/server/src/system/migrations/20200420134633_create_users_table.js @@ -4,18 +4,15 @@ exports.up = function (knex) { table.increments(); table.string('first_name'); table.string('last_name'); - table.string('email').unique(); - table.string('phone_number').unique(); + table.string('email').unique().index(); + table.string('phone_number').unique().index(); table.string('password'); - table.boolean('active'); + table.boolean('active').index(); table.string('language'); - - table.integer('tenant_id').unsigned(); - - table.date('invite_accepted_at'); - table.date('last_login_at'); - - table.dateTime('deleted_at'); + table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants'); + table.date('invite_accepted_at').index(); + table.date('last_login_at').index(); + table.dateTime('deleted_at').index(); table.timestamps(); }); }; diff --git a/server/src/system/migrations/20200422225247_create_user_invites_table.js b/server/src/system/migrations/20200422225247_create_user_invites_table.js index 21f1e7fa6..abb723b20 100644 --- a/server/src/system/migrations/20200422225247_create_user_invites_table.js +++ b/server/src/system/migrations/20200422225247_create_user_invites_table.js @@ -2,9 +2,9 @@ exports.up = function(knex) { return knex.schema.createTable('user_invites', (table) => { table.increments(); - table.string('email'); - table.string('token').unique(); - table.integer('tenant_id').unsigned(); + table.string('email').index(); + table.string('token').unique().index(); + table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants'); table.datetime('created_at'); }); }; diff --git a/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js b/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js deleted file mode 100644 index 5ab810315..000000000 --- a/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js +++ /dev/null @@ -1,18 +0,0 @@ -exports.up = function(knex) { - return knex.schema.createTable('subscriptions_usage', table => { - table.increments(); - table.integer('user_id'); - table.integer('plan_id'); - - table.dateTime('trial_ends_at'); - - table.dateTime('subscription_starts_at'); - table.dateTime('subscription_ends_at'); - - table.timestamps(); - }); -}; - -exports.down = function(knex) { - return knex.schema.dropTableIfExists('subscriptions_usage'); -}; diff --git a/server/src/system/migrations/20200823234134_create_plans_table.js b/server/src/system/migrations/20200823234134_create_plans_table.js index 80392db62..2fc61a43a 100644 --- a/server/src/system/migrations/20200823234134_create_plans_table.js +++ b/server/src/system/migrations/20200823234134_create_plans_table.js @@ -17,7 +17,6 @@ exports.up = function(knex) { table.string('invoice_interval').nullable(); table.integer('index').unsigned(); - table.timestamps(); }).then(() => { return knex.seed.run({ diff --git a/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js b/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js index 78aaf3356..43fea2798 100644 --- a/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js +++ b/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js @@ -2,12 +2,10 @@ exports.up = function(knex) { return knex.schema.createTable('subscription_plan_features', table => { table.increments(); - - table.integer('plan_id').unsigned(); + table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans'); table.string('slug'); table.string('name'); table.string('description'); - table.timestamps(); }); }; diff --git a/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js b/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js index a8b7e2621..acdc9a0e6 100644 --- a/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js +++ b/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js @@ -4,8 +4,8 @@ exports.up = function(knex) { table.increments('id'); table.string('slug'); - table.integer('plan_id').unsigned(); - table.integer('tenant_id').unsigned(); + table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans'); + table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants'); table.dateTime('trial_started_at').nullable(); table.dateTime('trial_ends_at').nullable(); diff --git a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js index 5b79cd4a2..206721cb2 100644 --- a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js +++ b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js @@ -3,15 +3,15 @@ exports.up = function(knex) { return knex.schema.createTable('subscription_licenses', table => { table.increments(); - table.string('license_code').unique(); - table.integer('plan_id').unsigned(); + table.string('license_code').unique().index(); + table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans'); table.integer('license_period').unsigned(); table.string('period_interval'); - table.dateTime('sent_at'); - table.dateTime('disabled_at'); - table.dateTime('used_at'); + table.dateTime('sent_at').index(); + table.dateTime('disabled_at').index(); + table.dateTime('used_at').index(); table.timestamps(); }) diff --git a/server/src/system/repositories/SystemUserRepository.ts b/server/src/system/repositories/SystemUserRepository.ts index ccfa76fa4..7ab28b3ba 100644 --- a/server/src/system/repositories/SystemUserRepository.ts +++ b/server/src/system/repositories/SystemUserRepository.ts @@ -12,21 +12,22 @@ export default class SystemUserRepository extends SystemRepository { /** * Patches the last login date to the given system user. * @param {number} userId + * @return {Promise} */ - async patchLastLoginAt(userId: number) { - const user = await SystemUser.query().patchAndFetchById(userId, { + async patchLastLoginAt(userId: number): Promise { + await SystemUser.query().patchAndFetchById(userId, { last_login_at: moment().toMySqlDateTime() }); - this.flushUserCache(user); - return user; + this.flushCache(); } /** * Finds system user by crediential. * @param {string} crediential - Phone number or email. * @return {ISystemUser} + * @return {Promise} */ - findByCrediential(crediential: string) { + findByCrediential(crediential: string): Promise { return SystemUser.query().whereNotDeleted() .findOne('email', crediential) .orWhere('phone_number', crediential); @@ -34,9 +35,10 @@ export default class SystemUserRepository extends SystemRepository { /** * Retrieve system user details of the given id. - * @param {number} userId + * @param {number} userId - User id. + * @return {Promise} */ - getById(userId: number) { + getById(userId: number): Promise { return this.cache.get(`systemUser.id.${userId}`, () => { return SystemUser.query().whereNotDeleted().findById(userId); }); @@ -44,10 +46,11 @@ export default class SystemUserRepository extends SystemRepository { /** * Retrieve user by id and tenant id. - * @param {number} userId - * @param {number} tenantId + * @param {number} userId - User id. + * @param {number} tenantId - Tenant id. + * @return {Promise} */ - getByIdAndTenant(userId: number, tenantId: number) { + getByIdAndTenant(userId: number, tenantId: number): Promise { return this.cache.get(`systemUser.id.${userId}.tenant.${tenantId}`, () => { return SystemUser.query().whereNotDeleted() .findOne({ id: userId, tenant_id: tenantId }); @@ -56,9 +59,10 @@ export default class SystemUserRepository extends SystemRepository { /** * Retrieve system user details by the given email. - * @param {string} email + * @param {string} email - Email + * @return {Promise} */ - getByEmail(email: string) { + getByEmail(email: string): Promise { return this.cache.get(`systemUser.email.${email}`, () => { return SystemUser.query().whereNotDeleted().findOne('email', email); }); @@ -66,9 +70,10 @@ export default class SystemUserRepository extends SystemRepository { /** * Retrieve user by phone number. - * @param {string} phoneNumber + * @param {string} phoneNumber - Phone number + * @return {Promise} */ - getByPhoneNumber(phoneNumber: string) { + getByPhoneNumber(phoneNumber: string): Promise { return this.cache.get(`systemUser.phoneNumber.${phoneNumber}`, () => { return SystemUser.query().whereNotDeleted().findOne('phoneNumber', phoneNumber); }); @@ -76,62 +81,61 @@ export default class SystemUserRepository extends SystemRepository { /** * Edits details. - * @param {number} userId - * @param {number} user + * @param {number} userId - User id. + * @param {number} user - User input. + * @return {Promise} */ - edit(userId: number, userInput: ISystemUser) { - const user = SystemUser.query().patchAndFetchById(userId, { ...userInput }); - this.flushUserCache(user); - return user; + async edit(userId: number, userInput: ISystemUser): Promise { + await SystemUser.query().patchAndFetchById(userId, { ...userInput }); + this.flushCache(); } /** * Creates a new user. - * @param {IUser} userInput + * @param {IUser} userInput - User input. + * @return {Promise} */ - create(userInput: ISystemUser) { - return SystemUser.query().insert({ ...userInput }); + async create(userInput: ISystemUser): Promise { + const systemUser = await SystemUser.query().insert({ ...userInput }); + this.flushCache(); + + return systemUser; } /** * Deletes user by the given id. - * @param {number} userId + * @param {number} userId - User id. + * @return {Promise} */ - async deleteById(userId: number) { - const user = await this.getById(userId); + async deleteById(userId: number): Promise { await SystemUser.query().where('id', userId).delete(); - this.flushUserCache(user); + this.flushCache(); } /** * Activate user by the given id. - * @param {number} userId + * @param {number} userId - User id. + * @return {Promise} */ - async activateById(userId: number) { - const user = await SystemUser.query().patchAndFetchById(userId, { active: 1 }); - this.flushUserCache(user); - return user; + async activateById(userId: number): Promise { + await SystemUser.query().patchAndFetchById(userId, { active: 1 }); + this.flushCache(); } /** * Inactivate user by the given id. - * @param {number} userId + * @param {number} userId - User id. + * @return {Promise} */ - async inactivateById(userId: number) { - const user = await SystemUser.query().patchAndFetchById(userId, { active: 0 }); - this.flushUserCache(user); - return user; + async inactivateById(userId: number): Promise { + await SystemUser.query().patchAndFetchById(userId, { active: 0 }); + this.flushCache(); } /** - * Flush user cache. - * @param {IUser} user + * Flushes user repository cache. */ - flushUserCache(user: ISystemUser) { - this.cache.del(`systemUser.phoneNumber.${user.phoneNumber}`); - this.cache.del(`systemUser.email.${user.email}`); - - this.cache.del(`systemUser.id.${user.id}`); - this.cache.del(`systemUser.id.${user.id}.tenant.${user.tenantId}`); + flushCache() { + this.cache.delStartWith('systemUser'); } } \ No newline at end of file