feat: advanced payments

This commit is contained in:
Ahmed Bouhuolia
2024-07-23 13:52:25 +02:00
parent 8cd3a6c48d
commit 1141991e44
9 changed files with 295 additions and 37 deletions

View File

@@ -40,7 +40,7 @@ export interface ILedgerEntry {
date: Date | string; date: Date | string;
transactionType: string; transactionType: string;
transactionSubType: string; transactionSubType?: string;
transactionId: number; transactionId: number;

View File

@@ -187,3 +187,11 @@ export interface PaymentReceiveMailPresendEvent {
paymentReceiveId: number; paymentReceiveId: number;
messageOptions: PaymentReceiveMailOptsDTO; messageOptions: PaymentReceiveMailOptsDTO;
} }
export interface PaymentReceiveUnearnedRevenueAppliedEventPayload {
tenantId: number;
paymentReceiveId: number;
saleInvoiceId: number;
appliedAmount: number;
trx?: Knex.Transaction;
}

View File

@@ -216,3 +216,9 @@ export interface ISaleInvoiceMailSent {
saleInvoiceId: number; saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO; messageOptions: SendInvoiceMailDTO;
} }
export interface SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload {
tenantId: number;
saleInvoiceId: number;
trx?: Knex.Transaction;
}

View File

@@ -13,7 +13,7 @@ export class AutoApplyPrepardExpensesOnBillCreated {
*/ */
public attach(bus) { public attach(bus) {
bus.subscribe( bus.subscribe(
events.saleInvoice.onCreated, events.bill.onCreated,
this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this) this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this)
); );
} }

View File

@@ -1,22 +1,34 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex'; import { Knex } from 'knex';
import PromisePool from '@supercharge/promise-pool'; import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IPaymentReceive,
PaymentReceiveUnearnedRevenueAppliedEventPayload,
SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload,
} from '@/interfaces';
@Service() @Service()
export class AutoApplyUnearnedRevenue { export class AutoApplyUnearnedRevenue {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Auto apply invoice to advanced payment received transactions. * Auto apply invoice to advanced payment received transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} invoiceId * @param {number} invoiceId
* @returns {Promise<void>}
*/ */
public async autoApplyUnearnedRevenueToInvoice( public async autoApplyUnearnedRevenueToInvoice(
tenantId: number, tenantId: number,
invoiceId: number, saleInvoiceId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
) { ): Promise<void> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
const unappliedPayments = await PaymentReceive.query(trx).where( const unappliedPayments = await PaymentReceive.query(trx).where(
@@ -25,26 +37,51 @@ export class AutoApplyUnearnedRevenue {
0 0
); );
const invoice = await SaleInvoice.query(trx) const invoice = await SaleInvoice.query(trx)
.findById(invoiceId) .findById(saleInvoiceId)
.throwIfNotFound(); .throwIfNotFound();
let unappliedAmount = invoice.balance; let unappliedAmount = invoice.total;
let appliedTotalAmount = 0;
const processHandler: ProcessHandler<
IPaymentReceive,
Promise<void>
> = 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) await PromisePool.withConcurrency(1)
.for(unappliedPayments) .for(unappliedPayments)
.process(async (unappliedPayment: any) => { .process(processHandler);
const appliedAmount = unappliedAmount;
await this.applyInvoiceToPaymentReceived(
tenantId,
unappliedPayment.id,
invoice.id,
appliedAmount,
trx
);
});
// Increase the paid amount of the sale invoice. // 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} invoiceId
* @param {number} appliedAmount * @param {number} appliedAmount
* @param {Knex.Transaction} trx * @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/ */
public applyInvoiceToPaymentReceived = async ( public applyInvoiceToPaymentReceived = async (
tenantId: number, tenantId: number,
@@ -61,7 +99,7 @@ export class AutoApplyUnearnedRevenue {
invoiceId: number, invoiceId: number,
appliedAmount: number, appliedAmount: number,
trx?: Knex.Transaction trx?: Knex.Transaction
) => { ): Promise<void> => {
const { PaymentReceiveEntry, PaymentReceive } = const { PaymentReceiveEntry, PaymentReceive } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
@@ -71,5 +109,17 @@ export class AutoApplyUnearnedRevenue {
paymentAmount: appliedAmount, paymentAmount: appliedAmount,
}); });
await PaymentReceive.query(trx).increment('usedAmount', 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
);
}; };
} }

