mariiaKraievska commented on code in PR #6067:
URL: https://github.com/apache/fineract/pull/6067#discussion_r3505081397
##########
fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java:
##########
@@ -80,14 +80,17 @@ public void processBuyDownFeeAmortizationTillDate(@NonNull
Loan loan, @NonNull L
AmortizationType amortizationType;
if (!balance.isDeleted()) {
final List<LoanTransaction> adjustments =
loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
- final Money amortizationTillDate =
BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance,
adjustments,
- maturityDate,
loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), tillDatePlusOne,
loan.getCurrency());
final BigDecimal alreadyAmortizedAmount =
loanAmortizationAllocationService
.calculateAlreadyAmortizedAmount(balance.getLoanTransaction().getId(),
loan.getId());
if (MathUtil.isZero(balance.getUnrecognizedAmount()) &&
adjustments.isEmpty()) {
totalAmortization =
totalAmortization.add(Money.of(loan.getCurrency(), alreadyAmortizedAmount));
continue;
}
+ final boolean fullyAmortizedOnSaleOrClosure =
MathUtil.isZero(balance.getUnrecognizedAmount())
+ &&
alreadyAmortizedAmount.compareTo(balance.getAmount()) >= 0;
+ final LocalDate effectiveTillDate =
fullyAmortizedOnSaleOrClosure ? maturityDate : tillDatePlusOne;
+ final Money amortizationTillDate =
BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance,
adjustments,
+ maturityDate,
loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), effectiveTillDate,
loan.getCurrency());
Review Comment:
@Cocoa-Puffs @peter-kovacs-dpc We also have a mirror method for Capitalized
Income — processCapitalizedIncomeAmortizationTillDate. Maybe it makes sense to
apply these changes there as well and cover it with the similar tests? What do
you think?
##########
integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanBuyDownFeeTest.java:
##########
@@ -1029,4 +1032,68 @@ public void
testBuyDownFeeWithAdvanceAccountingMappings() {
credit(feeIncomeAccount, 0.14)); // Second BuyDown Fee
Without classification
});
}
+
+ @Test
+ public void
testBuyDownFeePartialAdjustmentAfterFullAmortizationViaInvestorSale() {
+ final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+ final AtomicReference<Long> buyDownFeeTransactionIdRef = new
AtomicReference<>();
+
+ final Account transferAccount =
accountHelper.createAssetAccount("transferInSuspense");
+ final FinancialActivityAccountHelper financialActivityAccountHelper =
new FinancialActivityAccountHelper(requestSpec);
+ final ExternalAssetOwnerHelper externalAssetOwnerHelper = new
ExternalAssetOwnerHelper();
+
externalAssetOwnerHelper.setProperFinancialActivity(financialActivityAccountHelper,
transferAccount);
+
+ final PostClientsResponse client =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+ final PostLoanProductsResponse loanProduct =
loanProductHelper.createLoanProduct(createProgressiveLoanProductWithBuyDownFee(null));
+
+ // Step 1: Create and disburse loan on July 1, 2026, add buydown fee,
sell to investor
+ runAt("01 July 2026", () -> {
+ final Long loanId =
applyAndApproveProgressiveLoan(client.getClientId(),
loanProduct.getResourceId(), "01 July 2026", 1000.0,
+ 10.0, 6, null);
+ loanIdRef.set(loanId);
+ disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 July 2026");
+
+ final PostLoansLoanIdTransactionsResponse buyDownFeeResponse =
loanTransactionHelper.makeLoanBuyDownFee(loanId, "01 July 2026",
+ 50.0);
+ buyDownFeeTransactionIdRef.set(buyDownFeeResponse.getResourceId());
+
+ externalAssetOwnerHelper.initiateTransferByLoanId(loanId, "sale",
+ new
ExternalAssetOwnerRequest().settlementDate("2026-07-01").dateFormat("yyyy-MM-dd").locale("en")
+
.transferExternalId(UUID.randomUUID().toString()).ownerExternalId(UUID.randomUUID().toString())
+ .purchasePriceRatio("1.0"));
+ });
+
+ // Step 2: COB on July 2 — investor sale is processed → buydown fee
should be fully amortized
+ runAt("02 July 2026", () -> {
+ final Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ final GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(loanId);
+ assertNotNull(loanDetails.getTransactions());
+
+ final BigDecimal totalAmortized =
loanDetails.getTransactions().stream()
+ .filter(t -> t.getType() != null &&
Boolean.TRUE.equals(t.getType().getBuyDownFeeAmortization()))
+
.map(GetLoansLoanIdTransactions::getAmount).reduce(BigDecimal.ZERO,
BigDecimal::add);
+ assertEquals(0, BigDecimal.valueOf(50.0).compareTo(totalAmortized),
+ "Buydown fee should be fully amortized after investor
sale");
+
+ loanTransactionHelper.buyDownFeeAdjustment(loanId,
buyDownFeeTransactionIdRef.get(), "02 July 2026", 20.0);
+ });
+
+ // Step 4: COB on July 3 — expect amortization adjustment equal to the
$20 partial adjustment
+ runAt("03 July 2026", () -> {
+ final Long loanId = loanIdRef.get();
+ executeInlineCOB(loanId);
+
+ final GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(loanId);
+ assertNotNull(loanDetails.getTransactions());
+
+ final Optional<GetLoansLoanIdTransactions> amortizationAdjustment
= loanDetails.getTransactions().stream()
+ .filter(t -> t.getType() != null &&
Boolean.TRUE.equals(t.getType().getBuyDownFeeAmortizationAdjustment())).findFirst();
+
+ assertTrue(amortizationAdjustment.isPresent());
+ assertEquals(0,
BigDecimal.valueOf(20.0).compareTo(amortizationAdjustment.get().getAmount()),
+ "Buy Down Fee Amortization Adjustment should match the $20
partial adjustment");
+ });
Review Comment:
Yes, I tried to run similar test, and it creates unnecessary Buy Down Fee
Amortization Adjustment, after the second COB run after adjustment.
@Cocoa-Puffs @peter-kovacs-dpc please check the test below (it fails for me):
Scenario: Verify second daily COB after buy down fee adjustment post
investor sale does not duplicate amortization adjustment
When Admin sets the business date to "01 August 2026"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted
on date | with Principal | ANNUAL interest rate % | interest type |
interest calculation period | amortization type | loanTermFrequency |
loanTermFrequencyType | repaymentEvery | repaymentFrequencyType |
numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment |
interest free period | Payment strategy |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES | 01 August
2026 | 1000 | 7 | DECLINING_BALANCE | DAILY
| EQUAL_INSTALLMENTS | 3 | MONTHS
| 1 | MONTHS | 3 | 0
| 0 | 0 |
ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 August 2026" with "1000"
amount and expected disbursement date on "01 August 2026"
And Admin successfully disburse the loan on "01 August 2026" with "1000"
EUR transaction amount
And Admin adds buy down fee with "AUTOPAY" payment type to the loan on
"01 August 2026" with "50" EUR transaction amount
When Admin makes asset externalization request by Loan ID with unique
ownerExternalId, system-generated transferExternalId and the following data:
| Transaction type | settlementDate | purchasePriceRatio |
| sale | 2026-08-01 | 1 |
When Admin sets the business date to "02 August 2026"
And Admin runs inline COB job for Loan
And Admin adds buy down fee adjustment with "AUTOPAY" payment type to
the loan on "02 August 2026" with "20" EUR transaction amount
When Admin sets the business date to "03 August 2026"
And Admin runs inline COB job for Loan
And Loan Transactions tab has the following data without accruals:
| Transaction date | Transaction Type | Amount |
Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 August 2026 | Disbursement | 1000.0 |
0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 01 August 2026 | Buy Down Fee | 50.0 |
0.0 | 50.0 | 0.0 | 0.0 | 0.0 | false |
| 01 August 2026 | Buy Down Fee Amortization | 0.54 |
0.0 | 0.54 | 0.0 | 0.0 | 0.0 | false |
| 01 August 2026 | Buy Down Fee Amortization | 49.46 |
0.0 | 49.46 | 0.0 | 0.0 | 0.0 | false |
| 02 August 2026 | Buy Down Fee Adjustment | 20.0 |
0.0 | 20.0 | 0.0 | 0.0 | 0.0 | false |
| 02 August 2026 | Buy Down Fee Amortization Adjustment | 20.0 |
0.0 | 20.0 | 0.0 | 0.0 | 0.0 | false |
When Admin sets the business date to "04 August 2026"
And Admin runs inline COB job for Loan
Then Loan Transactions tab has the following data without accruals:
| Transaction date | Transaction Type | Amount |
Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 August 2026 | Disbursement | 1000.0 |
0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
| 01 August 2026 | Buy Down Fee | 50.0 |
0.0 | 50.0 | 0.0 | 0.0 | 0.0 | false |
| 01 August 2026 | Buy Down Fee Amortization | 0.54 |
0.0 | 0.54 | 0.0 | 0.0 | 0.0 | false |
| 01 August 2026 | Buy Down Fee Amortization | 49.46 |
0.0 | 49.46 | 0.0 | 0.0 | 0.0 | false |
| 02 August 2026 | Buy Down Fee Adjustment | 20.0 |
0.0 | 20.0 | 0.0 | 0.0 | 0.0 | false |
| 02 August 2026 | Buy Down Fee Amortization Adjustment | 20.0 |
0.0 | 20.0 | 0.0 | 0.0 | 0.0 | false |
When Loan Pay-off is made on "04 August 2026" with transfer external
owner
Then Loan is closed with zero outstanding balance and it's all
installments have obligations met
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]