import { omit, sumBy } from 'lodash'; import moment from 'moment'; import { Inject, Injectable } from '@nestjs/common'; import * as R from 'ramda'; import { formatDateFields } from '@/utils/format-date-fields'; import composeAsync from 'async/compose'; import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { ItemEntry } from '@/modules/Items/models/ItemEntry'; import { Item } from '@/modules/Items/models/Item'; import { ItemEntriesTaxTransactions } from '@/modules/TaxRates/ItemEntriesTaxTransactions.service'; import { IBillDTO } from '../Bills.types'; import { Vendor } from '@/modules/Vendors/models/Vendor'; import { Bill } from '../models/Bill'; import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; @Injectable() export class BillDTOTransformer { constructor( private branchDTOTransform: BranchTransactionDTOTransformer, private warehouseDTOTransform: WarehouseTransactionDTOTransform, private taxDTOTransformer: ItemEntriesTaxTransactions, private tenancyContext: TenancyContext, @Inject(ItemEntry) private itemEntryModel: typeof ItemEntry, @Inject(Item) private itemModel: typeof Item, ) {} /** * Retrieve the bill entries total. * @param {IItemEntry[]} entries * @returns {number} */ private getBillEntriesTotal(entries: ItemEntry[]): number { return sumBy(entries, (e) => this.itemEntryModel.calcAmount(e)); } /** * Retrieve the bill landed cost amount. * @param {IBillDTO} billDTO * @returns {number} */ private getBillLandedCostAmount(billDTO: IBillDTO): number { const costEntries = billDTO.entries.filter((entry) => entry.landedCost); return this.getBillEntriesTotal(costEntries); } /** * Converts create bill DTO to model. * @param {IBillDTO} billDTO * @param {IBill} oldBill * @returns {IBill} */ public async billDTOToModel( billDTO: IBillDTO, vendor: Vendor, oldBill?: Bill, ) { const amount = sumBy(billDTO.entries, (e) => this.itemEntryModel.calcAmount(e), ); // Retrieve the landed cost amount from landed cost entries. const landedCostAmount = this.getBillLandedCostAmount(billDTO); // Retrieve the authorized user. const authorizedUser = await this.tenancyContext.getSystemUser(); // Bill number from DTO or frprom auto-increment. const billNumber = billDTO.billNumber || oldBill?.billNumber; const initialEntries = billDTO.entries.map((entry) => ({ referenceType: 'Bill', isInclusiveTax: billDTO.isInclusiveTax, ...omit(entry, ['amount']), })); const asyncEntries = await composeAsync( // Associate tax rate from tax id to entries. this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries, // Associate tax rate id from tax code to entries. this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries, // Sets the default cost account to the bill entries. this.setBillEntriesDefaultAccounts(), )(initialEntries); const entries = R.compose( // Remove tax code from entries. R.map(R.omit(['taxCode'])), // Associate the default index to each item entry line. assocItemEntriesDefaultIndex, )(asyncEntries); const initialDTO = { ...formatDateFields(omit(billDTO, ['open', 'entries', 'attachments']), [ 'billDate', 'dueDate', ]), amount, landedCostAmount, currencyCode: vendor.currencyCode, exchangeRate: billDTO.exchangeRate || 1, billNumber, entries, // Avoid rewrite the open date in edit mode when already opened. ...(billDTO.open && !oldBill?.openedAt && { openedAt: moment().toMySqlDateTime(), }), userId: authorizedUser.id, }; return R.compose( // Associates tax amount withheld to the model. this.taxDTOTransformer.assocTaxAmountWithheldFromEntries, this.branchDTOTransform.transformDTO, this.warehouseDTOTransform.transformDTO, )(initialDTO); } /** * Sets the default cost account to the bill entries. */ private setBillEntriesDefaultAccounts() { return async (entries: ItemEntry[]) => { const entriesItemsIds = entries.map((e) => e.itemId); const items = await this.itemModel.query().whereIn('id', entriesItemsIds); return entries.map((entry) => { const item = items.find((i) => i.id === entry.itemId); return { ...entry, ...(item.type !== 'inventory' && { costAccountId: entry.costAccountId || item.costAccountId, }), }; }); }; } }