View File

@@ -1,3 +1,4 @@
import { Knex } from 'knex';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { import {
AccountNormal, AccountNormal,
@@ -5,7 +6,6 @@ import {
IPaymentReceive, IPaymentReceive,
IPaymentReceiveGLCommonEntry, IPaymentReceiveGLCommonEntry,
} from '@/interfaces'; } from '@/interfaces';
import { Knex } from 'knex';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
export class PaymentReceivedGLCommon { export class PaymentReceivedGLCommon {

View File

@@ -1,8 +1,9 @@
import * as R from 'ramda'; import * as R from 'ramda';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; 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 { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon';
import { import {
AccountNormal, AccountNormal,
@@ -10,12 +11,17 @@ import {
IPaymentReceive, IPaymentReceive,
IPaymentReceiveEntry, IPaymentReceiveEntry,
} from '@/interfaces'; } from '@/interfaces';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
@Service() @Service()
export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/** /**
* Writes payment GL entries to the storage. * Writes payment GL entries to the storage.
* @param {number} tenantId * @param {number} tenantId
@@ -30,38 +36,118 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
): Promise<void> => { ): Promise<void> => {
const { PaymentReceive } = this.tenancy.models(tenantId); 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. // Retrieves the payment receive with associated entries.
const paymentReceive = await PaymentReceive.query(trx) const paymentReceive = await PaymentReceive.query(trx)
.findById(paymentReceiveId) .findById(paymentReceiveId)
.withGraphFetched('entries.invoice'); .withGraphFetched('entries.invoice');
if (paymentReceive.unearnedRevenueAccountId) { // Stop early if
if (!paymentReceive.unearnedRevenueAccountId) {
return; 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<Ledger>}
*/
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<ILedgerEntry>}
*/
private getPaymentGLEntries = R.curry(
(
ARAccountId: number,
paymentReceive: IPaymentReceive
): Array<ILedgerEntry> => {
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 {IPaymentReceiveEntry} paymentReceivedEntry -
* @param {IPaymentReceive} paymentReceive - * @param {IPaymentReceive} paymentReceive -
* @returns {Array<ILedgerEntry>}
*/ */
private getPaymentEntryGLEntries = R.curry( private getPaymentEntryGLEntries = R.curry(
( (
paymentReceivedEntry: IPaymentReceiveEntry, ARAccountId: number,
paymentReceive: IPaymentReceive paymentReceive: IPaymentReceive,
) => { paymentReceivedEntry: IPaymentReceiveEntry
const depositEntry = this.getPaymentDepositGLEntry( ): Array<ILedgerEntry> => {
const unearnedRevenueEntry = this.getDebitUnearnedRevenueGLEntry(
paymentReceivedEntry.paymentAmount, paymentReceivedEntry.paymentAmount,
paymentReceive paymentReceive
); );
const AREntry = this.getPaymentReceivableEntry(
paymentReceivedEntry.paymentAmount,
paymentReceive,
ARAccountId
);
return [unearnedRevenueEntry, AREntry];
} }
); );
@@ -70,7 +156,7 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
* @param {IPaymentReceive} paymentReceive * @param {IPaymentReceive} paymentReceive
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
*/ */
private getPaymentDepositGLEntry = ( private getDebitUnearnedRevenueGLEntry = (
amount: number, amount: number,
paymentReceive: IPaymentReceive paymentReceive: IPaymentReceive
): ILedgerEntry => { ): ILedgerEntry => {
@@ -78,10 +164,11 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
return { return {
...commonJournal, ...commonJournal,
credit: amount, debit: amount,
accountId: paymentReceive.unearnedRevenueAccountId, accountId: paymentReceive.unearnedRevenueAccountId,
accountNormal: AccountNormal.CREDIT, accountNormal: AccountNormal.CREDIT,
index: 2, index: 2,
indexGroup: 20,
}; };
}; };
@@ -100,11 +187,66 @@ export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
return { return {
...commonJournal, ...commonJournal,
debit: amount, credit: amount,
contactId: paymentReceive.customerId, contactId: paymentReceive.customerId,
accountId: ARAccountId, accountId: ARAccountId,
accountNormal: AccountNormal.DEBIT, accountNormal: AccountNormal.DEBIT,
index: 1, index: 1,
indexGroup: 20,
};
};
/**
* Retrieves the unearned revenue entries.
* @param {IPaymentReceive} paymentReceived -
* @returns {Array<ILedgerEntry>}
*/
private getUnearnedRevenueEntries = (
paymentReceive: IPaymentReceive
): Array<ILedgerEntry> => {
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,
}; };
}; };
} }

View File

@@ -3,15 +3,20 @@ import {
IPaymentReceiveCreatedPayload, IPaymentReceiveCreatedPayload,
IPaymentReceiveDeletedPayload, IPaymentReceiveDeletedPayload,
IPaymentReceiveEditedPayload, IPaymentReceiveEditedPayload,
PaymentReceiveUnearnedRevenueAppliedEventPayload,
} from '@/interfaces'; } from '@/interfaces';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { PaymentReceiveGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceiveGLEntries'; import { PaymentReceiveGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceiveGLEntries';
import { PaymentReceivedUnearnedGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries';
@Service() @Service()
export default class PaymentReceivesWriteGLEntriesSubscriber { export default class PaymentReceivesWriteGLEntriesSubscriber {
@Inject() @Inject()
private paymentReceiveGLEntries: PaymentReceiveGLEntries; private paymentReceiveGLEntries: PaymentReceiveGLEntries;
@Inject()
private paymentReceivedUnearnedGLEntries: PaymentReceivedUnearnedGLEntries;
/** /**
* Attaches events with handlers. * Attaches events with handlers.
*/ */
@@ -20,6 +25,14 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
events.paymentReceive.onCreated, events.paymentReceive.onCreated,
this.handleWriteJournalEntriesOnceCreated this.handleWriteJournalEntriesOnceCreated
); );
bus.subscribe(
events.paymentReceive.onCreated,
this.handleWriteUnearnedRevenueGLEntriesOnCreated.bind(this)
);
bus.subscribe(
events.paymentReceive.onUnearnedRevenueApplied,
this.handleRewriteUnearnedRevenueGLEntriesOnApply
);
bus.subscribe( bus.subscribe(
events.paymentReceive.onEdited, events.paymentReceive.onEdited,
this.handleOverwriteJournalEntriesOnceEdited this.handleOverwriteJournalEntriesOnceEdited
@@ -32,6 +45,7 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
/** /**
* Handle journal entries writing once the payment receive created. * Handle journal entries writing once the payment receive created.
* @param {IPaymentReceiveCreatedPayload} payload -
*/ */
private handleWriteJournalEntriesOnceCreated = async ({ private handleWriteJournalEntriesOnceCreated = async ({
tenantId, 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. * Handle journal entries writing once the payment receive edited.
* @param {IPaymentReceiveEditedPayload} payload -
*/ */
private handleOverwriteJournalEntriesOnceEdited = async ({ private handleOverwriteJournalEntriesOnceEdited = async ({
tenantId, tenantId,
@@ -62,6 +109,7 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
/** /**
* Handles revert journal entries once deleted. * Handles revert journal entries once deleted.
* @param {IPaymentReceiveDeletedPayload} payload -
*/ */
private handleRevertJournalEntriesOnceDeleted = async ({ private handleRevertJournalEntriesOnceDeleted = async ({
tenantId, tenantId,

View File

@@ -142,6 +142,8 @@ export default {
onMailReminderSend: 'onSaleInvoiceMailReminderSend', onMailReminderSend: 'onSaleInvoiceMailReminderSend',
onMailReminderSent: 'onSaleInvoiceMailReminderSent', onMailReminderSent: 'onSaleInvoiceMailReminderSent',
onUnearnedRevenueApplied: 'onSaleInvoiceUnearnedRevenue',
}, },
/** /**
@@ -230,6 +232,8 @@ export default {
onPreMailSend: 'onPaymentReceivePreMailSend', onPreMailSend: 'onPaymentReceivePreMailSend',
onMailSend: 'onPaymentReceiveMailSend', onMailSend: 'onPaymentReceiveMailSend',
onMailSent: 'onPaymentReceiveMailSent', onMailSent: 'onPaymentReceiveMailSent',
onUnearnedRevenueApplied: 'onPaymentReceivedUnearnedRevenue',
}, },
/** /**