fix: system repositories.

This commit is contained in:
a.bouhuolia
2020-12-17 17:19:16 +02:00
parent 7a847fc895
commit a67b1fbdd0
54 changed files with 1452 additions and 983 deletions

View File

@@ -10,13 +10,13 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from 'data/DataTypes';
@Service()
export default class AccountsController extends BaseController{
export default class AccountsController extends BaseController {
@Inject()
accountsService: AccountsService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Router constructor method.
*/
@@ -24,84 +24,72 @@ export default class AccountsController extends BaseController{
const router = Router();
router.post(
'/bulk/:type(activate|inactivate)', [
...this.bulkSelectIdsQuerySchema
],
'/bulk/:type(activate|inactivate)',
[...this.bulkSelectIdsQuerySchema],
this.validationResult,
asyncMiddleware(this.bulkToggleActivateAccounts.bind(this))
);
router.post(
'/:id/activate', [
...this.accountParamSchema,
],
'/:id/activate',
[...this.accountParamSchema],
asyncMiddleware(this.activateAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.post(
'/:id/inactivate', [
...this.accountParamSchema,
],
'/:id/inactivate',
[...this.accountParamSchema],
asyncMiddleware(this.inactivateAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.post(
'/:id/close', [
...this.accountParamSchema,
...this.closingAccountSchema,
],
'/:id/close',
[...this.accountParamSchema, ...this.closingAccountSchema],
this.validationResult,
asyncMiddleware(this.closeAccount.bind(this)),
this.catchServiceErrors,
)
this.catchServiceErrors
);
router.post(
'/:id', [
...this.accountDTOSchema,
...this.accountParamSchema,
],
'/:id',
[...this.accountDTOSchema, ...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.editAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.post(
'/', [
...this.accountDTOSchema,
],
'/',
[...this.accountDTOSchema],
this.validationResult,
asyncMiddleware(this.newAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.get(
'/:id', [
...this.accountParamSchema,
],
'/:id',
[...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.getAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.get(
'/', [
...this.accountsListSchema,
],
'/',
[...this.accountsListSchema],
this.validationResult,
asyncMiddleware(this.getAccountsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors,
this.catchServiceErrors
);
router.delete(
'/', [
...this.bulkSelectIdsQuerySchema,
],
'/',
[...this.bulkSelectIdsQuerySchema],
this.validationResult,
asyncMiddleware(this.deleteBulkAccounts.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.delete(
'/:id', [
...this.accountParamSchema
],
'/:id',
[...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.deleteAccount.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
return router;
}
@@ -138,9 +126,7 @@ export default class AccountsController extends BaseController{
}
get accountParamSchema() {
return [
param('id').exists().isNumeric().toInt()
];
return [param('id').exists().isNumeric().toInt()];
}
get accountsListSchema() {
@@ -164,21 +150,24 @@ export default class AccountsController extends BaseController{
return [
check('to_account_id').exists().isNumeric().toInt(),
check('delete_after_closing').exists().isBoolean(),
]
];
}
/**
* Creates a new account.
* @param {Request} req -
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
* @param {NextFunction} next -
*/
async newAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const accountDTO: IAccountDTO = this.matchedBodyData(req);
try {
const account = await this.accountsService.newAccount(tenantId, accountDTO);
const account = await this.accountsService.newAccount(
tenantId,
accountDTO
);
return res.status(200).send({ id: account.id });
} catch (error) {
@@ -188,7 +177,7 @@ export default class AccountsController extends BaseController{
/**
* Edit account details.
* @param {Request} req
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
@@ -198,7 +187,11 @@ export default class AccountsController extends BaseController{
const accountDTO: IAccountDTO = this.matchedBodyData(req);
try {
const account = await this.accountsService.editAccount(tenantId, accountId, accountDTO);
const account = await this.accountsService.editAccount(
tenantId,
accountId,
accountDTO
);
return res.status(200).send({
id: account.id,
@@ -211,18 +204,20 @@ export default class AccountsController extends BaseController{
/**
* Get details of the given account.
* @param {Request} req
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async getAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: accountId } = req.params;
try {
const account = await this.accountsService.getAccount(tenantId, accountId);
return res.status(200).send({ account });
try {
const account = await this.accountsService.getAccount(
tenantId,
accountId
);
return res.status(200).send({ account });
} catch (error) {
next(error);
}
@@ -230,7 +225,7 @@ export default class AccountsController extends BaseController{
/**
* Delete the given account.
* @param {Request} req
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
@@ -252,11 +247,11 @@ export default class AccountsController extends BaseController{
/**
* Activate the given account.
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @return {Response}
*/
async activateAccount(req: Request, res: Response, next: Function){
async activateAccount(req: Request, res: Response, next: Function) {
const { id: accountId } = req.params;
const { tenantId } = req;
@@ -270,18 +265,17 @@ export default class AccountsController extends BaseController{
/**
* Inactive the given account.
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @return {Response}
*/
async inactivateAccount(req: Request, res: Response, next: Function){
async inactivateAccount(req: Request, res: Response, next: Function) {
const { id: accountId } = req.params;
const { tenantId } = req;
try {
await this.accountsService.activateAccount(tenantId, accountId, false);
return res.status(200).send({ id: accountId });
} catch (error) {
next(error);
}
@@ -289,18 +283,26 @@ export default class AccountsController extends BaseController{
/**
* Bulk activate/inactivate accounts.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async bulkToggleActivateAccounts(req: Request, res: Response, next: Function) {
async bulkToggleActivateAccounts(
req: Request,
res: Response,
next: Function
) {
const { type } = req.params;
const { tenantId } = req;
const { tenantId } = req;
const { ids: accountsIds } = req.query;
try {
const isActive = (type === 'activate' ? true : false);
await this.accountsService.activateAccounts(tenantId, accountsIds, isActive);
const isActive = type === 'activate' ? true : false;
await this.accountsService.activateAccounts(
tenantId,
accountsIds,
isActive
);
const activatedText = isActive ? 'activated' : 'inactivated';
@@ -315,9 +317,9 @@ export default class AccountsController extends BaseController{
/**
* Deletes accounts in bulk.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteBulkAccounts(req: Request, res: Response, next: NextFunction) {
const { ids: accountsIds } = req.query;
@@ -337,8 +339,8 @@ export default class AccountsController extends BaseController{
/**
* Retrieve accounts datatable list.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async getAccountsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -352,11 +354,14 @@ export default class AccountsController extends BaseController{
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const { accounts, filterMeta } = await this.accountsService.getAccountsList(tenantId, filter);
const {
accounts,
filterMeta,
} = await this.accountsService.getAccountsList(tenantId, filter);
return res.status(200).send({
accounts,
filter_meta: this.transfromToResponse(filterMeta)
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
@@ -365,9 +370,9 @@ export default class AccountsController extends BaseController{
/**
* Closes the given account.
* @param {Request} req
* @param {Response} res
* @param next
* @param {Request} req
* @param {Response} res
* @param next
*/
async closeAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -389,72 +394,72 @@ export default class AccountsController extends BaseController{
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'account_not_found') {
return res.boom.notFound(
'The given account not found.',
{ errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] }
);
return res.boom.notFound('The given account not found.', {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'account_name_not_unqiue') {
return res.boom.badRequest(
'The given account not unique.',
{ errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], }
);
return res.boom.badRequest('The given account not unique.', {
errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }],
});
}
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 }] }
);
return res.boom.badRequest('The given account type not found.', {
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }],
});
}
if (error.errorType === 'account_type_not_allowed_to_changed') {
return res.boom.badRequest(
'Not allowed to change account type of the account.',
{ errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] }
{
errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }],
}
);
}
if (error.errorType === 'parent_account_not_found') {
return res.boom.badRequest(
'The parent account not found.',
{ errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] },
);
return res.boom.badRequest('The parent account not found.', {
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }],
});
}
if (error.errorType === 'parent_has_different_type') {
return res.boom.badRequest(
'The parent account has different type.',
{ errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] }
);
return res.boom.badRequest('The parent account has different type.', {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 },
],
});
}
if (error.errorType === 'account_code_not_unique') {
return res.boom.badRequest(
'The given account code is not unique.',
{ errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] }
);
return res.boom.badRequest('The given account code is not unique.', {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }],
});
}
if (error.errorType === 'account_has_associated_transactions') {
return res.boom.badRequest(
'You could not delete account has associated transactions.',
{ errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] }
{
errors: [
{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 },
],
}
);
}
if (error.errorType === 'account_predefined') {
return res.boom.badRequest(
'You could not delete predefined account',
{ errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] }
);
return res.boom.badRequest('You could not delete predefined account', {
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }],
});
}
if (error.errorType === 'accounts_not_found') {
return res.boom.notFound(
'Some of the given accounts not found.',
{ errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] },
);
return res.boom.notFound('Some of the given accounts not found.', {
errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }],
});
}
if (error.errorType === 'predefined_accounts') {
return res.boom.badRequest(
@@ -465,10 +470,17 @@ export default class AccountsController extends BaseController{
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 }] },
{
errors: [
{
type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE',
code: 1200,
},
],
}
);
}
}
next(error)
next(error);
}
};
}

View File

