diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java index e2851b5db61..0a9014db4f9 100644 --- a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java +++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java @@ -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; @@ -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); } diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index d96301cbf7d..7234454fdf4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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; diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/NearBreachEvaluationBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/NearBreachEvaluationBusinessStep.java index f0ccd687a3b..cdc3f2c7366 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/NearBreachEvaluationBusinessStep.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/NearBreachEvaluationBusinessStep.java @@ -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; @@ -33,6 +37,7 @@ public class NearBreachEvaluationBusinessStep extends WorkingCapitalLoanCOBBusinessStep { private final WorkingCapitalLoanNearBreachEvaluationService nearBreachEvaluationService; + private final WorkingCapitalLoanNearBreachActionRepository nearBreachActionRepository; @Override public WorkingCapitalLoan execute(final WorkingCapitalLoan loan) { @@ -42,13 +47,16 @@ public WorkingCapitalLoan execute(final WorkingCapitalLoan loan) { } final WorkingCapitalLoanProductRelatedDetails details = loan.getLoanProductRelatedDetails(); - if (details == null || details.getNearBreach() == null) { + final Optional 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; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java index 7e6096d7de0..c698a8e549a 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/WorkingCapitalLoanConstants.java @@ -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"; } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResource.java new file mode 100644 index 00000000000..2183e7d6b8d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResource.java @@ -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 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 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); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResourceSwagger.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResourceSwagger.java new file mode 100644 index 00000000000..ea61ce6befd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanNearBreachActionApiResourceSwagger.java @@ -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; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java new file mode 100644 index 00000000000..26f5cbf2260 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WorkingCapitalLoanNearBreachActionData.java @@ -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) { + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/NearBreachActionType.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/NearBreachActionType.java new file mode 100644 index 00000000000..a3cbdae6c8f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/NearBreachActionType.java @@ -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 +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java new file mode 100644 index 00000000000..76f94347920 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanNearBreachAction.java @@ -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 { + + @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; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateNearBreachActionCommandHandler.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateNearBreachActionCommandHandler.java new file mode 100644 index 00000000000..acd9df13b81 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/handler/CreateNearBreachActionCommandHandler.java @@ -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.WorkingCapitalLoanNearBreachActionWriteService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "WC_NEAR_BREACH_ACTION", action = "CREATE") +public class CreateNearBreachActionCommandHandler implements NewCommandSourceHandler { + + private final WorkingCapitalLoanNearBreachActionWriteService writeService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return writeService.createNearBreachAction(command.entityId(), command); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanNearBreachActionMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanNearBreachActionMapper.java new file mode 100644 index 00000000000..599ab6436b9 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WorkingCapitalLoanNearBreachActionMapper.java @@ -0,0 +1,37 @@ +/** + * 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.mapper; + +import java.util.List; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanNearBreachActionData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachAction; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface WorkingCapitalLoanNearBreachActionMapper { + + @Mapping(target = "loanId", source = "workingCapitalLoan.id") + @Mapping(target = "frequencyType", expression = "java(entity.getFrequencyType() != null ? entity.getFrequencyType().name() : null)") + @Mapping(target = "createdDate", expression = "java(entity.getCreatedDate().orElse(null))") + WorkingCapitalLoanNearBreachActionData toData(WorkingCapitalLoanNearBreachAction entity); + + List toDataList(List entities); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanNearBreachActionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanNearBreachActionRepository.java new file mode 100644 index 00000000000..456f24c19aa --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanNearBreachActionRepository.java @@ -0,0 +1,35 @@ +/** + * 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 java.util.Optional; +import org.apache.fineract.portfolio.workingcapitalloan.domain.NearBreachActionType; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachAction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface WorkingCapitalLoanNearBreachActionRepository extends JpaRepository { + + List findByWorkingCapitalLoanIdOrderByIdDesc(Long loanId); + + Optional findTopByWorkingCapitalLoanIdAndActionOrderByIdDesc(Long loanId, + NearBreachActionType action); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java index 3a0956369fa..1a8415a14cc 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java @@ -47,7 +47,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; +import org.apache.fineract.portfolio.workingcapitalloan.domain.NearBreachActionType; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductRelatedDetail; @@ -107,6 +109,11 @@ public class WorkingCapitalLoanDataValidator { Arrays.asList(WorkingCapitalLoanConstants.localeParameterName, WorkingCapitalLoanConstants.periodPaymentRateParamName, WorkingCapitalLoanConstants.noteParamName)); + private static final Set NEAR_BREACH_ACTION_SUPPORTED_PARAMETERS = new HashSet<>( + Arrays.asList(WorkingCapitalLoanConstants.localeParameterName, WorkingCapitalLoanConstants.nearBreachActionParamName, + WorkingCapitalLoanConstants.nearBreachThresholdParamName, WorkingCapitalLoanConstants.nearBreachFrequencyParamName, + WorkingCapitalLoanConstants.nearBreachFrequencyTypeParamName)); + private static final int NOTE_MAX_LENGTH = 1000; private static final int EXTERNAL_ID_MAX_LENGTH = 100; private static final int PAYMENT_DETAIL_STRING_MAX_LENGTH = 50; @@ -771,6 +778,73 @@ public void validateCreditBalanceRefund(final String json, final WorkingCapitalL throwExceptionIfValidationWarningsExist(dataValidationErrors); } + public void validateNearBreachAction(final String json, final WorkingCapitalLoan loan) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, NEAR_BREACH_ACTION_SUPPORTED_PARAMETERS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(WorkingCapitalLoanConstants.RESOURCE_NAME); + final JsonElement element = this.fromApiJsonHelper.parse(json); + + if (loan.getLoanStatus() != LoanStatus.ACTIVE) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.loanStatusParamName) + .failWithCode("near.breach.action.not.allowed.for.non.active.loan"); + } + + if (loan.getLoanProductRelatedDetails().getNearBreach() == null) { + baseDataValidator.reset() + .failWithCodeNoParameterAddedToErrorCode("near.breach.action.not.allowed.loan.has.no.near.breach.configuration"); + } + + final String actionStr = this.fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.nearBreachActionParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachActionParamName).value(actionStr).notBlank(); + if (StringUtils.isNotBlank(actionStr)) { + try { + NearBreachActionType.valueOf(actionStr); + } catch (IllegalArgumentException e) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachActionParamName).failWithCode("invalid.action"); + } + + final NearBreachActionType currentNearBreachAction = NearBreachActionType.valueOf(actionStr); + + if (currentNearBreachAction == NearBreachActionType.RESCHEDULE) { + validateActionReschedule(element, baseDataValidator); + } + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void validateActionReschedule(JsonElement element, DataValidatorBuilder baseDataValidator) { + final BigDecimal threshold = this.fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.nearBreachThresholdParamName, + element, new HashSet<>()); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachThresholdParamName).value(threshold).notNull() + .positiveAmount(); + if (threshold != null && threshold.compareTo(BigDecimal.valueOf(100)) > 0) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachThresholdParamName) + .failWithCode("must.not.exceed.100.percent"); + } + + final Integer frequency = this.fromApiJsonHelper + .extractIntegerSansLocaleNamed(WorkingCapitalLoanConstants.nearBreachFrequencyParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachFrequencyParamName).value(frequency).notNull() + .integerGreaterThanZero(); + + final String frequencyTypeStr = this.fromApiJsonHelper + .extractStringNamed(WorkingCapitalLoanConstants.nearBreachFrequencyTypeParamName, element); + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachFrequencyTypeParamName).value(frequencyTypeStr) + .notBlank(); + if (StringUtils.isNotBlank(frequencyTypeStr) && WorkingCapitalLoanPeriodFrequencyType.fromString(frequencyTypeStr) == null) { + baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.nearBreachFrequencyTypeParamName) + .failWithCode("invalid.frequency.type"); + } + } + public void validateUpdatePeriodPaymentRate(final String json, final WorkingCapitalLoan loan) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadService.java new file mode 100644 index 00000000000..4371a60aac4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadService.java @@ -0,0 +1,27 @@ +/** + * 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.service; + +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanNearBreachActionData; + +public interface WorkingCapitalLoanNearBreachActionReadService { + + List retrieveNearBreachActions(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadServiceImpl.java new file mode 100644 index 00000000000..b9face3931c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionReadServiceImpl.java @@ -0,0 +1,47 @@ +/** + * 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.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanNearBreachActionData; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanNearBreachActionMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNearBreachActionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class WorkingCapitalLoanNearBreachActionReadServiceImpl implements WorkingCapitalLoanNearBreachActionReadService { + + private final WorkingCapitalLoanNearBreachActionRepository actionRepository; + private final WorkingCapitalLoanRepository loanRepository; + private final WorkingCapitalLoanNearBreachActionMapper mapper; + + @Override + public List retrieveNearBreachActions(final Long loanId) { + if (!loanRepository.existsById(loanId)) { + throw new WorkingCapitalLoanNotFoundException(loanId); + } + return mapper.toDataList(actionRepository.findByWorkingCapitalLoanIdOrderByIdDesc(loanId)); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteService.java new file mode 100644 index 00000000000..d90d8ea918a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteService.java @@ -0,0 +1,27 @@ +/** + * 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.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface WorkingCapitalLoanNearBreachActionWriteService { + + CommandProcessingResult createNearBreachAction(Long loanId, JsonCommand command); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteServiceImpl.java new file mode 100644 index 00000000000..43bc2c8d6cf --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachActionWriteServiceImpl.java @@ -0,0 +1,86 @@ +/** + * 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.service; + +import java.math.BigDecimal; +import java.util.HashSet; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; +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.domain.WorkingCapitalLoanPeriodFrequencyType; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNearBreachActionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.apache.fineract.portfolio.workingcapitalloan.serialization.WorkingCapitalLoanDataValidator; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class WorkingCapitalLoanNearBreachActionWriteServiceImpl implements WorkingCapitalLoanNearBreachActionWriteService { + + private final WorkingCapitalLoanRepository loanRepository; + private final WorkingCapitalLoanNearBreachActionRepository actionRepository; + private final WorkingCapitalLoanDataValidator validator; + private final FromJsonHelper fromApiJsonHelper; + + @Transactional + @Override + public CommandProcessingResult createNearBreachAction(final Long loanId, final JsonCommand command) { + final WorkingCapitalLoan loan = loanRepository.findById(loanId).orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + + validator.validateNearBreachAction(command.json(), loan); + + final String actionStr = fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.nearBreachActionParamName, + command.parsedJson()); + final NearBreachActionType actionType = NearBreachActionType.valueOf(actionStr); + + final BigDecimal threshold = fromApiJsonHelper.extractBigDecimalNamed(WorkingCapitalLoanConstants.nearBreachThresholdParamName, + command.parsedJson(), new HashSet<>()); + final Integer frequency = fromApiJsonHelper.extractIntegerNamed(WorkingCapitalLoanConstants.nearBreachFrequencyParamName, + command.parsedJson(), new HashSet<>()); + final String frequencyTypeStr = fromApiJsonHelper.extractStringNamed(WorkingCapitalLoanConstants.nearBreachFrequencyTypeParamName, + command.parsedJson()); + final WorkingCapitalLoanPeriodFrequencyType frequencyType = frequencyTypeStr != null + ? WorkingCapitalLoanPeriodFrequencyType.fromString(frequencyTypeStr) + : null; + + final WorkingCapitalLoanNearBreachAction action = WorkingCapitalLoanNearBreachAction.create(loan, actionType, threshold, frequency, + frequencyType); + final WorkingCapitalLoanNearBreachAction saved = actionRepository.saveAndFlush(action); + + log.debug("Created near breach action {} ({}) for WC loan {}", saved.getId(), actionType, loanId); + + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(saved.getId()) // + .withLoanId(loanId) // + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .build(); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationService.java index 79c84bb5729..675ef1016b1 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationService.java @@ -20,9 +20,10 @@ import java.time.LocalDate; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachAction; public interface WorkingCapitalLoanNearBreachEvaluationService { - void evaluateNearBreach(WorkingCapitalLoan loan, LocalDate effectiveDate); + void evaluateNearBreach(WorkingCapitalLoan loan, WorkingCapitalLoanNearBreachAction latestAction, LocalDate effectiveDate); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationServiceImpl.java index 549096d43af..97376ff9178 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanNearBreachEvaluationServiceImpl.java @@ -30,6 +30,7 @@ import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanBreachSchedule; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanNearBreachAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloannearbreach.domain.WorkingCapitalNearBreach; @@ -43,7 +44,8 @@ public class WorkingCapitalLoanNearBreachEvaluationServiceImpl implements Workin private final WorkingCapitalLoanBreachScheduleRepository breachScheduleRepository; @Override - public void evaluateNearBreach(final WorkingCapitalLoan loan, final LocalDate effectiveDate) { + public void evaluateNearBreach(final WorkingCapitalLoan loan, final WorkingCapitalLoanNearBreachAction latestAction, + final LocalDate effectiveDate) { final Optional relevantPeriod = breachScheduleRepository .findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(loan.getId(), effectiveDate, effectiveDate); if (relevantPeriod.isEmpty()) { @@ -54,26 +56,38 @@ public void evaluateNearBreach(final WorkingCapitalLoan loan, final LocalDate ef return; } final WorkingCapitalNearBreach config = loan.getLoanProductRelatedDetails().getNearBreach(); - if (evaluatePeriod(loan.getId(), period, config, effectiveDate)) { + + final BigDecimal effectiveThreshold; + final Integer effectiveFrequency; + final WorkingCapitalLoanPeriodFrequencyType effectiveFrequencyType; + if (latestAction != null) { + effectiveThreshold = latestAction.getThreshold(); + effectiveFrequency = latestAction.getFrequency(); + effectiveFrequencyType = latestAction.getFrequencyType(); + } else { + effectiveThreshold = config.getThreshold(); + effectiveFrequency = config.getFrequency(); + effectiveFrequencyType = config.getFrequencyType(); + } + if (evaluatePeriod(loan.getId(), period, effectiveThreshold, effectiveFrequency, effectiveFrequencyType, effectiveDate)) { breachScheduleRepository.saveAndFlush(period); } } - private boolean evaluatePeriod(final Long loanId, final WorkingCapitalLoanBreachSchedule period, final WorkingCapitalNearBreach config, - final LocalDate effectiveDate) { + private boolean evaluatePeriod(final Long loanId, final WorkingCapitalLoanBreachSchedule period, final BigDecimal threshold, + final Integer frequency, final WorkingCapitalLoanPeriodFrequencyType frequencyType, final LocalDate effectiveDate) { if (period.getMinPaymentAmount().compareTo(BigDecimal.ZERO) == 0) { return false; } - final LocalDate firstEvalDate = addFrequency(period.getFromDate(), config.getFrequency(), config.getFrequencyType()); + final LocalDate firstEvalDate = addFrequency(period.getFromDate(), frequency, frequencyType); if (firstEvalDate.isAfter(period.getToDate())) { return false; } - final List evalDates = listEvalDates(period.getFromDate(), period.getToDate(), config.getFrequency(), - config.getFrequencyType()); + final List evalDates = listEvalDates(period.getFromDate(), period.getToDate(), frequency, frequencyType); final int evalIndex = evalDates.indexOf(effectiveDate); if (evalIndex >= 0) { final MonetaryCurrency currency = period.getLoan().getLoanProductRelatedDetails().getCurrency(); - final BigDecimal thresholdFraction = config.getThreshold().divide(BigDecimal.valueOf(100), MoneyHelper.getMathContext()); + final BigDecimal thresholdFraction = threshold.divide(BigDecimal.valueOf(100), MoneyHelper.getMathContext()); final Money requiredCumulative = calculateRequiredCumulative(currency, period.getMinPaymentAmount(), thresholdFraction, evalIndex); final Money paidCumulative = Money.of(currency, period.getPaidAmount()); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 85cf5d85d01..58391e63c0e 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -72,6 +72,7 @@ import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBalanceRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanChargeRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanNoteRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanPeriodPaymentRateChangeRepository; @@ -106,6 +107,7 @@ public class WorkingCapitalLoanWritePlatformServiceImpl implements WorkingCapita private final WorkingCapitalLoanAccountingProcessor accountingProcessor; private final WorkingCapitalLoanTransactionRelationRepository relationRepository; private final WorkingCapitalLoanPeriodPaymentRateChangeRepository rateChangeRepository; + private final WorkingCapitalLoanBreachScheduleRepository breachScheduleRepository; private final WorkingCapitalLoanDiscountFeeAmortizationService discountFeeAmortizationService; private final WorkingCapitalLoanTransactionReprocessingService transactionReprocessingService; private final WorkingCapitalLoanChargeRepository chargeRepository; diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 6fcc1ddd652..e262d3c0104 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -66,4 +66,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_near_breach_action.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_near_breach_action.xml new file mode 100644 index 00000000000..e7bc644da89 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0045_wc_loan_near_breach_action.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_WC_NEAR_BREACH_ACTION'; + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'READ_WC_NEAR_BREACH_ACTION'; + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanNearBreachConfigTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanNearBreachConfigTest.java new file mode 100644 index 00000000000..ccbe6d3ff7a --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WorkingCapitalLoanNearBreachConfigTest.java @@ -0,0 +1,399 @@ +/** + * 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.integrationtests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.feign.util.FeignCalls; +import org.apache.fineract.client.models.InlineJobRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.NearBreachFrequencyTypeEnum; +import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; +import org.apache.fineract.client.models.WorkingCapitalBreachRequest; +import org.apache.fineract.client.models.WorkingCapitalLoanBreachScheduleData; +import org.apache.fineract.client.models.WorkingCapitalLoanNearBreachActionData; +import org.apache.fineract.client.models.WorkingCapitalNearBreachRequest; +import org.apache.fineract.integrationtests.client.feign.modules.WorkingCapitalLoanRequestBuilders; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.workingcapitalloan.WorkingCapitalLoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.workingcapitalloan.WorkingCapitalLoanDisbursementTestBuilder; +import org.apache.fineract.integrationtests.common.workingcapitalloan.WorkingCapitalLoanHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloanbreach.WorkingCapitalBreachHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloannearbreach.WorkingCapitalLoanNearBreachActionsHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloannearbreach.WorkingCapitalNearBreachHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloanproduct.WorkingCapitalLoanProductTestBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class WorkingCapitalLoanNearBreachConfigTest { + + private final WorkingCapitalLoanHelper loanHelper = new WorkingCapitalLoanHelper(); + private final WorkingCapitalLoanProductHelper productHelper = new WorkingCapitalLoanProductHelper(); + private final WorkingCapitalNearBreachHelper nearBreachHelper = new WorkingCapitalNearBreachHelper(); + private final WorkingCapitalBreachHelper breachHelper = new WorkingCapitalBreachHelper(); + private final WorkingCapitalLoanNearBreachActionsHelper nearBreachActionsHelper = new WorkingCapitalLoanNearBreachActionsHelper(); + + private final List createdLoanIds = new ArrayList<>(); + private final List createdProductIds = new ArrayList<>(); + private final List createdNearBreachIds = new ArrayList<>(); + private final List createdBreachIds = new ArrayList<>(); + private final Long createdClientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + @AfterEach + void cleanup() { + for (final Long loanId : createdLoanIds) { + if (loanId == null) { + continue; + } + try { + loanHelper.undoDisbursalById(loanId, WorkingCapitalLoanDisbursementTestBuilder.buildUndoDisburseRequest()); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + try { + loanHelper.undoApprovalById(loanId, WorkingCapitalLoanApplicationTestBuilder.buildUndoApproveRequest()); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + try { + loanHelper.deleteById(loanId); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + } + createdLoanIds.clear(); + for (final Long productId : createdProductIds) { + if (productId == null) { + continue; + } + try { + productHelper.deleteWorkingCapitalLoanProductById(productId); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + } + createdProductIds.clear(); + for (final Long nearBreachId : createdNearBreachIds) { + if (nearBreachId == null) { + continue; + } + try { + nearBreachHelper.delete(nearBreachId); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + } + createdNearBreachIds.clear(); + for (final Long breachId : createdBreachIds) { + if (breachId == null) { + continue; + } + try { + breachHelper.delete(breachId); + } catch (final CallFailedRuntimeException ignored) { + // best-effort cleanup + } + } + createdBreachIds.clear(); + } + + @Test + public void testCreateNearBreachActionByIdSucceeds() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + final BigDecimal newThreshold = BigDecimal.valueOf(40); + final Integer newFrequency = 15; + final String newFrequencyType = "DAYS"; + + nearBreachActionsHelper.createNearBreachActionById(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(newThreshold, newFrequency, newFrequencyType)); + + final List history = nearBreachActionsHelper.getNearBreachChangeActionsById(loanId); + assertNotNull(history); + assertFalse(history.isEmpty()); + final WorkingCapitalLoanNearBreachActionData latest = history.getFirst(); + assertNotNull(latest.getId()); + assertEquals(0, newThreshold.compareTo(latest.getThreshold())); + assertEquals(newFrequency, latest.getFrequency()); + assertEquals(newFrequencyType, latest.getFrequencyType()); + } + + @Test + public void testCreateNearBreachActionByExternalIdSucceeds() { + final String externalId = "wcl-nb-ext-" + UUID.randomUUID().toString().substring(0, 8); + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId, externalId); + final BigDecimal newThreshold = BigDecimal.valueOf(55); + final Integer newFrequency = 7; + final String newFrequencyType = "DAYS"; + + nearBreachActionsHelper.createNearBreachActionByExternalId(externalId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(newThreshold, newFrequency, newFrequencyType)); + + final List history = nearBreachActionsHelper + .getNearBreachChangeActionsByExternalId(externalId); + assertNotNull(history); + assertFalse(history.isEmpty()); + final WorkingCapitalLoanNearBreachActionData latest = history.getFirst(); + assertEquals(0, newThreshold.compareTo(latest.getThreshold())); + assertEquals(newFrequency, latest.getFrequency()); + assertEquals(newFrequencyType, latest.getFrequencyType()); + } + + @Test + public void testMultipleActionsAppendToHistory() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + + nearBreachActionsHelper.createNearBreachActionById(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(30), 7, "DAYS")); + nearBreachActionsHelper.createNearBreachActionById(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(45), 14, "DAYS")); + nearBreachActionsHelper.createNearBreachActionById(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(60), 21, "DAYS")); + + final List history = nearBreachActionsHelper.getNearBreachChangeActionsById(loanId); + assertThat(history.size()).isGreaterThanOrEqualTo(3); + assertEquals(21, history.get(0).getFrequency()); + assertEquals(14, history.get(1).getFrequency()); + assertEquals(7, history.get(2).getFrequency()); + } + + @Test + public void testGetHistoryReturnsEmptyListWhenNoChangesMade() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + + final List history = nearBreachActionsHelper.getNearBreachChangeActionsById(loanId); + assertNotNull(history); + assertThat(history).isEmpty(); + } + + @Test + public void testCobEvaluatesNearBreachWithUpdatedConfig() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(50), 7, "DAYS"); + final Long[] loanIdHolder = new Long[1]; + BusinessDateHelper.runAt("01 January 2026", () -> { + loanIdHolder[0] = createActiveLoanWithNearBreach(nearBreachId, null, LocalDate.of(2026, 1, 1)); + nearBreachActionsHelper.createNearBreachActionById(loanIdHolder[0], + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(1), 14, "DAYS")); + }); + final Long loanId = loanIdHolder[0]; + + BusinessDateHelper.runAt("09 January 2026", () -> { + FeignCalls.ok(() -> FineractFeignClientHelper.getFineractFeignClient().inlineJob().executeInlineJob("WC_LOAN_COB", + new InlineJobRequest().addLoanIdsItem(loanId))); + + final List schedule = nearBreachActionsHelper.getBreachSchedule(loanId); + assertFalse(schedule.isEmpty()); + final WorkingCapitalLoanBreachScheduleData period = schedule.getFirst(); + assertTrue(period.getNearBreach() == null || !Boolean.TRUE.equals(period.getNearBreach()), + "Near breach should not be flagged on day 7 after config changed to 14-day frequency"); + }); + + BusinessDateHelper.runAt("16 January 2026", () -> { + FeignCalls.ok(() -> FineractFeignClientHelper.getFineractFeignClient().inlineJob().executeInlineJob("WC_LOAN_COB", + new InlineJobRequest().addLoanIdsItem(loanId))); + + final List schedule = nearBreachActionsHelper.getBreachSchedule(loanId); + assertFalse(schedule.isEmpty()); + final WorkingCapitalLoanBreachScheduleData period = schedule.getFirst(); + assertEquals(Boolean.TRUE, period.getNearBreach(), + "Near breach should be flagged on the 14-day evaluation point with no payment"); + }); + } + + @Test + public void testCreateNearBreachActionWithMissingThresholdFails() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request = new PostWorkingCapitalLoansLoanIdNearBreachActionsRequest() + .action(PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.ActionEnum.RESCHEDULE).nearBreachFrequency(7) + .nearBreachFrequencyType(NearBreachFrequencyTypeEnum.DAYS).locale("en"); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, request); + assertEquals(400, ex.getStatus()); + } + + @Test + public void testCreateNearBreachActionWithThresholdOver100Fails() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(101), 7, "DAYS")); + assertEquals(400, ex.getStatus()); + assertThat(ex.getDeveloperMessage()).contains("must.not.exceed.100.percent"); + } + + @Test + public void testCreateNearBreachActionWithMissingFrequencyFails() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request = new PostWorkingCapitalLoansLoanIdNearBreachActionsRequest() + .action(PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.ActionEnum.RESCHEDULE) + .nearBreachThreshold(BigDecimal.valueOf(40)).nearBreachFrequencyType(NearBreachFrequencyTypeEnum.DAYS).locale("en"); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, request); + assertEquals(400, ex.getStatus()); + } + + @Test + public void testCreateNearBreachActionWithMissingActionFails() { + final Long nearBreachId = createNearBreachTemplate(BigDecimal.valueOf(20), 7, "DAYS"); + final Long loanId = createActiveLoanWithNearBreach(nearBreachId); + final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request = new PostWorkingCapitalLoansLoanIdNearBreachActionsRequest() + .nearBreachThreshold(BigDecimal.valueOf(40)).nearBreachFrequency(7) + .nearBreachFrequencyType(NearBreachFrequencyTypeEnum.DAYS).locale("en"); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, request); + assertEquals(400, ex.getStatus()); + } + + @Test + public void testCreateNearBreachActionOnLoanWithoutNearBreachConfigFails() { + final Long loanId = createActiveLoan(); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(40), 7, "DAYS")); + assertEquals(400, ex.getStatus()); + assertThat(ex.getDeveloperMessage()).contains("near.breach.action.not.allowed.loan.has.no.near.breach.configuration"); + } + + @Test + public void testCreateNearBreachActionOnPendingLoanFails() { + final Long productId = createProduct(); + final Long loanId = submitAndTrack(new WorkingCapitalLoanApplicationTestBuilder().withClientId(createdClientId) + .withProductId(productId).withPrincipal(BigDecimal.valueOf(5000)) + .withPeriodPaymentRate(WorkingCapitalLoanProductTestBuilder.DEFAULT_PERIOD_PAYMENT_RATE_PERCENT) + .withTotalPaymentVolume(BigDecimal.valueOf(100000)).buildSubmitRequest()); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(40), 7, "DAYS")); + assertEquals(400, ex.getStatus()); + assertThat(ex.getDeveloperMessage()).contains("near.breach.action.not.allowed.for.non.active.loan"); + } + + @Test + public void testCreateNearBreachActionOnApprovedLoanFails() { + final Long productId = createProduct(); + final Long loanId = submitAndTrack(new WorkingCapitalLoanApplicationTestBuilder().withClientId(createdClientId) + .withProductId(productId).withPrincipal(BigDecimal.valueOf(5000)) + .withPeriodPaymentRate(WorkingCapitalLoanProductTestBuilder.DEFAULT_PERIOD_PAYMENT_RATE_PERCENT) + .withTotalPaymentVolume(BigDecimal.valueOf(100000)).buildSubmitRequest()); + loanHelper.approveById(loanId, + WorkingCapitalLoanApplicationTestBuilder.buildApproveRequest(Utils.getLocalDateOfTenant(), BigDecimal.valueOf(5000), null)); + + final CallFailedRuntimeException ex = nearBreachActionsHelper.createNearBreachActionByIdExpectingFailure(loanId, + WorkingCapitalLoanRequestBuilders.createNearBreachRescheduleAction(BigDecimal.valueOf(40), 7, "DAYS")); + assertEquals(400, ex.getStatus()); + assertThat(ex.getDeveloperMessage()).contains("near.breach.action.not.allowed.for.non.active.loan"); + } + + private Long createActiveLoan() { + final Long productId = createProduct(); + final Long loanId = submitAndTrack(new WorkingCapitalLoanApplicationTestBuilder().withClientId(createdClientId) + .withProductId(productId).withPrincipal(BigDecimal.valueOf(5000)) + .withPeriodPaymentRate(WorkingCapitalLoanProductTestBuilder.DEFAULT_PERIOD_PAYMENT_RATE_PERCENT) + .withTotalPaymentVolume(BigDecimal.valueOf(100000)).buildSubmitRequest()); + final LocalDate today = Utils.getLocalDateOfTenant(); + loanHelper.approveById(loanId, WorkingCapitalLoanApplicationTestBuilder.buildApproveRequest(today, BigDecimal.valueOf(5000), null)); + loanHelper.disburseById(loanId, WorkingCapitalLoanDisbursementTestBuilder.buildDisburseRequest(today, BigDecimal.valueOf(5000))); + return loanId; + } + + private Long createActiveLoanWithNearBreach(final Long nearBreachId) { + return createActiveLoanWithNearBreach(nearBreachId, null); + } + + private Long createActiveLoanWithNearBreach(final Long nearBreachId, final String externalId) { + final LocalDate today = Utils.getLocalDateOfTenant(); + return createActiveLoanWithNearBreach(nearBreachId, externalId, today); + } + + private Long createActiveLoanWithNearBreach(final Long nearBreachId, final String externalId, + final LocalDate approvalAndDisbursementDate) { + final Long productId = createProductWithNearBreach(nearBreachId); + final WorkingCapitalLoanApplicationTestBuilder builder = new WorkingCapitalLoanApplicationTestBuilder() + .withClientId(createdClientId).withProductId(productId).withPrincipal(BigDecimal.valueOf(5000)) + .withPeriodPaymentRate(WorkingCapitalLoanProductTestBuilder.DEFAULT_PERIOD_PAYMENT_RATE_PERCENT) + .withTotalPaymentVolume(BigDecimal.valueOf(100000)); + if (externalId != null) { + builder.withExternalId(externalId); + } + final Long loanId = submitAndTrack(builder.buildSubmitRequest()); + loanHelper.approveById(loanId, + WorkingCapitalLoanApplicationTestBuilder.buildApproveRequest(approvalAndDisbursementDate, BigDecimal.valueOf(5000), null)); + loanHelper.disburseById(loanId, + WorkingCapitalLoanDisbursementTestBuilder.buildDisburseRequest(approvalAndDisbursementDate, BigDecimal.valueOf(5000))); + return loanId; + } + + private Long createProduct() { + final String uniqueName = "WCL NB Product " + UUID.randomUUID().toString().substring(0, 8); + final String uniqueShortName = Utils.uniqueRandomStringGenerator("", 4); + final Long productId = productHelper + .createWorkingCapitalLoanProduct( + new WorkingCapitalLoanProductTestBuilder().withName(uniqueName).withShortName(uniqueShortName).build()) + .getResourceId(); + createdProductIds.add(productId); + return productId; + } + + private Long createProductWithNearBreach(final Long nearBreachId) { + final String uniqueName = "WCL NB Product " + UUID.randomUUID().toString().substring(0, 8); + final String uniqueShortName = Utils.uniqueRandomStringGenerator("", 4); + final Long breachId = breachHelper + .create(new WorkingCapitalBreachRequest().name(Utils.randomStringGenerator("Breach", 12)).breachFrequency(60) + .breachFrequencyType("DAYS").breachAmountCalculationType("PERCENTAGE").breachAmount(BigDecimal.valueOf(10))); + createdBreachIds.add(breachId); + final Long productId = productHelper.createWorkingCapitalLoanProduct(new WorkingCapitalLoanProductTestBuilder().withName(uniqueName) + .withShortName(uniqueShortName).withBreachId(breachId).withNearBreachId(nearBreachId).build()).getResourceId(); + createdProductIds.add(productId); + return productId; + } + + private Long createNearBreachTemplate(final BigDecimal threshold, final Integer frequency, final String frequencyType) { + final String name = Utils.randomStringGenerator("NearBreach", 12); + final Long id = nearBreachHelper.create(new WorkingCapitalNearBreachRequest().nearBreachName(name).nearBreachThreshold(threshold) + .nearBreachFrequency(frequency).nearBreachFrequencyType(frequencyType)).getResourceId(); + createdNearBreachIds.add(id); + return id; + } + + private Long submitAndTrack(final PostWorkingCapitalLoansRequest request) { + final Long loanId = loanHelper.submit(request); + createdLoanIds.add(loanId); + return loanId; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java index d448419ad66..5188b1f37e1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/modules/WorkingCapitalLoanRequestBuilders.java @@ -20,6 +20,8 @@ import java.math.BigDecimal; import org.apache.fineract.client.models.PostWorkingCapitalLoanTransactionsRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.NearBreachFrequencyTypeEnum; import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoansRequest; import org.apache.fineract.client.models.PutWorkingCapitalLoansLoanIdRateRequest; @@ -73,6 +75,14 @@ public static PutWorkingCapitalLoansLoanIdRateRequest updateRate(BigDecimal newR return new PutWorkingCapitalLoansLoanIdRateRequest().periodPaymentRate(newRate).locale(LOCALE); } + public static PostWorkingCapitalLoansLoanIdNearBreachActionsRequest createNearBreachRescheduleAction(BigDecimal threshold, + Integer frequency, String frequencyType) { + return new PostWorkingCapitalLoansLoanIdNearBreachActionsRequest() + .action(PostWorkingCapitalLoansLoanIdNearBreachActionsRequest.ActionEnum.RESCHEDULE).nearBreachThreshold(threshold) + .nearBreachFrequency(frequency).nearBreachFrequencyType(NearBreachFrequencyTypeEnum.fromValue(frequencyType)) + .locale(LOCALE); + } + public static PostWorkingCapitalLoanTransactionsRequest repayment(BigDecimal amount, String transactionDate) { return new PostWorkingCapitalLoanTransactionsRequest().transactionAmount(amount).transactionDate(transactionDate).locale(LOCALE) .dateFormat(DATE_FORMAT); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloannearbreach/WorkingCapitalLoanNearBreachActionsHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloannearbreach/WorkingCapitalLoanNearBreachActionsHelper.java new file mode 100644 index 00000000000..a106fd63e42 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloannearbreach/WorkingCapitalLoanNearBreachActionsHelper.java @@ -0,0 +1,68 @@ +/** + * 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.integrationtests.common.workingcapitalloannearbreach; + +import java.util.List; +import org.apache.fineract.client.feign.services.WorkingCapitalLoanBreachScheduleApi; +import org.apache.fineract.client.feign.services.WorkingCapitalLoanNearBreachActionsApi; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.feign.util.FeignCalls; +import org.apache.fineract.client.models.PostWorkingCapitalLoansLoanIdNearBreachActionsRequest; +import org.apache.fineract.client.models.WorkingCapitalLoanBreachScheduleData; +import org.apache.fineract.client.models.WorkingCapitalLoanNearBreachActionData; +import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; + +public class WorkingCapitalLoanNearBreachActionsHelper { + + public WorkingCapitalLoanNearBreachActionsHelper() {} + + private static WorkingCapitalLoanNearBreachActionsApi api() { + return FineractFeignClientHelper.getFineractFeignClient().workingCapitalLoanNearBreachActions(); + } + + private static WorkingCapitalLoanBreachScheduleApi breachScheduleApi() { + return FineractFeignClientHelper.getFineractFeignClient().workingCapitalLoanBreachSchedule(); + } + + public void createNearBreachActionById(final Long loanId, final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request) { + FeignCalls.ok(() -> api().createWorkingCapitalLoanNearBreachActionById(loanId, request)); + } + + public void createNearBreachActionByExternalId(final String externalId, + final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request) { + FeignCalls.ok(() -> api().createWorkingCapitalLoanNearBreachActionByExternalId(externalId, request)); + } + + public CallFailedRuntimeException createNearBreachActionByIdExpectingFailure(final Long loanId, + final PostWorkingCapitalLoansLoanIdNearBreachActionsRequest request) { + return FeignCalls.fail(() -> api().createWorkingCapitalLoanNearBreachActionById(loanId, request)); + } + + public List getBreachSchedule(final Long loanId) { + return FeignCalls.ok(() -> breachScheduleApi().retrieveBreachSchedule(loanId)); + } + + public List getNearBreachChangeActionsById(final Long loanId) { + return FeignCalls.ok(() -> api().getWorkingCapitalLoanNearBreachActionsById(loanId)); + } + + public List getNearBreachChangeActionsByExternalId(final String externalId) { + return FeignCalls.ok(() -> api().getWorkingCapitalLoanNearBreachActionsByExternalId(externalId)); + } +}