import { Inject, Injectable } from '@nestjs/common'; import { Knex } from 'knex'; import { sumBy } from 'lodash'; import { ICreditNoteAppliedToInvoice, ICreditNoteAppliedToInvoiceModel, IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesCreatedPayload, } from '../types/CreditNoteApplyInvoice.types'; import { ERRORS } from '../../CreditNotes/constants'; import { PaymentReceivedValidators } from '@/modules/PaymentReceived/commands/PaymentReceivedValidators.service'; import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { events } from '@/common/events/events'; import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice'; import { ServiceError } from '@/modules/Items/ServiceError'; import { CreditNote } from '@/modules/CreditNotes/models/CreditNote'; import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice'; import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { ApplyCreditNoteToInvoicesDto } from '../dtos/ApplyCreditNoteToInvoices.dto'; @Injectable() export class CreditNoteApplyToInvoices { /** * @param {PaymentReceivedValidators} paymentReceiveValidators - The payment received validators service. * @param {UnitOfWork} uow - The unit of work service. * @param {EventEmitter2} eventPublisher - The event emitter service. * @param {typeof CreditNoteAppliedInvoice} creditNoteAppliedInvoiceModel - The credit note applied invoice model. */ constructor( private readonly paymentReceiveValidators: PaymentReceivedValidators, private readonly uow: UnitOfWork, private readonly eventPublisher: EventEmitter2, private readonly creditNoteDTOTransform: CommandCreditNoteDTOTransform, @Inject(CreditNoteAppliedInvoice.name) private readonly creditNoteAppliedInvoiceModel: TenantModelProxy< typeof CreditNoteAppliedInvoice >, @Inject(CreditNote.name) private readonly creditNoteModel: TenantModelProxy, ) {} /** * Apply credit note to the given invoices. * @param {number} creditNoteId * @param {IApplyCreditToInvoicesDTO} applyCreditToInvoicesDTO */ public async applyCreditNoteToInvoices( creditNoteId: number, applyCreditToInvoicesDTO: ApplyCreditNoteToInvoicesDto, ): Promise { // Saves the credit note or throw not found service error. const creditNote = await this.creditNoteModel() .query() .findById(creditNoteId) .throwIfNotFound(); // Retrieve the applied invoices that associated to the credit note customer. const appliedInvoicesEntries = await this.paymentReceiveValidators.validateInvoicesIDsExistance( creditNote.customerId, applyCreditToInvoicesDTO.entries, ); // Transformes apply DTO to model. const creditNoteAppliedModel = this.transformApplyDTOToModel( applyCreditToInvoicesDTO, creditNote, ); // Validate invoices has remaining amount to apply. this.validateInvoicesRemainingAmount( appliedInvoicesEntries, creditNoteAppliedModel.entries, ); // Validate the credit note remaining amount. this.creditNoteDTOTransform.validateCreditRemainingAmount( creditNote, creditNoteAppliedModel.amount, ); // Creates credit note apply to invoice transaction. return this.uow.withTransaction(async (trx: Knex.Transaction) => { // Saves the credit note apply to invoice graph to the storage layer. const creditNoteAppliedInvoices = await this.creditNoteAppliedInvoiceModel() .query() .insertGraph(creditNoteAppliedModel.entries); // Triggers `onCreditNoteApplyToInvoiceCreated` event. await this.eventPublisher.emitAsync( events.creditNote.onApplyToInvoicesCreated, { creditNote, creditNoteAppliedInvoices, trx, } as IApplyCreditToInvoicesCreatedPayload, ); return creditNoteAppliedInvoices; }); } /** * Transformes apply DTO to model. * @param {IApplyCreditToInvoicesDTO} applyDTO * @param {ICreditNote} creditNote * @returns */ private transformApplyDTOToModel = ( applyDTO: IApplyCreditToInvoicesDTO, creditNote: CreditNote, ): ICreditNoteAppliedToInvoiceModel => { const entries = applyDTO.entries.map((entry) => ({ invoiceId: entry.invoiceId, amount: entry.amount, creditNoteId: creditNote.id, })); return { amount: sumBy(entries, 'amount'), entries, }; }; /** * Validate each invoice has sufficient remaining amount for the applied credit. * @param {ISaleInvoice[]} invoices * @param {ICreditNoteAppliedToInvoice[]} entries */ private validateInvoicesRemainingAmount = ( invoices: SaleInvoice[], entries: ICreditNoteAppliedToInvoice[], ) => { const invoiceMap = new Map(invoices.map((inv) => [inv.id, inv])); const invalidEntries = entries.filter((entry) => { const invoice = invoiceMap.get(entry.invoiceId); return invoice != null && invoice.dueAmount < entry.amount; }); if (invalidEntries.length > 0) { throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT); } }; }