From cf496909a520cb0c278260befff80b7ec86cfa0f Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 13 Mar 2025 00:44:11 +0200 Subject: [PATCH] refactor: inventory transfers to nestjs --- docker/redis/Dockerfile | 2 +- packages/server-nest/package.json | 1 + .../modules/Accounts/models/Account.model.ts | 1 + .../server-nest/src/modules/App/App.module.ts | 4 + .../src/modules/Import/ImportALS.ts | 2 +- .../InventoryCost/InventoryCost.module.ts | 30 ++- .../commands/InventoryAverageCostMethod.ts | 6 - .../commands/InventoryComputeCost.service.ts | 1 - .../commands/InventoryCosts.service.ts | 49 ++--- .../InventoryItemOpeningAvgCost.service.ts | 12 +- .../commands/InventoryTransactions.service.ts | 2 +- .../models/InventoryTransaction.ts | 19 +- .../processors/ComputeItemCost.processor.ts | 48 +++-- ...nventoryTransactionsGLEntries.processor.ts | 20 ++ .../subscribers/InventoryCost.subscriber.ts | 140 +++++++++++++ .../types/InventoryCost.types.ts | 15 +- .../src/modules/Items/models/Item.ts | 163 +++++++++++++- .../modules/SaleInvoices/SalesInvoicesCost.ts | 13 +- .../commands/DeleteSaleInvoice.service.ts | 16 +- .../commands/EditSaleInvoice.service.ts | 9 + .../interceptors/Subscription.guard.ts | 6 +- .../src/modules/TaxRates/TaxRate.module.ts | 4 + .../WriteTaxTransactionsItemEntries.ts | 5 +- .../models/TaxRateTransaction.model.ts | 12 +- .../Tenancy/TenancyDB/UnitOfWork.service.ts | 4 +- .../src/modules/Warehouses/Warehouse.types.ts | 54 ++--- .../WarehouseTransferApplication.ts | 121 +++++++++++ .../WarehouseTransfers.controller.ts | 181 ++++++++++++++++ .../WarehouseTransfers.module.ts | 55 +++++ .../commands/CommandWarehouseTransfer.ts | 118 +++++++++++ .../commands/CreateWarehouseTransfer.ts | 198 ++++++++++++++++++ .../commands/DeleteWarehouseTransfer.ts | 77 +++++++ .../commands/EditWarehouseTransfer.ts | 95 +++++++++ .../commands/InitiateWarehouseTransfer.ts | 92 ++++++++ .../commands/TransferredWarehouseTransfer.ts | 106 ++++++++++ .../WarehouseTransferAutoIncrement.ts | 28 +++ ...houseTransferWriteInventoryTransactions.ts | 167 +++++++++++++++ .../modules/WarehousesTransfers/constants.ts | 57 +++++ .../models/WarehouseTransfer.ts | 152 ++++++++++++++ .../models/WarehouseTransferEntry.ts | 54 +++++ .../queries/GetWarehouseTransfer.ts | 43 ++++ .../queries/GetWarehouseTransfers.ts | 70 +++++++ .../WarehouseTransferItemTransformer.ts | 38 ++++ .../queries/WarehouseTransferTransfomer.ts | 27 +++ ...arehouseTransferAutoIncrementSubscriber.ts | 21 ++ ...TransferInventoryTransactionsSubscriber.ts | 123 +++++++++++ packages/server-nest/tsconfig.json | 3 +- .../server/src/models/TaxRateTransaction.ts | 5 + 48 files changed, 2334 insertions(+), 135 deletions(-) create mode 100644 packages/server-nest/src/modules/InventoryCost/processors/WriteInventoryTransactionsGLEntries.processor.ts create mode 100644 packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCost.subscriber.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransferApplication.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.controller.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.module.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/CommandWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/CreateWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/DeleteWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/EditWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/InitiateWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/TransferredWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferAutoIncrement.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferWriteInventoryTransactions.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/constants.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransferEntry.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfers.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferItemTransformer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferTransfomer.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferAutoIncrementSubscriber.ts create mode 100644 packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferInventoryTransactionsSubscriber.ts diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile index 1e9373e29..2bae8c91c 100644 --- a/docker/redis/Dockerfile +++ b/docker/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:4.0 +FROM redis:6.2.0 COPY redis.conf /usr/local/etc/redis/redis.conf diff --git a/packages/server-nest/package.json b/packages/server-nest/package.json index 3341ff4f6..9ae666cf1 100644 --- a/packages/server-nest/package.json +++ b/packages/server-nest/package.json @@ -22,6 +22,7 @@ "dependencies": { "@bigcapital/email-components": "*", "@bigcapital/pdf-templates": "*", + "@bigcapital/server": "*", "@bigcapital/utils": "*", "@nestjs/bull": "^10.2.1", "@nestjs/bullmq": "^10.2.2", diff --git a/packages/server-nest/src/modules/Accounts/models/Account.model.ts b/packages/server-nest/src/modules/Accounts/models/Account.model.ts index 5f6e6b466..92cef8f59 100644 --- a/packages/server-nest/src/modules/Accounts/models/Account.model.ts +++ b/packages/server-nest/src/modules/Accounts/models/Account.model.ts @@ -230,6 +230,7 @@ export class Account extends TenantBaseModel { to: 'accounts_transactions.accountId', }, }, + /** * Account may has many items as cost account. */ diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index a5f902c9e..bbacc4100 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -69,6 +69,8 @@ import { MailModule } from '../Mail/Mail.module'; import { FinancialStatementsModule } from '../FinancialStatements/FinancialStatements.module'; import { StripePaymentModule } from '../StripePayment/StripePayment.module'; import { FeaturesModule } from '../Features/Features.module'; +import { InventoryCostModule } from '../InventoryCost/InventoryCost.module'; +import { WarehousesTransfersModule } from '../WarehousesTransfers/WarehouseTransfers.module'; @Module({ imports: [ @@ -135,6 +137,7 @@ import { FeaturesModule } from '../Features/Features.module'; PdfTemplatesModule, BranchesModule, WarehousesModule, + WarehousesTransfersModule, CustomersModule, VendorsModule, SaleInvoicesModule, @@ -160,6 +163,7 @@ import { FeaturesModule } from '../Features/Features.module'; SettingsModule, FeaturesModule, InventoryAdjustmentsModule, + InventoryCostModule, PostHogModule, EventTrackerModule, FinancialStatementsModule, diff --git a/packages/server-nest/src/modules/Import/ImportALS.ts b/packages/server-nest/src/modules/Import/ImportALS.ts index 172edb57f..30b4453df 100644 --- a/packages/server-nest/src/modules/Import/ImportALS.ts +++ b/packages/server-nest/src/modules/Import/ImportALS.ts @@ -15,7 +15,7 @@ export class ImportAls { * @returns The result of the callback function. */ public run(callback: () => T): T { - return this.als.run(new Map(), callback); + return this.als.run(new Map(), callback); } /** diff --git a/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts b/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts index 0560c8a8a..5e0be9452 100644 --- a/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts +++ b/packages/server-nest/src/modules/InventoryCost/InventoryCost.module.ts @@ -8,6 +8,18 @@ import { InventoryItemsQuantitySyncService } from './commands/InventoryItemsQuan import { InventoryTransactionsService } from './commands/InventoryTransactions.service'; import { LedgerModule } from '../Ledger/Ledger.module'; import { InventoryComputeCostService } from './commands/InventoryComputeCost.service'; +import { InventoryCostApplication } from './InventoryCostApplication'; +import { StoreInventoryLotsCostService } from './commands/StoreInventortyLotsCost.service'; +import { ComputeItemCostProcessor } from './processors/ComputeItemCost.processor'; +import { WriteInventoryTransactionsGLEntriesProcessor } from './processors/WriteInventoryTransactionsGLEntries.processor'; +import { + ComputeItemCostQueue, + WriteInventoryTransactionsGLEntriesQueue, +} from './types/InventoryCost.types'; +import { BullModule } from '@nestjs/bullmq'; +import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service'; +import { InventoryItemCostService } from './commands/InventoryCosts.service'; +import { InventoryItemOpeningAvgCostService } from './commands/InventoryItemOpeningAvgCost.service'; const models = [ RegisterTenancyModel(InventoryCostLotTracker), @@ -15,14 +27,28 @@ const models = [ ]; @Module({ - imports: [LedgerModule, ...models], + imports: [ + LedgerModule, + ...models, + BullModule.registerQueue({ name: ComputeItemCostQueue }), + BullModule.registerQueue({ + name: WriteInventoryTransactionsGLEntriesQueue, + }), + ], providers: [ InventoryCostGLBeforeWriteSubscriber, InventoryCostGLStorage, InventoryItemsQuantitySyncService, InventoryTransactionsService, InventoryComputeCostService, + InventoryCostApplication, + StoreInventoryLotsCostService, + ComputeItemCostProcessor, + WriteInventoryTransactionsGLEntriesProcessor, + InventoryAverageCostMethodService, + InventoryItemCostService, + InventoryItemOpeningAvgCostService, ], - exports: [...models, InventoryTransactionsService], + exports: [...models, InventoryTransactionsService, InventoryItemCostService], }) export class InventoryCostModule {} diff --git a/packages/server-nest/src/modules/InventoryCost/commands/InventoryAverageCostMethod.ts b/packages/server-nest/src/modules/InventoryCost/commands/InventoryAverageCostMethod.ts index 0e8d548ce..baadca84c 100644 --- a/packages/server-nest/src/modules/InventoryCost/commands/InventoryAverageCostMethod.ts +++ b/packages/server-nest/src/modules/InventoryCost/commands/InventoryAverageCostMethod.ts @@ -3,12 +3,6 @@ import { Knex } from 'knex'; import { InventoryTransaction } from '../models/InventoryTransaction'; export class InventoryAverageCostMethod { - /** - * Constructor method. - * @param {number} tenantId - The given tenant id. - * @param {Date} startingDate - - * @param {number} itemId - The given inventory item id. - */ constructor() {} /** diff --git a/packages/server-nest/src/modules/InventoryCost/commands/InventoryComputeCost.service.ts b/packages/server-nest/src/modules/InventoryCost/commands/InventoryComputeCost.service.ts index c0a81e6b1..b6a98526d 100644 --- a/packages/server-nest/src/modules/InventoryCost/commands/InventoryComputeCost.service.ts +++ b/packages/server-nest/src/modules/InventoryCost/commands/InventoryComputeCost.service.ts @@ -72,7 +72,6 @@ export class InventoryComputeCostService { * @param {Date} startingDate */ async scheduleComputeItemCost( - tenantId: number, itemId: number, startingDate: Date | string, ) { diff --git a/packages/server-nest/src/modules/InventoryCost/commands/InventoryCosts.service.ts b/packages/server-nest/src/modules/InventoryCost/commands/InventoryCosts.service.ts index 431fed8f9..4da3d4f65 100644 --- a/packages/server-nest/src/modules/InventoryCost/commands/InventoryCosts.service.ts +++ b/packages/server-nest/src/modules/InventoryCost/commands/InventoryCosts.service.ts @@ -1,6 +1,4 @@ import { keyBy, get } from 'lodash'; -import { Knex } from 'knex'; -import * as R from 'ramda'; import { IInventoryItemCostMeta } from '../types/InventoryCost.types'; import { Inject, Injectable } from '@nestjs/common'; import { InventoryTransaction } from '../models/InventoryTransaction'; @@ -22,33 +20,30 @@ export class InventoryItemCostService { /** * - * @param {} INValuationMap - - * @param {} OUTValuationMap - + * @param {Map} INValuationMap - + * @param {Map} OUTValuationMap - * @param {number} itemId */ - private getItemInventoryMeta = R.curry( - ( - INValuationMap, - OUTValuationMap, - itemId: number, - ): IInventoryItemCostMeta => { - const INCost = get(INValuationMap, `[${itemId}].cost`, 0); - const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0); + private getItemInventoryMeta( + INValuationMap: Map, + OUTValuationMap: Map, + itemId: number, + ) { + const INCost = get(INValuationMap, `[${itemId}].cost`, 0); + const INQuantity = get(INValuationMap, `[${itemId}].quantity`, 0); - const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0); - const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0); + const OUTCost = get(OUTValuationMap, `[${itemId}].cost`, 0); + const OUTQuantity = get(OUTValuationMap, `[${itemId}].quantity`, 0); - const valuation = INCost - OUTCost; - const quantity = INQuantity - OUTQuantity; - const average = quantity ? valuation / quantity : 0; + const valuation = INCost - OUTCost; + const quantity = INQuantity - OUTQuantity; + const average = quantity ? valuation / quantity : 0; - return { itemId, valuation, quantity, average }; - }, - ); + return { itemId, valuation, quantity, average }; + } /** * - * @param {number} tenantId * @param {number} itemsId * @param {Date} date * @returns @@ -57,7 +52,7 @@ export class InventoryItemCostService { itemsId: number[], date: Date, ): Promise => { - const commonBuilder = (builder: Knex.QueryBuilder) => { + const commonBuilder = (builder) => { if (date) { builder.where('date', '<', date); } @@ -84,7 +79,6 @@ export class InventoryItemCostService { /** * - * @param {number} tenantId - * @param {number[]} itemsIds - * @param {Date} date - */ @@ -122,11 +116,10 @@ export class InventoryItemCostService { const [OUTValuationMap, INValuationMap] = await this.getItemsInventoryInOutMap(itemsId, date); - const getItemValuation = this.getItemInventoryMeta( - INValuationMap, - OUTValuationMap, - ); - const itemsValuations = inventoryItemsIds.map(getItemValuation); + const getItemValuation = (itemId: number) => + this.getItemInventoryMeta(INValuationMap, OUTValuationMap, itemId); + + const itemsValuations = inventoryItemsIds.map((id) => getItemValuation(id)); const itemsValuationsMap = new Map( itemsValuations.map((i) => [i.itemId, i]), ); diff --git a/packages/server-nest/src/modules/InventoryCost/commands/InventoryItemOpeningAvgCost.service.ts b/packages/server-nest/src/modules/InventoryCost/commands/InventoryItemOpeningAvgCost.service.ts index 6cf6834e4..6c5ce7206 100644 --- a/packages/server-nest/src/modules/InventoryCost/commands/InventoryItemOpeningAvgCost.service.ts +++ b/packages/server-nest/src/modules/InventoryCost/commands/InventoryItemOpeningAvgCost.service.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { TenantModelProxy } from '../../System/models/TenantBaseModel'; import { InventoryCostLotTracker } from '../models/InventoryCostLotTracker'; @Injectable() export class InventoryItemOpeningAvgCostService { constructor( + @Inject(InventoryCostLotTracker.name) private readonly inventoryCostLotTrackerModel: TenantModelProxy< typeof InventoryCostLotTracker >, @@ -31,6 +32,10 @@ export class InventoryItemOpeningAvgCostService { builder.sum('cost as cost'); builder.first(); }; + interface QueryResult { + cost: number; + quantity: number; + } // Calculates the total inventory total quantity and rate `IN` transactions. const inInvSumationOper = this.inventoryCostLotTrackerModel() .query() @@ -43,10 +48,11 @@ export class InventoryItemOpeningAvgCostService { .onBuild(commonBuilder) .where('direction', 'OUT'); - const [inInvSumation, outInvSumation] = await Promise.all([ + const [inInvSumation, outInvSumation] = (await Promise.all([ inInvSumationOper, outInvSumationOper, - ]); + ])) as unknown as [QueryResult, QueryResult]; + return this.computeItemAverageCost( inInvSumation?.cost || 0, inInvSumation?.quantity || 0, diff --git a/packages/server-nest/src/modules/InventoryCost/commands/InventoryTransactions.service.ts b/packages/server-nest/src/modules/InventoryCost/commands/InventoryTransactions.service.ts index 689701415..dc6d5ce2d 100644 --- a/packages/server-nest/src/modules/InventoryCost/commands/InventoryTransactions.service.ts +++ b/packages/server-nest/src/modules/InventoryCost/commands/InventoryTransactions.service.ts @@ -33,7 +33,7 @@ export class InventoryTransactionsService { * @return {Promise} */ async recordInventoryTransactions( - transactions: InventoryTransaction[], + transactions: ModelObject[], override: boolean = false, trx?: Knex.Transaction, ): Promise { diff --git a/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts b/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts index 4a8e75349..01ba3114a 100644 --- a/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts +++ b/packages/server-nest/src/modules/InventoryCost/models/InventoryTransaction.ts @@ -7,21 +7,20 @@ import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { InventoryTransactionMeta } from './InventoryTransactionMeta'; export class InventoryTransaction extends TenantBaseModel { - date: Date | string; - direction: TInventoryTransactionDirection; - itemId: number; - quantity: number | null; - rate: number; - transactionType: string; - transactionId: number; + date!: Date | string; + direction!: TInventoryTransactionDirection; + itemId!: number; + quantity!: number | null; + rate!: number; + transactionType!: string; + transactionId!: number; costAccountId?: number; - entryId: number; + entryId!: number; createdAt?: Date; updatedAt?: Date; warehouseId?: number; - meta?: InventoryTransactionMeta; /** @@ -34,7 +33,7 @@ export class InventoryTransaction extends TenantBaseModel { /** * Model timestamps. */ - get timestamps() { + static get timestamps() { return ['createdAt', 'updatedAt']; } diff --git a/packages/server-nest/src/modules/InventoryCost/processors/ComputeItemCost.processor.ts b/packages/server-nest/src/modules/InventoryCost/processors/ComputeItemCost.processor.ts index 148c13aab..dcf244576 100644 --- a/packages/server-nest/src/modules/InventoryCost/processors/ComputeItemCost.processor.ts +++ b/packages/server-nest/src/modules/InventoryCost/processors/ComputeItemCost.processor.ts @@ -1,40 +1,58 @@ -import { JOB_REF, Processor } from '@nestjs/bullmq'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { JOB_REF, Processor, WorkerHost } from '@nestjs/bullmq'; import { Inject, Scope } from '@nestjs/common'; import { Job } from 'bullmq'; import { ClsService } from 'nestjs-cls'; import { TenantJobPayload } from '@/interfaces/Tenant'; import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service'; +import { events } from '@/common/events/events'; +import { ComputeItemCostQueueJob } from '../types/InventoryCost.types'; interface ComputeItemCostJobPayload extends TenantJobPayload { itemId: number; startingDate: Date; } - @Processor({ - name: 'compute-item-cost', + name: ComputeItemCostQueueJob, scope: Scope.REQUEST, }) -export class ComputeItemCostProcessor { +export class ComputeItemCostProcessor extends WorkerHost { + /** + * @param {InventoryComputeCostService} inventoryComputeCostService - + * @param {ClsService} clsService - + * @param {EventEmitter2} eventEmitter - + */ constructor( private readonly inventoryComputeCostService: InventoryComputeCostService, private readonly clsService: ClsService, - - @Inject(JOB_REF) - private readonly jobRef: Job, - ) {} + private readonly eventEmitter: EventEmitter2, + ) { + super(); + } /** - * Handle compute item cost job. + * Process the compute item cost job. + * @param {Job} job - The job to process */ - async handleComputeItemCost() { - const { itemId, startingDate, organizationId, userId } = this.jobRef.data; + async process(job: Job) { + const { itemId, startingDate, organizationId, userId } = job.data; this.clsService.set('organizationId', organizationId); this.clsService.set('userId', userId); - await this.inventoryComputeCostService.computeItemCost( - startingDate, - itemId, - ); + try { + await this.inventoryComputeCostService.computeItemCost( + startingDate, + itemId, + ); + // Emit job completed event + await this.eventEmitter.emitAsync( + events.inventory.onComputeItemCostJobCompleted, + { startingDate, itemId, organizationId, userId }, + ); + } catch (error) { + console.error('Error computing item cost:', error); + throw error; + } } } diff --git a/packages/server-nest/src/modules/InventoryCost/processors/WriteInventoryTransactionsGLEntries.processor.ts b/packages/server-nest/src/modules/InventoryCost/processors/WriteInventoryTransactionsGLEntries.processor.ts new file mode 100644 index 000000000..c17b804ef --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/processors/WriteInventoryTransactionsGLEntries.processor.ts @@ -0,0 +1,20 @@ +import { Process } from '@nestjs/bull'; +import { + WriteInventoryTransactionsGLEntriesQueue, + WriteInventoryTransactionsGLEntriesQueueJob, +} from '../types/InventoryCost.types'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Scope } from '@nestjs/common'; + +@Processor({ + name: WriteInventoryTransactionsGLEntriesQueue, + scope: Scope.REQUEST, +}) +export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost { + constructor() { + super(); + } + + @Process(WriteInventoryTransactionsGLEntriesQueueJob) + async process() {} +} diff --git a/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCost.subscriber.ts b/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCost.subscriber.ts new file mode 100644 index 000000000..eb0cc6a81 --- /dev/null +++ b/packages/server-nest/src/modules/InventoryCost/subscribers/InventoryCost.subscriber.ts @@ -0,0 +1,140 @@ +import { map, head } from 'lodash'; +import { OnEvent } from '@nestjs/event-emitter'; +import { + IComputeItemCostJobCompletedPayload, + IInventoryTransactionsCreatedPayload, + IInventoryTransactionsDeletedPayload, +} from '../types/InventoryCost.types'; +import { ImportAls } from '@/modules/Import/ImportALS'; +import { InventoryItemsQuantitySyncService } from '../commands/InventoryItemsQuantitySync.service'; +import { SaleInvoicesCost } from '@/modules/SaleInvoices/SalesInvoicesCost'; +import { events } from '@/common/events/events'; +import { runAfterTransaction } from '@/modules/Tenancy/TenancyDB/TransactionsHooks'; +import { Injectable } from '@nestjs/common'; +import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service'; + +@Injectable() +export default class InventorySubscriber { + constructor( + private readonly saleInvoicesCost: SaleInvoicesCost, + private readonly itemsQuantitySync: InventoryItemsQuantitySyncService, + private readonly inventoryService: InventoryComputeCostService, + private readonly importAls: ImportAls, + ) {} + + /** + * Sync inventory items quantity once inventory transactions created. + * @param {IInventoryTransactionsCreatedPayload} payload - + */ + @OnEvent(events.inventory.onInventoryTransactionsCreated) + async syncItemsQuantityOnceInventoryTransactionsCreated({ + inventoryTransactions, + trx, + }: IInventoryTransactionsCreatedPayload) { + const itemsQuantityChanges = this.itemsQuantitySync.getItemsQuantityChanges( + inventoryTransactions, + ); + await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx); + } + + /** + * Handles schedule compute inventory items cost once inventory transactions created. + * @param {IInventoryTransactionsCreatedPayload} payload - + */ + @OnEvent(events.inventory.onInventoryTransactionsCreated) + async handleScheduleItemsCostOnInventoryTransactionsCreated({ + inventoryTransactions, + trx, + }: IInventoryTransactionsCreatedPayload) { + const inImportPreviewScope = this.importAls.isImportPreview; + + // Avoid running the cost items job if the async process is in import preview. + if (inImportPreviewScope) return; + + await this.saleInvoicesCost.computeItemsCostByInventoryTransactions( + inventoryTransactions, + ); + } + + /** + * Marks items cost compute running state. + */ + @OnEvent(events.inventory.onInventoryTransactionsCreated) + async markGlobalSettingsComputeItems({}) { + await this.inventoryService.markItemsCostComputeRunning(true); + } + + /** + * Marks items cost compute as completed. + */ + @OnEvent(events.inventory.onInventoryCostEntriesWritten) + async markGlobalSettingsComputeItemsCompeted({}) { + await this.inventoryService.markItemsCostComputeRunning(false); + } + + /** + * Handle run writing the journal entries once the compute items jobs completed. + */ + @OnEvent(events.inventory.onComputeItemCostJobCompleted) + async onComputeItemCostJobFinished({ + itemId, + startingDate, + }: IComputeItemCostJobCompletedPayload) { + // const dependsComputeJobs = await this.agenda.jobs({ + // name: 'compute-item-cost', + // nextRunAt: { $ne: null }, + // 'data.tenantId': tenantId, + // }); + // // There is no scheduled compute jobs waiting. + // if (dependsComputeJobs.length === 0) { + // await this.saleInvoicesCost.scheduleWriteJournalEntries(startingDate); + // } + } + + /** + * Sync inventory items quantity once inventory transactions deleted. + */ + @OnEvent(events.inventory.onInventoryTransactionsDeleted) + async syncItemsQuantityOnceInventoryTransactionsDeleted({ + oldInventoryTransactions, + trx, + }: IInventoryTransactionsDeletedPayload) { + const itemsQuantityChanges = + this.itemsQuantitySync.getReverseItemsQuantityChanges( + oldInventoryTransactions, + ); + await this.itemsQuantitySync.changeItemsQuantity(itemsQuantityChanges, trx); + } + + /** + * Schedules compute items cost once the inventory transactions deleted. + */ + @OnEvent(events.inventory.onInventoryTransactionsDeleted) + async handleScheduleItemsCostOnInventoryTransactionsDeleted({ + transactionType, + transactionId, + oldInventoryTransactions, + trx, + }: IInventoryTransactionsDeletedPayload) { + // Ignore compute item cost with theses transaction types. + const ignoreWithTransactionTypes = ['OpeningItem']; + + if (ignoreWithTransactionTypes.indexOf(transactionType) !== -1) { + return; + } + const inventoryItemsIds = map(oldInventoryTransactions, 'itemId'); + const startingDates = map(oldInventoryTransactions, 'date'); + const startingDate: Date = head(startingDates); + + runAfterTransaction(trx, async () => { + try { + await this.saleInvoicesCost.scheduleComputeCostByItemsIds( + inventoryItemsIds, + startingDate, + ); + } catch (error) { + console.error(error); + } + }); + } +} diff --git a/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts b/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts index 3120da8ea..4a0167a2e 100644 --- a/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts +++ b/packages/server-nest/src/modules/InventoryCost/types/InventoryCost.types.ts @@ -1,6 +1,13 @@ -import { Knex } from "knex"; -import { InventoryTransaction } from "../models/InventoryTransaction"; +import { Knex } from 'knex'; +import { InventoryTransaction } from '../models/InventoryTransaction'; +export const ComputeItemCostQueue = 'ComputeItemCostQueue'; +export const ComputeItemCostQueueJob = 'ComputeItemCostQueueJob'; + +export const WriteInventoryTransactionsGLEntriesQueue = + 'WriteInventoryTransactionsGLEntriesQueue'; +export const WriteInventoryTransactionsGLEntriesQueueJob = + 'WriteInventoryTransactionsGLEntriesQueueJob'; export interface IInventoryItemCostMeta { itemId: number; @@ -10,8 +17,8 @@ export interface IInventoryItemCostMeta { } export interface IInventoryCostLotsGLEntriesWriteEvent { - startingDate: Date, - trx: Knex.Transaction + startingDate: Date; + trx: Knex.Transaction; } export type TInventoryTransactionDirection = 'IN' | 'OUT'; diff --git a/packages/server-nest/src/modules/Items/models/Item.ts b/packages/server-nest/src/modules/Items/models/Item.ts index 4137da231..ec6b6bf89 100644 --- a/packages/server-nest/src/modules/Items/models/Item.ts +++ b/packages/server-nest/src/modules/Items/models/Item.ts @@ -1,7 +1,8 @@ import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { Model } from 'objection'; -export class Item extends TenantBaseModel{ +export class Item extends TenantBaseModel { public readonly quantityOnHand: number; public readonly name: string; public readonly active: boolean; @@ -30,4 +31,164 @@ export class Item extends TenantBaseModel{ static get tableName() { return 'items'; } + + /** + * Relationship mapping. + */ + static get relationMappings() { + // const { Media } = require('../../Media/models/Media.model'); + const { Account } = require('../../Accounts/models/Account.model'); + const { + ItemCategory, + } = require('../../ItemCategories/models/ItemCategory.model'); + const { + ItemWarehouseQuantity, + } = require('../../Warehouses/models/ItemWarehouseQuantity'); + const { + ItemEntry, + } = require('../../TransactionItemEntry/models/ItemEntry'); + // const WarehouseTransferEntry = require('../../Warehouses/'); + const { + InventoryAdjustmentEntry, + } = require('../../InventoryAdjutments/models/InventoryAdjustment'); + const { TaxRate } = require('../../TaxRates/models/TaxRate.model'); + + return { + /** + * Item may belongs to cateogory model. + */ + category: { + relation: Model.BelongsToOneRelation, + modelClass: ItemCategory, + join: { + from: 'items.categoryId', + to: 'items_categories.id', + }, + }, + + /** + * Item may belongs to cost account. + */ + costAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'items.costAccountId', + to: 'accounts.id', + }, + }, + + /** + * Item may belongs to sell account. + */ + sellAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'items.sellAccountId', + to: 'accounts.id', + }, + }, + + /** + * Item may belongs to inventory account. + */ + inventoryAccount: { + relation: Model.BelongsToOneRelation, + modelClass: Account, + join: { + from: 'items.inventoryAccountId', + to: 'accounts.id', + }, + }, + + /** + * Item has many warehouses quantities. + */ + itemWarehouses: { + relation: Model.HasManyRelation, + modelClass: ItemWarehouseQuantity, + join: { + from: 'items.id', + to: 'items_warehouses_quantity.itemId', + }, + }, + + /** + * Item may has many item entries. + */ + itemEntries: { + relation: Model.HasManyRelation, + modelClass: ItemEntry, + join: { + from: 'items.id', + to: 'items_entries.itemId', + }, + }, + + /** + * Item may has many warehouses transfers entries. + */ + // warehousesTransfersEntries: { + // relation: Model.HasManyRelation, + // modelClass: WarehouseTransferEntry, + // join: { + // from: 'items.id', + // to: 'warehouses_transfers_entries.itemId', + // }, + // }, + + /** + * Item has many inventory adjustment entries. + */ + inventoryAdjustmentsEntries: { + relation: Model.HasManyRelation, + modelClass: InventoryAdjustmentEntry, + join: { + from: 'items.id', + to: 'inventory_adjustments_entries.itemId', + }, + }, + + /** + * + */ + // media: { + // relation: Model.ManyToManyRelation, + // modelClass: Media.default, + // join: { + // from: 'items.id', + // through: { + // from: 'media_links.model_id', + // to: 'media_links.media_id', + // }, + // to: 'media.id', + // }, + // }, + + /** + * Item may has sell tax rate. + */ + sellTaxRate: { + relation: Model.BelongsToOneRelation, + modelClass: TaxRate, + join: { + from: 'items.sellTaxRateId', + to: 'tax_rates.id', + }, + }, + + /** + * Item may has purchase tax rate. + */ + purchaseTaxRate: { + relation: Model.BelongsToOneRelation, + modelClass: TaxRate, + join: { + from: 'items.purchaseTaxRateId', + to: 'tax_rates.id', + }, + }, + }; + } } diff --git a/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts b/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts index f6d2dde39..551d5bb0a 100644 --- a/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts +++ b/packages/server-nest/src/modules/SaleInvoices/SalesInvoicesCost.ts @@ -9,11 +9,12 @@ import { events } from '@/common/events/events'; import { ModelObject } from 'objection'; import { InventoryTransaction } from '../InventoryCost/models/InventoryTransaction'; import { IInventoryCostLotsGLEntriesWriteEvent } from '../InventoryCost/types/InventoryCost.types'; +import { InventoryComputeCostService } from '../InventoryCost/commands/InventoryComputeCost.service'; @Injectable() export class SaleInvoicesCost { constructor( - private readonly inventoryService: InventoryService, + private readonly inventoryService: InventoryComputeCostService, private readonly uow: UnitOfWork, private readonly eventPublisher: EventEmitter2, ) {} @@ -112,12 +113,12 @@ export class SaleInvoicesCost { * @return {Promise} */ scheduleWriteJournalEntries(startingDate?: Date) { - const agenda = Container.get('agenda'); + // const agenda = Container.get('agenda'); - return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', { - startingDate, - tenantId, - }); + // return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', { + // startingDate, + // tenantId, + // }); } /** diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts index 57a3f600f..1f3e0c91e 100644 --- a/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts +++ b/packages/server-nest/src/modules/SaleInvoices/commands/DeleteSaleInvoice.service.ts @@ -19,6 +19,14 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; @Injectable() export class DeleteSaleInvoice { + /** + * @param {UnlinkConvertedSaleEstimate} unlockEstimateFromInvoice - Unlink converted sale estimate service. + * @param {EventEmitter2} eventPublisher - Event emitter. + * @param {UnitOfWork} uow - Unit of work. + * @param {TenantModelProxy} paymentReceivedEntryModel - Payment received entry model. + * @param {TenantModelProxy} creditNoteAppliedInvoiceModel - Credit note applied invoice model. + * @param {TenantModelProxy} saleInvoiceModel - Sale invoice model. + */ constructor( private unlockEstimateFromInvoice: UnlinkConvertedSaleEstimate, private eventPublisher: EventEmitter2, @@ -36,6 +44,9 @@ export class DeleteSaleInvoice { @Inject(SaleInvoice.name) private saleInvoiceModel: TenantModelProxy, + + @Inject(ItemEntry.name) + private itemEntryModel: TenantModelProxy, ) {} /** @@ -113,12 +124,13 @@ export class DeleteSaleInvoice { saleInvoiceId, trx, ); - await ItemEntry.query(trx) + await this.itemEntryModel() + .query(trx) .where('reference_id', saleInvoiceId) .where('reference_type', 'SaleInvoice') .delete(); - await SaleInvoice.query(trx).findById(saleInvoiceId).delete(); + await this.saleInvoiceModel().query(trx).findById(saleInvoiceId).delete(); // Triggers `onSaleInvoiceDeleted` event. await this.eventPublisher.emitAsync(events.saleInvoice.onDeleted, { diff --git a/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts b/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts index 8be2c5b54..82269cfe8 100644 --- a/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts +++ b/packages/server-nest/src/modules/SaleInvoices/commands/EditSaleInvoice.service.ts @@ -17,6 +17,15 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; @Injectable() export class EditSaleInvoice { + /** + * @param {ItemsEntriesService} itemsEntriesService - Items entries service. + * @param {EventEmitter2} eventPublisher - Event emitter. + * @param {CommandSaleInvoiceValidators} validators - Command sale invoice validators. + * @param {CommandSaleInvoiceDTOTransformer} transformerDTO - Command sale invoice DTO transformer. + * @param {UnitOfWork} uow - Unit of work. + * @param {TenantModelProxy} saleInvoiceModel - Sale invoice model. + * @param {TenantModelProxy} customerModel - Customer model. + */ constructor( private readonly itemsEntriesService: ItemsEntriesService, private readonly eventPublisher: EventEmitter2, diff --git a/packages/server-nest/src/modules/Subscription/interceptors/Subscription.guard.ts b/packages/server-nest/src/modules/Subscription/interceptors/Subscription.guard.ts index bb31cb2ef..db1f02f9e 100644 --- a/packages/server-nest/src/modules/Subscription/interceptors/Subscription.guard.ts +++ b/packages/server-nest/src/modules/Subscription/interceptors/Subscription.guard.ts @@ -13,9 +13,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; export class SubscriptionGuard implements CanActivate { constructor( @Inject(PlanSubscription.name) - private readonly planSubscriptionModel: TenantModelProxy< - typeof PlanSubscription - >, + private readonly planSubscriptionModel: typeof PlanSubscription, private readonly tenancyContext: TenancyContext, ) {} @@ -30,7 +28,7 @@ export class SubscriptionGuard implements CanActivate { subscriptionSlug: string = 'main', // Default value ): Promise { const tenant = await this.tenancyContext.getTenant(); - const subscription = await this.planSubscriptionModel() + const subscription = await this.planSubscriptionModel .query() .findOne('slug', subscriptionSlug) .where('tenant_id', tenant.id); diff --git a/packages/server-nest/src/modules/TaxRates/TaxRate.module.ts b/packages/server-nest/src/modules/TaxRates/TaxRate.module.ts index b7006dbd9..57ccfc427 100644 --- a/packages/server-nest/src/modules/TaxRates/TaxRate.module.ts +++ b/packages/server-nest/src/modules/TaxRates/TaxRate.module.ts @@ -17,6 +17,8 @@ import { WriteInvoiceTaxTransactionsSubscriber } from './subscribers/WriteInvoic import { BillTaxRateValidateSubscriber } from './subscribers/BillTaxRateValidateSubscriber'; import { SaleInvoiceTaxRateValidateSubscriber } from './subscribers/SaleInvoiceTaxRateValidateSubscriber'; import { SyncItemTaxRateOnEditTaxSubscriber } from './subscribers/SyncItemTaxRateOnEditTaxSubscriber'; +import { WriteTaxTransactionsItemEntries } from './WriteTaxTransactionsItemEntries'; +import { SyncItemTaxRateOnEditTaxRate } from './SyncItemTaxRateOnEditTaxRate'; @Module({ imports: [], @@ -39,6 +41,8 @@ import { SyncItemTaxRateOnEditTaxSubscriber } from './subscribers/SyncItemTaxRat BillTaxRateValidateSubscriber, SaleInvoiceTaxRateValidateSubscriber, SyncItemTaxRateOnEditTaxSubscriber, + WriteTaxTransactionsItemEntries, + SyncItemTaxRateOnEditTaxRate ], exports: [ItemEntriesTaxTransactions], }) diff --git a/packages/server-nest/src/modules/TaxRates/WriteTaxTransactionsItemEntries.ts b/packages/server-nest/src/modules/TaxRates/WriteTaxTransactionsItemEntries.ts index a9813df1c..a5eaeae45 100644 --- a/packages/server-nest/src/modules/TaxRates/WriteTaxTransactionsItemEntries.ts +++ b/packages/server-nest/src/modules/TaxRates/WriteTaxTransactionsItemEntries.ts @@ -6,6 +6,7 @@ import { TaxRateModel } from './models/TaxRate.model'; import { Inject, Injectable } from '@nestjs/common'; import { ModelObject } from 'objection'; import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry'; +import { TaxRateTransaction } from './models/TaxRateTransaction.model'; @Injectable() export class WriteTaxTransactionsItemEntries { @@ -40,11 +41,11 @@ export class WriteTaxTransactionsItemEntries { taxRateId: entry.taxRateId, referenceType: entry.referenceType, referenceId: entry.referenceId, - rate: entry.taxRate || taxRatesById[entry.taxRateId]?.rate, + rate: entry.taxRate || (taxRatesById[entry.taxRateId]?.rate as number), })); await this.taxRateTransactionModel() .query(trx) - .upsertGraph(taxTransactions); + .upsertGraph(taxTransactions as ModelObject[]); } /** diff --git a/packages/server-nest/src/modules/TaxRates/models/TaxRateTransaction.model.ts b/packages/server-nest/src/modules/TaxRates/models/TaxRateTransaction.model.ts index 238dd345a..5c8e88718 100644 --- a/packages/server-nest/src/modules/TaxRates/models/TaxRateTransaction.model.ts +++ b/packages/server-nest/src/modules/TaxRates/models/TaxRateTransaction.model.ts @@ -4,12 +4,12 @@ import { mixin, Model, raw } from 'objection'; import { BaseModel } from '@/models/Model'; export class TaxRateTransaction extends BaseModel { - id: number; - taxRateId: number; - referenceType: string; - referenceId: number; - rate: number; - taxAccountId: number; + public id: number; + public taxRateId: number; + public referenceType: string; + public referenceId: string; + public rate: number; + public taxAccountId?: number; /** * Table name diff --git a/packages/server-nest/src/modules/Tenancy/TenancyDB/UnitOfWork.service.ts b/packages/server-nest/src/modules/Tenancy/TenancyDB/UnitOfWork.service.ts index 135a43429..f903bc467 100644 --- a/packages/server-nest/src/modules/Tenancy/TenancyDB/UnitOfWork.service.ts +++ b/packages/server-nest/src/modules/Tenancy/TenancyDB/UnitOfWork.service.ts @@ -8,7 +8,7 @@ import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.con export class UnitOfWork { constructor( @Inject(TENANCY_DB_CONNECTION) - private readonly tenantKex: Knex, + private readonly tenantKex: () => Knex, ) {} /** @@ -23,7 +23,7 @@ export class UnitOfWork { trx?: Transaction, isolationLevel: IsolationLevel = IsolationLevel.READ_UNCOMMITTED, ): Promise => { - const knex = this.tenantKex; + const knex = this.tenantKex(); let _trx = trx; if (!_trx) { diff --git a/packages/server-nest/src/modules/Warehouses/Warehouse.types.ts b/packages/server-nest/src/modules/Warehouses/Warehouse.types.ts index 241af6772..95d96e877 100644 --- a/packages/server-nest/src/modules/Warehouses/Warehouse.types.ts +++ b/packages/server-nest/src/modules/Warehouses/Warehouse.types.ts @@ -1,23 +1,12 @@ import { Knex } from 'knex'; import { Warehouse } from './models/Warehouse.model'; +import { WarehouseTransfer } from '../WarehousesTransfers/models/WarehouseTransfer'; +import { ModelObject } from 'objection'; export interface IWarehouse { id?: number; } -export interface IWarehouseTransfer { - id?: number; - date: Date; - fromWarehouseId: number; - toWarehouseId: number; - reason?: string; - transactionNumber: string; - entries: IWarehouseTransferEntry[]; - transferInitiatedAt?: Date; - transferDeliveredAt?: Date; - isInitiated?: boolean; - isTransferred?: boolean; -} export interface IWarehouseTransferEntry { id?: number; index?: number; @@ -118,37 +107,31 @@ export interface IWarehouseTransferCreate { } export interface IWarehouseTransferCreated { - trx: Knex.Transaction; - warehouseTransfer: IWarehouseTransfer; + trx?: Knex.Transaction; + warehouseTransfer: ModelObject; warehouseTransferDTO: ICreateWarehouseTransferDTO; - // tenantId: number; } export interface IWarehouseTransferEditPayload { - // tenantId: number; editWarehouseDTO: IEditWarehouseTransferDTO; - oldWarehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } export interface IWarehouseTransferEditedPayload { - // tenantId: number; editWarehouseDTO: IEditWarehouseTransferDTO; - oldWarehouseTransfer: IWarehouseTransfer; - warehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; + warehouseTransfer: ModelObject; trx: Knex.Transaction; } export interface IWarehouseTransferDeletePayload { - // tenantId: number; - oldWarehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } export interface IWarehouseTransferDeletedPayload { - // tenantId: number; - warehouseTransfer: IWarehouseTransfer; - oldWarehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } @@ -173,40 +156,33 @@ export interface IWarehousesActivatedPayload { } export interface IWarehouseMarkAsPrimaryPayload { - // tenantId: number; oldWarehouse: Warehouse; trx: Knex.Transaction; } export interface IWarehouseMarkedAsPrimaryPayload { - // tenantId: number; oldWarehouse: Warehouse; markedWarehouse: Warehouse; trx: Knex.Transaction; } export interface IWarehouseTransferInitiatePayload { - // tenantId: number; - oldWarehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } - export interface IWarehouseTransferInitiatedPayload { - // tenantId: number; - warehouseTransfer: IWarehouseTransfer; - oldWarehouseTransfer: IWarehouseTransfer; + warehouseTransfer: ModelObject; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } export interface IWarehouseTransferTransferingPayload { - // tenantId: number; - oldWarehouseTransfer: IWarehouseTransfer; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } export interface IWarehouseTransferTransferredPayload { - // tenantId: number; - warehouseTransfer: IWarehouseTransfer; - oldWarehouseTransfer: IWarehouseTransfer; + warehouseTransfer: ModelObject; + oldWarehouseTransfer: ModelObject; trx: Knex.Transaction; } diff --git a/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransferApplication.ts b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransferApplication.ts new file mode 100644 index 000000000..4eafc2da6 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransferApplication.ts @@ -0,0 +1,121 @@ +import { + ICreateWarehouseTransferDTO, + IEditWarehouseTransferDTO, + IGetWarehousesTransfersFilterDTO, +} from '@/modules/Warehouses/Warehouse.types'; +import { CreateWarehouseTransfer } from './commands/CreateWarehouseTransfer'; +import { DeleteWarehouseTransfer } from './commands/DeleteWarehouseTransfer'; +import { EditWarehouseTransfer } from './commands/EditWarehouseTransfer'; +import { GetWarehouseTransfer } from './queries/GetWarehouseTransfer'; +import { GetWarehouseTransfers } from './queries/GetWarehouseTransfers'; +import { InitiateWarehouseTransfer } from './commands/InitiateWarehouseTransfer'; +import { TransferredWarehouseTransfer } from './commands/TransferredWarehouseTransfer'; +import { Injectable } from '@nestjs/common'; +import { WarehouseTransfer } from './models/WarehouseTransfer'; +import { ModelObject } from 'objection'; + +@Injectable() +export class WarehouseTransferApplication { + constructor( + private readonly createWarehouseTransferService: CreateWarehouseTransfer, + private readonly editWarehouseTransferService: EditWarehouseTransfer, + private readonly deleteWarehouseTransferService: DeleteWarehouseTransfer, + private readonly getWarehouseTransferService: GetWarehouseTransfer, + private readonly getWarehousesTransfersService: GetWarehouseTransfers, + private readonly initiateWarehouseTransferService: InitiateWarehouseTransfer, + private readonly transferredWarehouseTransferService: TransferredWarehouseTransfer, + ) {} + + /** + * Creates a warehouse transfer transaction. + * @param {number} tenantId + * @param {ICreateWarehouseTransferDTO} createWarehouseTransferDTO + * @returns {} + */ + public createWarehouseTransfer = ( + createWarehouseTransferDTO: ICreateWarehouseTransferDTO, + ): Promise> => { + return this.createWarehouseTransferService.createWarehouseTransfer( + createWarehouseTransferDTO, + ); + }; + + /** + * Edits warehouse transfer transaction. + * @param {number} tenantId - + * @param {number} warehouseTransferId - number + * @param {IEditWarehouseTransferDTO} editWarehouseTransferDTO + */ + public editWarehouseTransfer = ( + warehouseTransferId: number, + editWarehouseTransferDTO: IEditWarehouseTransferDTO, + ): Promise> => { + return this.editWarehouseTransferService.editWarehouseTransfer( + warehouseTransferId, + editWarehouseTransferDTO, + ); + }; + + /** + * Deletes warehouse transfer transaction. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public deleteWarehouseTransfer = ( + warehouseTransferId: number, + ): Promise => { + return this.deleteWarehouseTransferService.deleteWarehouseTransfer( + warehouseTransferId, + ); + }; + + /** + * Retrieves warehouse transfer transaction. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public getWarehouseTransfer = ( + warehouseTransferId: number, + ): Promise> => { + return this.getWarehouseTransferService.getWarehouseTransfer( + warehouseTransferId, + ); + }; + + /** + * Retrieves warehouses trans + * @param {IGetWarehousesTransfersFilterDTO} filterDTO + * @returns {Promise} + */ + public getWarehousesTransfers = ( + filterDTO: IGetWarehousesTransfersFilterDTO, + ) => { + return this.getWarehousesTransfersService.getWarehouseTransfers(filterDTO); + }; + + /** + * Marks the warehouse transfer order as transfered. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public transferredWarehouseTransfer = ( + warehouseTransferId: number, + ): Promise> => { + return this.transferredWarehouseTransferService.transferredWarehouseTransfer( + warehouseTransferId, + ); + }; + + /** + * Marks the warehouse transfer order as initiated. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public initiateWarehouseTransfer = ( + warehouseTransferId: number, + ): Promise> => { + return this.initiateWarehouseTransferService.initiateWarehouseTransfer( + warehouseTransferId, + ); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.controller.ts b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.controller.ts new file mode 100644 index 000000000..10089d29f --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.controller.ts @@ -0,0 +1,181 @@ +import { + Controller, + Post, + Put, + Get, + Delete, + Body, + Param, + Query, + Inject, +} from '@nestjs/common'; +import { WarehouseTransferApplication } from './WarehouseTransferApplication'; +import { + ICreateWarehouseTransferDTO, + IEditWarehouseTransferDTO, +} from '@/modules/Warehouses/Warehouse.types'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { PublicRoute } from '../Auth/Jwt.guard'; + +@Controller('warehouse-transfers') +@ApiTags('warehouse-transfers') +@PublicRoute() +export class WarehouseTransfersController { + /** + * @param {WarehouseTransferApplication} warehouseTransferApplication - Warehouse transfer application. + */ + constructor( + @Inject(WarehouseTransferApplication) + private readonly warehouseTransferApplication: WarehouseTransferApplication, + ) {} + + /** + * Creates a new warehouse transfer transaction. + */ + @Post() + @ApiOperation({ summary: 'Create a new warehouse transfer transaction.' }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer transaction has been created successfully.', + }) + async createWarehouseTransfer( + @Body() createWarehouseTransferDTO: ICreateWarehouseTransferDTO, + ) { + const warehouse = + await this.warehouseTransferApplication.createWarehouseTransfer( + createWarehouseTransferDTO, + ); + + return { + id: warehouse.id, + message: + 'The warehouse transfer transaction has been created successfully.', + }; + } + + /** + * Edits warehouse transfer transaction. + */ + @Post(':id') + @ApiOperation({ summary: 'Edit the given warehouse transfer transaction.' }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer transaction has been edited successfully.', + }) + async editWarehouseTransfer( + @Param('id') id: number, + @Body() editWarehouseTransferDTO: IEditWarehouseTransferDTO, + ) { + const warehouseTransfer = + await this.warehouseTransferApplication.editWarehouseTransfer( + id, + editWarehouseTransferDTO, + ); + + return { + id: warehouseTransfer.id, + message: + 'The warehouse transfer transaction has been edited successfully.', + }; + } + + /** + * Initiates the warehouse transfer. + */ + @Put(':id/initiate') + @ApiOperation({ summary: 'Initiate the given warehouse transfer.' }) + @ApiResponse({ + status: 200, + description: 'The warehouse transfer has been initiated successfully.', + }) + async initiateTransfer(@Param('id') id: number) { + await this.warehouseTransferApplication.initiateWarehouseTransfer(id); + + return { + id, + message: 'The given warehouse transfer has been initialized.', + }; + } + + /** + * Marks the given warehouse transfer as transferred. + */ + @Put(':id/transferred') + @ApiOperation({ + summary: 'Mark the given warehouse transfer as transferred.', + }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer has been marked as transferred successfully.', + }) + async deliverTransfer(@Param('id') id: number) { + await this.warehouseTransferApplication.transferredWarehouseTransfer(id); + + return { + id, + message: 'The given warehouse transfer has been delivered.', + }; + } + + /** + * Retrieves warehouse transfer transactions with pagination. + */ + @Get() + @ApiOperation({ + summary: 'Retrieve warehouse transfer transactions with pagination.', + }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer transactions have been retrieved successfully.', + }) + async getWarehousesTransfers(@Query() query: any) { + const { warehousesTransfers, pagination, filter } = + await this.warehouseTransferApplication.getWarehousesTransfers(query); + + return { + data: warehousesTransfers, + pagination, + filter, + }; + } + + /** + * Retrieves warehouse transfer transaction details. + */ + @Get(':id') + @ApiOperation({ summary: 'Retrieve warehouse transfer transaction details.' }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer transaction details have been retrieved successfully.', + }) + async getWarehouseTransfer(@Param('id') id: number) { + const warehouseTransfer = + await this.warehouseTransferApplication.getWarehouseTransfer(id); + + return { data: warehouseTransfer }; + } + + /** + * Deletes the given warehouse transfer transaction. + */ + @Delete(':id') + @ApiOperation({ summary: 'Delete the given warehouse transfer transaction.' }) + @ApiResponse({ + status: 200, + description: + 'The warehouse transfer transaction has been deleted successfully.', + }) + async deleteWarehouseTransfer(@Param('id') id: number) { + await this.warehouseTransferApplication.deleteWarehouseTransfer(id); + + return { + message: + 'The warehouse transfer transaction has been deleted successfully.', + }; + } +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.module.ts b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.module.ts new file mode 100644 index 000000000..292966f83 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/WarehouseTransfers.module.ts @@ -0,0 +1,55 @@ +import { Module } from '@nestjs/common'; +import { CreateWarehouseTransfer } from './commands/CreateWarehouseTransfer'; +import { EditWarehouseTransfer } from './commands/EditWarehouseTransfer'; +import { DeleteWarehouseTransfer } from './commands/DeleteWarehouseTransfer'; +import { GetWarehouseTransfer } from './queries/GetWarehouseTransfer'; +import { GetWarehouseTransfers } from './queries/GetWarehouseTransfers'; +import { WarehouseTransferApplication } from './WarehouseTransferApplication'; +import { WarehouseTransfersController } from './WarehouseTransfers.controller'; +import { WarehouseTransferInventoryTransactions } from './commands/WarehouseTransferWriteInventoryTransactions'; +import { WarehouseTransferAutoIncrement } from './commands/WarehouseTransferAutoIncrement'; +import { WarehouseTransferAutoIncrementSubscriber } from './susbcribers/WarehouseTransferAutoIncrementSubscriber'; +import { WarehouseTransferInventoryTransactionsSubscriber } from './susbcribers/WarehouseTransferInventoryTransactionsSubscriber'; +import { InitiateWarehouseTransfer } from './commands/InitiateWarehouseTransfer'; +import { TransferredWarehouseTransfer } from './commands/TransferredWarehouseTransfer'; +import { CommandWarehouseTransfer } from './commands/CommandWarehouseTransfer'; +import { ItemsModule } from '../Items/items.module'; +import { InventoryCostModule } from '../InventoryCost/InventoryCost.module'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { WarehouseTransfer } from './models/WarehouseTransfer'; +import { WarehouseTransferEntry } from './models/WarehouseTransferEntry'; +import { DynamicListModule } from '../DynamicListing/DynamicList.module'; +import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module'; + +const models = [ + RegisterTenancyModel(WarehouseTransfer), + RegisterTenancyModel(WarehouseTransferEntry), +]; + +@Module({ + imports: [ + ItemsModule, + InventoryCostModule, + DynamicListModule, + AutoIncrementOrdersModule, + ...models, + ], + providers: [ + WarehouseTransferApplication, + CreateWarehouseTransfer, + EditWarehouseTransfer, + DeleteWarehouseTransfer, + GetWarehouseTransfer, + GetWarehouseTransfers, + WarehouseTransferInventoryTransactions, + WarehouseTransferAutoIncrement, + WarehouseTransferAutoIncrementSubscriber, + WarehouseTransferInventoryTransactionsSubscriber, + TransferredWarehouseTransfer, + InitiateWarehouseTransfer, + CommandWarehouseTransfer, + ], + exports: [...models], + controllers: [WarehouseTransfersController], +}) +export class WarehousesTransfersModule {} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/CommandWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/CommandWarehouseTransfer.ts new file mode 100644 index 000000000..4b25bf557 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/CommandWarehouseTransfer.ts @@ -0,0 +1,118 @@ +import { ERRORS } from '../constants'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ModelObject } from 'objection'; +import { Item } from '@/modules/Items/models/Item'; +import { + ICreateWarehouseTransferDTO, + IEditWarehouseTransferDTO, +} from '@/modules/Warehouses/Warehouse.types'; +import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; + +@Injectable() +export class CommandWarehouseTransfer { + constructor( + @Inject(Warehouse.name) + private readonly warehouseModel: TenantModelProxy, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * + * @param {WarehouseTransfer} warehouseTransfer + */ + throwIfTransferNotFound = (warehouseTransfer: WarehouseTransfer) => { + if (!warehouseTransfer) { + throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_FOUND); + } + }; + + /** + * + * @param {number} branchId + * @returns + */ + async getWarehouseTransferOrThrowNotFound(branchId: number) { + const foundTransfer = await this.warehouseTransferModel() + .query() + .findById(branchId); + + if (!foundTransfer) { + throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_FOUND); + } + return foundTransfer; + } + + /** + * Validate the from/to warehouses should not be the same. + * @param {ICreateWarehouseTransferDTO|IEditWarehouseTransferDTO} warehouseTransferDTO + */ + validateWarehouseFromToNotSame( + warehouseTransferDTO: + | ICreateWarehouseTransferDTO + | IEditWarehouseTransferDTO, + ) { + if ( + warehouseTransferDTO.fromWarehouseId === + warehouseTransferDTO.toWarehouseId + ) { + throw new ServiceError(ERRORS.WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME); + } + } + + /** + * Validates entries items should be inventory. + * @param {IItem[]} items + * @returns {void} + */ + validateItemsShouldBeInventory = ( + items: ModelObject[], + ): void => { + const nonInventoryItems = items.filter((item) => item.type !== 'inventory'); + + if (nonInventoryItems.length > 0) { + throw new ServiceError( + ERRORS.WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY, + ); + } + }; + + /** + * + * @param {number} fromWarehouseId + * @returns + */ + getToWarehouseOrThrow = async (fromWarehouseId: number) => { + const warehouse = await this.warehouseModel() + .query() + .findById(fromWarehouseId); + + if (!warehouse) { + throw new ServiceError(ERRORS.TO_WAREHOUSE_NOT_FOUND); + } + return warehouse; + }; + + /** + * + * @param {number} fromWarehouseId + * @returns + */ + getFromWarehouseOrThrow = async (fromWarehouseId: number) => { + const warehouse = await this.warehouseModel() + .query() + .findById(fromWarehouseId); + + if (!warehouse) { + throw new ServiceError(ERRORS.FROM_WAREHOUSE_NOT_FOUND); + } + return warehouse; + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/CreateWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/CreateWarehouseTransfer.ts new file mode 100644 index 000000000..52c77bd0f --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/CreateWarehouseTransfer.ts @@ -0,0 +1,198 @@ +import { Knex } from 'knex'; +import { omit, get, isNumber } from 'lodash'; +import * as R from 'ramda'; +import { + ICreateWarehouseTransferDTO, + IWarehouseTransferCreate, + IWarehouseTransferCreated, + IWarehouseTransferEntryDTO, +} from '@/modules/Warehouses/Warehouse.types'; +import { CommandWarehouseTransfer } from './CommandWarehouseTransfer'; +import { WarehouseTransferAutoIncrement } from './WarehouseTransferAutoIncrement'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { InventoryItemCostService } from '@/modules/InventoryCost/commands/InventoryCosts.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; +import { IInventoryItemCostMeta } from '@/modules/InventoryCost/types/InventoryCost.types'; +import { ModelObject } from 'objection'; +import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry'; + +@Injectable() +export class CreateWarehouseTransfer { + /** + * @param {UnitOfWork} uow - Unit of work. + * @param {EventEmitter2} eventPublisher - Event publisher. + * @param {ItemsEntriesService} itemsEntries - Items entries service. + * @param {InventoryItemCostService} inventoryItemCost - Inventory item cost service. + * @param {WarehouseTransferAutoIncrement} autoIncrementOrders - Warehouse transfer auto increment. + * @param {CommandWarehouseTransfer} commandWarehouseTransfer - Command warehouse transfer. + * @param {TenantModelProxy} warehouseTransferModel - Warehouse transfer model. + */ + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + private readonly itemsEntries: ItemsEntriesService, + private readonly inventoryItemCost: InventoryItemCostService, + private readonly autoIncrementOrders: WarehouseTransferAutoIncrement, + private readonly commandWarehouseTransfer: CommandWarehouseTransfer, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Transformes the givne new warehouse transfer DTO to model. + * @param {ICreateWarehouseTransferDTO} warehouseTransferDTO + * @returns {IWarehouseTransfer} + */ + private transformDTOToModel = async ( + warehouseTransferDTO: ICreateWarehouseTransferDTO, + ): Promise> => { + const entries = await this.transformEntries( + warehouseTransferDTO, + warehouseTransferDTO.entries, + ); + // Retrieves the auto-increment the warehouse transfer number. + const autoNextNumber = this.autoIncrementOrders.getNextTransferNumber(); + + // Warehouse transfer order transaction number. + const transactionNumber = + warehouseTransferDTO.transactionNumber || autoNextNumber; + + return { + ...omit(warehouseTransferDTO, ['transferDelivered', 'transferInitiated']), + transactionNumber, + ...(warehouseTransferDTO.transferDelivered + ? { + transferDeliveredAt: new Date(), + } + : {}), + ...(warehouseTransferDTO.transferDelivered || + warehouseTransferDTO.transferInitiated + ? { + transferInitiatedAt: new Date(), + } + : {}), + entries, + }; + }; + + /** + * Assoc average cost to the entry that has no cost. + * @param {Promise} inventoryItemsCostMap - + * @param {IWarehouseTransferEntryDTO} entry - + */ + private transformEntryAssocAverageCost = R.curry( + ( + inventoryItemsCostMap: Map, + entry: IWarehouseTransferEntryDTO, + ): IWarehouseTransferEntryDTO => { + const itemValuation = inventoryItemsCostMap.get(entry.itemId); + const itemCost = get(itemValuation, 'average', 0); + + return isNumber(entry.cost) ? entry : R.assoc('cost', itemCost, entry); + }, + ); + + /** + * Transformes warehouse transfer entries. + * @param {ICreateWarehouseTransferDTO} warehouseTransferDTO + * @param {IWarehouseTransferEntryDTO[]} entries + * @returns {Promise} + */ + public transformEntries = async ( + warehouseTransferDTO: ICreateWarehouseTransferDTO, + entries: IWarehouseTransferEntryDTO[], + ): Promise[]> => { + const inventoryItemsIds = warehouseTransferDTO.entries.map((e) => e.itemId); + + // Retrieves the inventory items valuation map. + const inventoryItemsCostMap = + await this.inventoryItemCost.getItemsInventoryValuation( + inventoryItemsIds, + warehouseTransferDTO.date, + ); + // Assoc average cost to the entry. + const assocAverageCost = this.transformEntryAssocAverageCost( + inventoryItemsCostMap, + ); + return entries.map((entry) => assocAverageCost(entry)); + }; + + /** + * Authorize warehouse transfer before creating. + * @param {number} tenantId + * @param {ICreateWarehouseTransferDTO} warehouseTransferDTO + */ + public authorize = async ( + warehouseTransferDTO: ICreateWarehouseTransferDTO, + ) => { + // Validate warehouse from and to should not be the same. + this.commandWarehouseTransfer.validateWarehouseFromToNotSame( + warehouseTransferDTO, + ); + + // Retrieves the from warehouse or throw not found service error. + const fromWarehouse = + await this.commandWarehouseTransfer.getFromWarehouseOrThrow( + warehouseTransferDTO.fromWarehouseId, + ); + // Retrieves the to warehouse or throw not found service error. + const toWarehouse = + await this.commandWarehouseTransfer.getToWarehouseOrThrow( + warehouseTransferDTO.toWarehouseId, + ); + // Validates the not found entries items ids. + const items = await this.itemsEntries.validateItemsIdsExistance( + warehouseTransferDTO.entries, + ); + // Validate the items entries should be inventory type. + this.commandWarehouseTransfer.validateItemsShouldBeInventory(items); + }; + + /** + * Creates a new warehouse transfer transaction. + * @param {ICreateWarehouseTransferDTO} warehouseDTO - + * @returns {Promise>} + */ + public createWarehouseTransfer = async ( + warehouseTransferDTO: ICreateWarehouseTransferDTO, + ): Promise> => { + // Authorize warehouse transfer before creating. + await this.authorize(warehouseTransferDTO); + + // Transformes the warehouse transfer DTO to model. + const warehouseTransferModel = + await this.transformDTOToModel(warehouseTransferDTO); + + // Create warehouse transfer under unit-of-work. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onWarehouseTransferCreate` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onCreate, { + trx, + warehouseTransferDTO, + } as IWarehouseTransferCreate); + + // Stores the warehouse transfer transaction graph to the storage. + const warehouseTransfer = await this.warehouseTransferModel() + .query(trx) + .upsertGraphAndFetch({ + ...warehouseTransferModel, + }); + // Triggers `onWarehouseTransferCreated` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onCreated, { + trx, + warehouseTransfer, + warehouseTransferDTO, + } as IWarehouseTransferCreated); + + return warehouseTransfer; + }); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/DeleteWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/DeleteWarehouseTransfer.ts new file mode 100644 index 000000000..fc8bb9ed5 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/DeleteWarehouseTransfer.ts @@ -0,0 +1,77 @@ +import { Knex } from 'knex'; +import { + IWarehouseTransferDeletedPayload, + IWarehouseTransferDeletePayload, +} from '@/modules/Warehouses/Warehouse.types'; +import { Inject, Injectable } from '@nestjs/common'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { events } from '@/common/events/events'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry'; + +@Injectable() +export class DeleteWarehouseTransfer { + /** + * @param {UnitOfWork} uow - Unit of work service. + * @param {EventEmitter2} eventPublisher - Event emitter service. + * @param {TenantModelProxy} warehouseTransferModel - Warehouse transfer model. + * @param {TenantModelProxy} warehouseTransferEntryModel - Warehouse transfer entry model. + */ + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + @Inject(WarehouseTransferEntry.name) + private readonly warehouseTransferEntryModel: TenantModelProxy< + typeof WarehouseTransferEntry + >, + ) {} + + /** + * Deletes warehouse transfer transaction. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public deleteWarehouseTransfer = async ( + warehouseTransferId: number, + ): Promise => { + // Retrieve the old warehouse transfer or throw not found service error. + const oldWarehouseTransfer = await this.warehouseTransferModel() + .query() + .findById(warehouseTransferId) + .throwIfNotFound(); + + // Deletes the warehouse transfer under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onWarehouseTransferCreate` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onDelete, { + oldWarehouseTransfer, + trx, + } as IWarehouseTransferDeletePayload); + + // Delete warehouse transfer entries. + await this.warehouseTransferEntryModel() + .query(trx) + .where('warehouseTransferId', warehouseTransferId) + .delete(); + + // Delete warehouse transfer. + await this.warehouseTransferModel() + .query(trx) + .findById(warehouseTransferId) + .delete(); + + // Triggers `onWarehouseTransferDeleted` event + await this.eventPublisher.emitAsync(events.warehouseTransfer.onDeleted, { + oldWarehouseTransfer, + trx, + } as IWarehouseTransferDeletedPayload); + }); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/EditWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/EditWarehouseTransfer.ts new file mode 100644 index 000000000..075460553 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/EditWarehouseTransfer.ts @@ -0,0 +1,95 @@ +import { Knex } from 'knex'; +import { + IEditWarehouseTransferDTO, + IWarehouseTransferEditPayload, + IWarehouseTransferEditedPayload, +} from '@/modules/Warehouses/Warehouse.types'; +import { CommandWarehouseTransfer } from './CommandWarehouseTransfer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TenantModelProxy } from '../../System/models/TenantBaseModel'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service'; +import { ModelObject } from 'objection'; + +@Injectable() +export class EditWarehouseTransfer { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + private readonly commandWarehouseTransfer: CommandWarehouseTransfer, + private readonly itemsEntries: ItemsEntriesService, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Edits warehouse transfer. + * @param {number} warehouseTransferId - Warehouse transfer id. + * @param {IEditWarehouseTransferDTO} editWarehouseDTO - + * @returns {Promise>} + */ + public editWarehouseTransfer = async ( + warehouseTransferId: number, + editWarehouseDTO: IEditWarehouseTransferDTO, + ): Promise> => { + // Retrieves the old warehouse transfer transaction. + const oldWarehouseTransfer = await this.warehouseTransferModel() + .query() + .findById(warehouseTransferId) + .throwIfNotFound(); + + // Validate warehouse from and to should not be the same. + this.commandWarehouseTransfer.validateWarehouseFromToNotSame( + editWarehouseDTO, + ); + // Retrieves the from warehouse or throw not found service error. + const fromWarehouse = + await this.commandWarehouseTransfer.getFromWarehouseOrThrow( + editWarehouseDTO.fromWarehouseId, + ); + // Retrieves the to warehouse or throw not found service error. + const toWarehouse = + await this.commandWarehouseTransfer.getToWarehouseOrThrow( + editWarehouseDTO.toWarehouseId, + ); + // Validates the not found entries items ids. + const items = await this.itemsEntries.validateItemsIdsExistance( + editWarehouseDTO.entries, + ); + // Validate the items entries should be inventory type. + this.commandWarehouseTransfer.validateItemsShouldBeInventory(items); + + // Edits warehouse transfer transaction under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onWarehouseTransferEdit` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onEdit, { + editWarehouseDTO, + oldWarehouseTransfer, + trx, + } as IWarehouseTransferEditPayload); + + // Updates warehouse transfer graph on the storage. + const warehouseTransfer = await this.warehouseTransferModel() + .query(trx) + .upsertGraphAndFetch({ + id: warehouseTransferId, + ...editWarehouseDTO, + }); + // Triggers `onWarehouseTransferEdit` event + await this.eventPublisher.emitAsync(events.warehouseTransfer.onEdited, { + editWarehouseDTO, + warehouseTransfer, + oldWarehouseTransfer, + trx, + } as IWarehouseTransferEditedPayload); + + return warehouseTransfer; + }); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/InitiateWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/InitiateWarehouseTransfer.ts new file mode 100644 index 000000000..74b51c216 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/InitiateWarehouseTransfer.ts @@ -0,0 +1,92 @@ +import { Knex } from 'knex'; +import { + IWarehouseTransferEditedPayload, + IWarehouseTransferInitiatedPayload, + IWarehouseTransferInitiatePayload, +} from '@/modules/Warehouses/Warehouse.types'; +import { ERRORS } from '../constants'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { events } from '@/common/events/events'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { ModelObject } from 'objection'; + +@Injectable() +export class InitiateWarehouseTransfer { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Validate the given warehouse transfer not already initiated. + * @param {IWarehouseTransfer} warehouseTransfer + */ + private validateWarehouseTransferNotAlreadyInitiated = ( + warehouseTransfer: ModelObject, + ) => { + if (warehouseTransfer.transferInitiatedAt) { + throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_ALREADY_INITIATED); + } + }; + + /** + * Initiate warehouse transfer. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public initiateWarehouseTransfer = async ( + warehouseTransferId: number, + ): Promise> => { + // Retrieves the old warehouse transfer transaction. + const oldWarehouseTransfer = await this.warehouseTransferModel() + .query() + .findById(warehouseTransferId) + .throwIfNotFound(); + + // Validate the given warehouse transfer not already initiated. + this.validateWarehouseTransferNotAlreadyInitiated(oldWarehouseTransfer); + + // Edits warehouse transfer transaction under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onWarehouseTransferInitiate` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onInitiate, { + oldWarehouseTransfer, + trx, + } as IWarehouseTransferInitiatePayload); + + // Updates warehouse transfer graph on the storage. + const warehouseTransferUpdated = await this.warehouseTransferModel() + .query(trx) + .findById(warehouseTransferId) + .patch({ + transferInitiatedAt: new Date(), + }); + // Fetches the warehouse transfer with entries. + const warehouseTransfer = await this.warehouseTransferModel() + .query(trx) + .findById(warehouseTransferId) + .withGraphFetched('entries'); + + // Triggers `onWarehouseTransferEdit` event + await this.eventPublisher.emitAsync( + events.warehouseTransfer.onInitiated, + { + warehouseTransfer, + oldWarehouseTransfer, + trx, + } as IWarehouseTransferInitiatedPayload, + ); + return warehouseTransfer; + }); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/TransferredWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/TransferredWarehouseTransfer.ts new file mode 100644 index 000000000..7c29669d2 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/TransferredWarehouseTransfer.ts @@ -0,0 +1,106 @@ +import { Knex } from 'knex'; +import { + IWarehouseTransferTransferingPayload, + IWarehouseTransferTransferredPayload, +} from '@/modules/Warehouses/Warehouse.types'; +import { CommandWarehouseTransfer } from './CommandWarehouseTransfer'; +import { ERRORS } from '../constants'; +import { Inject, Injectable } from '@nestjs/common'; +import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TenantModelProxy } from '../../System/models/TenantBaseModel'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { ServiceError } from '../../Items/ServiceError'; +import { events } from '@/common/events/events'; +import { ModelObject } from 'objection'; + +@Injectable() +export class TransferredWarehouseTransfer { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Validate the warehouse transfer not already transferred. + * @param {IWarehouseTransfer} warehouseTransfer + */ + private validateWarehouseTransferNotTransferred = ( + warehouseTransfer: ModelObject, + ) => { + if (warehouseTransfer.transferDeliveredAt) { + throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED); + } + }; + + /** + * Validate the warehouse transfer should be initiated. + * @param {IWarehouseTransfer} warehouseTransfer + */ + private validateWarehouseTranbsferShouldInitiated = ( + warehouseTransfer: ModelObject, + ) => { + if (!warehouseTransfer.transferInitiatedAt) { + throw new ServiceError(ERRORS.WAREHOUSE_TRANSFER_NOT_INITIATED); + } + }; + + /** + * Transferred warehouse transfer. + * @param {number} warehouseTransferId + * @returns {Promise} + */ + public transferredWarehouseTransfer = async ( + warehouseTransferId: number, + ): Promise> => { + // Retrieves the old warehouse transfer transaction. + const oldWarehouseTransfer = await this.warehouseTransferModel() + .query() + .findById(warehouseTransferId) + .throwIfNotFound(); + + // Validate the warehouse transfer not already transferred. + this.validateWarehouseTransferNotTransferred(oldWarehouseTransfer); + + // Validate the warehouse transfer should be initiated. + this.validateWarehouseTranbsferShouldInitiated(oldWarehouseTransfer); + + // Edits warehouse transfer transaction under unit-of-work envirement. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Triggers `onWarehouseTransferInitiate` event. + await this.eventPublisher.emitAsync(events.warehouseTransfer.onTransfer, { + oldWarehouseTransfer, + trx, + } as IWarehouseTransferTransferingPayload); + + // Updates warehouse transfer graph on the storage. + const warehouseTransferUpdated = await this.warehouseTransferModel() + .query(trx) + .findById(warehouseTransferId) + .patch({ + transferDeliveredAt: new Date(), + }); + // Fetches the warehouse transfer with entries. + const warehouseTransfer = await this.warehouseTransferModel() + .query(trx) + .findById(warehouseTransferId) + .withGraphFetched('entries'); + + // Triggers `onWarehouseTransferEdit` event + await this.eventPublisher.emitAsync( + events.warehouseTransfer.onTransferred, + { + warehouseTransfer, + oldWarehouseTransfer, + trx, + } as IWarehouseTransferTransferredPayload, + ); + return warehouseTransfer; + }); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferAutoIncrement.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferAutoIncrement.ts new file mode 100644 index 000000000..66b8c9fc9 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferAutoIncrement.ts @@ -0,0 +1,28 @@ +import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class WarehouseTransferAutoIncrement { + constructor( + private readonly autoIncrementOrdersService: AutoIncrementOrdersService, + ) {} + + /** + * Retrieve the next unique invoice number. + * @return {Promise} + */ + public getNextTransferNumber(): Promise { + return this.autoIncrementOrdersService.getNextTransactionNumber( + 'warehouse_transfers', + ); + } + + /** + * Increment the invoice next number. + */ + public incrementNextTransferNumber() { + return this.autoIncrementOrdersService.incrementSettingsNextNumber( + 'warehouse_transfers', + ); + } +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferWriteInventoryTransactions.ts b/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferWriteInventoryTransactions.ts new file mode 100644 index 000000000..3525be71d --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/commands/WarehouseTransferWriteInventoryTransactions.ts @@ -0,0 +1,167 @@ +import { Knex } from 'knex'; +import { IWarehouseTransferEntry } from '@/modules/Warehouses/Warehouse.types'; +import { Injectable } from '@nestjs/common'; +import { InventoryTransactionsService } from '../../InventoryCost/commands/InventoryTransactions.service'; +import { ModelObject } from 'objection'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { InventoryTransaction } from '../../InventoryCost/models/InventoryTransaction'; +import { WarehouseTransferEntry } from '../models/WarehouseTransferEntry'; + +@Injectable() +export class WarehouseTransferInventoryTransactions { + constructor(private readonly inventory: InventoryTransactionsService) {} + + /** + * Writes all (initiate and transfer) inventory transactions. + * @param {ModelObject} warehouseTransfer - Warehouse transfer. + * @param {Boolean} override - Override the inventory transactions. + * @param {Knex.Transaction} trx - Knex transcation. + * @returns {Promise} + */ + public writeAllInventoryTransactions = async ( + warehouseTransfer: ModelObject, + override?: boolean, + trx?: Knex.Transaction, + ): Promise => { + const inventoryTransactions = + this.getWarehouseTransferInventoryTransactions(warehouseTransfer); + + await this.inventory.recordInventoryTransactions( + inventoryTransactions, + override, + trx, + ); + }; + + /** + * Writes initiate inventory transactions of warehouse transfer transaction. + * @param {ModelObject} warehouseTransfer - Warehouse transfer. + * @param {boolean} override - Override the inventory transactions. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public writeInitiateInventoryTransactions = async ( + warehouseTransfer: ModelObject, + override?: boolean, + trx?: Knex.Transaction, + ): Promise => { + const inventoryTransactions = + this.getWarehouseFromTransferInventoryTransactions(warehouseTransfer); + + await this.inventory.recordInventoryTransactions( + inventoryTransactions, + override, + trx, + ); + }; + + /** + * Writes transferred inventory transaction of warehouse transfer transaction. + * @param {ModelObject} warehouseTransfer - Warehouse transfer. + * @param {boolean} override - Override the inventory transactions. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public writeTransferredInventoryTransactions = async ( + warehouseTransfer: ModelObject, + override?: boolean, + trx?: Knex.Transaction, + ): Promise => { + const inventoryTransactions = + this.getWarehouseToTransferInventoryTransactions(warehouseTransfer); + + await this.inventory.recordInventoryTransactions( + inventoryTransactions, + override, + trx, + ); + }; + + /** + * Reverts warehouse transfer inventory transactions. + * @param {number} warehouseTransferId - Warehouse transfer id. + * @param {Knex.Transaction} trx - Knex transaction. + * @returns {Promise} + */ + public revertInventoryTransactions = async ( + warehouseTransferId: number, + trx?: Knex.Transaction, + ): Promise => { + await this.inventory.deleteInventoryTransactions( + warehouseTransferId, + 'WarehouseTransfer', + trx, + ); + }; + + /** + * Retrieves the inventory transactions of the given warehouse transfer. + * @param {IWarehouseTransfer} warehouseTransfer + * @returns {IInventoryTransaction[]} + */ + private getWarehouseFromTransferInventoryTransactions = ( + warehouseTransfer: ModelObject, + ) => { + const commonEntry = { + date: warehouseTransfer.date, + transactionType: 'WarehouseTransfer', + transactionId: warehouseTransfer.id, + }; + return warehouseTransfer.entries.map( + (entry: ModelObject) => ({ + ...commonEntry, + entryId: entry.id, + itemId: entry.itemId, + quantity: entry.quantity, + rate: entry.cost, + direction: 'OUT', + warehouseId: warehouseTransfer.fromWarehouseId, + }), + ); + }; + + /** + * Retrieves the inventory transactions of the given warehouse transfer. + * @param {ModelObject} warehouseTransfer - Warehouse transfer. + * @returns {IInventoryTransaction[]} + */ + private getWarehouseToTransferInventoryTransactions = ( + warehouseTransfer: ModelObject, + ) => { + const commonEntry = { + date: warehouseTransfer.date, + transactionType: 'WarehouseTransfer', + transactionId: warehouseTransfer.id, + }; + return warehouseTransfer.entries.map( + (entry: ModelObject) => ({ + ...commonEntry, + entryId: entry.id, + itemId: entry.itemId, + quantity: entry.quantity, + rate: entry.cost, + direction: 'IN', + warehouseId: warehouseTransfer.toWarehouseId, + }), + ); + }; + + /** + * Retrieves the inventory transactions of the given warehouse transfer. + * @param {ModelObject} warehouseTransfer - Warehouse transfer. + * @returns {IInventoryTransaction[]} + */ + private getWarehouseTransferInventoryTransactions = ( + warehouseTransfer: ModelObject, + ) => { + // Retrieve the to inventory transactions of warehouse transfer. + const toTransactions = + this.getWarehouseToTransferInventoryTransactions(warehouseTransfer); + + // Retrieve the from inventory transactions of warehouse transfer. + const fromTransactions = + this.getWarehouseFromTransferInventoryTransactions(warehouseTransfer); + + return [...toTransactions, ...fromTransactions]; + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/constants.ts b/packages/server-nest/src/modules/WarehousesTransfers/constants.ts new file mode 100644 index 000000000..790061627 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/constants.ts @@ -0,0 +1,57 @@ +export const ERRORS = { + WAREHOUSE_TRANSFER_NOT_FOUND: 'WAREHOUSE_TRANSFER_NOT_FOUND', + WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME: + 'WAREHOUSES_TRANSFER_SHOULD_NOT_BE_SAME', + + FROM_WAREHOUSE_NOT_FOUND: 'FROM_WAREHOUSE_NOT_FOUND', + TO_WAREHOUSE_NOT_FOUND: 'TO_WAREHOUSE_NOT_FOUND', + WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY: + 'WAREHOUSE_TRANSFER_ITEMS_SHOULD_BE_INVENTORY', + + WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED: + 'WAREHOUSE_TRANSFER_ALREADY_TRANSFERRED', + + WAREHOUSE_TRANSFER_ALREADY_INITIATED: 'WAREHOUSE_TRANSFER_ALREADY_INITIATED', + WAREHOUSE_TRANSFER_NOT_INITIATED: 'WAREHOUSE_TRANSFER_NOT_INITIATED', +}; + +// Warehouse transfers default views. +export const DEFAULT_VIEWS = [ + { + name: 'warehouse_transfer.view.draft.name', + slug: 'draft', + rolesLogicExpression: '1', + roles: [ + { index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' }, + ], + columns: [], + }, + { + name: 'warehouse_transfer.view.in_transit.name', + slug: 'in-transit', + rolesLogicExpression: '1', + roles: [ + { + index: 1, + fieldKey: 'status', + comparator: 'equals', + value: 'in-transit', + }, + ], + columns: [], + }, + { + name: 'warehouse_transfer.view.transferred.name', + slug: 'transferred', + rolesLogicExpression: '1', + roles: [ + { + index: 1, + fieldKey: 'status', + comparator: 'equals', + value: 'tansferred', + }, + ], + columns: [], + }, +]; diff --git a/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransfer.ts new file mode 100644 index 000000000..080018c1b --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransfer.ts @@ -0,0 +1,152 @@ +import { Model, mixin } from 'objection'; +import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; +import { WarehouseTransferEntry } from './WarehouseTransferEntry'; + +export class WarehouseTransfer extends TenantBaseModel { + public date!: Date; + public transferInitiatedAt!: Date; + public transferDeliveredAt!: Date; + public fromWarehouseId!: number; + public toWarehouseId!: number; + + public entries!: WarehouseTransferEntry[]; + public fromWarehouse!: Warehouse; + public toWarehouse!: Warehouse; + + + /** + * Table name. + */ + static get tableName() { + return 'warehouses_transfers'; + } + + /** + * Timestamps columns. + */ + get timestamps() { + return ['created_at', 'updated_at']; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['isInitiated', 'isTransferred']; + } + + /** + * Detarmines whether the warehouse transfer initiated. + * @retruns {boolean} + */ + get isInitiated() { + return !!this.transferInitiatedAt; + } + + /** + * Detarmines whether the warehouse transfer transferred. + * @returns {boolean} + */ + get isTransferred() { + return !!this.transferDeliveredAt; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + filterByDraft(query) { + query.whereNull('transferInitiatedAt'); + query.whereNull('transferDeliveredAt'); + }, + filterByInTransit(query) { + query.whereNotNull('transferInitiatedAt'); + query.whereNull('transferDeliveredAt'); + }, + filterByTransferred(query) { + query.whereNotNull('transferInitiatedAt'); + query.whereNotNull('transferDeliveredAt'); + }, + filterByStatus(query, status) { + switch (status) { + case 'draft': + default: + return query.modify('filterByDraft'); + case 'in-transit': + return query.modify('filterByInTransit'); + case 'transferred': + return query.modify('filterByTransferred'); + } + }, + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { WarehouseTransferEntry } = require('./WarehouseTransferEntry'); + const { Warehouse } = require('../../Warehouses/models/Warehouse.model'); + + return { + /** + * View model may has many columns. + */ + entries: { + relation: Model.HasManyRelation, + modelClass: WarehouseTransferEntry, + join: { + from: 'warehouses_transfers.id', + to: 'warehouses_transfers_entries.warehouseTransferId', + }, + }, + + /** + * + */ + fromWarehouse: { + relation: Model.BelongsToOneRelation, + modelClass: Warehouse, + join: { + from: 'warehouses_transfers.fromWarehouseId', + to: 'warehouses.id', + }, + }, + + toWarehouse: { + relation: Model.BelongsToOneRelation, + modelClass: Warehouse, + join: { + from: 'warehouses_transfers.toWarehouseId', + to: 'warehouses.id', + }, + }, + }; + } + + /** + * Model settings. + */ + // static get meta() { + // return WarehouseTransferSettings; + // } + + // /** + // * Retrieve the default custom views, roles and columns. + // */ + // static get defaultViews() { + // return DEFAULT_VIEWS; + // } + + /** + * Model search roles. + */ + static get searchRoles() { + return [ + // { fieldKey: 'name', comparator: 'contains' }, + // { condition: 'or', fieldKey: 'code', comparator: 'like' }, + ]; + } +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransferEntry.ts b/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransferEntry.ts new file mode 100644 index 000000000..894bffcfb --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/models/WarehouseTransferEntry.ts @@ -0,0 +1,54 @@ +import { Model } from 'objection'; +import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { WarehouseTransfer } from './WarehouseTransfer'; +import { Item } from '@/modules/Items/models/Item'; + +export class WarehouseTransferEntry extends TenantBaseModel { + public warehouseTransferId!: number; + public itemId!: number; + public quantity!: number; + public cost!: number; + + public warehouseTransfer!: WarehouseTransfer; + public item!: Item; + + /** + * Table name. + */ + static get tableName() { + return 'warehouses_transfers_entries'; + } + + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['total']; + } + + /** + * Invoice amount in local currency. + * @returns {number} + */ + get total() { + return this.cost * this.quantity; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { Item } = require('../../Items/models/Item'); + + return { + item: { + relation: Model.BelongsToOneRelation, + modelClass: Item, + join: { + from: 'warehouses_transfers_entries.itemId', + to: 'items.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfer.ts b/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfer.ts new file mode 100644 index 000000000..b103a22ea --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfer.ts @@ -0,0 +1,43 @@ +import { WarehouseTransferTransformer } from './WarehouseTransferTransfomer'; +import { Inject, Injectable } from '@nestjs/common'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; +import { ModelObject } from 'objection'; + +@Injectable() +export class GetWarehouseTransfer { + constructor( + private readonly transformer: TransformerInjectable, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Retrieves the specific warehouse transfer transaction. + * @param {number} warehouseTransferId + * @param {IEditWarehouseTransferDTO} editWarehouseDTO + * @returns {Promise} + */ + public getWarehouseTransfer = async ( + warehouseTransferId: number, + ): Promise> => { + // Retrieves the old warehouse transfer transaction. + const warehouseTransfer = await this.warehouseTransferModel() + .query() + .findById(warehouseTransferId) + .withGraphFetched('entries.item') + .withGraphFetched('fromWarehouse') + .withGraphFetched('toWarehouse') + .throwIfNotFound(); + + // Retrieves the transfromed warehouse transfers. + return this.transformer.transform( + warehouseTransfer, + new WarehouseTransferTransformer(), + ); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfers.ts b/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfers.ts new file mode 100644 index 000000000..778ff7cfd --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/queries/GetWarehouseTransfers.ts @@ -0,0 +1,70 @@ +import * as R from 'ramda'; +import { WarehouseTransferTransformer } from './WarehouseTransferTransfomer'; +import { IGetWarehousesTransfersFilterDTO } from '../../Warehouses/Warehouse.types'; +import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service'; +import { Inject, Injectable } from '@nestjs/common'; +import { DynamicListService } from '../../DynamicListing/DynamicList.service'; +import { TenantModelProxy } from '../../System/models/TenantBaseModel'; +import { WarehouseTransfer } from '../models/WarehouseTransfer'; + +@Injectable() +export class GetWarehouseTransfers { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, + + @Inject(WarehouseTransfer.name) + private readonly warehouseTransferModel: TenantModelProxy< + typeof WarehouseTransfer + >, + ) {} + + /** + * Parses the sale invoice list filter DTO. + * @param filterDTO + * @returns + */ + private parseListFilterDTO(filterDTO) { + return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); + } + + /** + * Retrieves warehouse transfers paginated list. + * @param {number} tenantId + * @param {IGetWarehousesTransfersFilterDTO} filterDTO + * @returns {} + */ + public getWarehouseTransfers = async ( + filterDTO: IGetWarehousesTransfersFilterDTO, + ) => { + // Parses stringified filter roles. + const filter = this.parseListFilterDTO(filterDTO); + + // Dynamic list service. + const dynamicFilter = await this.dynamicListService.dynamicList( + this.warehouseTransferModel(), + filter, + ); + const { results, pagination } = await this.warehouseTransferModel() + .query() + .onBuild((query) => { + query.withGraphFetched('entries.item'); + query.withGraphFetched('fromWarehouse'); + query.withGraphFetched('toWarehouse'); + + dynamicFilter.buildQuery()(query); + }) + .pagination(filter.page - 1, filter.pageSize); + + // Retrieves the transformed warehouse transfers + const warehousesTransfers = await this.transformer.transform( + results, + new WarehouseTransferTransformer(), + ); + return { + warehousesTransfers, + pagination, + filter, + }; + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferItemTransformer.ts b/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferItemTransformer.ts new file mode 100644 index 000000000..d1c4a6055 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferItemTransformer.ts @@ -0,0 +1,38 @@ +import { Transformer } from "../../Transformer/Transformer"; + +export class WarehouseTransferItemTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['formattedQuantity', 'formattedCost', 'formattedTotal']; + }; + + /** + * Formats the total. + * @param {IWarehouseTransferEntry} entry + * @returns {string} + */ + public formattedTotal = (entry) => { + return this.formatMoney(entry.total); + }; + + /** + * Formats the quantity. + * @param {IWarehouseTransferEntry} entry + * @returns {string} + */ + public formattedQuantity = (entry) => { + return this.formatNumber(entry.quantity); + }; + + /** + * Formats the cost. + * @param {IWarehouseTransferEntry} entry + * @returns {string} + */ + public formattedCost = (entry) => { + return this.formatMoney(entry.cost); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferTransfomer.ts b/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferTransfomer.ts new file mode 100644 index 000000000..13f2e8152 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/queries/WarehouseTransferTransfomer.ts @@ -0,0 +1,27 @@ +import { WarehouseTransferItemTransformer } from './WarehouseTransferItemTransformer'; +import { Transformer } from '@/modules/Transformer/Transformer'; +export class WarehouseTransferTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {Array} + */ + public includeAttributes = (): string[] => { + return ['formattedDate', 'entries']; + }; + + /** + * + * @param transfer + * @returns + */ + protected formattedDate = (transfer) => { + return this.formatDate(transfer.date); + }; + + /** + * + */ + protected entries = (transfer) => { + return this.item(transfer.entries, new WarehouseTransferItemTransformer()); + }; +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferAutoIncrementSubscriber.ts b/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferAutoIncrementSubscriber.ts new file mode 100644 index 000000000..71db02033 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferAutoIncrementSubscriber.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; +import { IWarehouseTransferCreated } from '../../Warehouses/Warehouse.types'; +import { WarehouseTransferAutoIncrement } from '../commands/WarehouseTransferAutoIncrement'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class WarehouseTransferAutoIncrementSubscriber { + constructor( + private readonly warehouseTransferAutoIncrement: WarehouseTransferAutoIncrement, + ) {} + + /** + * Writes inventory transactions once warehouse transfer created. + * @param {IInventoryTransactionsCreatedPayload} - + */ + @OnEvent(events.warehouseTransfer.onCreated) + async incrementTransferAutoIncrementOnCreated({}: IWarehouseTransferCreated) { + await this.warehouseTransferAutoIncrement.incrementNextTransferNumber(); + } +} diff --git a/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferInventoryTransactionsSubscriber.ts b/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferInventoryTransactionsSubscriber.ts new file mode 100644 index 000000000..46cc72ab4 --- /dev/null +++ b/packages/server-nest/src/modules/WarehousesTransfers/susbcribers/WarehouseTransferInventoryTransactionsSubscriber.ts @@ -0,0 +1,123 @@ +import { + IWarehouseTransferEditedPayload, + IWarehouseTransferDeletedPayload, + IWarehouseTransferCreated, + IWarehouseTransferInitiatedPayload, + IWarehouseTransferTransferredPayload, +} from '@/modules/Warehouses/Warehouse.types'; +import { WarehouseTransferInventoryTransactions } from '../commands/WarehouseTransferWriteInventoryTransactions'; +import { OnEvent } from '@nestjs/event-emitter'; +import { events } from '@/common/events/events'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class WarehouseTransferInventoryTransactionsSubscriber { + constructor( + private readonly warehouseTransferInventoryTransactions: WarehouseTransferInventoryTransactions, + ) {} + + /** + * Writes inventory transactions once warehouse transfer created. + * @param {IInventoryTransactionsCreatedPayload} - + */ + @OnEvent(events.warehouseTransfer.onCreated) + async writeInventoryTransactionsOnWarehouseTransferCreated({ + warehouseTransfer, + trx, + }: IWarehouseTransferCreated) { + // Can't continue if the warehouse transfer is not initiated yet. + if (!warehouseTransfer.isInitiated) return; + + // Write all inventory transaction if warehouse transfer initiated and transferred. + if (warehouseTransfer.isInitiated && warehouseTransfer.isTransferred) { + await this.warehouseTransferInventoryTransactions.writeAllInventoryTransactions( + warehouseTransfer, + false, + trx, + ); + // Write initiate inventory transaction if warehouse transfer initited and transferred yet. + } else if (warehouseTransfer.isInitiated) { + await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions( + warehouseTransfer, + false, + trx, + ); + } + } + + /** + * Rewrite inventory transactions once warehouse transfer edited. + * @param {IWarehouseTransferEditedPayload} - + */ + @OnEvent(events.warehouseTransfer.onEdited) + async rewriteInventoryTransactionsOnWarehouseTransferEdited({ + warehouseTransfer, + trx, + }: IWarehouseTransferEditedPayload) { + // Can't continue if the warehouse transfer is not initiated yet. + if (!warehouseTransfer.isInitiated) return; + + // Write all inventory transaction if warehouse transfer initiated and transferred. + if (warehouseTransfer.isInitiated && warehouseTransfer.isTransferred) { + await this.warehouseTransferInventoryTransactions.writeAllInventoryTransactions( + warehouseTransfer, + true, + trx, + ); + // Write initiate inventory transaction if warehouse transfer initited and transferred yet. + } else if (warehouseTransfer.isInitiated) { + await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions( + warehouseTransfer, + true, + trx, + ); + } + } + + /** + * Reverts inventory transactions once warehouse transfer deleted. + * @parma {IWarehouseTransferDeletedPayload} - + */ + @OnEvent(events.warehouseTransfer.onDeleted) + async revertInventoryTransactionsOnWarehouseTransferDeleted({ + oldWarehouseTransfer, + trx, + }: IWarehouseTransferDeletedPayload) { + await this.warehouseTransferInventoryTransactions.revertInventoryTransactions( + oldWarehouseTransfer.id, + trx, + ); + } + + /** + * Write inventory transactions of warehouse transfer once the transfer initiated. + * @param {IWarehouseTransferInitiatedPayload} + */ + @OnEvent(events.warehouseTransfer.onInitiated) + async writeInventoryTransactionsOnTransferInitiated({ + trx, + warehouseTransfer, + }: IWarehouseTransferInitiatedPayload) { + await this.warehouseTransferInventoryTransactions.writeInitiateInventoryTransactions( + warehouseTransfer, + false, + trx, + ); + } + + /** + * Write inventory transactions of warehouse transfer once the transfer completed. + * @param {IWarehouseTransferTransferredPayload} + */ + @OnEvent(events.warehouseTransfer.onTransferred) + async writeInventoryTransactionsOnTransferred({ + trx, + warehouseTransfer, + }: IWarehouseTransferTransferredPayload) { + await this.warehouseTransferInventoryTransactions.writeTransferredInventoryTransactions( + warehouseTransfer, + false, + trx, + ); + } +} diff --git a/packages/server-nest/tsconfig.json b/packages/server-nest/tsconfig.json index b32c9b439..5f0ef4040 100644 --- a/packages/server-nest/tsconfig.json +++ b/packages/server-nest/tsconfig.json @@ -18,7 +18,8 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@bigcapital/server/*": ["../server/*"] } } } diff --git a/packages/server/src/models/TaxRateTransaction.ts b/packages/server/src/models/TaxRateTransaction.ts index 3cbca88a0..f38c655eb 100644 --- a/packages/server/src/models/TaxRateTransaction.ts +++ b/packages/server/src/models/TaxRateTransaction.ts @@ -5,6 +5,11 @@ import ModelSearchable from './ModelSearchable'; export default class TaxRateTransaction extends mixin(TenantModel, [ ModelSearchable, ]) { + public rate: number; + public referenceId: number; + public referenceType: string; + public taxRateId: number; + /** * Table name */