diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 524d42a8bd5..3907e7cc48a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -910,6 +910,7 @@ public CommandWrapperBuilder repaymentWorkingCapitalLoanTransaction(final Long l this.actionName = ACTION_REPAYMENT; this.entityName = ENTITY_WORKINGCAPITALLOAN; this.entityId = loanId; + this.loanId = loanId; this.href = "/working-capital-loans/" + loanId + "/transactions?command=repayment"; return this; } @@ -1249,6 +1250,15 @@ public CommandWrapperBuilder loanGoodwillCreditTransaction(final Long loanId) { return this; } + public CommandWrapperBuilder goodwillCreditWorkingCapitalLoanTransaction(final Long loanId) { + this.actionName = ACTION_GOODWILLCREDIT; + this.entityName = ENTITY_WORKINGCAPITALLOAN; + this.entityId = loanId; + this.loanId = loanId; + this.href = "/working-capital-loans/" + loanId + "/transactions/template?command=goodwillcredit"; + return this; + } + public CommandWrapperBuilder loanInterestPaymentWaiverTransaction(final Long loanId) { this.actionName = ACTION_INTERESTPAYMENTWAIVER; this.entityName = ENTITY_LOAN; diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc index 2e3e2e79bed..bac25720bea 100644 --- a/fineract-doc/src/docs/en/chapters/features/index.adoc +++ b/fineract-doc/src/docs/en/chapters/features/index.adoc @@ -16,7 +16,8 @@ include::re-amortization.adoc[leveloffset=+1] include::re-ageing.adoc[leveloffset=+1] include::delayed-schedule-captures.adoc[leveloffset=+1] include::loan-origination-details.adoc[leveloffset=+1] +include::taxes-on-loan-charges.adoc[leveloffset=+1] include::working-capital-amortization-schedule.adoc[leveloffset=+1] include::working-capital-discount-fee-txn.adoc[leveloffset=+1] include::working-capital-credit-balance-refund.adoc[leveloffset=+1] -include::taxes-on-loan-charges.adoc[leveloffset=+1] +include::working-capital-goodwill-credit.adoc[leveloffset=+1] diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-goodwill-credit.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-goodwill-credit.adoc new file mode 100644 index 00000000000..2649632f937 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-goodwill-credit.adoc @@ -0,0 +1,128 @@ += Goodwill Credit Transaction + +== Purpose + +The Goodwill Credit transaction represents a *credit adjustment provided to the borrower*, typically as a gesture of goodwill to address *customer grievances, service issues, or operational corrections.* + +It ensures: + +* Controlled handling of *customer compensation* +* Proper *adjustment of outstanding dues* +* Accurate *accounting of goodwill expenses* + +== Functionality + +* Goodwill Credit is a *monetary transaction* applied to a loan account. +* Functionally, it is the same as a Repayment Transaction. +* It is performed when: +** A lender decides to credit an amount to the borrower without expecting repayment. +* It results in: +** Reduction in outstanding loan balance +** Triggering of accounting entries + +== Goodwill Credit Handling + +* It results in: +** Reduction of outstanding balance +** Calculation of the actual Amortization amount +** Posting Discount Fees Amortization transaction +** Adjustment of the Amortization schedule +** Allocation of payment across components +** Triggering of accounting entries +* API and parameters are similar to term loans + +== Validation Rules + +Goodwill credit can be applied only if: +* Loan is *Active*, *Charged-Off*, or *Closed* +Amount validation: +* Must be *greater than zero* + +== Allocation Logic + +Repayment amount follows the allocation strategy set for the Repayment transaction type, if not, follows the default strategy + +== Schedule Impact +* On Goodwill Credit: +** System will: +*** Adjust outstanding balances +*** Update amortization schedule +*** Recalculate future periods +* For backdated payments: +** System will: +*** Reprocess schedule +*** Replay all subsequent transactions and accounting entries + +== Accounting Treatment +Goodwill Credit triggers Journal Entries (JE) + +=== Goodwill Credit + +|=== +|Dr|Expense from Goodwill Credit|Expense|Principal + Excess +|Dr|Income from Goodwill Credit Fees|Income|Fees +|Dr|Income from Goodwill Credit Penalty|Income|Penalty +|Cr|Loan Portfolio|Asset|Principal +|Cr|Fees Receivable|Asset|Fees +|Cr|Penalty Receivable|Asset|Penalty +|Cr|Overpayment Liability|Liability|Excess +|=== + +=== Goodwill Credit Reversal + +|=== +|Debit|Loan Portfolio|Asset|Principal +|Debit|Fees Receivable|Asset|Fees +|Debit|Penalty Receivable|Asset|Penalty +|Debit|Overpayment Liability|Liability|Excess +|Credit|Income from Goodwill Credit Fees|Income|Fees +|Credit|Income from Goodwill Credit Penalty|Income|Penalty +|Credit|Expense from Goodwill Credit|Expense|Principal + Excess +|=== + +=== Goodwill Credit After Charge Off + +|=== +|Debit|Expense from Goodwill Credit|Expense|Principal + Excess +|Debit|Income from Goodwill Credit Fees|Income|Fees +|Debit|Income from Goodwill Credit Penalty|Income|Penalty +|Credit|Income from Recovery Repayment|Income|Principal +|Credit|Income from Recovery Repayment|Income|Fees +|Credit|Income from Recovery Repayment|Income|Penalty +|Credit|Overpayment Liability|Liability|Excess +|=== + +=== Goodwill Credit After Charge Off Reversal + +|=== +|Debit|Income from Recovery Repayment|Income|Principal +|Debit|Income from Recovery Repayment|Income|Fees +|Debit|Income from Recovery Repayment|Income|Penalty +|Debit|Overpayment Liability|Liability|Excess +|Credit|Income from Goodwill Credit Fees|Income|Fees +|Credit|Income from Goodwill Credit Penalty|Income|Penalty +|Credit|Expense from Goodwill Credit|Expense|Principal + Excess +|=== + +== Reversal Handling + +* Reversal of Goodwill Credit transaction is allowed +* Reversal behavior: +** Creates a linked reversal transaction +** Reverses: +*** Allocated components (penalty, fee, principal) +*** Outstanding balance impact +*** Accounting entries +* System will: +** Reprocess the Amortization schedule +** Replay subsequent transactions (if applicable) + +== Special Scenarios + +* Overpayment Handling: +** Excess amount will be refunded via credit balance refund transaction +* Post Charge-Off Repayment: +** Treated as regular payment +** Accounting follows charge-off rules +* Post Write-Off Repayment: +** Treated as recovery income \ No newline at end of file diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index 4f800563e06..4c3f22557e0 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -83,6 +83,7 @@ import org.apache.fineract.client.models.WorkingCapitalLoanCommandTemplateData; import org.apache.fineract.client.models.WorkingCapitalLoanPeriodPaymentRateChangeData; import org.apache.fineract.test.data.LoanStatus; +import org.apache.fineract.test.data.TransactionType; import org.apache.fineract.test.data.paymenttype.DefaultPaymentType; import org.apache.fineract.test.data.paymenttype.PaymentTypeResolver; import org.apache.fineract.test.data.workingcapitalproduct.DefaultWorkingCapitalLoanProduct; @@ -2095,9 +2096,18 @@ public void verifyModifyWorkingCapitalLoanAccountFailure(PutWorkingCapitalLoansL @Then("Customer makes repayment on {string} with {double} transaction amount on Working Capital loan") public void makeWorkingCapitalLoanRepayment(final String transactionDate, final double transactionAmount) { + makeWorkingCapitalLoanRepaymentLike("REPAYMENT", transactionDate, transactionAmount); + } + + @Then("Customer makes {string} transaction on {string} with {double} transaction amount on Working Capital loan") + public void makeWorkingCapitalLoanRepaymentLike(final String transactionTypeInput, final String transactionDate, + final double transactionAmount) { final Long loanId = getCreatedLoanId(); + final TransactionType transactionType = TransactionType.valueOf(transactionTypeInput); + final String transactionTypeValue = transactionType.getValue(); final PostWorkingCapitalLoanTransactionsRequest repaymentRequest = buildRepaymentRequest(transactionDate, transactionAmount, null); - final PostWorkingCapitalLoanTransactionsResponse response = executeRepaymentById(loanId, repaymentRequest); + final PostWorkingCapitalLoanTransactionsResponse response = executeRepaymentLikeById(loanId, transactionTypeValue, + repaymentRequest); validateRepaymentResponse(response, transactionAmount, transactionDate, loanId); } @@ -2117,7 +2127,7 @@ public void makeWorkingCapitalLoanRepaymentWithPaymentDetails(final String trans final PostWorkingCapitalLoanTransactionsPaymentDetailRequest paymentDetails = buildPaymentDetailsFromTable(table); final PostWorkingCapitalLoanTransactionsRequest repaymentRequest = buildRepaymentRequest(transactionDate, transactionAmount, paymentDetails); - final PostWorkingCapitalLoanTransactionsResponse response = executeRepaymentById(loanId, repaymentRequest); + final PostWorkingCapitalLoanTransactionsResponse response = executeRepaymentLikeById(loanId, "repayment", repaymentRequest); validateRepaymentResponse(response, transactionAmount, transactionDate, loanId); } @@ -2179,12 +2189,12 @@ private PostWorkingCapitalLoanTransactionsRequest buildRepaymentRequest(final St return request; } - private PostWorkingCapitalLoanTransactionsResponse executeRepaymentById(final Long loanId, + private PostWorkingCapitalLoanTransactionsResponse executeRepaymentLikeById(final Long loanId, final String transactionType, final PostWorkingCapitalLoanTransactionsRequest repaymentRequest) { - log.debug("Making repayment for loan ID: {}, transactionDate: {}, transactionAmount: {}", loanId, + log.debug("Making {} for loan ID: {}, transactionDate: {}, transactionAmount: {}", transactionType, loanId, repaymentRequest.getTransactionDate(), repaymentRequest.getTransactionAmount()); - return ok(() -> fineractClient.workingCapitalLoanTransactions().executeWorkingCapitalLoanTransactionById(loanId, "repayment", + return ok(() -> fineractClient.workingCapitalLoanTransactions().executeWorkingCapitalLoanTransactionById(loanId, transactionType, repaymentRequest)); } @@ -2334,6 +2344,25 @@ private PostWorkingCapitalLoanTransactionsPaymentDetailRequest buildPaymentDetai return paymentDetails; } + @Then("Initiating a {string} transaction on {string} with {double} transaction amount on Working Capital loan results an error with the following data:") + public void initiateTransactionResultsAnErrorWithDetails(final String transactionTypeInput, final String transactionDate, + final double transactionAmount, final DataTable table) { + final Long loanId = getCreatedLoanId(); + final TransactionType transactionType = TransactionType.valueOf(transactionTypeInput); + final String transactionTypeValue = transactionType.getValue(); + final PostWorkingCapitalLoanTransactionsRequest transactionRequest = buildRepaymentRequest(transactionDate, transactionAmount, + null); + + final CallFailedRuntimeException exception = fail(() -> fineractClient.workingCapitalLoanTransactions() + .executeWorkingCapitalLoanTransactionById(loanId, transactionTypeValue, transactionRequest)); + + if (table != null) { + verifyRepaymentErrorWithTable(exception, table); + } + + log.debug("Verified working capital loan {} transaction failed with expected error for loan {}", transactionTypeValue, loanId); + } + @Then("Initiating a repayment on {string} with {double} transaction amount on Working Capital loan results an error with the following data:") public void initiateRepaymentResultsAnErrorWithDetails(final String transactionDate, final double transactionAmount, final DataTable table) { diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanGoodwillCredit.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanGoodwillCredit.feature new file mode 100644 index 00000000000..cf13af81be1 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanGoodwillCredit.feature @@ -0,0 +1,482 @@ +@WorkingCapitalLoanGoodwillCreditFeature +Feature: Working Capital Loan Goodwill Credit + + @TestRailId:C80923 + Scenario: Verify working capital loan Goodwill Credit transaction - UC1: simple Goodwill Credit transaction + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes "GOODWILL_CREDIT" transaction on "10 January 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | + + @TestRailId:C80924 + Scenario: Verify working capital loan Goodwill Credit transaction - UC2: Goodwill Credit transaction with zero amount results an error (Negative) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Initiating a "GOODWILL_CREDIT" transaction on "10 January 2026" with 0.0 transaction amount on Working Capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | The parameter `transactionAmount` must be greater than 0. | + + @TestRailId:C80925 + Scenario: Verify working capital loan Goodwill Credit transaction - UC3: Goodwill Credit transaction with negative amount results an error (Negative) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Initiating a "GOODWILL_CREDIT" transaction on "10 January 2026" with -100.0 transaction amount on Working Capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | The parameter `transactionAmount` must be greater than 0. | + + @TestRailId:C80926 + Scenario: Verify working capital loan Goodwill Credit transaction - UC4: Goodwill Credit transaction with future date results an error (Negative) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Initiating a "GOODWILL_CREDIT" transaction on "15 January 2026" with 270.0 transaction amount on Working Capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | Failed data validation due to: cannot.be.a.future.date. | + + @TestRailId:C80927 + Scenario: Verify working capital loan Goodwill Credit transaction - UC5: Goodwill Credit transaction on disbursement day + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes "GOODWILL_CREDIT" transaction on "01 January 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 01 January 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | + + @TestRailId:C80928 + Scenario: Verify working capital loan Goodwill Credit transaction - UC6: Goodwill Credit transaction after disbursement day + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid via goodwill credit --- + When Admin sets the business date to "02 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "02 January 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C80929 + Scenario: Verify working capital loan Goodwill Credit transaction - UC7: Goodwill Credit transaction on last day of first period + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid via goodwill credit on last day --- + When Admin sets the business date to "30 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "30 January 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + + @TestRailId:C80930 + Scenario: Verify working capital loan Goodwill Credit transaction - UC8: Goodwill Credit transaction on first day of second period (delinquent) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid via goodwill credit on first day of next period --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | false | 270.0 | 1 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | | D00 | 1 | 30 | + And Customer makes "GOODWILL_CREDIT" transaction on "31 January 2026" with 270.0 transaction amount on Working Capital loan +# --- Check --- + When Admin sets the business date to "01 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + | 1 | 2026-01-31 | 2026-01-31 | D00 | 1 | 30 | + + @TestRailId:C80931 + Scenario: Verify working capital loan Goodwill Credit transaction - UC9: multiple Goodwill Credit transactions on same day (full payment) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid via 2 goodwill credits on the same day--- + When Admin sets the business date to "02 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "02 January 2026" with 170.0 transaction amount on Working Capital loan + And Customer makes "GOODWILL_CREDIT" transaction on "02 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 02 January 2026 | Goodwill Credit | 170.0 | 170.0 | 0.0 | 0.0 | false | + | 02 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | + + @TestRailId:C80932 + Scenario: Verify working capital loan Goodwill Credit transaction - UC10: multiple Goodwill Credit transactions on different days (full payment) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Full expectedAmount paid via 2 goodwill credits on different days--- + When Admin sets the business date to "02 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "02 January 2026" with 170.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 170.0 | 100.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "15 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "15 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 02 January 2026 | Goodwill Credit | 170.0 | 170.0 | 0.0 | 0.0 | false | + | 15 January 2026 | Goodwill Credit | 100.0 | 100.0 | 0.0 | 0.0 | false | + + @TestRailId:C80933 + Scenario: Verify working capital loan Goodwill Credit transaction - UC11: Goodwill Credit transaction combined with regular repayment (same period) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Mixed: repayment + goodwill credit in same period --- + When Admin sets the business date to "05 January 2026" + And Customer makes repayment on "05 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 100.0 | 170.0 | null | null | null | + When Admin sets the business date to "10 January 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "10 January 2026" with 170.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 05 January 2026 | Repayment | 100.0 | 100.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Goodwill Credit | 170.0 | 170.0 | 0.0 | 0.0 | false | + + @TestRailId:C80934 + Scenario: Verify working capital loan Goodwill Credit transaction - UC12: repayment and Goodwill Credit transaction in different periods + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And Verify Working Capital loan disbursement was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | + | WCLP | 2026-01-01 | 2026-01-01 | Active | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + When Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 0.0 | 270.0 | null | null | null | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Period 1: Regular repayment --- + When Admin sets the business date to "10 January 2026" + And Customer makes repayment on "10 January 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Delinquency Tag History for Working Capital loan has lines: + | periodNumber | addedOnDate | liftedOnDate | classification | minimumAgeDays | maximumAgeDays | +# --- Period 2: Goodwill credit --- + When Admin sets the business date to "31 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 0.0 | 270.0 | null | null | null | + When Admin sets the business date to "10 February 2026" + And Customer makes "GOODWILL_CREDIT" transaction on "10 February 2026" with 270.0 transaction amount on Working Capital loan + Then Working Capital loan delinquency range schedule has the following data: + | periodNumber | fromDate | toDate | expectedAmount | paidAmount | outstandingAmount | minPaymentCriteriaMet | delinquentAmount | delinquentDays | + | 1 | 2026-01-01 | 2026-01-30 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + | 2 | 2026-01-31 | 2026-03-01 | 270.0 | 270.0 | 0.0 | true | 0.0 | 0 | + And Working Capital Loan has transactions: + | transactionDate | type | transactionAmount | principalPortion | feeChargesPortion | penaltyChargesPortion | reversed | + | 01 January 2026 | Disbursement | 9000.0 | 9000.0 | 0.0 | 0.0 | false | + | 10 January 2026 | Repayment | 270.0 | 270.0 | 0.0 | 0.0 | false | + | 10 February 2026 | Goodwill Credit | 270.0 | 270.0 | 0.0 | 0.0 | false | + + @TestRailId:C80935 + Scenario: Verify working capital loan Goodwill Credit transaction - UC13: Goodwill Credit transaction after loan creation results an error (Negative) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Initiating a "GOODWILL_CREDIT" transaction on "01 January 2026" with 270.0 transaction amount on Working Capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | Goodwill Credit is allowed only for active/closed obligations met/overpaid loans | + + @TestRailId:C80936 + Scenario: Verify working capital loan Goodwill Credit transaction - UC14: Goodwill Credit transaction after approval results an error (Negative) + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + And Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And Working capital loan account has the correct data: + | product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountApproved | + | WCLP | 2026-01-01 | 2026-01-01 | Approved | 9000.0 | 9000.0 | 100000.0 | 18.0 | null | + Then Initiating a "GOODWILL_CREDIT" transaction on "01 January 2026" with 270.0 transaction amount on Working Capital loan results an error with the following data: + | httpCode | errorMessage | + | 400 | Goodwill Credit is allowed only for active/closed obligations met/overpaid loans | diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java index 3c4bcba0595..baf46ba2072 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java @@ -47,6 +47,7 @@ private WorkingCapitalLoanConstants() { public static final String APPROVE_LOAN_COMMAND = "approve"; public static final String DISBURSE_LOAN_COMMAND = "disburse"; public static final String REPAYMENT_LOAN_COMMAND = "repayment"; + public static final String GOODWILL_CREDIT_LOAN_COMMAND = "goodwillCredit"; public static final String CREDIT_BALANCE_REFUND_COMMAND = "creditBalanceRefund"; // Approval / Rejection / Undo-approval parameters diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java index 7edf1cb6039..9b2be8cb6c2 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanTransactionsApiResource.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.workingcapitalloan.api; +import static org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants.GOODWILL_CREDIT_LOAN_COMMAND; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -221,6 +223,8 @@ private CommandProcessingResult executeTransaction(final Long loanId, final Stri commandRequest = builder.repaymentWorkingCapitalLoanTransaction(resolvedLoanId).build(); } else if (CommandParameterUtil.is(commandParam, "creditBalanceRefund")) { commandRequest = builder.creditBalanceRefundWorkingCapitalLoanTransaction(resolvedLoanId).build(); + } else if (CommandParameterUtil.is(commandParam, GOODWILL_CREDIT_LOAN_COMMAND)) { + commandRequest = builder.goodwillCreditWorkingCapitalLoanTransaction(resolvedLoanId).build(); } else { throw new UnrecognizedQueryParamException("command", commandParam); } 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 862c198e302..cc315114850 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 @@ -123,6 +123,14 @@ public static WorkingCapitalLoanTransaction repayment(final WorkingCapitalLoan l return txn; } + public static WorkingCapitalLoanTransaction goodwillCredit(final WorkingCapitalLoan loan, final BigDecimal amount, + final PaymentDetail paymentDetail, final LocalDate transactionDate, final CodeValue classification, + final ExternalId externalId) { + final WorkingCapitalLoanTransaction txn = new WorkingCapitalLoanTransaction(); + txn.initialize(loan, LoanTransactionType.GOODWILL_CREDIT, transactionDate, amount, paymentDetail, classification, externalId); + return txn; + } + public static WorkingCapitalLoanTransaction creditBalanceRefund(final WorkingCapitalLoan loan, final BigDecimal amount, final PaymentDetail paymentDetail, final LocalDate transactionDate, final CodeValue classification, final ExternalId externalId) { diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanGoodwillCreditCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanGoodwillCreditCommandHandler.java new file mode 100644 index 00000000000..74b2ef231aa --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/WorkingCapitalLoanGoodwillCreditCommandHandler.java @@ -0,0 +1,43 @@ +/** + * 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.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.domain.CommandWrapperConstants; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = CommandWrapperConstants.ENTITY_WORKINGCAPITALLOAN, action = CommandWrapperConstants.ACTION_GOODWILLCREDIT) +public class WorkingCapitalLoanGoodwillCreditCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.makeGoodwillCredit(command.getLoanId(), command); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java index 7e2d07bef02..643c63a10c6 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java @@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.loanaccount.domain.ExpectedDisbursementDateValidator; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; @@ -461,7 +462,7 @@ public void validateUndoDisbursal(final String json) { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateRepayment(final String json, final WorkingCapitalLoan loan) { + public void validateRepayment(final String json, final WorkingCapitalLoan loan, LoanTransactionType goodwillCredit) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -537,6 +538,19 @@ public void validateRepayment(final String json, final WorkingCapitalLoan loan) validatePaymentDetails(baseDataValidator, element); throwExceptionIfValidationWarningsExist(dataValidationErrors); + + if (LoanTransactionType.REPAYMENT.equals(goodwillCredit)) { + if (loan.getLoanStatus() != LoanStatus.ACTIVE && loan.getLoanStatus() != LoanStatus.OVERPAID) { + throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed", + "Repayment is allowed only for active/overpaid loans", "loanStatus"); + } + } else if (LoanTransactionType.GOODWILL_CREDIT.equals(goodwillCredit)) { + if (!LoanStatus.ACTIVE.equals(loan.getLoanStatus()) && !LoanStatus.CLOSED_OBLIGATIONS_MET.equals(loan.getLoanStatus()) + && !LoanStatus.OVERPAID.equals(loan.getLoanStatus())) { + throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed", + "Goodwill Credit is allowed only for active/closed obligations met/overpaid loans", "loanStatus"); + } + } } public void validateCreditBalanceRefund(final String json, final WorkingCapitalLoan loan) { diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionReadPlatformServiceImpl.java index 58cd51947d3..242de482c47 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionReadPlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanTransactionReadPlatformServiceImpl.java @@ -68,7 +68,8 @@ public WorkingCapitalLoanCommandTemplateData retrieveLoanTransactionTemplate(fin .classificationOptions(codeValueReadPlatformService .retrieveCodeValuesByCode(WorkingCapitalLoanConstants.DISBURSEMENT_CLASSIFICATION_CODE_NAME)) .build(); - } else if (WorkingCapitalLoanConstants.REPAYMENT_LOAN_COMMAND.equals(command)) { + } else if (WorkingCapitalLoanConstants.REPAYMENT_LOAN_COMMAND.equals(command) + || WorkingCapitalLoanConstants.GOODWILL_CREDIT_LOAN_COMMAND.equals(command)) { return WorkingCapitalLoanCommandTemplateData.builder() .expectedAmount(wcLoan.getBalance() != null ? wcLoan.getBalance().getPrincipalOutstanding() : null) .currency(wcLoan.getLoanProduct().getCurrency().toData()) diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java index 85594fc1416..6d604b0ef5b 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformService.java @@ -39,5 +39,7 @@ public interface WorkingCapitalLoanWritePlatformService { CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command); + CommandProcessingResult makeGoodwillCredit(Long loanId, JsonCommand command); + CommandProcessingResult updatePeriodPaymentRate(Long loanId, JsonCommand command); } 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 03bb222a31a..202a129c052 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 @@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeTransactionBusinessEvent; @@ -324,13 +325,8 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand amortizationScheduleWriteService.generateAndSaveAmortizationScheduleOnDisbursement(loan, transactionAmount, actualDisbursementDate); this.loanRepository.saveAndFlush(loan); - - final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); - if (StringUtils.isNotBlank(noteText)) { - changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); - } changes.put("status", loan.getLoanStatus()); - createNote(noteText, loan); + handleNote(loan, command, changes); log.debug("Working capital loan {} disbursed by user {}", loanId, currentUser != null ? currentUser.getId() : "system"); @@ -384,11 +380,7 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman changes.put("status", loan.getLoanStatus()); changes.put(WorkingCapitalLoanConstants.actualDisbursementDateParamName, null); changes.put("actualAmount", null); - final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); - if (StringUtils.isNotBlank(noteText)) { - changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); - } - createNote(noteText, loan); + handleNote(loan, command, changes); log.debug("Working capital loan {} disbursal undone", loanId); @@ -484,10 +476,7 @@ public CommandProcessingResult makeDiscountFee(Long loanId, JsonCommand command) final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, WorkingCapitalLoanConstants.externalIdParameterName); final PaymentDetail paymentDetail = createAndPersistPaymentDetailFromCommand(command, changes); - createNote(note, loan); - if (note != null) { - changes.put(WorkingCapitalLoanConstants.noteParamName, note); - } + handleNote(loan, command, changes); changes.put(WorkingCapitalLoanConstants.transactionAmountParamName, amount); changes.put(WorkingCapitalLoanConstants.relatedResourceIdParamName, relatedDisbursementTransactionId); @@ -511,12 +500,7 @@ public CommandProcessingResult makeDiscountFee(Long loanId, JsonCommand command) public CommandProcessingResult makeRepayment(final Long loanId, final JsonCommand command) { final WorkingCapitalLoan loan = this.loanRepository.findById(loanId) .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); - this.validator.validateRepayment(command.json(), loan); - - if (loan.getLoanStatus() != LoanStatus.ACTIVE && loan.getLoanStatus() != LoanStatus.OVERPAID) { - throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed", - "Repayment is allowed only for active/overpaid loans", "loanStatus"); - } + this.validator.validateRepayment(command.json(), loan, LoanTransactionType.REPAYMENT); final LocalDate transactionDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.transactionDateParamName); final BigDecimal transactionAmount = this.fromApiJsonHelper @@ -540,9 +524,7 @@ public CommandProcessingResult makeRepayment(final Long loanId, final JsonComman final WorkingCapitalLoanBalance currentBalance = this.balanceRepository.findByWcLoan_Id(loan.getId()) .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); - final BigDecimal outstandingBeforeRepayment = currentBalance.getPrincipalOutstanding() != null - ? currentBalance.getPrincipalOutstanding() - : BigDecimal.ZERO; + final BigDecimal outstandingBeforeRepayment = MathUtil.nullToZero(currentBalance.getPrincipalOutstanding()); final BigDecimal amountAppliedToOutstanding = transactionAmount.min(outstandingBeforeRepayment); final WorkingCapitalLoanTransactionAllocation allocation = WorkingCapitalLoanTransactionAllocation @@ -554,29 +536,10 @@ public CommandProcessingResult makeRepayment(final Long loanId, final JsonComman updateBalanceOnRepayment(loan, transactionAmount, amortizationData); internalWorkingCapitalLoanPaymentService.makePayment(loanId, amountAppliedToOutstanding, transactionDate); - if (loan.getBalance() != null) { - final BigDecimal overpaymentAmount = loan.getBalance().getOverpaymentAmount() != null ? loan.getBalance().getOverpaymentAmount() - : BigDecimal.ZERO; - final BigDecimal principalOutstanding = loan.getBalance().getPrincipalOutstanding() != null - ? loan.getBalance().getPrincipalOutstanding() - : BigDecimal.ZERO; - if (overpaymentAmount.compareTo(BigDecimal.ZERO) > 0) { - this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_OVERPAID, loan); - loan.setMaturedOnDate(transactionDate); - } else if (principalOutstanding.compareTo(BigDecimal.ZERO) == 0) { - this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REPAID_IN_FULL, loan); - loan.setMaturedOnDate(transactionDate); - } - } - - final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); - if (StringUtils.isNotBlank(noteText)) { - changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); - } + handleStateChanges(loan, transactionDate); changes.put("status", loan.getLoanStatus()); - createNote(noteText, loan); - this.loanRepository.saveAndFlush(loan); + handleNote(loan, command, changes); if (loan.getLoanProduct().getAccountingRule().isCashBased()) { accountingProcessor.postJournalEntriesForRepayment(loan, repaymentTransaction, allocation, false); @@ -591,6 +554,31 @@ public CommandProcessingResult makeRepayment(final Long loanId, final JsonComman .withClientId(loan.getClientId()).withLoanId(loanId).with(changes).build(); } + private void handleNote(WorkingCapitalLoan loan, JsonCommand command, Map changes) { + final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); + if (StringUtils.isNotBlank(noteText)) { + changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); + } + createNote(noteText, loan); + } + + private void handleStateChanges(WorkingCapitalLoan loan, LocalDate transactionDate) { + if (loan.getBalance() != null) { + final BigDecimal overpaymentAmount = loan.getBalance().getOverpaymentAmount() != null ? loan.getBalance().getOverpaymentAmount() + : BigDecimal.ZERO; + final BigDecimal principalOutstanding = loan.getBalance().getPrincipalOutstanding() != null + ? loan.getBalance().getPrincipalOutstanding() + : BigDecimal.ZERO; + if (overpaymentAmount.compareTo(BigDecimal.ZERO) > 0) { + this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_OVERPAID, loan); + loan.setMaturedOnDate(transactionDate); + } else if (principalOutstanding.compareTo(BigDecimal.ZERO) == 0) { + this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REPAID_IN_FULL, loan); + loan.setMaturedOnDate(transactionDate); + } + } + } + @Override public CommandProcessingResult creditBalanceRefund(final Long loanId, final JsonCommand command) { final WorkingCapitalLoan loan = this.loanRepository.findById(loanId) @@ -654,12 +642,8 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json } } - final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); - if (StringUtils.isNotBlank(noteText)) { - changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); - } changes.put("status", loan.getLoanStatus()); - createNote(noteText, loan); + handleNote(loan, command, changes); this.loanRepository.saveAndFlush(loan); businessEventNotifierService.notifyPostBusinessEvent( @@ -717,6 +701,56 @@ public CommandProcessingResult updatePeriodPaymentRate(final Long loanId, final .withLoanId(loanId).with(changes).build(); } + @Override + public CommandProcessingResult makeGoodwillCredit(Long loanId, JsonCommand command) { + final WorkingCapitalLoan loan = this.loanRepository.findById(loanId) + .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + this.validator.validateRepayment(command.json(), loan, LoanTransactionType.GOODWILL_CREDIT); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.transactionDateParamName); + final BigDecimal transactionAmount = this.fromApiJsonHelper + .extractBigDecimalNamed(WorkingCapitalLoanConstants.transactionAmountParamName, command.parsedJson(), new HashSet<>()); + final Map changes = new LinkedHashMap<>(); + changes.put(WorkingCapitalLoanConstants.transactionDateParamName, transactionDate); + changes.put(WorkingCapitalLoanConstants.transactionAmountParamName, transactionAmount); + final PaymentDetail paymentDetail = createAndPersistPaymentDetailFromCommand(command, changes); + + final Long classificationId = command.longValueOfParameterNamed(WorkingCapitalLoanConstants.classificationIdParamName); + final CodeValue classification = classificationId != null + ? codeValueRepository.findByCodeNameAndId(WorkingCapitalLoanConstants.REPAYMENT_CLASSIFICATION_CODE_NAME, classificationId) + : null; + changes.put(WorkingCapitalLoanConstants.classificationIdParamName, classificationId); + + final ExternalId txnExternalId = this.externalIdFactory.createFromCommand(command, + WorkingCapitalLoanConstants.externalIdParameterName); + final WorkingCapitalLoanTransaction repaymentTransaction = WorkingCapitalLoanTransaction.goodwillCredit(loan, transactionAmount, + paymentDetail, transactionDate, classification, txnExternalId); + this.transactionRepository.saveAndFlush(repaymentTransaction); + final WorkingCapitalLoanBalance currentBalance = this.balanceRepository.findByWcLoan_Id(loan.getId()) + .orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan)); + final BigDecimal outstandingBeforeRepayment = MathUtil.nullToZero(currentBalance.getPrincipalOutstanding()); + final BigDecimal amountAppliedToOutstanding = transactionAmount.min(outstandingBeforeRepayment); + + final WorkingCapitalLoanTransactionAllocation allocation = WorkingCapitalLoanTransactionAllocation + .forPrincipalAllocation(repaymentTransaction, amountAppliedToOutstanding); + this.allocationRepository.saveAndFlush(allocation); + + final RepaymentAmortizationData amortizationData = amortizationScheduleWriteService.applyRepayment(loan, transactionDate, + amountAppliedToOutstanding); + updateBalanceOnRepayment(loan, transactionAmount, amortizationData); + internalWorkingCapitalLoanPaymentService.makePayment(loanId, amountAppliedToOutstanding, transactionDate); + + handleStateChanges(loan, transactionDate); + changes.put("status", loan.getLoanStatus()); + + handleNote(loan, command, changes); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withLoanId(loanId) + .withLoanExternalId(loan.getExternalId()).withEntityId(repaymentTransaction.getId()) + .withEntityExternalId(repaymentTransaction.getExternalId()).withOfficeId(loan.getOfficeId()) + .withClientId(loan.getClientId()).with(changes).build(); + } + private PaymentDetail createAndPersistPaymentDetailFromCommand(final JsonCommand command, final Map changes) { final JsonElement paymentDetailsElement = command.jsonElement(WorkingCapitalLoanConstants.paymentDetailsParamName); if (paymentDetailsElement != null && paymentDetailsElement.isJsonNull()) {