import { Inject, Service } from 'typedi'; import { omit } from 'lodash'; import moment from 'moment'; import * as R from 'ramda'; import { Knex } from 'knex'; import { ServiceError } from '@/exceptions'; import { IQuickInventoryAdjustmentDTO, IInventoryAdjustment, IPaginationMeta, IInventoryAdjustmentsFilter, ISystemUser, IInventoryTransaction, IInventoryAdjustmentEventCreatedPayload, IInventoryAdjustmentEventPublishedPayload, IInventoryAdjustmentEventDeletedPayload, IInventoryAdjustmentCreatingPayload, IInventoryAdjustmentDeletingPayload, IInventoryAdjustmentPublishingPayload, } from '@/interfaces'; import events from '@/subscribers/events'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import InventoryService from './Inventory'; import UnitOfWork from '@/services/UnitOfWork'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import InventoryAdjustmentTransformer from './InventoryAdjustmentTransformer'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; const ERRORS = { INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND', ITEM_SHOULD_BE_INVENTORY_TYPE: 'ITEM_SHOULD_BE_INVENTORY_TYPE', INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED: 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED', }; @Service() export default class InventoryAdjustmentService { @Inject() private tenancy: HasTenancyService; @Inject() private eventPublisher: EventPublisher; @Inject() private inventoryService: InventoryService; @Inject() private dynamicListService: DynamicListingService; @Inject() private uow: UnitOfWork; @Inject() private branchDTOTransform: BranchTransactionDTOTransform; @Inject() private warehouseDTOTransform: WarehouseTransactionDTOTransform; @Inject() private transfromer: TransformerInjectable; /** * Transformes the quick inventory adjustment DTO to model object. * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - * @return {IInventoryAdjustment} */ private transformQuickAdjToModel( tenantId: number, adjustmentDTO: IQuickInventoryAdjustmentDTO, authorizedUser: ISystemUser ): IInventoryAdjustment { const entries = [ { index: 1, itemId: adjustmentDTO.itemId, ...('increment' === adjustmentDTO.type ? { quantity: adjustmentDTO.quantity, cost: adjustmentDTO.cost, } : {}), ...('decrement' === adjustmentDTO.type ? { quantity: adjustmentDTO.quantity, } : {}), }, ]; const initialDTO = { ...omit(adjustmentDTO, ['quantity', 'cost', 'itemId', 'publish']), userId: authorizedUser.id, ...(adjustmentDTO.publish ? { publishedAt: moment().toMySqlDateTime(), } : {}), entries, }; return R.compose( this.warehouseDTOTransform.transformDTO(tenantId), this.branchDTOTransform.transformDTO(tenantId) )(initialDTO); } /** * Validate the item inventory type. * @param {IItem} item */ validateItemInventoryType(item) { if (item.type !== 'inventory') { throw new ServiceError(ERRORS.ITEM_SHOULD_BE_INVENTORY_TYPE); } } /** * Retrieve the inventory adjustment or throw not found service error. * @param {number} tenantId - * @param {number} adjustmentId - */ async getInventoryAdjustmentOrThrowError( tenantId: number, adjustmentId: number ) { const { InventoryAdjustment } = this.tenancy.models(tenantId); const inventoryAdjustment = await InventoryAdjustment.query() .findById(adjustmentId) .withGraphFetched('entries'); if (!inventoryAdjustment) { throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND); } return inventoryAdjustment; } /** * Creates a quick inventory adjustment for specific item. * @param {number} tenantId - Tenant id. * @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - qucik adjustment DTO. */ public async createQuickAdjustment( tenantId: number, quickAdjustmentDTO: IQuickInventoryAdjustmentDTO, authorizedUser: ISystemUser ): Promise { const { InventoryAdjustment, Account, Item } = this.tenancy.models(tenantId); // Retrieve the adjustment account or throw not found error. const adjustmentAccount = await Account.query() .findById(quickAdjustmentDTO.adjustmentAccountId) .throwIfNotFound(); // Retrieve the item model or throw not found service error. const item = await Item.query() .findById(quickAdjustmentDTO.itemId) .throwIfNotFound(); // Validate item inventory type. this.validateItemInventoryType(item); // Transform the DTO to inventory adjustment model. const invAdjustmentObject = this.transformQuickAdjToModel( tenantId, quickAdjustmentDTO, authorizedUser ); // Writes inventory adjustment transaction with associated transactions // under unit-of-work envirment. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onInventoryAdjustmentCreating` event. await this.eventPublisher.emitAsync( events.inventoryAdjustment.onQuickCreating, { tenantId, trx, quickAdjustmentDTO, } as IInventoryAdjustmentCreatingPayload ); // Saves the inventory adjustment with associated entries to the storage. const inventoryAdjustment = await InventoryAdjustment.query( trx ).upsertGraph({ ...invAdjustmentObject, }); // Triggers `onInventoryAdjustmentQuickCreated` event. await this.eventPublisher.emitAsync( events.inventoryAdjustment.onQuickCreated, { tenantId, inventoryAdjustment, inventoryAdjustmentId: inventoryAdjustment.id, trx, } as IInventoryAdjustmentEventCreatedPayload ); return inventoryAdjustment; }); } /** * Deletes the inventory adjustment transaction. * @param {number} tenantId - Tenant id. * @param {number} inventoryAdjustmentId - Inventory adjustment id. */ public async deleteInventoryAdjustment( tenantId: number, inventoryAdjustmentId: number ): Promise { const { InventoryAdjustmentEntry, InventoryAdjustment } = this.tenancy.models(tenantId); // Retrieve the inventory adjustment or throw not found service error. const oldInventoryAdjustment = await this.getInventoryAdjustmentOrThrowError( tenantId, inventoryAdjustmentId ); // Deletes the inventory adjustment transaction and associated transactions // under unit-of-work env. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onInventoryAdjustmentDeleting` event. await this.eventPublisher.emitAsync( events.inventoryAdjustment.onDeleting, { trx, oldInventoryAdjustment, tenantId, } as IInventoryAdjustmentDeletingPayload ); // Deletes the inventory adjustment entries. await InventoryAdjustmentEntry.query(trx) .where('adjustment_id', inventoryAdjustmentId) .delete(); // Deletes the inventory adjustment transaction. await InventoryAdjustment.query(trx) .findById(inventoryAdjustmentId) .delete(); // Triggers `onInventoryAdjustmentDeleted` event. await this.eventPublisher.emitAsync( events.inventoryAdjustment.onDeleted, { tenantId, inventoryAdjustmentId, oldInventoryAdjustment, trx, } as IInventoryAdjustmentEventDeletedPayload ); }); } /** * Publish the inventory adjustment transaction. * @param {number} tenantId * @param {number} inventoryAdjustmentId */ public async publishInventoryAdjustment( tenantId: number, inventoryAdjustmentId: number ): Promise { const { InventoryAdjustment } = this.tenancy.models(tenantId); // Retrieve the inventory adjustment or throw not found service error. const oldInventoryAdjustment = await this.getInventoryAdjustmentOrThrowError( tenantId, inventoryAdjustmentId ); // Validate adjustment not already published. this.validateAdjustmentTransactionsNotPublished(oldInventoryAdjustment); // Publishes inventory adjustment with associated inventory transactions // under unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync( events.inventoryAdjustment.onPublishing, { trx, tenantId, oldInventoryAdjustment, } as IInventoryAdjustmentPublishingPayload ); // Publish the inventory adjustment transaction. await InventoryAdjustment.query().findById(inventoryAdjustmentId).patch({ publishedAt: moment().toMySqlDateTime(), }); // Retrieve the inventory adjustment after the modification. const inventoryAdjustment = await InventoryAdjustment.query() .findById(inventoryAdjustmentId) .withGraphFetched('entries'); // Triggers `onInventoryAdjustmentDeleted` event. await this.eventPublisher.emitAsync( events.inventoryAdjustment.onPublished, { tenantId, inventoryAdjustmentId, inventoryAdjustment, oldInventoryAdjustment, trx, } as IInventoryAdjustmentEventPublishedPayload ); }); } /** * Parses inventory adjustments list filter DTO. * @param filterDTO - */ private parseListFilterDTO(filterDTO) { return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); } /** * Retrieve the inventory adjustments paginated list. * @param {number} tenantId * @param {IInventoryAdjustmentsFilter} adjustmentsFilter */ public async getInventoryAdjustments( tenantId: number, filterDTO: IInventoryAdjustmentsFilter ): Promise<{ inventoryAdjustments: IInventoryAdjustment[]; pagination: IPaginationMeta; }> { const { InventoryAdjustment } = this.tenancy.models(tenantId); // Parses inventory adjustments list filter DTO. const filter = this.parseListFilterDTO(filterDTO); // Dynamic list service. const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, InventoryAdjustment, filter ); const { results, pagination } = await InventoryAdjustment.query() .onBuild((query) => { query.withGraphFetched('entries.item'); query.withGraphFetched('adjustmentAccount'); dynamicFilter.buildQuery()(query); }) .pagination(filter.page - 1, filter.pageSize); // Retrieves the transformed inventory adjustments. const inventoryAdjustments = await this.transfromer.transform( tenantId, results, new InventoryAdjustmentTransformer() ); return { inventoryAdjustments, pagination, }; } /** * Writes the inventory transactions from the inventory adjustment transaction. * @param {number} tenantId - * @param {IInventoryAdjustment} inventoryAdjustment - * @param {boolean} override - * @param {Knex.Transaction} trx - * @return {Promise} */ public async writeInventoryTransactions( tenantId: number, inventoryAdjustment: IInventoryAdjustment, override: boolean = false, trx?: Knex.Transaction ): Promise { const commonTransaction = { direction: inventoryAdjustment.inventoryDirection, date: inventoryAdjustment.date, transactionType: 'InventoryAdjustment', transactionId: inventoryAdjustment.id, createdAt: inventoryAdjustment.createdAt, costAccountId: inventoryAdjustment.adjustmentAccountId, branchId: inventoryAdjustment.branchId, warehouseId: inventoryAdjustment.warehouseId, }; const inventoryTransactions = []; inventoryAdjustment.entries.forEach((entry) => { inventoryTransactions.push({ ...commonTransaction, itemId: entry.itemId, quantity: entry.quantity, rate: entry.cost, }); }); // Saves the given inventory transactions to the storage. await this.inventoryService.recordInventoryTransactions( tenantId, inventoryTransactions, override, trx ); } /** * Reverts the inventory transactions from the inventory adjustment transaction. * @param {number} tenantId * @param {number} inventoryAdjustmentId */ async revertInventoryTransactions( tenantId: number, inventoryAdjustmentId: number, trx?: Knex.Transaction ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> { return this.inventoryService.deleteInventoryTransactions( tenantId, inventoryAdjustmentId, 'InventoryAdjustment', trx ); } /** * Retrieve specific inventory adjustment transaction details. * @param {number} tenantId * @param {number} inventoryAdjustmentId */ async getInventoryAdjustment( tenantId: number, inventoryAdjustmentId: number ) { const { InventoryAdjustment } = this.tenancy.models(tenantId); // Retrieve inventory adjustment transation with associated models. const inventoryAdjustment = await InventoryAdjustment.query() .findById(inventoryAdjustmentId) .withGraphFetched('entries.item') .withGraphFetched('adjustmentAccount'); // Throw not found if the given adjustment transaction not exists. this.throwIfAdjustmentNotFound(inventoryAdjustment); return this.transfromer.transform( tenantId, inventoryAdjustment, new InventoryAdjustmentTransformer() ); } /** * Validate the adjustment transaction is exists. * @param {IInventoryAdjustment} inventoryAdjustment */ private throwIfAdjustmentNotFound(inventoryAdjustment: IInventoryAdjustment) { if (!inventoryAdjustment) { throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND); } } /** * Validates the adjustment transaction is not already published. * @param {IInventoryAdjustment} oldInventoryAdjustment */ private validateAdjustmentTransactionsNotPublished( oldInventoryAdjustment: IInventoryAdjustment ) { if (oldInventoryAdjustment.isPublished) { throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED); } } }