From 717747981eef2de3469b12dbfa2f5a2949dc6186 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 2 Jan 2021 11:07:42 +0200 Subject: [PATCH] feat: item categories events. --- server/src/api/controllers/ItemCategories.ts | 39 ++- .../ItemCategories/ItemCategoriesService.ts | 304 +++++++++++++----- server/src/subscribers/events.ts | 10 + 3 files changed, 266 insertions(+), 87 deletions(-) diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts index 96efa90b3..22aa6b20a 100644 --- a/server/src/api/controllers/ItemCategories.ts +++ b/server/src/api/controllers/ItemCategories.ts @@ -147,7 +147,10 @@ export default class ItemsCategoriesController extends BaseController { itemCategoryOTD, user ); - return res.status(200).send({ id: itemCategory.id }); + return res.status(200).send({ + id: itemCategory.id, + message: 'The item category has been created successfully.', + }); } catch (error) { next(error); } @@ -171,7 +174,10 @@ export default class ItemsCategoriesController extends BaseController { itemCategoryOTD, user ); - return res.status(200).send({ id: itemCategoryId }); + return res.status(200).send({ + id: itemCategoryId, + message: 'The item category has been edited successfully.', + }); } catch (error) { next(error); } @@ -193,7 +199,10 @@ export default class ItemsCategoriesController extends BaseController { itemCategoryId, user ); - return res.status(200).send({ id: itemCategoryId }); + return res.status(200).send({ + id: itemCategoryId, + message: 'The item category has been deleted successfully.', + }); } catch (error) { next(error); } @@ -270,7 +279,10 @@ export default class ItemsCategoriesController extends BaseController { itemCategoriesIds, user ); - return res.status(200).send({ ids: itemCategoriesIds }); + return res.status(200).send({ + ids: itemCategoriesIds, + message: 'The item categories have been deleted successfully.', + }); } catch (error) { next(error); } @@ -297,37 +309,42 @@ export default class ItemsCategoriesController extends BaseController { } if (error.errorType === 'ITEM_CATEGORIES_NOT_FOUND') { return res.boom.badRequest(null, { - errors: [{ type: 'ITEM_CATEGORIES_NOT_FOUND', code: 120 }], + errors: [{ type: 'ITEM_CATEGORIES_NOT_FOUND', code: 200 }], + }); + } + if (error.errorType === 'CATEGORY_NAME_EXISTS') { + return res.boom.badRequest(null, { + errors: [{ type: 'CATEGORY_NAME_EXISTS', code: 300 }], }); } if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') { return res.boom.badRequest(null, { - errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }], + errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 400 }], }); } if (error.errorType === 'COST_ACCOUNT_NOT_COGS') { return res.boom.badRequest(null, { - errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }], + errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 500 }], }); } if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') { return res.boom.badRequest(null, { - errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }], + errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 600 }], }); } if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') { return res.boom.badRequest(null, { - errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }], + errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 700 }], }); } 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: 800 }], }); } if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') { return res.boom.badRequest(null, { - errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], + errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 900 }], }); } } diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index ba892a6ed..cd721ccfd 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -12,13 +12,14 @@ import { IItemCategoriesFilter, ISystemUser, IFilterMeta, -} from "interfaces"; +} from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import events from 'subscribers/events'; const ERRORS = { ITEM_CATEGORIES_NOT_FOUND: 'ITEM_CATEGORIES_NOT_FOUND', + CATEGORY_NAME_EXISTS: 'CATEGORY_NAME_EXISTS', CATEGORY_NOT_FOUND: 'CATEGORY_NOT_FOUND', COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD', COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS', @@ -26,7 +27,7 @@ const ERRORS = { SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', - CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS' + CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS', }; export default class ItemCategoriesService implements IItemCategoriesService { @@ -44,25 +45,31 @@ export default class ItemCategoriesService implements IItemCategoriesService { /** * Retrieve item category or throw not found error. - * @param {number} tenantId - * @param {number} itemCategoryId + * @param {number} tenantId + * @param {number} itemCategoryId */ - private async getItemCategoryOrThrowError(tenantId: number, itemCategoryId: number) { + private async getItemCategoryOrThrowError( + tenantId: number, + itemCategoryId: number + ) { const { ItemCategory } = this.tenancy.models(tenantId); const category = await ItemCategory.query().findById(itemCategoryId); if (!category) { - throw new ServiceError(ERRORS.CATEGORY_NOT_FOUND) + throw new ServiceError(ERRORS.CATEGORY_NOT_FOUND); } return category; } /** * Transforms OTD to model object. - * @param {IItemCategoryOTD} itemCategoryOTD - * @param {ISystemUser} authorizedUser + * @param {IItemCategoryOTD} itemCategoryOTD + * @param {ISystemUser} authorizedUser */ - private transformOTDToObject(itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser) { + private transformOTDToObject( + itemCategoryOTD: IItemCategoryOTD, + authorizedUser: ISystemUser + ) { return { ...itemCategoryOTD, userId: authorizedUser.id }; } @@ -72,14 +79,48 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @param {number} itemCategoryId - * @returns {IItemCategory} */ - public async getItemCategory(tenantId: number, itemCategoryId: number, user: ISystemUser) { + public async getItemCategory( + tenantId: number, + itemCategoryId: number, + user: ISystemUser + ) { return this.getItemCategoryOrThrowError(tenantId, itemCategoryId); } + /** + * Validates the category name uniquiness. + * @param {number} tenantId - Tenant id. + * @param {string} categoryName - Category name. + * @param {number} notAccountId - Ignore the account id. + */ + private async validateCategoryNameUniquiness( + tenantId: number, + categoryName: string, + notCategoryId?: number + ) { + const { ItemCategory } = this.tenancy.models(tenantId); + + this.logger.info('[item_category] validating category name uniquiness.', { + tenantId, + categoryName, + notCategoryId, + }); + const foundItemCategory = await ItemCategory.query() + .findOne('name', categoryName) + .onBuild((query) => { + if (notCategoryId) { + query.whereNot('id', notCategoryId); + } + }); + if (foundItemCategory) { + throw new ServiceError(ERRORS.CATEGORY_NAME_EXISTS); + } + } + /** * Inserts a new item category. - * @param {number} tenantId - * @param {IItemCategoryOTD} itemCategoryOTD + * @param {number} tenantId + * @param {IItemCategoryOTD} itemCategoryOTD * @return {Promise} */ public async newItemCategory( @@ -88,7 +129,11 @@ export default class ItemCategoriesService implements IItemCategoriesService { authorizedUser: ISystemUser ): Promise { const { ItemCategory } = this.tenancy.models(tenantId); - this.logger.info('[item_category] trying to insert a new item category.', { tenantId }); + this.logger.info('[item_category] trying to insert a new item category.', { + tenantId, + }); + // Validate the category name uniquiness. + await this.validateCategoryNameUniquiness(tenantId, itemCategoryOTD.name); if (itemCategoryOTD.sellAccountId) { await this.validateSellAccount(tenantId, itemCategoryOTD.sellAccountId); @@ -97,14 +142,25 @@ export default class ItemCategoriesService implements IItemCategoriesService { await this.validateCostAccount(tenantId, itemCategoryOTD.costAccountId); } if (itemCategoryOTD.inventoryAccountId) { - await this.validateInventoryAccount(tenantId, itemCategoryOTD.inventoryAccountId); + await this.validateInventoryAccount( + tenantId, + itemCategoryOTD.inventoryAccountId + ); } - const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser); - const itemCategory = await ItemCategory.query().insert({ ...itemCategoryObj }); + const itemCategoryObj = this.transformOTDToObject( + itemCategoryOTD, + authorizedUser + ); + const itemCategory = await ItemCategory.query().insert({ + ...itemCategoryObj, + }); - await this.eventDispatcher.dispatch(events.items.onCreated); - this.logger.info('[item_category] item category inserted successfully.', { tenantId, itemCategoryOTD }); + await this.eventDispatcher.dispatch(events.itemCategory.onCreated); + this.logger.info('[item_category] item category inserted successfully.', { + tenantId, + itemCategoryOTD, + }); return itemCategory; } @@ -116,17 +172,29 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @return {Promise} */ private async validateSellAccount(tenantId: number, sellAccountId: number) { - const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId); + const { + accountRepository, + accountTypeRepository, + } = this.tenancy.repositories(tenantId); - this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); + this.logger.info('[items] validate sell account existance.', { + tenantId, + sellAccountId, + }); const incomeType = await accountTypeRepository.getByKey('income'); const foundAccount = await accountRepository.findOneById(sellAccountId); if (!foundAccount) { - this.logger.info('[items] sell account not found.', { tenantId, sellAccountId }); - throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND) + this.logger.info('[items] sell account not found.', { + tenantId, + sellAccountId, + }); + throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND); } else if (foundAccount.accountTypeId !== incomeType.id) { - this.logger.info('[items] sell account not income type.', { tenantId, sellAccountId }); + this.logger.info('[items] sell account not income type.', { + tenantId, + sellAccountId, + }); throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME); } } @@ -138,73 +206,124 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @return {Promise} */ private async validateCostAccount(tenantId: number, costAccountId: number) { - const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId); + const { + accountRepository, + accountTypeRepository, + } = this.tenancy.repositories(tenantId); - this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); + this.logger.info('[items] validate cost account existance.', { + tenantId, + costAccountId, + }); const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); - const foundAccount = await accountRepository.findOneById(costAccountId) + const foundAccount = await accountRepository.findOneById(costAccountId); if (!foundAccount) { - this.logger.info('[items] cost account not found.', { tenantId, costAccountId }); + this.logger.info('[items] cost account not found.', { + tenantId, + costAccountId, + }); throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD); } else if (foundAccount.accountTypeId !== COGSType.id) { - this.logger.info('[items] validate cost account not COGS type.', { tenantId, costAccountId }); + this.logger.info('[items] validate cost account not COGS type.', { + tenantId, + costAccountId, + }); throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS); } } /** * Validates inventory account existance and type. - * @param {number} tenantId - * @param {number} inventoryAccountId + * @param {number} tenantId + * @param {number} inventoryAccountId * @return {Promise} */ - private async validateInventoryAccount(tenantId: number, inventoryAccountId: number) { - const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + private async validateInventoryAccount( + tenantId: number, + inventoryAccountId: number + ) { + const { + accountTypeRepository, + accountRepository, + } = this.tenancy.repositories(tenantId); - this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); + this.logger.info('[items] validate inventory account existance.', { + tenantId, + inventoryAccountId, + }); const otherAsset = await accountTypeRepository.getByKey('other_asset'); - const foundAccount = await accountRepository.findOneById(inventoryAccountId); + const foundAccount = await accountRepository.findOneById( + inventoryAccountId + ); if (!foundAccount) { - this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId }); - throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND) + this.logger.info('[items] inventory account not found.', { + tenantId, + inventoryAccountId, + }); + throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND); } else if (otherAsset.id !== foundAccount.accountTypeId) { - this.logger.info('[items] inventory account not inventory type.', { tenantId, inventoryAccountId }); + this.logger.info('[items] inventory account not inventory type.', { + tenantId, + inventoryAccountId, + }); throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY); } } /** * Edits item category. - * @param {number} tenantId - * @param {number} itemCategoryId - * @param {IItemCategoryOTD} itemCategoryOTD + * @param {number} tenantId + * @param {number} itemCategoryId + * @param {IItemCategoryOTD} itemCategoryOTD * @return {Promise} */ public async editItemCategory( tenantId: number, itemCategoryId: number, itemCategoryOTD: IItemCategoryOTD, - authorizedUser: ISystemUser, + authorizedUser: ISystemUser ): Promise { const { ItemCategory } = this.tenancy.models(tenantId); - const oldItemCategory = await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); + const oldItemCategory = await this.getItemCategoryOrThrowError( + tenantId, + itemCategoryId + ); - if (itemCategoryOTD.sellAccountId) { + // Validate the category name whether unique on the storage. + await this.validateCategoryNameUniquiness( + tenantId, + itemCategoryOTD.name, + itemCategoryId + ); + if (itemCategoryOTD.sellAccountId) { await this.validateSellAccount(tenantId, itemCategoryOTD.sellAccountId); } if (itemCategoryOTD.costAccountId) { await this.validateCostAccount(tenantId, itemCategoryOTD.costAccountId); } if (itemCategoryOTD.inventoryAccountId) { - await this.validateInventoryAccount(tenantId, itemCategoryOTD.inventoryAccountId); + await this.validateInventoryAccount( + tenantId, + itemCategoryOTD.inventoryAccountId + ); } - const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser); - const itemCategory = await ItemCategory.query().patchAndFetchById(itemCategoryId, { ...itemCategoryObj }); + const itemCategoryObj = this.transformOTDToObject( + itemCategoryOTD, + authorizedUser + ); + const itemCategory = await ItemCategory.query().patchAndFetchById( + itemCategoryId, + { ...itemCategoryObj } + ); - await this.eventDispatcher.dispatch(events.items.onEdited); - this.logger.info('[item_category] edited successfully.', { tenantId, itemCategoryId, itemCategoryOTD }); + await this.eventDispatcher.dispatch(events.itemCategory.onEdited); + this.logger.info('[item_category] edited successfully.', { + tenantId, + itemCategoryId, + itemCategoryOTD, + }); return itemCategory; } @@ -215,8 +334,15 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @param {number} itemCategoryId - Item category id. * @return {Promise} */ - public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) { - this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId }); + public async deleteItemCategory( + tenantId: number, + itemCategoryId: number, + authorizedUser: ISystemUser + ) { + this.logger.info('[item_category] trying to delete item category.', { + tenantId, + itemCategoryId, + }); // Retrieve item category or throw not found error. await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); @@ -226,40 +352,58 @@ export default class ItemCategoriesService implements IItemCategoriesService { const { ItemCategory } = this.tenancy.models(tenantId); await ItemCategory.query().findById(itemCategoryId).delete(); - this.logger.info('[item_category] deleted successfully.', { tenantId, itemCategoryId }); + this.logger.info('[item_category] deleted successfully.', { + tenantId, + itemCategoryId, + }); - await this.eventDispatcher.dispatch(events.items.onDeleted); + await this.eventDispatcher.dispatch(events.itemCategory.onDeleted); } - + /** * Retrieve item categories or throw not found error. - * @param {number} tenantId - * @param {number[]} itemCategoriesIds + * @param {number} tenantId + * @param {number[]} itemCategoriesIds */ - private async getItemCategoriesOrThrowError(tenantId: number, itemCategoriesIds: number[]) { + private async getItemCategoriesOrThrowError( + tenantId: number, + itemCategoriesIds: number[] + ) { const { ItemCategory } = this.tenancy.models(tenantId); - const itemCategories = await ItemCategory.query().whereIn('id', itemCategoriesIds); + const itemCategories = await ItemCategory.query().whereIn( + 'id', + itemCategoriesIds + ); - const storedItemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id); - const notFoundCategories = difference(itemCategoriesIds, storedItemCategoriesIds); + const storedItemCategoriesIds = itemCategories.map( + (category: IItemCategory) => category.id + ); + const notFoundCategories = difference( + itemCategoriesIds, + storedItemCategoriesIds + ); if (notFoundCategories.length > 0) { throw new ServiceError(ERRORS.ITEM_CATEGORIES_NOT_FOUND); } } - + /** * Retrieve item categories list. - * @param {number} tenantId - * @param filter + * @param {number} tenantId + * @param filter */ public async getItemCategoriesList( tenantId: number, filter: IItemCategoriesFilter, authorizedUser: ISystemUser - ): Promise<{ itemCategories: IItemCategory[], filterMeta: IFilterMeta }> { + ): Promise<{ itemCategories: IItemCategory[]; filterMeta: IFilterMeta }> { const { ItemCategory } = this.tenancy.models(tenantId); - const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter); + const dynamicList = await this.dynamicListService.dynamicList( + tenantId, + ItemCategory, + filter + ); const itemCategories = await ItemCategory.query().onBuild((query) => { // Subquery to calculate sumation of assocaited items to the item category. @@ -272,39 +416,47 @@ export default class ItemCategoriesService implements IItemCategoriesService { /** * Unlink items relations with item categories. - * @param {number} tenantId - * @param {number|number[]} itemCategoryId - + * @param {number} tenantId + * @param {number|number[]} itemCategoryId - * @return {Promise} */ private async unassociateItemsWithCategories( tenantId: number, - itemCategoryId: number | number[], + itemCategoryId: number | number[] ): Promise { const { Item } = this.tenancy.models(tenantId); - const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId]; + const ids = Array.isArray(itemCategoryId) + ? itemCategoryId + : [itemCategoryId]; await Item.query().whereIn('category_id', ids).patch({ category_id: null }); } /** * Deletes item categories in bulk. - * @param {number} tenantId - * @param {number[]} itemCategoriesIds + * @param {number} tenantId + * @param {number[]} itemCategoriesIds */ public async deleteItemCategories( tenantId: number, itemCategoriesIds: number[], - authorizedUser: ISystemUser, + authorizedUser: ISystemUser ) { - this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds }); + this.logger.info('[item_category] trying to delete item categories.', { + tenantId, + itemCategoriesIds, + }); const { ItemCategory } = this.tenancy.models(tenantId); await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds); await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds); - + await ItemCategory.query().whereIn('id', itemCategoriesIds).delete(); - await this.eventDispatcher.dispatch(events.items.onBulkDeleted); - this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds }); + await this.eventDispatcher.dispatch(events.itemCategory.onBulkDeleted); + this.logger.info('[item_category] item categories deleted successfully.', { + tenantId, + itemCategoriesIds, + }); } -} \ No newline at end of file +} diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 28a8622b0..402a83ee8 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -175,6 +175,16 @@ export default { onBulkDeleted: 'onItemBulkDeleted', }, + /** + * Item category service. + */ + itemCategory: { + onCreated: 'onItemCategoryCreated', + onEdited: 'onItemCategoryEdited', + onDeleted: 'onItemCategoryDeleted', + onBulkDeleted: 'onItemCategoryBulkDeleted', + }, + /** * Inventory service. */