mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
refactor: item categories service.
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
} from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import { Service } from 'typedi';
|
||||
import ItemCategoriesService from 'services/ItemCategories/ItemCategoriesService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import validateMiddleware from 'api/middleware/validateMiddleware';
|
||||
import { IItemCategory, IItemCategoryOTD } from 'interfaces';
|
||||
import { IItemCategoryOTD } from 'interfaces';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import BaseController from 'api/controllers/BaseController';
|
||||
|
||||
@Service()
|
||||
export default class ItemsCategoriesController extends BaseController {
|
||||
@Inject()
|
||||
itemCategoriesService: ItemCategoriesService;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
@@ -24,43 +28,43 @@ export default class ItemsCategoriesController extends BaseController {
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
asyncMiddleware(this.validateInventoryAccountExistance),
|
||||
asyncMiddleware(this.editCategory)
|
||||
asyncMiddleware(this.editCategory.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
router.post('/',
|
||||
this.categoryValidationSchema,
|
||||
router.post('/', [
|
||||
...this.categoryValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateParentCategoryExistance),
|
||||
asyncMiddleware(this.validateSellAccountExistance),
|
||||
asyncMiddleware(this.validateCostAccountExistance),
|
||||
asyncMiddleware(this.validateInventoryAccountExistance),
|
||||
asyncMiddleware(this.newCategory),
|
||||
asyncMiddleware(this.newCategory.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
router.delete('/bulk',
|
||||
this.categoriesBulkValidationSchema,
|
||||
router.delete('/', [
|
||||
...this.categoriesBulkValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateCategoriesIdsExistance),
|
||||
asyncMiddleware(this.bulkDeleteCategories),
|
||||
asyncMiddleware(this.bulkDeleteCategories.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
router.delete('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
router.delete('/:id', [
|
||||
...this.specificCategoryValidationSchema
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.deleteItem),
|
||||
asyncMiddleware(this.deleteItem.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
router.get('/:id',
|
||||
this.specificCategoryValidationSchema,
|
||||
router.get('/:id', [
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.validateItemCategoryExistance),
|
||||
asyncMiddleware(this.getCategory)
|
||||
asyncMiddleware(this.getCategory.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
router.get('/',
|
||||
this.categoriesListValidationSchema,
|
||||
router.get('/', [
|
||||
...this.categoriesListValidationSchema
|
||||
],
|
||||
validateMiddleware,
|
||||
asyncMiddleware(this.getList)
|
||||
asyncMiddleware(this.getList.bind(this)),
|
||||
this.handlerServiceError,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
@@ -122,242 +126,168 @@ export default class ItemsCategoriesController extends BaseController {
|
||||
return [
|
||||
param('id').exists().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the item category existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async validateItemCategoryExistance(req: Request, res: Response, next: Function) {
|
||||
const categoryId: number = req.params.id;
|
||||
const { ItemCategory } = req.models;
|
||||
const category = await ItemCategory.query().findById(categoryId);
|
||||
|
||||
if (!category) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
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 category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.costAccountId) {
|
||||
const COGSType = await AccountType.query().findOne('key', 'cost_of_goods_sold');
|
||||
const foundAccount = await Account.query().findById(category.costAccountId)
|
||||
|
||||
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 category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.sellAccountId) {
|
||||
const incomeType = await AccountType.query().findOne('key', 'income');
|
||||
const foundAccount = await Account.query().findById(category.sellAccountId);
|
||||
|
||||
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 category: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
if (category.inventoryAccountId) {
|
||||
const otherAsset = await AccountType.query().findOne('key', 'other_asset');
|
||||
const foundAccount = await Account.query().findById(category.inventoryAccountId);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the item category parent category whether exists on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateParentCategoryExistance(req: Request, res: Response, next: Function) {
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
if (category.parentCategoryId) {
|
||||
const foundParentCategory = await ItemCategory.query()
|
||||
.where('id', category.parentCategoryId)
|
||||
.first();
|
||||
|
||||
if (!foundParentCategory) {
|
||||
return res.boom.notFound('The parent category ID is not found.', {
|
||||
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item categories ids existance.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateCategoriesIdsExistance(req: Request, res: Response, next: Function) {
|
||||
const ids: number[] = (req.query?.ids || []);
|
||||
const { ItemCategory } = req.models;
|
||||
|
||||
const itemCategories = await ItemCategory.query().whereIn('id', ids);
|
||||
const itemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id);
|
||||
const notFoundCategories = difference(ids, itemCategoriesIds);
|
||||
|
||||
if (notFoundCategories.length > 0) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.CATEGORIES.IDS.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item category.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newCategory(req: Request, res: Response) {
|
||||
const { user } = req;
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
async newCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { user, tenantId } = req;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
const storedCategory = await ItemCategory.query().insert({
|
||||
...category,
|
||||
user_id: user.id,
|
||||
});
|
||||
return res.status(200).send({ category: storedCategory });
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.newItemCategory(tenantId, itemCategoryOTD, user);
|
||||
return res.status(200).send({ id: itemCategory.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const { id } = req.params;
|
||||
const category: IItemCategory = this.matchedBodyData(req);
|
||||
const { ItemCategory } = req.models;
|
||||
async editCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
const updateItemCategory = await ItemCategory.query()
|
||||
.where('id', id)
|
||||
.update({ ...category });
|
||||
|
||||
return res.status(200).send({ id });
|
||||
try {
|
||||
await this.itemCategoriesService.editItemCategory(tenantId, itemCategoryId, itemCategoryOTD, user);
|
||||
return res.status(200).send({ id: itemCategoryId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const { id } = req.params;
|
||||
const { ItemCategory } = req.models;
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
await ItemCategory.query()
|
||||
.where('id', id)
|
||||
.delete();
|
||||
|
||||
return res.status(200).send({ id });
|
||||
try {
|
||||
await this.itemCategoriesService.deleteItemCategory(tenantId, itemCategoryId, user);
|
||||
return res.status(200).send({ id: itemCategoryId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
async getList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const itemCategoriesFilter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const itemCategories = await this.itemCategoriesService.getItemCategoriesList(
|
||||
tenantId, itemCategoriesFilter, user,
|
||||
);
|
||||
return res.status(200).send({ item_categories: itemCategories });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
async getCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const itemCategoryId: number = req.params.id;
|
||||
const { ItemCategory } = req.models;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
const itemCategory = await ItemCategory.query().findById(itemCategoryId);
|
||||
|
||||
return res.status(200).send({ category: itemCategory });
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.getItemCategory(tenantId, itemCategoryId, user);
|
||||
return res.status(200).send({ category: itemCategory });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const ids = req.query.ids;
|
||||
const { ItemCategory } = req.models;
|
||||
async bulkDeleteCategories(req: Request, res: Response, next: NextFunction) {
|
||||
const itemCategoriesIds = req.query.ids;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
await ItemCategory.query().whereIn('id', ids).delete();
|
||||
try {
|
||||
await this.itemCategoriesService.deleteItemCategories(tenantId, itemCategoriesIds, user);
|
||||
return res.status(200).send({ ids: itemCategoriesIds });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send({ ids: filter.ids });
|
||||
/**
|
||||
* Handles service error.
|
||||
* @param {Error} error
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CATEGORY_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CATEGORIES_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORIES_NOT_FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
|
||||
|
||||
import { IDynamicListFilterDTO } from "./DynamicFilter";
|
||||
import { ISystemUser } from "./User";
|
||||
|
||||
export interface IItemCategory {
|
||||
id: number,
|
||||
name: string,
|
||||
|
||||
parentCategoryId?: number,
|
||||
@@ -27,4 +28,19 @@ export interface IItemCategoryOTD {
|
||||
inventoryAccountId?: number,
|
||||
|
||||
costMethod?: string,
|
||||
};
|
||||
};
|
||||
|
||||
export interface IItemCategoriesFilter extends IDynamicListFilterDTO {
|
||||
stringifiedFilterRoles?: string,
|
||||
}
|
||||
|
||||
export interface IItemCategoriesService {
|
||||
newItemCategory(tenantId: number, itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser): Promise<IItemCategory>;
|
||||
editItemCategory(tenantId: number, itemCategoryId: number, itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser): Promise<IItemCategory>;
|
||||
|
||||
deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser): Promise<void>;
|
||||
deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser): Promise<void>;
|
||||
|
||||
getItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser): Promise<void>;
|
||||
getItemCategoriesList(tenantId: number, itemCategoriesFilter: IItemCategoriesFilter, authorizedUser: ISystemUser): Promise<void>;
|
||||
}
|
||||
254
server/src/services/ItemCategories/ItemCategoriesService.ts
Normal file
254
server/src/services/ItemCategories/ItemCategoriesService.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { difference } from 'lodash';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import {
|
||||
IItemCategory,
|
||||
IItemCategoryOTD,
|
||||
IItemCategoriesService,
|
||||
IItemCategoriesFilter,
|
||||
ISystemUser,
|
||||
} from "interfaces";
|
||||
import ItemCategory from "models/ItemCategory";
|
||||
import DynamicListingService from 'services/DynamicListing/DynamicListService';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
|
||||
const ERRORS = {
|
||||
ITEM_CATEGORIES_NOT_FOUND: 'ITEM_CATEGORIES_NOT_FOUND',
|
||||
CATEGORY_NOT_FOUND: 'CATEGORY_NOT_FOUND',
|
||||
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
|
||||
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
|
||||
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
|
||||
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
};
|
||||
|
||||
export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Retrieve item category or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemCategoryId
|
||||
*/
|
||||
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)
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms OTD to model object.
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @param {ISystemUser} authorizedUser
|
||||
*/
|
||||
private transformOTDToObject(itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser) {
|
||||
return { ...itemCategoryOTD, userId: authorizedUser.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item category of the given id.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} itemCategoryId -
|
||||
* @returns {IItemCategory}
|
||||
*/
|
||||
public async getItemCategory(tenantId: number, itemCategoryId: number, user: ISystemUser) {
|
||||
return this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new item category.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async newItemCategory(tenantId: number, itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser): Promise<IItemCategory> {
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
this.logger.info('[item_category] trying to insert a new item category.', { tenantId });
|
||||
|
||||
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);
|
||||
}
|
||||
if (itemCategoryOTD.parentCategoryId) {
|
||||
await this.getItemCategoryOrThrowError(tenantId, itemCategoryOTD.parentCategoryId);
|
||||
}
|
||||
|
||||
const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser);
|
||||
const itemCategory = await ItemCategory.query().insert({ ...itemCategoryObj });
|
||||
this.logger.info('[item_category] item category inserted successfully.', { tenantId, itemCategoryOTD });
|
||||
|
||||
return itemCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates sell account existance and type.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} sellAccountId - Sell account id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
private async validateSellAccount(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates COGS account existance and type.
|
||||
* @param {number} tenantId -
|
||||
* @param {number} costAccountId -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
private async validateCostAccount(tenantId: number, costAccountId: number) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates inventory account existance and type.
|
||||
* @param {number} tenantId
|
||||
* @param {number} inventoryAccountId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
private async validateInventoryAccount(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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits item category.
|
||||
* @param {number} tenantId
|
||||
* @param {number} itemCategoryId
|
||||
* @param {IItemCategoryOTD} itemCategoryOTD
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async editItemCategory(tenantId: number, itemCategoryId: number, itemCategoryOTD: IItemCategoryOTD, authorizedUser: ISystemUser): Promise<IItemCategory> {
|
||||
const oldItemCategory = await this.getItemCategoryOrThrowError(tenantId, 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);
|
||||
}
|
||||
if (itemCategoryOTD.parentCategoryId) {
|
||||
await this.getItemCategoryOrThrowError(tenantId, itemCategoryOTD.parentCategoryId);
|
||||
}
|
||||
|
||||
const itemCategoryObj = this.transformOTDToObject(itemCategoryOTD, authorizedUser);
|
||||
const itemCategory = await ItemCategory.query().patchAndFetchById(itemCategoryId, { ...itemCategoryObj });
|
||||
this.logger.info('[item_category] edited successfully.', { tenantId, itemCategoryId, itemCategoryOTD });
|
||||
|
||||
return itemCategory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given item category.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} itemCategoryId - Item category id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) {
|
||||
this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId });
|
||||
await this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
|
||||
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
await ItemCategory.query().findById(itemCategoryId).delete();
|
||||
this.logger.info('[item_category] deleted successfully.', { tenantId, itemCategoryId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item categories or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} itemCategoriesIds
|
||||
*/
|
||||
private async getItemCategoriesOrThrowError(tenantId: number, itemCategoriesIds: number[]) {
|
||||
const itemCategories = await ItemCategory.query().whereIn('id', ids);
|
||||
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
|
||||
*/
|
||||
public async getItemCategoriesList(tenantId: number, filter: IItemCategoriesFilter, authorizedUser: ISystemUser) {
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter);
|
||||
|
||||
const itemCategories = await ItemCategory.query().onBuild((query) => {
|
||||
query.orderBy('createdAt', 'ASC');
|
||||
dynamicList.buildQuery()(query);
|
||||
});
|
||||
return itemCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes item categories in bulk.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} itemCategoriesIds
|
||||
*/
|
||||
public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) {
|
||||
this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds });
|
||||
await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
|
||||
|
||||
await ItemCategory.query().whereIn('id', itemCategoriesIds).delete();
|
||||
this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user