Skip to content

Commit 38d6c50

Browse files
FINERACT-2455: WC - Discount
1 parent 42432df commit 38d6c50

12 files changed

Lines changed: 524 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
@@ -613,6 +613,14 @@ public CommandWrapperBuilder undoWorkingCapitalLoanApplicationDisbursal(final Lo
613613
return this;
614614
}
615615

616+
public CommandWrapperBuilder updateDiscountWorkingCapitalLoanApplication(final Long loanId) {
617+
this.actionName = "UPDATEDISCOUNT";
618+
this.entityName = "WORKINGCAPITALLOAN";
619+
this.entityId = loanId;
620+
this.href = "/workingcapitalloans/" + loanId;
621+
return this;
622+
}
623+
616624
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
617625
this.actionName = "CREATE";
618626
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
@@ -480,4 +480,22 @@ private PostWorkingCapitalLoansLoanIdRequest() {}
480480
@Schema(description = "Payment details (Account No, Cheque No, Routing Code, Receipt No, Bank code)")
481481
public PostWorkingCapitalLoansLoanIdDisbursementPaymentDetails paymentDetails;
482482
}
483+
484+
@Schema(description = "Request for updating discount on a disbursed Working Capital Loan")
485+
public static final class PutWorkingCapitalLoansLoanIdDiscountRequest {
486+
487+
private PutWorkingCapitalLoansLoanIdDiscountRequest() {}
488+
489+
@Schema(example = "0.0", description = "Discount amount")
490+
public BigDecimal discountAmount;
491+
492+
@Schema(example = "Discount update Note")
493+
public String note;
494+
495+
@Schema(example = "en_GB")
496+
public String locale;
497+
498+
@Schema(example = "dd MMMM yyyy")
499+
public String dateFormat;
500+
}
483501
}
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 AddDiscountWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {
34+
35+
private final WorkingCapitalLoanWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return this.writePlatformService.addDiscount(command.entityId(), command);
41+
}
42+
}

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

Lines changed: 47 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,51 @@ public void validateUndoDisbursal(final String json) {
351353
throwExceptionIfValidationWarningsExist(dataValidationErrors);
352354
}
353355

