refactor(nestjs): landed cost

This commit is contained in:
Ahmed Bouhuolia
2025-06-10 17:08:32 +02:00
parent fa180b3ac5
commit 1130975efd
20 changed files with 1511 additions and 10 deletions

View File

@@ -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<typeof Bill>,
@Inject(BillLandedCost.name)
private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>
) {
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<BillLandedCost> {
// 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;
});
};
}

View File

@@ -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<typeof Bill>,
@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<IBillLandedCostTransaction>}
*/
public getBillLandedCostTransactions = async (
billId: number,
): Promise<IBillLandedCostTransaction> => {
// 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);
};
}

View File

@@ -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<typeof BillLandedCost>,
) {
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
);
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ILandedCostTransaction[]>}
*/
public getLandedCostTransactions = async (
query: ILandedCostTransactionsQueryDTO
): Promise<ILandedCostTransaction[]> => {
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,
};
}
);
}

View File

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

View File

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