add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
@Service()
export class ActivateItem {
@Inject()
tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Activates the given item on the storage.
* @param {number} tenantId -
* @param {number} itemId -
* @return {Promise<void>}
*/
public async activateItem(tenantId: number, itemId: number): Promise<void> {
const { Item } = this.tenancy.models(tenantId);
// Retreives the given item or throw not found error.
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
// Activate the given item with associated transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Mutate item on the storage.
await Item.query(trx).findById(itemId).patch({ active: true });
// Triggers `onItemActivated` event.
await this.eventPublisher.emitAsync(events.item.onActivated, {});
});
}
}

View File

@@ -0,0 +1,106 @@
import { Knex } from 'knex';
import { defaultTo } from 'lodash';
import { Service, Inject } from 'typedi';
import { IItem, IItemDTO, IItemEventCreatedPayload } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { ItemsValidators } from './ItemValidators';
@Service()
export class CreateItem {
@Inject()
private validators: ItemsValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Authorize the creating item.
* @param {number} tenantId
* @param {IItemDTO} itemDTO
*/
async authorize(tenantId: number, itemDTO: IItemDTO) {
// Validate whether the given item name already exists on the storage.
await this.validators.validateItemNameUniquiness(tenantId, itemDTO.name);
if (itemDTO.categoryId) {
await this.validators.validateItemCategoryExistance(
tenantId,
itemDTO.categoryId
);
}
if (itemDTO.sellAccountId) {
await this.validators.validateItemSellAccountExistance(
tenantId,
itemDTO.sellAccountId
);
}
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(
tenantId,
itemDTO.costAccountId
);
}
if (itemDTO.inventoryAccountId) {
await this.validators.validateItemInventoryAccountExistance(
tenantId,
itemDTO.inventoryAccountId
);
}
}
/**
* 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 {number} tenantId DTO
* @param {IItemDTO} item
* @return {Promise<IItem>}
*/
public async createItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem> {
const { Item } = this.tenancy.models(tenantId);
// Authorize the item before creating.
await this.authorize(tenantId, itemDTO);
// Creates a new item with associated transactions under unit-of-work envirement.
const item = this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Inserts a new item and fetch the created item.
const item = await Item.query(trx).insertAndFetch({
...this.transformNewItemDTOToModel(itemDTO),
});
// Triggers `onItemCreated` event.
await this.eventPublisher.emitAsync(events.item.onCreated, {
tenantId,
item,
itemId: item.id,
trx,
} as IItemEventCreatedPayload);
return item;
}
);
return item;
}
}

View File

@@ -0,0 +1,66 @@
import { Service, Inject } from 'typedi';
import {
IItemEventDeletedPayload,
IItemEventDeletingPayload,
} from '@/interfaces';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import { ERRORS } from './constants';
import { ItemsValidators } from './ItemValidators';
@Service()
export class DeleteItem {
@Inject()
private validators: ItemsValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Delete the given item from the storage.
* @param {number} tenantId - Tenant id.
* @param {number} itemId - Item id.
* @return {Promise<void>}
*/
public async deleteItem(tenantId: number, itemId: number) {
const { Item } = this.tenancy.models(tenantId);
// Retreive the given item or throw not found service error.
const oldItem = await Item.query()
.findById(itemId)
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTIONS,
});
// Delete item in unit of work.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onItemDeleting` event.
await this.eventPublisher.emitAsync(events.item.onDeleting, {
tenantId,
trx,
oldItem,
} as IItemEventDeletingPayload);
// Deletes the item.
await Item.query(trx).findById(itemId).delete();
const eventPayload: IItemEventDeletedPayload = {
tenantId,
oldItem,
itemId,
trx,
};
// Triggers `onItemDeleted` event.
await this.eventPublisher.emitAsync(events.item.onDeleted, eventPayload);
});
}
}

View File

