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;
transactionType: string;
transactionSubType: string;
transactionSubType?: string;
transactionId: number;

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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<void>}
*/
public async autoApplyUnearnedRevenueToInvoice(
tenantId: number,
invoiceId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
) {
): Promise<void> {
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<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)
.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<void>}
*/
public applyInvoiceToPaymentReceived = async (
tenantId: number,
@@ -61,7 +99,7 @@ export class AutoApplyUnearnedRevenue {
invoiceId: number,
appliedAmount: number,
trx?: Knex.Transaction
) => {
): Promise<void> => {
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
);
};
}

View File

@@ -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 {

View File

@@ -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<void> => {
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<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 {IPaymentReceive} paymentReceive -
* @returns {Array<ILedgerEntry>}
*/
private getPaymentEntryGLEntries = R.curry(
(
paymentReceivedEntry: IPaymentReceiveEntry,
paymentReceive: IPaymentReceive
) => {
const depositEntry = this.getPaymentDepositGLEntry(
ARAccountId: number,
paymentReceive: IPaymentReceive,
paymentReceivedEntry: IPaymentReceiveEntry
): Array<ILedgerEntry> => {
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<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,
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,

View File

@@ -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',
},
/**