refactor: inventory adjustments to nestjs

This commit is contained in:
Ahmed Bouhuolia
2025-01-07 22:17:23 +02:00
parent abf92ac83f
commit 1773df1858
15 changed files with 1154 additions and 1 deletions

View File

@@ -62,6 +62,7 @@ import { BankingMatchingModule } from '../BankingMatching/BankingMatching.module
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { TransactionsLockingModule } from '../TransactionsLocking/TransactionsLocking.module';
import { SettingsModule } from '../Settings/Settings.module';
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
@Module({
imports: [
@@ -147,7 +148,8 @@ import { SettingsModule } from '../Settings/Settings.module';
BankingTransactionsRegonizeModule,
BankingMatchingModule,
TransactionsLockingModule,
SettingsModule
SettingsModule,
InventoryAdjustmentsModule
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,226 @@
// import { Service, Inject } from 'typedi';
// import { Knex } from 'knex';
// import * as R from 'ramda';
// import TenancyService from '@/services/Tenancy/TenancyService';
// import {
// AccountNormal,
// IInventoryAdjustment,
// IInventoryAdjustmentEntry,
// ILedgerEntry,
// } from '@/interfaces';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import { TenantMetadata } from '@/system/models';
// @Service()
// export default class InventoryAdjustmentsGL {
// @Inject()
// private tenancy: TenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Retrieves the inventory adjustment common GL entry.
// * @param {InventoryAdjustment} inventoryAdjustment -
// * @param {string} baseCurrency -
// * @returns {ILedgerEntry}
// */
// private getAdjustmentGLCommonEntry = (
// inventoryAdjustment: IInventoryAdjustment,
// baseCurrency: string
// ) => {
// return {
// currencyCode: baseCurrency,
// exchangeRate: 1,
// transactionId: inventoryAdjustment.id,
// transactionType: 'InventoryAdjustment',
// referenceNumber: inventoryAdjustment.referenceNo,
// date: inventoryAdjustment.date,
// userId: inventoryAdjustment.userId,
// branchId: inventoryAdjustment.branchId,
// createdAt: inventoryAdjustment.createdAt,
// credit: 0,
// debit: 0,
// };
// };
// /**
// * Retrieve the inventory adjustment inventory GL entry.
// * @param {IInventoryAdjustment} inventoryAdjustment -Inventory adjustment model.
// * @param {string} baseCurrency - Base currency of the organization.
// * @param {IInventoryAdjustmentEntry} entry -
// * @param {number} index -
// * @returns {ILedgerEntry}
// */
// private getAdjustmentGLInventoryEntry = R.curry(
// (
// inventoryAdjustment: IInventoryAdjustment,
// baseCurrency: string,
// entry: IInventoryAdjustmentEntry,
// index: number
// ): ILedgerEntry => {
// const commonEntry = this.getAdjustmentGLCommonEntry(
// inventoryAdjustment,
// baseCurrency
// );
// const amount = entry.cost * entry.quantity;
// return {
// ...commonEntry,
// debit: amount,
// accountId: entry.item.inventoryAccountId,
// accountNormal: AccountNormal.DEBIT,
// index,
// };
// }
// );
// /**
// * Retrieves the inventory adjustment
// * @param {IInventoryAdjustment} inventoryAdjustment
// * @param {IInventoryAdjustmentEntry} entry
// * @returns {ILedgerEntry}
// */
// private getAdjustmentGLCostEntry = R.curry(
// (
// inventoryAdjustment: IInventoryAdjustment,
// baseCurrency: string,
// entry: IInventoryAdjustmentEntry,
// index: number
// ): ILedgerEntry => {
// const commonEntry = this.getAdjustmentGLCommonEntry(
// inventoryAdjustment,
// baseCurrency
// );
// const amount = entry.cost * entry.quantity;
// return {
// ...commonEntry,
// accountId: inventoryAdjustment.adjustmentAccountId,
// accountNormal: AccountNormal.DEBIT,
// credit: amount,
// index: index + 2,
// };
// }
// );
// /**
// * Retrieve the inventory adjustment GL item entry.
// * @param {InventoryAdjustment} adjustment
// * @param {string} baseCurrency
// * @param {InventoryAdjustmentEntry} entry
// * @param {number} index
// * @returns {}
// */
// private getAdjustmentGLItemEntry = R.curry(
// (
// adjustment: IInventoryAdjustment,
// baseCurrency: string,
// entry: IInventoryAdjustmentEntry,
// index: number
// ): ILedgerEntry[] => {
// const getInventoryEntry = this.getAdjustmentGLInventoryEntry(
// adjustment,
// baseCurrency
// );
// const inventoryEntry = getInventoryEntry(entry, index);
// const costEntry = this.getAdjustmentGLCostEntry(
// adjustment,
// baseCurrency,
// entry,
// index
// );
// return [inventoryEntry, costEntry];
// }
// );
// /**
// * Writes increment inventroy adjustment GL entries.
// * @param {InventoryAdjustment} inventoryAdjustment -
// * @param {JournalPoster} jorunal -
// * @returns {ILedgerEntry[]}
// */
// public getIncrementAdjustmentGLEntries(
// inventoryAdjustment: IInventoryAdjustment,
// baseCurrency: string
// ): ILedgerEntry[] {
// const getItemEntry = this.getAdjustmentGLItemEntry(
// inventoryAdjustment,
// baseCurrency
// );
// return inventoryAdjustment.entries.map(getItemEntry).flat();
// }
// /**
// * Writes inventory increment adjustment GL entries.
// * @param {number} tenantId
// * @param {number} inventoryAdjustmentId
// */
// public writeAdjustmentGLEntries = async (
// tenantId: number,
// inventoryAdjustmentId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { InventoryAdjustment } = this.tenancy.models(tenantId);
// // Retrieves the inventory adjustment with associated entries.
// const adjustment = await InventoryAdjustment.query(trx)
// .findById(inventoryAdjustmentId)
// .withGraphFetched('entries.item');
// const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// // Retrieves the inventory adjustment GL entries.
// const entries = this.getIncrementAdjustmentGLEntries(
// adjustment,
// tenantMeta.baseCurrency
// );
// const ledger = new Ledger(entries);
// // Commits the ledger entries to the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Reverts the adjustment transactions GL entries.
// * @param {number} tenantId
// * @param {number} inventoryAdjustmentId
// * @returns {Promise<void>}
// */
// public revertAdjustmentGLEntries = (
// tenantId: number,
// inventoryAdjustmentId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// return this.ledgerStorage.deleteByReference(
// tenantId,
// inventoryAdjustmentId,
// 'InventoryAdjustment',
// trx
// );
// };
// /**
// * Rewrite inventory adjustment GL entries.
// * @param {number} tenantId
// * @param {number} inventoryAdjustmentId
// * @param {Knex.Transaction} trx
// */
// public rewriteAdjustmentGLEntries = async (
// tenantId: number,
// inventoryAdjustmentId: number,
// trx?: Knex.Transaction
// ) => {
// // Reverts GL entries of the given inventory adjustment.
// await this.revertAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx);
// // Writes GL entries of th egiven inventory adjustment.
// await this.writeAdjustmentGLEntries(tenantId, inventoryAdjustmentId, trx);
// };
// }

View File

@@ -0,0 +1,154 @@
// import { Inject, Service } from 'typedi';
// import { omit } from 'lodash';
// import moment from 'moment';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { ServiceError } from '@/exceptions';
// import {
// IInventoryAdjustment,
// IPaginationMeta,
// IInventoryAdjustmentsFilter,
// IInventoryTransaction,
// IInventoryAdjustmentEventPublishedPayload,
// IInventoryAdjustmentEventDeletedPayload,
// IInventoryAdjustmentDeletingPayload,
// IInventoryAdjustmentPublishingPayload,
// } from '@/interfaces';
// import events from '@/subscribers/events';
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import InventoryService from './Inventory';
// import UnitOfWork from '@/services/UnitOfWork';
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
// import InventoryAdjustmentTransformer from './InventoryAdjustmentTransformer';
// import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
// import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
// const ERRORS = {
// INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND',
// ITEM_SHOULD_BE_INVENTORY_TYPE: 'ITEM_SHOULD_BE_INVENTORY_TYPE',
// INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED:
// 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED',
// };
// @Service()
// export default class InventoryAdjustmentService {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private eventPublisher: EventPublisher;
// @Inject()
// private inventoryService: InventoryService;
// @Inject()
// private dynamicListService: DynamicListingService;
// @Inject()
// private uow: UnitOfWork;
// @Inject()
// private branchDTOTransform: BranchTransactionDTOTransform;
// @Inject()
// private warehouseDTOTransform: WarehouseTransactionDTOTransform;
// @Inject()
// private transfromer: TransformerInjectable;
// /**
// * Retrieve the inventory adjustment or throw not found service error.
// * @param {number} tenantId -
// * @param {number} adjustmentId -
// */
// async getInventoryAdjustmentOrThrowError(
// tenantId: number,
// adjustmentId: number
// ) {
// const { InventoryAdjustment } = this.tenancy.models(tenantId);
// const inventoryAdjustment = await InventoryAdjustment.query()
// .findById(adjustmentId)
// .withGraphFetched('entries');
// if (!inventoryAdjustment) {
// throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND);
// }
// return inventoryAdjustment;
// }
// /**
// * Parses inventory adjustments list filter DTO.
// * @param filterDTO -
// */
// private parseListFilterDTO(filterDTO) {
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
// }
// /**
// * Writes the inventory transactions from the inventory adjustment transaction.
// * @param {number} tenantId -
// * @param {IInventoryAdjustment} inventoryAdjustment -
// * @param {boolean} override -
// * @param {Knex.Transaction} trx -
// * @return {Promise<void>}
// */
// public async writeInventoryTransactions(
// tenantId: number,
// inventoryAdjustment: IInventoryAdjustment,
// override: boolean = false,
// trx?: Knex.Transaction
// ): Promise<void> {
// const commonTransaction = {
// direction: inventoryAdjustment.inventoryDirection,
// date: inventoryAdjustment.date,
// transactionType: 'InventoryAdjustment',
// transactionId: inventoryAdjustment.id,
// createdAt: inventoryAdjustment.createdAt,
// costAccountId: inventoryAdjustment.adjustmentAccountId,
// branchId: inventoryAdjustment.branchId,
// warehouseId: inventoryAdjustment.warehouseId,
// };
// const inventoryTransactions = [];
// inventoryAdjustment.entries.forEach((entry) => {
// inventoryTransactions.push({
// ...commonTransaction,
// itemId: entry.itemId,
// quantity: entry.quantity,
// rate: entry.cost,
// });
// });
// // Saves the given inventory transactions to the storage.
// await this.inventoryService.recordInventoryTransactions(
// tenantId,
// inventoryTransactions,
// override,
// trx
// );
// }
// /**
// * Reverts the inventory transactions from the inventory adjustment transaction.
// * @param {number} tenantId
// * @param {number} inventoryAdjustmentId
// */
// async revertInventoryTransactions(
// tenantId: number,
// inventoryAdjustmentId: number,
// trx?: Knex.Transaction
// ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> {
// return this.inventoryService.deleteInventoryTransactions(
// tenantId,
// inventoryAdjustmentId,
// 'InventoryAdjustment',
// trx
// );
// }
// }

View File

@@ -0,0 +1,25 @@
import { Transformer } from "../Transformer/Transformer";
import { InventoryAdjustment } from "./models/InventoryAdjustment";
export class InventoryAdjustmentTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['formattedType'];
};
/**
* Retrieves the formatted and localized adjustment type.
* @param {IInventoryAdjustment} inventoryAdjustment
* @returns {string}
*/
formattedType(inventoryAdjustment: InventoryAdjustment) {
const types = {
increment: 'inventory_adjustment.type.increment',
decrement: 'inventory_adjustment.type.decrement',
};
return this.context.i18n.t(types[inventoryAdjustment.type] || '');
}
}

View File

@@ -0,0 +1,38 @@
import { Body, Controller, Delete, Param, Post } from '@nestjs/common';
import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service';
import { IQuickInventoryAdjustmentDTO } from './types/InventoryAdjustments.types';
import { InventoryAdjustment } from './models/InventoryAdjustment';
@Controller('inventory-adjustments')
export class InventoryAdjustmentsController {
constructor(
private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService,
) {}
@Post('quick')
public async createQuickInventoryAdjustment(
@Body() quickAdjustmentDTO: IQuickInventoryAdjustmentDTO,
): Promise<InventoryAdjustment> {
return this.inventoryAdjustmentsApplicationService.createQuickInventoryAdjustment(
quickAdjustmentDTO,
);
}
@Delete(':id')
public async deleteInventoryAdjustment(
@Param('id') inventoryAdjustmentId: number,
): Promise<void> {
return this.inventoryAdjustmentsApplicationService.deleteInventoryAdjustment(
inventoryAdjustmentId,
);
}
@Post(':id/publish')
public async publishInventoryAdjustment(
@Param('id') inventoryAdjustmentId: number,
): Promise<void> {
return this.inventoryAdjustmentsApplicationService.publishInventoryAdjustment(
inventoryAdjustmentId,
);
}
}

View File

@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { InventoryAdjustment } from './models/InventoryAdjustment';
import { InventoryAdjustmentEntry } from './models/InventoryAdjustmentEntry';
import { CreateQuickInventoryAdjustmentService } from './commands/CreateQuickInventoryAdjustment.service';
import { PublishInventoryAdjustmentService } from './commands/PublishInventoryAdjustment.service';
import { GetInventoryAdjustmentService } from './queries/GetInventoryAdjustment.service';
import { GetInventoryAdjustmentsService } from './queries/GetInventoryAdjustments.service';
import { DeleteInventoryAdjustmentService } from './commands/DeleteInventoryAdjustment.service';
import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service';
import { InventoryAdjustmentsController } from './InventoryAdjustments.controller';
const models = [
RegisterTenancyModel(InventoryAdjustment),
RegisterTenancyModel(InventoryAdjustmentEntry),
];
@Module({
controllers: [InventoryAdjustmentsController],
providers: [
...models,
CreateQuickInventoryAdjustmentService,
PublishInventoryAdjustmentService,
GetInventoryAdjustmentsService,
GetInventoryAdjustmentService,
DeleteInventoryAdjustmentService,
InventoryAdjustmentsApplicationService,
],
exports: [...models],
})
export class InventoryAdjustmentsModule {}

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { DeleteInventoryAdjustmentService } from './commands/DeleteInventoryAdjustment.service';
import { PublishInventoryAdjustmentService } from './commands/PublishInventoryAdjustment.service';
import { CreateQuickInventoryAdjustmentService } from './commands/CreateQuickInventoryAdjustment.service';
import { IQuickInventoryAdjustmentDTO } from './types/InventoryAdjustments.types';
import { InventoryAdjustment } from './models/InventoryAdjustment';
@Injectable()
export class InventoryAdjustmentsApplicationService {
constructor(
private readonly createQuickInventoryAdjustmentService: CreateQuickInventoryAdjustmentService,
private readonly deleteInventoryAdjustmentService: DeleteInventoryAdjustmentService,
private readonly publishInventoryAdjustmentService: PublishInventoryAdjustmentService,
) {}
/**
* Creates a quick inventory adjustment transaction.
* @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - Quick inventory adjustment DTO.
*/
public async createQuickInventoryAdjustment(
quickAdjustmentDTO: IQuickInventoryAdjustmentDTO,
): Promise<InventoryAdjustment> {
return this.createQuickInventoryAdjustmentService.createQuickAdjustment(
quickAdjustmentDTO,
);
}
/**
* Deletes the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
*/
public async deleteInventoryAdjustment(
inventoryAdjustmentId: number,
): Promise<void> {
return this.deleteInventoryAdjustmentService.deleteInventoryAdjustment(
inventoryAdjustmentId,
);
}
/**
* Publishes the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
*/
public async publishInventoryAdjustment(
inventoryAdjustmentId: number,
): Promise<void> {
return this.publishInventoryAdjustmentService.publishInventoryAdjustment(
inventoryAdjustmentId,
);
}
}

View File

@@ -0,0 +1,147 @@
import { Knex } from 'knex';
import { Inject } from '@nestjs/common';
import * as R from 'ramda';
import { omit } from 'lodash';
import { events } from '@/common/events/events';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry';
import {
IInventoryAdjustmentCreatingPayload,
IInventoryAdjustmentEventCreatedPayload,
IQuickInventoryAdjustmentDTO,
} from '../types/InventoryAdjustments.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ServiceError } from '@/modules/Items/ServiceError';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Item } from '@/modules/Items/models/Item';
import { Account } from '@/modules/Accounts/models/Account.model';
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
export class CreateQuickInventoryAdjustmentService {
constructor(
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: typeof InventoryAdjustment,
@Inject(InventoryAdjustmentEntry.name)
private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry,
@Inject(Item.name)
private readonly itemModel: typeof Item,
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
) {}
/**
* Transformes the quick inventory adjustment DTO to model object.
* @param {IQuickInventoryAdjustmentDTO} adjustmentDTO -
* @return {IInventoryAdjustment}
*/
private transformQuickAdjToModel(
adjustmentDTO: IQuickInventoryAdjustmentDTO,
): InventoryAdjustment {
const entries = [
{
index: 1,
itemId: adjustmentDTO.itemId,
...('increment' === adjustmentDTO.type
? {
quantity: adjustmentDTO.quantity,
cost: adjustmentDTO.cost,
}
: {}),
...('decrement' === adjustmentDTO.type
? {
quantity: adjustmentDTO.quantity,
}
: {}),
},
];
const initialDTO = {
...omit(adjustmentDTO, ['quantity', 'cost', 'itemId', 'publish']),
userId: authorizedUser.id,
...(adjustmentDTO.publish
? {
publishedAt: moment().toMySqlDateTime(),
}
: {}),
entries,
};
return R.compose(
this.warehouseDTOTransform.transformDTO<InventoryAdjustment>,
this.branchDTOTransform.transformDTO<InventoryAdjustment>,
)(initialDTO) as InventoryAdjustment;
}
/**
* Creates a quick inventory adjustment for specific item.
* @param {number} tenantId - Tenant id.
* @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - qucik adjustment DTO.
*/
public async createQuickAdjustment(
quickAdjustmentDTO: IQuickInventoryAdjustmentDTO,
): Promise<InventoryAdjustment> {
// Retrieve the adjustment account or throw not found error.
const adjustmentAccount = await this.accountModel
.query()
.findById(quickAdjustmentDTO.adjustmentAccountId)
.throwIfNotFound();
// Retrieve the item model or throw not found service error.
const item = await this.itemModel
.query()
.findById(quickAdjustmentDTO.itemId)
.throwIfNotFound();
// Validate item inventory type.
this.validateItemInventoryType(item);
// Transform the DTO to inventory adjustment model.
const invAdjustmentObject =
this.transformQuickAdjToModel(quickAdjustmentDTO);
// Writes inventory adjustment transaction with associated transactions
// under unit-of-work envirment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onInventoryAdjustmentCreating` event.
await this.eventEmitter.emitAsync(
events.inventoryAdjustment.onQuickCreating,
{
quickAdjustmentDTO,
trx,
} as IInventoryAdjustmentCreatingPayload,
);
// Saves the inventory adjustment with associated entries to the storage.
const inventoryAdjustment = await this.inventoryAdjustmentModel
.query(trx)
.upsertGraph({
...invAdjustmentObject,
});
// Triggers `onInventoryAdjustmentQuickCreated` event.
await this.eventEmitter.emitAsync(
events.inventoryAdjustment.onQuickCreated,
{
inventoryAdjustment,
inventoryAdjustmentId: inventoryAdjustment.id,
trx,
} as IInventoryAdjustmentEventCreatedPayload,
);
return inventoryAdjustment;
});
}
/**
* Validate the item inventory type.
* @param {IItem} item
*/
validateItemInventoryType(item) {
if (item.type !== 'inventory') {
throw new ServiceError(ERRORS.ITEM_SHOULD_BE_INVENTORY_TYPE);
}
}
}