@@ -0,0 +1,147 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IItem,
IItemDTO,
IItemEditDTO,
IItemEventEditedPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { ItemsValidators } from './ItemValidators';
import events from '@/subscribers/events';
@Service()
export class EditItem {
@Inject()
private validators: ItemsValidators;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Authorize the editing item.
* @param {number} tenantId
* @param {IItemEditDTO} itemDTO
* @param {IItem} oldItem
*/
async authorize(tenantId: number, itemDTO: IItemEditDTO, oldItem: IItem) {
// Validate edit item type from inventory type.
this.validators.validateEditItemFromInventory(itemDTO, oldItem);
// Validate edit item type to inventory type.
await this.validators.validateEditItemTypeToInventory(
tenantId,
oldItem,
itemDTO
);
// Validate whether the given item name already exists on the storage.
await this.validators.validateItemNameUniquiness(
tenantId,
itemDTO.name,
oldItem.id
);
// Validate the item category existance on the storage,
if (itemDTO.categoryId) {
await this.validators.validateItemCategoryExistance(
tenantId,
itemDTO.categoryId
);
}
// Validate the sell account existance on the storage.
if (itemDTO.sellAccountId) {
await this.validators.validateItemSellAccountExistance(
tenantId,
itemDTO.sellAccountId
);
}
// Validate the cost account existance on the storage.
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(
tenantId,
itemDTO.costAccountId
);
}
// Validate the inventory account existance onthe storage.
if (itemDTO.inventoryAccountId) {
await this.validators.validateItemInventoryAccountExistance(
tenantId,
itemDTO.inventoryAccountId
);
}
// Validate inventory account should be modified in inventory item
// has inventory transactions.
await this.validators.validateItemInvnetoryAccountModified(
tenantId,
oldItem,
itemDTO
);
}
/**
* Transformes edit item DTO to model.
* @param {IItemDTO} itemDTO - Item DTO.
* @param {IItem} oldItem -
*/
private transformEditItemDTOToModel(itemDTO: IItemDTO, oldItem: IItem) {
return {
...itemDTO,
...(itemDTO.type === 'inventory' && oldItem.type !== 'inventory'
? {
quantityOnHand: 0,
}
: {}),
};
}
/**
* 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);
// Validates the given item existance on the storage.
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
// Authorize before editing item.
await this.authorize(tenantId, 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 envirement.
const newItem = this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Updates the item on the storage and fetches the updated once.
const newItem = await Item.query(trx).patchAndFetchById(itemId, {
...itemModel,
});
// Edit event payload.
const eventPayload: IItemEventEditedPayload = {
tenantId,
item: newItem,
oldItem,
itemId: newItem.id,
trx,
};
// Triggers `onItemEdited` event.
await this.eventPublisher.emitAsync(events.item.onEdited, eventPayload);
return newItem;
}
);
return newItem;
}
}

View File

@@ -0,0 +1,34 @@
import { Inject } from 'typedi';
import { IItem } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import ItemTransformer from './ItemTransformer';
@Inject()
export class GetItem {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the item details of the given id with associated details.
* @param {number} tenantId
* @param {number} itemId
*/
public async getItem(tenantId: number, itemId: number): Promise<IItem> {
const { Item } = this.tenancy.models(tenantId);
const item = await Item.query()
.findById(itemId)
.withGraphFetched('sellAccount')
.withGraphFetched('inventoryAccount')
.withGraphFetched('category')
.withGraphFetched('costAccount')
.withGraphFetched('itemWarehouses.warehouse')
.throwIfNotFound();
return this.transformer.transform(tenantId, item, new ItemTransformer());
}
}

View File

