Skip to content

Commit 135e49c

Browse files
FINERACT-2455: WC - Discount
1 parent 83fcc5c commit 135e49c

12 files changed

Lines changed: 559 additions & 2 deletions

File tree

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,14 @@ public CommandWrapperBuilder createWorkingCapitalLoanDelinquencyAction(final Lon
638638
return this;
639639
}
640640

641+
public CommandWrapperBuilder updateDiscountWorkingCapitalLoanApplication(final Long loanId) {
642+
this.actionName = "UPDATEDISCOUNT";
643+
this.entityName = "WORKINGCAPITALLOAN";
644+
this.entityId = loanId;
645+
this.href = "/workingcapitalloans/" + loanId;
646+
return this;
647+
}
648+
641649
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
642650
this.actionName = "CREATE";
643651
this.entityName = "CLIENTIDENTIFIER";

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,43 @@ private CommandProcessingResult handleStateTransition(final Long loanId, final S
273273

274274
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
275275
}
276+
277+
@PUT
278+
@Path("{loanId}/discount")
279+
@Consumes({ MediaType.APPLICATION_JSON })
280+
@Produces({ MediaType.APPLICATION_JSON })
281+
@Operation(operationId = "updateWorkingCapitalLoanDiscountById", summary = "Update discount for a disbursed Working Capital Loan", description = "Discount can be added one time after disbursement and only on disbursement date.")
282+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdDiscountRequest.class)))
283+
@ApiResponses({
284+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdResponse.class))) })
285+
public CommandProcessingResult updateDiscountById(
286+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
287+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
288+
return updateDiscount(loanId, null, apiRequestBodyAsJson);
289+
}
290+
291+
@PUT
292+
@Path("external-id/{loanExternalId}/discount")
293+
@Consumes({ MediaType.APPLICATION_JSON })
294+
@Produces({ MediaType.APPLICATION_JSON })
295+
@Operation(operationId = "updateWorkingCapitalLoanDiscountByExternalId", summary = "Update discount for a disbursed Working Capital Loan by external id", description = "Discount can be added one time after disbursement and only on disbursement date.")
296+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdDiscountRequest.class)))
297+
@ApiResponses({
298+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdResponse.class))) })
299+
public CommandProcessingResult updateDiscountByExternalId(
300+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
301+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
302+
return updateDiscount(null, loanExternalId, apiRequestBodyAsJson);
303+
}
304+
305+
private CommandProcessingResult updateDiscount(final Long loanId, final String loanExternalIdStr, final String apiRequestBodyAsJson) {
306+
final Long resolvedLoanId = loanId != null ? loanId
307+
: readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
308+
if (resolvedLoanId == null) {
309+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
310+
}
311+
final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson)
312+
.updateDiscountWorkingCapitalLoanApplication(resolvedLoanId).build();
313+
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
314+
}
276315
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,22 @@ private PostWorkingCapitalLoansLoanIdRequest() {}
500500
@Schema(description = "Payment details (Account No, Cheque No, Routing Code, Receipt No, Bank code)")
501501
public PostWorkingCapitalLoansLoanIdDisbursementPaymentDetails paymentDetails;
502502
}
503+
504+
@Schema(description = "Request for updating discount on a disbursed Working Capital Loan")
505+
public static final class PutWorkingCapitalLoansLoanIdDiscountRequest {
506+
507+
private PutWorkingCapitalLoansLoanIdDiscountRequest() {}
508+
509+
@Schema(example = "0.0", description = "Discount amount")
510+
public BigDecimal discountAmount;
511+
512+
@Schema(example = "Discount update Note")
513+
public String note;
514+
515+
@Schema(example = "en_GB")
516+
public String locale;
517+
518+
@Schema(example = "dd MMMM yyyy")
519+
public String dateFormat;
520+
}
503521
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.workingcapitalloan.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "WORKINGCAPITALLOAN", action = "UPDATEDISCOUNT")
33+
public class UpdateDiscountWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {
34+
35+
private final WorkingCapitalLoanWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return this.writePlatformService.updateDiscount(command.entityId(), command);
41+
}
42+
}

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public class WorkingCapitalLoanDataValidator {
7979

8080
private static final Set<String> UNDO_DISBURSAL_SUPPORTED_PARAMETERS = new HashSet<>(
8181
Arrays.asList("locale", "dateFormat", WorkingCapitalLoanConstants.noteParamName));
82+
private static final Set<String> ADD_DISCOUNT_SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList("locale", "dateFormat",
83+
WorkingCapitalLoanConstants.discountAmountParamName, WorkingCapitalLoanConstants.noteParamName));
8284