View File

@@ -0,0 +1,67 @@
import { Inject } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import {
IInventoryAdjustmentDeletingPayload,
IInventoryAdjustmentEventDeletedPayload,
} from '../types/InventoryAdjustments.types';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
export class DeleteInventoryAdjustmentService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: typeof InventoryAdjustment,
@Inject(InventoryAdjustmentEntry.name)
private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry,
) {}
/**
* Deletes the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
*/
public async deleteInventoryAdjustment(
inventoryAdjustmentId: number,
): Promise<void> {
// Retrieve the inventory adjustment or throw not found service error.
const oldInventoryAdjustment = await this.inventoryAdjustmentModel
.query()
.findById(inventoryAdjustmentId)
.throwIfNotFound();
// Deletes the inventory adjustment transaction and associated transactions
// under unit-of-work env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onInventoryAdjustmentDeleting` event.
await this.eventEmitter.emitAsync(events.inventoryAdjustment.onDeleting, {
oldInventoryAdjustment,
trx,
} as IInventoryAdjustmentDeletingPayload);
// Deletes the inventory adjustment entries.
await this.inventoryAdjustmentEntryModel
.query(trx)
.where('adjustment_id', inventoryAdjustmentId)
.delete();
// Deletes the inventory adjustment transaction.
await this.inventoryAdjustmentModel
.query(trx)
.findById(inventoryAdjustmentId)
.delete();
// Triggers `onInventoryAdjustmentDeleted` event.
await this.eventEmitter.emitAsync(events.inventoryAdjustment.onDeleted, {
inventoryAdjustmentId,
oldInventoryAdjustment,
trx,
} as IInventoryAdjustmentEventDeletedPayload);
});
}
}

View File

@@ -0,0 +1,97 @@
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Inject } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import { InventoryAdjustmentEntry } from '../models/InventoryAdjustmentEntry';
import {
IInventoryAdjustmentEventPublishedPayload,
IInventoryAdjustmentPublishingPayload,
} from '../types/InventoryAdjustments.types';
import { events } from '@/common/events/events';
import { Knex } from 'knex';
import { ServiceError } from '@/modules/Items/ServiceError';
export class PublishInventoryAdjustmentService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: typeof InventoryAdjustment,
@Inject(InventoryAdjustmentEntry.name)
private readonly inventoryAdjustmentEntryModel: typeof InventoryAdjustmentEntry,
) {}
/**
* Publish the inventory adjustment transaction.
* @param {number} tenantId
* @param {number} inventoryAdjustmentId
*/
public async publishInventoryAdjustment(
inventoryAdjustmentId: number,
): Promise<void> {
// Retrieve the inventory adjustment or throw not found service error.
const oldInventoryAdjustment = await this.inventoryAdjustmentModel
.query()
.findById(inventoryAdjustmentId)
.throwIfNotFound();
// Validate adjustment not already published.
this.validateAdjustmentTransactionsNotPublished(oldInventoryAdjustment);
// Publishes inventory adjustment with associated inventory transactions
// under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
await this.eventEmitter.emitAsync(
events.inventoryAdjustment.onPublishing,
{
trx,
oldInventoryAdjustment,
} as IInventoryAdjustmentPublishingPayload,
);
// Publish the inventory adjustment transaction.
await InventoryAdjustment.query().findById(inventoryAdjustmentId).patch({
publishedAt: moment().toMySqlDateTime(),
});
// Retrieve the inventory adjustment after the modification.
const inventoryAdjustment = await InventoryAdjustment.query()
.findById(inventoryAdjustmentId)
.withGraphFetched('entries');
// Triggers `onInventoryAdjustmentDeleted` event.
await this.eventEmitter.emitAsync(
events.inventoryAdjustment.onPublished,
{
inventoryAdjustmentId,
inventoryAdjustment,
oldInventoryAdjustment,
trx,
} as IInventoryAdjustmentEventPublishedPayload,
);
});
}
/**
* Validate the adjustment transaction is exists.
* @param {IInventoryAdjustment} inventoryAdjustment
*/
private throwIfAdjustmentNotFound(inventoryAdjustment: InventoryAdjustment) {
if (!inventoryAdjustment) {
throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_NOT_FOUND);
}
}
/**
* Validates the adjustment transaction is not already published.
* @param {IInventoryAdjustment} oldInventoryAdjustment
*/
private validateAdjustmentTransactionsNotPublished(
oldInventoryAdjustment: InventoryAdjustment,
) {
if (oldInventoryAdjustment.isPublished) {
throw new ServiceError(ERRORS.INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED);
}
}
}