@@ -0,0 +1,70 @@
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { IItemsFilter } from '@/interfaces';
import ItemTransformer from './ItemTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetItems {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private dynamicListService: DynamicListingService;
@Inject()
private transformer: TransformerInjectable;
/**
* Parses items list filter DTO.
* @param {} filterDTO - Filter DTO.
*/
private parseItemsListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve items datatable list.
* @param {number} tenantId -
* @param {IItemsFilter} itemsFilter -
*/
public async getItems(tenantId: number, filterDTO: IItemsFilter) {
const { Item } = this.tenancy.models(tenantId);
// Parses items list filter DTO.
const filter = this.parseItemsListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Item,
filter
);
const { results: items, pagination } = await Item.query()
.onBuild((builder) => {
builder.modify('inactiveMode', filter.inactiveMode);
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount');
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed items.
const transformedItems = await this.transformer.transform(
tenantId,
items,
new ItemTransformer()
);
return {
items: transformedItems,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
}

View File

@@ -0,0 +1,40 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
@Service()
export class InactivateItem {
@Inject()
tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private uow: UnitOfWork;
/**
* Inactivates the given item on the storage.
* @param {number} tenantId
* @param {number} itemId
* @return {Promise<void>}
*/
public async inactivateItem(tenantId: number, itemId: number): Promise<void> {
const { Item } = this.tenancy.models(tenantId);
// Retrieves the item or throw not found error.
const oldItem = await Item.query().findById(itemId).throwIfNotFound();
// Inactivate item under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Activate item on the storage.
await Item.query(trx).findById(itemId).patch({ active: false });
// Triggers `onItemInactivated` event.
await this.eventPublisher.emitAsync(events.item.onInactivated, { trx });
});
}
}

View File

@@ -0,0 +1,97 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ItemBillTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedBillDate',
'formattedRate',
'formattedCost',
];
};
/**
* Formatted sell price.
* @param item
* @returns {string}
*/
public formattedAmount(item): string {
return formatNumber(item.amount, {
currencyCode: item.bill.currencyCode,
});
}
/**
* Formatted bill date.
* @param item
* @returns {string}
*/
public formattedBillDate = (entry): string => {
return this.formatDate(entry.bill.billDate);
};
/**
* Formatted quantity.
* @returns {string}
*/
public formattedQuantity = (entry): string => {
return entry.quantity;
};
/**
* Formatted rate.
* @param entry
* @returns {string}
*/
public formattedRate = (entry): string => {
return formatNumber(entry.rate, {
currencyCode: entry.bill.currencyCode,
});
};
/**
* Formatted bill due date.
* @param entry
* @returns
*/
public formattedBillDueDate = (entry): string => {
return this.formatDate(entry.bill.dueDate);
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
billId: entry.bill.id,
billNumber: entry.bill.billNumber,
referenceNumber: entry.bill.referenceNo,
billDate: entry.bill.billDate,
formattedBillDate: entry.formattedBillDate,
billDueDate: entry.bill.dueDate,
formattedBillDueDate: entry.formattedBillDueDate,
amount: entry.amount,
formattedAmount: entry.formattedAmount,
quantity: entry.quantity,
formattedQuantity: entry.formattedQuantity,
rate: entry.rate,
formattedRate: entry.formattedRate,
vendorDisplayName: entry.bill.vendor.displayName,
vendorCurrencyCode: entry.bill.vendor.currencyCode,
};
};
}

View File

@@ -0,0 +1,82 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ItemEstimateTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedEstimateDate',
'formattedRate',
'formattedCost',
];
};
/**
* Formatted sell price.
* @returns {string}
*/
public formattedAmount(item): string {
return formatNumber(item.amount, {
currencyCode: item.estimate.currencyCode,
});
}
/**
* Formatted estimate date.
* @returns {string}
*/
public formattedEstimateDate = (entry): string => {
return this.formatDate(entry.estimate.estimateDate);
};
/**
* Formatted quantity.
* @returns {string}
*/
public formattedQuantity = (entry): string => {
return entry.quantity;
};
/**
* Formatted rate.
* @returns {string}
*/
public formattedRate = (entry): string => {
return formatNumber(entry.rate, {
currencyCode: entry.estimate.currencyCode,
});
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
estimateId: entry.estimate.id,
estimateNumber: entry.estimate.estimateNumber,
referenceNumber: entry.estimate.referenceNo,
estimateDate: entry.estimate.estimateDate,
formattedEstimateDate: entry.formattedEstimateDate,
amount: entry.amount,
formattedAmount: entry.formattedAmount,
quantity: entry.quantity,
formattedQuantity: entry.formattedQuantity,
rate: entry.rate,
formattedRate: entry.formattedRate,
customerDisplayName: entry.estimate.customer.displayName,
customerCurrencyCode: entry.estimate.customer.currencyCode,
};
};
}

