Skip to content

Commit d1fdabe

Browse files
committed
FINERACT-2455: WorkingCapital - % repayment modification options in the middle of the loan life cycle
1 parent 8442c2d commit d1fdabe

27 files changed

Lines changed: 1368 additions & 47 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
@@ -913,6 +913,14 @@ public CommandWrapperBuilder creditBalanceRefundWorkingCapitalLoanTransaction(fi
913913
return this;
914914
}
915915

916+
public CommandWrapperBuilder updatePeriodPaymentRateWorkingCapitalLoanApplication(final Long loanId) {
917+
this.actionName = "UPDATERATE";
918+
this.entityName = "WORKINGCAPITALLOAN";
919+
this.entityId = loanId;
920+
this.href = "/workingcapitalloans/" + loanId;
921+
return this;
922+
}
923+
916924
public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
917925
this.actionName = ACTION_CREATE;
918926
this.entityName = ENTITY_CLIENTIDENTIFIER;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ private WorkingCapitalLoanConstants() {
7777

7878
public static final String WRITE_OFF_REASONS = "WriteOffReasons";
7979
public static final String CHARGE_OFF_REASONS = "ChargeOffReasons";
80+
81+
// Period payment rate change parameters
82+
public static final String periodPaymentRateParamName = "periodPaymentRate";
8083
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@
5454
import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
5555
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData;
5656
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData;
57+
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanPeriodPaymentRateChangeData;
5758
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData;
5859
import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException;
5960
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService;
6061
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyReadPlatformService;
62+
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanPeriodPaymentRateChangeReadService;
6163
import org.springframework.data.domain.Page;
6264
import org.springframework.data.domain.Pageable;
6365
import org.springframework.stereotype.Component;
@@ -74,6 +76,7 @@ public class WorkingCapitalLoanApiResource {
7476
private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService;
7577
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
7678
private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService;
79+
private final WorkingCapitalLoanPeriodPaymentRateChangeReadService rateChangeReadService;
7780

7881
@GET
7982
@Path("template")
@@ -346,4 +349,64 @@ private CommandProcessingResult updateDiscount(final Long loanId, final String l
346349
.updateDiscountWorkingCapitalLoanApplication(resolvedLoanId).build();
347350
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
348351
}
352+
353+
@PUT
354+
@Path("{loanId}/payment-rate")
355+
@Consumes({ MediaType.APPLICATION_JSON })
356+
@Produces({ MediaType.APPLICATION_JSON })
357+
@Operation(operationId = "updateWorkingCapitalLoanRateById", summary = "Update period payment rate for an active Working Capital Loan", description = "Modifies the period payment rate and triggers schedule recalculation for the remaining term.")
358+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdRateRequest.class)))
359+
public CommandProcessingResult updatePeriodPaymentRateById(
360+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
361+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
362+
return updatePeriodPaymentRate(loanId, null, apiRequestBodyAsJson);
363+
}
364+
365+
@PUT
366+
@Path("external-id/{loanExternalId}/payment-rate")
367+
@Consumes({ MediaType.APPLICATION_JSON })
368+
@Produces({ MediaType.APPLICATION_JSON })
369+
@Operation(operationId = "updateWorkingCapitalLoanRateByExternalId", summary = "Update period payment rate for an active Working Capital Loan by external id", description = "Modifies the period payment rate and triggers schedule recalculation for the remaining term.")
370+
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdRateRequest.class)))
371+
public CommandProcessingResult updatePeriodPaymentRateByExternalId(
372+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
373+
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
374+
return updatePeriodPaymentRate(null, loanExternalId, apiRequestBodyAsJson);
375+
}
376+
377+
private CommandProcessingResult updatePeriodPaymentRate(final Long loanId, final String loanExternalIdStr,
378+
final String apiRequestBodyAsJson) {
379+
final Long resolvedLoanId = loanId != null ? loanId
380+
: readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
381+
if (resolvedLoanId == null) {
382+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
383+
}
384+
final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson)
385+
.updatePeriodPaymentRateWorkingCapitalLoanApplication(resolvedLoanId).build();
386+
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
387+
}
388+
389+
@GET
390+
@Path("{loanId}/rate-changes")
391+
@Produces({ MediaType.APPLICATION_JSON })
392+
@Operation(operationId = "getWorkingCapitalLoanRateChangeHistoryById", summary = "Retrieve rate change history for a Working Capital Loan", description = "Returns all rate change records for the loan, ordered by most recent first.")
393+
public List<WorkingCapitalLoanPeriodPaymentRateChangeData> getRateChangeHistoryById(
394+
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId) {
395+
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
396+
return this.rateChangeReadService.retrieveRateChangeHistory(loanId);
397+
}
398+
399+
@GET
400+
@Path("external-id/{loanExternalId}/rate-changes")
401+
@Produces({ MediaType.APPLICATION_JSON })
402+
@Operation(operationId = "getWorkingCapitalLoanRateChangeHistoryByExternalId", summary = "Retrieve rate change history for a Working Capital Loan by external id", description = "Returns all rate change records for the loan, ordered by most recent first.")
403+
public List<WorkingCapitalLoanPeriodPaymentRateChangeData> getRateChangeHistoryByExternalId(
404+
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId) {
405+
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
406+
final Long resolvedLoanId = readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalId));
407+
if (resolvedLoanId == null) {
408+
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalId));
409+
}
410+
return this.rateChangeReadService.retrieveRateChangeHistory(resolvedLoanId);
411+
}
349412
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,4 +609,19 @@ private GetWorkingCapitalLoanDelinquencyRangeScheduleTagHistoryResponse() {}
609609
public BigDecimal delinquentAmount;
610610
}
611611

