refactor: inventory transfers to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-03-13 00:44:11 +02:00
parent 67ae7ad037
commit cf496909a5
48 changed files with 2334 additions and 135 deletions

View File

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

View File

@@ -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() {}
/**

View File

@@ -72,7 +72,6 @@ export class InventoryComputeCostService {
* @param {Date} startingDate
*/
async scheduleComputeItemCost(
tenantId: number,
itemId: number,
startingDate: Date | string,
) {

View File

@@ -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<number, IInventoryItemCostMeta>} INValuationMap -
* @param {Map<number, IInventoryItemCostMeta>} 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<number, IInventoryItemCostMeta>,
OUTValuationMap: Map<number, IInventoryItemCostMeta>,
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<any> => {
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]),
);

View File

@@ -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,

View File

@@ -33,7 +33,7 @@ export class InventoryTransactionsService {
* @return {Promise<void>}
*/
async recordInventoryTransactions(
transactions: InventoryTransaction[],
transactions: ModelObject<InventoryTransaction>[],
override: boolean = false,
trx?: Knex.Transaction,
): Promise<void> {

View File

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

View File

@@ -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<ComputeItemCostJobPayload>,
) {}
private readonly eventEmitter: EventEmitter2,
) {
super();
}
/**
* Handle compute item cost job.
* Process the compute item cost job.
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
*/
async handleComputeItemCost() {
const { itemId, startingDate, organizationId, userId } = this.jobRef.data;
async process(job: Job<ComputeItemCostJobPayload>) {
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;
}
}
}

View File

@@ -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() {}
}

View File

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

View File

@@ -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';