8385
private static final int NOTE_MAX_LENGTH = 1000;
8486
private static final int EXTERNAL_ID_MAX_LENGTH = 100;
@@ -351,6 +353,58 @@ public void validateUndoDisbursal(final String json) {
351353
throwExceptionIfValidationWarningsExist(dataValidationErrors);
352354
}
353355

356+
public void validateUpdateDiscount(final String json, final WorkingCapitalLoan loan) {
357+
if (StringUtils.isBlank(json)) {
358+
throw new InvalidJsonException();
359+
}
360+
361+
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
362+
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, ADD_DISCOUNT_SUPPORTED_PARAMETERS);
363+
364+
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
365+
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
366+
.resource(WorkingCapitalLoanConstants.RESOURCE_NAME);
367+
final JsonElement element = this.fromApiJsonHelper.parse(json);
368+
369+
final boolean discountOverrideAllowed = loan.getLoanProduct() != null && loan.getLoanProduct().getConfigurableAttributes() != null
370+
&& loan.getLoanProduct().getConfigurableAttributes().isDiscountDefault();
371+
if (!discountOverrideAllowed) {
372+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
373+
.failWithCode("override.not.allowed.by.product");
374+
}
375+
376+
final BigDecimal discountAmount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
377+
element, new HashSet<>());
378+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).notNull()
379+
.zeroOrPositiveAmount();
380+
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount()
381+
: null;
382+
if (discountAmount != null && currentDiscount != null && discountAmount.compareTo(currentDiscount) > 0) {
383+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
384+
.failWithCode("amount.cannot.exceed.created.discount");
385+
}
386+
387+
final LocalDate actualDisbursementDate = loan.getDisbursementDetails() != null && !loan.getDisbursementDetails().isEmpty()
388+
? loan.getDisbursementDetails().getFirst().getActualDisbursementDate()
389+
: null;
390+
if (actualDisbursementDate == null) {
391+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.actualDisbursementDateParamName)
392+
.failWithCode("loan.not.disbursed");
393+
}
394+
395+
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
396+
if (actualDisbursementDate != null && !actualDisbursementDate.equals(businessDate)) {
397+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.actualDisbursementDateParamName).value(businessDate)
398+
.failWithCode("transaction.date.must.be.equal.disbursement.date");
399+
}
400+
401+
final String note = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.noteParamName, element);
402+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull()
403+
.notExceedingLengthOf(NOTE_MAX_LENGTH);
404+
405+
throwExceptionIfValidationWarningsExist(dataValidationErrors);
406+
}
407+
354408
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
355409
if (!dataValidationErrors.isEmpty()) {
356410
throw new PlatformApiDataValidationException(dataValidationErrors);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ public interface WorkingCapitalLoanWritePlatformService {
3232
CommandProcessingResult disburseLoan(Long loanId, JsonCommand command);
3333

3434
CommandProcessingResult undoDisbursal(Long loanId, JsonCommand command);
35+
36+
CommandProcessingResult updateDiscount(Long loanId, JsonCommand command);
3537
}

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
4040
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
4141
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
42+
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
4243
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
4344
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
4445
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
@@ -352,6 +353,41 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman
352353
.build();
353354
}
354355

