From 9fbad4ac46703972c92cba50cbccf7d6300f5972 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 29 Sep 2020 16:04:13 +0200 Subject: [PATCH] feat: items bulk delete. refactor: items to service design. --- server/src/api/controllers/Items.ts | 382 ++++++++++------------ server/src/interfaces/Item.ts | 58 ++++ server/src/loaders/tenantModels.ts | 2 + server/src/services/Items/ItemsService.ts | 271 ++++++++++++--- 4 files changed, 470 insertions(+), 243 deletions(-) diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 9fdb7b1ab..672c09bb5 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -2,13 +2,15 @@ import { Inject, Service } from 'typedi'; import { Router, Request, Response, NextFunction } from 'express'; import { check, param, query, ValidationChain, matchedData } from 'express-validator'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import validateMiddleware from 'api/middleware/validateMiddleware'; import ItemsService from 'services/Items/ItemsService'; import BaseController from 'api/controllers/BaseController'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { ServiceError } from 'exceptions'; +import { IItemDTO } from 'interfaces'; +import { Request } from 'express-validator/src/base'; @Service() -export default class ItemsController extends BaseController { +export default class ItemsController extends BaseController { @Inject() itemsService: ItemsService; @@ -22,50 +24,53 @@ export default class ItemsController extends BaseController { const router = Router(); router.post( - '/', - this.validateItemSchema, - validateMiddleware, - asyncMiddleware(this.validateCategoryExistance.bind(this)), - asyncMiddleware(this.validateCostAccountExistance.bind(this)), - asyncMiddleware(this.validateSellAccountExistance.bind(this)), - asyncMiddleware(this.validateInventoryAccountExistance.bind(this)), - asyncMiddleware(this.validateItemNameExistance.bind(this)), + '/', [ + ...this.validateItemSchema, + ], + this.validationResult, asyncMiddleware(this.newItem.bind(this)), + this.handlerServiceErrors, ); router.post( '/:id', [ - ...this.validateItemSchema, - ...this.validateSpecificItemSchema, - ], - validateMiddleware, - asyncMiddleware(this.validateItemExistance.bind(this)), - asyncMiddleware(this.validateCategoryExistance.bind(this)), - asyncMiddleware(this.validateCostAccountExistance.bind(this)), - asyncMiddleware(this.validateSellAccountExistance.bind(this)), - asyncMiddleware(this.validateInventoryAccountExistance.bind(this)), - asyncMiddleware(this.validateItemNameExistance.bind(this)), + ...this.validateItemSchema, + ...this.validateSpecificItemSchema, + ], + this.validationResult, asyncMiddleware(this.editItem.bind(this)), + this.handlerServiceErrors, + ); + router.delete('/', [ + ...this.validateBulkSelectSchema, + ], + this.validationResult, + asyncMiddleware(this.bulkDeleteItems.bind(this)), + this.handlerServiceErrors ); router.delete( - '/:id', - this.validateSpecificItemSchema, - validateMiddleware, - asyncMiddleware(this.validateItemExistance.bind(this)), + '/:id', [ + ...this.validateSpecificItemSchema, + ], + this.validationResult, asyncMiddleware(this.deleteItem.bind(this)), + this.handlerServiceErrors, ); router.get( - '/:id', - this.validateSpecificItemSchema, - validateMiddleware, - asyncMiddleware(this.validateItemExistance.bind(this)), + '/:id', [ + ...this.validateSpecificItemSchema, + ], + this.validationResult, asyncMiddleware(this.getItem.bind(this)), + this.handlerServiceErrors, ); router.get( - '/', - this.validateListQuerySchema, - validateMiddleware, + '/', [ + ...this.validateListQuerySchema, + ], + this.validationResult, asyncMiddleware(this.getItemsList.bind(this)), this.dynamicListService.handlerErrorsToResponse, + this.handlerServiceErrors, ); return router; } @@ -88,7 +93,7 @@ export default class ItemsController extends BaseController { .toFloat(), check('cost_account_id') .if(check('purchasable').equals('true')) - .exists() + .exists() .isInt() .toInt(), // Sell attributes. @@ -121,6 +126,7 @@ export default class ItemsController extends BaseController { /** * Validate specific item params schema. + * @return {ValidationChain[]} */ get validateSpecificItemSchema(): ValidationChain[] { return [ @@ -128,6 +134,16 @@ export default class ItemsController extends BaseController { ]; } + /** + * Bulk select validation schema. + * @return {ValidationChain[]} + */ + get validateBulkSelectSchema(): ValidationChain[] { + return [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ]; + } /** * Validate list query schema @@ -143,169 +159,21 @@ export default class ItemsController extends BaseController { ] } - /** - * Validates the given item existance on the storage. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - - */ - async validateItemExistance(req: Request, res: Response, next: Function) { - const { Item } = req.models; - const itemId: number = req.params.id; - - const foundItem = await Item.query().findById(itemId); - - if (!foundItem) { - return res.status(400).send({ - errors: [{ type: 'ITEM.NOT.FOUND', code: 100 }], - }); - } - next(); - } - - /** - * Validate wether the given item name already exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async validateItemNameExistance(req: Request, res: Response, next: Function) { - const { Item } = req.models; - const item = req.body; - const itemId: number = req.params.id; - - const foundItems: [] = await Item.query().onBuild((builder: any) => { - builder.where('name', item.name); - if (itemId) { - builder.whereNot('id', itemId); - } - }); - if (foundItems.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ITEM.NAME.ALREADY.EXISTS', code: 210 }], - }); - } - next(); - } - - /** - * Validate wether the given category existance on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateCategoryExistance(req: Request, res: Response, next: Function) { - const { ItemCategory } = req.models; - const item = req.body; - - if (item.category_id) { - const foundCategory = await ItemCategory.query().findById(item.category_id); - - if (!foundCategory) { - return res.status(400).send({ - errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 140 }], - }); - } - } - next(); - } - - /** - * Validate wether the given cost account exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next - */ - async validateCostAccountExistance(req: Request, res: Response, next: Function) { - const { Account, AccountType } = req.models; - const item = req.body; - - if (item.cost_account_id) { - const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold'); - const foundAccount = await Account.query().findById(item.cost_account_id) - - if (!foundAccount) { - return res.status(400).send({ - errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }], - }); - } else if (foundAccount.accountTypeId !== COGSType.id) { - return res.status(400).send({ - errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }], - }); - } - } - next(); - } - - /** - * Validate wether the given sell account exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async validateSellAccountExistance(req: Request, res: Response, next: Function) { - const { Account, AccountType } = req.models; - const item = req.body; - - if (item.sell_account_id) { - const incomeType = await AccountType.query().findOne('key', 'income'); - const foundAccount = await Account.query().findById(item.sell_account_id); - - if (!foundAccount) { - return res.status(400).send({ - errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }], - }); - } else if (foundAccount.accountTypeId !== incomeType.id) { - return res.status(400).send({ - errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }], - }) - } - } - next(); - } - - /** - * Validates wether the given inventory account exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async validateInventoryAccountExistance(req: Request, res: Response, next: Function) { - const { Account, AccountType } = req.models; - const item = req.body; - - if (item.inventory_account_id) { - const otherAsset = await AccountType.query().findOne('key', 'other_asset'); - const foundAccount = await Account.query().findById(item.inventory_account_id); - - if (!foundAccount) { - return res.status(400).send({ - errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}], - }); - } else if (otherAsset.id !== foundAccount.accountTypeId) { - return res.status(400).send({ - errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], - }); - } - } - next(); - } - /** * Stores the given item details to the storage. * @param {Request} req * @param {Response} res */ - async newItem(req: Request, res: Response,) { + async newItem(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; + const itemDTO: IItemDTO = this.matchedBodyData(req); - const item = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); - const storedItem = await this.itemsService.newItem(tenantId, item); - - return res.status(200).send({ id: storedItem.id }); + try { + const storedItem = await this.itemsService.newItem(tenantId, itemDTO); + return res.status(200).send({ id: storedItem.id }); + } catch (error) { + next(error); + } } /** @@ -313,17 +181,17 @@ export default class ItemsController extends BaseController { * @param {Request} req * @param {Response} res */ - async editItem(req: Request, res: Response) { + async editItem(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; - const itemId: number = req.params.id; - const item = matchedData(req, { - locations: ['body'], - includeOptionals: true - }); - const updatedItem = await this.itemsService.editItem(tenantId, item, itemId); - - return res.status(200).send({ id: itemId }); + const item: IItemDTO = this.matchedBodyData(req); + + try { + await this.itemsService.editItem(tenantId, itemId, item); + return res.status(200).send({ id: itemId }); + } catch (error) { + next(error); + } } /** @@ -331,13 +199,16 @@ export default class ItemsController extends BaseController { * @param {Request} req * @param {Response} res */ - async deleteItem(req: Request, res: Response) { + async deleteItem(req: Request, res: Response, next: NextFunction) { const itemId: number = req.params.id; const { tenantId } = req; - await this.itemsService.deleteItem(tenantId, itemId); - - return res.status(200).send({ id: itemId }); + try { + await this.itemsService.deleteItem(tenantId, itemId); + return res.status(200).send({ id: itemId }); + } catch (error) { + next(error); + } } /** @@ -346,13 +217,17 @@ export default class ItemsController extends BaseController { * @param {Response} res * @return {Response} */ - async getItem(req: Request, res: Response) { + async getItem(req: Request, res: Response, next: NextFunction) { const itemId: number = req.params.id; const { tenantId } = req; - const storedItem = await this.itemsService.getItemWithMetadata(tenantId, itemId); + try { + const storedItem = await this.itemsService.getItem(tenantId, itemId); - return res.status(200).send({ item: storedItem }); + return res.status(200).send({ item: storedItem }); + } catch (error) { + next(error) + } } /** @@ -371,10 +246,105 @@ export default class ItemsController extends BaseController { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } try { - const items = await this.itemsService.getItemsList(tenantId, filter); + const items = await this.itemsService.itemsList(tenantId, filter); return res.status(200).send({ items }); } catch (error) { next(error); } - } + } + + /** + * Deletes items in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async bulkDeleteItems(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { ids: itemsIds } = req.query; + + try { + await this.itemsService.bulkDeleteItems(tenantId, itemsIds); + return res.status(200).send({ ids: itemsIds }); + } catch (error) { + next(error); + } + } + + /** + * Handles service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }], + }); + } + if (error.errorType === 'ITEM_CATEOGRY_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 140 }], + }); + } + if (error.errorType === 'ITEM_NAME_EXISTS') { + return res.status(400).send({ + errors: [{ type: 'ITEM.NAME.ALREADY.EXISTS', code: 210 }], + }); + } + if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') { + return res.status(400).send({ + errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }], + }); + } + if (error.errorType === 'COST_ACCOUNT_NOT_COGS') { + return res.status(400).send({ + errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }], + }); + } + if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }], + }); + } + if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') { + return res.status(400).send({ + errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }], + }); + } + if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') { + return res.status(400).send({ + errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }], + }); + } + if (error.errorType === 'COST_ACCOUNT_NOT_COGS') { + return res.status(400).send({ + errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }], + }); + } + if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }], + }); + } + if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') { + return res.status(400).send({ + errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200 }], + }); + } + if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') { + return res.status(400).send({ + errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }], + }); + } + if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') { + return res.status(400).send({ + errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], + }); + } + } + } } \ No newline at end of file diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index e7fa44cce..d0824056c 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -4,10 +4,68 @@ export interface IItem{ id: number, name: string, type: string, + sku: string, + + sellable: boolean, + purchasable: boolean, + + costPrice: number, + sellPrice: number, + currencyCode: string, + + costAccountId: number, + sellAccountId: number, + inventoryAccountId: number, + + sellDescription: string, + purchaseDescription: string, + + quantityOnHand: number, + note: string, + + categoryId: number, + userId: number, + + createdAt: Date, + updatedAt: Date, +} + +export interface IItemDTO { + name: string, + type: string, + sku: string, + + sellable: boolean, + purchasable: boolean, + + costPrice: number, + sellPrice: number, + + currencyCode: string, + + costAccountId: number, + sellAccountId: number, + inventoryAccountId: number, + + sellDescription: string, + purchaseDescription: string, + + quantityOnHand: number, + note: string, + + categoryId: number, } export interface IItemsService { + bulkDeleteItems(tenantId: number, itemsIds: number[]): Promise; + getItem(tenantId: number, itemId: number): Promise; + deleteItem(tenantId: number, itemId: number): Promise; + editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise; + + newItem(tenantId: number, itemDTO: IItemDTO): Promise; + + itemsList(tenantId: number, itemsFilter: IItemsFilter): Promise<{items: IItem[]}>; } export interface IItemsFilter extends IDynamicListFilter { diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 24bf17db7..d5c124dce 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -5,6 +5,7 @@ import AccountTransaction from 'models/AccountTransaction'; import AccountType from 'models/AccountType'; import Item from 'models/Item'; import ItemEntry from 'models/ItemEntry'; +import ItemCategory from 'models/ItemCategory'; import Bill from 'models/Bill'; import BillPayment from 'models/BillPayment'; import BillPaymentEntry from 'models/BillPaymentEntry'; @@ -41,6 +42,7 @@ export default (knex) => { AccountTransaction, AccountType, Item, + ItemCategory, ItemEntry, ManualJournal, Bill, diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 469721aee..8428826e2 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,41 +1,230 @@ import { difference } from "lodash"; import { Service, Inject } from "typedi"; -import { IItemsFilter } from 'interfaces'; +import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; +import { ServiceError } from "exceptions"; +import { Item } from "models"; + +const ERRORS = { + NOT_FOUND: 'NOT_FOUND', + ITEM_NAME_EXISTS: 'ITEM_NAME_EXISTS', + ITEM_CATEOGRY_NOT_FOUND: 'ITEM_CATEOGRY_NOT_FOUND', + COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS', + COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD', + SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND', + SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME', + + INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', + INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', +} @Service() -export default class ItemsService { +export default class ItemsService implements IItemsService { @Inject() tenancy: TenancyService; @Inject() dynamicListService: DynamicListingService; - async newItem(tenantId: number, item: any) { + @Inject('logger') + logger: any; + + /** + * Retrieve item details or throw not found error. + * @param {number} tenantId + * @param {number} itemId + * @return {Promise} + */ + private async getItemOrThrowError(tenantId: number, itemId: number): Promise { const { Item } = this.tenancy.models(tenantId); - const storedItem = await Item.query() - .insertAndFetch({ - ...item, - }); + + this.logger.info('[items] validate item id existance.', { itemId }); + const foundItem = await Item.query().findById(itemId); + + if (!foundItem) { + this.logger.info('[items] item not found.', { itemId }); + throw new ServiceError(ERRORS.NOT_FOUND); + } + return foundItem; + } + + /** + * Validate wether the given item name already exists on the storage. + * @param {number} tenantId + * @param {string} itemName + * @param {number} notItemId + * @return {Promise} + */ + private async validateItemNameUniquiness(tenantId: number, itemName: string, notItemId?: number): Promise { + const { Item } = this.tenancy.models(tenantId); + + this.logger.info('[items] validate item name uniquiness.', { itemName, tenantId }); + const foundItems: [] = await Item.query().onBuild((builder: any) => { + builder.where('name', itemName); + if (notItemId) { + builder.whereNot('id', notItemId); + } + }); + if (foundItems.length > 0) { + this.logger.info('[items] item name already exists.', { itemName, tenantId }); + throw new ServiceError(ERRORS.ITEM_NAME_EXISTS); + } + } + + /** + * Validate item COGS account existance and type. + * @param {number} tenantId + * @param {number} costAccountId + * @return {Promise} + */ + private async validateItemCostAccountExistance(tenantId: number, costAccountId: number): Promise { + const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId }); + const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); + const foundAccount = await accountRepository.getById(costAccountId) + + if (!foundAccount) { + 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 }); + throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS); + } + } + + /** + * Validate item sell account existance and type. + * @param {number} tenantId - Tenant id. + * @param {number} sellAccountId - Sell account id. + */ + private async validateItemSellAccountExistance(tenantId: number, sellAccountId: number) { + const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId }); + const incomeType = await accountTypeRepository.getByKey('income'); + const foundAccount = await accountRepository.getById(sellAccountId); + + if (!foundAccount) { + 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 }); + throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME); + } + } + + /** + * Validate item inventory account existance and type. + * @param {number} tenantId + * @param {number} inventoryAccountId + */ + private async validateItemInventoryAccountExistance(tenantId: number, inventoryAccountId: number) { + const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + + this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId }); + const otherAsset = await accountTypeRepository.getByKey('other_asset'); + const foundAccount = await accountRepository.getById(inventoryAccountId); + + if (!foundAccount) { + 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 }); + throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY); + } + } + + /** + * Validate item category existance. + * @param {number} tenantId + * @param {number} itemCategoryId + */ + private async validateItemCategoryExistance(tenantId: number, itemCategoryId: number) { + const { ItemCategory } = this.tenancy.models(tenantId); + const foundCategory = await ItemCategory.query().findById(itemCategoryId); + + if (!foundCategory) { + throw new ServiceError(ERRORS.ITEM_CATEOGRY_NOT_FOUND); + } + } + + /** + * Creates a new item. + * @param {number} tenantId DTO + * @param {IItemDTO} item + * @return {Promise} + */ + public async newItem(tenantId: number, itemDTO: IItemDTO): Promise { + const { Item } = this.tenancy.models(tenantId); + + // Validate whether the given item name already exists on the storage. + await this.validateItemNameUniquiness(tenantId, itemDTO.name); + + if (itemDTO.categoryId) { + await this.validateItemCategoryExistance(tenantId, itemDTO.categoryId); + } + if (itemDTO.sellAccountId) { + await this.validateItemSellAccountExistance(tenantId, itemDTO.sellAccountId); + } + if (itemDTO.costAccountId) { + await this.validateItemCostAccountExistance(tenantId, itemDTO.costAccountId); + } + if (itemDTO.inventoryAccountId) { + await this.validateItemInventoryAccountExistance(tenantId, itemDTO.inventoryAccountId); + } + const storedItem = await Item.query().insertAndFetch({ ...itemDTO }); + this.logger.info('[items] item inserted successfully.', { tenantId, itemDTO }); + return storedItem; } - async editItem(tenantId: number, item: any, itemId: number) { + /** + * Edits the item metadata. + * @param {number} tenantId + * @param {number} itemId + * @param {IItemDTO} itemDTO + */ + public async editItem(tenantId: number, itemId: number, itemDTO: IItemDTO) { const { Item } = this.tenancy.models(tenantId); - const updateItem = await Item.query() - .findById(itemId) - .patch({ - ...item, - }); - return updateItem; + + // Validates the given item existance on the storage. + const oldItem = await this.getItemOrThrowError(tenantId, itemId); + + if (itemDTO.categoryId) { + await this.validateItemCategoryExistance(tenantId, itemDTO.categoryId); + } + if (itemDTO.sellAccountId) { + await this.validateItemSellAccountExistance(tenantId, itemDTO.sellAccountId); + } + if (itemDTO.costAccountId) { + await this.validateItemCostAccountExistance(tenantId, itemDTO.costAccountId); + } + if (itemDTO.inventoryAccountId) { + await this.validateItemInventoryAccountExistance(tenantId, itemDTO.inventoryAccountId); + } + + const newItem = await Item.query().patchAndFetchById(itemId, { ...itemDTO }); + this.logger.info('[items] item edited successfully.', { tenantId, itemId, itemDTO }); + + return newItem; } - async deleteItem(tenantId: number, itemId: number) { + /** + * Delete the given item from the storage. + * @param {number} tenantId - Tenant id. + * @param {number} itemId - Item id. + * @return {Promise} + */ + public async deleteItem(tenantId: number, itemId: number) { const { Item } = this.tenancy.models(tenantId); - return Item.query() - .findById(itemId) - .delete(); + + this.logger.info('[items] trying to delete item.', { tenantId, itemId }); + await this.getItemOrThrowError(tenantId, itemId); + + await Item.query().findById(itemId).delete(); + this.logger.info('[items] deleted successfully.', { tenantId, itemId }); } /** @@ -43,46 +232,54 @@ export default class ItemsService { * @param {number} tenantId * @param {number} itemId */ - async getItemWithMetadata(tenantId: number, itemId: number) { + public async getItem(tenantId: number, itemId: number): Promise { const { Item } = this.tenancy.models(tenantId); - return Item.query() - .findById(itemId) - .withGraphFetched( - 'costAccount', - 'sellAccount', - 'inventoryAccount', - 'category' - ); + + const item = Item.query().findById(itemId) + .withGraphFetched('costAccount', 'sellAccount', 'inventoryAccount', 'category'); + + if (!item) { + throw new ServiceError(ERRORS.NOT_FOUND); + } + return item; } /** * Validates the given items IDs exists or not returns the not found ones. - * @param {Array} itemsIDs + * @param {Array} itemsIDs * @return {Array} */ - async isItemsIdsExists(tenantId: number, itemsIDs: number[]) { + private async validateItemsIdsExists(tenantId: number, itemsIDs: number[]) { const { Item } = this.tenancy.models(tenantId); const storedItems = await Item.query().whereIn('id', itemsIDs); const storedItemsIds = storedItems.map((t) => t.id); - const notFoundItemsIds = difference( - itemsIDs, - storedItemsIds, - ); + const notFoundItemsIds = difference(itemsIDs, storedItemsIds); return notFoundItemsIds; } - writeItemInventoryOpeningQuantity(tenantId: number, itemId: number, openingQuantity: number, averageCost: number) { - - } + /** + * Deletes items in bulk. + * @param {number} tenantId + * @param {number[]} itemsIds + */ + public async bulkDeleteItems(tenantId: number, itemsIds: number[]) { + const { Item } = this.tenancy.models(tenantId); + this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds }); + await this.validateItemsIdsExists(tenantId, itemsIds); + + await Item.query().whereIn('id', itemsIds).delete(); + this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds }); + } + /** * Retrieve items datatable list. * @param {number} tenantId * @param {IItemsFilter} itemsFilter */ - async getItemsList(tenantId: number, itemsFilter: IItemsFilter) { + public async itemsList(tenantId: number, itemsFilter: IItemsFilter) { const { Item } = this.tenancy.models(tenantId); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter);