feat(nestjs): migrate to NestJS

This commit is contained in:
Ahmed Bouhuolia
2025-04-07 11:51:24 +02:00
parent f068218a16
commit 55fcc908ef
3779 changed files with 631 additions and 195332 deletions

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,98 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Query,
} from '@nestjs/common';
import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service';
import { IInventoryAdjustmentsFilter } from './types/InventoryAdjustments.types';
import { InventoryAdjustment } from './models/InventoryAdjustment';
import { IPaginationMeta } from '@/interfaces/Model';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CreateQuickInventoryAdjustmentDto } from './dtos/CreateQuickInventoryAdjustment.dto';
@Controller('inventory-adjustments')
@ApiTags('inventory-adjustments')
export class InventoryAdjustmentsController {
constructor(
private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService,
) {}
@Post('quick')
@ApiOperation({ summary: 'Create a quick inventory adjustment.' })
@ApiResponse({
status: 200,
description: 'The inventory adjustment has been successfully created.',
})
public async createQuickInventoryAdjustment(
@Body() quickAdjustmentDTO: CreateQuickInventoryAdjustmentDto,
): Promise<InventoryAdjustment> {
return this.inventoryAdjustmentsApplicationService.createQuickInventoryAdjustment(
quickAdjustmentDTO,
);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given inventory adjustment.' })
@ApiResponse({
status: 200,
description: 'The inventory adjustment has been successfully deleted.',
})
public async deleteInventoryAdjustment(
@Param('id') inventoryAdjustmentId: number,
): Promise<void> {
return this.inventoryAdjustmentsApplicationService.deleteInventoryAdjustment(
inventoryAdjustmentId,
);
}
@Get()
@ApiOperation({ summary: 'Retrieves the inventory adjustments.' })
@ApiResponse({
status: 200,
description: 'The inventory adjustments have been successfully retrieved.',
})
public async getInventoryAdjustments(
@Query() filterDTO: IInventoryAdjustmentsFilter,
): Promise<{
inventoryAdjustments: InventoryAdjustment[];
pagination: IPaginationMeta;
}> {
return this.inventoryAdjustmentsApplicationService.getInventoryAdjustments(
filterDTO,
);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the inventory adjustment details.' })
@ApiResponse({
status: 200,
description:
'The inventory adjustment details have been successfully retrieved.',
})
public async getInventoryAdjustment(
@Param('id') inventoryAdjustmentId: number,
): Promise<InventoryAdjustment> {
return this.inventoryAdjustmentsApplicationService.getInventoryAdjustment(
inventoryAdjustmentId,
);
}
@Put(':id/publish')
@ApiOperation({ summary: 'Publish the given inventory adjustment.' })
@ApiResponse({
status: 200,
description: 'The inventory adjustment has been successfully published.',
})
public async publishInventoryAdjustment(
@Param('id') inventoryAdjustmentId: number,
): Promise<void> {
return this.inventoryAdjustmentsApplicationService.publishInventoryAdjustment(
inventoryAdjustmentId,
);
}
}

View File

@@ -0,0 +1,52 @@
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';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { InventoryAdjustmentsGLSubscriber } from './subscribers/InventoryAdjustmentGL.subscriber';
import { InventoryAdjustmentsGLEntries } from './commands/ledger/InventoryAdjustmentsGLEntries';
import { InventoryAdjustmentInventoryTransactionsSubscriber } from './inventory/InventoryAdjustmentInventoryTransactionsSubscriber';
import { InventoryAdjustmentInventoryTransactions } from './inventory/InventoryAdjustmentInventoryTransactions';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { LedgerModule } from '../Ledger/Ledger.module';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { InventoryCostModule } from '../InventoryCost/InventoryCost.module';
const models = [
RegisterTenancyModel(InventoryAdjustment),
RegisterTenancyModel(InventoryAdjustmentEntry),
];
@Module({
imports: [
BranchesModule,
WarehousesModule,
LedgerModule,
DynamicListModule,
InventoryCostModule,
...models,
],
controllers: [InventoryAdjustmentsController],
providers: [
CreateQuickInventoryAdjustmentService,
PublishInventoryAdjustmentService,
GetInventoryAdjustmentsService,
GetInventoryAdjustmentService,
DeleteInventoryAdjustmentService,
InventoryAdjustmentsApplicationService,
InventoryAdjustmentsGLSubscriber,
InventoryAdjustmentsGLEntries,
TenancyContext,
InventoryAdjustmentInventoryTransactionsSubscriber,
InventoryAdjustmentInventoryTransactions,
],
exports: [...models],
})
export class InventoryAdjustmentsModule {}

View File

@@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { DeleteInventoryAdjustmentService } from './commands/DeleteInventoryAdjustment.service';
import { PublishInventoryAdjustmentService } from './commands/PublishInventoryAdjustment.service';
import { CreateQuickInventoryAdjustmentService } from './commands/CreateQuickInventoryAdjustment.service';
import {
IInventoryAdjustmentsFilter,
IQuickInventoryAdjustmentDTO,
} from './types/InventoryAdjustments.types';
import { InventoryAdjustment } from './models/InventoryAdjustment';
import { GetInventoryAdjustmentService } from './queries/GetInventoryAdjustment.service';
import { GetInventoryAdjustmentsService } from './queries/GetInventoryAdjustments.service';
import { IPaginationMeta } from '@/interfaces/Model';
import { CreateQuickInventoryAdjustmentDto } from './dtos/CreateQuickInventoryAdjustment.dto';
@Injectable()
export class InventoryAdjustmentsApplicationService {
constructor(
private readonly createQuickInventoryAdjustmentService: CreateQuickInventoryAdjustmentService,
private readonly deleteInventoryAdjustmentService: DeleteInventoryAdjustmentService,
private readonly publishInventoryAdjustmentService: PublishInventoryAdjustmentService,
private readonly getInventoryAdjustmentService: GetInventoryAdjustmentService,
private readonly getInventoryAdjustmentsService: GetInventoryAdjustmentsService,
) {}
/**
* Retrieves the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId - Inventory adjustment id.
* @returns {Promise<InventoryAdjustment>}
*/
public async getInventoryAdjustment(
inventoryAdjustmentId: number,
): Promise<InventoryAdjustment> {
return this.getInventoryAdjustmentService.getInventoryAdjustment(
inventoryAdjustmentId,
);
}
/**
* Creates a quick inventory adjustment transaction.
* @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - Quick inventory adjustment DTO.
*/
public async createQuickInventoryAdjustment(
quickAdjustmentDTO: CreateQuickInventoryAdjustmentDto,
): 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,
);
}
/**
* Retrieves the inventory adjustments paginated list.
* @param {IInventoryAdjustmentsFilter} adjustmentsFilter - Inventory adjustments filter.
*/
public async getInventoryAdjustments(
filterDTO: IInventoryAdjustmentsFilter,
): Promise<{
inventoryAdjustments: InventoryAdjustment[];
pagination: IPaginationMeta;
}> {
return this.getInventoryAdjustmentsService.getInventoryAdjustments(
filterDTO,
);
}
}