356+
@Override
357+
public CommandProcessingResult updateDiscount(final Long loanId, final JsonCommand command) {
358+
final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
359+
.orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId));
360+
this.validator.validateUpdateDiscount(command.json(), loan);
361+
362+
if (loan.getLoanStatus() != LoanStatus.ACTIVE) {
363+
throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed",
364+
"Add discount is allowed only for disbursed (active) loans", "loanStatus");
365+
}
366+
367+
ensureDiscountNotAlreadySetBeforeDisbursement(loan);
368+
369+
final BigDecimal discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
370+
command.parsedJson(), new HashSet<>());
371+
if (discount != null) {
372+
loan.getLoanProductRelatedDetails().setDiscount(discount);
373+
}
374+
updateBalanceForDiscountChange(loan);
375+
376+
final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName);
377+
createNote(noteText, loan);
378+
this.loanRepository.saveAndFlush(loan);
379+
380+
final Map<String, Object> changes = new LinkedHashMap<>();
381+
changes.put(WorkingCapitalLoanConstants.discountAmountParamName, discount);
382+
if (StringUtils.isNotBlank(noteText)) {
383+
changes.put(WorkingCapitalLoanConstants.noteParamName, noteText);
384+
}
385+
386+
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId)
387+
.withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
388+
.withLoanId(loanId).with(changes).build();
389+
}
390+
355391
private PaymentDetail createAndPersistPaymentDetailFromCommand(final JsonCommand command, final Map<String, Object> changes) {
356392
final JsonElement paymentDetailsElement = command.jsonElement(WorkingCapitalLoanConstants.paymentDetailsParamName);
357393
if (paymentDetailsElement != null && paymentDetailsElement.isJsonObject()) {
@@ -366,7 +402,24 @@ private void updateBalanceOnDisburse(final WorkingCapitalLoan loan, final BigDec
366402
if (balance == null) {
367403
balance = WorkingCapitalLoanBalance.createFor(loan);
368404
}
369-
balance.setPrincipalOutstanding(disbursedAmount);
405+
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
406+
? loan.getLoanProductRelatedDetails().getDiscount()
407+
: BigDecimal.ZERO;
408+
balance.setPrincipalOutstanding(disbursedAmount.add(discount));
409+
this.balanceRepository.saveAndFlush(balance);
410+
}
411+
412+
private void updateBalanceForDiscountChange(final WorkingCapitalLoan loan) {
413+
final WorkingCapitalLoanBalance balance = this.balanceRepository.findByWcLoan_Id(loan.getId())
414+
.orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan));
415+
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
416+
? loan.getLoanProductRelatedDetails().getDiscount()
417+
: BigDecimal.ZERO;
418+
final BigDecimal disbursedAmount = loan.getDisbursementDetails() != null && !loan.getDisbursementDetails().isEmpty()
419+
&& loan.getDisbursementDetails().getFirst().getActualAmount() != null
420+
? loan.getDisbursementDetails().getFirst().getActualAmount()
421+
: BigDecimal.ZERO;
422+
balance.setPrincipalOutstanding(disbursedAmount.add(discount));
370423
this.balanceRepository.saveAndFlush(balance);
371424
}
372425

@@ -396,4 +449,19 @@ private void createNote(final String noteText, final WorkingCapitalLoan loan) {
396449
this.noteRepository.save(note);
397450
}
398451
}
452+
453+
private void ensureDiscountNotAlreadySetBeforeDisbursement(final WorkingCapitalLoan loan) {
454+
final BigDecimal productDefaultDiscount = loan.getLoanProduct() != null && loan.getLoanProduct().getRelatedDetail() != null
455+
? loan.getLoanProduct().getRelatedDetail().getDiscount()
456+
: null;
457+
final BigDecimal loanDiscount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount()
458+
: null;
459+
final boolean equalByValue = productDefaultDiscount == null ? loanDiscount == null
460+
: loanDiscount != null && productDefaultDiscount.compareTo(loanDiscount) == 0;
461+
if (!equalByValue) {
462+
throw new PlatformApiDataValidationException("validation.msg.wc.loan.discount.already.set.before.disbursement",
463+
"Discount was already set before disbursement and cannot be added again",
464+
WorkingCapitalLoanConstants.discountAmountParamName);
465+
}
466+
}
399467
}

fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@
3939
<include relativeToChangelogFile="true" file="parts/0015_wc_loan_delinquency_action.xml"/>
4040
<include relativeToChangelogFile="true" file="parts/0016_configurable_attributes_not_null.xml"/>
4141
<include relativeToChangelogFile="true" file="parts/0017_wc_loan_breach_management.xml"/>
42+
<include relativeToChangelogFile="true" file="parts/0018_wc_loan_discount_permissions.xml"/>
4243
</databaseChangeLog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Licensed to the Apache Software Foundation (ASF) under one
5+
or more contributor license agreements. See the NOTICE file
6+
distributed with this work for additional information
7+
regarding copyright ownership. The ASF licenses this file
8+
to you under the Apache License, Version 2.0 (the
9+
"License"); you may not use this file except in compliance
10+
with the License. You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing,
15+
software distributed under the License is distributed on an
16+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
KIND, either express or implied. See the License for the
18+
specific language governing permissions and limitations
19+
under the License.
20+
21+
-->
22+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
23+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
24+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
25+
26+
<changeSet author="fineract" id="wcl-0018-1-update-discount-permission">
27+
<preConditions onFail="MARK_RAN">
28+
<sqlCheck expectedResult="0">
29+
SELECT COUNT(*) FROM m_permission WHERE code = 'UPDATEDISCOUNT_WORKINGCAPITALLOAN';
30+
</sqlCheck>
31+
</preConditions>
32+
<insert tableName="m_permission">
33+
<column name="grouping" value="portfolio"/>
34+
<column name="code" value="UPDATEDISCOUNT_WORKINGCAPITALLOAN"/>
35+
<column name="entity_name" value="WORKINGCAPITALLOAN"/>
36+
<column name="action_name" value="UPDATEDISCOUNT"/>
37+
<column name="can_maker_checker" valueBoolean="false"/>
38+
</insert>
39+
</changeSet>
40+
41+
</databaseChangeLog>

0 commit comments

Comments
 (0)