356+
public void validateAddDiscount(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 BigDecimal discountAmount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
370+
element, new HashSet<>());
371+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName).value(discountAmount).notNull()
372+
.zeroOrPositiveAmount();
373+
final BigDecimal currentDiscount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount()
374+
: null;
375+
if (discountAmount != null && currentDiscount != null && discountAmount.compareTo(currentDiscount) > 0) {
376+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.discountAmountParamName)
377+
.failWithCode("amount.cannot.exceed.created.discount");
378+
}
379+
380+
final LocalDate actualDisbursementDate = loan.getDisbursementDetails() != null && !loan.getDisbursementDetails().isEmpty()
381+
? loan.getDisbursementDetails().getFirst().getActualDisbursementDate()
382+
: null;
383+
if (actualDisbursementDate == null) {
384+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.actualDisbursementDateParamName)
385+
.failWithCode("loan.not.disbursed");
386+
}
387+
388+
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
389+
if (actualDisbursementDate != null && !actualDisbursementDate.equals(businessDate)) {
390+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.actualDisbursementDateParamName).value(businessDate)
391+
.failWithCode("transaction.date.must.be.equal.disbursement.date");
392+
}
393+
394+
final String note = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.noteParamName, element);
395+
baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.noteParamName).value(note).ignoreIfNull()
396+
.notExceedingLengthOf(NOTE_MAX_LENGTH);
397+
398+
throwExceptionIfValidationWarningsExist(dataValidationErrors);
399+
}
400+
354401
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
355402
if (!dataValidationErrors.isEmpty()) {
356403
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 addDiscount(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.workingcapitalloan.WorkingCapitalLoanConstants;
4445
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
@@ -285,6 +286,41 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand
285286
.build();
286287
}
287288

289+
@Override
290+
public CommandProcessingResult addDiscount(final Long loanId, final JsonCommand command) {
291+
final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
292+
.orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId));
293+
this.validator.validateAddDiscount(command.json(), loan);
294+
295+
if (loan.getLoanStatus() != LoanStatus.ACTIVE) {
296+
throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed",
297+
"Add discount is allowed only for disbursed (active) loans", "loanStatus");
298+
}
299+
300+
ensureDiscountNotAlreadySetBeforeDisbursement(loan);
301+
302+
final BigDecimal discount = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.discountAmountParamName,
303+
command.parsedJson(), new HashSet<>());
304+
if (discount != null) {
305+
loan.getLoanProductRelatedDetails().setDiscount(discount);
306+
}
307+
updateBalanceForDiscountChange(loan);
308+
309+
final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName);
310+
createNote(noteText, loan);
311+
this.loanRepository.saveAndFlush(loan);
312+
313+
final Map<String, Object> changes = new LinkedHashMap<>();
314+
changes.put(WorkingCapitalLoanConstants.discountAmountParamName, discount);
315+
if (StringUtils.isNotBlank(noteText)) {
316+
changes.put(WorkingCapitalLoanConstants.noteParamName, noteText);
317+
}
318+
319+
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId)
320+
.withEntityExternalId(loan.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
321+
.withLoanId(loanId).with(changes).build();
322+
}
323+
288324
@Override
289325
public CommandProcessingResult undoDisbursal(final Long loanId, final JsonCommand command) {
290326
final WorkingCapitalLoan loan = this.loanRepository.findById(loanId)
@@ -379,7 +415,24 @@ private void updateBalanceOnDisburse(final WorkingCapitalLoan loan, final BigDec
379415
if (balance == null) {
380416
balance = WorkingCapitalLoanBalance.createFor(loan);
381417
}
382-
balance.setPrincipalOutstanding(disbursedAmount);
418+
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
419+
? loan.getLoanProductRelatedDetails().getDiscount()
420+
: BigDecimal.ZERO;
421+
balance.setPrincipalOutstanding(disbursedAmount.add(discount));
422+
this.balanceRepository.saveAndFlush(balance);
423+
}
424+
425+
private void updateBalanceForDiscountChange(final WorkingCapitalLoan loan) {
426+
final WorkingCapitalLoanBalance balance = this.balanceRepository.findByWcLoan_Id(loan.getId())
427+
.orElseGet(() -> WorkingCapitalLoanBalance.createFor(loan));
428+
final BigDecimal discount = loan.getLoanProductRelatedDetails() != null && loan.getLoanProductRelatedDetails().getDiscount() != null
429+
? loan.getLoanProductRelatedDetails().getDiscount()
430+
: BigDecimal.ZERO;
431+
final BigDecimal disbursedAmount = loan.getDisbursementDetails() != null && !loan.getDisbursementDetails().isEmpty()
432+
&& loan.getDisbursementDetails().getFirst().getActualAmount() != null
433+
? loan.getDisbursementDetails().getFirst().getActualAmount()
434+
: BigDecimal.ZERO;
435+
balance.setPrincipalOutstanding(disbursedAmount.add(discount));
383436
this.balanceRepository.saveAndFlush(balance);
384437
}
385438

@@ -410,4 +463,19 @@ private void createNote(final String noteText, final WorkingCapitalLoan loan) {
410463
this.noteRepository.save(note);
411464
}
412465
}
466+
467+
private void ensureDiscountNotAlreadySetBeforeDisbursement(final WorkingCapitalLoan loan) {
468+
final BigDecimal productDefaultDiscount = loan.getLoanProduct() != null && loan.getLoanProduct().getRelatedDetail() != null
469+
? loan.getLoanProduct().getRelatedDetail().getDiscount()
470+
: null;
471+
final BigDecimal loanDiscount = loan.getLoanProductRelatedDetails() != null ? loan.getLoanProductRelatedDetails().getDiscount()
472+
: null;
473+
final boolean equalByValue = productDefaultDiscount == null ? loanDiscount == null
474+
: loanDiscount != null && productDefaultDiscount.compareTo(loanDiscount) == 0;
475+
if (!equalByValue) {
476+
throw new PlatformApiDataValidationException("validation.msg.wc.loan.discount.already.set.before.disbursement",
477+
"Discount was already set before disbursement and cannot be added again",
478+
WorkingCapitalLoanConstants.discountAmountParamName);
479+
}
480+
}
413481
}

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
@@ -34,4 +34,5 @@
3434
<include relativeToChangelogFile="true" file="parts/0010_loan_account_permissions.xml"/>
3535
<include relativeToChangelogFile="true" file="parts/0011_wc_loan_transaction.xml"/>
3636
<include relativeToChangelogFile="true" file="parts/0012_wc_loan_disbursement_permissions.xml"/>
37+
<include relativeToChangelogFile="true" file="parts/0013_wc_loan_discount_permissions.xml"/>
3738
</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-0010-1-add-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)