FINERACT-1981: Disbursement on overpaid loan

This commit is contained in:
Adam Saghy
2024-02-06 13:20:01 +01:00
parent 8a67b5af65
commit 99352c66b2
9 changed files with 186 additions and 72 deletions
@@ -2483,7 +2483,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges, public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail, boolean downPaymentEnabled) { final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE);
@@ -2501,7 +2501,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(this.repaymentScheduleDetail().isInterestRecalculationEnabled(), regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(this.repaymentScheduleDetail().isInterestRecalculationEnabled(),
isDisbursementMissed(), scheduleGeneratorDTO, downPaymentEnabled); isDisbursementMissed(), scheduleGeneratorDTO);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate); updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
@@ -2533,7 +2533,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam, private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam,
boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean downPaymentEnabled) { boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO) {
LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate(); LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate();
if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) { if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) {
@@ -3837,7 +3837,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) { if (loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency)); totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
} else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback()) { } else if (loanTransaction.isCreditBalanceRefund() || loanTransaction.isChargeback() || loanTransaction.isDisbursement()) {
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency)); totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
} }
} }
@@ -5940,7 +5940,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals(); List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) { for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) { if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())); outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()))
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getAmount(getCurrency()); Money transactionOutstanding = loanTransaction.getAmount(getCurrency());
@@ -154,7 +154,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) { List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
switch (loanTransaction.getTypeOf()) { switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments); case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments, overpaymentHolder);
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder); case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder);
@@ -281,12 +281,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency, private void handleDisbursement(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
updateLoanSchedule(loanTransaction, currency, installments); updateLoanSchedule(loanTransaction, currency, installments, overpaymentHolder);
} }
private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency, private void updateLoanSchedule(LoanTransaction disbursementTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
final MathContext mc = MoneyHelper.getMathContext(); final MathContext mc = MoneyHelper.getMathContext();
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter( List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
@@ -304,23 +304,43 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf); downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
} }
downPaymentAmount = Money.of(currency, downPaymentAmt); downPaymentAmount = Money.of(currency, downPaymentAmt);
Money autoPayFromOverpayment = overpaymentHolder.getMoneyObject();
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount); downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
Money paidAmount = downPaymentInstallment.payPrincipalComponent(disbursementTransaction.getTransactionDate(),
autoPayFromOverpayment);
disbursementTransaction.setOverPayments(paidAmount);
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(paidAmount));
} }
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode()); Money overpaidAmount = overpaymentHolder.getMoneyObject();
if (installmentAmountInMultiplesOf != null) { if (overpaidAmount.isGreaterThanZero()) {
increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf); if (amortizableAmount.isGreaterThan(overpaidAmount)) {
overpaymentHolder.setMoneyObject(Money.zero(currency));
amortizableAmount = amortizableAmount.minus(overpaidAmount);
disbursementTransaction.setOverPayments(disbursementTransaction.getOverPaymentPortion(currency).add(overpaidAmount));
} else {
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(amortizableAmount));
amortizableAmount = Money.zero(currency);
disbursementTransaction.setOverPayments(disbursementTransaction.getOverPaymentPortion(currency).add(amortizableAmount));
}
} }
Money remainingAmount = amortizableAmount
.minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()));
Money finalIncreasePrincipalBy = increasePrincipalBy; if (amortizableAmount.isGreaterThanZero()) {
candidateRepaymentInstallments Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
.forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy)); if (installmentAmountInMultiplesOf != null) {
// Hence the rounding, we might need to amend the last installment amount increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf);
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) }
.addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount);
Money remainingAmount = amortizableAmount
.minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()));
Money finalIncreasePrincipalBy = increasePrincipalBy;
candidateRepaymentInstallments
.forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy));
// Hence the rounding, we might need to amend the last installment amount
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
.addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount);
}
} }
private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency, private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
@@ -784,7 +784,7 @@ public class AccountingProcessorHelper {
return this.officeRepository.getReferenceById(officeId); return this.officeRepository.getReferenceById(officeId);
} }
private void createCreditJournalEntryOrReversalForLoan(final Office office, final String currencyCode, final int accountMappingTypeId, public void createCreditJournalEntryOrReversalForLoan(final Office office, final String currencyCode, final int accountMappingTypeId,
final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId, final Long loanProductId, final Long paymentTypeId, final Long loanId, final String transactionId,
final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) { final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) {
final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId); final GLAccount account = getLinkedGLAccountForLoanProduct(loanProductId, accountMappingTypeId, paymentTypeId);
@@ -33,6 +33,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -467,30 +468,34 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
// transaction properties // transaction properties
final String transactionId = loanTransactionDTO.getTransactionId(); final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal disbursalAmount = loanTransactionDTO.getAmount(); final BigDecimal overpaymentPortion = loanTransactionDTO.getOverPayment() != null ? loanTransactionDTO.getOverPayment()
: BigDecimal.ZERO;
final BigDecimal principalPortion = loanTransactionDTO.getAmount().subtract(overpaymentPortion);
final boolean isReversed = loanTransactionDTO.isReversed(); final boolean isReversed = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
// create journal entries for the disbursement (or disbursement // create journal entries for the disbursement (or disbursement
// reversal) // reversal)
if (loanTransactionDTO.isLoanToLoanTransfer()) { this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversed);
FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversed);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversed);
} else {
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversed);
}
if (MathUtil.isGreaterThanZero(overpaymentPortion)) {
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, overpaymentPortion, isReversed);
}
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed);
} else {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversed);
}
} }
/** /**
*
* Handles repayments using the following posting rules <br/> * Handles repayments using the following posting rules <br/>
* <br/> * <br/>
* <br/> * <br/>
@@ -520,7 +525,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
* @param loanTransactionDTO * @param loanTransactionDTO
* @param loanDTO * @param loanDTO
* @param office * @param office
*
*/ */
private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office, final boolean writeOff, final boolean isIncomeFromFee) { final Office office, final boolean writeOff, final boolean isIncomeFromFee) {
@@ -32,6 +32,7 @@ import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO; import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO; import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO; import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.office.domain.Office;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -468,23 +469,29 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
// transaction properties // transaction properties
final String transactionId = loanTransactionDTO.getTransactionId(); final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal disbursalAmount = loanTransactionDTO.getAmount(); final BigDecimal overpaymentPortion = loanTransactionDTO.getOverPayment() != null ? loanTransactionDTO.getOverPayment()
: BigDecimal.ZERO;
final BigDecimal principalPortion = loanTransactionDTO.getAmount().subtract(overpaymentPortion);
final boolean isReversal = loanTransactionDTO.isReversed(); final boolean isReversal = loanTransactionDTO.isReversed();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(),
FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversal);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(),
FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversal);
} else {
this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(),
CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
disbursalAmount, isReversal);
}
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPortion, isReversal);
if (MathUtil.isGreaterThanZero(overpaymentPortion)) {
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.OVERPAYMENT.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, overpaymentPortion, isReversal);
}
if (loanTransactionDTO.isLoanToLoanTransfer()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal);
} else if (loanTransactionDTO.isAccountTransfer()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal);
} else {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, loanTransactionDTO.getAmount(), isReversal);
}
} }
/** /**
@@ -427,7 +427,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
disburseLoanToLoan(loan, command, loanOutstanding); disburseLoanToLoan(loan, command, loanOutstanding);
} }
LoanTransaction disbursementTransaction = null;
if (isAccountTransfer) { if (isAccountTransfer) {
disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail); disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail);
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
@@ -435,11 +435,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} else { } else {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail,
actualDisbursementDate, txnExternalId); actualDisbursementDate, txnExternalId);
disbursementTransaction.updateLoan(loan); disbursementTransaction.updateLoan(loan);
loan.addLoanTransaction(disbursementTransaction); loan.addLoanTransaction(disbursementTransaction);
LoanTransaction savedLoanTransaction = loanTransactionRepository.saveAndFlush(disbursementTransaction);
} }
if (loan.getRepaymentScheduleInstallments().isEmpty()) { if (loan.getRepaymentScheduleInstallments().isEmpty()) {
/* /*
@@ -454,12 +453,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
} }
if (isPaymentTypeApplicableForDisbursementCharge) { if (isPaymentTypeApplicableForDisbursementCharge) {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail);
downPaymentEnabled);
} else { } else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
} }
loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); loan.adjustNetDisbursalAmount(amountToDisburse.getAmount());
if (disbursementTransaction != null) {
loanTransactionRepository.saveAndFlush(disbursementTransaction);
}
if (loan.isAutoRepaymentForDownPaymentEnabled()) { if (loan.isAutoRepaymentForDownPaymentEnabled()) {
// updating linked savings account for auto down payment transaction for disbursement to savings account // updating linked savings account for auto down payment transaction for disbursement to savings account
if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) {
@@ -764,10 +765,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
} }
if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) { if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail, changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail);
downPaymentEnabled);
} else { } else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null, downPaymentEnabled); changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
} }
} }
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
@@ -46,6 +46,8 @@ import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
@@ -3124,6 +3126,85 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
}); });
} }
// UC122: Advanced payment allocation, 2nd disbursement on overpaid loan
// ADVANCED_PAYMENT_ALLOCATION_STRATEGY
// 1. Create a Loan product with Adv. Pment. Alloc.
// 2. Submit Loan and approve
// 3. Disburse only 100 from 1000
// 4. Overpay the loan (110)
// 5. Disburse again 100
@Test
public void uc122() {
runAt("24 November 2023", () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true)
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)).enableAutoRepaymentForDownPayment(false);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023",
1000.0, 4);
applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("22 November 2023").locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 100.0, 0.0, 100.0, 0.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 0.0, 25.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 7), 25.0, 0.0, 25.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 22), 25.0, 0.0, 25.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 1, 6), 25.0, 0.0, 25.0, 0.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest()
.dateFormat(DATETIME_PATTERN).transactionDate("23 November 2023").locale("en").transactionAmount(110.0));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 0.0, 100.0, 0.0, 100.0, 10.0);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 25.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 12, 7), 25.0, 25.0, 0.0, 25.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 22), 25.0, 25.0, 0.0, 25.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 1, 6), 25.0, 25.0, 0.0, 25.0, 0.0);
assertTrue(loanDetails.getStatus().getOverpaid());
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("24 November 2023").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 90.0, 110.0, 90.0, 110.0, null);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2023, 11, 22), 25.0, 25.0, 0.0, 0.0, 25.0);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2023, 11, 24), 25.0, 10.0, 15.0, 0.0, 0.0);
validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2023, 12, 7), 50.0, 25.0, 25.0, 25.0, 0.0);
validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2023, 12, 22), 50.0, 25.0, 25.0, 25.0, 0.0);
validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 1, 6), 50.0, 25.0, 25.0, 25.0, 0.0);
assertTrue(loanDetails.getStatus().getActive());
verifyTransactions(loanResponse.getLoanId(), //
transaction(100, "Disbursement", "22 November 2023", 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(110, "Repayment", "23 November 2023", 0.0, 100.0, 0.0, 0.0, 0.0, 0.0, 10.0), //
transaction(100, "Disbursement", "24 November 2023", 90.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0) //
);
// verify journal entries
verifyJournalEntries(loanResponse.getLoanId(), journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
journalEntry(10.0, overpaymentAccount, "CREDIT"), //
journalEntry(110.0, suspenseClearingAccount, "DEBIT"), //
journalEntry(90.0, loansReceivableAccount, "DEBIT"), //
journalEntry(10.0, overpaymentAccount, "DEBIT"), //
journalEntry(100.0, suspenseClearingAccount, "CREDIT") //
);
});
}
private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment,
Double principalOutstanding, Double principalPaid, Double totalOverpaid) { Double principalOutstanding, Double principalPaid, Double totalOverpaid) {
assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding());
@@ -66,8 +66,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati
// Then verify // Then verify
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) //
); );
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
@@ -99,8 +99,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati
// Then verify // Then verify
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // transaction(50, "Waive loan charges", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) //
); );
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
@@ -137,8 +137,8 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati
// Then verify // Then verify
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(50, "Waive loan charges", "05 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0) // transaction(50, "Waive loan charges", "05 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) //
); );
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
@@ -151,9 +151,9 @@ public class AdvancedPaymentAllocationWaiveLoanCharges extends BaseLoanIntegrati
addRepaymentForLoan(loanId, 200.0, "03 January 2023"); addRepaymentForLoan(loanId, 200.0, "03 January 2023");
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0), // transaction(1000, "Disbursement", "01 January 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(200, "Repayment", "03 January 2023", 800.0, 200.0, 0.0, 0.0, 0.0, 0.0), // transaction(200, "Repayment", "03 January 2023", 800.0, 200.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
transaction(50, "Waive loan charges", "05 January 2023", 800.0, 0.0, 0.0, 0.0, 0.0, 50.0) // transaction(50, "Waive loan charges", "05 January 2023", 800.0, 0.0, 0.0, 0.0, 0.0, 50.0, 0.0) //
); );
}); });
} }
@@ -489,9 +489,9 @@ public abstract class BaseLoanIntegrationTest {
} }
protected TransactionExt transaction(double amount, String type, String date, double outstandingAmount, double principalPortion, protected TransactionExt transaction(double amount, String type, String date, double outstandingAmount, double principalPortion,
double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion) { double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion, double overpaymentPortion) {
return new TransactionExt(amount, type, date, outstandingAmount, principalPortion, interestPortion, feePortion, penaltyPortion, return new TransactionExt(amount, type, date, outstandingAmount, principalPortion, interestPortion, feePortion, penaltyPortion,
unrecognizedIncomePortion); unrecognizedIncomePortion, overpaymentPortion);
} }
protected Installment installment(double principalAmount, Boolean completed, String dueDate) { protected Installment installment(double principalAmount, Boolean completed, String dueDate) {
@@ -530,6 +530,7 @@ public abstract class BaseLoanIntegrationTest {
Double feePortion; Double feePortion;
Double penaltyPortion; Double penaltyPortion;
Double unrecognizedPortion; Double unrecognizedPortion;
Double overpaymentPortion;
} }
@ToString @ToString