Skip to content

Commit e7dc573

Browse files
committed
FINERACT-2455: Working Capital - Undo disbursement and discount is not handled properly
1 parent 414aec0 commit e7dc573

14 files changed

Lines changed: 123 additions & 64 deletions

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

Lines changed: 75 additions & 28 deletions
Large diffs are not rendered by default.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Feature: WorkingCapitalLoanAccount
6767
Then Working capital loan creation was successful
6868
And Working capital loan account has the correct data:
6969
| product.name | submittedOnDate | expectedDisbursementDate | status | principal | approvedPrincipal | totalPayment | periodPaymentRate | discountProposed |
70-
| WCLP_DISCOUNT_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | null |
70+
| WCLP_DISCOUNT_DISALLOW_ATTRIBUTES_OVERRIDE | 2026-01-01 | 2026-01-01 | Submitted and pending approval | 100.0 | 0.0 | 100.0 | 1.0 | 50.0 |
7171

7272
@TestRailId:C70254
7373
Scenario: Create Working Capital Loan account - UC5: Create with principal amount greater than WCLP max (Negative)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class WorkingCapitalLoanCommandTemplateData implements Serializable {
4444
private Long loanId;
4545
private LocalDate approvalDate;
4646
private BigDecimal approvalAmount;
47+
private BigDecimal discountAmount;
48+
private Boolean overrideDiscountDisabled;
4749

4850
private LocalDate expectedDisbursementDate;
4951
private BigDecimal expectedAmount;

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ List<Long> findAllLoansByLastClosedBusinessDateAndMinAndMaxLoanIdAndStatuses(Lon
9292
List<COBIdAndLastClosedBusinessDate> findAllLoansBehindOrNullByLoanIdsAndStatuses(@Param("cobBusinessDate") LocalDate cobBusinessDate,
9393
@Param("loanIds") List<Long> loanIds, @Param("loanStatuses") Collection<LoanStatus> loanStatuses);
9494

95-
Long findIdByExternalId(ExternalId externalId);
95+
@Query("select loan.id as id from WorkingCapitalLoan loan where loan.externalId = :externalId")
96+
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
9697

9798
@Query("select loan.id, loan.lastClosedBusinessDate from WorkingCapitalLoan loan where loan.id IN :loanIds and loan.loanStatus in :loanStatuses and loan.lastClosedBusinessDate < :cobBusinessDate")
9899
List<COBIdAndLastClosedBusinessDate> findAllLoansBehindByLoanIdsAndStatuses(@Param("cobBusinessDate") LocalDate cobBusinessDate,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,7 +596,7 @@ private void validateOverridables(final JsonElement element, final DataValidator
596596
}
597597
}
598598
if (this.fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.discountParamName, element)) {
599-
if (config.isDiscountDefault()) {
599+
if (config.isDiscountDefaultOverridable()) {
600600
final BigDecimal discount = this.fromApiJsonHelper
601601
.extractBigDecimalNamed(WorkingCapitalLoanProductConstants.discountParamName, element, new java.util.HashSet<>());
602602
baseDataValidator.reset().parameter(WorkingCapitalLoanProductConstants.discountParamName).value(discount).ignoreIfNull()

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

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,16 @@ public void validateDiscountTransaction(final WorkingCapitalLoan loan, final Str
119119
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
120120
.resource(WorkingCapitalLoanConstants.RESOURCE_NAME);
121121

122-
if (isDiscountOverrideAllowed(loan)) {
123-
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
124-
.failWithCode("override.not.allowed.by.product");
125-
}
126122
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).ignoreIfNull()
127123
.zeroOrPositiveAmount();
128124

129-
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount()
125+
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null
126+
? loan.getLoanProductRelatedDetails().getDiscountApproved()
130127
: null;
128+
if (isDiscountOverrideDisallowed(loan) && (currentDiscount == null || discountAmount.compareTo(currentDiscount) != 0)) {
129+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
130+
.failWithCode("override.not.allowed.by.product");
131+
}
131132
if (currentDiscount == null) {
132133
validateDiscountAmountWithProductDiscount(discountAmount, loan.getLoanProduct().getRelatedDetail(), baseDataValidator);
133134
}
@@ -209,17 +210,17 @@ public void validateApproval(final String json, final WorkingCapitalLoan loan) {
209210

210211
// discountAmount must be >= 0 and <= proposed discount (creation-time) discount
211212
if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.discountAmountParamName, element)) {
212-
if (isDiscountOverrideAllowed(loan)) {
213-
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
214-
.failWithCode("override.not.allowed.by.product");
215-
}
216213
final BigDecimal discountAmount = this.fromApiJsonHelper
217214
.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName, element, new HashSet<>());
218215
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).ignoreIfNull()
219216
.zeroOrPositiveAmount();
220217
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null
221218
? loan.getLoanProductRelatedDetails().getDiscountProposed()
222219
: null;
220+
if (isDiscountOverrideDisallowed(loan) && (currentDiscount == null || discountAmount.compareTo(currentDiscount) != 0)) {
221+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
222+
.failWithCode("override.not.allowed.by.product");
223+
}
223224
if (currentDiscount != null) {
224225
if (discountAmount.compareTo(currentDiscount) > 0) {
225226
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
@@ -352,10 +353,6 @@ public void validateDisbursement(final String json, final WorkingCapitalLoan loa
352353
}
353354

354355
if (this.fromApiJsonHelper.parameterHasValue(WorkingCapitalLoanConstants.discountAmountParamName, element)) {
355-
if (isDiscountOverrideAllowed(loan)) {
356-
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
357-
.failWithCode("override.not.allowed.by.product");
358-
}
359356
final BigDecimal discountAmount = this.fromApiJsonHelper
360357
.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName, element, new HashSet<>());
361358
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).ignoreIfNull()
@@ -365,6 +362,10 @@ public void validateDisbursement(final String json, final WorkingCapitalLoan loa
365362
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null
366363
? loan.getLoanProductRelatedDetails().getDiscountApproved()
367364
: null;
365+
if (isDiscountOverrideDisallowed(loan) && (currentDiscount == null || currentDiscount.compareTo(discountAmount) != 0)) {
366+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
367+
.failWithCode("override.not.allowed.by.product");
368+
}
368369
if (discountAmount != null) {
369370
if (currentDiscount != null) {
370371
if (discountAmount.compareTo(currentDiscount) > 0) {
@@ -691,8 +692,8 @@ private void throwExceptionIfValidationWarningsExist(final List<ApiParameterErro
691692
}
692693
}
693694

694-
private boolean isDiscountOverrideAllowed(final WorkingCapitalLoan loan) {
695+
private boolean isDiscountOverrideDisallowed(final WorkingCapitalLoan loan) {
695696
return loan.getLoanProduct() == null || loan.getLoanProduct().getConfigurableAttributes() == null
696-
|| !loan.getLoanProduct().getConfigurableAttributes().isDiscountDefault();
697+
|| !loan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable();
697698
}
698699
}

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,7 @@ public WorkingCapitalLoanData retrieveOne(final Long loanId) {
173173

174174
@Override
175175
public WorkingCapitalLoanData retrieveOne(final ExternalId externalId) {
176-
final WorkingCapitalLoan loan = this.repository.findByExternalIdWithDetails(externalId)
177-
.orElseThrow(() -> new WorkingCapitalLoanNotFoundException(externalId));
178-
final WorkingCapitalLoan loanWithDetails = this.repository.findByIdWithFullDetails(loan.getId())
179-
.orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loan.getId()));
180-
WorkingCapitalLoanData data = this.mapper.toData(loanWithDetails);
181-
WorkingCapitalLoanCollectionData collectionData = workingCapitalLoanDelinquencyReadPlatformService.getCollectionData(loan.getId(),
182-
ThreadLocalContextUtil.getBusinessDate());
183-
data.setCollectionData(collectionData);
184-
enrichWithRateAndTerm(loanWithDetails, data);
185-
return data;
176+
return retrieveOne(repository.findIdByExternalId(externalId));
186177
}
187178

188179
private void enrichWithRateAndTerm(final WorkingCapitalLoan loan, final WorkingCapitalLoanData data) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public WorkingCapitalLoan assembleFrom(final JsonCommand command) {
138138
detail.setExpectedAmount(principal);
139139
loan.getDisbursementDetails().add(detail);
140140
}
141+
141142
loan.setProposedPrincipal(principal);
142143
loan.setApprovedPrincipal(BigDecimal.ZERO);
143144
final WorkingCapitalLoanBalance balance = WorkingCapitalLoanBalance.createFor(loan);
@@ -180,6 +181,11 @@ private WorkingCapitalLoanProductRelatedDetails buildLoanProductRelatedDetails(f
180181
detail.setDiscountProposed(fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.discountParamName, element)
181182
? fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanProductConstants.discountParamName, element, new HashSet<>())
182183
: null);
184+
if (detail.getDiscountProposed() == null && productDetail.getDiscount() != null
185+
&& productDetail.getDiscount().compareTo(BigDecimal.ZERO) > 0
186+
&& !product.getConfigurableAttributes().isDiscountDefaultOverridable()) {
187+
detail.setDiscountProposed(productDetail.getDiscount());
188+
}
183189
final Long breachId = fromApiJsonHelper.parameterExists(WorkingCapitalLoanProductConstants.breachIdParamName, element)
184190
? fromApiJsonHelper.extractLongNamed(WorkingCapitalLoanProductConstants.breachIdParamName, element)
185191
: null;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,14 @@ public WorkingCapitalLoanCommandTemplateData retrieveLoanTransactionTemplate(fin
6060
if (WorkingCapitalLoanConstants.APPROVE_LOAN_COMMAND.equals(command)) {
6161
return WorkingCapitalLoanCommandTemplateData.builder().approvalAmount(wcLoan.getProposedPrincipal())
6262
.approvalDate(expectedDisbursementDate).expectedDisbursementDate(expectedDisbursementDate)
63+
.discountAmount(wcLoan.getLoanProductRelatedDetails().getDiscountProposed())
64+
.overrideDiscountDisabled(!wcLoan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable())
6365
.currency(wcLoan.getLoanProduct().getCurrency().toData()).build();
6466
} else if (WorkingCapitalLoanConstants.DISBURSE_LOAN_COMMAND.equals(command)) {
6567
return WorkingCapitalLoanCommandTemplateData.builder().expectedAmount(wcLoan.getApprovedPrincipal())
6668
.expectedDisbursementDate(expectedDisbursementDate).currency(wcLoan.getLoanProduct().getCurrency().toData())
69+
.discountAmount(wcLoan.getLoanProductRelatedDetails().getDiscountApproved())
70+
.overrideDiscountDisabled(!wcLoan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable())
6771
.paymentTypeOptions(paymentTypeReadPlatformService.retrieveAllPaymentTypes())
6872
.classificationOptions(codeValueReadPlatformService
6973
.retrieveCodeValuesByCode(WorkingCapitalLoanConstants.DISBURSEMENT_CLASSIFICATION_CODE_NAME))

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC
146146
final BigDecimal discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
147147
command.parsedJson(), new HashSet<>());
148148
loan.getLoanProductRelatedDetails().setDiscountApproved(discount);
149+
} else if (!loan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable()) {
150+
loan.getLoanProductRelatedDetails().setDiscountApproved(loan.getLoanProductRelatedDetails().getDiscountProposed());
149151
}
150152

151153
// Keep first tranche expected amount aligned with approved principal (submit stores proposed principal only).
@@ -293,7 +295,12 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
293295

294296
// Discount amount (optional, can only be reduced per requirement)
295297
BigDecimal discount = null;
296-
if (command.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName)) {
298+
if (!loan.getLoanProduct().getConfigurableAttributes().isDiscountDefaultOverridable()) {
299+
// if default discount is NOT overridable, then we set the approved discount value as default.
300+
if (loan.getLoanProductRelatedDetails().getDiscountApproved() != null) {
301+
discount = loan.getLoanProductRelatedDetails().getDiscountApproved();
302+
}
303+
} else if (command.parameterExists(WorkingCapitalLoanConstants.discountAmountParamName)) {
297304
discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
298305
command.parsedJson(), new HashSet<>());
299306
if (discount != null) {
@@ -318,10 +325,10 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
318325
ExternalId discountTxnExternalId = null;
319326

320327
if (discount != null && discount.compareTo(BigDecimal.ZERO) > 0) {
321-
WorkingCapitalLoanTransaction discountTransaction = createAndPersistDiscountFeeTransaction(loan, disbursementTransaction, null,
322-
discount, actualDisbursementDate, null, null);
328+
discountTxnExternalId = this.externalIdFactory.create();
329+
WorkingCapitalLoanTransaction discountTransaction = createAndPersistDiscountFeeTransaction(loan, disbursementTransaction,
330+
discountTxnExternalId, discount, actualDisbursementDate, null, null);
323331
discountTransactionId = discountTransaction.getId();
324-
discountTxnExternalId = discountTransaction.getExternalId();
325332
changes.put(WorkingCapitalLoanConstants.discountAmountParamName, discount);
326333
}
327334
updateBalanceOnDisburse(loan, transactionAmount);

0 commit comments

Comments
 (0)