mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: wip migrate server to nestjs
This commit is contained in:
120
packages/server-nest/src/modules/Items/CreateItem.service.ts
Normal file
120
packages/server-nest/src/modules/Items/CreateItem.service.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Knex } from 'knex';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { IItemDTO, IItemEventCreatedPayload } from '@/interfaces/Item';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { Item } from './models/Item';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class CreateItemService {
|
||||
/**
|
||||
* Constructor for the CreateItemService class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item creation events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {ItemsValidators} validators - Service for validating item data.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validators: ItemsValidators,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: typeof Item,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the creating item.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
async authorize(itemDTO: IItemDTO) {
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(itemDTO.name);
|
||||
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||
}
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
itemDTO.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
itemDTO.purchaseTaxRateId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the item DTO to model.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @return {IItem}
|
||||
*/
|
||||
private transformNewItemDTOToModel(itemDTO: IItemDTO) {
|
||||
return {
|
||||
...itemDTO,
|
||||
active: defaultTo(itemDTO.active, 1),
|
||||
quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
* @param {IItemDTO} itemDTO
|
||||
* @return {Promise<IItem>}
|
||||
*/
|
||||
public async createItem(
|
||||
itemDTO: IItemDTO,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<Item> {
|
||||
// Authorize the item before creating.
|
||||
await this.authorize(itemDTO);
|
||||
|
||||
// Creates a new item with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction<Item>(async (trx: Knex.Transaction) => {
|
||||
const itemInsert = this.transformNewItemDTOToModel(itemDTO);
|
||||
|
||||
// Inserts a new item and fetch the created item.
|
||||
const item = await this.itemModel.query(trx).insertAndFetch({
|
||||
...itemInsert,
|
||||
});
|
||||
// Triggers `onItemCreated` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onCreated, {
|
||||
item,
|
||||
itemId: item.id,
|
||||
trx,
|
||||
} as IItemEventCreatedPayload);
|
||||
|
||||
return item;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
67
packages/server-nest/src/modules/Items/DeleteItem.service.ts
Normal file
67
packages/server-nest/src/modules/Items/DeleteItem.service.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import {
|
||||
IItemEventDeletedPayload,
|
||||
IItemEventDeletingPayload,
|
||||
} from 'src/interfaces/Item';
|
||||
import { events } from 'src/common/events/events';
|
||||
import { Item } from './models/Item';
|
||||
import { ERRORS } from './Items.constants';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteItemService {
|
||||
/**
|
||||
* Constructor for the class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item deletion events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: typeof Item,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Delete the given item from the storage.
|
||||
* @param {number} itemId - Item id.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteItem(
|
||||
itemId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> {
|
||||
// Retrieve the given item or throw not found service error.
|
||||
const oldItem = await this.itemModel
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound()
|
||||
// @ts-expect-error
|
||||
.queryAndThrowIfHasRelations({
|
||||
type: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTIONS,
|
||||
});
|
||||
|
||||
// Delete item in unit of work.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onItemDeleting` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onDeleting, {
|
||||
trx,
|
||||
oldItem,
|
||||
} as IItemEventDeletingPayload);
|
||||
|
||||
// Deletes the item.
|
||||
await this.itemModel.query(trx).findById(itemId).delete();
|
||||
|
||||
// Triggers `onItemDeleted` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onDeleted, {
|
||||
itemId,
|
||||
oldItem,
|
||||
trx,
|
||||
} as IItemEventDeletedPayload);
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
143
packages/server-nest/src/modules/Items/EditItem.service.ts
Normal file
143
packages/server-nest/src/modules/Items/EditItem.service.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { IItemDTO, IItemEventEditedPayload } from 'src/interfaces/Item';
|
||||
import { events } from 'src/common/events/events';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { Item } from './models/Item';
|
||||
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
|
||||
@Injectable()
|
||||
export class EditItemService {
|
||||
/**
|
||||
* Constructor for the class.
|
||||
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item edit events.
|
||||
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||
* @param {ItemsValidators} validators - Service for validating item data.
|
||||
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly validators: ItemsValidators,
|
||||
|
||||
@Inject(Item.name)
|
||||
private readonly itemModel: typeof Item,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Authorize the editing item.
|
||||
* @param {IItemDTO} itemDTO
|
||||
* @param {Item} oldItem
|
||||
*/
|
||||
async authorize(itemDTO: IItemDTO, oldItem: Item) {
|
||||
// Validate edit item type from inventory type.
|
||||
this.validators.validateEditItemFromInventory(itemDTO, oldItem);
|
||||
|
||||
// Validate edit item type to inventory type.
|
||||
await this.validators.validateEditItemTypeToInventory(oldItem, itemDTO);
|
||||
|
||||
// Validate whether the given item name already exists on the storage.
|
||||
await this.validators.validateItemNameUniquiness(itemDTO.name, oldItem.id);
|
||||
|
||||
if (itemDTO.categoryId) {
|
||||
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||
}
|
||||
if (itemDTO.sellAccountId) {
|
||||
await this.validators.validateItemSellAccountExistance(
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the income account id existance if the item is sellable.
|
||||
this.validators.validateIncomeAccountExistance(
|
||||
itemDTO.sellable,
|
||||
itemDTO.sellAccountId,
|
||||
);
|
||||
if (itemDTO.costAccountId) {
|
||||
await this.validators.validateItemCostAccountExistance(
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
}
|
||||
// Validate the cost account id existance if the item is purchasable.
|
||||
this.validators.validateCostAccountExistance(
|
||||
itemDTO.purchasable,
|
||||
itemDTO.costAccountId,
|
||||
);
|
||||
if (itemDTO.inventoryAccountId) {
|
||||
await this.validators.validateItemInventoryAccountExistance(
|
||||
itemDTO.inventoryAccountId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.purchaseTaxRateId) {
|
||||
await this.validators.validatePurchaseTaxRateExistance(
|
||||
itemDTO.purchaseTaxRateId,
|
||||
);
|
||||
}
|
||||
if (itemDTO.sellTaxRateId) {
|
||||
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the edit item DTO to model.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @param {Item} oldItem - Old item.
|
||||
* @return {Partial<Item>}
|
||||
*/
|
||||
private transformEditItemDTOToModel(
|
||||
itemDTO: IItemDTO,
|
||||
oldItem: Item,
|
||||
): Partial<Item> {
|
||||
return {
|
||||
...itemDTO,
|
||||
...(itemDTO.type === 'inventory' && oldItem.type !== 'inventory'
|
||||
? {
|
||||
quantityOnHand: 0,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the item metadata.
|
||||
* @param {number} itemId
|
||||
* @param {IItemDTO} itemDTO
|
||||
*/
|
||||
public async editItem(
|
||||
itemId: number,
|
||||
itemDTO: IItemDTO,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<Item> {
|
||||
// Validates the given item existance on the storage.
|
||||
const oldItem = await this.itemModel
|
||||
.query()
|
||||
.findById(itemId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize before editing item.
|
||||
await this.authorize(itemDTO, oldItem);
|
||||
|
||||
// Transform the edit item DTO to model.
|
||||
const itemModel = this.transformEditItemDTOToModel(itemDTO, oldItem);
|
||||
|
||||
// Edits the item with associated transactions under unit-of-work environment.
|
||||
return this.uow.withTransaction<Item>(async (trx: Knex.Transaction) => {
|
||||
// Updates the item on the storage and fetches the updated one.
|
||||
const newItem = await this.itemModel
|
||||
.query(trx)
|
||||
.patchAndFetchById(itemId, itemModel);
|
||||
|
||||
// Edit event payload.
|
||||
const eventPayload: IItemEventEditedPayload = {
|
||||
item: newItem,
|
||||
oldItem,
|
||||
itemId: newItem.id,
|
||||
trx,
|
||||
};
|
||||
// Triggers `onItemEdited` event.
|
||||
await this.eventEmitter.emitAsync(events.item.onEdited, eventPayload);
|
||||
|
||||
return newItem;
|
||||
}, trx);
|
||||
}
|
||||
}
|
||||
44
packages/server-nest/src/modules/Items/Item.controller.ts
Normal file
44
packages/server-nest/src/modules/Items/Item.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Param,
|
||||
Post,
|
||||
UsePipes,
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ZodValidationPipe } from 'src/common/pipes/ZodValidation.pipe';
|
||||
import { createItemSchema } from './Item.schema';
|
||||
import { CreateItemService } from './CreateItem.service';
|
||||
import { Item } from './models/Item';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
import { TenantController } from '../Tenancy/Tenant.controller';
|
||||
import { SubscriptionGuard } from '../Subscription/interceptors/Subscription.guard';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { PublicRoute } from '../Auth/Jwt.guard';
|
||||
|
||||
@Controller('/items')
|
||||
@UseGuards(SubscriptionGuard)
|
||||
@PublicRoute()
|
||||
export class ItemsController extends TenantController {
|
||||
constructor(
|
||||
private readonly createItemService: CreateItemService,
|
||||
private readonly deleteItemService: DeleteItemService,
|
||||
private readonly cls: ClsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UsePipes(new ZodValidationPipe(createItemSchema))
|
||||
async createItem(@Body() createItemDto: any): Promise<Item> {
|
||||
return this.createItemService.createItem(createItemDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteItem(@Param('id') id: string): Promise<void> {
|
||||
const itemId = parseInt(id, 10);
|
||||
return this.deleteItemService.deleteItem(itemId);
|
||||
}
|
||||
}
|
||||
111
packages/server-nest/src/modules/Items/Item.schema.ts
Normal file
111
packages/server-nest/src/modules/Items/Item.schema.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { DATATYPES_LENGTH } from 'src/constants/data-types';
|
||||
import z from 'zod';
|
||||
|
||||
export const createItemSchema = z
|
||||
.object({
|
||||
name: z.string().max(DATATYPES_LENGTH.STRING),
|
||||
type: z.enum(['service', 'non-inventory', 'inventory']),
|
||||
code: z.string().max(DATATYPES_LENGTH.STRING).nullable().optional(),
|
||||
purchasable: z.boolean().optional(),
|
||||
cost_price: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||
.nullable()
|
||||
.optional(),
|
||||
cost_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sellable: z.boolean().optional(),
|
||||
sell_price: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
inventory_account_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_description: z
|
||||
.string()
|
||||
.max(DATATYPES_LENGTH.TEXT)
|
||||
.nullable()
|
||||
.optional(),
|
||||
purchase_description: z
|
||||
.string()
|
||||
.max(DATATYPES_LENGTH.TEXT)
|
||||
.nullable()
|
||||
.optional(),
|
||||
sell_tax_rate_id: z.number().int().nullable().optional(),
|
||||
purchase_tax_rate_id: z.number().int().nullable().optional(),
|
||||
category_id: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.optional(),
|
||||
note: z.string().max(DATATYPES_LENGTH.TEXT).optional(),
|
||||
active: z.boolean().optional(),
|
||||
media_ids: z.array(z.number().int()).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.purchasable) {
|
||||
return (
|
||||
data.cost_price !== undefined && data.cost_account_id !== undefined
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Cost price and cost account ID are required when item is purchasable',
|
||||
path: ['cost_price', 'cost_account_id'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.sellable) {
|
||||
return (
|
||||
data.sell_price !== undefined && data.sell_account_id !== undefined
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
'Sell price and sell account ID are required when item is sellable',
|
||||
path: ['sell_price', 'sell_account_id'],
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.type === 'inventory') {
|
||||
return data.inventory_account_id !== undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Inventory account ID is required for inventory items',
|
||||
path: ['inventory_account_id'],
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
export type createItemDTO = z.infer<typeof createItemSchema>;
|
||||
285
packages/server-nest/src/modules/Items/ItemValidator.service.ts
Normal file
285
packages/server-nest/src/modules/Items/ItemValidator.service.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ACCOUNT_PARENT_TYPE,
|
||||
ACCOUNT_ROOT_TYPE,
|
||||
ACCOUNT_TYPE,
|
||||
} from 'src/constants/accounts';
|
||||
import { ServiceError } from './ServiceError';
|
||||
import { IItem, IItemDTO } from 'src/interfaces/Item';
|
||||
import { ERRORS } from './Items.constants';
|
||||
import { Item } from './models/Item';
|
||||
import { Account } from '../Accounts/models/Account';
|
||||
|
||||
@Injectable()
|
||||
export class ItemsValidators {
|
||||
constructor(
|
||||
@Inject(Item.name) private itemModel: typeof Item,
|
||||
@Inject(Account.name) private accountModel: typeof Account,
|
||||
@Inject(Item.name) private taxRateModel: typeof Item,
|
||||
@Inject(Item.name) private itemEntryModel: typeof Item,
|
||||
@Inject(Item.name) private itemCategoryModel: typeof Item,
|
||||
@Inject(Item.name) private accountTransactionModel: typeof Item,
|
||||
@Inject(Item.name) private inventoryAdjustmentEntryModel: typeof Item,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate wether the given item name already exists on the storage.
|
||||
* @param {string} itemName
|
||||
* @param {number} notItemId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemNameUniquiness(
|
||||
itemName: string,
|
||||
notItemId?: number,
|
||||
): Promise<void> {
|
||||
const foundItems = await this.itemModel.query().onBuild((builder: any) => {
|
||||
builder.where('name', itemName);
|
||||
if (notItemId) {
|
||||
builder.whereNot('id', notItemId);
|
||||
}
|
||||
});
|
||||
|
||||
if (foundItems.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.ITEM_NAME_EXISTS,
|
||||
'The item name is already exist.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item COGS account existance and type.
|
||||
* @param {number} costAccountId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async validateItemCostAccountExistance(
|
||||
costAccountId: number,
|
||||
): Promise<void> {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(costAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD);
|
||||
|
||||
// Detarmines the cost of goods sold account.
|
||||
} else if (!foundAccount.isParentType(ACCOUNT_PARENT_TYPE.EXPENSE)) {
|
||||
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item sell account existance and type.
|
||||
* @param {number} sellAccountId - Sell account id.
|
||||
*/
|
||||
public async validateItemSellAccountExistance(sellAccountId: number) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isParentType(ACCOUNT_ROOT_TYPE.INCOME)) {
|
||||
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates income account existance.
|
||||
* @param {number|null} sellable - Detarmines if the item sellable.
|
||||
* @param {number|null} incomeAccountId - Income account id.
|
||||
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
|
||||
*/
|
||||
public validateIncomeAccountExistance(
|
||||
sellable?: boolean,
|
||||
incomeAccountId?: number,
|
||||
) {
|
||||
if (sellable && !incomeAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
|
||||
'Income account is require with sellable item.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the cost account existance.
|
||||
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
|
||||
* @param {number|null} costAccountId - Cost account id.
|
||||
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
|
||||
*/
|
||||
public validateCostAccountExistance(
|
||||
purchasable: boolean,
|
||||
costAccountId?: number,
|
||||
) {
|
||||
if (purchasable && !costAccountId) {
|
||||
throw new ServiceError(
|
||||
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
|
||||
'The cost account is required with purchasable item.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item inventory account existance and type.
|
||||
* @param {number} inventoryAccountId
|
||||
*/
|
||||
public async validateItemInventoryAccountExistance(
|
||||
inventoryAccountId: number,
|
||||
) {
|
||||
const foundAccount = await this.accountModel
|
||||
.query()
|
||||
.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND);
|
||||
} else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item category existance.
|
||||
* @param {number} itemCategoryId
|
||||
*/
|
||||
public async validateItemCategoryExistance(itemCategoryId: number) {
|
||||
const foundCategory = await this.itemCategoryModel
|
||||
.query()
|
||||
.findById(itemCategoryId);
|
||||
|
||||
if (!foundCategory) {
|
||||
throw new ServiceError(ERRORS.ITEM_CATEOGRY_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given item or items have no associated invoices or bills.
|
||||
* @param {number|number[]} itemId - Item id.
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateHasNoInvoicesOrBills(itemId: number[] | number) {
|
||||
const ids = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const foundItemEntries = await this.itemEntryModel
|
||||
.query()
|
||||
.whereIn('item_id', ids);
|
||||
|
||||
if (foundItemEntries.length > 0) {
|
||||
throw new ServiceError(
|
||||
ids.length > 1
|
||||
? ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS
|
||||
: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given item has no associated inventory adjustment transactions.
|
||||
* @param {number} itemId -
|
||||
*/
|
||||
public async validateHasNoInventoryAdjustments(
|
||||
itemId: number[] | number,
|
||||
): Promise<void> {
|
||||
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
|
||||
const inventoryAdjEntries = await this.inventoryAdjustmentEntryModel
|
||||
.query()
|
||||
.whereIn('item_id', itemsIds);
|
||||
|
||||
if (inventoryAdjEntries.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates edit item type from service/non-inventory to inventory.
|
||||
* Should item has no any relations with accounts transactions.
|
||||
* @param {number} itemId - Item id.
|
||||
*/
|
||||
public async validateEditItemTypeToInventory(
|
||||
oldItem: Item,
|
||||
newItemDTO: IItemDTO,
|
||||
) {
|
||||
// We have no problem in case the item type not modified.
|
||||
if (newItemDTO.type === oldItem.type || oldItem.type === 'inventory') {
|
||||
return;
|
||||
}
|
||||
// Retrieve all transactions that associated to the given item id.
|
||||
const itemTransactionsCount = await this.accountTransactionModel
|
||||
.query()
|
||||
.where('item_id', oldItem.id)
|
||||
.count('item_id', { as: 'transactions' })
|
||||
.first();
|
||||
|
||||
// @ts-ignore
|
||||
if (itemTransactionsCount.transactions > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the item inventory account whether modified and item
|
||||
* has associated inventory transactions.
|
||||
* @param {Item} oldItem
|
||||
* @param {IItemDTO} newItemDTO
|
||||
* @returns
|
||||
*/
|
||||
async validateItemInvnetoryAccountModified(
|
||||
oldItem: Item,
|
||||
newItemDTO: IItemDTO,
|
||||
) {
|
||||
if (
|
||||
newItemDTO.type !== 'inventory' ||
|
||||
oldItem.inventoryAccountId === newItemDTO.inventoryAccountId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Inventory transactions associated to the given item id.
|
||||
const transactions = await this.accountTransactionModel.query().where({
|
||||
itemId: oldItem.id,
|
||||
});
|
||||
// Throw the service error in case item has associated inventory transactions.
|
||||
if (transactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_CANNOT_MODIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate edit item type from inventory to another type that not allowed.
|
||||
* @param {IItemDTO} itemDTO - Item DTO.
|
||||
* @param {IItem} oldItem - Old item.
|
||||
*/
|
||||
public validateEditItemFromInventory(itemDTO: IItemDTO, oldItem: Item) {
|
||||
if (
|
||||
itemDTO.type &&
|
||||
oldItem.type === 'inventory' &&
|
||||
itemDTO.type !== oldItem.type
|
||||
) {
|
||||
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the purchase tax rate id existance.
|
||||
* @param {number} taxRateId -
|
||||
*/
|
||||
public async validatePurchaseTaxRateExistance(taxRateId: number) {
|
||||
const foundTaxRate = await this.taxRateModel.query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the sell tax rate id existance.
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public async validateSellTaxRateExistance(taxRateId: number) {
|
||||
const foundTaxRate = await this.taxRateModel.query().findById(taxRateId);
|
||||
|
||||
if (!foundTaxRate) {
|
||||
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
packages/server-nest/src/modules/Items/Items.constants.ts
Normal file
141
packages/server-nest/src/modules/Items/Items.constants.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
export const ERRORS = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
ITEMS_NOT_FOUND: 'ITEMS_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',
|
||||
|
||||
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
||||
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
|
||||
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
|
||||
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
|
||||
|
||||
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
|
||||
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
|
||||
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
|
||||
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'Services',
|
||||
slug: 'services',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'service' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Inventory',
|
||||
slug: 'inventory',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'inventory' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'Non Inventory',
|
||||
slug: 'non-inventory',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'type',
|
||||
comparator: 'equals',
|
||||
value: 'non-inventory',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const ItemsSampleData = [
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
'Item Code': '1000',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'At dolor est non tempore et quisquam.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Schmitt Group',
|
||||
'Item Code': '1001',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Marks - Carroll',
|
||||
'Item Code': '1002',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Odio odio minus similique.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'VonRueden, Ruecker and Hettinger',
|
||||
'Item Code': '1003',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Quibusdam dolores illo.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
];
|
||||
19
packages/server-nest/src/modules/Items/Items.module.ts
Normal file
19
packages/server-nest/src/modules/Items/Items.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ItemsController } from './Item.controller';
|
||||
import { CreateItemService } from './CreateItem.service';
|
||||
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||
import { ItemsValidators } from './ItemValidator.service';
|
||||
import { DeleteItemService } from './DeleteItem.service';
|
||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||
|
||||
@Module({
|
||||
imports: [TenancyDatabaseModule],
|
||||
controllers: [ItemsController],
|
||||
providers: [
|
||||
ItemsValidators,
|
||||
CreateItemService,
|
||||
DeleteItemService,
|
||||
TenancyContext,
|
||||
],
|
||||
})
|
||||
export class ItemsModule {}
|
||||
13
packages/server-nest/src/modules/Items/ServiceError.ts
Normal file
13
packages/server-nest/src/modules/Items/ServiceError.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
export class ServiceError {
|
||||
errorType: string;
|
||||
message: string;
|
||||
payload: any;
|
||||
|
||||
constructor(errorType: string, message?: string, payload?: any) {
|
||||
this.errorType = errorType;
|
||||
this.message = message || null;
|
||||
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class ItemCreatedEvent {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { ItemCreatedEvent } from '../events/ItemCreated.event';
|
||||
|
||||
@Injectable()
|
||||
export class ItemCreatedListener {
|
||||
|
||||
@OnEvent('order.created')
|
||||
handleItemCreatedEvent(event: ItemCreatedEvent) {
|
||||
// handle and process "OrderCreatedEvent" event
|
||||
console.log(event);
|
||||
}
|
||||
}
|
||||
26
packages/server-nest/src/modules/Items/models/Item.ts
Normal file
26
packages/server-nest/src/modules/Items/models/Item.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as F from 'fp-ts/function';
|
||||
import * as R from 'ramda';
|
||||
import { SearchableModel } from '@/modules/Search/SearchableMdel';
|
||||
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||
|
||||
const Extend = R.compose(SearchableModel)(TenantModel);
|
||||
|
||||
export class Item extends Extend {
|
||||
public readonly quantityOnHand: number;
|
||||
public readonly name: string;
|
||||
public readonly active: boolean;
|
||||
public readonly type: string;
|
||||
public readonly code: string;
|
||||
public readonly sellable: boolean;
|
||||
public readonly purchasable: boolean;
|
||||
public readonly costPrice: number;
|
||||
public readonly sellPrice: number;
|
||||
public readonly currencyCode: string;
|
||||
public readonly costAccountId: number;
|
||||
public readonly inventoryAccountId: number;
|
||||
public readonly categoryId: number;
|
||||
|
||||
static get tableName() {
|
||||
return 'items';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user