View File

@@ -0,0 +1,124 @@
import { Model, mixin } from 'objection';
// import TenantModel from 'models/TenantModel';
// import InventoryAdjustmentSettings from './InventoryAdjustment.Settings';
// import ModelSetting from './ModelSetting';
import { BaseModel } from '@/models/Model';
export class InventoryAdjustment extends BaseModel {
date!: string;
type!: string;
adjustmentAccountId!: number;
reason?: string;
referenceNo!: string;
description?: string;
userId!: number;
publishedAt?: string;
branchId!: number;
warehouseId!: number;
/**
* Table name
*/
static get tableName() {
return 'inventory_adjustments';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['formattedType', 'inventoryDirection', 'isPublished'];
}
/**
* Retrieve formatted adjustment type.
*/
get formattedType() {
return InventoryAdjustment.getFormattedType(this.type);
}
/**
* Retrieve formatted reference type.
*/
get inventoryDirection() {
return InventoryAdjustment.getInventoryDirection(this.type);
}
/**
* Detarmines whether the adjustment is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
static getInventoryDirection(type) {
const directions = {
increment: 'IN',
decrement: 'OUT',
};
return directions[type] || '';
}
/**
* Retrieve the formatted adjustment type of the given type.
* @param {string} type
* @returns {string}
*/
static getFormattedType(type) {
const types = {
increment: 'inventory_adjustment.type.increment',
decrement: 'inventory_adjustment.type.decrement',
};
return types[type];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { InventoryAdjustmentEntry } = require('./InventoryAdjustmentEntry');
const { Account } = require('../../Accounts/models/Account.model');
return {
/**
* Adjustment entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: InventoryAdjustmentEntry,
join: {
from: 'inventory_adjustments.id',
to: 'inventory_adjustments_entries.adjustmentId',
},
},
/**
* Inventory adjustment account.
*/
adjustmentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'inventory_adjustments.adjustmentAccountId',
to: 'accounts.id',
},
},
};
}
/**
* Model settings.
*/
// static get meta() {
// return InventoryAdjustmentSettings;
// }
}

