Skip to content

Commit 5ec60de

Browse files
FINERACT-2455: Working Capital product near breach configuration
1 parent e489d55 commit 5ec60de

41 files changed

Lines changed: 1501 additions & 52 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi;
163163
import org.apache.fineract.client.feign.services.WorkingCapitalLoanTransactionsApi;
164164
import org.apache.fineract.client.feign.services.WorkingCapitalLoansApi;
165+
import org.apache.fineract.client.feign.services.WorkingCapitalNearBreachApi;
165166
import org.apache.fineract.client.feign.services.WorkingDaysApi;
166167

167168
/**
@@ -787,6 +788,10 @@ public WorkingCapitalBreachApi workingCapitalBreaches() {
787788
return create(WorkingCapitalBreachApi.class);
788789
}
789790

791+
public WorkingCapitalNearBreachApi workingCapitalNearBreaches() {
792+
return create(WorkingCapitalNearBreachApi.class);
793+
}
794+
790795
public WorkingDaysApi workingDays() {
791796
return create(WorkingDaysApi.class);
792797
}

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,30 @@ public CommandWrapperBuilder deleteWorkingCapitalBreach(final Long breachId) {
790790
return this;
791791
}
792792

793+
public CommandWrapperBuilder createWorkingCapitalNearBreach() {
794+
this.actionName = "CREATE";
795+
this.entityName = "WORKINGCAPITALNEARBREACH";
796+
this.entityId = null;
797+
this.href = "/working-capital/near-breach";
798+
return this;
799+
}
800+
801+
public CommandWrapperBuilder updateWorkingCapitalNearBreach(final Long breachId) {
802+
this.actionName = "UPDATE";
803+
this.entityName = "WORKINGCAPITALNEARBREACH";
804+
this.entityId = breachId;
805+
this.href = "/working-capital/near-breach/" + breachId;
806+
return this;
807+
}
808+
809+
public CommandWrapperBuilder deleteWorkingCapitalNearBreach(final Long breachId) {
810+
this.actionName = "DELETE";
811+
this.entityName = "WORKINGCAPITALNEARBREACH";
812+
this.entityId = breachId;
813+
this.href = "/working-capital/near-breach/" + breachId;
814+
return this;
815+
}
816+
793817
public CommandWrapperBuilder createWorkingCapitalLoanApplication() {
794818
this.actionName = ACTION_CREATE;
795819
this.entityName = ENTITY_WORKINGCAPITALLOAN;

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,31 @@ public DataValidatorBuilder positiveAmount() {
383383
return this;
384384
}
385385

386+
public DataValidatorBuilder percentage() {
387+
if (this.value == null && this.ignoreNullValue) {
388+
return this;
389+
}
390+
391+
if (this.value != null) {
392+
final BigDecimal number = BigDecimal.valueOf(Double.parseDouble(this.value.toString()));
393+
if (number.compareTo(BigDecimal.ZERO) <= 0) {
394+
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.greater.than.zero";
395+
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be greater than 0.";
396+
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter,
397+
number, 0);
398+
this.dataValidationErrors.add(error);
399+
}
400+
if (number.compareTo(BigDecimal.valueOf(100.0)) > 0) {
401+
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".greater.than.one.hundred";
402+
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be not greater than 100.";
403+
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter,
404+
number, 0);
405+
this.dataValidationErrors.add(error);
406+
}
407+
}
408+
return this;
409+
}
410+
386411
/*
387412
* should be used with .notNull() before it
388413
*/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.infrastructure.core.service;
20+
21+
public final class FrequencyTypeUtil {
22+
23+
private FrequencyTypeUtil() {}
24+
25+
public static int compareFrequencies(final Integer frequency1, final String frequencyType1, final Integer frequency2,
26+
final String frequencyType2) {
27+
return frequencyToDays(frequency1, frequencyType1).compareTo(frequencyToDays(frequency2, frequencyType2));
28+
}
29+
30+
public static Integer frequencyToDays(final Integer frequency, final String frequencyType) {
31+
return switch (frequencyType) {
32+
case "DAYS" -> frequency;
33+
case "WEEKS" -> frequency * 7;
34+
case "MONTHS" -> frequency * 30;
35+
case "YEARS" -> frequency * 365;
36+
default -> 0;
37+
};
38+
}
39+
}

fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.fineract.portfolio.common.service;
2020

21+
import java.math.BigDecimal;
2122
import java.util.ArrayList;
2223
import java.util.List;
2324
import java.util.function.Consumer;
@@ -55,4 +56,21 @@ private static List<ApiParameterError> getApiParameterErrors(String resource, Co
5556
baseDataValidator.accept(dataValidatorBuilder);
5657
return dataValidationErrors;
5758
}
59+
60+
public static boolean isChanged(final Object newValue, final Object currentValue) {
61+
if (newValue == null) {
62+
return currentValue != null;
63+
}
64+
return !newValue.equals(currentValue);
65+
}
66+
67+
public static boolean isBigDecimalChanged(final BigDecimal newValue, final BigDecimal currentValue) {
68+
if (newValue == null) {
69+
return currentValue != null;
70+
}
71+
if (currentValue == null) {
72+
return true;
73+
}
74+
return newValue.compareTo(currentValue) != 0;
75+
}
5876
}

fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@
310310
<class>org.apache.fineract.portfolio.workingcapitalloanbreach.domain.WorkingCapitalBreach</class>
311311
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction</class>
312312
<class>org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionAllocation</class>
313+
<class>org.apache.fineract.portfolio.workingcapitalloannearbreach.domain.WorkingCapitalNearBreach</class>
313314
<class>org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProduct</class>
314315
<class>org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductConfigurableAttributes</class>
315316
<class>org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProductPaymentAllocationRule</class>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ public static WorkingCapitalLoanPeriodFrequencyType fromString(final String peri
6565
return null;
6666
}
6767

68+
public long toDays(int amount) {
69+
return switch (this) {
70+
case DAYS -> amount;
71+
case WEEKS -> (long) amount * 7;
72+
case MONTHS -> (long) amount * 30;
73+
case YEARS -> (long) amount * 365;
74+
};
75+
}
76+
6877
public StringEnumOptionData toStringEnumOptionData() {
6978
return new StringEnumOptionData(name(), getCode(), name());
7079
}

fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanbreach/service/WorkingCapitalBreachWritePlatformServiceImpl.java

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
2828
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
2929
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
30+
import org.apache.fineract.portfolio.common.service.Validator;
3031
import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanPeriodFrequencyType;
3132
import org.apache.fineract.portfolio.workingcapitalloanbreach.data.WorkingCapitalBreachRequest;
3233
import org.apache.fineract.portfolio.workingcapitalloanbreach.domain.WorkingCapitalBreach;
@@ -128,49 +129,32 @@ private WorkingCapitalBreach updateAndPersistBreach(final WorkingCapitalBreach i
128129
: null;
129130
final BigDecimal breachAmount = request.breachAmount();
130131

131-
if (isChanged(name, item.getName())) {
132+
if (Validator.isChanged(name, item.getName())) {
132133
validateDuplicateName(name, item.getId());
133134
item.setName(name);
134135
changes.put(NAME_PARAM, name);
135136
}
136-
if (isChanged(breachFrequency, item.getBreachFrequency())) {
137+
if (Validator.isChanged(breachFrequency, item.getBreachFrequency())) {
137138
item.setBreachFrequency(breachFrequency);
138139
changes.put(BREACH_FREQUENCY_PARAM, breachFrequency);
139140
}
140-
if (isChanged(breachFrequencyType, item.getBreachFrequencyType())) {
141+
if (Validator.isChanged(breachFrequencyType, item.getBreachFrequencyType())) {
141142
item.setBreachFrequencyType(breachFrequencyType);
142143
changes.put(BREACH_FREQUENCY_TYPE_PARAM, breachFrequencyType != null ? breachFrequencyType.name() : null);
143144
}
144-
if (isChanged(breachAmountCalculationType, item.getBreachAmountCalculationType())) {
145+
if (Validator.isChanged(breachAmountCalculationType, item.getBreachAmountCalculationType())) {
145146
item.setBreachAmountCalculationType(breachAmountCalculationType);
146147
changes.put(BREACH_AMOUNT_CALCULATION_TYPE_PARAM,
147148
breachAmountCalculationType != null ? breachAmountCalculationType.name() : null);
148149
}
149-
if (isBigDecimalChanged(breachAmount, item.getBreachAmount())) {
150+
if (Validator.isChanged(breachAmount, item.getBreachAmount())) {
150151
item.setBreachAmount(breachAmount);
151152
changes.put(BREACH_AMOUNT_PARAM, breachAmount);
152153
}
153154

154155
return changes.isEmpty() ? item : repository.save(item);
155156
}
156157

157-
private static boolean isChanged(final Object newValue, final Object currentValue) {
158-
if (newValue == null) {
159-
return currentValue != null;
160-
}
161-
return !newValue.equals(currentValue);
162-
}
163-
164-
private static boolean isBigDecimalChanged(final BigDecimal newValue, final BigDecimal currentValue) {
165-
if (newValue == null) {
166-
return currentValue != null;
167-
}
168-
if (currentValue == null) {
169-
return true;
170-
}
171-
return newValue.compareTo(currentValue) != 0;
172-
}
173-
174158
private void validateDuplicateName(final String name, final Long currentId) {
175159
repository.findByName(name).ifPresent(existing -> {
176160
final boolean sameEntity = currentId != null && Objects.equals(existing.getId(), currentId);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.workingcapitalloannearbreach.api;
20+
21+
import io.swagger.v3.oas.annotations.Operation;
22+
import io.swagger.v3.oas.annotations.Parameter;
23+
import io.swagger.v3.oas.annotations.tags.Tag;
24+
import jakarta.ws.rs.Consumes;
25+
import jakarta.ws.rs.DELETE;
26+
import jakarta.ws.rs.GET;
27+
import jakarta.ws.rs.POST;
28+
import jakarta.ws.rs.PUT;
29+
import jakarta.ws.rs.Path;
30+
import jakarta.ws.rs.PathParam;
31+
import jakarta.ws.rs.Produces;
32+
import jakarta.ws.rs.core.MediaType;
33+
import java.util.List;
34+
import lombok.RequiredArgsConstructor;
35+
import org.apache.fineract.commands.domain.CommandWrapper;
36+
import org.apache.fineract.commands.service.CommandWrapperBuilder;
37+
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
38+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
39+
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
40+
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
41+
import org.apache.fineract.portfolio.workingcapitalloannearbreach.data.WorkingCapitalNearBreachData;
42+
import org.apache.fineract.portfolio.workingcapitalloannearbreach.data.WorkingCapitalNearBreachRequest;
43+
import org.apache.fineract.portfolio.workingcapitalloannearbreach.service.WorkingCapitalNearBreachReadPlatformService;
44+
import org.apache.fineract.portfolio.workingcapitalloanproduct.WorkingCapitalLoanProductConstants;
45+
import org.springframework.stereotype.Component;
46+
47+
@Component
48+
@Path("/v1/working-capital/near-breach")
49+
@Tag(name = "Working Capital Near Breach", description = "Working Capital Near Breach")
50+
@RequiredArgsConstructor
51+
public class WorkingCapitalNearBreachApiResource {
52+
53+
private final PlatformSecurityContext context;
54+
private final WorkingCapitalNearBreachReadPlatformService breachReadPlatformService;
55+
private final PortfolioCommandSourceWritePlatformService commandSourceWritePlatformService;
56+
private final DefaultToApiJsonSerializer<String> jsonSerializer;
57+
58+
@GET
59+
@Consumes({ MediaType.APPLICATION_JSON })
60+
@Produces({ MediaType.APPLICATION_JSON })
61+
@Operation(operationId = "retrieveAllWorkingCapitalNearBreaches", summary = "List Working Capital Near Breaches")
62+
public List<WorkingCapitalNearBreachData> retrieveAll() {
63+
this.context.authenticatedUser().validateHasReadPermission(WorkingCapitalLoanProductConstants.WCLP_RESOURCE_NAME);
64+
return this.breachReadPlatformService.retrieveAll();
65+
}
66+
67+
@GET
68+
@Path("{breachId}")
69+
@Consumes({ MediaType.APPLICATION_JSON })
70+
@Produces({ MediaType.APPLICATION_JSON })
71+
@Operation(operationId = "retrieveWorkingCapitalNearBreach", summary = "Retrieve Working Capital Breach")
72+
public WorkingCapitalNearBreachData retrieveOne(@PathParam("breachId") @Parameter(description = "breachId") final Long breachId) {
73+
this.context.authenticatedUser().validateHasReadPermission(WorkingCapitalLoanProductConstants.WCLP_RESOURCE_NAME);
74+
return this.breachReadPlatformService.retrieveOne(breachId);
75+
}
76+
77+
@POST
78+
@Consumes({ MediaType.APPLICATION_JSON })
79+
@Produces({ MediaType.APPLICATION_JSON })
80+
@Operation(operationId = "createWorkingCapitalNearBreach", summary = "Create Working Capital Breach")
81+
public CommandProcessingResult create(final WorkingCapitalNearBreachRequest request) {
82+
this.context.authenticatedUser().validateHasCreatePermission(WorkingCapitalLoanProductConstants.WCLP_RESOURCE_NAME);
83+
final CommandWrapper commandRequest = new CommandWrapperBuilder().createWorkingCapitalNearBreach()
84+
.withJson(jsonSerializer.serialize(request)).build();
85+
return this.commandSourceWritePlatformService.logCommandSource(commandRequest);
86+
}
87+
88+
@PUT
89+
@Path("{breachId}")
90+
@Consumes({ MediaType.APPLICATION_JSON })
91+
@Produces({ MediaType.APPLICATION_JSON })
92+
@Operation(operationId = "updateWorkingCapitalNearBreach", summary = "Update Working Capital Breach")
93+
public CommandProcessingResult update(@PathParam("breachId") @Parameter(description = "breachId") final Long breachId,
94+
final WorkingCapitalNearBreachRequest request) {
95+
this.context.authenticatedUser().validateHasUpdatePermission(WorkingCapitalLoanProductConstants.WCLP_RESOURCE_NAME);
96+
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateWorkingCapitalNearBreach(breachId)
97+
.withJson(jsonSerializer.serialize(request)).build();
98+
return this.commandSourceWritePlatformService.logCommandSource(commandRequest);
99+
}
100+
101+
@DELETE
102+
@Path("{breachId}")
103+
@Consumes({ MediaType.APPLICATION_JSON })
104+
@Produces({ MediaType.APPLICATION_JSON })
105+
@Operation(operationId = "deleteWorkingCapitalNearBreach", summary = "Delete Working Capital Breach")
106+
public CommandProcessingResult delete(@PathParam("breachId") @Parameter(description = "breachId") final Long breachId) {
107+
this.context.authenticatedUser().validateHasDeletePermission(WorkingCapitalLoanProductConstants.WCLP_RESOURCE_NAME);
108+
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteWorkingCapitalNearBreach(breachId).build();
109+
return this.commandSourceWritePlatformService.logCommandSource(commandRequest);
110+
}
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.workingcapitalloannearbreach.data;
20+
21+
import java.math.BigDecimal;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Builder;
24+
import lombok.Getter;
25+
import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
26+
27+
@Getter
28+
@Builder
29+
@AllArgsConstructor
30+
public class WorkingCapitalNearBreachData {
31+
32+
private Long id;
33+
private String name;
34+
private Integer frequency;
35+
private StringEnumOptionData frequencyType;
36+
private BigDecimal threshold;
37+
}

0 commit comments

Comments
 (0)