@@ -1,9 +1,5 @@
import { Router, Request, Response, NextFunction } from 'express';
import {
check,
param,
query,
} from 'express-validator';
import { check, param, query } from 'express-validator';
import ItemCategoriesService from 'services/ItemCategories/ItemCategoriesService';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
@@ -27,49 +23,51 @@ export default class ItemsCategoriesController extends BaseController {
router() {
const router = Router();
router.post('/:id', [
router.post(
'/:id',
[
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
],
this.validationResult,
asyncMiddleware(this.editCategory.bind(this)),
this.handlerServiceError,
this.handlerServiceError
);
router.post('/', [
...this.categoryValidationSchema,
],
router.post(
'/',
[...this.categoryValidationSchema],
this.validationResult,
asyncMiddleware(this.newCategory.bind(this)),
this.handlerServiceError,
this.handlerServiceError
);
router.delete('/', [
...this.categoriesBulkValidationSchema,
],
router.delete(
'/',
[...this.categoriesBulkValidationSchema],
this.validationResult,
asyncMiddleware(this.bulkDeleteCategories.bind(this)),
this.handlerServiceError,
this.handlerServiceError
);
router.delete('/:id', [
...this.specificCategoryValidationSchema
],
router.delete(
'/:id',
[...this.specificCategoryValidationSchema],
this.validationResult,
asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceError,
this.handlerServiceError
);
router.get('/:id', [
...this.specificCategoryValidationSchema,
],
router.get(
'/:id',
[...this.specificCategoryValidationSchema],
this.validationResult,
asyncMiddleware(this.getCategory.bind(this)),
this.handlerServiceError,
this.handlerServiceError
);
router.get('/', [
...this.categoriesListValidationSchema
],
router.get(
'/',
[...this.categoriesListValidationSchema],
this.validationResult,
asyncMiddleware(this.getList.bind(this)),
this.handlerServiceError,
this.dynamicListService.handlerErrorsToResponse,
this.dynamicListService.handlerErrorsToResponse
);
return router;
}
@@ -102,7 +100,7 @@ export default class ItemsCategoriesController extends BaseController {
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt(),
]
];
}
/**
@@ -131,22 +129,24 @@ export default class ItemsCategoriesController extends BaseController {
* Validate specific item category schema.
*/
get specificCategoryValidationSchema() {
return [
param('id').exists().toInt(),
];
}
return [param('id').exists().toInt()];
}
/**
* Creates a new item category.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async newCategory(req: Request, res: Response, next: NextFunction) {
const { user, tenantId } = req;
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
try {
const itemCategory = await this.itemCategoriesService.newItemCategory(tenantId, itemCategoryOTD, user);
const itemCategory = await this.itemCategoriesService.newItemCategory(
tenantId,
itemCategoryOTD,
user
);
return res.status(200).send({ id: itemCategory.id });
} catch (error) {
next(error);
@@ -155,8 +155,8 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Edit details of the given category item.
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async editCategory(req: Request, res: Response, next: NextFunction) {
@@ -165,7 +165,12 @@ export default class ItemsCategoriesController extends BaseController {
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
try {
await this.itemCategoriesService.editItemCategory(tenantId, itemCategoryId, itemCategoryOTD, user);
await this.itemCategoriesService.editItemCategory(
tenantId,
itemCategoryId,
itemCategoryOTD,
user
);
return res.status(200).send({ id: itemCategoryId });
} catch (error) {
next(error);
@@ -174,8 +179,8 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Delete the give item category.
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async deleteItem(req: Request, res: Response, next: NextFunction) {
@@ -183,7 +188,11 @@ export default class ItemsCategoriesController extends BaseController {
const { tenantId, user } = req;
try {
await this.itemCategoriesService.deleteItemCategory(tenantId, itemCategoryId, user);
await this.itemCategoriesService.deleteItemCategory(
tenantId,
itemCategoryId,
user
);
return res.status(200).send({ id: itemCategoryId });
} catch (error) {
next(error);
@@ -192,8 +201,8 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Retrieve the list of items.
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async getList(req: Request, res: Response, next: NextFunction) {
@@ -206,8 +215,13 @@ export default class ItemsCategoriesController extends BaseController {
};
try {
const { itemCategories, filterMeta } = await this.itemCategoriesService.getItemCategoriesList(
tenantId, itemCategoriesFilter, user,
const {
itemCategories,
filterMeta,
} = await this.itemCategoriesService.getItemCategoriesList(
tenantId,
itemCategoriesFilter,
user
);
return res.status(200).send({
item_categories: itemCategories,
@@ -220,8 +234,8 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Retrieve details of the given category.
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async getCategory(req: Request, res: Response, next: NextFunction) {
@@ -229,7 +243,11 @@ export default class ItemsCategoriesController extends BaseController {
const { tenantId, user } = req;
try {
const itemCategory = await this.itemCategoriesService.getItemCategory(tenantId, itemCategoryId, user);
const itemCategory = await this.itemCategoriesService.getItemCategory(
tenantId,
itemCategoryId,
user
);
return res.status(200).send({ category: itemCategory });
} catch (error) {
next(error);
@@ -238,8 +256,8 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Bulk delete the given item categories.
* @param {Request} req -
* @param {Response} res -
* @param {Request} req -
* @param {Response} res -
* @return {Response}
*/
async bulkDeleteCategories(req: Request, res: Response, next: NextFunction) {
@@ -247,7 +265,11 @@ export default class ItemsCategoriesController extends BaseController {
const { tenantId, user } = req;
try {
await this.itemCategoriesService.deleteItemCategories(tenantId, itemCategoriesIds, user);
await this.itemCategoriesService.deleteItemCategories(
tenantId,
itemCategoriesIds,
user
);
return res.status(200).send({ ids: itemCategoriesIds });
} catch (error) {
next(error);
@@ -256,12 +278,17 @@ export default class ItemsCategoriesController extends BaseController {
/**
* Handles service error.
* @param {Error} error
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next
*/
handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
handlerServiceError(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'CATEGORY_NOT_FOUND') {
return res.boom.badRequest(null, {
@@ -295,7 +322,7 @@ export default class ItemsCategoriesController extends BaseController {
}
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}],
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
@@ -306,4 +333,4 @@ export default class ItemsCategoriesController extends BaseController {
}
next(error);
}
};
}

View File

@@ -109,7 +109,7 @@ export default class OrganizationController extends BaseController{
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'tenant_seeded') {
if (error.errorType === 'tenant_already_seeded') {
return res.status(400).send({
errors: [{ type: 'TENANT.DATABASE.ALREADY.SEEDED', code: 200 }],
});

View File

@@ -49,8 +49,8 @@ export default () => {
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
app.use('/licenses', Container.get(Licenses).router());
app.use('/subscription', Container.get(Subscription).router());
app.use('/organization', Container.get(Organization).router());
app.use('/ping', Container.get(Ping).router());
// - Settings routes.
// ---------------------------
@@ -81,7 +81,6 @@ export default () => {
dashboard.use(EnsureConfiguredMiddleware);
dashboard.use(EnsureTenantIsSeeded);
dashboard.use('/ping', Container.get(Ping).router());
dashboard.use('/users', Container.get(Users).router());
dashboard.use('/invite', Container.get(InviteUsers).authRouter());

View File

@@ -13,7 +13,7 @@ const attachCurrentUser = async (req: Request, res: Response, next: Function) =>
try {
Logger.info('[attach_user_middleware] finding system user by id.');
const user = await systemUserRepository.getById(req.token.id);
const user = await systemUserRepository.findOneById(req.token.id);
console.log(user);
if (!user) {

View File

@@ -20,7 +20,7 @@ export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantRepository } = Container.get('repositories');
Logger.info('[tenancy_middleware] trying get tenant by org. id from storage.');
const tenant = await tenantRepository.getByOrgId(organizationId);
const tenant = await tenantRepository.findOne({ organizationId });
// When the given organization id not found on the system storage.
if (!tenant) {

View File

@@ -160,5 +160,8 @@ export default {
],
blacklist: [],
}
}
},
protocol: '',
hostname: '',
};

View File

@@ -2,38 +2,43 @@ import { Container, Inject } from 'typedi';
import InviteUserService from 'services/InviteUsers';
export default class UserInviteMailJob {
@Inject()
inviteUsersService: InviteUserService;
/**
* Constructor method.
* @param {Agenda} agenda
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define(
'user-invite-mail',
{ priority: 'high' },
this.handler.bind(this),
this.handler.bind(this)
);
}
/**
* Handle invite user job.
* @param {Job} job
* @param {Function} done
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { email, organizationName, firstName } = job.attrs.data;
const { invite, authorizedUser, tenantId } = job.attrs.data;
const Logger = Container.get('logger');
const inviteUsersService = Container.get(InviteUserService);
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
try {
await this.inviteUsersService.mailMessages.sendInviteMail();
await inviteUsersService.mailMessages.sendInviteMail(
tenantId,
authorizedUser,
invite
);
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
done()
done();
} catch (error) {
Logger.info(`Send invite user mail - error: ${job.attrs.data}, error: ${error}`);
Logger.info(
`Send invite user mail - error: ${job.attrs.data}, error: ${error}`
);
done(error);
}
}

View File

@@ -2,6 +2,7 @@
// Here we import all events.
import 'subscribers/authentication';
import 'subscribers/organization';
import 'subscribers/inviteUser';
import 'subscribers/manualJournals';
import 'subscribers/expenses';
import 'subscribers/bills';

View File

@@ -21,10 +21,8 @@ export default async ({ expressApp }) => {
objectionLoader({ knex });
// It returns the agenda instance because it's needed in the subsequent loaders
const { agenda } = await dependencyInjectorLoader({
mongoConnection,
knex,
});
const { agenda } = await dependencyInjectorLoader({ mongoConnection, knex });
await jobsLoader({ agenda });
Logger.info('[init] Jobs loaded');

View File

@@ -6,9 +6,12 @@ import {
} from 'system/repositories';
export default () => {
const knex = Container.get('knex');
const cache = Container.get('cache');
return {
systemUserRepository: Container.get(SystemUserRepository),
subscriptionRepository: Container.get(SubscriptionRepository),
tenantRepository: Container.get(TenantRepository),
systemUserRepository: new SystemUserRepository(knex, cache),
subscriptionRepository: new SubscriptionRepository(knex, cache),
tenantRepository: new TenantRepository(knex, cache),
};
}

View File

@@ -1,8 +1,8 @@
import { Model, QueryBuilder } from 'objection';
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
class CustomerQueryBuilder extends QueryBuilder {
class CustomerQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
super(...args);

View File

@@ -1,5 +1,5 @@
import { Model, mixin } from 'objection';
import { snakeCase, each } from 'lodash';
import { snakeCase } from 'lodash';
import { mapKeysDeep } from 'utils';
import PaginationQueryBuilder from 'models/Pagination';
import DateSession from 'models/DateSession';

View File

@@ -1,8 +1,8 @@
import { Model, QueryBuilder } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
class VendorQueryBuilder extends QueryBuilder {
class VendorQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
super(...args);

View File

@@ -1,24 +1,25 @@
import { Account } from 'models';
import TenantRepository from 'repositories/TenantRepository';
import { IAccount } from 'interfaces';
export default class AccountRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Account;
get model() {
return Account.bindKnex(this.knex);
}
/**
* Retrieve accounts dependency graph.
* @returns {}
*/
async getDependencyGraph(withRelation) {
const accounts = await this.all(withRelation);
async getDependencyGraph(withRelation) {
const cacheKey = this.getCacheKey('accounts.depGraph', withRelation);
return this.cache.get(cacheKey, async () => {
const accounts = await this.all(withRelation);
return this.model.toDependencyGraph(accounts);
});
}
@@ -35,4 +36,50 @@ export default class AccountRepository extends TenantRepository {
await this.model.query().where('id', accountId)[method]('amount', amount);
this.flushCache();
}
/**
* Activate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
activateById(userId: number): Promise<IAccount> {
return super.update({ active: 1 }, { id: userId });
}
/**
* Inactivate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
inactivateById(userId: number): Promise<void> {
return super.update({ active: 0 }, { id: userId });
}
/**
* Activate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async activateByIds(userIds: number[]): Promise<IAccount> {
const results = await this.model.query()
.whereIn('id', userIds)
.patch({ active: true });
this.flushCache();
return results;
}
/**
* Inactivate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async inactivateByIds(userIds: number[]): Promise<IAccount> {
const results = await this.model.query()
.whereIn('id', userIds)
.patch({ active: false });
this.flushCache();
return results;
}
}

View File

@@ -15,11 +15,10 @@ interface IJournalTransactionsFilter {
export default class AccountTransactionsRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = AccountTransaction;
get model() {
return AccountTransaction.bindKnex(this.knex);
}
journal(filter: IJournalTransactionsFilter) {

View File

@@ -4,11 +4,10 @@ import { AccountType } from 'models';
export default class AccountTypeRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = AccountType;
get model() {
return AccountType.bindKnex(this.knex);
}
/**

View File

@@ -3,10 +3,9 @@ import TenantRepository from 'repositories/TenantRepository';
export default class BillRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Bill;
get model() {
return Bill.bindKnex(this.knex);
}
}

View File

@@ -13,6 +13,7 @@ export default class CachableRepository extends EntityRepository{
constructor(knex, cache) {
super(knex);
this.cache = cache;
this.repositoryName = this.constructor.name;
}
/**

View File

@@ -4,10 +4,9 @@ import { Contact } from 'models'
export default class ContactRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Contact;
get model() {
return Contact.bindKnex(this.knex);
}
}

View File

@@ -1,12 +1,20 @@
import TenantRepository from "./TenantRepository";
import { Customer } from 'models'
import { Customer } from 'models';
export default class CustomerRepository extends TenantRepository {
/**
* Constructor method.
* Contact repository.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Customer;
this.repositoryName = 'ContactRepository';
}
/**
* Gets the repository's model.
*/
get model() {
return Customer.bindKnex(this.knex);
}
changeBalance(vendorId: number, amount: number) {

View File

@@ -2,7 +2,6 @@ import { cloneDeep, cloneDeepWith, forOwn, isString } from 'lodash';
import ModelEntityNotFound from 'exceptions/ModelEntityNotFound';
export default class EntityRepository {
modelInstance: any;
idColumn: string;
knex: any;
@@ -15,20 +14,11 @@ export default class EntityRepository {
this.idColumn = 'id';
}
/**
* Sets the model to the repository and bind it to knex instance.
*/
set model(model) {
if (!this.modelInstance) {
this.modelInstance = model.bindKnex(this.knex);
}
}
/**
* Retrieve the repository model binded it to knex instance.
*/
get model() {
return this.modelInstance;
throw new Error("The repository's model is not defined.");
}
/**

View File

@@ -3,10 +3,9 @@ import { ExpenseCategory } from 'models';
export default class ExpenseEntyRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = ExpenseCategory;
get model() {
return ExpenseCategory.bindKnex(this.knex);
}
}

View File

@@ -4,19 +4,18 @@ import { Expense } from 'models';
export default class ExpenseRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Expense;
get model() {
return Expense.bindKnex(this.knex);
}
/**
* Publish the given expense.
* @param {number} expenseId
*/
async publish(expenseId: number): Promise<void> {
super.update({
publish(expenseId: number): Promise<void> {
return super.update({
id: expenseId,
publishedAt: moment().toMySqlDateTime(),
});

View File

@@ -4,10 +4,9 @@ import TenantRepository from "./TenantRepository";
export default class ItemRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Item;
get model() {
return Item.bindKnex(this.knex);
}
}

View File

@@ -3,10 +3,9 @@ import TenantRepository from 'repositories/TenantRepository';
export default class JournalRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = ManualJournal;
get model() {
return ManualJournal.bindKnex(this.knex);
}
}

View File

@@ -2,11 +2,10 @@ import { PaymentReceiveEntry } from 'models';
import TenantRepository from 'repositories/TenantRepository';
export default class PaymentReceiveEntryRepository extends TenantRepository {
/**
* Constructor method.
/**
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = PaymentReceiveEntry;
get model() {
return PaymentReceiveEntry.bindKnex(this.knex);
}
}

View File

@@ -3,10 +3,9 @@ import TenantRepository from 'repositories/TenantRepository';
export default class PaymentReceiveRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = PaymentReceive;
get model() {
return PaymentReceive.bindKnex(this.knex);
}
}

View File

@@ -3,10 +3,9 @@ import TenantRepository from 'repositories/TenantRepository';
export default class SaleInvoiceRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = SaleInvoice;
}
get model() {
return SaleInvoice.bindKnex(this.knex);
}
}

View File

@@ -3,10 +3,9 @@ import Setting from 'models/Setting';
export default class SettingRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Setting;
get model() {
return Setting.bindKnex(this.knex);
}
}

View File

@@ -11,6 +11,5 @@ export default class TenantRepository extends CachableRepository {
*/
constructor(knex, cache) {
super(knex, cache);
this.repositoryName = this.constructor.name;
}
}

View File

@@ -3,11 +3,18 @@ import TenantRepository from "./TenantRepository";
export default class VendorRepository extends TenantRepository {
/**
* Constructor method.
* Contact repository.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = Vendor;
this.repositoryName = 'ContactRepository';
}
/**
* Gets the repository's model.
*/
get model() {
return Vendor.bindKnex(this.knex);
}
changeBalance(vendorId: number, amount: number) {

View File

@@ -3,11 +3,10 @@ import TenantRepository from 'repositories/TenantRepository';
export default class ViewRepository extends TenantRepository {
/**
* Constructor method.
* Gets the repository's model.
*/
constructor(knex, cache) {
super(knex, cache);
this.model = View;
get model() {
return View.bindKnex(this.knex);
}
/**

View File

@@ -1,9 +1,14 @@
import { Inject, Service } from 'typedi';
import { difference, chain, uniq } from 'lodash';
import { kebabCase } from 'lodash'
import { kebabCase } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
import {
IAccountDTO,
IAccount,
IAccountsFilter,
IFilterMeta,
} from 'interfaces';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -27,15 +32,21 @@ export default class AccountsService {
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
* @param {number} accountTypeId -
* @return {IAccountType}
* @param {number} tenantId -
* @param {number} accountTypeId -
* @return {IAccountType}
*/
private async getAccountTypeOrThrowError(tenantId: number, accountTypeId: number) {
const { AccountType } = this.tenancy.models(tenantId);
private async getAccountTypeOrThrowError(
tenantId: number,
accountTypeId: number
) {
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating account type existance.', { tenantId, accountTypeId });
const accountType = await AccountType.query().findById(accountTypeId);
this.logger.info('[accounts] validating account type existance.', {
tenantId,
accountTypeId,
});
const accountType = await accountTypeRepository.findOneById(accountTypeId);
if (!accountType) {
this.logger.info('[accounts] account type not found.');
@@ -46,24 +57,34 @@ export default class AccountsService {
/**
* Retrieve parent account or throw service error.
* @param {number} tenantId
* @param {number} accountId
* @param {number} notAccountId
* @param {number} tenantId
* @param {number} accountId
* @param {number} notAccountId
*/
private async getParentAccountOrThrowError(tenantId: number, accountId: number, notAccountId?: number) {
private async getParentAccountOrThrowError(
tenantId: number,
accountId: number,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating parent account existance.', {
tenantId, accountId, notAccountId,
tenantId,
accountId,
notAccountId,
});
const parentAccount = await Account.query().findById(accountId)
const parentAccount = await Account.query()
.findById(accountId)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (!parentAccount) {
this.logger.info('[accounts] parent account not found.', { tenantId, accountId });
this.logger.info('[accounts] parent account not found.', {
tenantId,
accountId,
});
throw new ServiceError('parent_account_not_found');
}
return parentAccount;
@@ -71,17 +92,27 @@ export default class AccountsService {
/**
* Throws error if the account type was not unique on the storage.
* @param {number} tenantId
* @param {string} accountCode
* @param {number} notAccountId
* @param {number} tenantId
* @param {string} accountCode
* @param {number} notAccountId
*/
private async isAccountCodeUniqueOrThrowError(tenantId: number, accountCode: string, notAccountId?: number) {
private async isAccountCodeUniqueOrThrowError(
tenantId: number,
accountCode: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating the account code unique on the storage.', {
tenantId, accountCode, notAccountId,
});
const account = await Account.query().where('code', accountCode)
this.logger.info(
'[accounts] validating the account code unique on the storage.',
{
tenantId,
accountCode,
notAccountId,
}
);
const account = await Account.query()
.where('code', accountCode)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
@@ -89,17 +120,23 @@ export default class AccountsService {
});
if (account.length > 0) {
this.logger.info('[accounts] account code is not unique.', { tenantId, accountCode });
this.logger.info('[accounts] account code is not unique.', {
tenantId,
accountCode,
});
throw new ServiceError('account_code_not_unique');
}
}
/**
* Throws service error if parent account has different type.
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
*/
private throwErrorIfParentHasDiffType(accountDTO: IAccountDTO, parentAccount: IAccount) {
private throwErrorIfParentHasDiffType(
accountDTO: IAccountDTO,
parentAccount: IAccount
) {
if (accountDTO.accountTypeId !== parentAccount.accountTypeId) {
throw new ServiceError('parent_has_different_type');
}
@@ -107,18 +144,23 @@ export default class AccountsService {
/**
* Retrieve account of throw service error in case account not found.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
* @return {IAccount}
*/
private async getAccountOrThrowError(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
this.logger.info('[accounts] validating the account existance.', {
tenantId,
accountId,
});
const account = await accountRepository.findOneById(accountId);
if (!account) {
this.logger.info('[accounts] the given account not found.', { accountId });
this.logger.info('[accounts] the given account not found.', {
accountId,
});
throw new ServiceError('account_not_found');
}
return account;
@@ -127,13 +169,13 @@ export default class AccountsService {
/**
* Diff account type between new and old account, throw service error
* if they have different account type.
*
*
* @param {IAccount|IAccountDTO} oldAccount
* @param {IAccount|IAccountDTO} newAccount
*/
private async isAccountTypeChangedOrThrowError(
oldAccount: IAccount|IAccountDTO,
newAccount: IAccount|IAccountDTO,
oldAccount: IAccount | IAccountDTO,
newAccount: IAccount | IAccountDTO
) {
if (oldAccount.accountTypeId !== newAccount.accountTypeId) {
throw new ServiceError('account_type_not_allowed_to_changed');
@@ -142,19 +184,29 @@ export default class AccountsService {
/**
* Validates the account name uniquiness.
* @param {number} tenantId
* @param {string} accountName
* @param {number} tenantId
* @param {string} accountName
* @param {number} notAccountId - Ignore the account id.
*/
private async validateAccountNameUniquiness(tenantId: number, accountName: string, notAccountId?: number) {
private async validateAccountNameUniquiness(
tenantId: number,
accountName: string,
notAccountId?: number
) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[accounts] validating account name uniquiness.', { tenantId, accountName, notAccountId });
const foundAccount = await Account.query().findOne('name', accountName).onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
this.logger.info('[accounts] validating account name uniquiness.', {
tenantId,
accountName,
notAccountId,
});
const foundAccount = await Account.query()
.findOne('name', accountName)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (foundAccount) {
throw new ServiceError('account_name_not_unqiue');
}
@@ -162,9 +214,9 @@ export default class AccountsService {
/**
* Creates a new account on the storage.
* @param {number} tenantId
* @param {IAccount} accountDTO
* @returns {IAccount}
* @param {number} tenantId
* @param {IAccount} accountDTO
* @returns {IAccount}
*/
public async newAccount(tenantId: number, accountDTO: IAccountDTO) {
const { accountRepository } = this.tenancy.repositories(tenantId);
@@ -180,7 +232,8 @@ export default class AccountsService {
if (accountDTO.parentAccountId) {
const parentAccount = await this.getParentAccountOrThrowError(
tenantId, accountDTO.parentAccountId
tenantId,
accountDTO.parentAccountId
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
@@ -191,7 +244,10 @@ export default class AccountsService {
...accountDTO,
slug: kebabCase(accountDTO.name),
});
this.logger.info('[account] account created successfully.', { account, accountDTO });
this.logger.info('[account] account created successfully.', {
account,
accountDTO,
});
// Triggers `onAccountCreated` event.
this.eventDispatcher.dispatch(events.accounts.onCreated);
@@ -201,21 +257,31 @@ export default class AccountsService {
/**
* Edits details of the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountDTO} accountDTO
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountDTO} accountDTO
*/
public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) {
this.logger.info('[account] trying to edit account.', { tenantId, accountId });
public async editAccount(
tenantId: number,
accountId: number,
accountDTO: IAccountDTO
) {
this.logger.info('[account] trying to edit account.', {
tenantId,
accountId,
});
const { accountRepository } = this.tenancy.repositories(tenantId);
const oldAccount = await this.getAccountOrThrowError(tenantId, accountId);
// Validate account name uniquiness.
await this.validateAccountNameUniquiness(tenantId, accountDTO.name, accountId);
await this.validateAccountNameUniquiness(
tenantId,
accountDTO.name,
accountId
);
await this.isAccountTypeChangedOrThrowError(oldAccount, accountDTO);
// Validate the account code not exists on the storage.
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
await this.isAccountCodeUniqueOrThrowError(
@@ -226,17 +292,21 @@ export default class AccountsService {
}
if (accountDTO.parentAccountId) {
const parentAccount = await this.getParentAccountOrThrowError(
tenantId, accountDTO.parentAccountId, oldAccount.id,
tenantId,
accountDTO.parentAccountId,
oldAccount.id
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
// Update the account on the storage.
const account = await accountRepository.updateAndFetch({
id: oldAccount.id,
...accountDTO
});
const account = await accountRepository.update(
{ ...accountDTO, },
{ id: oldAccount.id }
);
this.logger.info('[account] account edited successfully.', {
account, accountDTO, tenantId
account,
accountDTO,
tenantId,
});
// Triggers `onAccountEdited` event.
this.eventDispatcher.dispatch(events.accounts.onEdited);
@@ -246,8 +316,8 @@ export default class AccountsService {
/**
* Retrieve the given account details.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async getAccount(tenantId: number, accountId: number) {
return this.getAccountOrThrowError(tenantId, accountId);
@@ -255,22 +325,24 @@ export default class AccountsService {
/**
* Detarmine if the given account id exists on the storage.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async isAccountExists(tenantId: number, accountId: number) {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[account] validating the account existance.', { tenantId, accountId });
const foundAccounts = await Account.query()
.where('id', accountId);
this.logger.info('[account] validating the account existance.', {
tenantId,
accountId,
});
const foundAccounts = await Account.query().where('id', accountId);
return foundAccounts.length > 0;
}
/**
* Throws error if the account was prefined.
* @param {IAccount} account
* @param {IAccount} account
*/
private throwErrorIfAccountPredefined(account: IAccount) {
if (account.predefined) {
@@ -281,14 +353,16 @@ export default class AccountsService {
/**
* Unlink the given parent account with children accounts.
* @param {number} tenantId -
* @param {number|number[]} parentAccountId -
* @param {number|number[]} parentAccountId -
*/
private async unassociateChildrenAccountsFromParent(
tenantId: number,
parentAccountId: number | number[],
parentAccountId: number | number[]
) {
const { Account } = this.tenancy.models(tenantId);
const accountsIds = Array.isArray(parentAccountId) ? parentAccountId : [parentAccountId];
const accountsIds = Array.isArray(parentAccountId)
? parentAccountId
: [parentAccountId];
await Account.query()
.whereIn('parent_account_id', accountsIds)
@@ -297,13 +371,17 @@ export default class AccountsService {
/**
* Throws service error if the account has associated transactions.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
private async throwErrorIfAccountHasTransactions(tenantId: number, accountId: number) {
private async throwErrorIfAccountHasTransactions(
tenantId: number,
accountId: number
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const accountTransactions = await AccountTransaction.query().where(
'account_id', accountId,
'account_id',
accountId
);
if (accountTransactions.length > 0) {
throw new ServiceError('account_has_associated_transactions');
@@ -312,8 +390,8 @@ export default class AccountsService {
/**
* Deletes the account from the storage.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
*/
public async deleteAccount(tenantId: number, accountId: number) {
const { accountRepository } = this.tenancy.repositories(tenantId);
@@ -330,7 +408,8 @@ export default class AccountsService {
await accountRepository.deleteById(account.id);
this.logger.info('[account] account has been deleted successfully.', {
tenantId, accountId,
tenantId,
accountId,
});
// Triggers `onAccountDeleted` event.
@@ -339,8 +418,8 @@ export default class AccountsService {
/**
* Retrieve the given accounts details or throw error if one account not exists.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
* @return {IAccount[]}
*/
public async getAccountsOrThrowError(
@@ -349,13 +428,19 @@ export default class AccountsService {
): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
this.logger.info('[account] trying to validate accounts not exist.', { tenantId, accountsIds });
this.logger.info('[account] trying to validate accounts not exist.', {
tenantId,
accountsIds,
});
const storedAccounts = await Account.query().whereIn('id', accountsIds);
const storedAccountsIds = storedAccounts.map((account) => account.id);
const notFoundAccounts = difference(accountsIds, storedAccountsIds);
if (notFoundAccounts.length > 0) {
this.logger.error('[account] accounts not exists on the storage.', { tenantId, notFoundAccounts });
this.logger.error('[account] accounts not exists on the storage.', {
tenantId,
notFoundAccounts,
});
throw new ServiceError('accounts_not_found');
}
return storedAccounts;
@@ -367,7 +452,9 @@ export default class AccountsService {
* @return {IAccount[]} - Predefined accounts
*/
private validatePrefinedAccounts(accounts: IAccount[]) {
const predefined = accounts.filter((account: IAccount) => account.predefined);
const predefined = accounts.filter(
(account: IAccount) => account.predefined
);
if (predefined.length > 0) {
this.logger.error('[accounts] some accounts predefined.', { predefined });
@@ -378,10 +465,13 @@ export default class AccountsService {
/**
* Validating the accounts have associated transactions.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
*/
private async validateAccountsHaveTransactions(tenantId: number, accountsIds: number[]) {
private async validateAccountsHaveTransactions(
tenantId: number,
accountsIds: number[]
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
const accountsTransactions = await AccountTransaction.query()
.whereIn('account_id', accountsIds)
@@ -403,11 +493,11 @@ export default class AccountsService {
/**
* Deletes the given accounts in bulk.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {number} tenantId
* @param {number[]} accountsIds
*/
public async deleteAccounts(tenantId: number, accountsIds: number[]) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.models(tenantId);
const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds);
// Validate the accounts are not predefined.
@@ -420,10 +510,11 @@ export default class AccountsService {
await this.unassociateChildrenAccountsFromParent(tenantId, accountsIds);
// Delete the accounts in one query.
await Account.query().whereIn('id', accountsIds).delete();
await accountRepository.deleteWhereIdIn(accountsIds);
this.logger.info('[account] given accounts deleted in bulk successfully.', {
tenantId, accountsIds
tenantId,
accountsIds,
});
// Triggers `onBulkDeleted` event.
this.eventDispatcher.dispatch(events.accounts.onBulkDeleted);
@@ -431,12 +522,15 @@ export default class AccountsService {
/**
* Activate accounts in bulk.
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {boolean} activate
* @param {number} tenantId
* @param {number[]} accountsIds
* @param {boolean} activate
*/
public async activateAccounts(tenantId: number, accountsIds: number[], activate: boolean = true) {
const { Account } = this.tenancy.models(tenantId);
public async activateAccounts(
tenantId: number,
accountsIds: number[],
activate: boolean = true
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given account or throw not found.
@@ -445,32 +539,41 @@ export default class AccountsService {
// Get all children accounts.
const accountsGraph = await accountRepository.getDependencyGraph();
const dependenciesAccounts = chain(accountsIds)
.map(accountId => accountsGraph.dependenciesOf(accountId))
.map((accountId) => accountsGraph.dependenciesOf(accountId))
.flatten()
.value();
// The children and parent accounts.
const patchAccountsIds = uniq([...dependenciesAccounts, accountsIds]);
this.logger.info('[account] trying activate/inactive the given accounts ids.', { accountsIds });
await Account.query().whereIn('id', patchAccountsIds)
.patch({
active: activate ? 1 : 0,
});
this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds });
this.logger.info(
'[account] trying activate/inactive the given accounts ids.',
{ accountsIds }
);
// Activate or inactivate the given accounts ids in bulk.
(activate) ?
await accountRepository.activateByIds(patchAccountsIds) :
await accountRepository.inactivateByIds(patchAccountsIds);
this.logger.info('[account] accounts have been activated successfully.', {
tenantId,
accountsIds,
});
// Triggers `onAccountBulkActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
* Activates/Inactivates the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {boolean} activate
* @param {number} tenantId
* @param {number} accountId
* @param {boolean} activate
*/
public async activateAccount(tenantId: number, accountId: number, activate?: boolean) {
const { Account } = this.tenancy.models(tenantId);
public async activateAccount(
tenantId: number,
accountId: number,
activate?: boolean
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given account or throw not found error.
@@ -480,34 +583,44 @@ export default class AccountsService {
const accountsGraph = await accountRepository.getDependencyGraph();
const dependenciesAccounts = accountsGraph.dependenciesOf(accountId);
this.logger.info('[account] trying to activate/inactivate the given account id.');
await Account.query()
.whereIn('id', [...dependenciesAccounts, accountId])
.patch({
active: activate ? 1 : 0,
})
this.logger.info(
'[account] trying to activate/inactivate the given account id.'
);
const patchAccountsIds = [...dependenciesAccounts, accountId];
// Activate and inactivate the given accounts ids.
(activate) ?
await accountRepository.activateByIds(patchAccountsIds) :
await accountRepository.inactivateByIds(patchAccountsIds);
this.logger.info('[account] account have been activated successfully.', {
tenantId,
accountId
accountId,
});
// Triggers `onAccountActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
/**
* Retrieve accounts datatable list.
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
* @param {number} tenantId
* @param {IAccountsFilter} accountsFilter
*/
public async getAccountsList(
tenantId: number,
filter: IAccountsFilter,
): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> {
filter: IAccountsFilter
): Promise<{ accounts: IAccount[]; filterMeta: IFilterMeta }> {
const { Account } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Account,
filter
);
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
this.logger.info('[accounts] trying to get accounts datatable list.', {
tenantId,
filter,
});
const accounts = await Account.query().onBuild((builder) => {
builder.withGraphFetched('type');
dynamicList.buildQuery()(builder);
@@ -527,7 +640,7 @@ export default class AccountsService {
* - Transfer the given account transactions to another account with the same root type.
* - Delete the given account.
* -------
* @param {number} tenantId -
* @param {number} tenantId -
* @param {number} accountId -
* @param {number} toAccountId -
* @param {boolean} deleteAfterClosing -
@@ -536,32 +649,47 @@ export default class AccountsService {
tenantId: number,
accountId: number,
toAccountId: number,
deleteAfterClosing: boolean,
deleteAfterClosing: boolean
) {
this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing });
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 {
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.findOneById(account.accountTypeId);
const toAccountType = await accountTypeRepository.findOneById(toAccount.accountTypeId);
const accountType = await accountTypeRepository.findOneById(
account.accountTypeId
);
const toAccountType = await accountTypeRepository.findOneById(
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);
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 ]);
await Promise.all([moveTransactionsOper, updateAccountBalanceOper]);
if (deleteAfterClosing) {
await accountRepository.deleteById(accountId);

View File

@@ -4,17 +4,17 @@ import TenancyService from 'services/Tenancy/TenancyService';
import { IAccountsTypesService, IAccountType } from 'interfaces';
@Service()
export default class AccountsTypesService implements IAccountsTypesService{
export default class AccountsTypesService implements IAccountsTypesService {
@Inject()
tenancy: TenancyService;
/**
* Retrieve all accounts types.
* @param {number} tenantId -
* @param {number} tenantId -
* @return {Promise<IAccountType>}
*/
async getAccountsTypes(tenantId: number): Promise<IAccountType> {
const { accountTypeRepository } = this.tenancy.repositories(tenantId);
return accountTypeRepository.all();
}
}
}

View File

@@ -1,7 +1,7 @@
import { Service, Inject, Container } from 'typedi';
import JWT from 'jsonwebtoken';
import uniqid from 'uniqid';
import { omit } from 'lodash';
import { omit, cloneDeep } from 'lodash';
import moment from 'moment';
import {
EventDispatcher,
@@ -62,7 +62,6 @@ export default class AuthenticationService implements IAuthenticationService {
emailOrPhone,
password,
});
const { systemUserRepository } = this.sysRepositories;
const loginThrottler = Container.get('rateLimiter.login');
@@ -108,10 +107,13 @@ export default class AuthenticationService implements IAuthenticationService {
});
const tenant = await user.$relatedQuery('tenant');
// Remove password property from user object.
Reflect.deleteProperty(user, 'password');
// Keep the user object immutable
const outputUser = cloneDeep(user);
return { user, token, tenant };
// Remove password property from user object.
Reflect.deleteProperty(outputUser, 'password');
return { user: outputUser, token, tenant };
}
/**
@@ -121,10 +123,10 @@ export default class AuthenticationService implements IAuthenticationService {
*/
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.getByEmail(
const isEmailExists = await systemUserRepository.findOneByEmail(
registerDTO.email
);
const isPhoneExists = await systemUserRepository.getByPhoneNumber(
const isPhoneExists = await systemUserRepository.findOneByPhoneNumber(
registerDTO.phoneNumber
);
@@ -190,7 +192,7 @@ export default class AuthenticationService implements IAuthenticationService {
*/
private async validateEmailExistance(email: string): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const userByEmail = await systemUserRepository.getByEmail(email);
const userByEmail = await systemUserRepository.findOneByEmail(email);
if (!userByEmail) {
this.logger.info('[send_reset_password] The given email not found.');
@@ -255,7 +257,7 @@ export default class AuthenticationService implements IAuthenticationService {
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError('token_expired');
}
const user = await systemUserRepository.getByEmail(tokenModel.email);
const user = await systemUserRepository.findOneByEmail(tokenModel.email);
if (!user) {
throw new ServiceError('user_not_found');

View File

@@ -1,13 +1,9 @@
import { Inject, Service } from 'typedi';
import { difference, upperFirst, omit } from 'lodash';
import moment from 'moment';
import { ServiceError } from "exceptions";
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
} from "interfaces";
import { IContact, IContactNewDTO, IContactEditDTO } from 'interfaces';
import JournalPoster from '../Accounting/JournalPoster';
type TContactService = 'customer' | 'vendor';
@@ -26,8 +22,8 @@ export default class ContactsService {
/**
* Get the given contact or throw not found contact.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
@@ -38,10 +34,13 @@ export default class ContactsService {
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId });
this.logger.info('[contact] trying to validate contact existance.', {
tenantId,
contactId,
});
const contact = await contactRepository.findOne({
id: contactId,
...(contactService) && ({ contactService }),
...(contactService && { contactService }),
});
if (!contact) {
@@ -52,13 +51,15 @@ export default class ContactsService {
/**
* Converts contact DTO object to model object attributes to insert or update.
* @param {IContactNewDTO | IContactEditDTO} contactDTO
* @param {IContactNewDTO | IContactEditDTO} contactDTO
*/
private transformContactObj(contactDTO: IContactNewDTO | IContactEditDTO) {
return {
...omit(contactDTO, [
'billingAddress1', 'billingAddress2',
'shippingAddress1', 'shippingAddress2',
'billingAddress1',
'billingAddress2',
'shippingAddress1',
'shippingAddress2',
]),
billing_address_1: contactDTO?.billingAddress1,
billing_address_2: contactDTO?.billingAddress2,
@@ -69,54 +70,87 @@ export default class ContactsService {
/**
* Creates a new contact on the storage.
* @param {number} tenantId
* @param {number} tenantId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
* @param {IContactDTO} contactDTO
*/
async newContact(
tenantId: number,
contactDTO: IContactNewDTO,
contactService: TContactService,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
const contact = await contactRepository.create({ contactService, ...contactObj });
this.logger.info('[contacts] trying to insert contact to the storage.', {
tenantId,
contactDTO,
});
const contact = await contactRepository.create({
contactService,
...contactObj,
});
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
this.logger.info('[contacts] contact inserted successfully.', {
tenantId,
contact,
});
return contact;
}
/**
* Edit details of the given on the storage.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @param {IContactDTO} contactDTO
* @param {IContactDTO} contactDTO
*/
async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) {
async editContact(
tenantId: number,
contactId: number,
contactDTO: IContactEditDTO,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO });
this.logger.info('[contacts] trying to edit the given contact details.', {
tenantId,
contactId,
contactDTO,
});
await contactRepository.update({ ...contactObj }, { id: contactId });
}
/**
* Deletes the given contact from the storage.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteContact(tenantId: number, contactId: number, contactService: TContactService) {
async deleteContact(
tenantId: number,
contactId: number,
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
this.logger.info('[contacts] trying to delete the given contact.', {
tenantId,
contactId,
});
// Deletes contact of the given id.
await contactRepository.deleteById(contactId);
@@ -124,26 +158,36 @@ export default class ContactsService {
/**
* Get contact details of the given contact id.
* @param {number} tenantId
* @param {number} contactId
* @param {number} tenantId
* @param {number} contactId
* @param {TContactService} contactService
* @returns {Promise<IContact>}
*/
async getContact(tenantId: number, contactId: number, contactService: TContactService) {
async getContact(
tenantId: number,
contactId: number,
contactService: TContactService
) {
return this.getContactByIdOrThrowError(tenantId, contactId, contactService);
}
/**
* Retrieve contacts or throw not found error if one of ids were not found
* on the storage.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<IContact>}
*/
async getContactsOrThrowErrorNotFound(tenantId: number, contactsIds: number[], contactService: TContactService) {
async getContactsOrThrowErrorNotFound(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query().whereIn('id', contactsIds).where('contact_service', contactService);
const contacts = await Contact.query()
.whereIn('id', contactsIds)
.where('contact_service', contactService);
const storedContactsIds = contacts.map((contact: IContact) => contact.id);
const notFoundCustomers = difference(contactsIds, storedContactsIds);
@@ -156,12 +200,16 @@ export default class ContactsService {
/**
* Deletes the given contacts in bulk.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @return {Promise<void>}
*/
async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) {
async deleteBulkContacts(
tenantId: number,
contactsIds: number[],
contactService: TContactService
) {
const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
@@ -170,9 +218,9 @@ export default class ContactsService {
/**
* Reverts journal entries of the given contacts.
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
* @param {number} tenantId
* @param {number[]} contactsIds
* @param {TContactService} contactService
*/
async revertJEntriesContactsOpeningBalance(
tenantId: number,
@@ -189,17 +237,14 @@ export default class ContactsService {
journal.loadEntries(contactsTransactions);
journal.removeEntries();
await Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
]);
await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
}
/**
* Chanages the opening balance of the given contact.
* @param {number} tenantId
* @param {number} tenantId
* @param {number} contactId
* @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance
* @param {ICustomerChangeOpeningBalanceDTO} changeOpeningBalance
* @return {Promise<void>}
*/
public async changeOpeningBalance(
@@ -207,27 +252,34 @@ export default class ContactsService {
contactId: number,
contactService: string,
openingBalance: number,
openingBalanceAt?: Date|string,
openingBalanceAt?: Date | string
): Promise<void> {
const { contactRepository } = this.tenancy.repositories(tenantId);
// Retrieve the given contact details or throw not found service error.
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
// Should the opening balance date be required.
if (!contact.openingBalanceAt && !openingBalanceAt) {
throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED);
};
}
// Changes the customer the opening balance and opening balance date.
await contactRepository.update({
openingBalance: openingBalance,
await contactRepository.update(
{
openingBalance: openingBalance,
...(openingBalanceAt) && ({
openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(),
}),
}, {
id: contactId,
contactService,
});
...(openingBalanceAt && {
openingBalanceAt: moment(openingBalanceAt).toMySqlDateTime(),
}),
},
{
id: contactId,
contactService,
}
);
}
}
}

View File

@@ -181,11 +181,10 @@ export default class CustomersService {
pagination: IPaginationMeta,
filterMeta: IFilterMeta,
}> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, customersFilter);
const { Customer } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Customer, customersFilter);
const { results, pagination } = await Contact.query().onBuild((query) => {
query.modify('customer');
const { results, pagination } = await Customer.query().onBuild((query) => {
dynamicList.buildQuery()(query);
}).pagination(
customersFilter.page - 1,

View File

@@ -4,17 +4,17 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import JournalPoster from "services/Accounting/JournalPoster";
import JournalCommands from "services/Accounting/JournalCommands";
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands';
import ContactsService from 'services/Contacts/ContactsService';
import {
import {
IVendorNewDTO,
IVendorEditDTO,
IVendor,
IVendorsFilter,
IPaginationMeta,
IFilterMeta
} from 'interfaces';
IFilterMeta,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -39,10 +39,10 @@ export default class VendorsService {
/**
* Converts vendor to contact DTO.
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
* @param {IVendorNewDTO|IVendorEditDTO} vendorDTO
* @returns {IContactDTO}
*/
private vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) {
private vendorToContactDTO(vendorDTO: IVendorNewDTO | IVendorEditDTO) {
return {
...vendorDTO,
active: defaultTo(vendorDTO.active, true),
@@ -51,31 +51,49 @@ export default class VendorsService {
/**
* Creates a new vendor.
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @param {number} tenantId
* @param {IVendorNewDTO} vendorDTO
* @return {Promise<void>}
*/
public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) {
this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO });
this.logger.info('[vendor] trying create a new vendor.', {
tenantId,
vendorDTO,
});
const contactDTO = this.vendorToContactDTO(vendorDTO);
const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor');
const vendor = await this.contactService.newContact(
tenantId,
contactDTO,
'vendor'
);
// Triggers `onVendorCreated` event.
await this.eventDispatcher.dispatch(events.vendors.onCreated, {
tenantId, vendorId: vendor.id, vendor,
tenantId,
vendorId: vendor.id,
vendor,
});
return vendor;
}
/**
* Edits details of the given vendor.
* @param {number} tenantId
* @param {IVendorEditDTO} vendorDTO
* @param {number} tenantId
* @param {IVendorEditDTO} vendorDTO
*/
public async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) {
public async editVendor(
tenantId: number,
vendorId: number,
vendorDTO: IVendorEditDTO
) {
const contactDTO = this.vendorToContactDTO(vendorDTO);
const vendor = await this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor');
const vendor = await this.contactService.editContact(
tenantId,
vendorId,
contactDTO,
'vendor'
);
await this.eventDispatcher.dispatch(events.vendors.onEdited);
@@ -84,17 +102,21 @@ export default class VendorsService {
/**
* Retrieve the given vendor details by id or throw not found.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
private getVendorByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(tenantId, customerId, 'vendor');
return this.contactService.getContactByIdOrThrowError(
tenantId,
customerId,
'vendor'
);
}
/**
* Deletes the given vendor from the storage.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
* @return {Promise<void>}
*/
public async deleteVendor(tenantId: number, vendorId: number) {
@@ -103,17 +125,23 @@ export default class VendorsService {
await this.getVendorByIdOrThrowError(tenantId, vendorId);
await this.vendorHasNoBillsOrThrowError(tenantId, vendorId);
this.logger.info('[vendor] trying to delete vendor.', { tenantId, vendorId });
this.logger.info('[vendor] trying to delete vendor.', {
tenantId,
vendorId,
});
await Contact.query().findById(vendorId).delete();
await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId });
await this.eventDispatcher.dispatch(events.vendors.onDeleted, {
tenantId,
vendorId,
});
this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId });
}
/**
* Retrieve the given vendor details.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
*/
public async getVendor(tenantId: number, vendorId: number) {
return this.contactService.getContact(tenantId, vendorId, 'vendor');
@@ -121,26 +149,26 @@ export default class VendorsService {
/**
* Writes vendor opening balance journal entries.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @return {Promise<void>}
*/
public async writeVendorOpeningBalanceJournal(
tenantId: number,
vendorId: number,
openingBalance: number,
openingBalance: number
) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
this.logger.info('[vendor] writing opening balance journal entries.', { tenantId, vendorId });
await journalCommands.vendorOpeningBalance(vendorId, openingBalance)
await Promise.all([
journal.saveBalance(),
journal.saveEntries(),
]);
this.logger.info('[vendor] writing opening balance journal entries.', {
tenantId,
vendorId,
});
await journalCommands.vendorOpeningBalance(vendorId, openingBalance);
await Promise.all([journal.saveBalance(), journal.saveEntries()]);
}
/**
@@ -149,28 +177,43 @@ export default class VendorsService {
* @param {number} vendorId -
* @return {Promise<void>}
*/
public async revertOpeningBalanceEntries(tenantId: number, vendorId: number|number[]) {
public async revertOpeningBalanceEntries(
tenantId: number,
vendorId: number | number[]
) {
const id = Array.isArray(vendorId) ? vendorId : [vendorId];
this.logger.info('[customer] trying to revert opening balance journal entries.', { tenantId, customerId });
this.logger.info(
'[customer] trying to revert opening balance journal entries.',
{ tenantId, customerId }
);
await this.contactService.revertJEntriesContactsOpeningBalance(
tenantId, id, 'vendor',
tenantId,
id,
'vendor'
);
}
/**
* Retrieve the given vendors or throw error if one of them not found.
* @param {numebr} tenantId
* @param {numebr} tenantId
* @param {number[]} vendorsIds
*/
private getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) {
return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor');
private getVendorsOrThrowErrorNotFound(
tenantId: number,
vendorsIds: number[]
) {
return this.contactService.getContactsOrThrowErrorNotFound(
tenantId,
vendorsIds,
'vendor'
);
}
/**
* Deletes the given vendors from the storage.
* @param {number} tenantId
* @param {number[]} vendorsIds
* @param {number} tenantId
* @param {number[]} vendorsIds
* @return {Promise<void>}
*/
public async deleteBulkVendors(
@@ -183,38 +226,53 @@ export default class VendorsService {
await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds);
await Contact.query().whereIn('id', vendorsIds).delete();
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds });
await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, {
tenantId,
vendorsIds,
});
this.logger.info('[vendor] bulk deleted successfully.', { tenantId, vendorsIds });
this.logger.info('[vendor] bulk deleted successfully.', {
tenantId,
vendorsIds,
});
}
/**
* Validates the vendor has no associated bills or throw service error.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} tenantId
* @param {number} vendorId
*/
private async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) {
private async vendorHasNoBillsOrThrowError(
tenantId: number,
vendorId: number
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieve the bill that associated to the given vendor id.
const bills = await billRepository.find({ vendor_id: vendorId });
if (bills.length > 0) {
throw new ServiceError('vendor_has_bills')
throw new ServiceError('vendor_has_bills');
}
}
/**
* Throws error in case one of vendors have associated bills.
* @param {number} tenantId
* @param {number[]} customersIds
* @param {number} tenantId
* @param {number[]} customersIds
* @throws {ServiceError}
*/
private async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) {
private async vendorsHaveNoBillsOrThrowError(
tenantId: number,
vendorsIds: number[]
) {
const { billRepository } = this.tenancy.repositories(tenantId);
// Retrieves bills that assocaited to the given vendors.
const vendorsBills = await billRepository.findWhereIn('vendor_id', vendorsIds);
const vendorsBills = await billRepository.findWhereIn(
'vendor_id',
vendorsIds
);
const billsVendorsIds = vendorsBills.map((bill) => bill.vendorId);
// The difference between the vendors ids and bills vendors ids.
@@ -233,18 +291,24 @@ export default class VendorsService {
public async getVendorsList(
tenantId: number,
vendorsFilter: IVendorsFilter
): Promise<{ vendors: IVendor[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Contact, vendorsFilter);
const { results, pagination } = await Contact.query().onBuild((builder) => {
builder.modify('vendor');
dynamicFilter.buildQuery()(builder);
}).pagination(
vendorsFilter.page - 1,
vendorsFilter.pageSize,
): Promise<{
vendors: IVendor[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { Vendor } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Vendor,
vendorsFilter
);
const { results, pagination } = await Vendor.query()
.onBuild((builder) => {
dynamicFilter.buildQuery()(builder);
})
.pagination(vendorsFilter.page - 1, vendorsFilter.pageSize);
return {
vendors: results,
pagination,
@@ -254,28 +318,33 @@ export default class VendorsService {
/**
* Changes the opeing balance of the given vendor.
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {Date|string} openingBalanceAt
* @param {number} tenantId
* @param {number} vendorId
* @param {number} openingBalance
* @param {Date|string} openingBalanceAt
*/
public async changeOpeningBalance(
tenantId: number,
vendorId: number,
openingBalance: number,
openingBalanceAt: Date|string,
openingBalanceAt: Date | string
): Promise<void> {
await this.contactService.changeOpeningBalance(
tenantId,
vendorId,
'vendor',
openingBalance,
openingBalanceAt,
openingBalanceAt
);
// Triggers `onOpeingBalanceChanged` event.
await this.eventDispatcher.dispatch(events.vendors.onOpeningBalanceChanged, {
tenantId, vendorId, openingBalance, openingBalanceAt
});
await this.eventDispatcher.dispatch(
events.vendors.onOpeningBalanceChanged,
{
tenantId,
vendorId,
openingBalance,
openingBalanceAt,
}
);
}
}

View File

@@ -3,7 +3,7 @@ import {
ICurrencyEditDTO,
ICurrencyDTO,
ICurrenciesService,
ICurrency
ICurrency,
} from 'interfaces';
import {
EventDispatcher,
@@ -14,7 +14,7 @@ import TenancyService from 'services/Tenancy/TenancyService';
const ERRORS = {
CURRENCY_NOT_FOUND: 'currency_not_found',
CURRENCY_CODE_EXISTS: 'currency_code_exists'
CURRENCY_CODE_EXISTS: 'currency_code_exists',
};
@Service()
@@ -30,40 +30,62 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Retrieve currency by given currency code or throw not found error.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} currencyId
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} currencyId
*/
private async validateCurrencyCodeUniquiness(tenantId: number, currencyCode: string, currencyId?: number) {
private async validateCurrencyCodeUniquiness(
tenantId: number,
currencyCode: string,
currencyId?: number
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode });
this.logger.info(
'[currencies] trying to validate currency code existance.',
{ tenantId, currencyCode }
);
const foundCurrency = await Currency.query().onBuild((query) => {
query.findOne('currency_code', currencyCode);
if (currencyId) {
query.whereNot('id', currencyId)
query.whereNot('id', currencyId);
}
});
if (foundCurrency) {
this.logger.info('[currencies] the currency code already exists.', { tenantId, currencyCode });
this.logger.info('[currencies] the currency code already exists.', {
tenantId,
currencyCode,
});
throw new ServiceError(ERRORS.CURRENCY_CODE_EXISTS);
}
}
/**
* Retrieve currency by the given currency code or throw service error.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} tenantId
* @param {string} currencyCode
*/
private async getCurrencyByCodeOrThrowError(tenantId: number, currencyCode: string) {
private async getCurrencyByCodeOrThrowError(
tenantId: number,
currencyCode: string
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode });
const foundCurrency = await Currency.query().findOne('currency_code', currencyCode);
this.logger.info(
'[currencies] trying to validate currency code existance.',
{ tenantId, currencyCode }
);
const foundCurrency = await Currency.query().findOne(
'currency_code',
currencyCode
);
if (!foundCurrency) {
this.logger.info('[currencies] the given currency code not exists.', { tenantId, currencyCode });
this.logger.info('[currencies] the given currency code not exists.', {
tenantId,
currencyCode,
});
throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND);
}
return foundCurrency;
@@ -71,17 +93,26 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Retrieve currency by given id or throw not found error.
* @param {number} tenantId
* @param {number} currencyId
* @param {number} tenantId
* @param {number} currencyId
*/
private async getCurrencyIdOrThrowError(tenantId: number, currencyId: number) {
private async getCurrencyIdOrThrowError(
tenantId: number,
currencyId: number
) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to validate currency by id existance.', { tenantId, currencyId });
this.logger.info(
'[currencies] trying to validate currency by id existance.',
{ tenantId, currencyId }
);
const foundCurrency = await Currency.query().findOne('id', currencyId);
if (!foundCurrency) {
this.logger.info('[currencies] the currency code not found.', { tenantId, currencyId });
this.logger.info('[currencies] the currency code not found.', {
tenantId,
currencyId,
});
throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND);
}
return foundCurrency;
@@ -89,56 +120,87 @@ export default class CurrenciesService implements ICurrenciesService {
/**
* Creates a new currency.
* @param {number} tenantId
* @param {ICurrencyDTO} currencyDTO
* @param {number} tenantId
* @param {ICurrencyDTO} currencyDTO
*/
public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] try to insert new currency.', { tenantId, currencyDTO });
this.logger.info('[currencies] try to insert new currency.', {
tenantId,
currencyDTO,
});
await this.validateCurrencyCodeUniquiness(tenantId, currencyDTO.currencyCode);
await this.validateCurrencyCodeUniquiness(
tenantId,
currencyDTO.currencyCode
);
await Currency.query().insert({ ...currencyDTO });
this.logger.info('[currencies] the currency inserted successfully.', { tenantId, currencyDTO });
this.logger.info('[currencies] the currency inserted successfully.', {
tenantId,
currencyDTO,
});
}
/**
* Edit details of the given currency.
* @param {number} tenantId
* @param {number} currencyId
* @param {ICurrencyDTO} currencyDTO
* @param {number} tenantId
* @param {number} currencyId
* @param {ICurrencyDTO} currencyDTO
*/
public async editCurrency(tenantId: number, currencyId: number, currencyDTO: ICurrencyEditDTO): Promise<ICurrency> {
public async editCurrency(
tenantId: number,
currencyId: number,
currencyDTO: ICurrencyEditDTO
): Promise<ICurrency> {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] try to edit currency.', { tenantId, currencyId, currencyDTO });
this.logger.info('[currencies] try to edit currency.', {
tenantId,
currencyId,
currencyDTO,
});
await this.getCurrencyIdOrThrowError(tenantId, currencyId);
const currency = await Currency.query().patchAndFetchById(currencyId, { ...currencyDTO });
this.logger.info('[currencies] the currency edited successfully.', { tenantId, currencyDTO });
const currency = await Currency.query().patchAndFetchById(currencyId, {
...currencyDTO,
});
this.logger.info('[currencies] the currency edited successfully.', {
tenantId,
currencyDTO,
});
return currency;
}
/**
* Delete the given currency code.
* @param {number} tenantId
* @param {string} currencyCode
* @param {number} tenantId
* @param {string} currencyCode
* @return {Promise<}
*/
public async deleteCurrency(tenantId: number, currencyCode: string): Promise<void> {
public async deleteCurrency(
tenantId: number,
currencyCode: string
): Promise<void> {
const { Currency } = this.tenancy.models(tenantId);
this.logger.info('[currencies] trying to delete the given currency.', { tenantId, currencyCode });
this.logger.info('[currencies] trying to delete the given currency.', {
tenantId,
currencyCode,
});
await this.getCurrencyByCodeOrThrowError(tenantId, currencyCode);
await Currency.query().where('currency_code', currencyCode).delete();
this.logger.info('[currencies] the currency deleted successfully.', { tenantId, currencyCode });
this.logger.info('[currencies] the currency deleted successfully.', {
tenantId,
currencyCode,
});
}
/**
* Listing currencies.
* @param {number} tenantId
* @param {number} tenantId
* @return {Promise<ICurrency[]>}
*/
public async listCurrencies(tenantId: number): Promise<ICurrency[]> {
@@ -149,4 +211,4 @@ export default class CurrenciesService implements ICurrenciesService {
});
return currencies;
}
}
}

View File

@@ -1,29 +1,41 @@
import { IInviteUserInput, ISystemUser } from "interfaces";
import Mail from "lib/Mail";
import { Service } from "typedi";
import { ISystemUser } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import Mail from 'lib/Mail';
import { Service, Container } from 'typedi';
import config from 'config';
@Service()
export default class InviteUsersMailMessages {
/**
* Sends invite mail to the given email.
* @param user
* @param invite
* @param user
* @param invite
*/
async sendInviteMail(user: ISystemUser, invite) {
async sendInviteMail(tenantId: number, fromUser: ISystemUser, invite: any) {
const { protocol, hostname } = config;
const tenancyService = Container.get(TenancyService);
// Retrieve tenant's settings
const settings = tenancyService.settings(tenantId);
// Retreive tenant orgnaization name.
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const mail = new Mail()
.setSubject(`${user.fullName} has invited you to join a Bigcapital`)
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)
.setView('mail/UserInvite.html')
.setTo(invite.email)
.setData({
acceptUrl: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
fullName: `${user.firstName} ${user.lastName}`,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
organizationName: organizationOptions.getMeta('organization_name'),
acceptUrl: `${protocol}://${hostname}/invite/accept/${invite.token}`,
fullName: `${fromUser.firstName} ${fromUser.lastName}`,
firstName: fromUser.firstName,
lastName: fromUser.lastName,
email: fromUser.email,
organizationName,
});
await mail.send();
Logger.log('info', 'User has been sent invite user email successfuly.');
}
}
}

View File

@@ -42,23 +42,27 @@ export default class InviteUserService {
token: string,
inviteUserInput: IInviteUserInput
): Promise<void> {
const { systemUserRepository } = this.sysRepositories;
// Retrieve the invite token or throw not found error.
const inviteToken = await this.getInviteOrThrowError(token);
// Validates the user phone number.
await this.validateUserPhoneNumber(inviteUserInput);
this.logger.info('[aceept_invite] trying to hash the user password.');
const hashedPassword = await hashPassword(inviteUserInput.password);
this.logger.info('[accept_invite] trying to update user details.');
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.findOneByEmail(inviteToken.email);
const user = await systemUserRepository.getByEmail(inviteToken.email);
const updateUserOper = systemUserRepository.edit(user.id, {
// Sets the invited user details after invite accepting.
const updateUserOper = systemUserRepository.update({
...inviteUserInput,
active: 1,
invite_accepted_at: moment().format('YYYY-MM-DD'),
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
password: hashedPassword,
});
}, { id: user.id });
this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query()
@@ -70,7 +74,6 @@ export default class InviteUserService {
updateUserOper,
deleteInviteTokenOper,
]);
// Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken,
@@ -94,6 +97,9 @@ export default class InviteUserService {
invite: IInvite,
user: ISystemUser
}> {
const { systemUserRepository } = this.sysRepositories;
// Throw error in case user email exists.
await this.throwErrorIfUserEmailExists(email);
this.logger.info('[send_invite] trying to store invite token.');
@@ -106,16 +112,14 @@ export default class InviteUserService {
this.logger.info(
'[send_invite] trying to store user with email and tenant.'
);
const { systemUserRepository } = this.sysRepositories;
const user = await systemUserRepository.create({
email,
tenant_id: authorizedUser.tenantId,
active: 1,
});
// Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
invite,
invite, authorizedUser, tenantId
});
return { invite, user };
}
@@ -155,7 +159,7 @@ export default class InviteUserService {
email: string
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByEmail(email);
const foundUser = await systemUserRepository.findOneByEmail(email);
if (foundUser) {
throw new ServiceError('email_already_invited');
@@ -187,7 +191,7 @@ export default class InviteUserService {
inviteUserInput: IInviteUserInput
): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.getByPhoneNumber(
const foundUser = await systemUserRepository.findOneByPhoneNumber(
inviteUserInput.phoneNumber
);

View File

@@ -157,7 +157,7 @@ export default class MediaService implements IMediaService {
const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId];
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds);
const tenantPath = `${publicPath}${tenant.organizationId}`;
@@ -192,7 +192,7 @@ export default class MediaService implements IMediaService {
this.logger.info('[media] trying to upload media.', { tenantId });
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const fileName = `${attachment.md5}.png`;
// Validate the attachment.

View File

@@ -106,7 +106,7 @@ export default class OrganizationService {
});
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getById(user.tenantId);
const tenant = await tenantRepository.findOneById(user.tenantId);
return [tenant];
}
@@ -150,7 +150,8 @@ export default class OrganizationService {
*/
private async getTenantByOrgIdOrThrowError(organizationId: string) {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getByOrgId(organizationId);
const tenant = await tenantRepository.findOne({ organizationId });
this.throwIfTenantNotExists(tenant);
return tenant;

View File

@@ -28,14 +28,13 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
};
/**
@@ -72,13 +71,20 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
saleEstimatesService: SaleEstimateService;
/**
*
*
* Validate whether sale invoice number unqiue on the storage.
*/
async validateInvoiceNumberUnique(tenantId: number, invoiceNumber: string, notInvoiceId?: number) {
async validateInvoiceNumberUnique(
tenantId: number,
invoiceNumber: string,
notInvoiceId?: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
this.logger.info('[sale_invoice] validating sale invoice number existance.', { tenantId, invoiceNumber });
this.logger.info(
'[sale_invoice] validating sale invoice number existance.',
{ tenantId, invoiceNumber }
);
const saleInvoice = await SaleInvoice.query()
.findOne('invoice_no', invoiceNumber)
.onBuild((builder) => {
@@ -88,8 +94,11 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
});
if (saleInvoice) {
this.logger.info('[sale_invoice] sale invoice number not unique.', { tenantId, invoiceNumber });
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE)
this.logger.info('[sale_invoice] sale invoice number not unique.', {
tenantId,
invoiceNumber,
});
throw new ServiceError(ERRORS.INVOICE_NUMBER_NOT_UNIQUE);
}
}
@@ -116,73 +125,91 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
*/
transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO|ISaleInvoiceEditDTO,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): ISaleInvoice {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
return {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered']),
['invoiceDate', 'dueDate']
),
...formatDateFields(omit(saleInvoiceDTO, ['delivered', 'entries']), [
'invoiceDate',
'dueDate',
]),
// Avoid rewrite the deliver date in edit mode when already published.
...(saleInvoiceDTO.delivered && (!oldSaleInvoice?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
balance,
paymentAmount: 0,
}
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
})),
};
}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
* @async
* @param {number} tenantId =
* @param {ISaleInvoice} saleInvoiceDTO -
* @param {number} tenantId =
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
public async createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
const invLotNumber = 1;
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo);
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo
);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
const saleInvoice = await SaleInvoice.query()
.insertGraphAndFetch({
...omit(saleInvoiceObj, ['entries']),
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
}))
});
const saleInvoice = await saleInvoiceRepository.upsertGraph({
...saleInvoiceObj,
});
await this.eventDispatcher.dispatch(events.saleInvoice.onCreated, {
tenantId, saleInvoice, saleInvoiceId: saleInvoice.id,
tenantId,
saleInvoice,
saleInvoiceId: saleInvoice.id,
});
this.logger.info('[sale_invoice] successfully inserted.', {
tenantId,
saleInvoice,
});
this.logger.info('[sale_invoice] successfully inserted.', { tenantId, saleInvoice });
return saleInvoice;
}
@@ -190,50 +217,85 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Edit the given sale invoice.
* @async
* @param {number} tenantId -
* @param {number} tenantId -
* @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice -
*/
public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any): Promise<ISaleInvoice> {
public async editSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceDTO: any
): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO, oldSaleInvoice);
const saleInvoiceObj = this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceDTO.invoiceNo) {
await this.validateInvoiceNumberUnique(tenantId, saleInvoiceDTO.invoiceNo, saleInvoiceId);
await this.validateInvoiceNumberUnique(
tenantId,
saleInvoiceDTO.invoiceNo,
saleInvoiceId
);
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
saleInvoiceDTO.entries
);
// Validate non-sellable entries items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
saleInvoiceDTO.entries
);
// Validate the items entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(tenantId, saleInvoiceId, 'SaleInvoice', saleInvoiceDTO.entries);
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
saleInvoiceId,
'SaleInvoice',
saleInvoiceDTO.entries
);
this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await SaleInvoice.query()
.upsertGraphAndFetch({
const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch(
{
id: saleInvoiceId,
...omit(saleInvoiceObj, ['entries', 'invLotNumber']),
entries: saleInvoiceObj.entries.map((entry) => ({
reference_type: 'SaleInvoice',
...omit(entry, ['amount']),
}))
});
})),
}
);
// Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
saleInvoice,
oldSaleInvoice,
tenantId,
saleInvoiceId,
});
return saleInvoice;
}
@@ -246,21 +308,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
saleInvoiceId: number
): Promise<void> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve details of the given sale invoice id.
const saleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const saleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Throws error in case the sale invoice already published.
if (saleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await saleInvoiceRepository.update({
deliveredAt: moment().toMySqlDateTime()
}, { id: saleInvoiceId });
await saleInvoiceRepository.update(
{
deliveredAt: moment().toMySqlDateTime(),
},
{ id: saleInvoiceId }
);
}
/**
@@ -269,15 +337,21 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId - The given sale invoice id.
*/
public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number): Promise<void> {
public async deleteSaleInvoice(
tenantId: number,
saleInvoiceId: number
): Promise<void> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const oldSaleInvoice = await this.getInvoiceOrThrowError(
tenantId,
saleInvoiceId
);
// Unlink the converted sale estimates from the given sale invoice.
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
saleInvoiceId
);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
@@ -288,7 +362,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
.delete();
await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, {
tenantId, oldSaleInvoice,
tenantId,
oldSaleInvoice,
});
}
@@ -303,44 +378,48 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
saleInvoice,
saleInvoiceId: number,
override?: boolean
){
) {
this.logger.info('[sale_invoice] saving inventory transactions');
const inventortyTransactions = saleInvoice.entries
.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate',]),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
entryId: entry.id,
}));
const inventortyTransactions = saleInvoice.entries.map((entry) => ({
...pick(entry, ['item_id', 'quantity', 'rate']),
lotNumber: saleInvoice.invLotNumber,
transactionType: 'SaleInvoice',
transactionId: saleInvoiceId,
direction: 'OUT',
date: saleInvoice.invoice_date,
entryId: entry.id,
}));
return this.inventoryService.recordInventoryTransactions(
tenantId, inventortyTransactions, override,
tenantId,
inventortyTransactions,
override
);
}
/**
* Deletes the inventory transactions.
* @param {string} transactionType
* @param {number} transactionId
* @param {string} transactionType
* @param {number} transactionId
*/
private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
private async revertInventoryTransactions(
tenantId: number,
inventoryTransactions: array
) {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const opers: Promise<[]>[] = [];
this.logger.info('[sale_invoice] reverting inventory transactions');
inventoryTransactions.forEach((trans: any) => {
switch(trans.direction) {
switch (trans.direction) {
case 'OUT':
if (trans.inventoryTransactionId) {
const revertRemaining = InventoryTransaction.query()
.where('id', trans.inventoryTransactionId)
.where('direction', 'OUT')
.increment('remaining', trans.quanitity);
opers.push(revertRemaining);
}
break;
@@ -361,16 +440,19 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Retrieve sale invoice with associated entries.
* @async
* @param {Number} saleInvoiceId
* @param {Number} saleInvoiceId
*/
public async getSaleInvoice(tenantId: number, saleInvoiceId: number): Promise<ISaleInvoice> {
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('entries')
.withGraphFetched('customer');
if (!saleInvoice) {
throw new ServiceError(ERRORS.SALE_INVOICE_NOT_FOUND);
}
@@ -378,9 +460,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
/**
* Schedules compute sale invoice items cost based on each item
* Schedules compute sale invoice items cost based on each item
* cost method.
* @param {ISaleInvoice} saleInvoice
* @param {ISaleInvoice} saleInvoice
* @return {Promise}
*/
async scheduleComputeInvoiceItemsCost(
@@ -396,15 +478,20 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const inventoryItemsIds = chain(saleInvoice.entries)
.filter((entry: IItemEntry) => entry.item.type === 'inventory')
.map((entry: IItemEntry) => entry.itemId)
.uniq().value();
.uniq()
.value();
if (inventoryItemsIds.length === 0) {
await this.writeNonInventoryInvoiceJournals(tenantId, saleInvoice, override);
await this.writeNonInventoryInvoiceJournals(
tenantId,
saleInvoice,
override
);
} else {
await this.scheduleComputeItemsCost(
tenantId,
inventoryItemsIds,
saleInvoice.invoice_date,
saleInvoice.invoice_date
);
}
}
@@ -436,40 +523,43 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
await Promise.all([
journal.deleteEntries(),
journal.saveEntries(),
journal.saveBalance(),
journal.saveBalance(),
]);
}
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async salesInvoicesList(
tenantId: number,
salesInvoicesFilter: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[],
pagination: IPaginationMeta,
filterMeta: IFilterMeta
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,
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,
@@ -479,12 +569,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
/**
* Retrieve due sales invoices.
* @param {number} tenantId
* @param {number} customerId
* @param {number} tenantId
* @param {number} customerId
*/
public async getPayableInvoices(
tenantId: number,
customerId?: number,
customerId?: number
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);

View File

@@ -41,7 +41,7 @@ export default class SubscriptionService {
const { tenantRepository } = this.sysRepositories;
const plan = await Plan.query().findOne('slug', planSlug);
const tenant = await tenantRepository.getById(tenantId);
const tenant = await tenantRepository.findOneById(tenantId);
const paymentViaLicense = new LicensePaymentMethod();
const paymentContext = new PaymentContext(paymentViaLicense);

View File

@@ -45,7 +45,7 @@ export default class TenantsManagerService implements ITenantManager{
*/
public async createTenant(): Promise<ITenant> {
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.newTenantWithUniqueOrgId();
const tenant = await tenantRepository.createWithUniqueOrgId();
return tenant;
}

View File

@@ -1,9 +1,9 @@
import { Inject, Service } from "typedi";
import { Inject, Service } from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService';
import { SystemUser } from "system/models";
import { ServiceError, ServiceErrors } from "exceptions";
import { ISystemUser, ISystemUserDTO } from "interfaces";
import systemRepositories from "loaders/systemRepositories";
import { SystemUser } from 'system/models';
import { ServiceError, ServiceErrors } from 'exceptions';
import { ISystemUser, ISystemUserDTO } from 'interfaces';
import systemRepositories from 'loaders/systemRepositories';
@Service()
export default class UsersService {
@@ -18,47 +18,64 @@ export default class UsersService {
/**
* Creates a new user.
* @param {number} tenantId
* @param {number} userId
* @param {IUserDTO} userDTO
* @param {number} tenantId
* @param {number} userId
* @param {IUserDTO} userDTO
* @return {Promise<ISystemUser>}
*/
async editUser(tenantId: number, userId: number, userDTO: ISystemUserDTO): Promise<ISystemUser> {
async editUser(
tenantId: number,
userId: number,
userDTO: ISystemUserDTO
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const isEmailExists = await systemUserRepository.isEmailExists(userDTO.email, userId);
const isPhoneNumberExists = await systemUserRepository.isPhoneNumberExists(userDTO.phoneNumber, userId);
const userByEmail = await systemUserRepository.findOne({
email: userDTO.email,
id: userId,
});
const userByPhoneNumber = await systemUserRepository.findOne({
phoneNumber: userDTO.phoneNumber,
id: userId
});
const serviceErrors: ServiceError[] = [];
if (isEmailExists) {
if (userByEmail) {
serviceErrors.push(new ServiceError('email_already_exists'));
}
if (isPhoneNumberExists) {
if (userByPhoneNumber) {
serviceErrors.push(new ServiceError('phone_number_already_exist'));
}
if (serviceErrors.length > 0) {
throw new ServiceErrors(serviceErrors);
}
const updateSystemUser = await SystemUser.query()
.where('id', userId)
.update({ ...userDTO });
const updateSystemUser = await systemUserRepository
.update({ ...userDTO, }, { id: userId });
return updateSystemUser;
}
/**
* Validate user existance throw error in case user was not found.,
* @param {number} tenantId -
* @param {number} tenantId -
* @param {number} userId -
* @returns {ISystemUser}
*/
async getUserOrThrowError(tenantId: number, userId: number): Promise<ISystemUser> {
async getUserOrThrowError(
tenantId: number,
userId: number
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.getByIdAndTenant(userId, tenantId);
const user = await systemUserRepository.findOneByIdAndTenant(
userId,
tenantId
);
if (!user) {
this.logger.info('[users] the given user not found.', { tenantId, userId });
this.logger.info('[users] the given user not found.', {
tenantId,
userId,
});
throw new ServiceError('user_not_found');
}
return user;
@@ -66,25 +83,35 @@ export default class UsersService {
/**
* Deletes the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @param {number} userId
*/
async deleteUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories;
await this.getUserOrThrowError(tenantId, userId);
this.logger.info('[users] trying to delete the given user.', { tenantId, userId });
this.logger.info('[users] trying to delete the given user.', {
tenantId,
userId,
});
await systemUserRepository.deleteById(userId);
this.logger.info('[users] the given user deleted successfully.', { tenantId, userId });
this.logger.info('[users] the given user deleted successfully.', {
tenantId,
userId,
});
}
/**
* Activate the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @param {number} userId
*/
async activateUser(tenantId: number, userId: number, authorizedUser: ISystemUser): Promise<void> {
async activateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
@@ -96,11 +123,15 @@ export default class UsersService {
/**
* Inactivate the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId
* @param {number} userId
* @return {Promise<void>}
*/
async inactivateUser(tenantId: number, userId: number, authorizedUser: ISystemUser): Promise<void> {
async inactivateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
@@ -112,8 +143,8 @@ export default class UsersService {
/**
* Retrieve users list based on the given filter.
* @param {number} tenantId
* @param {object} filter
* @param {number} tenantId
* @param {object} filter
*/
async getList(tenantId: number) {
const users = await SystemUser.query()
@@ -134,7 +165,7 @@ export default class UsersService {
/**
* Throws service error in case the user was already active.
* @param {ISystemUser} user
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserActive(user: ISystemUser) {
@@ -145,7 +176,7 @@ export default class UsersService {
/**
* Throws service error in case the user was already inactive.
* @param {ISystemUser} user
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserInactive(user: ISystemUser) {
@@ -155,13 +186,16 @@ export default class UsersService {
}
/**
* Throw service error in case the given user same the authorized user.
* @param {number} userId
* @param {ISystemUser} authorizedUser
* Throw service error in case the given user same the authorized user.
* @param {number} userId
* @param {ISystemUser} authorizedUser
*/
throwErrorIfUserIdSameAuthorizedUser(userId: number, authorizedUser: ISystemUser) {
throwErrorIfUserIdSameAuthorizedUser(
userId: number,
authorizedUser: ISystemUser
) {
if (userId === authorizedUser.id) {
throw new ServiceError('user_same_the_authorized_user');
}
}
}
}

View File

@@ -1,28 +1,22 @@
import { Container } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import InviteUserService from 'services/InviteUsers';
@EventSubscriber()
export class InviteUserSubscriber {
@On(events.inviteUser.acceptInvite)
public onAcceptInvite(payload) {
const { inviteToken, user } = payload;
const agenda = Container.get('agenda');
}
@On(events.inviteUser.checkInvite)
public onCheckInvite(payload) {
const { inviteToken, organizationOptions } = payload;
const agenda = Container.get('agenda');
}
/**
*
*/
@On(events.inviteUser.sendInvite)
public onSendInvite(payload) {
const { invite } = payload;
const { invite, authorizedUser, tenantId } = payload;
const agenda = Container.get('agenda');
agenda.now('user-invite-mail', {
invite, authorizedUser, tenantId
});
}
}
}

View File

@@ -23,6 +23,14 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
return ['createdAt', 'updatedAt'];
}
static get virtualAttributes() {
return ['fullName'];
}
get fullName() {
return (this.firstName + ' ' + this.lastName).trim();
}
/**
* Relationship mapping.
*/

View File

@@ -1,22 +1,26 @@
import { Service, Inject } from 'typedi';
import SystemRepository from "system/repositories/SystemRepository";
import { PlanSubscription } from 'system/models'
import SystemRepository from 'system/repositories/SystemRepository';
import { PlanSubscription } from 'system/models';
@Service()
export default class SubscriptionRepository extends SystemRepository{
@Inject('cache')
cache: any;
export default class SubscriptionRepository extends SystemRepository {
/**
* Gets the repository's model.
*/
get model() {
return PlanSubscription.bindKnex(this.knex);
}
/**
* Retrieve subscription from a given slug in specific tenant.
* @param {string} slug
* @param {number} tenantId
*/
getBySlugInTenant(slug: string, tenantId: number) {
const key = `subscription.slug.${slug}.tenant.${tenantId}`;
/**
* Retrieve subscription from a given slug in specific tenant.
* @param {string} slug
* @param {number} tenantId
*/
getBySlugInTenant(slug: string, tenantId: number) {
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
return this.cache.get(key, () => {
return PlanSubscription.query().findOne('slug', slug).where('tenant_id', tenantId);
});
}
}
return this.cache.get(cacheKey, () => {
return PlanSubscription.query()
.findOne('slug', slug)
.where('tenant_id', tenantId);
});
}
}

View File

@@ -1,5 +1,5 @@
import CachableRepository from "repositories/CachableRepository";
export default class SystemRepository extends CachableRepository {
export default class SystemRepository {
}

View File

@@ -1,24 +1,14 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import SystemRepository from "system/repositories/SystemRepository";
import { SystemUser } from "system/models";
import SystemRepository from 'system/repositories/SystemRepository';
import { SystemUser } from 'system/models';
import { ISystemUser } from 'interfaces';
@Service()
export default class SystemUserRepository extends SystemRepository {
@Inject('cache')
cache: any;
/**
* Patches the last login date to the given system user.
* @param {number} userId
* @return {Promise<void>}
* Gets the repository's model.
*/
async patchLastLoginAt(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, {
last_login_at: moment().toMySqlDateTime()
});
this.flushCache();
get model() {
return SystemUser.bindKnex(this.knex);
}
/**
@@ -28,43 +18,42 @@ export default class SystemUserRepository extends SystemRepository {
* @return {Promise<ISystemUser>}
*/
findByCrediential(crediential: string): Promise<ISystemUser> {
return SystemUser.query().whereNotDeleted()
.findOne('email', crediential)
.orWhere('phone_number', crediential);
}
/**
* Retrieve system user details of the given id.
* @param {number} userId - User id.
* @return {Promise<ISystemUser>}
*/
getById(userId: number): Promise<ISystemUser> {
return this.cache.get(`systemUser.id.${userId}`, () => {
return SystemUser.query().whereNotDeleted().findById(userId);
const cacheKey = this.getCacheKey('findByCrediential', crediential);
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne('email', crediential)
.orWhere('phone_number', crediential);
});
}
/**
* Retrieve user by id and tenant id.
* @param {number} userId - User id.
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
* @param {number} tenantId - Tenant id.
* @return {Promise<ISystemUser>}
*/
getByIdAndTenant(userId: number, tenantId: number): Promise<ISystemUser> {
return this.cache.get(`systemUser.id.${userId}.tenant.${tenantId}`, () => {
return SystemUser.query().whereNotDeleted()
findOneByIdAndTenant(userId: number, tenantId: number): Promise<ISystemUser> {
const cacheKey = this.getCacheKey('findOneByIdAndTenant', userId, tenantId);
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne({ id: userId, tenant_id: tenantId });
});
}
/**
* Retrieve system user details by the given email.
* @param {string} email - Email
* @param {string} email - Email
* @return {Promise<ISystemUser>}
*/
getByEmail(email: string): Promise<ISystemUser> {
return this.cache.get(`systemUser.email.${email}`, () => {
return SystemUser.query().whereNotDeleted().findOne('email', email);
findOneByEmail(email: string): Promise<ISystemUser> {
const cacheKey = this.getCacheKey('findOneByEmail', email);
return this.cache.get(cacheKey, () => {
return this.model.query().whereNotDeleted().findOne('email', email);
});
}
@@ -73,69 +62,43 @@ export default class SystemUserRepository extends SystemRepository {
* @param {string} phoneNumber - Phone number
* @return {Promise<ISystemUser>}
*/
getByPhoneNumber(phoneNumber: string): Promise<ISystemUser> {
return this.cache.get(`systemUser.phoneNumber.${phoneNumber}`, () => {
return SystemUser.query().whereNotDeleted().findOne('phoneNumber', phoneNumber);
findOneByPhoneNumber(phoneNumber: string): Promise<ISystemUser> {
const cacheKey = this.getCacheKey('findOneByPhoneNumber', phoneNumber);
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne('phoneNumber', phoneNumber);
});
}
/**
* Edits details.
* @param {number} userId - User id.
* @param {number} user - User input.
* @return {Promise<void>}
*/
async edit(userId: number, userInput: ISystemUser): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { ...userInput });
this.flushCache();
}
/**
* Creates a new user.
* @param {IUser} userInput - User input.
* @return {Promise<ISystemUser>}
*/
async create(userInput: ISystemUser): Promise<ISystemUser> {
const systemUser = await SystemUser.query().insert({ ...userInput });
this.flushCache();
return systemUser;
}
/**
* Deletes user by the given id.
* @param {number} userId - User id.
* Patches the last login date to the given system user.
* @param {number} userId
* @return {Promise<void>}
*/
async deleteById(userId: number): Promise<void> {
await SystemUser.query().where('id', userId).delete();
this.flushCache();
patchLastLoginAt(userId: number): Promise<void> {
return super.update(
{ last_login_at: moment().toMySqlDateTime() },
{ id: userId }
);
}
/**
* Activate user by the given id.
* @param {number} userId - User id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async activateById(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { active: 1 });
this.flushCache();
activateById(userId: number): Promise<void> {
return super.update({ active: 1 }, { id: userId });
}
/**
* Inactivate user by the given id.
* @param {number} userId - User id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async inactivateById(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { active: 0 });
this.flushCache();
inactivateById(userId: number): Promise<void> {
return super.update({ active: 0 }, { id: userId });
}
/**
* Flushes user repository cache.
*/
flushCache() {
this.cache.delStartWith('systemUser');
}
}
}

View File

@@ -1,83 +1,43 @@
import { Inject } from 'typedi';
import moment from "moment";
import { Tenant } from 'system/models';
import SystemRepository from "./SystemRepository";
import { ITenant } from 'interfaces';
import uniqid from 'uniqid';
import SystemRepository from "./SystemRepository";
import { Tenant } from "system/models";
import { ITenant } from 'interfaces';
export default class TenantRepository extends SystemRepository {
@Inject('cache')
cache: any;
/**
* Flush the given tenant stored cache.
* @param {ITenant} tenant
* Gets the repository's model.
*/
flushTenantCache(tenant: ITenant) {
this.cache.del(`tenant.org.${tenant.organizationId}`);
this.cache.del(`tenant.id.${tenant.id}`);
get model() {
return Tenant.bindKnex(this.knex);
}
/**
* Creates a new tenant with random organization id.
* @return {ITenant}
*/
newTenantWithUniqueOrgId(uniqId?: string): Promise<ITenant>{
createWithUniqueOrgId(uniqId?: string): Promise<ITenant>{
const organizationId = uniqid() || uniqId;
return Tenant.query().insert({ organizationId });
return super.create({ organizationId });
}
/**
* Mark as seeded.
* @param {number} tenantId
*/
async markAsSeeded(tenantId: number) {
const tenant = await Tenant.query()
.patchAndFetchById(tenantId, {
seeded_at: moment().toMySqlDateTime(),
});
this.flushTenantCache(tenant);
markAsSeeded(tenantId: number) {
return super.update({
seededAt: moment().toMySqlDateTime(),
}, { id: tenantId })
}
/**
* Mark the the given organization as initialized.
* @param {string} organizationId
*/
async markAsInitialized(tenantId: number) {
const tenant = await Tenant.query()
.patchAndFetchById(tenantId, {
initialized_at: moment().toMySqlDateTime(),
});
this.flushTenantCache(tenant);
}
/**
* Retrieve tenant details by the given organization id.
* @param {string} organizationId
*/
getByOrgId(organizationId: string) {
return this.cache.get(`tenant.org.${organizationId}`, () => {
return Tenant.query().findOne('organization_id', organizationId);
});
}
/**
* Retrieve tenant details by the given tenant id.
* @param {string} tenantId - Tenant id.
*/
getById(tenantId: number) {
return this.cache.get(`tenant.id.${tenantId}`, () => {
return Tenant.query().findById(tenantId);
});
}
/**
* Retrieve tenant details with associated subscriptions
* and plans by the given tenant id.
* @param {number} tenantId - Tenant id.
*/
getByIdWithSubscriptions(tenantId: number) {
return Tenant.query().findById(tenantId)
.withGraphFetched('subscriptions.plan');
markAsInitialized(tenantId: number) {
return super.update({
initializedAt: moment().toMySqlDateTime(),
}, { id: tenantId });
}
}