View File

@@ -0,0 +1,43 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
// import TenantModel from 'models/TenantModel';
export class InventoryAdjustmentEntry extends BaseModel {
/**
* Table name.
*/
static get tableName() {
return 'inventory_adjustments_entries';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { InventoryAdjustment } = require('./InventoryAdjustment');
const { Item } = require('../../Items/models/Item');
return {
inventoryAdjustment: {
relation: Model.BelongsToOneRelation,
modelClass: InventoryAdjustment,
join: {
from: 'inventory_adjustments_entries.adjustmentId',
to: 'inventory_adjustments.id',
},
},
/**
* Entry item.
*/
item: {
relation: Model.BelongsToOneRelation,
modelClass: Item,
join: {
from: 'inventory_adjustments_entries.itemId',
to: 'items.id',
},
},
};
}
}

View File

@@ -0,0 +1,31 @@
import { TransformerInjectable } from "@/modules/Transformer/TransformerInjectable.service";
import { InventoryAdjustment } from "../models/InventoryAdjustment";
import { InventoryAdjustmentTransformer } from "../InventoryAdjustmentTransformer";
export class GetInventoryAdjustmentService {
constructor(
private readonly transformer: TransformerInjectable,
) {}
/**
* Retrieve specific inventory adjustment transaction details.
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
*/
async getInventoryAdjustment(
inventoryAdjustmentId: number,
) {
// Retrieve inventory adjustment transation with associated models.
const inventoryAdjustment = await InventoryAdjustment.query()
.findById(inventoryAdjustmentId)
.withGraphFetched('entries.item')
.withGraphFetched('adjustmentAccount');
// Throw not found if the given adjustment transaction not exists.
this.throwIfAdjustmentNotFound(inventoryAdjustment);
return this.transformer.transform(
inventoryAdjustment,
new InventoryAdjustmentTransformer(),
);
}
}