View File

@@ -0,0 +1,153 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import * as moment from 'moment';
import * as composeAsync from 'async/compose';
import { omit } from 'lodash';
import { events } from '@/common/events/events';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
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';
import { CreateQuickInventoryAdjustmentDto } from '../dtos/CreateQuickInventoryAdjustment.dto';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ERRORS } from '../constants/InventoryAdjustments.constants';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class CreateQuickInventoryAdjustmentService {
constructor(
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: TenantModelProxy<
typeof InventoryAdjustment
>,
@Inject(Item.name)
private readonly itemModel: TenantModelProxy<typeof Item>,
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
private readonly tenancyContext: TenancyContext,
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 async transformQuickAdjToModel(
adjustmentDTO: IQuickInventoryAdjustmentDTO,
): Promise<InventoryAdjustment> {
const authorizedUser = await this.tenancyContext.getSystemUser();
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 composeAsync(
this.warehouseDTOTransform.transformDTO<InventoryAdjustment>,
this.branchDTOTransform.transformDTO<InventoryAdjustment>,
)(initialDTO) as InventoryAdjustment;
}
/**
* Creates a quick inventory adjustment for specific item.
* @param {IQuickInventoryAdjustmentDTO} quickAdjustmentDTO - qucik adjustment DTO.
*/
public async createQuickAdjustment(
quickAdjustmentDTO: CreateQuickInventoryAdjustmentDto,
): 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 =
await 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)
.upsertGraphAndFetch({
...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,73 @@
import { Inject, Injectable } 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';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DeleteInventoryAdjustmentService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: TenantModelProxy<
typeof InventoryAdjustment
>,
@Inject(InventoryAdjustmentEntry.name)
private readonly inventoryAdjustmentEntryModel: TenantModelProxy<
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,102 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import * as moment from 'moment';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import {
IInventoryAdjustmentEventPublishedPayload,
IInventoryAdjustmentPublishingPayload,
} from '../types/InventoryAdjustments.types';
import { events } from '@/common/events/events';
import { ServiceError } from '@/modules/Items/ServiceError';
import { ERRORS } from '../constants/InventoryAdjustments.constants';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class PublishInventoryAdjustmentService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: TenantModelProxy<
typeof InventoryAdjustment
>,
) {}
/**
* Publish the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId - Inventory adjustment ID.
*/
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 this.inventoryAdjustmentModel()
.query()
.findById(inventoryAdjustmentId)
.patch({
publishedAt: moment().toMySqlDateTime(),
});
// Retrieve the inventory adjustment after the modification.
const inventoryAdjustment = await this.inventoryAdjustmentModel()
.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,128 @@
import * as R from 'ramda';
import { InventoryAdjustment } from '../../models/InventoryAdjustment';
import { InventoryAdjustmentEntry } from '../../models/InventoryAdjustmentEntry';
import { ILedgerEntry } from '../../../Ledger/types/Ledger.types';
import { AccountNormal } from '@/interfaces/Account';
import { Ledger } from '../../../Ledger/Ledger';
export class InventoryAdjustmentsGL {
private inventoryAdjustment: InventoryAdjustment;
private baseCurrency: string;
constructor(inventoryAdjustmentModel: InventoryAdjustment) {
this.inventoryAdjustment = inventoryAdjustmentModel;
}
/**
* Sets the base currency.
* @param {string} baseCurrency - Base currency.
* @returns {InventoryAdjustmentsGL}
*/
public setBaseCurrency(baseCurrency: string) {
this.baseCurrency = baseCurrency;
return this;
}
/**
* Retrieves the inventory adjustment common GL entry.
* @returns {ILedgerEntry}
*/
private get adjustmentGLCommonEntry() {
return {
currencyCode: this.baseCurrency,
exchangeRate: 1,
transactionId: this.inventoryAdjustment.id,
transactionType: 'InventoryAdjustment',
referenceNumber: this.inventoryAdjustment.referenceNo,
date: this.inventoryAdjustment.date,
userId: this.inventoryAdjustment.userId,
branchId: this.inventoryAdjustment.branchId,
createdAt: this.inventoryAdjustment.createdAt,
credit: 0,
debit: 0,
};
}
/**
* Retrieve the inventory adjustment inventory GL entry.
* @param {InventoryAdjustmentEntry} entry - Inventory adjustment entry.
* @param {number} index - Entry index.
* @returns {ILedgerEntry}
*/
private getAdjustmentGLInventoryEntry = R.curry(
(entry: InventoryAdjustmentEntry, index: number): ILedgerEntry => {
const commonEntry = this.adjustmentGLCommonEntry;
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(
entry: InventoryAdjustmentEntry,
index: number,
): ILedgerEntry {
const commonEntry = this.adjustmentGLCommonEntry;
const amount = entry.cost * entry.quantity;
return {
...commonEntry,
accountId: this.inventoryAdjustment.adjustmentAccountId,
accountNormal: AccountNormal.DEBIT,
credit: amount,
index: index + 2,
};
}
/**
* Retrieve the inventory adjustment GL item entry.
* @param {InventoryAdjustmentEntry} entry - Inventory adjustment entry.
* @param {number} index - Entry index.
* @returns {ILedgerEntry[]}
*/
private getAdjustmentGLItemEntry(
entry: InventoryAdjustmentEntry,
index: number,
): ILedgerEntry[] {
const getInventoryEntry = this.getAdjustmentGLInventoryEntry();
const inventoryEntry = getInventoryEntry(entry, index);
const costEntry = this.getAdjustmentGLCostEntry(entry, index);
return [inventoryEntry, costEntry];
}
/**
* Writes increment inventroy adjustment GL entries.
* @param {InventoryAdjustment} inventoryAdjustment -
* @param {JournalPoster} jorunal -
* @returns {ILedgerEntry[]}
*/
public getIncrementAdjustmentGLEntries(): ILedgerEntry[] {
return this.inventoryAdjustment.entries
.map((entry, index) => this.getAdjustmentGLItemEntry(entry, index))
.flat();
}
public getAdjustmentGL(): Ledger {
const entries = this.getIncrementAdjustmentGLEntries();
return new Ledger(entries);
}
}

View File

@@ -0,0 +1,80 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '../../../Ledger/LedgerStorage.service';
import { InventoryAdjustment } from '../../models/InventoryAdjustment';
import { TenancyContext } from '../../../Tenancy/TenancyContext.service';
import { InventoryAdjustmentsGL } from './InventoryAdjustmentGL';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class InventoryAdjustmentsGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly tenancyContext: TenancyContext,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustment: TenantModelProxy<
typeof InventoryAdjustment
>,
) {}
/**
* Writes inventory increment adjustment GL entries.
* @param {number} inventoryAdjustmentId - Inventory adjustment ID.
* @param {Knex.Transaction} trx - Knex transaction.
*/
public writeAdjustmentGLEntries = async (
inventoryAdjustmentId: number,
trx?: Knex.Transaction,
): Promise<void> => {
// Retrieves the inventory adjustment with associated entries.
const adjustment = await this.inventoryAdjustment()
.query(trx)
.findById(inventoryAdjustmentId)
.withGraphFetched('entries.item');
const tenantMeta = await this.tenancyContext.getTenantMetadata();
// Retrieves the inventory adjustment GL entries.
const ledger = new InventoryAdjustmentsGL(adjustment)
.setBaseCurrency(tenantMeta.baseCurrency)
.getAdjustmentGL();
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Reverts the adjustment transactions GL entries.
* @param {number} tenantId
* @param {number} inventoryAdjustmentId
* @returns {Promise<void>}
*/
public revertAdjustmentGLEntries = (
inventoryAdjustmentId: number,
trx?: Knex.Transaction,
): Promise<void> => {
return this.ledgerStorage.deleteByReference(
inventoryAdjustmentId,
'InventoryAdjustment',
trx,
);
};
/**
* Rewrite inventory adjustment GL entries.
* @param {number} tenantId
* @param {number} inventoryAdjustmentId
* @param {Knex.Transaction} trx
*/
public rewriteAdjustmentGLEntries = async (
inventoryAdjustmentId: number,
trx?: Knex.Transaction,
) => {
// Reverts GL entries of the given inventory adjustment.
await this.revertAdjustmentGLEntries(inventoryAdjustmentId, trx);
// Writes GL entries of th egiven inventory adjustment.
await this.writeAdjustmentGLEntries(inventoryAdjustmentId, trx);
};
}

View File

@@ -0,0 +1,6 @@
export 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',
};

View File

@@ -0,0 +1,86 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsDate,
IsEnum,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
} from 'class-validator';
import { Type } from 'class-transformer';
enum IAdjustmentTypes {
INCREMENT = 'increment',
DECREMENT = 'decrement',
}
export class CreateQuickInventoryAdjustmentDto {
@ApiProperty({ description: 'Date of the inventory adjustment' })
@IsNotEmpty()
@IsDate()
@Type(() => Date)
date: Date;
@ApiProperty({ description: 'Type of adjustment', enum: IAdjustmentTypes })
@IsNotEmpty()
@IsEnum(IAdjustmentTypes)
type: 'increment' | 'decrement';
@ApiProperty({ description: 'ID of the adjustment account' })
@IsNotEmpty()
@IsNumber()
@IsPositive()
adjustmentAccountId: number;
@ApiProperty({ description: 'Reason for the adjustment' })
@IsNotEmpty()
@IsString()
reason: string;
@ApiProperty({ description: 'Description of the adjustment' })
@IsNotEmpty()
@IsString()
description: string;
@ApiProperty({ description: 'Reference number' })
@IsNotEmpty()
@IsString()
referenceNo: string;
@ApiProperty({ description: 'ID of the item being adjusted' })
@IsNotEmpty()
@IsNumber()
@IsPositive()
itemId: number;
@ApiProperty({ description: 'Quantity to adjust' })
@IsNotEmpty()
@IsNumber()
@IsPositive()
quantity: number;
@ApiProperty({ description: 'Cost of the item' })
@IsNotEmpty()
@IsNumber()
@IsPositive()
cost: number;
@ApiProperty({ description: 'Whether to publish the adjustment immediately' })
@IsNotEmpty()
@IsBoolean()
publish: boolean;
@ApiPropertyOptional({ description: 'ID of the warehouse (optional)' })
@IsOptional()
@IsNumber()
@IsPositive()
warehouseId?: number;
@ApiPropertyOptional({ description: 'ID of the branch (optional)' })
@IsOptional()
@IsNumber()
@IsPositive()
branchId?: number;
}

View File

@@ -0,0 +1,70 @@
import { Injectable } from "@nestjs/common";
import { Knex } from "knex";
import { InventoryAdjustment } from "../models/InventoryAdjustment";
import { InventoryTransaction } from "@/modules/InventoryCost/models/InventoryTransaction";
import { InventoryTransactionsService } from "@/modules/InventoryCost/commands/InventoryTransactions.service";
@Injectable()
export class InventoryAdjustmentInventoryTransactions {
constructor(
private readonly inventoryService: InventoryTransactionsService
) {}
/**
* 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(
inventoryAdjustment: InventoryAdjustment,
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(
inventoryTransactions,
override,
trx
);
}
/**
* Reverts the inventory transactions from the inventory adjustment transaction.
* @param {number} inventoryAdjustmentId
*/
async revertInventoryTransactions(
inventoryAdjustmentId: number,
trx?: Knex.Transaction
): Promise<{ oldInventoryTransactions: InventoryTransaction[] }> {
return this.inventoryService.deleteInventoryTransactions(
inventoryAdjustmentId,
'InventoryAdjustment',
trx
);
}
}

View File

@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import {
IInventoryAdjustmentEventCreatedPayload,
IInventoryAdjustmentEventPublishedPayload,
} from '../types/InventoryAdjustments.types';
import { IInventoryAdjustmentEventDeletedPayload } from '../types/InventoryAdjustments.types';
import { InventoryAdjustmentInventoryTransactions } from './InventoryAdjustmentInventoryTransactions';
import { events } from '@/common/events/events';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class InventoryAdjustmentInventoryTransactionsSubscriber {
constructor(
private readonly inventoryTransactions: InventoryAdjustmentInventoryTransactions,
) {}
/**
* Handles writing inventory transactions once the quick adjustment created.
* @param {IInventoryAdjustmentEventPublishedPayload} payload
* @param {IInventoryAdjustmentEventCreatedPayload} payload -
*/
@OnEvent(events.inventoryAdjustment.onQuickCreated)
public async handleWriteInventoryTransactionsOncePublished({
inventoryAdjustment,
trx,
}:
| IInventoryAdjustmentEventPublishedPayload
| IInventoryAdjustmentEventCreatedPayload) {
await this.inventoryTransactions.writeInventoryTransactions(
inventoryAdjustment,
false,
trx,
);
}
/**
* Handles reverting invetory transactions once the inventory adjustment deleted.
* @param {IInventoryAdjustmentEventDeletedPayload} payload -
*/
@OnEvent(events.inventoryAdjustment.onDeleted)
public async handleRevertInventoryTransactionsOnceDeleted({
inventoryAdjustmentId,
oldInventoryAdjustment,
trx,
}: IInventoryAdjustmentEventDeletedPayload) {
// Can't continue if the inventory adjustment is not published.
if (!oldInventoryAdjustment.isPublished) {
return;
}
// Reverts the inventory transactions of adjustment transaction.
await this.inventoryTransactions.revertInventoryTransactions(
inventoryAdjustmentId,
trx,
);
}
}

View File

@@ -0,0 +1,126 @@
import { Model } from 'objection';
import { InventoryAdjustmentEntry } from './InventoryAdjustmentEntry';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class InventoryAdjustment extends TenantBaseModel {
public readonly date!: string;
public readonly type!: string;
public readonly adjustmentAccountId!: number;
public readonly reason?: string;
public readonly referenceNo!: string;
public readonly description?: string;
public readonly userId!: number;
public readonly publishedAt?: string;
public readonly branchId!: number;
public readonly warehouseId!: number;
public readonly createdAt!: Date | string;
public readonly entries: InventoryAdjustmentEntry[];
/**
* Table name
*/
static get tableName() {
return 'inventory_adjustments';
}
/**
* Timestamps columns.
*/
get timestamps(): Array<string> {
return ['created_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes(): Array<string> {
return ['formattedType', 'inventoryDirection', 'isPublished'];
}
/**
* Retrieve formatted adjustment type.
*/
get formattedType(): string {
return InventoryAdjustment.getFormattedType(this.type);
}
/**
* Retrieve formatted reference type.
*/
get inventoryDirection(): string {
return InventoryAdjustment.getInventoryDirection(this.type);
}
/**
* Detarmines whether the adjustment is published.
* @return {boolean}
*/
get isPublished(): boolean {
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,53 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
import { Item } from '@/modules/Items/models/Item';
// import TenantModel from 'models/TenantModel';
export class InventoryAdjustmentEntry extends BaseModel {
adjustmentId!: number;
index!: number;
itemId!: number;
quantity!: number;
cost!: number;
value!: number;
item!: Item;
/**
* 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,36 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import { InventoryAdjustmentTransformer } from '../InventoryAdjustmentTransformer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetInventoryAdjustmentService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: TenantModelProxy<
typeof InventoryAdjustment
>,
) {}
/**
* 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 this.inventoryAdjustmentModel()
.query()
.findById(inventoryAdjustmentId)
.withGraphFetched('entries.item')
.withGraphFetched('adjustmentAccount')
.throwIfNotFound();
return this.transformer.transform(
inventoryAdjustment,
new InventoryAdjustmentTransformer(),
);
}
}

View File

@@ -0,0 +1,69 @@
import { Inject, Injectable } from '@nestjs/common';
import * as R from 'ramda';
import { IPaginationMeta } from '@/interfaces/Model';
import { InventoryAdjustmentTransformer } from '../InventoryAdjustmentTransformer';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import { IInventoryAdjustmentsFilter } from '../types/InventoryAdjustments.types';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetInventoryAdjustmentsService {
constructor(
private readonly transformer: TransformerInjectable,
private readonly dynamicListService: DynamicListService,
@Inject(InventoryAdjustment.name)
private readonly inventoryAdjustmentModel: TenantModelProxy<
typeof InventoryAdjustment
>,
) {}
/**
* Retrieve the inventory adjustments paginated list.
* @param {number} tenantId
* @param {IInventoryAdjustmentsFilter} adjustmentsFilter
*/
public async getInventoryAdjustments(
filterDTO: IInventoryAdjustmentsFilter,
): Promise<{
inventoryAdjustments: InventoryAdjustment[];
pagination: IPaginationMeta;
}> {
// Parses inventory adjustments list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
this.inventoryAdjustmentModel(),
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,
};
}
/**
* Parses inventory adjustments list filter DTO.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { InventoryAdjustmentsGLEntries } from '../commands/ledger/InventoryAdjustmentsGLEntries';
import { IInventoryAdjustmentEventDeletedPayload } from '../types/InventoryAdjustments.types';
import { IInventoryAdjustmentEventCreatedPayload } from '../types/InventoryAdjustments.types';
import { events } from '@/common/events/events';
@Injectable()
export class InventoryAdjustmentsGLSubscriber {
constructor(
private readonly inventoryAdjustmentGL: InventoryAdjustmentsGLEntries,
) {}
/**
* Handles writing increment inventory adjustment GL entries.
*/
@OnEvent(events.inventoryAdjustment.onQuickCreated)
@OnEvent(events.inventoryAdjustment.onPublished)
public async handleGLEntriesOnceIncrementAdjustmentCreated({
inventoryAdjustmentId,
inventoryAdjustment,
trx,
}: IInventoryAdjustmentEventCreatedPayload) {
// Can't continue if the inventory adjustment is not published.
if (!inventoryAdjustment.isPublished) {
return;
}
// Can't continue if the inventory adjustment direction is not `IN`.
if (inventoryAdjustment.type !== 'increment') {
return;
}
await this.inventoryAdjustmentGL.writeAdjustmentGLEntries(
inventoryAdjustmentId,
trx,
);
}
/**
* Reverts the inventory adjustment GL entries once the transaction deleted.
* @param {IInventoryAdjustmentEventDeletedPayload} payload -
*/
@OnEvent(events.inventoryAdjustment.onDeleted)
public async revertAdjustmentGLEntriesOnceDeleted({
inventoryAdjustmentId,
oldInventoryAdjustment,
}: IInventoryAdjustmentEventDeletedPayload) {
// Can't continue if the inventory adjustment is not published.
if (!oldInventoryAdjustment.isPublished) {
return;
}
await this.inventoryAdjustmentGL.revertAdjustmentGLEntries(
inventoryAdjustmentId,
);
}
/**
* Handles writing inventory transactions once the quick adjustment created.
* @param {IInventoryAdjustmentEventPublishedPayload} payload
* @param {IInventoryAdjustmentEventCreatedPayload} payload -
*/
// private handleWriteInventoryTransactionsOncePublished = async ({
// inventoryAdjustment,
// trx,
// }:
// | IInventoryAdjustmentEventPublishedPayload
// | IInventoryAdjustmentEventCreatedPayload) => {
// await this.inventoryAdjustment.writeInventoryTransactions(
// tenantId,
// inventoryAdjustment,
// false,
// trx
// );
// };
/**
* Handles reverting invetory transactions once the inventory adjustment deleted.
* @param {IInventoryAdjustmentEventDeletedPayload} payload -
*/
// private handleRevertInventoryTransactionsOnceDeleted = async ({
// tenantId,
// inventoryAdjustmentId,
// oldInventoryAdjustment,
// trx,
// }: IInventoryAdjustmentEventDeletedPayload) => {
// // Can't continue if the inventory adjustment is not published.
// if (!oldInventoryAdjustment.isPublished) {
// return;
// }
// // Reverts the inventory transactions of adjustment transaction.
// await this.inventoryAdjustment.revertInventoryTransactions(
// tenantId,
// inventoryAdjustmentId,
// trx
// );
// };
}

View File

@@ -0,0 +1,63 @@
import { Knex } from 'knex';
import { InventoryAdjustment } from '../models/InventoryAdjustment';
import { CreateQuickInventoryAdjustmentDto } from '../dtos/CreateQuickInventoryAdjustment.dto';
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: CreateQuickInventoryAdjustmentDto;
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',
}