View File

@@ -0,0 +1,84 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ItemInvoicesTransactionsTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedInvoiceDate',
'formattedRate',
'formattedCost',
];
};
/**
* Formatted sell price.
* @param item
* @returns {string}
*/
public formattedAmount(item): string {
return formatNumber(item.amount, {
currencyCode: item.invoice.currencyCode,
});
}
/**
*
* @param item
* @returns
*/
public formattedInvoiceDate = (entry): string => {
return this.formatDate(entry.invoice.invoiceDate);
};
/**
*
*/
public formattedQuantity = (entry): string => {
return entry.quantity;
};
/**
*
* @param entry
* @returns
*/
public formattedRate = (entry): string => {
return formatNumber(entry.rate, {
currencyCode: entry.invoice.currencyCode,
});
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
invoiceId: entry.invoice.id,
invoiceNumber: entry.invoice.invoiceNo,
referenceNumber: entry.invoice.referenceNo,
invoiceDate: entry.invoice.invoiceDate,
formattedInvoiceDate: entry.formattedInvoiceDate,
amount: entry.amount,
formattedAmount: entry.formattedAmount,
quantity: entry.quantity,
formattedQuantity: entry.formattedQuantity,
rate: entry.rate,
formattedRate: entry.formattedRate,
customerDisplayName: entry.invoice.customer.displayName,
customerCurrencyCode: entry.invoice.customer.currencyCode,
};
};
}

View File

@@ -0,0 +1,84 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ItemReceiptTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedReceiptDate',
'formattedRate',
'formattedCost',
];
};
/**
* Formatted sell price.
* @param item
* @returns {string}
*/
public formattedAmount(item): string {
return formatNumber(item.amount, {
currencyCode: item.receipt.currencyCode,
});
}
/**
*
* @param item
* @returns
*/
public formattedReceiptDate = (entry): string => {
return this.formatDate(entry.receipt.receiptDate);
};
/**
*
*/
public formattedQuantity = (entry): string => {
return entry.quantity;
};
/**
*
* @param entry
* @returns
*/
public formattedRate = (entry): string => {
return formatNumber(entry.rate, {
currencyCode: entry.receipt.currencyCode,
});
};
/**
*
* @param entry
* @returns
*/
public transform = (entry) => {
return {
receiptId: entry.receipt.id,
receipNumber: entry.receipt.receiptNumber,
referenceNumber: entry.receipt.referenceNo,
receiptDate: entry.receipt.receiptDate,
formattedReceiptDate: entry.formattedReceiptDate,
amount: entry.amount,
formattedAmount: entry.formattedAmount,
quantity: entry.quantity,
formattedQuantity: entry.formattedQuantity,
rate: entry.rate,
formattedRate: entry.formattedRate,
customerDisplayName: entry.receipt.customer.displayName,
customerCurrencyCode: entry.receipt.customer.currencyCode,
};
};
}

View File

