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 @@ -162,6 +162,7 @@
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyActionsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanDelinquencyRangeScheduleApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanInternalCobApiApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanNearBreachActionsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoanTransactionsApi;
import org.apache.fineract.client.feign.services.WorkingCapitalLoansApi;
Expand Down Expand Up @@ -807,6 +808,10 @@ public WorkingCapitalNearBreachApi workingCapitalNearBreaches() {
return create(WorkingCapitalNearBreachApi.class);
}

public WorkingCapitalLoanNearBreachActionsApi workingCapitalLoanNearBreachActions() {
return create(WorkingCapitalLoanNearBreachActionsApi.class);
}

public WorkingDaysApi workingDays() {
return create(WorkingDaysApi.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,14 @@ public CommandWrapperBuilder updatePeriodPaymentRateWorkingCapitalLoanApplicatio
return this;
}

public CommandWrapperBuilder createNearBreachActionWorkingCapitalLoan(final Long loanId) {
this.actionName = "CREATE";
this.entityName = "WC_NEAR_BREACH_ACTION";
this.entityId = loanId;
this.href = "/working-capital-loans/" + loanId + "/near-breach-actions";
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 @@ -19,10 +19,14 @@
package org.apache.fineract.cob.workingcapitalloan.businessstep;

import java.time.LocalDate;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.workingcapitalloan.domain.NearBreachActionType;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan;
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachAction;
import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNearBreachActionRepository;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanNearBreachEvaluationService;
import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductRelatedDetails;
import org.springframework.stereotype.Component;
Expand All @@ -33,6 +37,7 @@
public class NearBreachEvaluationBusinessStep extends WorkingCapitalLoanCOBBusinessStep {

private final WorkingCapitalLoanNearBreachEvaluationService nearBreachEvaluationService;
private final WorkingCapitalLoanNearBreachActionRepository nearBreachActionRepository;

@Override
public WorkingCapitalLoan execute(final WorkingCapitalLoan loan) {
Expand All @@ -42,13 +47,16 @@ public WorkingCapitalLoan execute(final WorkingCapitalLoan loan) {
}

final WorkingCapitalLoanProductRelatedDetails details = loan.getLoanProductRelatedDetails();
if (details == null || details.getNearBreach() == null) {
final Optional<WorkingCapitalLoanNearBreachAction> latestAction = nearBreachActionRepository
.findTopByWorkingCapitalLoanIdAndActionOrderByIdDesc(loan.getId(), NearBreachActionType.RESCHEDULE);
final boolean hasConfig = details != null && (details.getNearBreach() != null || latestAction.isPresent());
if (!hasConfig) {
log.debug("Skipping near breach evaluation for WC loan {} - no near breach configuration", loan.getId());
return loan;
}

final LocalDate businessDate = DateUtils.getBusinessLocalDate();
nearBreachEvaluationService.evaluateNearBreach(loan, businessDate);
nearBreachEvaluationService.evaluateNearBreach(loan, latestAction.orElse(null), businessDate);
return loan;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,10 @@ private WorkingCapitalLoanConstants() {
// Period payment rate change parameters
public static final String periodPaymentRateParamName = "periodPaymentRate";
public static final String previousPeriodPaymentRateParamName = "previousRate";

// Near breach action parameters
public static final String nearBreachActionParamName = "action";
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
@@ -0,0 +1,120 @@
/**
* 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.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanNearBreachActionData;
import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService;
import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanNearBreachActionReadService;
import org.springframework.stereotype.Component;

@Path("/v1/working-capital-loans")
@Component
@Tag(name = "Working Capital Loan Near Breach Actions", description = "Manages near breach actions for Working Capital loans")
@RequiredArgsConstructor
public class WorkingCapitalLoanNearBreachActionApiResource {

private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WC_NEAR_BREACH_ACTION";

private final PlatformSecurityContext context;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final WorkingCapitalLoanNearBreachActionReadService nearBreachActionReadService;
private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService;

@POST
@Path("{loanId}/near-breach-actions")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "createWorkingCapitalLoanNearBreachActionById", summary = "Create a near breach action for an active Working Capital Loan", description = "Creates a near breach action (reschedule) for a Working Capital loan.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanNearBreachActionApiResourceSwagger.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.class)))
public CommandProcessingResult createNearBreachActionById(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return createNearBreachAction(loanId, null, apiRequestBodyAsJson);
}

@POST
@Path("external-id/{loanExternalId}/near-breach-actions")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "createWorkingCapitalLoanNearBreachActionByExternalId", summary = "Create a near breach action for an active Working Capital Loan by external id", description = "Creates a near breach action (reschedule) for a Working Capital loan.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = WorkingCapitalLoanNearBreachActionApiResourceSwagger.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.class)))
public CommandProcessingResult createNearBreachActionByExternalId(
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
return createNearBreachAction(null, loanExternalId, apiRequestBodyAsJson);
}

private CommandProcessingResult createNearBreachAction(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)
.createNearBreachActionWorkingCapitalLoan(resolvedLoanId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@GET
@Path("{loanId}/near-breach-actions")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "getWorkingCapitalLoanNearBreachActionsById", summary = "Retrieve near breach actions for a Working Capital Loan", description = "Returns all near breach action records for the loan, ordered by most recent first.")
public List<WorkingCapitalLoanNearBreachActionData> getNearBreachActionsById(
@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.nearBreachActionReadService.retrieveNearBreachActions(loanId);
}

@GET
@Path("external-id/{loanExternalId}/near-breach-actions")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(operationId = "getWorkingCapitalLoanNearBreachActionsByExternalId", summary = "Retrieve near breach actions for a Working Capital Loan by external id", description = "Returns all near breach action records for the loan, ordered by most recent first.")
public List<WorkingCapitalLoanNearBreachActionData> getNearBreachActionsByExternalId(
@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.nearBreachActionReadService.retrieveNearBreachActions(resolvedLoanId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* 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.api;

import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;

public final class WorkingCapitalLoanNearBreachActionApiResourceSwagger {

private WorkingCapitalLoanNearBreachActionApiResourceSwagger() {}

@Schema(description = "Request body for creating a near breach action on an active Working Capital Loan")
public static final class PostWorkingCapitalLoansLoanIdNearBreachActionsRequest {

private PostWorkingCapitalLoansLoanIdNearBreachActionsRequest() {}

@Schema(example = "RESCHEDULE", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {
"RESCHEDULE" }, description = "The near breach action type")
public String action;

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

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

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

@Schema(example = "en_GB")
public String locale;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 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.OffsetDateTime;
import org.apache.fineract.portfolio.workingcapitalloan.domain.NearBreachActionType;

public record WorkingCapitalLoanNearBreachActionData(Long id, Long loanId, NearBreachActionType action, BigDecimal threshold,
Integer frequency, String frequencyType, OffsetDateTime createdDate) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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;

public enum NearBreachActionType {
RESCHEDULE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* 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 java.math.BigDecimal;
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_action")
public class WorkingCapitalLoanNearBreachAction extends AbstractAuditableWithUTCDateTimeCustom<Long> {

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

@Enumerated(EnumType.STRING)
@Column(name = "action", nullable = false)
private NearBreachActionType action;

@Column(name = "threshold", scale = 6, precision = 19)
private BigDecimal threshold;

@Column(name = "frequency")
private Integer frequency;

@Enumerated(EnumType.STRING)
@Column(name = "frequency_type")
private WorkingCapitalLoanPeriodFrequencyType frequencyType;

public static WorkingCapitalLoanNearBreachAction create(final WorkingCapitalLoan loan, final NearBreachActionType action,
final BigDecimal threshold, final Integer frequency, final WorkingCapitalLoanPeriodFrequencyType frequencyType) {
final WorkingCapitalLoanNearBreachAction entity = new WorkingCapitalLoanNearBreachAction();
entity.workingCapitalLoan = loan;
entity.action = action;
entity.threshold = threshold;
entity.frequency = frequency;
entity.frequencyType = frequencyType;
return entity;
}
}
Loading