Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,14 @@ public CommandWrapperBuilder updatePeriodPaymentRateWorkingCapitalLoanApplicatio
return this;
}

public CommandWrapperBuilder updateNearBreachConfigWorkingCapitalLoan(final Long loanId) {
this.actionName = "UPDATENEARBREACH";
this.entityName = "WORKINGCAPITALLOAN";
this.entityId = loanId;
this.href = "/working-capital-loans/" + loanId + "/near-breach-config";
return this;
}

public CommandWrapperBuilder createClientIdentifier(final Long clientId) {
this.actionName = ACTION_CREATE;
this.entityName = ENTITY_CLIENTIDENTIFIER;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public WorkingCapitalLoan execute(final WorkingCapitalLoan loan) {
}

final WorkingCapitalLoanProductRelatedDetails details = loan.getLoanProductRelatedDetails();
if (details == null || details.getNearBreach() == null) {
final boolean hasConfig = details != null && (details.getNearBreach() != null || details.getNearBreachThresholdOverride() != null);
if (!hasConfig) {
log.debug("Skipping near breach evaluation for WC loan {} - no near breach configuration", loan.getId());
return loan;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,9 @@ private WorkingCapitalLoanConstants() {
// Period payment rate change parameters
public static final String periodPaymentRateParamName = "periodPaymentRate";
public static final String previousPeriodPaymentRateParamName = "previousRate";

// Near breach change parameters
public static final String nearBreachThresholdParamName = "nearBreachThreshold";
public static final String nearBreachFrequencyParamName = "nearBreachFrequency";
public static final String nearBreachFrequencyTypeParamName = "nearBreachFrequencyType";
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@
import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanNearBreachChangeData;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanPeriodPaymentRateChangeData;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanTemplateData;
import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanDelinquencyReadPlatformService;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanNearBreachChangeReadService;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanPeriodPaymentRateChangeReadService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -77,6 +79,7 @@ public class WorkingCapitalLoanApiResource {
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final WorkingCapitalLoanDelinquencyReadPlatformService workingCapitalLoanDelinquencyReadPlatformService;
private final WorkingCapitalLoanPeriodPaymentRateChangeReadService rateChangeReadService;
private final WorkingCapitalLoanNearBreachChangeReadService nearBreachChangeReadService;

@GET
@Path("template")
Expand Down Expand Up @@ -409,4 +412,64 @@ public List<WorkingCapitalLoanPeriodPaymentRateChangeData> getRateChangeHistoryB
}
return this.rateChangeReadService.retrieveRateChangeHistory(resolvedLoanId);
}

@PUT
@Path("{loanId}/near-breach-config")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "updateWorkingCapitalLoanNearBreachConfigById", summary = "Update near breach configuration for an active Working Capital Loan", description = "Overrides the near breach threshold and frequency for the loan. Applies only to future evaluation periods; history is preserved.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdNearBreachConfigRequest.class)))
public CommandProcessingResult updateNearBreachConfigById(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return updateNearBreachConfig(loanId, null, apiRequestBodyAsJson);
}

@PUT
@Path("external-id/{loanExternalId}/near-breach-config")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "updateWorkingCapitalLoanNearBreachConfigByExternalId", summary = "Update near breach configuration for an active Working Capital Loan by external id", description = "Overrides the near breach threshold and frequency for the loan. Applies only to future evaluation periods; history is preserved.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanApiResourceSwagger.PutWorkingCapitalLoansLoanIdNearBreachConfigRequest.class)))
public CommandProcessingResult updateNearBreachConfigByExternalId(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return updateNearBreachConfig(null, loanExternalId, apiRequestBodyAsJson);
}

private CommandProcessingResult updateNearBreachConfig(final Long loanId, final String loanExternalIdStr,
final String apiRequestBodyAsJson) {
final Long resolvedLoanId = loanId != null ? loanId
: readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalIdStr));
if (resolvedLoanId == null) {
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalIdStr));
}
final CommandWrapper commandRequest = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson)
.updateNearBreachConfigWorkingCapitalLoan(resolvedLoanId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@GET
@Path("{loanId}/near-breach-changes")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "getWorkingCapitalLoanNearBreachChangeHistoryById", summary = "Retrieve near breach configuration change history for a Working Capital Loan", description = "Returns all near breach configuration change records for the loan, ordered by most recent first.")
public List<WorkingCapitalLoanNearBreachChangeData> getNearBreachChangeHistoryById(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.nearBreachChangeReadService.retrieveNearBreachChangeHistory(loanId);
}

