import { Inject, Service } from 'typedi'; import { sumBy, difference } from 'lodash'; import * as R from 'ramda'; import { Knex } from 'knex'; import events from '@/subscribers/events'; import { IBill, IBillPaymentDTO, IBillPaymentEntryDTO, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta, IBillPaymentEntry, IBillPaymentEventCreatedPayload, IBillPaymentEventEditedPayload, IBillPaymentEventDeletedPayload, IBillPaymentCreatingPayload, IBillPaymentEditingPayload, IBillPaymentDeletingPayload, IVendor, } from '@/interfaces'; import JournalPosterService from '@/services/Sales/JournalPosterService'; import TenancyService from '@/services/Tenancy/TenancyService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import { entriesAmountDiff, formatDateFields } from 'utils'; import { ServiceError } from '@/exceptions'; import { ACCOUNT_TYPE } from '@/data/AccountTypes'; import { BillPaymentTransformer } from './BillPaymentTransformer'; import { ERRORS } from './constants'; import UnitOfWork from '@/services/UnitOfWork'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { TenantMetadata } from '@/system/models'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; /** * Bill payments service. * @service */ @Service('BillPayments') export default class BillPaymentsService implements IBillPaymentsService { @Inject() tenancy: TenancyService; @Inject() journalService: JournalPosterService; @Inject() dynamicListService: DynamicListingService; @Inject() eventPublisher: EventPublisher; @Inject() private transformer: TransformerInjectable; @Inject() uow: UnitOfWork; @Inject() private branchDTOTransform: BranchTransactionDTOTransform; /** * Validates the bill payment existance. * @param {Request} req * @param {Response} res * @param {Function} next */ private async getPaymentMadeOrThrowError( tenantid: number, paymentMadeId: number ) { const { BillPayment } = this.tenancy.models(tenantid); const billPayment = await BillPayment.query() .withGraphFetched('entries') .findById(paymentMadeId); if (!billPayment) { throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); } return billPayment; } /** * Validates the payment account. * @param {number} tenantId - * @param {number} paymentAccountId * @return {Promise} */ private async getPaymentAccountOrThrowError( tenantId: number, paymentAccountId: number ) { const { accountRepository } = this.tenancy.repositories(tenantId); const paymentAccount = await accountRepository.findOneById( paymentAccountId ); if (!paymentAccount) { throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); } // Validate the payment account type. if ( !paymentAccount.isAccountType([ ACCOUNT_TYPE.BANK, ACCOUNT_TYPE.CASH, ACCOUNT_TYPE.OTHER_CURRENT_ASSET, ]) ) { throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); } return paymentAccount; } /** * Validates the payment number uniqness. * @param {number} tenantId - * @param {string} paymentMadeNumber - * @return {Promise} */ private async validatePaymentNumber( tenantId: number, paymentMadeNumber: string, notPaymentMadeId?: number ) { const { BillPayment } = this.tenancy.models(tenantId); const foundBillPayment = await BillPayment.query().onBuild( (builder: any) => { builder.findOne('payment_number', paymentMadeNumber); if (notPaymentMadeId) { builder.whereNot('id', notPaymentMadeId); } } ); if (foundBillPayment) { throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE); } return foundBillPayment; } /** * Validate whether the entries bills ids exist on the storage. * @param {Request} req * @param {Response} res * @param {NextFunction} next */ public async validateBillsExistance( tenantId: number, billPaymentEntries: { billId: number }[], vendorId: number ) { const { Bill } = this.tenancy.models(tenantId); const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); const storedBills = await Bill.query() .whereIn('id', entriesBillsIds) .where('vendor_id', vendorId); const storedBillsIds = storedBills.map((t: IBill) => t.id); const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds); if (notFoundBillsIds.length > 0) { throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND); } // Validate the not opened bills. const notOpenedBills = storedBills.filter((bill) => !bill.openedAt); if (notOpenedBills.length > 0) { throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, { notOpenedBills, }); } return storedBills; } /** * Validate wether the payment amount bigger than the payable amount. * @param {Request} req * @param {Response} res * @param {NextFunction} next * @return {void} */ private async validateBillsDueAmount( tenantId: number, billPaymentEntries: IBillPaymentEntryDTO[], oldPaymentEntries: IBillPaymentEntry[] = [] ) { const { Bill } = this.tenancy.models(tenantId); const billsIds = billPaymentEntries.map( (entry: IBillPaymentEntryDTO) => entry.billId ); const storedBills = await Bill.query().whereIn('id', billsIds); const storedBillsMap = new Map( storedBills.map((bill) => { const oldEntries = oldPaymentEntries.filter( (entry) => entry.billId === bill.id ); const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; return [ bill.id, { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }, ]; }) ); interface invalidPaymentAmountError { index: number; due_amount: number; } const hasWrongPaymentAmount: invalidPaymentAmountError[] = []; billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => { const entryBill = storedBillsMap.get(entry.billId); const { dueAmount } = entryBill; if (dueAmount < entry.paymentAmount) { hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); } }); if (hasWrongPaymentAmount.length > 0) { throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT); } } /** * Validate the payment receive entries IDs existance. * @param {Request} req * @param {Response} res * @return {Response} */ private async validateEntriesIdsExistance( tenantId: number, billPaymentId: number, billPaymentEntries: IBillPaymentEntry[] ) { const { BillPaymentEntry } = this.tenancy.models(tenantId); const entriesIds = billPaymentEntries .filter((entry: any) => entry.id) .map((entry: any) => entry.id); const storedEntries = await BillPaymentEntry.query().where( 'bill_payment_id', billPaymentId ); const storedEntriesIds = storedEntries.map((entry: any) => entry.id); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); if (notFoundEntriesIds.length > 0) { throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND); } } /** * * Validate the payment vendor whether modified. * @param {string} billPaymentNo */ private validateVendorNotModified( billPaymentDTO: IBillPaymentDTO, oldBillPayment: IBillPayment ) { if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) { throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY); } } /** * Validates the payment account currency code. The deposit account curreny * should be equals the customer currency code or the base currency. * @param {string} paymentAccountCurrency * @param {string} customerCurrency * @param {string} baseCurrency * @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)} */ public validateWithdrawalAccountCurrency = ( paymentAccountCurrency: string, customerCurrency: string, baseCurrency: string ) => { if ( paymentAccountCurrency !== customerCurrency && paymentAccountCurrency !== baseCurrency ) { throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID); } }; /** * Transforms create/edit DTO to model. * @param {number} tenantId * @param {IBillPaymentDTO} billPaymentDTO - Bill payment. * @param {IBillPayment} oldBillPayment - Old bill payment. * @return {Promise} */ async transformDTOToModel( tenantId: number, billPaymentDTO: IBillPaymentDTO, vendor: IVendor, oldBillPayment?: IBillPayment ): Promise { const initialDTO = { ...formatDateFields(billPaymentDTO, ['paymentDate']), amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), currencyCode: vendor.currencyCode, exchangeRate: billPaymentDTO.exchangeRate || 1, entries: billPaymentDTO.entries, }; return R.compose( this.branchDTOTransform.transformDTO(tenantId) )(initialDTO); } /** * Creates a new bill payment transcations and store it to the storage * with associated bills entries and journal transactions. * * Precedures:- * ------ * - Records the bill payment transaction. * - Records the bill payment associated entries. * - Increment the payment amount of the given vendor bills. * - Decrement the vendor balance. * - Records payment journal entries. * ------ * @param {number} tenantId - Tenant id. * @param {BillPaymentDTO} billPayment - Bill payment object. */ public async createBillPayment( tenantId: number, billPaymentDTO: IBillPaymentDTO ): Promise { const { BillPayment, Contact } = this.tenancy.models(tenantId); const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); // Retrieves the payment vendor or throw not found error. const vendor = await Contact.query() .findById(billPaymentDTO.vendorId) .modify('vendor') .throwIfNotFound(); // Transform create DTO to model object. const billPaymentObj = await this.transformDTOToModel( tenantId, billPaymentDTO, vendor ); // Validate the payment account existance and type. const paymentAccount = await this.getPaymentAccountOrThrowError( tenantId, billPaymentObj.paymentAccountId ); // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber); } // Validates the bills existance and associated to the given vendor. await this.validateBillsExistance( tenantId, billPaymentObj.entries, billPaymentDTO.vendorId ); // Validates the bills due payment amount. await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); // Validates the withdrawal account currency code. this.validateWithdrawalAccountCurrency( paymentAccount.currencyCode, vendor.currencyCode, tenantMeta.baseCurrency ); // Writes bill payment transacation with associated transactions // under unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onBillPaymentCreating` event. await this.eventPublisher.emitAsync(events.billPayment.onCreating, { tenantId, billPaymentDTO, trx, } as IBillPaymentCreatingPayload); // Writes the bill payment graph to the storage. const billPayment = await BillPayment.query(trx).insertGraphAndFetch({ ...billPaymentObj, }); // Triggers `onBillPaymentCreated` event. await this.eventPublisher.emitAsync(events.billPayment.onCreated, { tenantId, billPayment, billPaymentId: billPayment.id, trx, } as IBillPaymentEventCreatedPayload); return billPayment; }); } /** * Edits the details of the given bill payment. * * Preceducres: * ------ * - Update the bill payment transaction. * - Insert the new bill payment entries that have no ids. * - Update the bill paymeny entries that have ids. * - Delete the bill payment entries that not presented. * - Re-insert the journal transactions and update the diff accounts balance. * - Update the diff vendor balance. * - Update the diff bill payment amount. * ------ * @param {number} tenantId - Tenant id * @param {Integer} billPaymentId * @param {BillPaymentDTO} billPayment * @param {IBillPayment} oldBillPayment */ public async editBillPayment( tenantId: number, billPaymentId: number, billPaymentDTO ): Promise { const { BillPayment, Contact } = this.tenancy.models(tenantId); const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); // const oldBillPayment = await this.getPaymentMadeOrThrowError( tenantId, billPaymentId ); // const vendor = await Contact.query() .modify('vendor') .findById(billPaymentDTO.vendorId) .throwIfNotFound(); // Transform bill payment DTO to model object. const billPaymentObj = await this.transformDTOToModel( tenantId, billPaymentDTO, vendor, oldBillPayment ); // Validate vendor not modified. this.validateVendorNotModified(billPaymentDTO, oldBillPayment); // Validate the payment account existance and type. const paymentAccount = await this.getPaymentAccountOrThrowError( tenantId, billPaymentObj.paymentAccountId ); // Validate the items entries IDs existance on the storage. await this.validateEntriesIdsExistance( tenantId, billPaymentId, billPaymentObj.entries ); // Validate the bills existance and associated to the given vendor. await this.validateBillsExistance( tenantId, billPaymentObj.entries, billPaymentDTO.vendorId ); // Validates the bills due payment amount. await this.validateBillsDueAmount( tenantId, billPaymentObj.entries, oldBillPayment.entries ); // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { await this.validatePaymentNumber( tenantId, billPaymentObj.paymentNumber, billPaymentId ); } // Validates the withdrawal account currency code. this.validateWithdrawalAccountCurrency( paymentAccount.currencyCode, vendor.currencyCode, tenantMeta.baseCurrency ); // Edits the bill transactions with associated transactions // under unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onBillPaymentEditing` event. await this.eventPublisher.emitAsync(events.billPayment.onEditing, { tenantId, oldBillPayment, billPaymentDTO, trx, } as IBillPaymentEditingPayload); // Deletes the bill payment transaction graph from the storage. const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({ id: billPaymentId, ...billPaymentObj, }); // Triggers `onBillPaymentEdited` event. await this.eventPublisher.emitAsync(events.billPayment.onEdited, { tenantId, billPaymentId, billPayment, oldBillPayment, trx, } as IBillPaymentEventEditedPayload); return billPayment; }); } /** * Deletes the bill payment and associated transactions. * @param {number} tenantId - Tenant id. * @param {Integer} billPaymentId - The given bill payment id. * @return {Promise} */ public async deleteBillPayment(tenantId: number, billPaymentId: number) { const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId); // Retrieve the bill payment or throw not found service error. const oldBillPayment = await this.getPaymentMadeOrThrowError( tenantId, billPaymentId ); // Deletes the bill transactions with associated transactions under // unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onBillPaymentDeleting` payload. await this.eventPublisher.emitAsync(events.billPayment.onDeleting, { tenantId, trx, oldBillPayment, } as IBillPaymentDeletingPayload); // Deletes the bill payment assocaited entries. await BillPaymentEntry.query(trx) .where('bill_payment_id', billPaymentId) .delete(); // Deletes the bill payment transaction. await BillPayment.query(trx).where('id', billPaymentId).delete(); // Triggers `onBillPaymentDeleted` event. await this.eventPublisher.emitAsync(events.billPayment.onDeleted, { tenantId, billPaymentId, oldBillPayment, trx, } as IBillPaymentEventDeletedPayload); }); } /** * Retrieve payment made associated bills. * @param {number} tenantId - * @param {number} billPaymentId - */ public async getPaymentBills(tenantId: number, billPaymentId: number) { const { Bill } = this.tenancy.models(tenantId); const billPayment = await this.getPaymentMadeOrThrowError( tenantId, billPaymentId ); const paymentBillsIds = billPayment.entries.map((entry) => entry.id); const bills = await Bill.query().whereIn('id', paymentBillsIds); return bills; } /** * Retrieve bill payment. * @param {number} tenantId * @param {number} billPyamentId * @return {Promise} */ public async getBillPayment( tenantId: number, billPyamentId: number ): Promise { const { BillPayment } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query() .withGraphFetched('entries.bill') .withGraphFetched('vendor') .withGraphFetched('paymentAccount') .withGraphFetched('transactions') .withGraphFetched('branch') .findById(billPyamentId); if (!billPayment) { throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND); } return this.transformer.transform( tenantId, billPayment, new BillPaymentTransformer() ); } private parseListFilterDTO(filterDTO) { return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO); } /** * Retrieve bill payment paginted and filterable list. * @param {number} tenantId * @param {IBillPaymentsFilter} billPaymentsFilter */ public async listBillPayments( tenantId: number, filterDTO: IBillPaymentsFilter ): Promise<{ billPayments: IBillPayment; pagination: IPaginationMeta; filterMeta: IFilterMeta; }> { const { BillPayment } = this.tenancy.models(tenantId); // Parses filter DTO. const filter = this.parseListFilterDTO(filterDTO); // Dynamic list service. const dynamicList = await this.dynamicListService.dynamicList( tenantId, BillPayment, filter ); const { results, pagination } = await BillPayment.query() .onBuild((builder) => { builder.withGraphFetched('vendor'); builder.withGraphFetched('paymentAccount'); dynamicList.buildQuery()(builder); }) .pagination(filter.page - 1, filter.pageSize); // Transformes the bill payments models to POJO. const billPayments = await this.transformer.transform( tenantId, results, new BillPaymentTransformer() ); return { billPayments, pagination, filterMeta: dynamicList.getResponseMeta(), }; } /** * Saves bills payment amount changes different. * @param {number} tenantId - * @param {IBillPaymentEntryDTO[]} paymentMadeEntries - * @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries - */ public async saveChangeBillsPaymentAmount( tenantId: number, paymentMadeEntries: IBillPaymentEntryDTO[], oldPaymentMadeEntries?: IBillPaymentEntryDTO[], trx?: Knex.Transaction ): Promise { const { Bill } = this.tenancy.models(tenantId); const opers: Promise[] = []; const diffEntries = entriesAmountDiff( paymentMadeEntries, oldPaymentMadeEntries, 'paymentAmount', 'billId' ); diffEntries.forEach( (diffEntry: { paymentAmount: number; billId: number }) => { if (diffEntry.paymentAmount === 0) { return; } const oper = Bill.changePaymentAmount( diffEntry.billId, diffEntry.paymentAmount, trx ); opers.push(oper); } ); await Promise.all(opers); } /** * Validates the given vendor has no associated payments. * @param {number} tenantId * @param {number} vendorId */ public async validateVendorHasNoPayments(tenantId: number, vendorId: number) { const { BillPayment } = this.tenancy.models(tenantId); const payments = await BillPayment.query().where('vendor_id', vendorId); if (payments.length > 0) { throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS); } } }