View File

@@ -0,0 +1,56 @@
import { InventoryAdjustmentTransformer } from "../InventoryAdjustmentTransformer";
import { InventoryAdjustment } from "../models/InventoryAdjustment";
import { IInventoryAdjustmentsFilter } from "../types/InventoryAdjustments.types";
import { TransformerInjectable } from "@/modules/Transformer/TransformerInjectable.service";
export class GetInventoryAdjustmentsService {
constructor(
public readonly transformer: TransformerInjectable,
private readonly inventoryAdjustmentModel: typeof InventoryAdjustment,
) {
}
/**
* Retrieve the inventory adjustments paginated list.
* @param {number} tenantId
* @param {IInventoryAdjustmentsFilter} adjustmentsFilter
*/
public async getInventoryAdjustments(
tenantId: number,
filterDTO: IInventoryAdjustmentsFilter,
): Promise<{
inventoryAdjustments: IInventoryAdjustment[];
pagination: IPaginationMeta;
}> {
// Parses inventory adjustments list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
InventoryAdjustment,
filter,
);
const { results, pagination } = await this.inventoryAdjustmentModel.query()
.onBuild((query) => {
query.withGraphFetched('entries.item');
query.withGraphFetched('adjustmentAccount');
dynamicFilter.buildQuery()(query);
})
.pagination(filter.page - 1, filter.pageSize);
// Retrieves the transformed inventory adjustments.
const inventoryAdjustments = await this.transformer.transform(
results,
new InventoryAdjustmentTransformer(),
);
return {
inventoryAdjustments,
pagination,
};
}
}

