import { Service, Inject } from 'typedi'; import { Knex } from 'knex'; import { sumBy } from 'lodash'; import { ICreditNote, ICreditNoteAppliedToInvoice, ICreditNoteAppliedToInvoiceModel, ISaleInvoice, } from '@/interfaces'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import UnitOfWork from '@/services/UnitOfWork'; import { PaymentReceivedValidators } from '../Sales/PaymentReceived/PaymentReceivedValidators'; import BaseCreditNotes from './CreditNotes'; import { IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesCreatedPayload, } from '@/interfaces'; import { ServiceError } from '@/exceptions'; import events from '@/subscribers/events'; import { ERRORS } from './constants'; import HasTenancyService from '../Tenancy/TenancyService'; @Service() export default class CreditNoteApplyToInvoices extends BaseCreditNotes { @Inject() private tenancy: HasTenancyService; @Inject() private paymentReceiveValidators: PaymentReceivedValidators; @Inject() private uow: UnitOfWork; @Inject() private eventPublisher: EventPublisher; /** * Apply credit note to the given invoices. * @param {number} tenantId * @param {number} creditNoteId * @param {IApplyCreditToInvoicesDTO} applyCreditToInvoicesDTO */ public applyCreditNoteToInvoices = async ( tenantId: number, creditNoteId: number, applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO ): Promise => { const { CreditNoteAppliedInvoice } = this.tenancy.models(tenantId); // Saves the credit note or throw not found service error. const creditNote = await this.getCreditNoteOrThrowError( tenantId, creditNoteId ); // Retrieve the applied invoices that associated to the credit note customer. const appliedInvoicesEntries = await this.paymentReceiveValidators.validateInvoicesIDsExistance( tenantId, 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.amount ); // Validate the credit note remaining amount. this.validateCreditRemainingAmount( creditNote, creditNoteAppliedModel.amount ); // Creates credit note apply to invoice transaction. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Saves the credit note apply to invoice graph to the storage layer. const creditNoteAppliedInvoices = await CreditNoteAppliedInvoice.query().insertGraph( creditNoteAppliedModel.entries ); // Triggers `onCreditNoteApplyToInvoiceCreated` event. await this.eventPublisher.emitAsync( events.creditNote.onApplyToInvoicesCreated, { tenantId, creditNote, creditNoteAppliedInvoices, trx, } as IApplyCreditToInvoicesCreatedPayload ); return creditNoteAppliedInvoices; }); }; /** * Transformes apply DTO to model. * @param {IApplyCreditToInvoicesDTO} applyDTO * @param {ICreditNote} creditNote * @returns */ private transformApplyDTOToModel = ( applyDTO: IApplyCreditToInvoicesDTO, creditNote: ICreditNote ): ICreditNoteAppliedToInvoiceModel => { const entries = applyDTO.entries.map((entry) => ({ invoiceId: entry.invoiceId, amount: entry.amount, creditNoteId: creditNote.id, })); return { amount: sumBy(entries, 'amount'), entries, }; }; /** * Validate the invoice remaining amount. * @param {ISaleInvoice[]} invoices * @param {number} amount */ private validateInvoicesRemainingAmount = ( invoices: ISaleInvoice[], amount: number ) => { const invalidInvoices = invoices.filter( (invoice) => invoice.dueAmount < amount ); if (invalidInvoices.length > 0) { throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT); } }; }