Skip to content

Commit 0d05907

Browse files
FINERACT-2455: Working Capital loan discount field enhancement
1 parent ca738eb commit 0d05907

15 files changed

Lines changed: 254 additions & 25 deletions

File tree

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,10 @@ private List<String> fetchValuesOfWorkingCapitalLoan(final List<String> header,
11471147
: new Utils.DoubleFormatter(response.getPeriodPaymentRate().doubleValue()).format());
11481148
case "discount" -> actualValues.add(
11491149
response.getDiscount() == null ? "null" : new Utils.DoubleFormatter(response.getDiscount().doubleValue()).format());
1150+
case "discountProposed" -> actualValues.add(response.getDiscountProposed() == null ? "null"
1151+
: new Utils.DoubleFormatter(response.getDiscountProposed().doubleValue()).format());
1152+
case "discountApproved" -> actualValues.add(response.getDiscountApproved() == null ? "null"
1153+
: new Utils.DoubleFormatter(response.getDiscountApproved().doubleValue()).format());
11501154
case "totalPaidPrincipal" ->
11511155
actualValues.add(response.getBalance() == null || response.getBalance().getTotalPaidPrincipal() == null ? null
11521156
: new Utils.DoubleFormatter(response.getBalance().getTotalPaidPrincipal().doubleValue()).format());

fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalProductLoanAccount.feature

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1505,4 +1505,30 @@ Feature: WorkingCapitalLoanAccount
15051505
Examples:
15061506
| near_breach_id |
15071507
| 0 |
1508-
| 9223372036854775807 |
1508+
| 9223372036854775807 |
1509+
1510+
@TestRailId:C74518
1511+
Scenario: Verify that undo disbursal of WCL account restores the approved discount after disbursement override - UC5.2
1512+
When Admin sets the business date to "01 January 2026"
1513+
And Admin creates a client with random data
1514+
And Admin creates a working capital loan with the following data:
1515+
| LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount |
1516+
| WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | |
1517+
And Working capital loan account has the correct data:
1518+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount |
1519+
| WCLP | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null | null | null |
1520+
Then Working capital loan creation was successful
1521+
Then Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and "14" discount amount and expected disbursement date on "01 January 2026"
1522+
And Working capital loan account has the correct data:
1523+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed | discountApproved | discount |
1524+
| WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | null | 14.0 | null |
1525+
Then Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount and "13" discount amount
1526+
Then Working Capital loan status will be "ACTIVE"
1527+
And Working capital loan account has the correct data:
1528+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount |
1529+
| WCLP | 2026-01-01 | 2026-01-01 | Active | 113.0 | 100.0 | 100.0 | 1.0 | 13.0 |
1530+
Then Admin successfully undo Working Capital disbursal
1531+
Then Working Capital loan status will be "APPROVED"
1532+
And Working capital loan account has the correct data:
1533+
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discount | totalPaidPrincipal | realizedIncome | unrealizedIncome |
1534+
| WCLP | 2026-01-01 | 2026-01-01 | Approved | 100.0 | 100.0 | 100.0 | 1.0 | 14.0 | 0.0 | 0.0 | 0.0 |

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanApiResourceSwagger.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,12 @@ private GetWorkingCapitalLoansLoanIdResponse() {}
191191
@Schema(example = "30")
192192
public Integer repaymentEvery;
193193
public StringEnumOptionData repaymentFrequencyType;
194-
@Schema(example = "0.0")
194+
@Schema(example = "0.0", description = "Discount set during loan disbursement")
195195
public BigDecimal discount;
196+
@Schema(example = "0.0", description = "Proposed discount at loan submission time")
197+
public BigDecimal discountProposed;
198+
@Schema(example = "0.0", description = "Approved discount set during loan approval")
199+
public BigDecimal discountApproved;
196200
@Schema(description = "Working capital breach)")
197201
public WorkingCapitalLoanProductApiResourceSwagger.GetWorkingCapitalLoanProductsResponse.GetWorkingCapitalLoanBreach breach;
198202
public WorkingCapitalLoanProductApiResourceSwagger.GetWorkingCapitalLoanNearBreach nearBreach;

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanData.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public class WorkingCapitalLoanData implements Serializable {
6969
private Integer repaymentEvery;
7070
private StringEnumOptionData repaymentFrequencyType;
7171
private BigDecimal discount;
72+
private BigDecimal discountProposed;
73+
private BigDecimal discountApproved;
7274
private DelinquencyBucketData delinquencyBucket;
7375
private WorkingCapitalBreachData breach;
7476
private WorkingCapitalNearBreachData nearBreach;

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanMapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public interface WorkingCapitalLoanMapper {
6464
@Mapping(target = "repaymentEvery", source = "loanProductRelatedDetails.repaymentEvery")
6565
@Mapping(target = "repaymentFrequencyType", source = "loanProductRelatedDetails", qualifiedByName = "repaymentFrequencyTypeData")
6666
@Mapping(target = "discount", source = "loanProductRelatedDetails.discount")
67+
@Mapping(target = "discountProposed", source = "loanProductRelatedDetails.discountProposed")
68+
@Mapping(target = "discountApproved", source = "loanProductRelatedDetails.discountApproved")
6769
@Mapping(target = "breach", source = "loanProductRelatedDetails.breach")
6870
@Mapping(target = "nearBreach", source = "loanProductRelatedDetails.nearBreach")
6971
@Mapping(target = "delinquencyBucket", source = "loanProductRelatedDetails.delinquencyBucket")

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public void validateApproval(final String json, final WorkingCapitalLoan loan) {
150150
.failWithCode("cannot.be.before.approval.date");
151151
}
152152

153-
// discountAmount must be >= 0 and <= current (creation-time) discount
153+
// discountAmount must be >= 0 and <= proposed discount (creation-time) discount
154154
if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.discountAmountParamName, element)) {
155155
if (isDiscountOverrideAllowed(loan)) {
156156
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
@@ -162,7 +162,7 @@ public void validateApproval(final String json, final WorkingCapitalLoan loan) {
162162
.zeroOrPositiveAmount();
163163

164164
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null
165-
? loan.getLoanProductRelatedDetails().getDiscount()
165+
? loan.getLoanProductRelatedDetails().getDiscountProposed()
166166
: null;
167167
if (discountAmount != null && currentDiscount != null && discountAmount.compareTo(currentDiscount) > 0) {
168168
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
@@ -290,12 +290,13 @@ public void validateDisbursement(final String json, final WorkingCapitalLoan loa
290290
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).ignoreIfNull()
291291
.zeroOrPositiveAmount();
292292

293+
// discountAmount must be >= 0 and <= approved discount (approval-time) discount
293294
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null
294-
? loan.getLoanProductRelatedDetails().getDiscount()
295+
? loan.getLoanProductRelatedDetails().getDiscountApproved()
295296
: null;
296297
if (discountAmount != null && currentDiscount != null && discountAmount.compareTo(currentDiscount) > 0) {
297298
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
298-
.failWithCode("amount.cannot.exceed.created.discount");
299+
.failWithCode("amount.cannot.exceed.approved.discount");
299300
}
300301
}
301302

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,6 @@ public interface WorkingCapitalLoanAmortizationScheduleWriteService {
3535
void regenerateAmortizationScheduleOnUndoDisbursal(WorkingCapitalLoan loan);
3636

3737
RepaymentAmortizationData applyRepayment(WorkingCapitalLoan loan, LocalDate transactionDate, BigDecimal repaymentAmount);
38+
39+
BigDecimal getWorkingCapitalLoanDiscountAmount(WorkingCapitalLoan loan);
3840
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ public void generateAndSaveAmortizationScheduleOnDisbursement(final WorkingCapit
7676
Validate.notNull(disbursementDate, "disbursementDate must not be null");
7777

7878
final MathContext mc = MoneyHelper.getMathContext();
79-
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
80-
? loan.getLoanProductRelatedDetails().getDiscount()
81-
: BigDecimal.ZERO;
79+
final BigDecimal discount = getWorkingCapitalLoanDiscountAmount(loan);
8280
final BigDecimal totalPayment = loan.getBalance() != null && loan.getBalance().getTotalPayment() != null
8381
? loan.getBalance().getTotalPayment()
8482
: BigDecimal.ZERO;
@@ -107,13 +105,26 @@ public void regenerateAmortizationScheduleOnUndoDisbursal(final WorkingCapitalLo
107105
generateAndSaveForApprovedLoanState(loan);
108106
}
109107

108+
@Override
109+
public BigDecimal getWorkingCapitalLoanDiscountAmount(WorkingCapitalLoan loan) {
110+
BigDecimal discount = BigDecimal.ZERO;
111+
if (loan.getLoanProductRelatedDetails() != null) {
112+
if (loan.getLoanStatus().isSubmittedAndPendingApproval() && loan.getLoanProductRelatedDetails().getDiscountProposed() != null) {
113+
discount = loan.getLoanProductRelatedDetails().getDiscountProposed();
114+
} else if (loan.getLoanStatus().isApproved() && loan.getLoanProductRelatedDetails().getDiscountApproved() != null) {
115+
discount = loan.getLoanProductRelatedDetails().getDiscountApproved();
116+
} else if (loan.getLoanStatus().isActive() && loan.getLoanProductRelatedDetails().getDiscount() != null) {
117+
discount = loan.getLoanProductRelatedDetails().getDiscount();
118+
}
119+
}
120+
return discount;
121+
}
122+
110123
private void generateAndSaveForApprovedLoanState(final WorkingCapitalLoan loan) {
111124
Validate.notNull(loan, "loan must not be null");
112125

113126
final MathContext mc = MoneyHelper.getMathContext();
114-
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
115-
? loan.getLoanProductRelatedDetails().getDiscount()
116-
: BigDecimal.ZERO;
127+
final BigDecimal discount = getWorkingCapitalLoanDiscountAmount(loan);
117128
final BigDecimal totalPayment = loan.getBalance() != null && loan.getBalance().getTotalPayment() != null
118129
? loan.getBalance().getTotalPayment()
119130
: BigDecimal.ZERO;

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAssemblerImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ private WorkingCapitalLoanProductRelatedDetails buildLoanProductRelatedDetails(f
177177
: productDetail.getRepaymentFrequencyType());
178178
detail.setAmortizationType(productDetail.getAmortizationType());
179179
detail.setNpvDayCount(productDetail.getNpvDayCount());
180-
detail.setDiscount(fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.discountParamName, element)
180+
detail.setDiscountProposed(fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.discountParamName, element)
181181
? fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanProductConstants.discountParamName, element, new HashSet<>())
182182
: productDetail.getDiscount());
183183
final Long breachId = fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachIdParamName, element)
@@ -355,7 +355,7 @@ public Map<String, Object> updateFrom(final JsonCommand command, final WorkingCa
355355
element, new HashSet<>());
356356
if (command.isChangeInBigDecimalParameterNamed(WorkingCapitalLoanProductConstants.discountParamName,
357357
detail.getDiscount())) {
358-
detail.setDiscount(discount);
358+
detail.setDiscountProposed(discount);
359359
changes.put(WorkingCapitalLoanProductConstants.discountParamName, discount);
360360
}
361361
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
4040
import org.apache.fineract.infrastructure.core.service.DateUtils;
4141
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
42+
import org.apache.fineract.infrastructure.core.service.MathUtil;
4243
import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent;
4344
import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent;
4445
import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanRepaymentTransactionBusinessEvent;
@@ -130,13 +131,12 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC
130131
}
131132

132133
// Discount amount (optional, can only be reduced per requirement)
134+
BigDecimal discount = loan.getLoanProductRelatedDetails().getDiscountProposed();
133135
if (command.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName)) {
134-
final BigDecimal discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
136+
discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
135137
command.parsedJson(), new HashSet<>());
136-
if (discount != null) {
137-
loan.getLoanProductRelatedDetails().setDiscount(discount);
138-
}
139138
}
139+
loan.getLoanProductRelatedDetails().setDiscountApproved(discount);
140140