@@ -0,0 +1,123 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ItemInvoicesTransactionsTransformer } from './ItemInvoicesTransactionsTransformer';
import { ItemEstimateTransactionTransformer } from './ItemEstimatesTransactionTransformer';
import { ItemBillTransactionTransformer } from './ItemBillsTransactionsTransformer';
import { ItemReceiptTransactionTransformer } from './ItemReceiptsTransactionsTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export default class ItemTransactionsService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the item associated invoices transactions.
* @param {number} tenantId -
* @param {number} itemId -
*/
public async getItemInvoicesTransactions(tenantId: number, itemId: number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const invoiceEntries = await ItemEntry.query()
.where('itemId', itemId)
.where('referenceType', 'SaleInvoice')
.withGraphJoined('invoice.customer(selectCustomerColumns)')
.orderBy('invoice:invoiceDate', 'ASC')
.modifiers({
selectCustomerColumns: (builder) => {
builder.select('displayName', 'currencyCode', 'id');
},
});
// Retrieves the transformed invoice entries.
return this.transformer.transform(
tenantId,
invoiceEntries,
new ItemInvoicesTransactionsTransformer()
);
}
/**
* Retrieve the item associated invoices transactions.
* @param {number} tenantId
* @param {number} itemId
* @returns
*/
public async getItemBillTransactions(tenantId: number, itemId: number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const billEntries = await ItemEntry.query()
.where('itemId', itemId)
.where('referenceType', 'Bill')
.withGraphJoined('bill.vendor(selectVendorColumns)')
.orderBy('bill:billDate', 'ASC')
.modifiers({
selectVendorColumns: (builder) => {
builder.select('displayName', 'currencyCode', 'id');
},
});
// Retrieves the transformed bill entries.
return this.transformer.transform(
tenantId,
billEntries,
new ItemBillTransactionTransformer()
);
}
/**
*
* @param {number} tenantId
* @param {number} itemId
* @returns
*/
public async getItemEstimateTransactions(tenantId: number, itemId: number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const estimatesEntries = await ItemEntry.query()
.where('itemId', itemId)
.where('referenceType', 'SaleEstimate')
.withGraphJoined('estimate.customer(selectCustomerColumns)')
.orderBy('estimate:estimateDate', 'ASC')
.modifiers({
selectCustomerColumns: (builder) => {
builder.select('displayName', 'currencyCode', 'id');
},
});
// Retrieves the transformed estimates entries.
return this.transformer.transform(
tenantId,
estimatesEntries,
new ItemEstimateTransactionTransformer()
);
}
/**
*
* @param {number} tenantId
* @param {number} itemId
* @returns
*/
public async getItemReceiptTransactions(tenantId: number, itemId: number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const receiptsEntries = await ItemEntry.query()
.where('itemId', itemId)
.where('referenceType', 'SaleReceipt')
.withGraphJoined('receipt.customer(selectCustomerColumns)')
.orderBy('receipt:receiptDate', 'ASC')
.modifiers({
selectCustomerColumns: (builder) => {
builder.select('displayName', 'currencyCode', 'id');
},
});
// Retrieves the transformed receipts entries.
return this.transformer.transform(
tenantId,
receiptsEntries,
new ItemReceiptTransactionTransformer()
);
}
}

View File

@@ -0,0 +1,62 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { GetItemWarehouseTransformer } from '@/services/Warehouses/Items/GettItemWarehouseTransformer';
import { formatNumber } from 'utils';
export default class ItemTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'typeFormatted',
'sellPriceFormatted',
'costPriceFormatted',
'itemWarehouses',
];
};
/**
* Formatted item type.
* @param {IItem} item
* @returns {string}
*/
public typeFormatted(item): string {
return this.context.i18n.__(`item.field.type.${item.type}`);
}
/**
* Formatted sell price.
* @param item
* @returns {string}
*/
public sellPriceFormatted(item): string {
return formatNumber(item.sellPrice, {
currencyCode: this.context.organization.baseCurrency,
});
}
/**
* Formatted cost price.
* @param item
* @returns {string}
*/
public costPriceFormatted(item): string {
return formatNumber(item.costPrice, {
currencyCode: this.context.organization.baseCurrency,
});
}
/**
* Associate the item warehouses quantity.
* @param item
* @returns
*/
public itemWarehouses = (item) => {
return this.item(
item.itemWarehouses,
new GetItemWarehouseTransformer(),
{}
);
};
}

View File

