Files
bigcapital/packages/server/src/services/Inventory/InventoryAdjustmentService.ts
Josh Soref b6d8766173 spelling: associate
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-06-27 09:34:47 -04:00

476 lines
15 KiB
TypeScript

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