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/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..eb9a08c4268 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 @@ -1993,3 +1994,21 @@ Feature: LoanAccrualTransaction | 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" + + @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/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..3e8b3e8afb6 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" @@ -182,6 +189,8 @@ 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 | | 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 | @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 @@ -210,6 +219,8 @@ 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 | | 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 | # --- 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: @@ -219,6 +230,8 @@ 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 | | 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 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @TestRailId:C85518 @@ -239,6 +252,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 +265,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 +297,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: @@ -823,6 +839,8 @@ 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 | | 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 | @TestRailId:C85525 Scenario: Verify Working Capital Goodwill Credit paid in excess follows the GOODWILL_CREDIT specific payment allocation rule with following CBR transaction - UC15 @@ -847,6 +865,8 @@ 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 | | 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 | # --- 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: @@ -856,6 +876,8 @@ 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 | | 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 | Credit Balance Refund | 160.0 | 160.0 | 0.0 | 0.0 | false | @TestRailId:C85526 @@ -892,6 +914,8 @@ 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 | | 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 | | 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 @@ -913,12 +937,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: @@ -951,6 +977,8 @@ 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 | | 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 | | 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 @@ -958,6 +986,8 @@ 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 | | 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 | | 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 | @@ -991,3 +1021,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..22fa5edc83b --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanChargeAccrual.feature @@ -0,0 +1,548 @@ +@SerialChargeAccrualConfig +@WorkingCapital +@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: + | 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 "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 | + 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 | + 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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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" + + @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: + | 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 | + 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: + | 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" + + @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: + | 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 | + 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: + | 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 | + 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/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/WorkingCapitalTransactionAllocation.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalTransactionAllocation.feature index 757641d3d51..c952e2d76df 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" @@ -167,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 @@ -195,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: @@ -204,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 @@ -224,6 +237,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 +250,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 +278,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 +720,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..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 @@ -52,6 +52,33 @@ public WorkingCapitalLoanAllocationRequest build(@NonNull final WorkingCapitalLo getAllocationRule(loan, transactionType).getAllocationTypes(), balance.getPrincipalOutstanding(), chargeBalances); } + /** + * 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, 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 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..de107978bc8 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeAccrualService.java @@ -0,0 +1,147 @@ +/** + * 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())); + } + + /** + * 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 accrueOnClosure(final WorkingCapitalLoan loan, final LocalDate closingDate) { + if (!loan.getLoanStatus().isClosed() && !loan.getLoanStatus().isOverpaid()) { + return; + } + 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()); + 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..91d708cbce3 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()) // @@ -271,8 +274,8 @@ public CommandProcessingResult adjustmentForLoanCharge(final Long loanId, final adjustmentTx.getLoanTransactionRelations().add(relation); transactionRepository.saveAndFlush(adjustmentTx); - final WorkingCapitalLoanTransactionAllocation allocation = transactionProcessor.processRepaymentLikeTransaction(loan, adjustmentTx, - LoanTransactionType.CHARGE_ADJUSTMENT, transactionDate, amount); + final WorkingCapitalLoanTransactionAllocation allocation = transactionProcessor.processChargeAdjustment(loan, adjustmentTx, + wcCharge, transactionDate, amount); if (loan.getLoanProduct().getAccountingRule().isAccrualWithDeferredRevenueAmortization()) { accountingProcessor.postJournalEntries(loan, adjustmentTx, allocation, false); 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..d0d8a798a79 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 @@ -23,7 +23,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanAllocationPlan; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanAllocationRequest; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; @@ -56,19 +55,21 @@ 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, + public WorkingCapitalLoanTransactionAllocation processChargeAdjustment(final WorkingCapitalLoan loan, + final WorkingCapitalLoanTransaction transaction, final WorkingCapitalLoanCharge adjustedCharge, final LocalDate transactionDate, final BigDecimal transactionAmount) { final Long loanId = loan.getId(); final WorkingCapitalLoanBalance balance = balanceRepository.findByWcLoan_Id(loanId) .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); + // 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. + final WorkingCapitalLoanAllocationRequest allocationRequest = allocationRequestFactory.buildForChargeAdjustment(loan, balance, + charges, adjustedCharge, transactionDate, transactionAmount); final WorkingCapitalLoanAllocationPlan allocationPlan = allocationProcessor.plan(allocationRequest); final WorkingCapitalLoanTransactionAllocation allocation = allocationApplier.apply(transaction, null, allocationPlan, charges); balanceUpdater.apply(balance, allocationPlan); @@ -101,6 +102,8 @@ public WorkingCapitalLoanTransactionAllocation processRepaymentLikeTransaction(f stateMachine.determineAndTransition(loan, transactionDate, charges); triggerInlineAmortizationIfLoanClosed(loan, transactionDate); + // A charge adjustment that pays down principal can close the loan, so accrue any pending charge income. + 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..a0a421d2706 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; @@ -61,6 +60,8 @@ import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; import org.apache.fineract.portfolio.workingcapitalloan.accounting.WorkingCapitalLoanAccountingProcessor; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanAllocationPlan; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanAllocationRequest; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBalance; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanCharge; @@ -75,7 +76,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 +109,17 @@ 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 WorkingCapitalLoanAllocationRequestFactory allocationRequestFactory; + private final WorkingCapitalLoanAllocationApplier allocationApplier; + private final WorkingCapitalLoanBalanceUpdater balanceUpdater; + private final WorkingCapitalLoanChargeAccrualService chargeAccrualService; + private final WorkingCapitalLoanPaymentAllocationProcessor allocationProcessor; @Override public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) { @@ -594,6 +596,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); @@ -691,9 +695,46 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, transactionDate, classification, txnExternalId); this.transactionRepository.saveAndFlush(transaction); - final WorkingCapitalLoanTransactionAllocation allocation = transactionProcessor.processRepaymentLikeTransaction(loan, transaction, - transactionType, transactionDate, transactionAmount); + final WorkingCapitalLoanBalance balance = this.balanceRepository.findByWcLoan_Id(loan.getId()) + .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); + final List charges = this.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); + final WorkingCapitalLoanAllocationPlan allocationPlan = allocationProcessor.plan(allocationRequest); + final WorkingCapitalLoanTransactionAllocation allocation = allocationApplier.apply(transaction, null, allocationPlan, charges); + balanceUpdater.apply(balance, allocationPlan); + this.chargeRepository.saveAll(charges); + this.balanceRepository.saveAndFlush(balance); + this.allocationRepository.saveAndFlush(allocation); + // A backdated transaction can change how the other transactions allocate across charges, so it triggers + // reprocessing. When the loan has charges, reprocessing rebuilds the amortization schedule from scratch, so + // the incremental apply below would be immediately overwritten and is skipped. For a charge-free loan + // reprocessing is a no-op (principal-only allocation is order-independent), so the incremental apply stands. + final List allTransactions = this.transactionRepository + .findByWcLoan_IdOrderByTransactionDateAscIdAsc(loanId); + final boolean backdated = isBackdatedTransaction(allTransactions, transaction); + final boolean reprocessingWillRebuildSchedule = backdated && !charges.isEmpty(); + if (!reprocessingWillRebuildSchedule) { + // The amortization model records the principal on its actual day and recalculates forward. + amortizationScheduleWriteService.applyRepayment(loan, transactionDate, allocationPlan.principalPortion()); + } + if (backdated) { + transactionReprocessingService.reprocessTransactions(loan, allTransactions); + delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); + } else { + delinquencyRangeScheduleService.applyRepayment(loan, transactionDate, transactionAmount); + } + // Breach schedule is maintained incrementally here; reprocessing does not rebuild it. + breachScheduleService.applyRepayment(loanId, transactionDate, transactionAmount); + 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); changes.put("status", loan.getLoanStatus()); handleNote(loan, command, changes); @@ -748,6 +789,20 @@ private void handleNote(WorkingCapitalLoan loan, JsonCommand command, Map allTransactions, + final WorkingCapitalLoanTransaction newTxn) { + // The same-date ID comparison is defensive only: the just-persisted transaction holds the highest + // ID, so in practice only a strictly later transaction date marks the new one as backdated. + return allTransactions.stream().filter(txn -> !txn.isReversed() && !txn.getId().equals(newTxn.getId())) + .anyMatch(txn -> txn.getTransactionDate().isAfter(newTxn.getTransactionDate()) + || (txn.getTransactionDate().equals(newTxn.getTransactionDate()) && txn.getId().compareTo(newTxn.getId()) > 0)); + } + private void markReversed(final WorkingCapitalLoanTransaction txn) { txn.setReversed(true); txn.setReversedOnDate(DateUtils.getBusinessLocalDate()); @@ -1038,10 +1104,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 +1141,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 3960d225792..d9a7494f5fe 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 @@ -73,4 +73,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0052_wc_loan_charge_accrual_business_step.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0052_wc_loan_charge_accrual_business_step.xml new file mode 100644 index 00000000000..a38024feca3 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0052_wc_loan_charge_accrual_business_step.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + step_name='WC_CHARGE_ACCRUAL' + + + + +