@@ -0,0 +1,244 @@
import {
ACCOUNT_PARENT_TYPE,
ACCOUNT_ROOT_TYPE,
ACCOUNT_TYPE,
} from '@/data/AccountTypes';
import { ServiceError } from '@/exceptions';
import { IItem, IItemDTO } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
@Service()
export class ItemsValidators {
@Inject()
private tenancy: HasTenancyService;
/**
* Validate wether the given item name already exists on the storage.
* @param {number} tenantId
* @param {string} itemName
* @param {number} notItemId
* @return {Promise<void>}
*/
public async validateItemNameUniquiness(
tenantId: number,
itemName: string,
notItemId?: number
): Promise<void> {
const { Item } = this.tenancy.models(tenantId);
const foundItems: [] = await Item.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);
}
}
/**
* Validate item COGS account existance and type.
* @param {number} tenantId
* @param {number} costAccountId
* @return {Promise<void>}
*/
public async validateItemCostAccountExistance(
tenantId: number,
costAccountId: number
): Promise<void> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const foundAccount = await accountRepository.findOneById(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} tenantId - Tenant id.
* @param {number} sellAccountId - Sell account id.
*/
public async validateItemSellAccountExistance(
tenantId: number,
sellAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const foundAccount = await accountRepository.findOneById(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);
}
}
/**
* Validate item inventory account existance and type.
* @param {number} tenantId
* @param {number} inventoryAccountId
*/
public async validateItemInventoryAccountExistance(
tenantId: number,
inventoryAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const foundAccount = await accountRepository.findOneById(
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} tenantId
* @param {number} itemCategoryId
*/
public 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);
}
}
/**
* Validates the given item or items have no associated invoices or bills.
* @param {number} tenantId - Tenant id.
* @param {number|number[]} itemId - Item id.
* @throws {ServiceError}
*/
public async validateHasNoInvoicesOrBills(
tenantId: number,
itemId: number[] | number
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const ids = Array.isArray(itemId) ? itemId : [itemId];
const foundItemEntries = await ItemEntry.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} tenantId -
* @param {number} itemId -
*/
public async validateHasNoInventoryAdjustments(
tenantId: number,
itemId: number[] | number
): Promise<void> {
const { InventoryAdjustmentEntry } = this.tenancy.models(tenantId);
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
const inventoryAdjEntries = await InventoryAdjustmentEntry.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} tenantId - Tenant id.
* @param {number} itemId - Item id.
*/
public async validateEditItemTypeToInventory(
tenantId: number,
oldItem: IItem,
newItemDTO: IItemDTO
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// 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 AccountTransaction.query()
.where('item_id', oldItem.id)
.count('item_id', { as: 'transactions' })
.first();
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 assocaited inventory transactions.
* @param {numnber} tenantId
* @param {IItem} oldItem
* @param {IItemDTO} newItemDTO
* @returns
*/
async validateItemInvnetoryAccountModified(
tenantId: number,
oldItem: IItem,
newItemDTO: IItemDTO
) {
const { AccountTransaction } = this.tenancy.models(tenantId);
if (
newItemDTO.type !== 'inventory' ||
oldItem.inventoryAccountId === newItemDTO.inventoryAccountId
) {
return;
}
// Inventory transactions associated to the given item id.
const transactions = await AccountTransaction.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: IItem) {
if (
itemDTO.type &&
oldItem.type === 'inventory' &&
itemDTO.type !== oldItem.type
) {
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
}
}
}

View File