View File

@@ -0,0 +1,62 @@
import { Knex } from 'knex';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
type IAdjustmentTypes = 'increment' | 'decrement';
export interface IQuickInventoryAdjustmentDTO {
date: Date;
type: IAdjustmentTypes;
adjustmentAccountId: number;
reason: string;
description: string;
referenceNo: string;
itemId: number;
quantity: number;
cost: number;
publish: boolean;
warehouseId?: number;
branchId?: number;
}
export interface IInventoryAdjustmentsFilter {
page: number;
pageSize: number;
}
export interface IInventoryAdjustmentEventCreatedPayload {
inventoryAdjustment: InventoryAdjustment;
inventoryAdjustmentId: number;
trx: Knex.Transaction;
}
export interface IInventoryAdjustmentCreatingPayload {
quickAdjustmentDTO: IQuickInventoryAdjustmentDTO;
trx: Knex.Transaction;
}
export interface IInventoryAdjustmentEventPublishedPayload {
inventoryAdjustmentId: number;
inventoryAdjustment: InventoryAdjustment;
trx: Knex.Transaction;
}
export interface IInventoryAdjustmentPublishingPayload {
trx: Knex.Transaction;
oldInventoryAdjustment: InventoryAdjustment;
}
export interface IInventoryAdjustmentEventDeletedPayload {
inventoryAdjustmentId: number;
oldInventoryAdjustment: InventoryAdjustment;
trx: Knex.Transaction;
}
export interface IInventoryAdjustmentDeletingPayload {
oldInventoryAdjustment: InventoryAdjustment;
trx: Knex.Transaction;
}
export enum InventoryAdjustmentAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}