From 3498c333cd3692a9cdd5c107fbc5f2aaf2fe516c Mon Sep 17 00:00:00 2001 From: mariiaKraievska Date: Fri, 10 Jul 2026 12:21:24 +0300 Subject: [PATCH 1/2] FINERACT-2455: WC - Transaction Type - Accrual and accrual handling --- .../resources/features/Loan-Part2.feature | 4 +- .../LoanAccrualActivity-Part1.feature | 4 +- .../features/LoanAccrualTransaction.feature | 3 +- .../features/LoanRepayment-Part3.feature | 4 +- ...talGoodwillCreditPaymentAllocation.feature | 46 +++ .../WorkingCapitalLoanChargeAccrual.feature | 386 ++++++++++++++++++ ...WorkingCapitalLoanChargeAdjustment.feature | 6 + .../WorkingCapitalLoanPayoutRefund.feature | 4 + ...orkingCapitalTransactionAllocation.feature | 12 +- .../features/WorkingCapital_COB.feature | 1 + .../test/resources/junit-platform.properties | 5 + ...ountingProcessorForWorkingCapitalLoan.java | 14 + .../ChargeAccrualBusinessStep.java | 48 +++ .../domain/WorkingCapitalLoanTransaction.java | 7 + ...rkingCapitalLoanTransactionAllocation.java | 9 + ...ngCapitalLoanAllocationRequestFactory.java | 16 + ...orkingCapitalLoanChargeAccrualService.java | 134 ++++++ ...talLoanChargeWritePlatformServiceImpl.java | 3 + ...orkingCapitalLoanTransactionProcessor.java | 25 +- ...ngCapitalLoanWritePlatformServiceImpl.java | 20 +- .../module-changelog-master.xml | 1 + ...5_wc_loan_charge_accrual_business_step.xml | 39 ++ 22 files changed, 773 insertions(+), 18 deletions(-) create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/ChargeAccrualBusinessStep.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0055_wc_loan_charge_accrual_business_step.xml diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan-Part2.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan-Part2.feature index 4574122c423..288bb527fa2 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan-Part2.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan-Part2.feature @@ -1,3 +1,4 @@ +@SerialChargeAccrualConfig @LoanFeature Feature: Loan - Part2 @@ -2577,5 +2578,4 @@ Feature: Loan - Part2 | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | NSF fee | true | Specified due date | 17 October 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | | NSF fee | true | Specified due date | 16 October 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | - | NSF fee | true | Specified due date | 17 September 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | - + | NSF fee | true | Specified due date | 17 September 2023 | Flat | 20.0 | 20.0 | 0.0 | 0.0 | \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity-Part1.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity-Part1.feature index ff3f71c3167..8a3aed73d97 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity-Part1.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualActivity-Part1.feature @@ -1,3 +1,4 @@ +@SerialChargeAccrualConfig @LoanAccrualActivityFeature Feature: LoanAccrualActivity - Part1 @@ -5466,5 +5467,4 @@ Feature: LoanAccrualActivity - Part1 | 10 March 2025 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 25 March 2025 | Accrual Activity | 0.44 | 0.0 | 0.44 | 0.0 | 0.0 | 0.0 | false | true | And "Accrual Activity" transaction on "25 March 2025" got reverse-replayed on "28 March 2025" - And External ID of replayed "Accrual Activity" on "25 March 2025" is matching with "saved-external-id" - + And External ID of replayed "Accrual Activity" on "25 March 2025" is matching with "saved-external-id" \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index df4c814e6af..63d0a9038cb 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -1,3 +1,4 @@ +@SerialChargeAccrualConfig @LoanAccrualFeature Feature: LoanAccrualTransaction @@ -1992,4 +1993,4 @@ Feature: LoanAccrualTransaction | 17 November 2025 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | | 18 November 2025 | Accrual Activity | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | Then LoanAccrualTransactionCreatedBusinessEvent is raised on "17 November 2025" - Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "18 November 2025" + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "18 November 2025" \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment-Part3.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment-Part3.feature index bf62cf36ba0..70222033c7d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment-Part3.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment-Part3.feature @@ -1,3 +1,4 @@ +@SerialChargeAccrualConfig @Repayment Feature: LoanRepayment - Part3 @@ -3506,5 +3507,4 @@ Feature: LoanRepayment - Part3 | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 26 October 2025 | Disbursement | 4.0 | 0.0 | 0.0 | 0.0 | 0.0 | 4.0 | false | false | | 27 October 2025 | Repayment | 4.0 | 4.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | - Then Loan is closed with zero outstanding balance and it's all installments have obligations met - + Then Loan is closed with zero outstanding balance and it's all installments have obligations met \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature index 8f22cca33d2..7a5424350c0 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature @@ -1,7 +1,14 @@ +@SerialChargeAccrualConfig @WorkingCapital @WorkingCapitalGoodwillCreditPaymentAllocationFeature Feature: Working Capital Goodwill Credit Payment Allocation + # Pin the charge accrual date to due-date so the scenarios do not depend on the global config value left behind by a + # previously executed feature. The transaction assertions below expect the charge accrual to be posted by the COB on + # the charge due date; a leaked submitted-date value would instead accrue at charge-add time and break them. + Background: + Given Global config "charge-accrual-date" value set to "due-date" + @TestRailId:C85511 Scenario: Verify Working Capital Goodwill Credit transaction with fee and penalty added with DUE_FEE_PENALTY_PRINCIPAL allocation - UC1 Given Admin sets the business date to "01 January 2026" @@ -28,6 +35,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 230.0 | 15.0 | 25.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -59,6 +68,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 30.0 | 0.0 | 5.0 | 25.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -90,6 +101,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -121,6 +134,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 255.0 | 15.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -181,6 +196,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | @TestRailId:C85517 @@ -209,6 +226,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | # --- make CBR trn to refund overpaid amount --- # And Customer makes credit balance refund on "12 January 2026" with 160.0 transaction amount on Working Capital loan @@ -218,6 +237,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @@ -239,6 +260,7 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 20 January 2026 | Goodwill Credit | 100.0 | 0.0 | 100.0 | 0.0 | false | And Working Capital Loan has charges with the following data: | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | @@ -251,6 +273,7 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 10 January 2026 | Goodwill Credit | 100.0 | 0.0 | 100.0 | 0.0 | false | | 20 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | And Working Capital Loan has charges with the following data: @@ -282,6 +305,7 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 11 January 2026 | Goodwill Credit | 30.0 | 0.0 | 30.0 | 0.0 | false | | 11 January 2026 | Charge Adjustment | 70.0 | 0.0 | 70.0 | 0.0 | false | And Working Capital Loan has charges with the following data: @@ -761,6 +785,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -782,6 +808,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | And Working Capital Loan charge balances has the following data: | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | @@ -791,6 +819,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | And Working Capital Loan charge balances has the following data: @@ -822,6 +852,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | @TestRailId:C85525 @@ -846,6 +878,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | # --- make CBR transaction to refund overpaid amount --- # And Customer makes credit balance refund on "12 January 2026" with 160.0 transaction amount on Working Capital loan @@ -855,6 +889,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @@ -891,6 +927,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | | 13 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | When Admin sets the business date to "15 January 2026" @@ -913,12 +951,14 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 20 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | # --- backdated goodwill credit triggers reprocessing: goodwill keeps its own DUE_PRINCIPAL-first rule (principal 100), repayment keeps DUE_FEE-first (fee 100) --- # And Customer makes "GOODWILL_CREDIT" transaction on "15 January 2026" with 100.0 transaction amount on Working Capital loan Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 15 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | | 20 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | And Working capital loan account has the correct data: @@ -950,6 +990,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | # --- undo the goodwill credit: the repayment is replayed with its own REPAYMENT (DUE_FEE first) rule and keeps its portions --- # @@ -957,6 +999,8 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | true | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | And Working capital loan account has the correct data: @@ -991,3 +1035,5 @@ Feature: Working Capital Goodwill Credit Payment Allocation | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 15 January 2026 | Goodwill Credit | 9020.0 | 9000.0 | 15.0 | 5.0 | false | + | 15 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 15 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature new file mode 100644 index 00000000000..fb46ced50f4 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature @@ -0,0 +1,386 @@ +@SerialChargeAccrualConfig +@WorkingCapital +@WorkingCapitalLoanChargeAccrualFeature +Feature: Working Capital Loan Charge Accrual + + Scenario: Verify Working Capital fee charge accrual is posted on charge due date when charge-accrual-date is due-date + Given Admin sets the business date to "01 January 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2026" due date and 50.0 transaction amount + And Admin sets the business date to "14 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + When Admin sets the business date to "16 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 15 January 2026 | Accrual | 50.0 | 0.0 | 50.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 January 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 50.0 | | + | INCOME | 404007 | Fee Income | | 50.0 | + And Admin sets the business date to "20 January 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 January 2026" + + Scenario: Verify Working Capital penalty charge accrual is posted in real time when charge-accrual-date is submitted-date + Given Admin sets the business date to "01 February 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 February 2026 | 01 February 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "10 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "20 February 2026" due date and 30.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 February 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 February 2026 | Accrual | 30.0 | 0.0 | 0.0 | 30.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "10 February 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 30.0 | | + | INCOME | 404007 | Fee Income | | 30.0 | + When Admin sets the business date to "11 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 February 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 February 2026 | Accrual | 30.0 | 0.0 | 0.0 | 30.0 | false | + And Admin sets the business date to "20 February 2026" + When Global config "charge-accrual-date" value set to "due-date" + Then Admin closes the Working Capital loan with a full repayment on "20 February 2026" + + Scenario: Verify Working Capital fee and penalty charge accrual is posted in real time when charge-accrual-date is submitted-date + Given Admin sets the business date to "01 March 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 March 2026 | 01 March 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "10 March 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 March 2026" due date and 40.0 transaction amount + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "21 March 2026" due date and 25.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 March 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 March 2026 | Accrual | 40.0 | 0.0 | 40.0 | 0.0 | false | + | 10 March 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | + And Working Capital Loan Transactions tab has 2 "ACCRUAL" transactions with date "10 March 2026" which have the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 40.0 | | + | INCOME | 404007 | Fee Income | | 40.0 | + | ASSET | 112603 | Interest/Fee Receivable | 25.0 | | + | INCOME | 404007 | Fee Income | | 25.0 | + And Admin sets the business date to "25 March 2026" + When Global config "charge-accrual-date" value set to "due-date" + Then Admin closes the Working Capital loan with a full repayment on "25 March 2026" + + Scenario: Verify Working Capital fee charge accrual is posted in real time when charge-accrual-date is submitted-date + Given Admin sets the business date to "01 April 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 April 2026 | 01 April 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "10 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 April 2026" due date and 50.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 April 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 April 2026 | Accrual | 50.0 | 0.0 | 50.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "10 April 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 50.0 | | + | INCOME | 404007 | Fee Income | | 50.0 | + When Admin sets the business date to "11 April 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 April 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 April 2026 | Accrual | 50.0 | 0.0 | 50.0 | 0.0 | false | + And Admin sets the business date to "25 April 2026" + When Global config "charge-accrual-date" value set to "due-date" + Then Admin closes the Working Capital loan with a full repayment on "25 April 2026" + + Scenario: Verify Working Capital penalty charge accrual is posted on charge due date when charge-accrual-date is due-date + Given Admin sets the business date to "01 May 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 May 2026 | 01 May 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 May 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "15 May 2026" due date and 30.0 transaction amount + And Admin sets the business date to "14 May 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 May 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + When Admin sets the business date to "16 May 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 May 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 15 May 2026 | Accrual | 30.0 | 0.0 | 0.0 | 30.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 May 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 30.0 | | + | INCOME | 404007 | Fee Income | | 30.0 | + And Admin sets the business date to "20 May 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 May 2026" + + Scenario: Verify Working Capital fee and penalty charge accrual is posted on charge due dates when charge-accrual-date is due-date + Given Admin sets the business date to "01 June 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 June 2026 | 01 June 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 June 2026" due date and 40.0 transaction amount + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "21 June 2026" due date and 25.0 transaction amount + And Admin sets the business date to "14 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 June 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + When Admin sets the business date to "22 June 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 June 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 20 June 2026 | Accrual | 40.0 | 0.0 | 40.0 | 0.0 | false | + | 21 June 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "20 June 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 40.0 | | + | INCOME | 404007 | Fee Income | | 40.0 | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "21 June 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 25.0 | | + | INCOME | 404007 | Fee Income | | 25.0 | + And Admin sets the business date to "25 June 2026" + Then Admin closes the Working Capital loan with a full repayment on "25 June 2026" + + Scenario: Verify Working Capital fee charge adjustment before accrual when charge-accrual-date is due-date + Given Admin sets the business date to "01 July 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 July 2026 | 01 July 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 July 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 July 2026" due date and 100.0 transaction amount + And Admin makes a charge adjustment for the last added charge with 100.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 July 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 July 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + And Working Capital Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "10 July 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | INCOME | 404007 | Fee Income | 100.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 100.0 | + When Admin sets the business date to "16 July 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 July 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 July 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + | 15 July 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 July 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + And Admin sets the business date to "20 July 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 July 2026" + + Scenario: Verify Working Capital penalty charge adjustment before accrual when charge-accrual-date is due-date + Given Admin sets the business date to "01 August 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 August 2026 | 01 August 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "15 August 2026" due date and 50.0 transaction amount + And Admin makes a charge adjustment for the last added charge with 50.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 August 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 August 2026 | Charge Adjustment | 50.0 | 0.0 | 0.0 | 50.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 0.0 | 0.0 | 0.0 | 50.0 | 0.0 | 50.0 | + And Working Capital Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "10 August 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | INCOME | 404007 | Fee Income | 50.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 50.0 | + When Admin sets the business date to "16 August 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 August 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 August 2026 | Charge Adjustment | 50.0 | 0.0 | 0.0 | 50.0 | false | + | 15 August 2026 | Accrual | 50.0 | 0.0 | 0.0 | 50.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 August 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 50.0 | | + | INCOME | 404007 | Fee Income | | 50.0 | + And Admin sets the business date to "20 August 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 August 2026" + + Scenario: Verify Working Capital partial fee charge adjustment before accrual when charge-accrual-date is due-date + Given Admin sets the business date to "01 November 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 November 2026 | 01 November 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 November 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 November 2026" due date and 100.0 transaction amount + And Admin makes a charge adjustment for the last added charge with 40.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 November 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 November 2026 | Charge Adjustment | 40.0 | 0.0 | 40.0 | 0.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 60.0 | 40.0 | 0.0 | 0.0 | 0.0 | + And Working Capital Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "10 November 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | INCOME | 404007 | Fee Income | 40.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 40.0 | + When Admin sets the business date to "16 November 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 November 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 November 2026 | Charge Adjustment | 40.0 | 0.0 | 40.0 | 0.0 | false | + | 15 November 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 November 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + And Admin sets the business date to "20 November 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 November 2026" + + Scenario: Verify Working Capital partial penalty charge adjustment before accrual when charge-accrual-date is due-date + Given Admin sets the business date to "01 December 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 December 2026 | 01 December 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 December 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "15 December 2026" due date and 60.0 transaction amount + And Admin makes a charge adjustment for the last added charge with 25.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 December 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 December 2026 | Charge Adjustment | 25.0 | 0.0 | 0.0 | 25.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 0.0 | 0.0 | 0.0 | 60.0 | 35.0 | 25.0 | + And Working Capital Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "10 December 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | INCOME | 404007 | Fee Income | 25.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 25.0 | + When Admin sets the business date to "16 December 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 December 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 December 2026 | Charge Adjustment | 25.0 | 0.0 | 0.0 | 25.0 | false | + | 15 December 2026 | Accrual | 60.0 | 0.0 | 0.0 | 60.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 December 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 60.0 | | + | INCOME | 404007 | Fee Income | | 60.0 | + And Admin sets the business date to "20 December 2026" + Then Admin closes the Working Capital loan with a full repayment on "20 December 2026" + + Scenario: Verify Working Capital fee charge accrual is still posted on due date when the charge was fully repaid before the accrual runs + Given Admin sets the business date to "01 January 2027" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 January 2027 | 01 January 2027 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 January 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "12 January 2027" due date and 100.0 transaction amount + And Admin sets the business date to "12 January 2027" + And Customer makes repayment on "12 January 2027" with 100.0 transaction amount on Working Capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2027 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + When Admin sets the business date to "13 January 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2027 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | + | 12 January 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "12 January 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + And Admin sets the business date to "20 January 2027" + Then Admin closes the Working Capital loan with a full repayment on "20 January 2027" + + Scenario: Verify Working Capital fee charge accrual is posted on the closing date when the loan is closed before the charge due date + Given Admin sets the business date to "01 February 2027" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 February 2027 | 01 February 2027 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 February 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 February 2027" due date and 100.0 transaction amount + And Admin sets the business date to "15 February 2027" + And Customer makes repayment on "15 February 2027" with 9100.0 transaction amount on Working Capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 February 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 15 February 2027 | Repayment | 9100.0 | 9000.0 | 100.0 | 0.0 | false | + | 15 February 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 February 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + + Scenario: Verify Working Capital charge accrual is reversed when the disbursal is undone + Given Admin sets the business date to "01 March 2027" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 March 2027 | 01 March 2027 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "10 March 2027" + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 March 2027" due date and 100.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 March 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 March 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "10 March 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + When Admin successfully undo Working Capital disbursal + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 March 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | true | + | 10 March 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | true | + When Global config "charge-accrual-date" value set to "due-date" \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature index 675ba1b5e10..ad4d8d3db78 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAdjustment.feature @@ -196,6 +196,8 @@ Feature: WorkingCapitalLoanChargeAdjustmentFeature Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 80.0 | 0.0 | 80.0 | 0.0 | false | + | 10 January 2026 | Accrual | 30.0 | 0.0 | 0.0 | 30.0 | false | | 11 January 2026 | Charge Adjustment | 80.0 | 0.0 | 80.0 | 0.0 | false | And Working Capital Loan has charges with the following data: | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | @@ -210,6 +212,8 @@ Feature: WorkingCapitalLoanChargeAdjustmentFeature Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 80.0 | 0.0 | 80.0 | 0.0 | false | + | 10 January 2026 | Accrual | 30.0 | 0.0 | 0.0 | 30.0 | false | | 11 January 2026 | Charge Adjustment | 80.0 | 0.0 | 80.0 | 0.0 | false | | 12 January 2026 | Charge Adjustment | 30.0 | 0.0 | 0.0 | 30.0 | false | And Working Capital Loan has charges with the following data: @@ -451,6 +455,7 @@ Feature: WorkingCapitalLoanChargeAdjustmentFeature | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 10 January 2026 | Charge Adjustment | 500.0 | 0.0 | 500.0 | 0.0 | false | + | 10 January 2026 | Accrual | 500.0 | 0.0 | 500.0 | 0.0 | false | | 10 January 2026 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | false | And Customer makes repayment on "20 January 2026" with 8500.0 transaction amount on Working Capital loan Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" @@ -460,6 +465,7 @@ Feature: WorkingCapitalLoanChargeAdjustmentFeature | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 10 January 2026 | Charge Adjustment | 500.0 | 0.0 | 500.0 | 0.0 | true | + | 10 January 2026 | Accrual | 500.0 | 0.0 | 500.0 | 0.0 | false | | 10 January 2026 | Repayment | 500.0 | 0.0 | 500.0 | 0.0 | false | | 20 January 2026 | Repayment | 8500.0 | 8500.0 | 0.0 | 0.0 | false | And Working Capital loan balance payload contains the following fields: diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanPayoutRefund.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanPayoutRefund.feature index ea4bd0891be..1037ad30cc6 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanPayoutRefund.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanPayoutRefund.feature @@ -118,6 +118,8 @@ Feature: Working Capital Loan Payout Refund | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 10 January 2026 | Payout Refund | 9140.0 | 9000.0 | 15.0 | 25.0 | false | + | 10 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 10 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | And Working Capital loan status will be "OVERPAID" And Working Capital Loan Transactions tab has a "PAYOUT_REFUND" transaction with date "10 January 2026" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -132,6 +134,8 @@ Feature: Working Capital Loan Payout Refund | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 10 January 2026 | Payout Refund | 9140.0 | 9000.0 | 15.0 | 25.0 | true | + | 10 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 10 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | And Working Capital loan status will be "ACTIVE" And Working Capital Loan Transactions tab has a reversed "PAYOUT_REFUND" transaction with date "10 January 2026" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature index 757641d3d51..907209c2589 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature @@ -1,7 +1,14 @@ +@SerialChargeAccrualConfig @WorkingCapital @WorkingCapitalTransactionAllocationFeature Feature: Working Capital Transaction Allocation + # Pin the charge accrual date to due-date so the scenarios do not depend on the global config value left behind by a + # previously executed feature. The transaction assertions below expect the charge accrual to be posted by the COB on + # the charge due date; a leaked submitted-date value would instead accrue at charge-add time and break them. + Background: + Given Global config "charge-accrual-date" value set to "due-date" + @TestRailId:C85412 Scenario: Verify Working Capital Repayment transaction with fee and penalty added with DUE_FEE_PENALTY_PRINCIPAL allocation - UC1 Given Admin sets the business date to "01 January 2026" @@ -224,6 +231,7 @@ Feature: Working Capital Transaction Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 20 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | And Working Capital Loan has charges with the following data: | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | @@ -236,6 +244,7 @@ Feature: Working Capital Transaction Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 10 January 2026 | Repayment | 100.0 | 0.0 | 100.0 | 0.0 | false | | 20 January 2026 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | false | And Working Capital Loan has charges with the following data: @@ -263,6 +272,7 @@ Feature: Working Capital Transaction Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | | 11 January 2026 | Charge Adjustment | 70.0 | 0.0 | 70.0 | 0.0 | false | And Working Capital Loan has charges with the following data: | Charge Name | Due Date | Amount | Currency | isPenalty | Charge Time Type | Charge Calculation Type | Charge Payment mode | @@ -704,4 +714,4 @@ Feature: Working Capital Transaction Allocation | 185 | 05 July 2026 | 50.00 | | 0.00 | | 0.00 | | 0.00 | | 186 | 06 July 2026 | 50.00 | | 0.00 | | 0.00 | | 0.00 | | 187 | 07 July 2026 | 50.00 | | 0.00 | | 0.00 | | 0.00 | - | 188 | 08 July 2026 | 8.00 | | 0.00 | | 0.00 | | 0.00 | + | 188 | 08 July 2026 | 8.00 | | 0.00 | | 0.00 | | 0.00 | \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature index 02fe01f3a48..eb70f6049e2 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapital_COB.feature @@ -16,6 +16,7 @@ Feature: Working Capital COB Job | WC_BREACH_SCHEDULE | 4 | | WC_NEAR_BREACH_EVALUATION | 5 | | WC_DISCOUNT_FEE_AMORTIZATION | 6 | + | WC_CHARGE_ACCRUAL | 7 | Then Admin verifies scheduler job "WC_COB" has display name "Working Capital Loan COB" Then Admin verifies scheduler job "WC_COB" has active status "false" diff --git a/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties b/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties index 8dd3c833704..a0531952e4d 100644 --- a/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties +++ b/fineract-e2e-tests-runner/src/test/resources/junit-platform.properties @@ -28,4 +28,9 @@ cucumber.plugin=pretty cucumber.glue=org.apache.fineract.test cucumber.filter.tags=not @Skip cucumber.execution.exclusive-resources.isolated.read-write=org.junit.platform.engine.support.hierarchical.ExclusiveResource.GLOBAL_KEY +# Serialize every feature that reads or writes the global "charge-accrual-date" configuration. It is a single shared +# tenant-level value, so under parallel execution one feature flipping it to submitted-date can corrupt another feature +# that relies on due-date between its charge-add and COB steps. Features tagged @SerialChargeAccrualConfig acquire this +# resource in read-write mode and therefore never run concurrently with each other. +cucumber.execution.exclusive-resources.SerialChargeAccrualConfig.read-write=charge-accrual-date-config cucumber.execution.execution-mode.feature=same_thread diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualWithDeferredRevenueAmortizationAccountingProcessorForWorkingCapitalLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualWithDeferredRevenueAmortizationAccountingProcessorForWorkingCapitalLoan.java index 5d4e364319a..134044c2d33 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualWithDeferredRevenueAmortizationAccountingProcessorForWorkingCapitalLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualWithDeferredRevenueAmortizationAccountingProcessorForWorkingCapitalLoan.java @@ -95,6 +95,7 @@ public void postJournalEntries(final WorkingCapitalLoan loan, final WorkingCapit case LoanTransactionType.CREDIT_BALANCE_REFUND -> postCreditBalanceRefundJournalEntries(loan, txn); case LoanTransactionType.CHARGE_ADJUSTMENT -> postChargeAdjustmentJournalEntries(loan, txn, principalPortion, feesPortion, penaltiesPortion, overpaymentPortion, isChargedOff); + case LoanTransactionType.ACCRUAL -> postChargeAccrualJournalEntries(loan, txn, feesPortion, penaltiesPortion); default -> { throw new NotImplementedException( "Post Journal Entries is not implemented yet for " + txn.getTypeOf().getCode() + " for Working Capital Loan"); @@ -141,6 +142,19 @@ private void postPayoutRefundJournalEntries(WorkingCapitalLoan loan, WorkingCapi accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.OVERPAYMENT, overpaymentPortion); } + private void postChargeAccrualJournalEntries(final WorkingCapitalLoan loan, final WorkingCapitalLoanTransaction txn, + final BigDecimal feesPortion, final BigDecimal penaltiesPortion) { + final JournalEntryPostingHelper accountPostHelper = new JournalEntryPostingHelper(loan, txn); + + // Debit receivable when the charge becomes accrued. + accountPostHelper.postDebitJournalEntry(CashAccountsForLoan.FEES_RECEIVABLE, feesPortion); + accountPostHelper.postDebitJournalEntry(CashAccountsForLoan.PENALTIES_RECEIVABLE, penaltiesPortion); + + // Credit the corresponding income account. + accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.INCOME_FROM_FEES, feesPortion); + accountPostHelper.postCreditJournalEntry(CashAccountsForLoan.INCOME_FROM_PENALTIES, penaltiesPortion); + } + private void postGoodwillCreditJournalEntries(WorkingCapitalLoan loan, WorkingCapitalLoanTransaction txn, BigDecimal principalPortion, BigDecimal feesPortion, BigDecimal penaltiesPortion, BigDecimal overpaymentPortion) { BigDecimal overpaymentPlusPrincipal = principalPortion.add(overpaymentPortion); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/ChargeAccrualBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/ChargeAccrualBusinessStep.java new file mode 100644 index 00000000000..7c8f5de3cf4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/ChargeAccrualBusinessStep.java @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.cob.workingcapitalloan.businessstep; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanChargeAccrualService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ChargeAccrualBusinessStep extends WorkingCapitalLoanCOBBusinessStep { + + private final WorkingCapitalLoanChargeAccrualService chargeAccrualService; + + @Override + public WorkingCapitalLoan execute(final WorkingCapitalLoan input) { + chargeAccrualService.processDueDateAccruals(input, DateUtils.getBusinessLocalDate()); + return input; + } + + @Override + public String getEnumStyledName() { + return "WC_CHARGE_ACCRUAL"; + } + + @Override + public String getHumanReadableName() { + return "WC Charge Accrual"; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java index 51a03225155..75ff8aac3ec 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransaction.java @@ -202,6 +202,13 @@ public static WorkingCapitalLoanTransaction chargeAdjustment(final WorkingCapita return transaction; } + public static WorkingCapitalLoanTransaction accrual(final WorkingCapitalLoan loan, final ExternalId externalId, final BigDecimal amount, + final LocalDate transactionDate) { + final WorkingCapitalLoanTransaction transaction = new WorkingCapitalLoanTransaction(); + transaction.initialize(loan, LoanTransactionType.ACCRUAL, transactionDate, amount, null, null, externalId); + return transaction; + } + private void initialize(final WorkingCapitalLoan loan, final LoanTransactionType transactionType, final LocalDate transactionDate, final BigDecimal amount, final PaymentDetail paymentDetail, final CodeValue classification, final ExternalId externalId) { this.wcLoan = loan; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java index 7ac71159718..7b33a3b3185 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionAllocation.java @@ -100,4 +100,13 @@ public static WorkingCapitalLoanTransactionAllocation forDiscountFeeAdjustment(f return allocation; } + public static WorkingCapitalLoanTransactionAllocation forChargeAccrual(final WorkingCapitalLoanTransaction transaction, + final BigDecimal amount, final boolean isPenalty) { + final WorkingCapitalLoanTransactionAllocation allocation = new WorkingCapitalLoanTransactionAllocation(); + allocation.wcLoanTransaction = transaction; + allocation.principalPortion = BigDecimal.ZERO; + allocation.feeChargesPortion = isPenalty ? BigDecimal.ZERO : MathUtil.nullToZero(amount); + allocation.penaltyChargesPortion = isPenalty ? MathUtil.nullToZero(amount) : BigDecimal.ZERO; + return allocation; + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java index 63447512031..e0c12d43d89 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java @@ -52,6 +52,22 @@ public WorkingCapitalLoanAllocationRequest build(@NonNull final WorkingCapitalLo getAllocationRule(loan, transactionType).getAllocationTypes(), balance.getPrincipalOutstanding(), chargeBalances); } + /** + * Builds the allocation request for a charge adjustment. Unlike a repayment, an adjustment settles one specific + * charge, so the request is scoped to that single charge and exposes no principal outstanding. This keeps the + * amount on the charge's fee/penalty bucket and prevents the configured order (which ranks DUE_PRINCIPAL ahead of + * the IN_ADVANCE buckets) from diverting a not-yet-due charge onto principal. + */ + public WorkingCapitalLoanAllocationRequest buildForChargeAdjustment(@NonNull final WorkingCapitalLoan loan, + @NonNull final WorkingCapitalLoanCharge adjustedCharge, @NonNull final LocalDate transactionDate, + @NonNull final BigDecimal amount) { + final ChargeBalance chargeBalance = new ChargeBalance(adjustedCharge.getId(), adjustedCharge.getAmountOutstanding(), + adjustedCharge.getDueDate(), adjustedCharge.isPenaltyCharge()); + return new WorkingCapitalLoanAllocationRequest(transactionDate, amount, + getAllocationRule(loan, LoanTransactionType.CHARGE_ADJUSTMENT).getAllocationTypes(), BigDecimal.ZERO, + List.of(chargeBalance)); + } + @NonNull private WorkingCapitalLoanPaymentAllocationRule getAllocationRule(@NonNull final WorkingCapitalLoan loan, @NonNull final LoanTransactionType transactionType) { diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java new file mode 100644 index 00000000000..38dff9e0ae6 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java @@ -0,0 +1,134 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; +import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.accounting.WorkingCapitalLoanAccountingProcessor; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanCharge; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionAllocationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class WorkingCapitalLoanChargeAccrualService { + + private static final String SUBMITTED_DATE = "submitted-date"; + private static final String DUE_DATE = "due-date"; + private static final String DEFAULT_ACCRUAL_DATE_CONFIG = DUE_DATE; + + private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final WorkingCapitalLoanChargeRepository chargeRepository; + private final WorkingCapitalLoanTransactionRepository transactionRepository; + private final WorkingCapitalLoanTransactionAllocationRepository allocationRepository; + private final WorkingCapitalLoanTransactionRelationRepository relationRepository; + private final WorkingCapitalLoanAccountingProcessor accountingProcessor; + + public void processOnChargeAdded(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge charge) { + if (isAccrualPostingDisabled(loan)) { + return; + } + if (!SUBMITTED_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) { + return; + } + createChargeAccrualIfMissing(loan, charge, charge.getSubmittedOnDate()); + } + + public void processDueDateAccruals(final WorkingCapitalLoan loan, final LocalDate businessDate) { + if (isAccrualPostingDisabled(loan)) { + return; + } + if (!DUE_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) { + return; + } + final List activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId()); + activeCharges.stream().filter(charge -> charge.getDueDate() != null && !charge.getDueDate().isAfter(businessDate)) + .forEach(charge -> createChargeAccrualIfMissing(loan, charge, charge.getDueDate())); + } + + /** + * Posts, on early closure, any pending charge accrual that the due-date COB step would not have reached yet because + * the charge due date is still in the future. Once the loan is closed it is no longer picked up by the end-of-day + * job, so the accrual is accelerated to the closing date to make sure the income is recognized. Charges already + * accrued are skipped by the idempotency guard; in submitted-date mode charges are accrued when added, so this is a + * no-op there. + */ + public void processClosureAccruals(final WorkingCapitalLoan loan, final LocalDate closingDate) { + if (isAccrualPostingDisabled(loan)) { + return; + } + if (!DUE_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) { + return; + } + final List activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId()); + activeCharges.forEach(charge -> createChargeAccrualIfMissing(loan, charge, closingDate)); + } + + private boolean isAccrualPostingDisabled(final WorkingCapitalLoan loan) { + return !loan.getLoanProduct().getAccountingRule().isAccrualWithDeferredRevenueAmortization(); + } + + private void createChargeAccrualIfMissing(final WorkingCapitalLoan loan, final WorkingCapitalLoanCharge charge, + final LocalDate accrualDate) { + // The accrual recognizes the full charge income regardless of whether the charge was already paid or adjusted; + // gating on the outstanding amount would skip fully-settled charges and leave income unrecognized (and the + // receivable/income pair un-netted). Waived charges are excluded upstream via the active-charge filter. + if (accrualDate == null || isAlreadyAccrued(charge) || !MathUtil.isGreaterThanZero(charge.getAmount())) { + return; + } + final WorkingCapitalLoanTransaction accrualTransaction = WorkingCapitalLoanTransaction.accrual(loan, ExternalId.empty(), + charge.getAmount(), accrualDate); + final WorkingCapitalLoanTransactionRelation relation = WorkingCapitalLoanTransactionRelation.linkToCharge(accrualTransaction, + charge, LoanTransactionRelationTypeEnum.RELATED); + accrualTransaction.getLoanTransactionRelations().add(relation); + + transactionRepository.saveAndFlush(accrualTransaction); + + final WorkingCapitalLoanTransactionAllocation allocation = WorkingCapitalLoanTransactionAllocation + .forChargeAccrual(accrualTransaction, charge.getAmount(), charge.isPenaltyCharge()); + allocationRepository.saveAndFlush(allocation); + accountingProcessor.postJournalEntries(loan, accrualTransaction, allocation, false); + } + + private boolean isAlreadyAccrued(final WorkingCapitalLoanCharge charge) { + return !relationRepository + .findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType(charge, false, LoanTransactionType.ACCRUAL) + .isEmpty(); + } + + private String retrieveChargeAccrualDateConfig() { + final String configuredValue = globalConfigurationRepository + .findOneByNameWithNotFoundDetection(GlobalConfigurationConstants.CHARGE_ACCRUAL_DATE).getStringValue(); + return configuredValue == null || configuredValue.isBlank() ? DEFAULT_ACCRUAL_DATE_CONFIG : configuredValue; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java index 660026ed8c6..5ae60f84db8 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java @@ -103,6 +103,7 @@ public class WorkingCapitalLoanChargeWritePlatformServiceImpl implements Working private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; private final WorkingCapitalLoanBreachScheduleService breachScheduleService; private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; + private final WorkingCapitalLoanChargeAccrualService chargeAccrualService; @Transactional @Override @@ -140,6 +141,8 @@ public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command changes.put("status", loan.getLoanStatus()); } + chargeAccrualService.processOnChargeAdded(loan, loanCharge); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanCharge.getId()) // diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionProcessor.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionProcessor.java index 47ad649bd3a..efc1c288ed6 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionProcessor.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionProcessor.java @@ -32,6 +32,7 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanLifecycleStateMachine; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelation; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionAllocationRepository; @@ -56,6 +57,7 @@ public class WorkingCapitalLoanTransactionProcessor { private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; private final WorkingCapitalLoanLifecycleStateMachine stateMachine; private final WorkingCapitalLoanDiscountFeeAmortizationService discountFeeAmortizationService; + private final WorkingCapitalLoanChargeAccrualService chargeAccrualService; public WorkingCapitalLoanTransactionAllocation processRepaymentLikeTransaction(final WorkingCapitalLoan loan, final WorkingCapitalLoanTransaction transaction, final LoanTransactionType transactionType, final LocalDate transactionDate, @@ -65,10 +67,22 @@ public WorkingCapitalLoanTransactionAllocation processRepaymentLikeTransaction(f .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); final List charges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loanId); - // Decide the allocation across penalty/fee/principal following the loan's configured payment allocation order - // (principal-only when no order is configured), then materialize it onto the charges and refresh the balance. - final WorkingCapitalLoanAllocationRequest allocationRequest = allocationRequestFactory.build(loan, balance, charges, - transactionDate, transactionAmount, transactionType); + WorkingCapitalLoanAllocationRequest allocationRequest; + if (transactionType == LoanTransactionType.CHARGE_ADJUSTMENT) { + final WorkingCapitalLoanTransactionRelation loanTransactionRelation = transaction.getLoanTransactionRelations().stream() + .filter(e -> e.getToCharge() != null).findFirst().orElseThrow(); + final WorkingCapitalLoanCharge adjustedCharge = loanTransactionRelation.getToCharge(); + // The factory honors a configured CHARGE_ADJUSTMENT allocation order when the product defines one; + // otherwise it scopes the adjustment to its own charge (no principal) so a not-yet-due charge is not + // diverted onto principal. + allocationRequest = allocationRequestFactory.buildForChargeAdjustment(loan, balance, charges, adjustedCharge, transactionDate, + transactionAmount); + } else { + // Decide the allocation across penalty/fee/principal following the loan's configured payment allocation + // order (principal-only when no order is configured), then materialize it onto the charges and refresh the + // balance. + allocationRequest = allocationRequestFactory.build(loan, balance, charges, transactionDate, transactionAmount, transactionType); + } final WorkingCapitalLoanAllocationPlan allocationPlan = allocationProcessor.plan(allocationRequest); final WorkingCapitalLoanTransactionAllocation allocation = allocationApplier.apply(transaction, null, allocationPlan, charges); balanceUpdater.apply(balance, allocationPlan); @@ -101,6 +115,9 @@ public WorkingCapitalLoanTransactionAllocation processRepaymentLikeTransaction(f stateMachine.determineAndTransition(loan, transactionDate, charges); triggerInlineAmortizationIfLoanClosed(loan, transactionDate); + // On early closure the loan leaves the COB scope, so any charge whose due-date accrual has not been posted yet + // is accrued as of the closing date to make sure the income is recognized before the loan is closed. + chargeAccrualService.accrueOnClosure(loan, transactionDate); return allocation; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 3d3a3185e8f..28068405a3b 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -33,7 +33,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; -import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; @@ -75,7 +74,6 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; -import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanPeriodPaymentRateChangeRepository; @@ -109,15 +107,13 @@ public class WorkingCapitalLoanWritePlatformServiceImpl implements WorkingCapita private final WorkingCapitalLoanAccountingProcessor accountingProcessor; private final WorkingCapitalLoanTransactionRelationRepository relationRepository; private final WorkingCapitalLoanPeriodPaymentRateChangeRepository rateChangeRepository; - private final WorkingCapitalLoanBreachScheduleRepository breachScheduleRepository; private final WorkingCapitalLoanDiscountFeeAmortizationService discountFeeAmortizationService; private final WorkingCapitalLoanTransactionReprocessingService transactionReprocessingService; private final WorkingCapitalLoanChargeRepository chargeRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleService delinquencyRangeScheduleService; - private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; - private final WorkingCapitalLoanDelinquencyClassificationService delinquencyClassificationService; private final WorkingCapitalLoanBreachScheduleService breachScheduleService; private final WorkingCapitalLoanTransactionProcessor transactionProcessor; + private final WorkingCapitalLoanChargeAccrualService chargeAccrualService; @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { @@ -594,6 +590,8 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina final List activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId()); stateMachine.determineAndTransition(loan, transactionDate, activeCharges); transactionProcessor.triggerInlineAmortizationIfLoanClosed(loan, transactionDate); + // A discount-fee adjustment can pay down principal and close the loan, so accrue any pending charge income. + chargeAccrualService.accrueOnClosure(loan, transactionDate); changes.put("status", loan.getLoanStatus()); loanRepository.save(loan); @@ -811,6 +809,8 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json } } + // Closing an overpaid loan via a full credit balance refund must still recognize any pending charge accrual. + chargeAccrualService.accrueOnClosure(loan, transactionDate); changes.put("status", loan.getLoanStatus()); handleNote(loan, command, changes); @@ -1038,10 +1038,17 @@ private WorkingCapitalLoanTransaction reverseDisbursementTransactionAndResetBala } final WorkingCapitalLoanTransaction txn = activeDisbursements.getFirst(); + final List accrualsToReverse = transactions.stream() + .filter(t -> t.getTypeOf() == LoanTransactionType.ACCRUAL && !t.isReversed()).toList(); + transactions.forEach(this::markReversed); this.transactionRepository.saveAll(transactions); this.transactionRepository.flush(); + // Reverse the journal entries of any charge accrual so the recognized income/receivable is backed out with the + // disbursement; marking the transaction reversed alone would leave the GL postings in place. + accrualsToReverse.forEach(accrual -> accountingProcessor.postReversalJournalEntries(loan, accrual)); + // Operate on loan.getBalance() directly: it is the single managed balance instance that // recalculateRealizedIncome writes to, so all updates here apply to the same object that gets persisted. final WorkingCapitalLoanBalance balance = loan.getBalance(); @@ -1068,7 +1075,8 @@ private void ensureUndoDisbursalAllowed(final WorkingCapitalLoan loan) { if (txn.getTypeOf() != LoanTransactionType.DISBURSEMENT && txn.getTypeOf() != LoanTransactionType.DISCOUNT_FEE && txn.getTypeOf() != LoanTransactionType.DISCOUNT_FEE_ADJUSTMENT && txn.getTypeOf() != LoanTransactionType.DISCOUNT_FEE_AMORTIZATION - && txn.getTypeOf() != LoanTransactionType.DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT) { + && txn.getTypeOf() != LoanTransactionType.DISCOUNT_FEE_AMORTIZATION_ADJUSTMENT + && txn.getTypeOf() != LoanTransactionType.ACCRUAL) { throw new PlatformApiDataValidationException("validation.msg.wc.loan.undo.disbursal.not.allowed", "Undo disbursal is not allowed when there are other monetary transactions on the loan", "loanId"); } diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 9bc0fba6d32..81301961016 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -76,4 +76,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0055_wc_loan_charge_accrual_business_step.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0055_wc_loan_charge_accrual_business_step.xml new file mode 100644 index 00000000000..ad6402299d9 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0055_wc_loan_charge_accrual_business_step.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + step_name='WC_CHARGE_ACCRUAL' + + + + + From c416ff8f5e9590267c99dd4973643c1374905306 Mon Sep 17 00:00:00 2001 From: Rustam Zeinalov Date: Fri, 17 Jul 2026 13:21:23 +0200 Subject: [PATCH 2/2] FINERACT-2455: WC charge accrual - restore e2e coverage, TestRail IDs and expectation fixes after rebase Restores the QA test layer dropped by the PR rebase (was 293586f7bd): - TestRailIds C85589-C85603 on all charge accrual scenarios - scenario hardening: pre-due-date boundary COB, idempotency re-run, closure/repayment/reversal journal entry assertions, re-disbursement re-accrual cycle - adjustment-after-accrual (submitted-date) and NONE-accounting negative scenarios - goodwill/allocation expectation fixes for closure accruals (COB runs with business date - 1; closure accruals post after the closing transaction) - After-hook restoring charge-accrual-date to due-date even when a scenario fails mid-way --- .../common/GlobalConfigurationStepDef.java | 7 + .../features/LoanAccrualTransaction.feature | 20 ++- ...talGoodwillCreditPaymentAllocation.feature | 32 +--- .../WorkingCapitalLoanChargeAccrual.feature | 164 +++++++++++++++++- ...orkingCapitalTransactionAllocation.feature | 6 + ...ngCapitalLoanAllocationRequestFactory.java | 25 ++- ...orkingCapitalLoanChargeAccrualService.java | 29 +++- 7 files changed, 243 insertions(+), 40 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java index 3f2b38a68a8..6fe8d10d47c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/common/GlobalConfigurationStepDef.java @@ -21,6 +21,7 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import io.cucumber.java.After; import io.cucumber.java.en.Given; import io.cucumber.java.en.When; import java.util.Collections; @@ -42,6 +43,12 @@ public class GlobalConfigurationStepDef { @Autowired private FineractFeignClient fineractClient; + // restore the default even when a scenario fails mid-way, so the submitted-date setting cannot leak + @After("@WorkingCapitalLoanChargeAccrualFeature") + public void restoreChargeAccrualDateConfig() { + globalConfigurationHelper.setGlobalConfigValueString("charge-accrual-date", "due-date"); + } + @Given("Global configuration {string} is disabled") public void disableGlobalConfiguration(String configKey) { globalConfigurationHelper.disableGlobalConfiguration(configKey, 0L); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index 63d0a9038cb..eb9a08c4268 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -1993,4 +1993,22 @@ Feature: LoanAccrualTransaction | 17 November 2025 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | | 18 November 2025 | Accrual Activity | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | Then LoanAccrualTransactionCreatedBusinessEvent is raised on "17 November 2025" - Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "18 November 2025" \ No newline at end of file + Then LoanTransactionAccrualActivityPostBusinessEvent is raised on "18 November 2025" + + @TestRailId:C85643 + Scenario: Core parity - accrual is posted on early loan closure even when charge-accrual-date is submitted-date + When Global config "charge-accrual-date" value set to "due-date" + When Admin sets the business date to "01 June 2023" + When Admin creates a client with random data + When Admin creates a new default Loan with date: "01 June 2023" + And Admin successfully approves the loan on "01 June 2023" with "1000" amount and expected disbursement date on "01 June 2023" + When Admin successfully disburse the loan on "01 June 2023" with "1000" EUR transaction amount + When Admin adds "LOAN_SNOOZE_FEE" due date charge with "20 June 2023" due date and 10 EUR transaction amount + When Global config "charge-accrual-date" value set to "submitted-date" + When Admin sets the business date to "05 June 2023" + And Customer makes "AUTOPAY" repayment on "05 June 2023" with 1010 EUR transaction amount + Then Loan status will be "CLOSED_OBLIGATIONS_MET" + Then Loan Transactions tab has a transaction with date: "05 June 2023", and with the following data: + | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | + | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 | + When Global config "charge-accrual-date" value set to "due-date" diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature index 7a5424350c0..3e8b3e8afb6 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalGoodwillCreditPaymentAllocation.feature @@ -35,8 +35,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 230.0 | 15.0 | 25.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -68,8 +66,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 30.0 | 0.0 | 5.0 | 25.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -101,8 +97,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -134,8 +128,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 255.0 | 15.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -196,9 +188,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | @TestRailId:C85517 Scenario: Verify Working Capital Goodwill Credit transaction that overpays loan with following CBR trn and with fee and penalty added with DUE_PRINCIPAL_FEE_PENALTY allocation - UC7 @@ -226,9 +218,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | # --- make CBR trn to refund overpaid amount --- # And Customer makes credit balance refund on "12 January 2026" with 160.0 transaction amount on Working Capital loan And Working capital loan account has the correct data: @@ -237,9 +229,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @TestRailId:C85518 @@ -785,8 +777,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | When Admin sets the business date to "15 March 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -808,8 +798,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | And Working Capital Loan charge balances has the following data: | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | @@ -819,8 +807,6 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | - | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | - | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | And Working Capital Loan charge balances has the following data: @@ -852,9 +838,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9040.0 | 9000.0 | 15.0 | 25.0 | false | @TestRailId:C85525 Scenario: Verify Working Capital Goodwill Credit paid in excess follows the GOODWILL_CREDIT specific payment allocation rule with following CBR transaction - UC15 @@ -878,9 +864,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | # --- make CBR transaction to refund overpaid amount --- # And Customer makes credit balance refund on "12 January 2026" with 160.0 transaction amount on Working Capital loan And Working capital loan account has the correct data: @@ -889,9 +875,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 9200.0 | 9000.0 | 15.0 | 25.0 | false | | 12 January 2026 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @TestRailId:C85526 @@ -927,9 +913,9 @@ Feature: Working Capital Goodwill Credit Payment Allocation And Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | | 13 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | When Admin sets the business date to "15 January 2026" And Admin runs inline COB job for Working Capital Loan by loanId @@ -990,18 +976,18 @@ Feature: Working Capital Goodwill Credit Payment Allocation Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | # --- undo the goodwill credit: the repayment is replayed with its own REPAYMENT (DUE_FEE first) rule and keeps its portions --- # When Customer undo "1"th "GOODWILL_CREDIT" transaction made on "12 January 2026" on Working Capital loan Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | true | | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | - | 12 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | true | | 15 January 2026 | Repayment | 40.0 | 0.0 | 15.0 | 25.0 | false | And Working capital loan account has the correct data: | principal | totalPaidPrincipal | totalPaymentVolume | realizedIncome | unrealizedIncome | overpaymentAmount | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature index fb46ced50f4..22fa5edc83b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature @@ -3,6 +3,7 @@ @WorkingCapitalLoanChargeAccrualFeature Feature: Working Capital Loan Charge Accrual + @TestRailId:C85589 Scenario: Verify Working Capital fee charge accrual is posted on charge due date when charge-accrual-date is due-date Given Admin sets the business date to "01 January 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -14,6 +15,11 @@ Feature: Working Capital Loan Charge Accrual And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2026" due date and 50.0 transaction amount And Admin sets the business date to "14 January 2026" And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + When Admin sets the business date to "15 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital Loan has transactions: | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | @@ -27,9 +33,21 @@ Feature: Working Capital Loan Charge Accrual | Type | Account code | Account name | Debit | Credit | | ASSET | 112603 | Interest/Fee Receivable | 50.0 | | | INCOME | 404007 | Fee Income | | 50.0 | + When Admin sets the business date to "17 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 15 January 2026 | Accrual | 50.0 | 0.0 | 50.0 | 0.0 | false | And Admin sets the business date to "20 January 2026" Then Admin closes the Working Capital loan with a full repayment on "20 January 2026" + And Working Capital Loan Transactions tab has a "REPAYMENT" transaction with date "20 January 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | LIABILITY | 145023 | Suspense/Clearing account | 9050.0 | | + | ASSET | 112601 | Loans Receivable | | 9000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 50.0 | + @TestRailId:C85590 Scenario: Verify Working Capital penalty charge accrual is posted in real time when charge-accrual-date is submitted-date Given Admin sets the business date to "01 February 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -57,6 +75,7 @@ Feature: Working Capital Loan Charge Accrual When Global config "charge-accrual-date" value set to "due-date" Then Admin closes the Working Capital loan with a full repayment on "20 February 2026" + @TestRailId:C85591 Scenario: Verify Working Capital fee and penalty charge accrual is posted in real time when charge-accrual-date is submitted-date Given Admin sets the business date to "01 March 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -82,6 +101,7 @@ Feature: Working Capital Loan Charge Accrual When Global config "charge-accrual-date" value set to "due-date" Then Admin closes the Working Capital loan with a full repayment on "25 March 2026" + @TestRailId:C85592 Scenario: Verify Working Capital fee charge accrual is posted in real time when charge-accrual-date is submitted-date Given Admin sets the business date to "01 April 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -109,6 +129,7 @@ Feature: Working Capital Loan Charge Accrual When Global config "charge-accrual-date" value set to "due-date" Then Admin closes the Working Capital loan with a full repayment on "25 April 2026" + @TestRailId:C85593 Scenario: Verify Working Capital penalty charge accrual is posted on charge due date when charge-accrual-date is due-date Given Admin sets the business date to "01 May 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -136,6 +157,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 May 2026" Then Admin closes the Working Capital loan with a full repayment on "20 May 2026" + @TestRailId:C85594 Scenario: Verify Working Capital fee and penalty charge accrual is posted on charge due dates when charge-accrual-date is due-date Given Admin sets the business date to "01 June 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -169,6 +191,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "25 June 2026" Then Admin closes the Working Capital loan with a full repayment on "25 June 2026" + @TestRailId:C85595 Scenario: Verify Working Capital fee charge adjustment before accrual when charge-accrual-date is due-date Given Admin sets the business date to "01 July 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -204,6 +227,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 July 2026" Then Admin closes the Working Capital loan with a full repayment on "20 July 2026" + @TestRailId:C85596 Scenario: Verify Working Capital penalty charge adjustment before accrual when charge-accrual-date is due-date Given Admin sets the business date to "01 August 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -239,6 +263,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 August 2026" Then Admin closes the Working Capital loan with a full repayment on "20 August 2026" + @TestRailId:C85597 Scenario: Verify Working Capital partial fee charge adjustment before accrual when charge-accrual-date is due-date Given Admin sets the business date to "01 November 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -274,6 +299,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 November 2026" Then Admin closes the Working Capital loan with a full repayment on "20 November 2026" + @TestRailId:C85598 Scenario: Verify Working Capital partial penalty charge adjustment before accrual when charge-accrual-date is due-date Given Admin sets the business date to "01 December 2026" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -309,6 +335,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 December 2026" Then Admin closes the Working Capital loan with a full repayment on "20 December 2026" + @TestRailId:C85599 Scenario: Verify Working Capital fee charge accrual is still posted on due date when the charge was fully repaid before the accrual runs Given Admin sets the business date to "01 January 2027" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -327,6 +354,10 @@ Feature: Working Capital Loan Charge Accrual And Working Capital Loan charge balances has the following data: | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + And Working Capital Loan Transactions tab has a "REPAYMENT" transaction with date "12 January 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | LIABILITY | 145023 | Suspense/Clearing account | 100.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 100.0 | When Admin sets the business date to "13 January 2027" And Admin runs inline COB job for Working Capital Loan by loanId Then Working Capital Loan has transactions: @@ -341,6 +372,7 @@ Feature: Working Capital Loan Charge Accrual And Admin sets the business date to "20 January 2027" Then Admin closes the Working Capital loan with a full repayment on "20 January 2027" + @TestRailId:C85600 Scenario: Verify Working Capital fee charge accrual is posted on the closing date when the loan is closed before the charge due date Given Admin sets the business date to "01 February 2027" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -361,7 +393,13 @@ Feature: Working Capital Loan Charge Accrual | Type | Account code | Account name | Debit | Credit | | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | | INCOME | 404007 | Fee Income | | 100.0 | + And Working Capital Loan Transactions tab has a "REPAYMENT" transaction with date "15 February 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | LIABILITY | 145023 | Suspense/Clearing account | 9100.0 | | + | ASSET | 112601 | Loans Receivable | | 9000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 100.0 | + @TestRailId:C85601 Scenario: Verify Working Capital charge accrual is reversed when the disbursal is undone Given Admin sets the business date to "01 March 2027" And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: @@ -383,4 +421,128 @@ Feature: Working Capital Loan Charge Accrual | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 March 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | true | | 10 March 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | true | - When Global config "charge-accrual-date" value set to "due-date" \ No newline at end of file + And Working Capital Loan Transactions tab has a reversed "ACCRUAL" transaction with date "10 March 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 100.0 | + | INCOME | 404007 | Fee Income | 100.0 | | + When Global config "charge-accrual-date" value set to "due-date" + And Admin successfully disburse the Working Capital loan on "10 March 2027" with "9000" EUR transaction amount + And Admin sets the business date to "21 March 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 March 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | true | + | 10 March 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | true | + | 10 March 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 20 March 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "20 March 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + And Admin sets the business date to "25 March 2027" + Then Admin closes the Working Capital loan with a full repayment on "25 March 2027" + + @TestRailId:C85602 + Scenario: Verify Working Capital fee charge adjustment after accrual when charge-accrual-date is submitted-date + Given Admin sets the business date to "01 September 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 September 2026 | 01 September 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "10 September 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 September 2026" due date and 100.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 September 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 September 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + When Admin makes a charge adjustment for the last added charge with 100.0 amount on working capital loan + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 September 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 September 2026 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + | 10 September 2026 | Charge Adjustment | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "10 September 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + And Working Capital Loan Transactions tab has a "CHARGE_ADJUSTMENT" transaction with date "10 September 2026" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | INCOME | 404007 | Fee Income | 100.0 | | + | ASSET | 112603 | Interest/Fee Receivable | | 100.0 | + And Admin sets the business date to "25 September 2026" + When Global config "charge-accrual-date" value set to "due-date" + Then Admin closes the Working Capital loan with a full repayment on "25 September 2026" + + @TestRailId:C85603 + Scenario: Verify no accrual is posted for Working Capital loan charges when product accounting rule is NONE + Given Admin sets the business date to "01 October 2026" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 October 2026 | 01 October 2026 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 October 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 October 2026" due date and 50.0 transaction amount + When Admin sets the business date to "16 October 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 October 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_PENALTY" specified due date charge to working capital loan with "20 October 2026" due date and 30.0 transaction amount + Then Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 October 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + And Working Capital Loan charge balances has the following data: + | Fee Amount | Fee Outstanding | Fee Paid | Penalty Amount | Penalty Outstanding | Penalty Paid | + | 50.0 | 50.0 | 0.0 | 30.0 | 30.0 | 0.0 | + And Admin sets the business date to "20 October 2026" + When Global config "charge-accrual-date" value set to "due-date" + Then Admin closes the Working Capital loan with a full repayment on "20 October 2026" + + @TestRailId:C85643 + Scenario: Verify pending charge accrual is posted at closure even when charge-accrual-date is submitted-date + Given Admin sets the business date to "01 November 2027" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 November 2027 | 01 November 2027 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "10 November 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 November 2027" due date and 100.0 transaction amount + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "15 November 2027" + Then Admin closes the Working Capital loan with a full repayment on "15 November 2027" + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 November 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 15 November 2027 | Repayment | 9100.0 | 9000.0 | 100.0 | 0.0 | false | + | 15 November 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + And Working Capital Loan Transactions tab has a "ACCRUAL" transaction with date "15 November 2027" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112603 | Interest/Fee Receivable | 100.0 | | + | INCOME | 404007 | Fee Income | | 100.0 | + + @TestRailId:C85643 + Scenario: Verify closure does not duplicate the accrual when the charge was already accrued in real time under submitted-date + Given Admin sets the business date to "01 December 2027" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 December 2027 | 01 December 2027 | 9000 | 100000 | 18 | 0 | + When Global config "charge-accrual-date" value set to "submitted-date" + And Admin sets the business date to "05 December 2027" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 December 2027" due date and 100.0 transaction amount + And Admin sets the business date to "10 December 2027" + Then Admin closes the Working Capital loan with a full repayment on "10 December 2027" + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 December 2027 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 05 December 2027 | Accrual | 100.0 | 0.0 | 100.0 | 0.0 | false | + | 10 December 2027 | Repayment | 9100.0 | 9000.0 | 100.0 | 0.0 | false | diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature index 907209c2589..c952e2d76df 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature @@ -174,6 +174,8 @@ Feature: Working Capital Transaction Allocation | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 12 January 2026 | Repayment | 9040.0 | 9000.0 | 15.0 | 25.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | @TestRailId:C85418 Scenario: Verify Working Capital Repayment transaction that overpays loon with following CBR trn and with fee and penalty added with DUE_PRINCIPAL_FEE_PENALTY allocation - UC7 @@ -202,6 +204,8 @@ Feature: Working Capital Transaction Allocation | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 12 January 2026 | Repayment | 9200.0 | 9000.0 | 15.0 | 25.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | # --- make CBR trn to refund overpaid amount --- # And Customer makes credit balance refund on "12 January 2026" with 160.0 transaction amount on Working Capital loan And Working capital loan account has the correct data: @@ -211,6 +215,8 @@ Feature: Working Capital Transaction Allocation | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | | 12 January 2026 | Repayment | 9200.0 | 9000.0 | 15.0 | 25.0 | false | + | 12 January 2026 | Accrual | 15.0 | 0.0 | 15.0 | 0.0 | false | + | 12 January 2026 | Accrual | 25.0 | 0.0 | 0.0 | 25.0 | false | | 12 January 2026 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @TestRailId:C85419 diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java index e0c12d43d89..07002d14428 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAllocationRequestFactory.java @@ -53,19 +53,30 @@ public WorkingCapitalLoanAllocationRequest build(@NonNull final WorkingCapitalLo } /** - * Builds the allocation request for a charge adjustment. Unlike a repayment, an adjustment settles one specific - * charge, so the request is scoped to that single charge and exposes no principal outstanding. This keeps the - * amount on the charge's fee/penalty bucket and prevents the configured order (which ranks DUE_PRINCIPAL ahead of - * the IN_ADVANCE buckets) from diverting a not-yet-due charge onto principal. + * Builds the allocation request for a charge adjustment. When the product configures its own CHARGE_ADJUSTMENT + * allocation order, that order is honored and the adjustment spreads across charges/principal like a repayment. + * Otherwise an adjustment settles only its own charge: the request is scoped to that single charge with no + * principal outstanding, which keeps the amount on the charge's fee/penalty bucket and prevents the fallback + * (default) order - which ranks DUE_PRINCIPAL ahead of the IN_ADVANCE buckets - from diverting a not-yet-due charge + * onto principal. */ public WorkingCapitalLoanAllocationRequest buildForChargeAdjustment(@NonNull final WorkingCapitalLoan loan, + @NonNull final WorkingCapitalLoanBalance balance, @NonNull final List charges, @NonNull final WorkingCapitalLoanCharge adjustedCharge, @NonNull final LocalDate transactionDate, @NonNull final BigDecimal amount) { + if (hasConfiguredAllocationRule(loan, LoanTransactionType.CHARGE_ADJUSTMENT)) { + return build(loan, balance, charges, transactionDate, amount, LoanTransactionType.CHARGE_ADJUSTMENT); + } final ChargeBalance chargeBalance = new ChargeBalance(adjustedCharge.getId(), adjustedCharge.getAmountOutstanding(), adjustedCharge.getDueDate(), adjustedCharge.isPenaltyCharge()); - return new WorkingCapitalLoanAllocationRequest(transactionDate, amount, - getAllocationRule(loan, LoanTransactionType.CHARGE_ADJUSTMENT).getAllocationTypes(), BigDecimal.ZERO, - List.of(chargeBalance)); + return new WorkingCapitalLoanAllocationRequest(transactionDate, amount, getDefaultAllocationRule(loan).getAllocationTypes(), + BigDecimal.ZERO, List.of(chargeBalance)); + } + + private boolean hasConfiguredAllocationRule(@NonNull final WorkingCapitalLoan loan, + @NonNull final LoanTransactionType transactionType) { + return loan.getPaymentAllocationRules().stream() + .anyMatch(rule -> transactionType.equals(rule.getTransactionType().getLoanTransactionType())); } @NonNull diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java index 38dff9e0ae6..de107978bc8 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java @@ -77,17 +77,30 @@ public void processDueDateAccruals(final WorkingCapitalLoan loan, final LocalDat } /** - * Posts, on early closure, any pending charge accrual that the due-date COB step would not have reached yet because - * the charge due date is still in the future. Once the loan is closed it is no longer picked up by the end-of-day - * job, so the accrual is accelerated to the closing date to make sure the income is recognized. Charges already - * accrued are skipped by the idempotency guard; in submitted-date mode charges are accrued when added, so this is a - * no-op there. + * Accrues any pending charge income once the loan has reached a closed or overpaid state, no matter which operation + * closed it (repayment, goodwill credit, charge adjustment, credit balance refund, discount-fee adjustment). It is + * a no-op while the loan is still active, so callers can invoke it unconditionally after any balance-changing + * operation. Mirrors how the term/progressive loan reacts to loan-closure events. */ - public void processClosureAccruals(final WorkingCapitalLoan loan, final LocalDate closingDate) { - if (isAccrualPostingDisabled(loan)) { + public void accrueOnClosure(final WorkingCapitalLoan loan, final LocalDate closingDate) { + if (!loan.getLoanStatus().isClosed() && !loan.getLoanStatus().isOverpaid()) { return; } - if (!DUE_DATE.equalsIgnoreCase(retrieveChargeAccrualDateConfig())) { + processClosureAccruals(loan, closingDate); + } + + /** + * Posts, on early closure, any pending charge accrual that has not been recognized yet. Once the loan is closed it + * is no longer picked up by the end-of-day job, so the accrual is accelerated to the closing date to make sure the + * income is recognized. This runs regardless of the {@code charge-accrual-date} mode: the idempotency guard skips + * charges already accrued (the common case in submitted-date mode, where charges are accrued when added), while + * charges that slipped through every accrual step are caught here. That gap is real when the mode changes between + * the charge being added and the loan closing: a charge added under due-date with a future due date is never + * accrued on add, never reached by the due-date COB step, and would be missed at closure if this were gated on the + * mode. + */ + public void processClosureAccruals(final WorkingCapitalLoan loan, final LocalDate closingDate) { + if (isAccrualPostingDisabled(loan)) { return; } final List activeCharges = chargeRepository.findByLoanIdAndActiveTrueOrderByDueDateAscIdAsc(loan.getId());