@GET
@Path("external-id/{loanExternalId}/near-breach-changes")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "getWorkingCapitalLoanNearBreachChangeHistoryByExternalId", summary = "Retrieve near breach configuration change history for a Working Capital Loan by external id", description = "Returns all near breach configuration change records for the loan, ordered by most recent first.")
public List<WorkingCapitalLoanNearBreachChangeData> getNearBreachChangeHistoryByExternalId(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Long resolvedLoanId = readPlatformService.getResolvedLoanId(ExternalIdFactory.produce(loanExternalId));
if (resolvedLoanId == null) {
throw new WorkingCapitalLoanNotFoundException(ExternalIdFactory.produce(loanExternalId));
}
return this.nearBreachChangeReadService.retrieveNearBreachChangeHistory(resolvedLoanId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -667,4 +667,26 @@ private PutWorkingCapitalLoansLoanIdRateRequest() {}
public String locale;
}

@Schema(description = "Request for updating near breach configuration on an active Working Capital Loan")
public static final class PutWorkingCapitalLoansLoanIdNearBreachConfigRequest {

private PutWorkingCapitalLoansLoanIdNearBreachConfigRequest() {}

@Schema(example = "40.0", requiredMode = Schema.RequiredMode.REQUIRED, description = "New near breach threshold percentage (must be > 0 and <= 100)")
public BigDecimal nearBreachThreshold;

@Schema(example = "7", requiredMode = Schema.RequiredMode.REQUIRED, description = "New near breach frequency (must be > 0)")
public Integer nearBreachFrequency;

@Schema(example = "DAYS", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = { "DAYS", "WEEKS",
"MONTHS" }, description = "New near breach frequency type")
public String nearBreachFrequencyType;

@Schema(example = "Near breach config change note", description = "Optional note (max 1000 characters)")
public String note;

@Schema(example = "en_GB")
public String locale;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.workingcapitalloan.data;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.OffsetDateTime;

public record WorkingCapitalLoanNearBreachChangeData(Long id, Long loanId, LocalDate effectiveDate, BigDecimal previousThreshold,
BigDecimal newThreshold, Integer previousFrequency, Integer newFrequency, String previousFrequencyType, String newFrequencyType,
boolean reversed, LocalDate reversedOnDate, OffsetDateTime createdDate) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.workingcapitalloan.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "m_wc_loan_near_breach_change")
public class WorkingCapitalLoanNearBreachChange extends AbstractAuditableWithUTCDateTimeCustom<Long> {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "wc_loan_id", nullable = false)
private WorkingCapitalLoan workingCapitalLoan;

@Column(name = "effective_date", nullable = false)
private LocalDate effectiveDate;

@Column(name = "previous_threshold", scale = 6, precision = 19, nullable = false)
private BigDecimal previousThreshold;

@Column(name = "new_threshold", scale = 6, precision = 19, nullable = false)
private BigDecimal newThreshold;

@Column(name = "previous_frequency", nullable = false)
private Integer previousFrequency;

@Column(name = "new_frequency", nullable = false)
private Integer newFrequency;

@Enumerated(EnumType.STRING)
@Column(name = "previous_frequency_type", nullable = false, length = 50)
private WorkingCapitalLoanPeriodFrequencyType previousFrequencyType;

@Enumerated(EnumType.STRING)
@Column(name = "new_frequency_type", nullable = false, length = 50)
private WorkingCapitalLoanPeriodFrequencyType newFrequencyType;

@Column(name = "is_reversed", nullable = false)
private boolean reversed;

@Column(name = "reversed_on_date")
private LocalDate reversedOnDate;

@Version
private int version;

public static WorkingCapitalLoanNearBreachChange create(final WorkingCapitalLoan loan, final LocalDate effectiveDate,
final BigDecimal previousThreshold, final BigDecimal newThreshold, final Integer previousFrequency, final Integer newFrequency,
final WorkingCapitalLoanPeriodFrequencyType previousFrequencyType,
final WorkingCapitalLoanPeriodFrequencyType newFrequencyType) {
final WorkingCapitalLoanNearBreachChange change = new WorkingCapitalLoanNearBreachChange();
change.workingCapitalLoan = loan;
change.effectiveDate = effectiveDate;
change.previousThreshold = previousThreshold;
change.newThreshold = newThreshold;
change.previousFrequency = previousFrequency;
change.newFrequency = newFrequency;
change.previousFrequencyType = previousFrequencyType;
change.newFrequencyType = newFrequencyType;
change.reversed = false;
return change;
}

public void reverse(final LocalDate reversalDate) {
this.reversed = true;
this.reversedOnDate = reversalDate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.workingcapitalloan.handler;

import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@CommandType(entity = "WORKINGCAPITALLOAN", action = "UPDATENEARBREACH")
public class UpdateNearBreachWorkingCapitalLoanCommandHandler implements NewCommandSourceHandler {

private final WorkingCapitalLoanWritePlatformService writePlatformService;

@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.updateNearBreachConfig(command.entityId(), command);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.workingcapitalloan.repository;

import java.util.List;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachChange;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface WorkingCapitalLoanNearBreachChangeRepository extends JpaRepository<WorkingCapitalLoanNearBreachChange, Long> {

List<WorkingCapitalLoanNearBreachChange> findByWorkingCapitalLoanIdOrderByCreatedDateDesc(Long loanId);

List<WorkingCapitalLoanNearBreachChange> findByWorkingCapitalLoanIdAndReversedFalse(Long loanId);
}
Loading