add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
IAllocatedLandedCostCreatedPayload,
IBillLandedCost,
ILandedCostDTO,
} from '@/interfaces';
import BaseLandedCostService from './BaseLandedCost';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export default class AllocateLandedCost extends BaseLandedCostService {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* =================================
* - 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 allocateLandedCost = async (
tenantId: number,
allocateCostDTO: ILandedCostDTO,
billId: number
): Promise<IBillLandedCost> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve total cost of allocated items.
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error.
const bill = await this.billsService.getBillOrThrowError(tenantId, billId);
// Retrieve landed cost transaction or throw not found service error.
const costTransaction = await this.getLandedCostOrThrowError(
tenantId,
allocateCostDTO.transactionType,
allocateCostDTO.transactionId
);
// Retrieve landed cost transaction entries.
const costTransactionEntry = await this.getLandedCostEntry(
tenantId,
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(tenantId, 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, {
tenantId,
bill,
billLandedCostId: billLandedCost.id,
billLandedCost,
costTransaction,
costTransactionEntry,
trx,
} as IAllocatedLandedCostCreatedPayload);
return billLandedCost;
});
};
}

View File

@@ -0,0 +1,200 @@
import { Inject, Service } from 'typedi';
import { difference, sumBy } from 'lodash';
import BillsService from '../Bills';
import { ServiceError } from '@/exceptions';
import {
IItemEntry,
IBill,
ILandedCostItemDTO,
ILandedCostDTO,
IBillLandedCostTransaction,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import TransactionLandedCost from './TransctionLandedCost';
import { ERRORS } from './utils';
import { CONFIG } from './utils';
@Service()
export default class BaseLandedCostService {
@Inject()
public billsService: BillsService;
@Inject()
public tenancy: HasTenancyService;
@Inject()
public transactionLandedCost: TransactionLandedCost;
/**
* Validates allocate cost items association with the purchase invoice entries.
* @param {IItemEntry[]} purchaseInvoiceEntries
* @param {ILandedCostItemDTO[]} landedCostItems
*/
protected validateAllocateCostItems = (
purchaseInvoiceEntries: IItemEntry[],
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: IBill,
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 (
tenantId: number,
transactionType: string,
transactionId: number
) => {
const Model = this.transactionLandedCost.getModel(
tenantId,
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 (
tenantId: number,
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 (
tenantId: number,
landedCostId: number
): Promise<IBillLandedCostTransaction> => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve the bill landed cost model.
const billLandedCost = await BillLandedCost.query().findById(landedCostId);
if (!billLandedCost) {
throw new ServiceError(ERRORS.BILL_LANDED_COST_NOT_FOUND);
}
return billLandedCost;
};
}

View File

@@ -0,0 +1,170 @@
import { Inject, Service } from 'typedi';
import { omit } from 'lodash';
import * as R from 'ramda';
import * as qim from 'qim';
import { IBillLandedCostTransaction } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { formatNumber } from 'utils';
import I18nService from '@/services/I18n/I18nService';
@Service()
export default class BillAllocatedLandedCostTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private i18nService: I18nService;
/**
* Retrieve the bill associated landed cost transactions.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @return {Promise<IBillLandedCostTransaction>}
*/
public getBillLandedCostTransactions = async (
tenantId: number,
billId: number
): Promise<IBillLandedCostTransaction> => {
const { BillLandedCost, Bill } = this.tenancy.models(tenantId);
// Retrieve the given bill id or throw not found service error.
const bill = await Bill.query().findById(billId).throwIfNotFound();
// Retrieve the bill associated allocated landed cost with bill and expense entry.
const landedCostTransactions = await BillLandedCost.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,58 @@
import { Service } from 'typedi';
import { isEmpty } from 'lodash';
import {
IBill,
IItem,
ILandedCostTransactionEntry,
ILandedCostTransaction,
IItemEntry,
} from '@/interfaces';
@Service()
export default class BillLandedCost {
/**
* Retrieve the landed cost transaction from the given bill transaction.
* @param {IBill} bill - Bill transaction.
* @returns {ILandedCostTransaction} - Landed cost transaction.
*/
public transformToLandedCost = (bill: IBill): ILandedCostTransaction => {
const name = bill.billNumber || bill.referenceNo;
return {
id: bill.id,
name,
allocatedCostAmount: bill.allocatedCostAmount,
amount: bill.landedCostAmount,
unallocatedCostAmount: bill.unallocatedCostAmount,
transactionType: 'Bill',
currencyCode: bill.currencyCode,
exchangeRate: bill.exchangeRate,
...(!isEmpty(bill.entries) && {
entries: bill.entries.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes bill entry to landed cost entry.
* @param {IBill} bill - Bill model.
* @param {IItemEntry} billEntry - Bill entry.
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry(
billEntry: IItemEntry & { item: IItem }
): ILandedCostTransactionEntry {
return {
id: billEntry.id,
name: billEntry.item.name,
code: billEntry.item.code,
amount: billEntry.amount,
unallocatedCostAmount: billEntry.unallocatedCostAmount,
allocatedCostAmount: billEntry.allocatedCostAmount,
description: billEntry.description,
costAccountId: billEntry.costAccountId || billEntry.item.costAccountId,
};
}
}

View File

@@ -0,0 +1,59 @@
import { Service } from 'typedi';
import { isEmpty } from 'lodash';
import * as R from 'ramda';
import {
IExpense,
ILandedCostTransactionEntry,
IExpenseCategory,
IAccount,
ILandedCostTransaction,
} from '@/interfaces';
@Service()
export default class ExpenseLandedCost {
/**
* Retrieve the landed cost transaction from the given expense transaction.
* @param {IExpense} expense
* @returns {ILandedCostTransaction}
*/
public transformToLandedCost = (
expense: IExpense
): ILandedCostTransaction => {
const name = 'EXP-100';
return {
id: expense.id,
name,
amount: expense.landedCostAmount,
allocatedCostAmount: expense.allocatedCostAmount,
unallocatedCostAmount: expense.unallocatedCostAmount,
transactionType: 'Expense',
currencyCode: expense.currencyCode,
exchangeRate: expense.exchangeRate || 1,
...(!isEmpty(expense.categories) && {
entries: expense.categories.map(this.transformToLandedCostEntry),
}),
};
};
/**
* Transformes expense entry to landed cost entry.
* @param {IExpenseCategory & { expenseAccount: IAccount }} expenseEntry -
* @return {ILandedCostTransactionEntry}
*/
public transformToLandedCostEntry = (
expenseEntry: IExpenseCategory & { expenseAccount: IAccount }
): ILandedCostTransactionEntry => {
return {
id: expenseEntry.id,
name: expenseEntry.expenseAccount.name,
code: expenseEntry.expenseAccount.code,
amount: expenseEntry.amount,
description: expenseEntry.description,
allocatedCostAmount: expenseEntry.allocatedCostAmount,
unallocatedCostAmount: expenseEntry.unallocatedCostAmount,
costAccountId: expenseEntry.expenseAccount.id,
};
};
}

View File

@@ -0,0 +1,249 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import {
AccountNormal,
IBill,
IBillLandedCost,
IBillLandedCostEntry,
ILandedCostTransactionEntry,
ILedger,
ILedgerEntry,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import { Service, Inject } from 'typedi';
import LedgerRepository from '@/services/Ledger/LedgerRepository';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import BaseLandedCostService from './BaseLandedCost';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export default class LandedCostGLEntries extends BaseLandedCostService {
@Inject()
private journalService: JournalPosterService;
@Inject()
private ledgerRepository: LedgerRepository;
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the landed cost GL common entry.
* @param {IBill} bill
* @param {IBillLandedCost} allocatedLandedCost
* @returns
*/
private getLandedCostGLCommonEntry = (
bill: IBill,
allocatedLandedCost: IBillLandedCost
) => {
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: IBill,
allocatedLandedCost: IBillLandedCost,
allocatedEntry: IBillLandedCostEntry
): 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: IBill,
allocatedLandedCost: IBillLandedCost,
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: IBill,
allocatedLandedCost: IBillLandedCost,
fromTransactionEntry: ILandedCostTransactionEntry,
allocatedEntry: IBillLandedCostEntry
): 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 {IBillLandedCost} allocatedLandedCost
* @param {IBill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedgerEntry[]}
*/
public getLandedCostGLEntries = (
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry
): ILedgerEntry[] => {
const getEntry = this.getLandedCostGLAllocateEntry(
bill,
allocatedLandedCost,
fromTransactionEntry
);
return allocatedLandedCost.allocateEntries.map(getEntry).flat();
};
/**
* Retrieves the landed cost GL ledger.
* @param {IBillLandedCost} allocatedLandedCost
* @param {IBill} bill
* @param {ILandedCostTransactionEntry} fromTransactionEntry
* @returns {ILedger}
*/
public getLandedCostLedger = (
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry
): 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 (
tenantId: number,
allocatedLandedCost: IBillLandedCost,
bill: IBill,
fromTransactionEntry: ILandedCostTransactionEntry,
trx?: Knex.Transaction
) => {
const ledgerEntries = this.getLandedCostGLEntries(
allocatedLandedCost,
bill,
fromTransactionEntry
);
await this.ledgerRepository.saveLedgerEntries(tenantId, ledgerEntries, trx);
};
/**
* Generates and writes GL entries of the given landed cost.
* @param {number} tenantId
* @param {number} billLandedCostId
* @param {Knex.Transaction} trx
*/
public createLandedCostGLEntries = async (
tenantId: number,
billLandedCostId: number,
trx?: Knex.Transaction
) => {
const { BillLandedCost } = this.tenancy.models(tenantId);
// Retrieve the bill landed cost transacion with associated
// allocated entries and items.
const allocatedLandedCost = await BillLandedCost.query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
// Retrieve the allocated from transactione entry.
const transactionEntry = await this.getLandedCostEntry(
tenantId,
allocatedLandedCost.fromTransactionType,
allocatedLandedCost.fromTransactionId,
allocatedLandedCost.fromTransactionEntryId
);
// Writes the given landed cost GL entries to the storage layer.
await this.writeLandedCostGLEntries(
tenantId,
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 (
tenantId: number,
landedCostId: number,
trx: Knex.Transaction
) => {
await this.journalService.revertJournalTransactions(
tenantId,
landedCostId,
'LandedCost',
trx
);
};
}

View File

@@ -0,0 +1,57 @@
import { Service, Inject } from 'typedi';
import events from '@/subscribers/events';
import LandedCostGLEntries from './LandedCostGLEntries';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
@Service()
export default class LandedCostGLEntriesSubscriber {
@Inject()
billLandedCostGLEntries: LandedCostGLEntries;
attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.writeGLEntriesOnceLandedCostCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.revertGLEnteriesOnceLandedCostDeleted
);
}
/**
* Writes GL entries once landed cost transaction created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private writeGLEntriesOnceLandedCostCreated = async ({
tenantId,
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) => {
await this.billLandedCostGLEntries.createLandedCostGLEntries(
tenantId,
billLandedCost.id,
trx
);
};
/**
* Reverts GL entries associated to landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private revertGLEnteriesOnceLandedCostDeleted = async ({
tenantId,
oldBillLandedCost,
billId,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
await this.billLandedCostGLEntries.revertLandedCostGLEntries(
tenantId,
oldBillLandedCost.id,
trx
);
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBill, IBillLandedCostTransaction } from '@/interfaces';
import InventoryService from '@/services/Inventory/Inventory';
import { mergeLocatedWithBillEntries } from './utils';
@Service()
export default class LandedCostInventoryTransactions {
@Inject()
public inventoryService: InventoryService;
/**
* Records inventory transactions.
* @param {number} tenantId
* @param {IBillLandedCostTransaction} billLandedCost
* @param {IBill} bill -
*/
public recordInventoryTransactions = async (
tenantId: number,
billLandedCost: IBillLandedCostTransaction,
bill: IBill,
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.inventoryService.recordInventoryTransactions(
tenantId,
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 = (
tenantId: number,
landedCostId: number,
trx?: Knex.Transaction
) => {
return this.inventoryService.deleteInventoryTransactions(
tenantId,
landedCostId,
'LandedCost',
trx
);
};
}

View File

@@ -0,0 +1,63 @@
import { Inject, Service } from 'typedi';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import LandedCostInventoryTransactions from './LandedCostInventoryTransactions';
@Service()
export default class LandedCostInventoryTransactionsSubscriber {
@Inject()
landedCostInventory: LandedCostInventoryTransactions;
/**
* Attaches events with handlers.
*/
public attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.writeInventoryTransactionsOnceCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.revertInventoryTransactionsOnceDeleted
);
}
/**
* Writes inventory transactions of the landed cost transaction once created.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private writeInventoryTransactionsOnceCreated = async ({
billLandedCost,
tenantId,
trx,
bill,
}: IAllocatedLandedCostCreatedPayload) => {
// Records the inventory transactions.
await this.landedCostInventory.recordInventoryTransactions(
tenantId,
billLandedCost,
bill,
trx
);
};
/**
* Reverts inventory transactions of the landed cost transaction once deleted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private revertInventoryTransactionsOnceDeleted = async ({
tenantId,
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
// Removes the inventory transactions.
await this.landedCostInventory.removeInventoryTransactions(
tenantId,
oldBillLandedCost.id,
trx
);
};
}

View File

@@ -0,0 +1,76 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import TransactionLandedCost from './TransctionLandedCost';
import { CONFIG } from './utils';
@Service()
export default class LandedCostSyncCostTransactions {
@Inject()
transactionLandedCost: TransactionLandedCost;
/**
* Allocate the landed cost amount to cost transactions.
* @param {number} tenantId -
* @param {string} transactionType
* @param {number} transactionId
*/
public incrementLandedCostAmount = async (
tenantId: number,
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
): Promise<void> => {
const Model = this.transactionLandedCost.getModel(
tenantId,
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 (
tenantId: number,
transactionType: string,
transactionId: number,
transactionEntryId: number,
amount: number,
trx?: Knex.Transaction
) => {
const Model = this.transactionLandedCost.getModel(
tenantId,
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,67 @@
import { Service, Inject } from 'typedi';
import {
IAllocatedLandedCostCreatedPayload,
IAllocatedLandedCostDeletedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import LandedCostSyncCostTransactions from './LandedCostSyncCostTransactions';
@Service()
export default class LandedCostSyncCostTransactionsSubscriber {
@Inject()
landedCostSyncCostTransaction: LandedCostSyncCostTransactions;
/**
* Attaches events with handlers.
*/
attach(bus) {
bus.subscribe(
events.billLandedCost.onCreated,
this.incrementCostTransactionsOnceCreated
);
bus.subscribe(
events.billLandedCost.onDeleted,
this.decrementCostTransactionsOnceDeleted
);
}
/**
* Increment cost transactions once the landed cost allocated.
* @param {IAllocatedLandedCostCreatedPayload} payload -
*/
private incrementCostTransactionsOnceCreated = async ({
tenantId,
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) => {
// Increment landed cost amount on transaction and entry.
await this.landedCostSyncCostTransaction.incrementLandedCostAmount(
tenantId,
billLandedCost.fromTransactionType,
billLandedCost.fromTransactionId,
billLandedCost.fromTransactionEntryId,
billLandedCost.amount,
trx
);
};
/**
* Decrement cost transactions once the allocated landed cost reverted.
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
private decrementCostTransactionsOnceDeleted = async ({
oldBillLandedCost,
tenantId,
trx,
}: IAllocatedLandedCostDeletedPayload) => {
// Reverts the landed cost amount to the cost transaction.
await this.landedCostSyncCostTransaction.revertLandedCostAmount(
tenantId,
oldBillLandedCost.fromTransactionType,
oldBillLandedCost.fromTransactionId,
oldBillLandedCost.fromTransactionEntryId,
oldBillLandedCost.amount,
trx
);
};
}

View File

@@ -0,0 +1,140 @@
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 BillsService from '../Bills';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { formatNumber } from 'utils';
@Service()
export default class LandedCostTranasctions {
@Inject()
transactionLandedCost: TransactionLandedCost;
@Inject()
billsService: BillsService;
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the landed costs based on the given query.
* @param {number} tenantId
* @param {ILandedCostTransactionsQueryDTO} query
* @returns {Promise<ILandedCostTransaction[]>}
*/
public getLandedCostTransactions = async (
tenantId: number,
query: ILandedCostTransactionsQueryDTO
): Promise<ILandedCostTransaction[]> => {
const { transactionType } = query;
const Model = this.transactionLandedCost.getModel(
tenantId,
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,78 @@
import Knex from 'knex';
import { Service, Inject } from 'typedi';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events';
import BaseLandedCost from './BaseLandedCost';
import { IAllocatedLandedCostDeletedPayload } from '@/interfaces';
@Service()
export default class RevertAllocatedLandedCost extends BaseLandedCost {
@Inject()
uow: UnitOfWork;
@Inject()
eventPublisher: EventPublisher;
/**
* 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 deleteAllocatedLandedCost = async (
tenantId: number,
landedCostId: number
): Promise<{
landedCostId: number;
}> => {
// Retrieves the bill landed cost.
const oldBillLandedCost = await this.getBillLandedCostOrThrowError(
tenantId,
landedCostId
);
// Deletes landed cost with associated transactions.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete landed cost transaction with assocaited locate entries.
await this.deleteLandedCost(tenantId, landedCostId, trx);
// Triggers the event `onBillLandedCostCreated`.
await this.eventPublisher.emitAsync(events.billLandedCost.onDeleted, {
tenantId,
oldBillLandedCost: oldBillLandedCost,
billId: oldBillLandedCost.billId,
trx,
} as IAllocatedLandedCostDeletedPayload);
return { landedCostId };
});
};
/**
* Deletes the landed cost transaction with assocaited allocate entries.
* @param {number} tenantId - Tenant id.
* @param {number} landedCostId - Landed cost id.
*/
public deleteLandedCost = async (
tenantId: number,
landedCostId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { BillLandedCost, BillLandedCostEntry } =
this.tenancy.models(tenantId);
// Deletes the bill landed cost allocated entries associated to landed cost.
await BillLandedCostEntry.query(trx)
.where('bill_located_cost_id', landedCostId)
.delete();
// Delete the bill landed cost from the storage.
await BillLandedCost.query(trx).where('id', landedCostId).delete();
};
}

View File

@@ -0,0 +1,88 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import { Model } from 'objection';
import {
IBill,
IExpense,
ILandedCostTransaction,
ILandedCostTransactionEntry,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import BillLandedCost from './BillLandedCost';
import ExpenseLandedCost from './ExpenseLandedCost';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './utils';
@Service()
export default class TransactionLandedCost {
@Inject()
billLandedCost: BillLandedCost;
@Inject()
expenseLandedCost: ExpenseLandedCost;
@Inject()
tenancy: HasTenancyService;
/**
* 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: IBill | IExpense
): 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,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',
},
},
};