feat: wip migrate server to nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-11-12 23:08:51 +02:00
parent f5834c72c6
commit 19080a67ab
94 changed files with 7587 additions and 98 deletions

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>;

View 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);
}
}
}

View 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',
},
];

View 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 {}

View 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;
}
}

View File

@@ -0,0 +1,4 @@
export class ItemCreatedEvent {
name: string;
description: string;
}

View File

@@ -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);
}
}

View 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';
}
}