Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion fineract-doc/src/docs/en/chapters/features/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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) {
Expand Down
Loading