141141
// Keep first tranche expected amount aligned with approved principal (submit stores proposed principal only).
142142
if (!loan.getDisbursementDetails().isEmpty()) {
@@ -185,7 +185,10 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final
185185
// The loan is back in SUBMITTED state and can be modified.
186186
final WorkingCapitalLoanProduct product = loan.getLoanProduct();
187187
final WorkingCapitalLoanProductRelatedDetail productDetail = product.getRelatedDetail();
188-
loan.getLoanProductRelatedDetails().setDiscount(productDetail.getDiscount());
188+
final BigDecimal discountProposed = (loan.getLoanProductRelatedDetails().getDiscountProposed() != null)
189+
? loan.getLoanProductRelatedDetails().getDiscountProposed()
190+
: MathUtil.nullToZero(productDetail.getDiscount());
191+
loan.getLoanProductRelatedDetails().setDiscountApproved(discountProposed);
189192

190193
this.loanRepository.saveAndFlush(loan);
191194

@@ -283,13 +286,16 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
283286
loan.getDisbursementDetails().getFirst().setDisbursedBy(currentUser);
284287
}
285288

289+
BigDecimal discount = loan.getLoanProductRelatedDetails().getDiscountApproved();
286290
if (command.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName)) {
287-
final BigDecimal discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
291+
discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
288292
command.parsedJson(), new HashSet<>());
289293
if (discount != null) {
290294
loan.getLoanProductRelatedDetails().setDiscount(discount);
291295
changes.put(WorkingCapitalLoanConstants.discountAmountParamName, discount);
292296
}
297+
} else {
298+
loan.getLoanProductRelatedDetails().setDiscount(discount);
293299
}
294300

295301
final ExternalId txnExternalId = this.externalIdFactory.createFromCommand(command,
@@ -359,6 +365,10 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman
359365
}
360366
}
361367
}
368+
final BigDecimal discountAmount = (loan.getLoanProductRelatedDetails().getDiscountApproved() != null)
369+
? loan.getLoanProductRelatedDetails().getDiscountApproved()
370+
: MathUtil.nullToZero(loan.getLoanProduct().getRelatedDetail().getDiscount());
371+
loan.getLoanProductRelatedDetails().setDiscount(discountAmount);
362372
amortizationScheduleWriteService.regenerateAmortizationScheduleOnUndoDisbursal(loan);
363373

364374
this.loanRepository.saveAndFlush(loan);

0 commit comments

Comments
 (0)