@@ -0,0 +1,112 @@
import { Service, Inject } from 'typedi';
import {
IItem,
IItemCreateDTO,
IItemDTO,
IItemEditDTO,
IItemsFilter,
} from '@/interfaces';
import { CreateItem } from './CreateItem';
import { EditItem } from './EditItem';
import { DeleteItem } from './DeleteItem';
import { GetItem } from './GetItem';
import { GetItems } from './GetItems';
import { ActivateItem } from './ActivateItem';
import { InactivateItem } from './InactivateItem';
@Service()
export class ItemsApplication {
@Inject()
private createItemService: CreateItem;
@Inject()
private editItemService: EditItem;
@Inject()
private getItemService: GetItem;
@Inject()
private getItemsService: GetItems;
@Inject()
private deleteItemService: DeleteItem;
@Inject()
private activateItemService: ActivateItem;
@Inject()
private inactivateItemService: InactivateItem;
/**
* Creates a new item (service/product).
* @param {number} tenantId
* @param {IItemCreateDTO} itemDTO
* @returns {Promise<IItem>}
*/
public async createItem(
tenantId: number,
itemDTO: IItemCreateDTO
): Promise<IItem> {
return this.createItemService.createItem(tenantId, itemDTO);
}
/**
* Retrieves the given item.
* @param {number} tenantId
* @param {number} itemId
* @returns {Promise<IItem>}
*/
public getItem(tenantId: number, itemId: number): Promise<IItem> {
return this.getItemService.getItem(tenantId, itemId);
}
/**
* Edits the given item (service/product).
* @param {number} tenantId
* @param {number} itemId
* @param {IItemEditDTO} itemDTO
* @returns {Promise<IItem>}
*/
public editItem(tenantId: number, itemId: number, itemDTO: IItemEditDTO) {
return this.editItemService.editItem(tenantId, itemId, itemDTO);
}
/**
* Deletes the given item (service/product).
* @param {number} tenantId
* @param {number} itemId
* @returns {Promise<void>}
*/
public deleteItem(tenantId: number, itemId: number) {
return this.deleteItemService.deleteItem(tenantId, itemId);
}
/**
* Activates the given item (service/product).
* @param {number} tenantId
* @param {number} itemId
* @returns
*/
public activateItem(tenantId: number, itemId: number): Promise<void> {
return this.activateItemService.activateItem(tenantId, itemId);
}
/**
* Inactivates the given item.
* @param {number} tenantId -
* @param {number} itemId -
*/
public inactivateItem(tenantId: number, itemId: number): Promise<void> {
return this.inactivateItemService.inactivateItem(tenantId, itemId);
}
/**
* Retrieves the items paginated list.
* @param {number} tenantId
* @param {IItemsFilter} filterDTO
* @returns {}
*/
public getItems(tenantId: number, filterDTO: IItemsFilter) {
return this.getItemsService.getItems(tenantId, filterDTO);
}
}

View File

@@ -0,0 +1,5 @@
export default class ItemsCostService {
}

View File

