From 1141991e44db42d56d736c629e465f4066f99572 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Tue, 23 Jul 2024 13:52:25 +0200 Subject: [PATCH] feat: advanced payments --- packages/server/src/interfaces/Ledger.ts | 2 +- .../server/src/interfaces/PaymentReceive.ts | 8 + packages/server/src/interfaces/SaleInvoice.ts | 6 + .../AutoApplyPrepardExpensesOnBillCreated.ts | 2 +- .../AutoApplyUnearnedRevenue.ts | 84 +++++++-- .../PaymentReceivedGLCommon.ts | 2 +- .../PaymentReceivedUnearnedGLEntries.ts | 176 ++++++++++++++++-- .../PaymentReceive/WriteGLEntries.ts | 48 +++++ packages/server/src/subscribers/events.ts | 4 + 9 files changed, 295 insertions(+), 37 deletions(-) diff --git a/packages/server/src/interfaces/Ledger.ts b/packages/server/src/interfaces/Ledger.ts index d7045eb41..0c39eef4f 100644 --- a/packages/server/src/interfaces/Ledger.ts +++ b/packages/server/src/interfaces/Ledger.ts @@ -40,7 +40,7 @@ export interface ILedgerEntry { date: Date | string; transactionType: string; - transactionSubType: string; + transactionSubType?: string; transactionId: number; diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index 31f36f8f8..dc35c03e4 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -187,3 +187,11 @@ export interface PaymentReceiveMailPresendEvent { paymentReceiveId: number; messageOptions: PaymentReceiveMailOptsDTO; } + +export interface PaymentReceiveUnearnedRevenueAppliedEventPayload { + tenantId: number; + paymentReceiveId: number; + saleInvoiceId: number; + appliedAmount: number; + trx?: Knex.Transaction; +} diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 6fd5e753b..fddd5e011 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -216,3 +216,9 @@ export interface ISaleInvoiceMailSent { saleInvoiceId: number; messageOptions: SendInvoiceMailDTO; } + +export interface SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload { + tenantId: number; + saleInvoiceId: number; + trx?: Knex.Transaction; +} diff --git a/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts b/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts index ea53fc1ad..51a48ed50 100644 --- a/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts +++ b/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts @@ -13,7 +13,7 @@ export class AutoApplyPrepardExpensesOnBillCreated { */ public attach(bus) { bus.subscribe( - events.saleInvoice.onCreated, + events.bill.onCreated, this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this) ); } diff --git a/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts b/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts index 7f1be4e09..2982002ec 100644 --- a/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts +++ b/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts @@ -1,22 +1,34 @@ import { Inject, Service } from 'typedi'; import { Knex } from 'knex'; -import PromisePool from '@supercharge/promise-pool'; +import PromisePool, { ProcessHandler } from '@supercharge/promise-pool'; import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; +import { + IPaymentReceive, + PaymentReceiveUnearnedRevenueAppliedEventPayload, + SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload, +} from '@/interfaces'; @Service() export class AutoApplyUnearnedRevenue { @Inject() private tenancy: HasTenancyService; + + @Inject() + private eventPublisher: EventPublisher; + /** * Auto apply invoice to advanced payment received transactions. * @param {number} tenantId * @param {number} invoiceId + * @returns {Promise} */ public async autoApplyUnearnedRevenueToInvoice( tenantId: number, - invoiceId: number, + saleInvoiceId: number, trx?: Knex.Transaction - ) { + ): Promise { const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const unappliedPayments = await PaymentReceive.query(trx).where( @@ -25,26 +37,51 @@ export class AutoApplyUnearnedRevenue { 0 ); const invoice = await SaleInvoice.query(trx) - .findById(invoiceId) + .findById(saleInvoiceId) .throwIfNotFound(); - let unappliedAmount = invoice.balance; + let unappliedAmount = invoice.total; + let appliedTotalAmount = 0; + const processHandler: ProcessHandler< + IPaymentReceive, + Promise + > = async (unappliedPayment: IPaymentReceive, index: number, pool) => { + const appliedAmount = Math.min(unappliedAmount, unappliedAmount); + unappliedAmount = unappliedAmount - appliedAmount; + appliedTotalAmount += appliedAmount; + + if (appliedAmount <= 0) { + pool.stop(); + return; + } + await this.applyInvoiceToPaymentReceived( + tenantId, + unappliedPayment.id, + invoice.id, + appliedAmount, + trx + ); + }; await PromisePool.withConcurrency(1) .for(unappliedPayments) - .process(async (unappliedPayment: any) => { - const appliedAmount = unappliedAmount; + .process(processHandler); - await this.applyInvoiceToPaymentReceived( - tenantId, - unappliedPayment.id, - invoice.id, - appliedAmount, - trx - ); - }); // Increase the paid amount of the sale invoice. - await SaleInvoice.changePaymentAmount(invoiceId, unappliedAmount, trx); + await SaleInvoice.changePaymentAmount( + saleInvoiceId, + appliedTotalAmount, + trx + ); + // Triggers event `onSaleInvoiceUnearnedRevenue`. + await this.eventPublisher.emitAsync( + events.saleInvoice.onUnearnedRevenueApplied, + { + tenantId, + saleInvoiceId, + trx, + } as SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload + ); } /** @@ -54,6 +91,7 @@ export class AutoApplyUnearnedRevenue { * @param {number} invoiceId * @param {number} appliedAmount * @param {Knex.Transaction} trx + * @returns {Promise} */ public applyInvoiceToPaymentReceived = async ( tenantId: number, @@ -61,7 +99,7 @@ export class AutoApplyUnearnedRevenue { invoiceId: number, appliedAmount: number, trx?: Knex.Transaction - ) => { + ): Promise => { const { PaymentReceiveEntry, PaymentReceive } = this.tenancy.models(tenantId); @@ -71,5 +109,17 @@ export class AutoApplyUnearnedRevenue { paymentAmount: appliedAmount, }); await PaymentReceive.query(trx).increment('usedAmount', appliedAmount); + + // Triggers the event `onPaymentReceivedUnearnedRevenue`. + await this.eventPublisher.emitAsync( + events.paymentReceive.onUnearnedRevenueApplied, + { + tenantId, + paymentReceiveId, + saleInvoiceId: invoiceId, + appliedAmount, + trx, + } as PaymentReceiveUnearnedRevenueAppliedEventPayload + ); }; } diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts index ba03cb512..a5edb5736 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts @@ -1,3 +1,4 @@ +import { Knex } from 'knex'; import { sumBy } from 'lodash'; import { AccountNormal, @@ -5,7 +6,6 @@ import { IPaymentReceive, IPaymentReceiveGLCommonEntry, } from '@/interfaces'; -import { Knex } from 'knex'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; export class PaymentReceivedGLCommon { diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts index f6036a4e6..68be589cb 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts @@ -1,8 +1,9 @@ import * as R from 'ramda'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; -import { TenantMetadata } from '@/system/models'; import { Knex } from 'knex'; import { Inject, Service } from 'typedi'; +import { flatten } from 'lodash'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TenantMetadata } from '@/system/models'; import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon'; import { AccountNormal, @@ -10,12 +11,17 @@ import { IPaymentReceive, IPaymentReceiveEntry, } from '@/interfaces'; +import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; +import Ledger from '@/services/Accounting/Ledger'; @Service() export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { @Inject() private tenancy: HasTenancyService; + @Inject() + private ledgerStorage: LedgerStorageService; + /** * Writes payment GL entries to the storage. * @param {number} tenantId @@ -30,38 +36,118 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { ): Promise => { const { PaymentReceive } = this.tenancy.models(tenantId); - // Retrieves the given tenant metadata. - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - // Retrieves the payment receive with associated entries. const paymentReceive = await PaymentReceive.query(trx) .findById(paymentReceiveId) .withGraphFetched('entries.invoice'); - if (paymentReceive.unearnedRevenueAccountId) { + // Stop early if + if (!paymentReceive.unearnedRevenueAccountId) { return; } - + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + const ledger = await this.getPaymentReceiveGLedger( + tenantId, + paymentReceive + ); + // Commit the ledger entries to the storage. + await this.ledgerStorage.commit(tenantId, ledger, trx); }; - private getPaymentGLEntries = (paymentReceive: IPaymentReceive) => {}; + /** + * Rewrites the given payment receive GL entries. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {Knex.Transaction} trx + */ + public rewritePaymentGLEntries = async ( + tenantId: number, + paymentReceiveId: number, + trx?: Knex.Transaction + ) => { + // Reverts the payment GL entries. + await this.revertPaymentGLEntries(tenantId, paymentReceiveId, trx); - private getPaymentUnearnedGLEntries = R.curry(() => {}); + // Writes the payment GL entries. + await this.writePaymentGLEntries(tenantId, paymentReceiveId, trx); + }; /** - * + * Retrieves the payment received GL entries. + * @param {number} tenantId + * @param {IPaymentReceive} paymentReceive + * @returns {Promise} + */ + private getPaymentReceiveGLedger = async ( + tenantId: number, + paymentReceive: IPaymentReceive + ) => { + const { accountRepository } = this.tenancy.repositories(tenantId); + + // Retrieve the A/R account of the given currency. + const receivableAccount = + await accountRepository.findOrCreateAccountReceivable( + paymentReceive.currencyCode + ); + // Retrieve the payment GL entries. + const entries = this.getPaymentGLEntries( + receivableAccount.id, + paymentReceive + ); + const unearnedRevenueEntries = + this.getUnearnedRevenueEntries(paymentReceive); + + const combinedEntries = [...unearnedRevenueEntries, ...entries]; + + return new Ledger(combinedEntries); + }; + + /** + * Retrieve the payment received GL entries. + * @param {number} ARAccountId - A/R account id. + * @param {IPaymentReceive} paymentReceive - + * @returns {Array} + */ + private getPaymentGLEntries = R.curry( + ( + ARAccountId: number, + paymentReceive: IPaymentReceive + ): Array => { + const getPaymentEntryGLEntries = this.getPaymentEntryGLEntries( + ARAccountId, + paymentReceive + ); + const entriesGroup = paymentReceive.entries.map((paymentEntry) => { + return getPaymentEntryGLEntries(paymentEntry); + }); + return flatten(entriesGroup); + } + ); + + /** + * Retrieve the payment entry GL entries. * @param {IPaymentReceiveEntry} paymentReceivedEntry - * @param {IPaymentReceive} paymentReceive - + * @returns {Array} */ private getPaymentEntryGLEntries = R.curry( ( - paymentReceivedEntry: IPaymentReceiveEntry, - paymentReceive: IPaymentReceive - ) => { - const depositEntry = this.getPaymentDepositGLEntry( + ARAccountId: number, + paymentReceive: IPaymentReceive, + paymentReceivedEntry: IPaymentReceiveEntry + ): Array => { + const unearnedRevenueEntry = this.getDebitUnearnedRevenueGLEntry( paymentReceivedEntry.paymentAmount, paymentReceive ); + const AREntry = this.getPaymentReceivableEntry( + paymentReceivedEntry.paymentAmount, + paymentReceive, + ARAccountId + ); + return [unearnedRevenueEntry, AREntry]; } ); @@ -70,7 +156,7 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { * @param {IPaymentReceive} paymentReceive * @returns {ILedgerEntry} */ - private getPaymentDepositGLEntry = ( + private getDebitUnearnedRevenueGLEntry = ( amount: number, paymentReceive: IPaymentReceive ): ILedgerEntry => { @@ -78,10 +164,11 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { return { ...commonJournal, - credit: amount, + debit: amount, accountId: paymentReceive.unearnedRevenueAccountId, accountNormal: AccountNormal.CREDIT, index: 2, + indexGroup: 20, }; }; @@ -100,11 +187,66 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { return { ...commonJournal, - debit: amount, + credit: amount, contactId: paymentReceive.customerId, accountId: ARAccountId, accountNormal: AccountNormal.DEBIT, index: 1, + indexGroup: 20, + }; + }; + + /** + * Retrieves the unearned revenue entries. + * @param {IPaymentReceive} paymentReceived - + * @returns {Array} + */ + private getUnearnedRevenueEntries = ( + paymentReceive: IPaymentReceive + ): Array => { + const depositEntry = this.getDepositPaymentGLEntry(paymentReceive); + const unearnedEntry = this.getUnearnedRevenueEntry(paymentReceive); + + return [depositEntry, unearnedEntry]; + }; + + /** + * Retrieve the payment deposit entry. + * @param {IPaymentReceive} paymentReceived - + * @returns {ILedgerEntry} + */ + private getDepositPaymentGLEntry = ( + paymentReceive: IPaymentReceive + ): ILedgerEntry => { + const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + + return { + ...commonJournal, + debit: paymentReceive.amount, + accountId: paymentReceive.depositAccountId, + accountNormal: AccountNormal.DEBIT, + indexGroup: 10, + index: 1, + }; + }; + + /** + * Retrieve the unearned revenue entry. + * @param {IPaymentReceive} paymentReceived - + * @returns {ILedgerEntry} + */ + private getUnearnedRevenueEntry = ( + paymentReceived: IPaymentReceive + ): ILedgerEntry => { + const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceived); + + return { + ...commonJournal, + credit: paymentReceived.amount, + accountId: paymentReceived.unearnedRevenueAccountId, + accountNormal: AccountNormal.CREDIT, + indexGroup: 10, + index: 1, }; }; } diff --git a/packages/server/src/subscribers/PaymentReceive/WriteGLEntries.ts b/packages/server/src/subscribers/PaymentReceive/WriteGLEntries.ts index bffe4260f..e81aefc16 100644 --- a/packages/server/src/subscribers/PaymentReceive/WriteGLEntries.ts +++ b/packages/server/src/subscribers/PaymentReceive/WriteGLEntries.ts @@ -3,15 +3,20 @@ import { IPaymentReceiveCreatedPayload, IPaymentReceiveDeletedPayload, IPaymentReceiveEditedPayload, + PaymentReceiveUnearnedRevenueAppliedEventPayload, } from '@/interfaces'; import events from '@/subscribers/events'; import { PaymentReceiveGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceiveGLEntries'; +import { PaymentReceivedUnearnedGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries'; @Service() export default class PaymentReceivesWriteGLEntriesSubscriber { @Inject() private paymentReceiveGLEntries: PaymentReceiveGLEntries; + @Inject() + private paymentReceivedUnearnedGLEntries: PaymentReceivedUnearnedGLEntries; + /** * Attaches events with handlers. */ @@ -20,6 +25,14 @@ export default class PaymentReceivesWriteGLEntriesSubscriber { events.paymentReceive.onCreated, this.handleWriteJournalEntriesOnceCreated ); + bus.subscribe( + events.paymentReceive.onCreated, + this.handleWriteUnearnedRevenueGLEntriesOnCreated.bind(this) + ); + bus.subscribe( + events.paymentReceive.onUnearnedRevenueApplied, + this.handleRewriteUnearnedRevenueGLEntriesOnApply + ); bus.subscribe( events.paymentReceive.onEdited, this.handleOverwriteJournalEntriesOnceEdited @@ -32,6 +45,7 @@ export default class PaymentReceivesWriteGLEntriesSubscriber { /** * Handle journal entries writing once the payment receive created. + * @param {IPaymentReceiveCreatedPayload} payload - */ private handleWriteJournalEntriesOnceCreated = async ({ tenantId, @@ -45,8 +59,41 @@ export default class PaymentReceivesWriteGLEntriesSubscriber { ); }; + /** + * Handles rewrite payment GL entries on unearned revenue payload. + * @param {PaymentReceiveUnearnedRevenueAppliedEventPayload} payload - + */ + private handleWriteUnearnedRevenueGLEntriesOnCreated = async ({ + tenantId, + paymentReceiveId, + trx, + }: IPaymentReceiveCreatedPayload) => { + await this.paymentReceivedUnearnedGLEntries.writePaymentGLEntries( + tenantId, + paymentReceiveId, + trx + ); + }; + + /** + * Handles rewrite unearned revenue GL entries on payment received applied. + * @param {PaymentReceiveUnearnedRevenueAppliedEventPayload} payload - + */ + private handleRewriteUnearnedRevenueGLEntriesOnApply = async ({ + tenantId, + paymentReceiveId, + trx, + }: PaymentReceiveUnearnedRevenueAppliedEventPayload) => { + await this.paymentReceivedUnearnedGLEntries.rewritePaymentGLEntries( + tenantId, + paymentReceiveId, + trx + ); + }; + /** * Handle journal entries writing once the payment receive edited. + * @param {IPaymentReceiveEditedPayload} payload - */ private handleOverwriteJournalEntriesOnceEdited = async ({ tenantId, @@ -62,6 +109,7 @@ export default class PaymentReceivesWriteGLEntriesSubscriber { /** * Handles revert journal entries once deleted. + * @param {IPaymentReceiveDeletedPayload} payload - */ private handleRevertJournalEntriesOnceDeleted = async ({ tenantId, diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 711dbce35..164fb4df0 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -142,6 +142,8 @@ export default { onMailReminderSend: 'onSaleInvoiceMailReminderSend', onMailReminderSent: 'onSaleInvoiceMailReminderSent', + + onUnearnedRevenueApplied: 'onSaleInvoiceUnearnedRevenue', }, /** @@ -230,6 +232,8 @@ export default { onPreMailSend: 'onPaymentReceivePreMailSend', onMailSend: 'onPaymentReceiveMailSend', onMailSent: 'onPaymentReceiveMailSent', + + onUnearnedRevenueApplied: 'onPaymentReceivedUnearnedRevenue', }, /**