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]

Reply via email to