@@ -0,0 +1,267 @@
import { sumBy, difference, map } from 'lodash';
import { Inject, Service } from 'typedi';
import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces';
import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import { entriesAmountDiff } from 'utils';
import { ItemEntry } from 'models';
const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS',
};
@Service()
export default class ItemsEntriesService {
@Inject()
tenancy: TenancyService;
/**
* Retrieve the inventory items entries of the reference id and type.
* @param {number} tenantId
* @param {string} referenceType
* @param {string} referenceId
* @return {Promise<IItemEntry[]>}
*/
public async getInventoryEntries(
tenantId: number,
referenceType: string,
referenceId: number
): Promise<IItemEntry[]> {
const { Item, ItemEntry } = this.tenancy.models(tenantId);
const itemsEntries = await ItemEntry.query()
.where('reference_type', referenceType)
.where('reference_id', referenceId);
// Inventory items.
const inventoryItems = await Item.query()
.whereIn('id', map(itemsEntries, 'itemId'))
.where('type', 'inventory');
// Inventory items ids.
const inventoryItemsIds = map(inventoryItems, 'id');
// Filtering the inventory items entries.
const inventoryItemsEntries = itemsEntries.filter(
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1
);
return inventoryItemsEntries;
}
/**
* Filter the given entries to inventory entries.
* @param {IItemEntry[]} entries -
* @returns {IItemEntry[]}
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[]
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items.
const inventoryItems = await Item.query()
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');
const inventoryEntries = entries.filter((entry) =>
inventoryItems.some((item) => item.id === entry.itemId)
);
return inventoryEntries;
}
/**
* Validates the entries items ids.
* @async
* @param {number} tenantId -
* @param {IItemEntryDTO} itemEntries -
*/
public async validateItemsIdsExistance(
tenantId: number,
itemEntries: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = itemEntries.map((e) => e.itemId);
const foundItems = await Item.query().whereIn('id', itemsIds);
const foundItemsIds = foundItems.map((item: IItem) => item.id);
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
if (notFoundItemsIds.length > 0) {
throw new ServiceError(ERRORS.ITEMS_NOT_FOUND);
}
return foundItems;
}
/**
* Validates the entries ids existance on the storage.
* @param {number} tenantId -
* @param {number} billId -
* @param {IItemEntry[]} billEntries -
*/
public async validateEntriesIdsExistance(
tenantId: number,
referenceId: number,
referenceType: string,
billEntries: IItemEntryDTO[]
) {
const { ItemEntry } = this.tenancy.models(tenantId);
const entriesIds = billEntries
.filter((e: IItemEntry) => e.id)
.map((e: IItemEntry) => e.id);
const storedEntries = await ItemEntry.query()
.whereIn('reference_id', [referenceId])
.whereIn('reference_type', [referenceType]);
const storedEntriesIds = storedEntries.map((entry) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND);
}
}
/**
* Validate the entries items that not purchase-able.
*/
public async validateNonPurchasableEntriesItems(
tenantId: number,
itemEntries: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
const purchasbleItems = await Item.query()
.where('purchasable', true)
.whereIn('id', itemsIds);
const purchasbleItemsIds = purchasbleItems.map((item: IItem) => item.id);
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
if (notPurchasableItems.length > 0) {
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
}
}
/**
* Validate the entries items that not sell-able.
*/
public async validateNonSellableEntriesItems(
tenantId: number,
itemEntries: IItemEntryDTO[]
) {
const { Item } = this.tenancy.models(tenantId);
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
const sellableItems = await Item.query()
.where('sellable', true)
.whereIn('id', itemsIds);
const sellableItemsIds = sellableItems.map((item: IItem) => item.id);
const nonSellableItems = difference(itemsIds, sellableItemsIds);
if (nonSellableItems.length > 0) {
throw new ServiceError(ERRORS.NOT_SELL_ABLE_ITEMS);
}
}
/**
* Changes items quantity from the given items entries the new and old onces.
* @param {number} tenantId
* @param {IItemEntry} entries - Items entries.
* @param {IItemEntry} oldEntries - Old items entries.
*/
public async changeItemsQuantity(
tenantId: number,
entries: IItemEntry[],
oldEntries?: IItemEntry[]
): Promise<void> {
const { itemRepository } = this.tenancy.repositories(tenantId);
const opers = [];
const diffEntries = entriesAmountDiff(
entries,
oldEntries,
'quantity',
'itemId'
);
diffEntries.forEach((entry: IItemEntry) => {
const changeQuantityOper = itemRepository.changeNumber(
{ id: entry.itemId, type: 'inventory' },
'quantityOnHand',
entry.quantity
);
opers.push(changeQuantityOper);
});
await Promise.all(opers);
}
/**
* Increment items quantity from the given items entries.
* @param {number} tenantId - Tenant id.
* @param {IItemEntry} entries - Items entries.
*/
public async incrementItemsEntries(
tenantId: number,
entries: IItemEntry[]
): Promise<void> {
return this.changeItemsQuantity(tenantId, entries);
}
/**
* Decrement items quantity from the given items entries.
* @param {number} tenantId - Tenant id.
* @param {IItemEntry} entries - Items entries.
*/
public async decrementItemsQuantity(
tenantId: number,
entries: IItemEntry[]
): Promise<void> {
return this.changeItemsQuantity(
tenantId,
entries.map((entry) => ({
...entry,
quantity: entry.quantity * -1,
}))
);
}
/**
* Sets the cost/sell accounts to the invoice entries.
*/
setItemsEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
const items = await Item.query().whereIn('id', entriesItemsIds);
return entries.map((entry) => {
const item = items.find((i) => i.id === entry.itemId);
return {
...entry,
sellAccountId: entry.sellAccountId || item.sellAccountId,
...(item.type === 'inventory' && {
costAccountId: entry.costAccountId || item.costAccountId,
}),
};
});
};
}
/**
* Retrieve the total items entries.
* @param entries
* @returns
*/
getTotalItemsEntries(entries: ItemEntry[]): number {
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
}
}

View File

@@ -0,0 +1,57 @@
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'
};
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,
},
]