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 { ItemEntry } from '@/models'; import { entriesAmountDiff } from 'utils'; 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() private tenancy: TenancyService; /** * Retrieve the inventory items entries of the reference id and type. * @param {number} tenantId * @param {string} referenceType * @param {string} referenceId * @return {Promise} */ public async getInventoryEntries( tenantId: number, referenceType: string, referenceId: number ): Promise { 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 { 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 { 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 { 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 { return this.changeItemsQuantity( tenantId, entries.map((entry) => ({ ...entry, quantity: entry.quantity * -1, })) ); } /** * Sets the cost/sell accounts to the invoice entries. */ public 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 */ public getTotalItemsEntries(entries: ItemEntry[]): number { return sumBy(entries, (e) => ItemEntry.calcAmount(e)); } /** * Retrieve the non-zero tax items entries. * @param {IItemEntry[]} entries - * @returns {IItemEntry[]} */ public getNonZeroEntries(entries: IItemEntry[]): IItemEntry[] { return entries.filter((e) => e.taxRate > 0); } }