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,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<typeof BillLandedCost>;
/**
* 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<any> => {
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<IBillLandedCost>}
*/
public getBillLandedCostOrThrowError = async (
landedCostId: number,
): Promise<BillLandedCost> => {
// 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;
};
}

View File

@@ -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 {}

View File

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

View File

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

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

View File

@@ -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[];
}

View File

@@ -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

View File

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

View File

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

View File

@@ -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';