From 1130975efd1dedc80f76fcedaadaabd6229f837e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 10 Jun 2025 17:08:32 +0200 Subject: [PATCH] refactor(nestjs): landed cost --- .../BillLandedCosts/BaseLandedCost.service.ts | 191 ++++++++++++++ .../BillLandedCosts/BillLandedCosts.module.ts | 26 +- .../BillLandedCosts/LandedCost.controller.ts | 84 +++++++ .../TransactionLandedCostEntries.service.ts | 15 +- .../commands/AllocateLandedCost.service.ts | 107 ++++++++ ...AllocatedLandedCostTransactions.service.ts | 177 +++++++++++++ .../commands/LandedCostGLEntries.service.ts | 234 ++++++++++++++++++ .../LandedCostGLEntries.subscriber.ts | 45 ++++ ...LandedCostInventoryTransactions.service.ts | 66 +++++ ...dedCostInventoryTransactions.subscriber.ts | 49 ++++ .../LandedCostSyncCostTransactions.service.ts | 73 ++++++ ...ndedCostSyncCostTransactions.subscriber.ts | 53 ++++ .../LandedCostTransactions.service.ts | 130 ++++++++++ .../RevertAllocatedLandedCost.service.ts | 85 +++++++ .../commands/TransctionLandedCost.service.ts | 82 ++++++ .../dtos/AllocateBillLandedCost.dto.ts | 45 ++++ .../BillLandedCosts/models/BillLandedCost.ts | 10 + .../types/BillLandedCosts.types.ts | 2 - .../src/modules/BillLandedCosts/utils.ts | 46 ++++ .../commands/BillDTOTransformer.service.ts | 1 - 20 files changed, 1511 insertions(+), 10 deletions(-) create mode 100644 packages/server/src/modules/BillLandedCosts/BaseLandedCost.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/LandedCost.controller.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/AllocateLandedCost.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/BillAllocatedLandedCostTransactions.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.subscriber.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.subscriber.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.subscriber.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/LandedCostTransactions.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/RevertAllocatedLandedCost.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/commands/TransctionLandedCost.service.ts create mode 100644 packages/server/src/modules/BillLandedCosts/dtos/AllocateBillLandedCost.dto.ts create mode 100644 packages/server/src/modules/BillLandedCosts/utils.ts diff --git a/packages/server/src/modules/BillLandedCosts/BaseLandedCost.service.ts b/packages/server/src/modules/BillLandedCosts/BaseLandedCost.service.ts new file mode 100644 index 000000000..037a25bd0 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/BaseLandedCost.service.ts @@ -0,0 +1,191 @@ +import { Inject } from '@nestjs/common'; +import { difference, sumBy } from 'lodash'; +import { + ILandedCostItemDTO, + ILandedCostDTO, + IBillLandedCostTransaction, + ILandedCostTransaction, + ILandedCostTransactionEntry, +} from './types/BillLandedCosts.types'; +import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { BillLandedCost } from './models/BillLandedCost'; +import { ServiceError } from '../Items/ServiceError'; +import { CONFIG, ERRORS } from './utils'; +import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry'; +import { Bill } from '../Bills/models/Bill'; +import { TransactionLandedCost } from './commands/TransctionLandedCost.service'; + +export class BaseLandedCostService { + @Inject() + public readonly transactionLandedCost: TransactionLandedCost; + + @Inject(BillLandedCost.name) + private readonly billLandedCostModel: TenantModelProxy; + + /** + * Validates allocate cost items association with the purchase invoice entries. + * @param {IItemEntry[]} purchaseInvoiceEntries + * @param {ILandedCostItemDTO[]} landedCostItems + */ + protected validateAllocateCostItems = ( + purchaseInvoiceEntries: ItemEntry[], + landedCostItems: ILandedCostItemDTO[], + ): void => { + // Purchase invoice entries items ids. + const purchaseInvoiceItems = purchaseInvoiceEntries.map((e) => e.id); + const landedCostItemsIds = landedCostItems.map((item) => item.entryId); + + // Not found items ids. + const notFoundItemsIds = difference( + purchaseInvoiceItems, + landedCostItemsIds, + ); + // Throw items ids not found service error. + if (notFoundItemsIds.length > 0) { + throw new ServiceError(ERRORS.LANDED_COST_ITEMS_IDS_NOT_FOUND); + } + }; + + /** + * Transformes DTO to bill landed cost model object. + * @param {ILandedCostDTO} landedCostDTO + * @param {IBill} bill + * @param {ILandedCostTransaction} costTransaction + * @param {ILandedCostTransactionEntry} costTransactionEntry + * @returns + */ + protected transformToBillLandedCost( + landedCostDTO: ILandedCostDTO, + bill: Bill, + costTransaction: ILandedCostTransaction, + costTransactionEntry: ILandedCostTransactionEntry, + ) { + const amount = sumBy(landedCostDTO.items, 'cost'); + + return { + billId: bill.id, + + fromTransactionType: landedCostDTO.transactionType, + fromTransactionId: landedCostDTO.transactionId, + fromTransactionEntryId: landedCostDTO.transactionEntryId, + + amount, + currencyCode: costTransaction.currencyCode, + exchangeRate: costTransaction.exchangeRate || 1, + + allocationMethod: landedCostDTO.allocationMethod, + allocateEntries: landedCostDTO.items, + + description: landedCostDTO.description, + costAccountId: costTransactionEntry.costAccountId, + }; + } + + /** + * Retrieve the cost transaction or throw not found error. + * @param {number} tenantId + * @param {transactionType} transactionType - + * @param {transactionId} transactionId - + */ + public getLandedCostOrThrowError = async ( + transactionType: string, + transactionId: number, + ) => { + const Model = this.transactionLandedCost.getModel( + transactionType, + ); + const model = await Model.query().findById(transactionId); + + if (!model) { + throw new ServiceError(ERRORS.LANDED_COST_TRANSACTION_NOT_FOUND); + } + return this.transactionLandedCost.transformToLandedCost( + transactionType, + model, + ); + }; + + /** + * Retrieve the landed cost entries. + * @param {number} tenantId + * @param {string} transactionType + * @param {number} transactionId + * @returns + */ + public getLandedCostEntry = async ( + transactionType: string, + transactionId: number, + transactionEntryId: number, + ): Promise => { + const Model = this.transactionLandedCost.getModel( + tenantId, + transactionType, + ); + const relation = CONFIG.COST_TYPES[transactionType].entries; + + const entry = await Model.relatedQuery(relation) + .for(transactionId) + .findOne('id', transactionEntryId) + .where('landedCost', true) + .onBuild((q) => { + if (transactionType === 'Bill') { + q.withGraphFetched('item'); + } else if (transactionType === 'Expense') { + q.withGraphFetched('expenseAccount'); + } + }); + + if (!entry) { + throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND); + } + return this.transactionLandedCost.transformToLandedCostEntry( + transactionType, + entry, + ); + }; + + /** + * Retrieve allocate items cost total. + * @param {ILandedCostDTO} landedCostDTO + * @returns {number} + */ + protected getAllocateItemsCostTotal = ( + landedCostDTO: ILandedCostDTO, + ): number => { + return sumBy(landedCostDTO.items, 'cost'); + }; + + /** + * Validates the landed cost entry amount. + * @param {number} unallocatedCost - + * @param {number} amount - + */ + protected validateLandedCostEntryAmount = ( + unallocatedCost: number, + amount: number, + ): void => { + if (unallocatedCost < amount) { + throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT); + } + }; + + /** + * Retrieve the give bill landed cost or throw not found service error. + * @param {number} tenantId - Tenant id. + * @param {number} landedCostId - Landed cost id. + * @returns {Promise} + */ + public getBillLandedCostOrThrowError = async ( + landedCostId: number, + ): Promise => { + // Retrieve the bill landed cost model. + const billLandedCost = await this.billLandedCostModel() + .query() + .findById(landedCostId); + + if (!billLandedCost) { + throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND); + } + return billLandedCost; + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/BillLandedCosts.module.ts b/packages/server/src/modules/BillLandedCosts/BillLandedCosts.module.ts index 67c95a8a7..7402b2933 100644 --- a/packages/server/src/modules/BillLandedCosts/BillLandedCosts.module.ts +++ b/packages/server/src/modules/BillLandedCosts/BillLandedCosts.module.ts @@ -1,8 +1,32 @@ import { Module } from '@nestjs/common'; import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service'; +import { AllocateLandedCostService } from './commands/AllocateLandedCost.service'; +import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber'; +import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service'; +import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service'; +import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber'; +import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service'; +import { BillAllocateLandedCostController } from './LandedCost.controller'; +import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service'; +import LandedCostTranasctions from './commands/LandedCostTransactions.service'; +import { LandedCostInventoryTransactions } from './commands/LandedCostInventoryTransactions.service'; +import { InventoryCostModule } from '../InventoryCost/InventoryCost.module'; @Module({ - providers: [TransactionLandedCostEntriesService], + imports: [InventoryCostModule], + providers: [ + AllocateLandedCostService, + TransactionLandedCostEntriesService, + BillAllocatedLandedCostTransactions, + LandedCostGLEntriesSubscriber, + LandedCostGLEntries, + LandedCostSyncCostTransactions, + RevertAllocatedLandedCost, + LandedCostInventoryTransactions, + LandedCostTranasctions, + LandedCostSyncCostTransactionsSubscriber, + ], exports: [TransactionLandedCostEntriesService], + controllers: [BillAllocateLandedCostController], }) export class BillLandedCostsModule {} diff --git a/packages/server/src/modules/BillLandedCosts/LandedCost.controller.ts b/packages/server/src/modules/BillLandedCosts/LandedCost.controller.ts new file mode 100644 index 000000000..fd94c6164 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/LandedCost.controller.ts @@ -0,0 +1,84 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, +} from '@nestjs/common'; +import { AllocateBillLandedCostDto } from './dtos/AllocateBillLandedCost.dto'; +import { AllocateLandedCostService } from './commands/AllocateLandedCost.service'; +import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service'; +import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service'; +import { LandedCostTranasctions } from './commands/LandedCostTransactions.service'; + +@Controller('landed-cost') +export class BillAllocateLandedCostController { + constructor( + private allocateLandedCost: AllocateLandedCostService, + private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions, + private revertAllocatedLandedCost: RevertAllocatedLandedCost, + private landedCostTranasctions: LandedCostTranasctions, + ) {} + + @Get('/transactions') + async getLandedCostTransactions( + @Query('transaction_type') transactionType: string, + ) { + const transactions = + await this.landedCostTranasctions.getLandedCostTransactions(transactionType); + + return transactions; + } + + @Post('/bills/:billId/allocate') + public async calculateLandedCost( + @Param('billId') billId: number, + @Body() landedCostDTO: AllocateBillLandedCostDto, + ) { + const billLandedCost = await this.allocateLandedCost.allocateLandedCost( + landedCostDTO, + billId, + ); + return { + id: billLandedCost.id, + message: 'The items cost are located successfully.', + }; + } + + @Delete('/:allocatedLandedCostId') + public async deleteAllocatedLandedCost( + @Param('allocatedLandedCostId') allocatedLandedCostId: number, + ) { + await this.revertAllocatedLandedCost.deleteAllocatedLandedCost( + allocatedLandedCostId, + ); + + return { + id: allocatedLandedCostId, + message: 'The allocated landed cost are delete successfully.', + }; + } + + public async listLandedCosts( + ) { + const transactions = + await this.landedCostTranasctions.getLandedCostTransactions(query); + + return transactions; + }; + + @Get('/bills/:billId/transactions') + async getBillLandedCostTransactions(@Param('billId') billId: number) { + const transactions = + await this.billAllocatedCostTransactions.getBillLandedCostTransactions( + billId, + ); + + return { + billId, + transactions, + }; + } +} diff --git a/packages/server/src/modules/BillLandedCosts/TransactionLandedCostEntries.service.ts b/packages/server/src/modules/BillLandedCosts/TransactionLandedCostEntries.service.ts index a78dbb1a0..616ef51e6 100644 --- a/packages/server/src/modules/BillLandedCosts/TransactionLandedCostEntries.service.ts +++ b/packages/server/src/modules/BillLandedCosts/TransactionLandedCostEntries.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@nestjs/common'; import { ServiceError } from '../Items/ServiceError'; import { transformToMap } from '@/utils/transform-to-key'; -import { ICommonLandedCostEntry, ICommonLandedCostEntryDTO } from './types/BillLandedCosts.types'; +import { + ICommonLandedCostEntry, + ICommonLandedCostEntryDTO, +} from './types/BillLandedCosts.types'; const ERRORS = { ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED: @@ -19,7 +22,7 @@ export class TransactionLandedCostEntriesService { */ public getLandedCostEntriesDeleted( oldCommonEntries: ICommonLandedCostEntry[], - newCommonEntriesDTO: ICommonLandedCostEntryDTO[] + newCommonEntriesDTO: ICommonLandedCostEntryDTO[], ): ICommonLandedCostEntry[] { const newBillEntriesById = transformToMap(newCommonEntriesDTO, 'id'); @@ -40,11 +43,11 @@ export class TransactionLandedCostEntriesService { */ public validateLandedCostEntriesNotDeleted( oldCommonEntries: ICommonLandedCostEntry[], - newCommonEntriesDTO: ICommonLandedCostEntryDTO[] + newCommonEntriesDTO: ICommonLandedCostEntryDTO[], ): void { const entriesDeleted = this.getLandedCostEntriesDeleted( oldCommonEntries, - newCommonEntriesDTO + newCommonEntriesDTO, ); if (entriesDeleted.length > 0) { throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED); @@ -58,7 +61,7 @@ export class TransactionLandedCostEntriesService { */ public validateLocatedCostEntriesSmallerThanNewEntries( oldCommonEntries: ICommonLandedCostEntry[], - newCommonEntriesDTO: ICommonLandedCostEntryDTO[] + newCommonEntriesDTO: ICommonLandedCostEntryDTO[], ): void { const oldBillEntriesById = transformToMap(oldCommonEntries, 'id'); @@ -67,7 +70,7 @@ export class TransactionLandedCostEntriesService { if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) { throw new ServiceError( - ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES + ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES, ); } }); diff --git a/packages/server/src/modules/BillLandedCosts/commands/AllocateLandedCost.service.ts b/packages/server/src/modules/BillLandedCosts/commands/AllocateLandedCost.service.ts new file mode 100644 index 000000000..7951d620e --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/AllocateLandedCost.service.ts @@ -0,0 +1,107 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { + IAllocatedLandedCostCreatedPayload, + ILandedCostDTO, +} from '../types/BillLandedCosts.types'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { BillLandedCost } from '../models/BillLandedCost'; +import { BaseLandedCostService } from '../BaseLandedCost.service'; +import { events } from '@/common/events/events'; +import { AllocateBillLandedCostDto } from '../dtos/AllocateBillLandedCost.dto'; + +@Injectable() +export class AllocateLandedCostService extends BaseLandedCostService { + constructor( + private readonly uow: UnitOfWork, + private readonly eventPublisher: EventEmitter2, + + @Inject(Bill.name) + private readonly billModel: TenantModelProxy, + + @Inject(BillLandedCost.name) + private readonly billLandedCostModel: TenantModelProxy + ) { + super(); + } + + /** + * ================================= + * - Allocate landed cost. + * ================================= + * - Validates the allocate cost not the same purchase invoice id. + * - Get the given bill (purchase invoice) or throw not found error. + * - Get the given landed cost transaction or throw not found error. + * - Validate landed cost transaction has enough unallocated cost amount. + * - Validate landed cost transaction entry has enough unallocated cost amount. + * - Validate allocate entries existance and associated with cost bill transaction. + * - Writes inventory landed cost transaction. + * - Increment the allocated landed cost transaction. + * - Increment the allocated landed cost transaction entry. + * -------------------------------- + * @param {ILandedCostDTO} landedCostDTO - Landed cost DTO. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Purchase invoice id. + */ + public async allocateLandedCost( + allocateCostDTO: AllocateBillLandedCostDto, + billId: number, + ): Promise { + // Retrieve total cost of allocated items. + const amount = this.getAllocateItemsCostTotal(allocateCostDTO); + + // Retrieve the purchase invoice or throw not found error. + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries') + .throwIfNotFound(); + + // Retrieve landed cost transaction or throw not found service error. + const costTransaction = await this.getLandedCostOrThrowError( + allocateCostDTO.transactionType, + allocateCostDTO.transactionId, + ); + // Retrieve landed cost transaction entries. + const costTransactionEntry = await this.getLandedCostEntry( + allocateCostDTO.transactionType, + allocateCostDTO.transactionId, + allocateCostDTO.transactionEntryId, + ); + // Validates allocate cost items association with the purchase invoice entries. + this.validateAllocateCostItems(bill.entries, allocateCostDTO.items); + + // Validate the amount of cost with unallocated landed cost. + this.validateLandedCostEntryAmount( + costTransactionEntry.unallocatedCostAmount, + amount, + ); + // Transformes DTO to bill landed cost model object. + const billLandedCostObj = this.transformToBillLandedCost( + allocateCostDTO, + bill, + costTransaction, + costTransactionEntry, + ); + // Saves landed cost transactions with associated tranasctions under + // unit-of-work eniverment. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Save the bill landed cost model. + const billLandedCost = + await BillLandedCost.query(trx).insertGraph(billLandedCostObj); + // Triggers `onBillLandedCostCreated` event. + await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, { + bill, + billLandedCostId: billLandedCost.id, + billLandedCost, + costTransaction, + costTransactionEntry, + trx, + } as IAllocatedLandedCostCreatedPayload); + + return billLandedCost; + }); + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/BillAllocatedLandedCostTransactions.service.ts b/packages/server/src/modules/BillLandedCosts/commands/BillAllocatedLandedCostTransactions.service.ts new file mode 100644 index 000000000..2360538b0 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/BillAllocatedLandedCostTransactions.service.ts @@ -0,0 +1,177 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { I18nService } from 'nestjs-i18n'; +import { omit } from 'lodash'; +import * as R from 'ramda'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { BillLandedCost } from '../models/BillLandedCost'; +import { IBillLandedCostTransaction } from '../types/BillLandedCosts.types'; + +@Injectable() +export class BillAllocatedLandedCostTransactions { + constructor( + private readonly i18nService: I18nService, + + @Inject(Bill.name) + private readonly billModel: TenantModelProxy, + + @Inject(BillLandedCost.name) + private readonly billLandedCostModel: TenantModelProxy< + typeof BillLandedCost + >, + ) {} + + /** + * Retrieve the bill associated landed cost transactions. + * @param {number} tenantId - Tenant id. + * @param {number} billId - Bill id. + * @return {Promise} + */ + public getBillLandedCostTransactions = async ( + billId: number, + ): Promise => { + // Retrieve the given bill id or throw not found service error. + const bill = await this.billModel() + .query() + .findById(billId) + .throwIfNotFound(); + + // Retrieve the bill associated allocated landed cost with bill and expense entry. + const landedCostTransactions = await this.billLandedCostModel() + .query() + .where('bill_id', billId) + .withGraphFetched('allocateEntries') + .withGraphFetched('allocatedFromBillEntry.item') + .withGraphFetched('allocatedFromExpenseEntry.expenseAccount') + .withGraphFetched('bill'); + + const transactionsJson = this.i18nService.i18nApply( + [[qim.$each, 'allocationMethodFormatted']], + landedCostTransactions.map((a) => a.toJSON()), + tenantId, + ); + return this.transformBillLandedCostTransactions(transactionsJson); + }; + + /** + * + * @param {IBillLandedCostTransaction[]} landedCostTransactions + * @returns + */ + private transformBillLandedCostTransactions = ( + landedCostTransactions: IBillLandedCostTransaction[], + ) => { + return landedCostTransactions.map(this.transformBillLandedCostTransaction); + }; + + /** + * + * @param {IBillLandedCostTransaction} transaction + * @returns + */ + private transformBillLandedCostTransaction = ( + transaction: IBillLandedCostTransaction, + ) => { + const getTransactionName = R.curry(this.condBillLandedTransactionName)( + transaction.fromTransactionType, + ); + const getTransactionDesc = R.curry( + this.condBillLandedTransactionDescription, + )(transaction.fromTransactionType); + + return { + formattedAmount: formatNumber(transaction.amount, { + currencyCode: transaction.currencyCode, + }), + ...omit(transaction, [ + 'allocatedFromBillEntry', + 'allocatedFromExpenseEntry', + ]), + name: getTransactionName(transaction), + description: getTransactionDesc(transaction), + formattedLocalAmount: formatNumber(transaction.localAmount, { + currencyCode: 'USD', + }), + }; + }; + + /** + * Retrieve bill landed cost tranaction name based on the given transaction type. + * @param transactionType + * @param transaction + * @returns + */ + private condBillLandedTransactionName = ( + transactionType: string, + transaction, + ) => { + return R.cond([ + [ + R.always(R.equals(transactionType, 'Bill')), + this.getLandedBillTransactionName, + ], + [ + R.always(R.equals(transactionType, 'Expense')), + this.getLandedExpenseTransactionName, + ], + ])(transaction); + }; + + /** + * + * @param transaction + * @returns + */ + private getLandedBillTransactionName = (transaction): string => { + return transaction.allocatedFromBillEntry.item.name; + }; + + /** + * + * @param transaction + * @returns + */ + private getLandedExpenseTransactionName = (transaction): string => { + return transaction.allocatedFromExpenseEntry.expenseAccount.name; + }; + + /** + * Retrieve landed cost. + * @param transaction + * @returns + */ + private getLandedBillTransactionDescription = (transaction): string => { + return transaction.allocatedFromBillEntry.description; + }; + + /** + * + * @param transaction + * @returns + */ + private getLandedExpenseTransactionDescription = (transaction): string => { + return transaction.allocatedFromExpenseEntry.description; + }; + + /** + * Retrieve the bill landed cost transaction description based on transaction type. + * @param {string} tranasctionType + * @param transaction + * @returns + */ + private condBillLandedTransactionDescription = ( + tranasctionType: string, + transaction, + ) => { + return R.cond([ + [ + R.always(R.equals(tranasctionType, 'Bill')), + this.getLandedBillTransactionDescription, + ], + [ + R.always(R.equals(tranasctionType, 'Expense')), + this.getLandedExpenseTransactionDescription, + ], + ])(transaction); + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.service.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.service.ts new file mode 100644 index 000000000..50325061c --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.service.ts @@ -0,0 +1,234 @@ +import * as R from 'ramda'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { BaseLandedCostService } from '../BaseLandedCost.service'; +import { BillLandedCost } from '../models/BillLandedCost'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { BillLandedCostEntry } from '../models/BillLandedCostEntry'; +import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; +import { Ledger } from '@/modules/Ledger/Ledger'; + +@Injectable() +export class LandedCostGLEntries extends BaseLandedCostService { + constructor( + private readonly journalService: JournalPosterService, + private readonly ledgerRepository: LedgerRepository, + + @Inject(BillLandedCost.name) + private readonly billLandedCostModel: TenantModelProxy, + ) { + super(); + } + + /** + * Retrieves the landed cost GL common entry. + * @param {IBill} bill + * @param {IBillLandedCost} allocatedLandedCost + * @returns + */ + private getLandedCostGLCommonEntry = ( + bill: Bill, + allocatedLandedCost: BillLandedCost + ) => { + return { + date: bill.billDate, + currencyCode: allocatedLandedCost.currencyCode, + exchangeRate: allocatedLandedCost.exchangeRate, + + transactionType: 'LandedCost', + transactionId: allocatedLandedCost.id, + transactionNumber: bill.billNumber, + + referenceNumber: bill.referenceNo, + + credit: 0, + debit: 0, + }; + }; + + /** + * Retrieves the landed cost GL inventory entry. + * @param {IBill} bill + * @param {IBillLandedCost} allocatedLandedCost + * @param {IBillLandedCostEntry} allocatedEntry + * @returns {ILedgerEntry} + */ + private getLandedCostGLInventoryEntry = ( + bill: Bill, + allocatedLandedCost: BillLandedCost, + allocatedEntry: BillLandedCostEntry + ): ILedgerEntry => { + const commonEntry = this.getLandedCostGLCommonEntry( + bill, + allocatedLandedCost + ); + return { + ...commonEntry, + debit: allocatedLandedCost.localAmount, + accountId: allocatedEntry.itemEntry.item.inventoryAccountId, + index: 1, + accountNormal: AccountNormal.DEBIT, + }; + }; + + /** + * Retrieves the landed cost GL cost entry. + * @param {IBill} bill + * @param {IBillLandedCost} allocatedLandedCost + * @param {ILandedCostTransactionEntry} fromTransactionEntry + * @returns {ILedgerEntry} + */ + private getLandedCostGLCostEntry = ( + bill: Bill, + allocatedLandedCost: BillLandedCost, + fromTransactionEntry: ILandedCostTransactionEntry + ): ILedgerEntry => { + const commonEntry = this.getLandedCostGLCommonEntry( + bill, + allocatedLandedCost + ); + return { + ...commonEntry, + credit: allocatedLandedCost.localAmount, + accountId: fromTransactionEntry.costAccountId, + index: 2, + accountNormal: AccountNormal.CREDIT, + }; + }; + + /** + * Retrieve allocated landed cost entry GL entries. + * @param {IBill} bill + * @param {IBillLandedCost} allocatedLandedCost + * @param {ILandedCostTransactionEntry} fromTransactionEntry + * @param {IBillLandedCostEntry} allocatedEntry + * @returns {ILedgerEntry} + */ + private getLandedCostGLAllocateEntry = R.curry( + ( + bill: Bill, + allocatedLandedCost: BillLandedCost, + fromTransactionEntry: LandedCostTransactionEntry, + allocatedEntry: BillLandedCostEntry + ): ILedgerEntry[] => { + const inventoryEntry = this.getLandedCostGLInventoryEntry( + bill, + allocatedLandedCost, + allocatedEntry + ); + const costEntry = this.getLandedCostGLCostEntry( + bill, + allocatedLandedCost, + fromTransactionEntry + ); + return [inventoryEntry, costEntry]; + } + ); + + /** + * Compose the landed cost GL entries. + * @param {BillLandedCost} allocatedLandedCost + * @param {Bill} bill + * @param {ILandedCostTransactionEntry} fromTransactionEntry + * @returns {ILedgerEntry[]} + */ + public getLandedCostGLEntries = ( + allocatedLandedCost: BillLandedCost, + bill: Bill, + fromTransactionEntry: LandedCostTransactionEntry + ): ILedgerEntry[] => { + const getEntry = this.getLandedCostGLAllocateEntry( + bill, + allocatedLandedCost, + fromTransactionEntry + ); + return allocatedLandedCost.allocateEntries.map(getEntry).flat(); + }; + + /** + * Retrieves the landed cost GL ledger. + * @param {IBillLandedCost} allocatedLandedCost + * @param {Bill} bill + * @param {ILandedCostTransactionEntry} fromTransactionEntry + * @returns {ILedger} + */ + public getLandedCostLedger = ( + allocatedLandedCost: BillLandedCost, + bill: Bill, + fromTransactionEntry: LandedCostTransactionEntry + ): ILedger => { + const entries = this.getLandedCostGLEntries( + allocatedLandedCost, + bill, + fromTransactionEntry + ); + return new Ledger(entries); + }; + + /** + * Writes landed cost GL entries to the storage layer. + * @param {number} tenantId - + */ + public writeLandedCostGLEntries = async ( + allocatedLandedCost: BillLandedCost, + bill: Bill, + fromTransactionEntry: ILandedCostTransactionEntry, + trx?: Knex.Transaction + ) => { + const ledgerEntries = this.getLandedCostGLEntries( + allocatedLandedCost, + bill, + fromTransactionEntry + ); + await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx); + }; + + /** + * Generates and writes GL entries of the given landed cost. + * @param {number} billLandedCostId + * @param {Knex.Transaction} trx + */ + public createLandedCostGLEntries = async ( + billLandedCostId: number, + trx?: Knex.Transaction + ) => { + // Retrieve the bill landed cost transacion with associated + // allocated entries and items. + const allocatedLandedCost = await this.billLandedCostModel().query(trx) + .findById(billLandedCostId) + .withGraphFetched('bill') + .withGraphFetched('allocateEntries.itemEntry.item'); + + // Retrieve the allocated from transactione entry. + const transactionEntry = await this.getLandedCostEntry( + allocatedLandedCost.fromTransactionType, + allocatedLandedCost.fromTransactionId, + allocatedLandedCost.fromTransactionEntryId + ); + // Writes the given landed cost GL entries to the storage layer. + await this.writeLandedCostGLEntries( + allocatedLandedCost, + allocatedLandedCost.bill, + transactionEntry, + trx + ); + }; + + /** + * Reverts GL entries of the given allocated landed cost transaction. + * @param {number} tenantId + * @param {number} landedCostId + * @param {Knex.Transaction} trx + */ + public revertLandedCostGLEntries = async ( + landedCostId: number, + trx: Knex.Transaction + ) => { + await this.journalService.revertJournalTransactions( + landedCostId, + 'LandedCost', + trx + ); + }; +} \ No newline at end of file diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.subscriber.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.subscriber.ts new file mode 100644 index 000000000..c793f6be2 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostGLEntries.subscriber.ts @@ -0,0 +1,45 @@ +import { + IAllocatedLandedCostCreatedPayload, + IAllocatedLandedCostDeletedPayload, +} from '../types/BillLandedCosts.types'; +import { OnEvent } from '@nestjs/event-emitter'; +import { LandedCostGLEntries } from './LandedCostGLEntries.service'; +import { Injectable } from '@nestjs/common'; +import { events } from '@/common/events/events'; + +@Injectable() +export class LandedCostGLEntriesSubscriber { + constructor( + private readonly billLandedCostGLEntries: LandedCostGLEntries, + ) {} + + /** + * Writes GL entries once landed cost transaction created. + * @param {IAllocatedLandedCostCreatedPayload} payload - + */ + @OnEvent(events.billLandedCost.onCreated) + async writeGLEntriesOnceLandedCostCreated({ + billLandedCost, + trx, + }: IAllocatedLandedCostCreatedPayload) { + await this.billLandedCostGLEntries.createLandedCostGLEntries( + billLandedCost.id, + trx + ); + }; + + /** + * Reverts GL entries associated to landed cost transaction once deleted. + * @param {IAllocatedLandedCostDeletedPayload} payload - + */ + @OnEvent(events.billLandedCost.onDeleted) + async revertGLEnteriesOnceLandedCostDeleted({ + oldBillLandedCost, + trx, + }: IAllocatedLandedCostDeletedPayload) { + await this.billLandedCostGLEntries.revertLandedCostGLEntries( + oldBillLandedCost.id, + trx + ); + }; +} \ No newline at end of file diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.service.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.service.ts new file mode 100644 index 000000000..2e4130f54 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; +import { IBillLandedCostTransaction } from '../types/BillLandedCosts.types'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { mergeLocatedWithBillEntries } from '../utils'; +import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service'; + +@Injectable() +export class LandedCostInventoryTransactions { + constructor( + private readonly inventoryTransactionsService: InventoryTransactionsService, + ) {} + + /** + * Records inventory transactions. + * @param {number} tenantId + * @param {IBillLandedCostTransaction} billLandedCost + * @param {IBill} bill - + */ + public recordInventoryTransactions = async ( + billLandedCost: IBillLandedCostTransaction, + bill: Bill, + trx?: Knex.Transaction, + ) => { + // Retrieve the merged allocated entries with bill entries. + const allocateEntries = mergeLocatedWithBillEntries( + billLandedCost.allocateEntries, + bill.entries, + ); + // Mappes the allocate cost entries to inventory transactions. + const inventoryTransactions = allocateEntries.map((allocateEntry) => ({ + date: bill.billDate, + itemId: allocateEntry.entry.itemId, + direction: 'IN', + quantity: null, + rate: allocateEntry.cost, + transactionType: 'LandedCost', + transactionId: billLandedCost.id, + entryId: allocateEntry.entryId, + })); + // Writes inventory transactions. + return this.inventoryTransactionsService.recordInventoryTransactions( + inventoryTransactions, + false, + trx, + ); + }; + + /** + * Deletes the inventory transaction. + * @param {number} tenantId - Tenant id. + * @param {number} landedCostId - Landed cost id. + * @param {Knex.Transaction} trx - Knex transactions. + * @returns + */ + public removeInventoryTransactions = ( + landedCostId: number, + trx?: Knex.Transaction, + ) => { + return this.inventoryTransactionsService.deleteInventoryTransactions( + landedCostId, + 'LandedCost', + trx, + ); + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.subscriber.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.subscriber.ts new file mode 100644 index 000000000..a2d4026c8 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostInventoryTransactions.subscriber.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { + IAllocatedLandedCostCreatedPayload, + IAllocatedLandedCostDeletedPayload, +} from '../types/BillLandedCosts.types'; +import { events } from '@/common/events/events'; +import { LandedCostInventoryTransactions } from './LandedCostInventoryTransactions.service'; + +@Injectable() +export class LandedCostInventoryTransactionsSubscriber { + constructor( + private readonly landedCostInventory: LandedCostInventoryTransactions, + ) {} + + /** + * Writes inventory transactions of the landed cost transaction once created. + * @param {IAllocatedLandedCostCreatedPayload} payload - + */ + @OnEvent(events.billLandedCost.onCreated) + async writeInventoryTransactionsOnceCreated({ + billLandedCost, + trx, + bill, + }: IAllocatedLandedCostCreatedPayload) { + // Records the inventory transactions. + await this.landedCostInventory.recordInventoryTransactions( + billLandedCost, + bill, + trx, + ); + } + + /** + * Reverts inventory transactions of the landed cost transaction once deleted. + * @param {IAllocatedLandedCostDeletedPayload} payload - + */ + @OnEvent(events.billLandedCost.onDeleted) + async revertInventoryTransactionsOnceDeleted({ + oldBillLandedCost, + trx, + }: IAllocatedLandedCostDeletedPayload) { + // Removes the inventory transactions. + await this.landedCostInventory.removeInventoryTransactions( + oldBillLandedCost.id, + trx, + ); + } +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.service.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.service.ts new file mode 100644 index 000000000..d41814b5b --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.service.ts @@ -0,0 +1,73 @@ +import { Knex } from 'knex'; +import { CONFIG } from '../utils'; +import { Injectable } from '@nestjs/common'; +import { TransactionLandedCost } from './TransctionLandedCost.service'; + +@Injectable() +export class LandedCostSyncCostTransactions { + constructor( + private readonly transactionLandedCost: TransactionLandedCost, + ) {} + + /** + * Allocate the landed cost amount to cost transactions. + * @param {number} tenantId - + * @param {string} transactionType + * @param {number} transactionId + */ + public incrementLandedCostAmount = async ( + transactionType: string, + transactionId: number, + transactionEntryId: number, + amount: number, + trx?: Knex.Transaction + ): Promise => { + const Model = this.transactionLandedCost.getModel( + transactionType + ); + const relation = CONFIG.COST_TYPES[transactionType].entries; + + // Increment the landed cost transaction amount. + await Model.query(trx) + .where('id', transactionId) + .increment('allocatedCostAmount', amount); + + // Increment the landed cost entry. + await Model.relatedQuery(relation, trx) + .for(transactionId) + .where('id', transactionEntryId) + .increment('allocatedCostAmount', amount); + }; + + /** + * Reverts the landed cost amount to cost transaction. + * @param {number} tenantId - Tenant id. + * @param {string} transactionType - Transaction type. + * @param {number} transactionId - Transaction id. + * @param {number} transactionEntryId - Transaction entry id. + * @param {number} amount - Amount + */ + public revertLandedCostAmount = async ( + transactionType: string, + transactionId: number, + transactionEntryId: number, + amount: number, + trx?: Knex.Transaction + ) => { + const Model = this.transactionLandedCost.getModel( + transactionType + ); + const relation = CONFIG.COST_TYPES[transactionType].entries; + + // Decrement the allocate cost amount of cost transaction. + await Model.query(trx) + .where('id', transactionId) + .decrement('allocatedCostAmount', amount); + + // Decrement the allocated cost amount cost transaction entry. + await Model.relatedQuery(relation, trx) + .for(transactionId) + .where('id', transactionEntryId) + .decrement('allocatedCostAmount', amount); + }; +} \ No newline at end of file diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.subscriber.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.subscriber.ts new file mode 100644 index 000000000..6cd6c9543 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostSyncCostTransactions.subscriber.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { + IAllocatedLandedCostCreatedPayload, + IAllocatedLandedCostDeletedPayload, +} from '../types/BillLandedCosts.types'; +import { events } from '@/common/events/events'; +import { LandedCostSyncCostTransactions } from './LandedCostSyncCostTransactions.service'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Injectable() +export class LandedCostSyncCostTransactionsSubscriber { + constructor( + private landedCostSyncCostTransaction: LandedCostSyncCostTransactions, + ) {} + + /** + * Increment cost transactions once the landed cost allocated. + * @param {IAllocatedLandedCostCreatedPayload} payload - + */ + @OnEvent(events.billLandedCost.onCreated) + async incrementCostTransactionsOnceCreated({ + billLandedCost, + trx, + }: IAllocatedLandedCostCreatedPayload) { + // Increment landed cost amount on transaction and entry. + await this.landedCostSyncCostTransaction.incrementLandedCostAmount( + billLandedCost.fromTransactionType, + billLandedCost.fromTransactionId, + billLandedCost.fromTransactionEntryId, + billLandedCost.amount, + trx, + ); + } + + /** + * Decrement cost transactions once the allocated landed cost reverted. + * @param {IAllocatedLandedCostDeletedPayload} payload - + */ + @OnEvent(events.billLandedCost.onDeleted) + async decrementCostTransactionsOnceDeleted({ + oldBillLandedCost, + trx, + }: IAllocatedLandedCostDeletedPayload) { + // Reverts the landed cost amount to the cost transaction. + await this.landedCostSyncCostTransaction.revertLandedCostAmount( + oldBillLandedCost.fromTransactionType, + oldBillLandedCost.fromTransactionId, + oldBillLandedCost.fromTransactionEntryId, + oldBillLandedCost.amount, + trx, + ); + } +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/LandedCostTransactions.service.ts b/packages/server/src/modules/BillLandedCosts/commands/LandedCostTransactions.service.ts new file mode 100644 index 000000000..3af3ba8c9 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/LandedCostTransactions.service.ts @@ -0,0 +1,130 @@ +import { Inject, Service } from 'typedi'; +import { ref } from 'objection'; +import * as R from 'ramda'; +import { + ILandedCostTransactionsQueryDTO, + ILandedCostTransaction, + ILandedCostTransactionDOJO, + ILandedCostTransactionEntry, + ILandedCostTransactionEntryDOJO, +} from '@/interfaces'; +import TransactionLandedCost from './TransctionLandedCost'; +import { formatNumber } from 'utils'; + +@Service() +export default class LandedCostTranasctions { + @Inject() + private transactionLandedCost: TransactionLandedCost; + + /** + * Retrieve the landed costs based on the given query. + * @param {number} tenantId + * @param {ILandedCostTransactionsQueryDTO} query + * @returns {Promise} + */ + public getLandedCostTransactions = async ( + query: ILandedCostTransactionsQueryDTO + ): Promise => { + const { transactionType } = query; + const Model = this.transactionLandedCost.getModel( + query.transactionType + ); + // Retrieve the model entities. + const transactions = await Model.query().onBuild((q) => { + q.where('allocated_cost_amount', '<', ref('landed_cost_amount')); + + if (query.transactionType === 'Bill') { + q.withGraphFetched('entries.item'); + } else if (query.transactionType === 'Expense') { + q.withGraphFetched('categories.expenseAccount'); + } + }); + const transformLandedCost = + this.transactionLandedCost.transformToLandedCost(transactionType); + + return R.compose( + this.transformLandedCostTransactions, + R.map(transformLandedCost) + )(transactions); + }; + + /** + * + * @param transactions + * @returns + */ + public transformLandedCostTransactions = ( + transactions: ILandedCostTransaction[] + ) => { + return R.map(this.transformLandedCostTransaction)(transactions); + }; + + /** + * Transformes the landed cost transaction. + * @param {ILandedCostTransaction} transaction + */ + public transformLandedCostTransaction = ( + transaction: ILandedCostTransaction + ): ILandedCostTransactionDOJO => { + const { currencyCode } = transaction; + + // Formatted transaction amount. + const formattedAmount = formatNumber(transaction.amount, { currencyCode }); + + // Formatted transaction unallocated cost amount. + const formattedUnallocatedCostAmount = formatNumber( + transaction.unallocatedCostAmount, + { currencyCode } + ); + // Formatted transaction allocated cost amount. + const formattedAllocatedCostAmount = formatNumber( + transaction.allocatedCostAmount, + { currencyCode } + ); + + return { + ...transaction, + formattedAmount, + formattedUnallocatedCostAmount, + formattedAllocatedCostAmount, + entries: R.map(this.transformLandedCostEntry(transaction))( + transaction.entries + ), + }; + }; + + /** + * + * @param {ILandedCostTransaction} transaction + * @param {ILandedCostTransactionEntry} entry + * @returns {ILandedCostTransactionEntryDOJO} + */ + public transformLandedCostEntry = R.curry( + ( + transaction: ILandedCostTransaction, + entry: ILandedCostTransactionEntry + ): ILandedCostTransactionEntryDOJO => { + const { currencyCode } = transaction; + + // Formatted entry amount. + const formattedAmount = formatNumber(entry.amount, { currencyCode }); + + // Formatted entry unallocated cost amount. + const formattedUnallocatedCostAmount = formatNumber( + entry.unallocatedCostAmount, + { currencyCode } + ); + // Formatted entry allocated cost amount. + const formattedAllocatedCostAmount = formatNumber( + entry.allocatedCostAmount, + { currencyCode } + ); + return { + ...entry, + formattedAmount, + formattedUnallocatedCostAmount, + formattedAllocatedCostAmount, + }; + } + ); +} \ No newline at end of file diff --git a/packages/server/src/modules/BillLandedCosts/commands/RevertAllocatedLandedCost.service.ts b/packages/server/src/modules/BillLandedCosts/commands/RevertAllocatedLandedCost.service.ts new file mode 100644 index 000000000..4d02004c2 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/RevertAllocatedLandedCost.service.ts @@ -0,0 +1,85 @@ +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { BaseLandedCostService } from '../BaseLandedCost.service'; +import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; +import { events } from '@/common/events/events'; +import { IAllocatedLandedCostDeletedPayload } from '../types/BillLandedCosts.types'; +import { BillLandedCostEntry } from '../models/BillLandedCostEntry'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { BillLandedCost } from '../models/BillLandedCost'; + +@Injectable() +export class RevertAllocatedLandedCost extends BaseLandedCostService { + constructor( + private readonly eventPublisher: EventEmitter2, + private readonly uow: UnitOfWork, + + @Inject(BillLandedCost.name) + private readonly billLandedCostModel: TenantModelProxy< + typeof BillLandedCost + >, + + @Inject(BillLandedCostEntry.name) + private readonly billLandedCostEntryModel: TenantModelProxy< + typeof BillLandedCostEntry + >, + ) { + super(); + } + + /** + * Deletes the allocated landed cost. + * ================================== + * - Delete bill landed cost transaction with associated allocate entries. + * - Delete the associated inventory transactions. + * - Decrement allocated amount of landed cost transaction and entry. + * - Revert journal entries. + * ---------------------------------- + * @param {number} tenantId - Tenant id. + * @param {number} landedCostId - Landed cost id. + * @return {Promise} + */ + public async deleteAllocatedLandedCost(landedCostId: number): Promise<{ + landedCostId: number; + }> { + // Retrieves the bill landed cost. + const oldBillLandedCost = + await this.getBillLandedCostOrThrowError(landedCostId); + // Deletes landed cost with associated transactions. + return this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Delete landed cost transaction with associated locate entries. + await this.deleteLandedCost(landedCostId, trx); + + // Triggers the event `onBillLandedCostCreated`. + await this.eventPublisher.emitAsync(events.billLandedCost.onDeleted, { + oldBillLandedCost: oldBillLandedCost, + billId: oldBillLandedCost.billId, + trx, + } as IAllocatedLandedCostDeletedPayload); + + return { landedCostId }; + }); + } + + /** + * Deletes the landed cost transaction with associated allocate entries. + * @param {number} landedCostId - Landed cost id. + */ + public deleteLandedCost = async ( + landedCostId: number, + trx?: Knex.Transaction, + ): Promise => { + // Deletes the bill landed cost allocated entries associated to landed cost. + await this.billLandedCostEntryModel() + .query(trx) + .where('bill_located_cost_id', landedCostId) + .delete(); + + // Delete the bill landed cost from the storage. + await this.billLandedCostModel() + .query(trx) + .where('id', landedCostId) + .delete(); + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/commands/TransctionLandedCost.service.ts b/packages/server/src/modules/BillLandedCosts/commands/TransctionLandedCost.service.ts new file mode 100644 index 000000000..720cbc343 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/commands/TransctionLandedCost.service.ts @@ -0,0 +1,82 @@ +import * as R from 'ramda'; +import { Model } from 'objection'; +import { + ILandedCostTransaction, + ILandedCostTransactionEntry, +} from '../types/BillLandedCosts.types'; +import { Injectable } from '@nestjs/common'; +import { BillLandedCost } from '../models/BillLandedCost'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { Expense } from '@/modules/Expenses/models/Expense.model'; +import { ServiceError } from '@/modules/Items/ServiceError'; +import { ERRORS } from '../utils'; +import { ExpenseLandedCost } from '../models/ExpenseLandedCost'; + +@Injectable() +export class TransactionLandedCost { + constructor( + private readonly billLandedCost: BillLandedCost, + private readonly expenseLandedCost: ExpenseLandedCost, + ) {} + /** + * Retrieve the cost transaction code model. + * @param {number} tenantId - Tenant id. + * @param {string} transactionType - Transaction type. + * @returns + */ + public getModel = (tenantId: number, transactionType: string): Model => { + const Models = this.tenancy.models(tenantId); + const Model = Models[transactionType]; + + if (!Model) { + throw new ServiceError(ERRORS.COST_TYPE_UNDEFINED); + } + return Model; + }; + + /** + * Mappes the given expense or bill transaction to landed cost transaction. + * @param {string} transactionType - Transaction type. + * @param {IBill|IExpense} transaction - Expense or bill transaction. + * @returns {ILandedCostTransaction} + */ + public transformToLandedCost = R.curry( + ( + transactionType: string, + transaction: Bill | Expense, + ): ILandedCostTransaction => { + return R.compose( + R.when( + R.always(transactionType === 'Bill'), + this.billLandedCost.transformToLandedCost, + ), + R.when( + R.always(transactionType === 'Expense'), + this.expenseLandedCost.transformToLandedCost, + ), + )(transaction); + }, + ); + + /** + * Transformes the given expense or bill entry to landed cost transaction entry. + * @param {string} transactionType + * @param {} transactionEntry + * @returns {ILandedCostTransactionEntry} + */ + public transformToLandedCostEntry = ( + transactionType: 'Bill' | 'Expense', + transactionEntry, + ): ILandedCostTransactionEntry => { + return R.compose( + R.when( + R.always(transactionType === 'Bill'), + this.billLandedCost.transformToLandedCostEntry, + ), + R.when( + R.always(transactionType === 'Expense'), + this.expenseLandedCost.transformToLandedCostEntry, + ), + )(transactionEntry); + }; +} diff --git a/packages/server/src/modules/BillLandedCosts/dtos/AllocateBillLandedCost.dto.ts b/packages/server/src/modules/BillLandedCosts/dtos/AllocateBillLandedCost.dto.ts new file mode 100644 index 000000000..91ca3f089 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/dtos/AllocateBillLandedCost.dto.ts @@ -0,0 +1,45 @@ +import { + IsInt, + IsIn, + IsOptional, + IsArray, + ValidateNested, + IsDecimal, + IsString, + IsNumber, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ToNumber } from '@/common/decorators/Validators'; + +class AllocateBillLandedCostItemDto { + @IsInt() + @ToNumber() + entryId: number; + + @IsDecimal() + cost: string; // Use string for IsDecimal, or use @IsNumber() if you want a number +} + +export class AllocateBillLandedCostDto { + @IsInt() + @ToNumber() + transactionId: number; + + @IsIn(['Expense', 'Bill']) + transactionType: string; + + @IsInt() + transactionEntryId: number; + + @IsIn(['value', 'quantity']) + allocationMethod: string; + + @IsOptional() + @IsString() + description?: string | null; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AllocateBillLandedCostItemDto) + items: AllocateBillLandedCostItemDto[]; +} \ No newline at end of file diff --git a/packages/server/src/modules/BillLandedCosts/models/BillLandedCost.ts b/packages/server/src/modules/BillLandedCosts/models/BillLandedCost.ts index 7373d9ada..4cf568cef 100644 --- a/packages/server/src/modules/BillLandedCosts/models/BillLandedCost.ts +++ b/packages/server/src/modules/BillLandedCosts/models/BillLandedCost.ts @@ -2,6 +2,10 @@ import { Model } from 'objection'; import { lowerCase } from 'lodash'; // import TenantModel from 'models/TenantModel'; import { BaseModel } from '@/models/Model'; +import { Bill } from '@/modules/Bills/models/Bill'; +import { BillLandedCostEntry } from './BillLandedCostEntry'; +import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry'; +import { ExpenseCategory } from '@/modules/Expenses/models/ExpenseCategory.model'; export class BillLandedCost extends BaseModel { amount!: number; @@ -13,6 +17,12 @@ export class BillLandedCost extends BaseModel { description!: string; billId!: number; exchangeRate!: number; + currencyCode!: string; + + bill!: Bill; + allocateEntries!: BillLandedCostEntry[]; + allocatedFromBillEntry!: ItemEntry; + allocatedFromExpenseEntry!: ExpenseCategory; /** * Table name diff --git a/packages/server/src/modules/BillLandedCosts/types/BillLandedCosts.types.ts b/packages/server/src/modules/BillLandedCosts/types/BillLandedCosts.types.ts index 912660a40..c1647d097 100644 --- a/packages/server/src/modules/BillLandedCosts/types/BillLandedCosts.types.ts +++ b/packages/server/src/modules/BillLandedCosts/types/BillLandedCosts.types.ts @@ -107,14 +107,12 @@ export interface IBillLandedCostTransactionEntry { } export interface IAllocatedLandedCostDeletedPayload { - tenantId: number; oldBillLandedCost: IBillLandedCostTransaction; billId: number; trx: Knex.Transaction; } export interface IAllocatedLandedCostCreatedPayload { - tenantId: number; bill: Bill; billLandedCostId: number; billLandedCost: IBillLandedCostTransaction; diff --git a/packages/server/src/modules/BillLandedCosts/utils.ts b/packages/server/src/modules/BillLandedCosts/utils.ts new file mode 100644 index 000000000..f7ea7da07 --- /dev/null +++ b/packages/server/src/modules/BillLandedCosts/utils.ts @@ -0,0 +1,46 @@ +import { IItemEntry, IBillLandedCostTransactionEntry } from '@/interfaces'; +import { transformToMap } from 'utils'; + +export const ERRORS = { + COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', + LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', + COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: + 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', + BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', + COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', + LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', + LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', + COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: + 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL', +}; + +/** + * Merges item entry to bill located landed cost entry. + * @param {IBillLandedCostTransactionEntry[]} locatedEntries - + * @param {IItemEntry[]} billEntries - + * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} + */ +export const mergeLocatedWithBillEntries = ( + locatedEntries: IBillLandedCostTransactionEntry[], + billEntries: IItemEntry[] +): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { + const billEntriesByEntryId = transformToMap(billEntries, 'id'); + + return locatedEntries.map((entry) => ({ + ...entry, + entry: billEntriesByEntryId.get(entry.entryId), + })); +}; + + +export const CONFIG = { + COST_TYPES: { + Expense: { + entries: 'categories', + }, + Bill: { + entries: 'entries', + }, + }, +}; \ No newline at end of file diff --git a/packages/server/src/modules/Bills/commands/BillDTOTransformer.service.ts b/packages/server/src/modules/Bills/commands/BillDTOTransformer.service.ts index ce73c2057..e3aa45b36 100644 --- a/packages/server/src/modules/Bills/commands/BillDTOTransformer.service.ts +++ b/packages/server/src/modules/Bills/commands/BillDTOTransformer.service.ts @@ -10,7 +10,6 @@ import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry'; import { Item } from '@/modules/Items/models/Item'; import { Vendor } from '@/modules/Vendors/models/Vendor'; import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service'; -import { IBillDTO } from '../Bills.types'; import { Bill } from '../models/Bill'; import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';