612+
@Schema(description = "Request for updating period payment rate on an active Working Capital Loan")
613+
public static final class PutWorkingCapitalLoansLoanIdRateRequest {
614+
615+
private PutWorkingCapitalLoansLoanIdRateRequest() {}
616+
617+
@Schema(example = "0.17", description = "New period payment rate")
618+
public BigDecimal periodPaymentRate;
619+
620+
@Schema(example = "Rate change note")
621+
public String note;
622+
623+
@Schema(example = "en_GB")
624+
public String locale;
625+
}
626+
612627
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,12 @@ public void applyPayment(@NonNull final ProjectedAmortizationScheduleModel model
5454
@NonNull final BigDecimal paymentAmount) {
5555
model.applyPayment(paymentDate, paymentAmount);
5656
}
57+
58+
@Override
59+
@NonNull
60+
public ProjectedAmortizationScheduleModel applyRateChange(@NonNull final ProjectedAmortizationScheduleModel model,
61+
@NonNull final BigDecimal newPeriodPaymentRate, @NonNull final LocalDate rateChangeDate) {
62+
model.applyRateChange(newPeriodPaymentRate, rateChangeDate);
63+
return model;
64+
}
5765
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,20 @@ ProjectedAmortizationScheduleModel addDisbursement(@NonNull ProjectedAmortizatio
7070
* actual payment amount
7171
*/
7272
void applyPayment(@NonNull ProjectedAmortizationScheduleModel model, @NonNull LocalDate paymentDate, @NonNull BigDecimal paymentAmount);
73+
74+
/**
75+
* Applies a rate change to the model in-place. Adds a {@link ProjectedAmortizationScheduleModel.RateSegment} and
76+
* rebuilds the payment list.
77+
*
78+
* @param model
79+
* the model to mutate
80+
* @param newPeriodPaymentRate
81+
* the new period payment rate
82+
* @param rateChangeDate
83+
* effective date of the rate change
84+
* @return the same model instance (mutated)
85+
*/
86+
@NonNull
87+
ProjectedAmortizationScheduleModel applyRateChange(@NonNull ProjectedAmortizationScheduleModel model,
88+
@NonNull BigDecimal newPeriodPaymentRate, @NonNull LocalDate rateChangeDate);
7389
}

0 commit comments

Comments
 (0)