From 9e949887b9098c24ba79349c0d9a4022b165f194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Alberto=20Hern=C3=A1ndez?= <44206706+josehernandezfintecheandomx@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:16:09 -0500 Subject: [PATCH] Delinquency buckets management for Loans (#2442) Co-authored-by: Jose Alberto Hernandez --- .../service/CommandWrapperBuilder.java | 46 +++ .../core/validator/ParseAndValidator.java | 51 +++ .../api/DelinquencyApiConstants.java | 34 ++ .../api/DelinquencyApiResource.java | 235 +++++++++++++ .../api/DelinquencyApiResourceSwagger.java | 136 ++++++++ .../data/DelinquencyBucketData.java | 38 ++ .../data/DelinquencyRangeData.java | 46 +++ .../delinquency/domain/DelinquencyBucket.java | 58 ++++ .../domain/DelinquencyBucketMappings.java | 60 ++++ .../DelinquencyBucketMappingsRepository.java | 30 ++ .../domain/DelinquencyBucketRepository.java | 28 ++ .../delinquency/domain/DelinquencyRange.java | 62 ++++ .../domain/DelinquencyRangeRepository.java | 28 ++ .../domain/LoanDelinquencyTagHistory.java | 66 ++++ .../LoanDelinquencyTagHistoryRepository.java | 39 +++ ...linquencyBucketAgesOverlapedException.java | 28 ++ .../DelinquencyBucketNotFoundException.java | 39 +++ .../DelinquencyRangeInvalidAgesException.java | 28 ++ .../DelinquencyRangeNotFoundException.java | 38 ++ ...CreateDelinquencyBucketCommandHandler.java | 42 +++ .../CreateDelinquencyRangeCommandHandler.java | 42 +++ ...DeleteDelinquencyBucketCommandHandler.java | 42 +++ .../DeleteDelinquencyRangeCommandHandler.java | 42 +++ ...UpdateDelinquencyBucketCommandHandler.java | 42 +++ .../UpdateDelinquencyRangeCommandHandler.java | 42 +++ .../mapper/DelinquencyBucketMapper.java | 36 ++ .../mapper/DelinquencyRangeMapper.java | 38 ++ .../DelinquencyReadPlatformService.java | 36 ++ .../DelinquencyReadPlatformServiceImpl.java | 125 +++++++ .../DelinquencyWritePlatformService.java | 40 +++ .../DelinquencyWritePlatformServiceImpl.java | 278 +++++++++++++++ .../DelinquencyBucketParseAndValidator.java | 76 ++++ .../DelinquencyRangeParseAndValidator.java | 77 +++++ .../loanaccount/data/LoanAccountData.java | 43 ++- .../RecalculateInterestForLoanTasklet.java | 2 +- .../service/LoanReadPlatformServiceImpl.java | 23 +- .../loanproduct/LoanProductConstants.java | 1 + .../api/LoanProductsApiResource.java | 15 +- .../loanproduct/data/LoanProductData.java | 53 +-- .../loanproduct/domain/LoanProduct.java | 22 ++ .../domain/LoanProductRepository.java | 3 + .../LoanProductDataValidator.java | 16 +- .../LoanProductReadPlatformServiceImpl.java | 31 +- ...WritePlatformServiceJpaRepositoryImpl.java | 13 +- .../GenerateAdhocClientScheduleTasklet.java | 2 +- .../useradministration/domain/AppUser.java | 4 + .../db/changelog/tenant/changelog-tenant.xml | 1 + .../parts/0029_add_delinquency_buckets.xml | 325 ++++++++++++++++++ .../DelinquencyBucketsIntegrationTest.java | 211 ++++++++++++ .../integrationtests/common/Utils.java | 5 + .../products/DelinquencyBucketsHelper.java | 91 +++++ .../products/DelinquencyRangesHelper.java | 95 +++++ 52 files changed, 2945 insertions(+), 59 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappings.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappingsRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistory.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketAgesOverlapedException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeInvalidAgesException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyBucketCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyRangeCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyBucketCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyRangeCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyBucketCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyRangeCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyRangeMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0029_add_delinquency_buckets.xml create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyRangesHelper.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 81746b1f5..06dff8f3b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3494,4 +3494,50 @@ public class CommandWrapperBuilder { return this; } + public CommandWrapperBuilder createDelinquencyRange() { + this.actionName = "CREATE"; + this.entityName = "DELINQUENCY_RANGE"; + this.href = "/delinquency/range"; + return this; + } + + public CommandWrapperBuilder updateDelinquencyRange(final Long delinquencyRangeId) { + this.actionName = "UPDATE"; + this.entityName = "DELINQUENCY_RANGE"; + this.entityId = delinquencyRangeId; + this.href = "/delinquency/range"; + return this; + } + + public CommandWrapperBuilder deleteDelinquencyRange(final Long delinquencyRangeId) { + this.actionName = "DELETE"; + this.entityName = "DELINQUENCY_RANGE"; + this.entityId = delinquencyRangeId; + this.href = "/delinquency/range"; + return this; + } + + public CommandWrapperBuilder createDelinquencyBucket() { + this.actionName = "CREATE"; + this.entityName = "DELINQUENCY_BUCKET"; + this.href = "/delinquency/bucket"; + return this; + } + + public CommandWrapperBuilder updateDelinquencyBucket(final Long delinquencyBucketId) { + this.actionName = "UPDATE"; + this.entityName = "DELINQUENCY_BUCKET"; + this.entityId = delinquencyBucketId; + this.href = "/delinquency/bucket"; + return this; + } + + public CommandWrapperBuilder deleteDelinquencyBucket(final Long delinquencyBucketId) { + this.actionName = "DELETE"; + this.entityName = "DELINQUENCY_BUCKET"; + this.entityId = delinquencyBucketId; + this.href = "/delinquency/bucket"; + return this; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java new file mode 100644 index 000000000..dbc6b285a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/validator/ParseAndValidator.java @@ -0,0 +1,51 @@ +/** + * 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.infrastructure.core.validator; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; + +@Slf4j +public class ParseAndValidator { + + protected JsonObject extractJsonObject(final FromJsonHelper jsonHelper, final JsonCommand command) { + String json = command.json(); + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final JsonElement element = jsonHelper.parse(json); + return element.getAsJsonObject(); + } + + protected void throwExceptionIfValidationWarningsExist(DataValidatorBuilder dataValidator) { + if (dataValidator.hasError()) { + log.error("Validation errors: {}", dataValidator.getDataValidationErrors()); + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidator.getDataValidationErrors()); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java new file mode 100644 index 000000000..d4bd7d410 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java @@ -0,0 +1,34 @@ +/** + * 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.delinquency.api; + +public final class DelinquencyApiConstants { + + private DelinquencyApiConstants() { + + } + + public static final String NAME_PARAM_NAME = "name"; + public static final String CLASSIFICATION_PARAM_NAME = "classification"; + public static final String RANGES_PARAM_NAME = "ranges"; + public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays"; + public static final String MAXIMUMAGEDAYS_PARAM_NAME = "maximumAgeDays"; + public static final String LOCALE_PARAM_NAME = "locale"; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java new file mode 100644 index 000000000..937361e8c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java @@ -0,0 +1,235 @@ +/** + * 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.delinquency.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +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.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Collection; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +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.api.ApiRequestParameterHelper; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Path("delinquency") +@Component +@Tag(name = "Delinquency Range and Buckets Management", description = "Delinquency Range and Buckets management enables you to set up, fetch and adjust Delinquency overdue ranges") +public class DelinquencyApiResource { + + private final ApiRequestParameterHelper parameterHelper; + private final PlatformSecurityContext securityContext; + private final DefaultToApiJsonSerializer jsonSerializerBucket; + private final DefaultToApiJsonSerializer jsonSerializerRange; + private final DelinquencyReadPlatformService readPlatformService; + private final PortfolioCommandSourceWritePlatformService commandWritePlatformService; + + @GET + @Path("ranges") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "List all Delinquency Ranges", description = "") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyRangesResponse.class)))) }) + public String getDelinquencyRanges(@Context final UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasReadPermission("DELINQUENCY_BUCKET"); + final Collection delinquencyRangeData = this.readPlatformService.retrieveAllDelinquencyRanges(); + ApiRequestJsonSerializationSettings settings = parameterHelper.process(uriInfo.getQueryParameters()); + return this.jsonSerializerRange.serialize(settings, delinquencyRangeData); + } + + @GET + @Path("ranges/{delinquencyRangeId}") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Retrieve a specific Delinquency Range based on the Id", description = "") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyRangesResponse.class))) }) + public String getDelinquencyRange( + @PathParam("delinquencyRangeId") @Parameter(description = "delinquencyRangeId") final Long delinquencyRangeId, + @Context final UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasReadPermission("DELINQUENCY_BUCKET"); + final DelinquencyRangeData delinquencyRangeData = this.readPlatformService.retrieveDelinquencyRange(delinquencyRangeId); + ApiRequestJsonSerializationSettings settings = parameterHelper.process(uriInfo.getQueryParameters()); + return this.jsonSerializerRange.serialize(settings, delinquencyRangeData); + } + + @POST + @Path("ranges") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create Delinquency Range", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyRangeRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyRangeResponse.class))) }) + public String createDelinquencyRange(final String jsonRequestBody, @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasCreatePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().createDelinquencyRange().withJson(jsonRequestBody).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + + @PUT + @Path("ranges/{delinquencyRangeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update Delinquency Range based on the Id", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyRangeRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PutDelinquencyRangeResponse.class))) }) + public String updateDelinquencyRange( + @PathParam("delinquencyRangeId") @Parameter(description = "delinquencyRangeId") final Long delinquencyRangeId, + final String jsonRequestBody, @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasUpdatePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateDelinquencyRange(delinquencyRangeId) + .withJson(jsonRequestBody).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + + @DELETE + @Path("ranges/{delinquencyRangeId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update Delinquency Range based on the Id", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyRangeRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.DeleteDelinquencyRangeResponse.class))) }) + public String deleteDelinquencyRange( + @PathParam("delinquencyRangeId") @Parameter(description = "delinquencyRangeId") final Long delinquencyRangeId, + @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasDeletePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteDelinquencyRange(delinquencyRangeId).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + + @GET + @Path("buckets") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "List all Delinquency Buckets", description = "") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyBucketsResponse.class)))) }) + public String getDelinquencyBuckets(@Context final UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasReadPermission("DELINQUENCY_BUCKET"); + final Collection delinquencyBucketData = this.readPlatformService.retrieveAllDelinquencyBuckets(); + ApiRequestJsonSerializationSettings settings = parameterHelper.process(uriInfo.getQueryParameters()); + return this.jsonSerializerBucket.serialize(settings, delinquencyBucketData); + } + + @GET + @Path("buckets/{delinquencyBucketId}") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Retrieve a specific Delinquency Bucket based on the Id", description = "") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyBucketsResponse.class))) }) + public String getDelinquencyBucket( + @PathParam("delinquencyBucketId") @Parameter(description = "delinquencyBucketId") final Long delinquencyBucketId, + @Context final UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasReadPermission("DELINQUENCY_BUCKET"); + final DelinquencyBucketData delinquencyBucketData = this.readPlatformService.retrieveDelinquencyBucket(delinquencyBucketId); + ApiRequestJsonSerializationSettings settings = parameterHelper.process(uriInfo.getQueryParameters()); + return this.jsonSerializerBucket.serialize(settings, delinquencyBucketData); + } + + @POST + @Path("buckets") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Create Delinquency Bucket", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyBucketRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyBucketResponse.class))) }) + public String createDelinquencyBucket(final String jsonRequestBody, @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasCreatePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().createDelinquencyBucket().withJson(jsonRequestBody).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + + @PUT + @Path("buckets/{delinquencyBucketId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update Delinquency Bucket based on the Id", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyBucketRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PutDelinquencyBucketResponse.class))) }) + public String updateDelinquencyBucket( + @PathParam("delinquencyBucketId") @Parameter(description = "delinquencyBucketId") final Long delinquencyBucketId, + final String jsonRequestBody, @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasUpdatePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().updateDelinquencyBucket(delinquencyBucketId) + .withJson(jsonRequestBody).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + + @DELETE + @Path("buckets/{delinquencyBucketId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Delete Delinquency Bucket based on the Id", description = "") + @RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostDelinquencyBucketRequest.class))) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.DeleteDelinquencyBucketResponse.class))) }) + public String deleteDelinquencyBucket( + @PathParam("delinquencyBucketId") @Parameter(description = "delinquencyBucketId") final Long delinquencyBucketId, + @Context UriInfo uriInfo) { + securityContext.authenticatedUser().validateHasDeletePermission("DELINQUENCY_BUCKET"); + final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteDelinquencyBucket(delinquencyBucketId).build(); + + CommandProcessingResult result = commandWritePlatformService.logCommandSource(commandRequest); + return jsonSerializerRange.serialize(result); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java new file mode 100644 index 000000000..6cbbfeca7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java @@ -0,0 +1,136 @@ +/** + * 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.delinquency.api; + +import io.swagger.v3.oas.annotations.media.Schema; + +public final class DelinquencyApiResourceSwagger { + + private DelinquencyApiResourceSwagger() {} + + @Schema(description = "GetDelinquencyRangesResponse") + public static final class GetDelinquencyRangesResponse { + + private GetDelinquencyRangesResponse() {} + + @Schema(example = "1") + public Integer id; + @Schema(example = "Delinquent 1") + public String classification; + @Schema(example = "1") + public Long minimumAgeDays; + @Schema(example = "3") + public Long maximumAgeDays; + } + + @Schema(description = "GetDelinquencyBucketsResponse") + public static final class GetDelinquencyBucketsResponse { + + private GetDelinquencyBucketsResponse() {} + + @Schema(example = "1") + public Integer id; + @Schema(example = "Delinquent Bucket Set 1") + public String name; + + public GetDelinquencyRangesResponse[] ranges; + } + + @Schema(description = "PostDelinquencyRangeRequest") + public static final class PostDelinquencyRangeRequest { + + private PostDelinquencyRangeRequest() {} + + @Schema(example = "Delinquent 1") + public String classification; + @Schema(example = "1") + public Integer minimumAgeDays; + @Schema(example = "3") + public Integer maximumAgeDays; + @Schema(example = "en") + public String locale; + } + + @Schema(description = "PostDelinquencyRangeResponse") + public static final class PostDelinquencyRangeResponse { + + private PostDelinquencyRangeResponse() {} + + @Schema(example = "1") + public Integer resourceId; + } + + @Schema(description = "PutDelinquencyRangeResponse") + public static final class PutDelinquencyRangeResponse { + + private PutDelinquencyRangeResponse() {} + + @Schema(example = "1") + public Integer resourceId; + public PostDelinquencyRangeRequest changes; + } + + @Schema(description = "DeleteDelinquencyRangeResponse") + public static final class DeleteDelinquencyRangeResponse { + + private DeleteDelinquencyRangeResponse() {} + + @Schema(example = "1") + public Integer resourceId; + } + + @Schema(description = "PostDelinquencyBucketRequest") + public static final class PostDelinquencyBucketRequest { + + private PostDelinquencyBucketRequest() {} + + @Schema(example = "Delinquent 1") + public String name; + @Schema(example = "[1,2,3]") + public Long[] ranges; + } + + @Schema(description = "PostDelinquencyBucketResponse") + public static final class PostDelinquencyBucketResponse { + + private PostDelinquencyBucketResponse() {} + + @Schema(example = "1") + public Integer resourceId; + } + + @Schema(description = "PutDelinquencyBucketResponse") + public static final class PutDelinquencyBucketResponse { + + private PutDelinquencyBucketResponse() {} + + @Schema(example = "1") + public Integer resourceId; + } + + @Schema(description = "DeleteDelinquencyBucketResponse") + public static final class DeleteDelinquencyBucketResponse { + + private DeleteDelinquencyBucketResponse() {} + + @Schema(example = "1") + public Integer resourceId; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java new file mode 100644 index 000000000..b078343cf --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java @@ -0,0 +1,38 @@ +/** + * 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.delinquency.data; + +import java.io.Serializable; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@AllArgsConstructor +@Getter +@Setter +public class DelinquencyBucketData implements Serializable { + + private Long id; + private String name; + private List ranges; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java new file mode 100644 index 000000000..52a11cddd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyRangeData.java @@ -0,0 +1,46 @@ +/** + * 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.delinquency.data; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@AllArgsConstructor +@Getter +@Setter +public class DelinquencyRangeData implements Serializable { + + private Long id; + private String classification; + private Integer minimumAgeDays; + private Integer maximumAgeDays; + + public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays) { + return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays); + } + + public static DelinquencyRangeData reference(Long id) { + return new DelinquencyRangeData(id, "", 0, 0); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java new file mode 100644 index 000000000..8f7d4968f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java @@ -0,0 +1,58 @@ +/** + * 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.delinquency.domain; + +import java.util.Set; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_delinquency_bucket", uniqueConstraints = { + @UniqueConstraint(name = "uq_delinquency_bucket_name", columnNames = { "name" }) }) +public class DelinquencyBucket extends AbstractAuditableWithUTCDateTimeCustom { + + @Column(name = "name", nullable = false) + private String name; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "m_delinquency_bucket_mappings", joinColumns = @JoinColumn(name = "delinquency_bucket_id"), inverseJoinColumns = @JoinColumn(name = "delinquency_range_id")) + private Set ranges; + + @Version + private Long version; + + public DelinquencyBucket(String name) { + this.name = name; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappings.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappings.java new file mode 100644 index 000000000..9fae6b3e7 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappings.java @@ -0,0 +1,60 @@ +/** + * 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.delinquency.domain; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_delinquency_bucket_mappings", uniqueConstraints = { + @UniqueConstraint(name = "uq_delinquency_bucket_mapping", columnNames = { "delinquencyBucket", "delinquencyRange" }) }) +public class DelinquencyBucketMappings extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne + @JoinColumn(name = "delinquency_bucket_id", nullable = false) + private DelinquencyBucket delinquencyBucket; + + @ManyToOne + @JoinColumn(name = "delinquency_range_id", nullable = false) + private DelinquencyRange delinquencyRange; + + @Version + private Long version; + + public DelinquencyBucketMappings(DelinquencyBucket delinquencyBucket, DelinquencyRange delinquencyRange) { + this.delinquencyBucket = delinquencyBucket; + this.delinquencyRange = delinquencyRange; + } + + public static DelinquencyBucketMappings instance(DelinquencyBucket delinquencyBucket, DelinquencyRange delinquencyRange) { + return new DelinquencyBucketMappings(delinquencyBucket, delinquencyRange); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappingsRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappingsRepository.java new file mode 100644 index 000000000..e83adb0de --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketMappingsRepository.java @@ -0,0 +1,30 @@ +/** + * 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.delinquency.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface DelinquencyBucketMappingsRepository + extends JpaRepository, JpaSpecificationExecutor { + + List findByDelinquencyBucket(DelinquencyBucket delinquencyBucket); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketRepository.java new file mode 100644 index 000000000..e7aadee33 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketRepository.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.delinquency.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface DelinquencyBucketRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByName(String name); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java new file mode 100644 index 000000000..83562b980 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java @@ -0,0 +1,62 @@ +/** + * 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.delinquency.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_delinquency_range", uniqueConstraints = { + @UniqueConstraint(name = "uq_delinquency_range_classification", columnNames = { "classification" }) }) +public class DelinquencyRange extends AbstractAuditableWithUTCDateTimeCustom { + + @Column(name = "classification", nullable = false) + private String classification; + + @Column(name = "min_age_days", nullable = false) + private Integer minimumAgeDays; + + @Column(name = "max_age_days", nullable = true) + private Integer maximumAgeDays; + + @Version + private Long version; + + protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays) { + this.classification = classification; + this.minimumAgeDays = minimumAgeDays; + this.maximumAgeDays = maximumAgeDays; + } + + public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge) { + return new DelinquencyRange(classification, minimumAge, maximumAge); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java new file mode 100644 index 000000000..9333f08ff --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.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.delinquency.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface DelinquencyRangeRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByClassification(String classification); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistory.java new file mode 100644 index 000000000..9ea27b0ad --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistory.java @@ -0,0 +1,66 @@ +/** + * 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.delinquency.domain; + +import java.time.LocalDate; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.Version; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_loan_delinquency_tag_history") +public class LoanDelinquencyTagHistory extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "delinquency_range_id", nullable = false) + private DelinquencyRange delinquencyRange; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "loan_id", nullable = false) + private Loan loan; + + @Column(name = "addedon_date", nullable = false) + private LocalDate addedOnDate; + + @Column(name = "liftedon_date", nullable = true) + private LocalDate liftedOnDate; + + @Version + private Long version; + + public LoanDelinquencyTagHistory(DelinquencyRange delinquencyRange, Loan loan, LocalDate addedOnDate, LocalDate liftedOnDate) { + this.delinquencyRange = delinquencyRange; + this.loan = loan; + this.addedOnDate = addedOnDate; + this.liftedOnDate = liftedOnDate; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java new file mode 100644 index 000000000..fe13827f2 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java @@ -0,0 +1,39 @@ +/** + * 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.delinquency.domain; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanDelinquencyTagHistoryRepository + extends JpaRepository, JpaSpecificationExecutor { + + List findByLoanOrderByAddedOnDateDesc(Loan loan); + + Optional findByLoanAndLiftedOnDate(Loan loan, LocalDate liftedOnDate); + + Long countByDelinquencyRangeAndLiftedOnDate(DelinquencyRange delinquencyRange, LocalDate liftedOnDate); + + Long countByDelinquencyRange(DelinquencyRange delinquencyRange); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketAgesOverlapedException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketAgesOverlapedException.java new file mode 100644 index 000000000..1757295ea --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketAgesOverlapedException.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.delinquency.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class DelinquencyBucketAgesOverlapedException extends AbstractPlatformDomainRuleException { + + public DelinquencyBucketAgesOverlapedException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loan.delinquency.bucket.ages.overlaped", defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketNotFoundException.java new file mode 100644 index 000000000..5e27bc4f3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyBucketNotFoundException.java @@ -0,0 +1,39 @@ +/** + * 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.delinquency.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class DelinquencyBucketNotFoundException extends AbstractPlatformResourceNotFoundException { + + public DelinquencyBucketNotFoundException(String globalisationMessageCode, String defaultUserMessage, + Object... defaultUserMessageArgs) { + super(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs); + } + + public static DelinquencyBucketNotFoundException notExist(final Long id, Throwable... e) { + return new DelinquencyBucketNotFoundException("error.msg.delinquency.bucket.id.not.exist", + "Delinquency bucket with id `" + id + "` does not exist.", id, e); + } + + public static DelinquencyBucketNotFoundException notFound(final Long id, Throwable... e) { + return new DelinquencyBucketNotFoundException("error.msg.delinquency.bucket.not.found", + "Delinquency bucket with id `" + id + "` does not found.", id, e); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeInvalidAgesException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeInvalidAgesException.java new file mode 100644 index 000000000..ee469c6bd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeInvalidAgesException.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.delinquency.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class DelinquencyRangeInvalidAgesException extends AbstractPlatformDomainRuleException { + + public DelinquencyRangeInvalidAgesException(final String defaultUserMessage, final Object... defaultUserMessageArgs) { + super("error.msg.loan.delinquency.age.days,invalid", defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeNotFoundException.java new file mode 100644 index 000000000..df6e8a952 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/exception/DelinquencyRangeNotFoundException.java @@ -0,0 +1,38 @@ +/** + * 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.delinquency.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class DelinquencyRangeNotFoundException extends AbstractPlatformResourceNotFoundException { + + public DelinquencyRangeNotFoundException(String globalisationMessageCode, String defaultUserMessage, Object... defaultUserMessageArgs) { + super(globalisationMessageCode, defaultUserMessage, defaultUserMessageArgs); + } + + public static DelinquencyRangeNotFoundException notExist(final Long id, Throwable... e) { + return new DelinquencyRangeNotFoundException("error.msg.delinquency.range.id.not.exist", + "Delinquency range with id `" + id + "` does not exist.", id, e); + } + + public static DelinquencyRangeNotFoundException notFound(final Long id, Throwable... e) { + return new DelinquencyRangeNotFoundException("error.msg.delinquency.range.not.found", + "Delinquency range with id `" + id + "` does not found.", id, e); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyBucketCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyBucketCommandHandler.java new file mode 100644 index 000000000..72eca2f4a --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyBucketCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_BUCKET", action = "CREATE") +public class CreateDelinquencyBucketCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.createDelinquencyBucket(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyRangeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyRangeCommandHandler.java new file mode 100644 index 000000000..39ee47504 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyRangeCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_RANGE", action = "CREATE") +public class CreateDelinquencyRangeCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.createDelinquencyRange(command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyBucketCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyBucketCommandHandler.java new file mode 100644 index 000000000..b18a1c482 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyBucketCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_BUCKET", action = "DELETE") +public class DeleteDelinquencyBucketCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.deleteDelinquencyBucket(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyRangeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyRangeCommandHandler.java new file mode 100644 index 000000000..ae0f58b3f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/DeleteDelinquencyRangeCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_RANGE", action = "DELETE") +public class DeleteDelinquencyRangeCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.deleteDelinquencyRange(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyBucketCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyBucketCommandHandler.java new file mode 100644 index 000000000..8b8891bf8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyBucketCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_BUCKET", action = "UPDATE") +public class UpdateDelinquencyBucketCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.updateDelinquencyBucket(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyRangeCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyRangeCommandHandler.java new file mode 100644 index 000000000..8f3f9d129 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateDelinquencyRangeCommandHandler.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.delinquency.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.delinquency.service.DelinquencyWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@CommandType(entity = "DELINQUENCY_RANGE", action = "UPDATE") +public class UpdateDelinquencyRangeCommandHandler implements NewCommandSourceHandler { + + private final DelinquencyWritePlatformService writePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.updateDelinquencyRange(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java new file mode 100644 index 000000000..cf306ff93 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java @@ -0,0 +1,36 @@ +/** + * 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.delinquency.mapper; + +import java.util.List; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; + +@Mapper(componentModel = "spring") +public interface DelinquencyBucketMapper { + + @Mappings({ @Mapping(target = "id", source = "source.id"), @Mapping(target = "name", source = "source.name") }) + DelinquencyBucketData map(DelinquencyBucket source); + + List map(List sources); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyRangeMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyRangeMapper.java new file mode 100644 index 000000000..8717e9e3e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyRangeMapper.java @@ -0,0 +1,38 @@ +/** + * 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.delinquency.mapper; + +import java.util.List; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; + +@Mapper(componentModel = "spring") +public interface DelinquencyRangeMapper { + + @Mappings({ @Mapping(target = "id", source = "source.id"), @Mapping(target = "classification", source = "source.classification"), + @Mapping(target = "minimumAgeDays", source = "source.minimumAgeDays"), + @Mapping(target = "maximumAgeDays", source = "source.maximumAgeDays") }) + DelinquencyRangeData map(DelinquencyRange source); + + List map(List sources); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java new file mode 100644 index 000000000..f05e06b64 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java @@ -0,0 +1,36 @@ +/** + * 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.delinquency.service; + +import java.util.Collection; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; + +public interface DelinquencyReadPlatformService { + + Collection retrieveAllDelinquencyRanges(); + + DelinquencyRangeData retrieveDelinquencyRange(Long delinquencyRangeId); + + Collection retrieveAllDelinquencyBuckets(); + + DelinquencyBucketData retrieveDelinquencyBucket(Long delinquencyBucketId); + + DelinquencyRangeData retrieveCurrentDelinquencyTag(Long loanId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java new file mode 100644 index 000000000..8f1f64984 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -0,0 +1,125 @@ +/** + * 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.delinquency.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; +import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketNotFoundException; +import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper; +import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +@Transactional(readOnly = true) +public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final DelinquencyRangeRepository repositoryRange; + private final DelinquencyBucketRepository repositoryBucket; + private final LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory; + private final DelinquencyRangeMapper mapperRange; + private final DelinquencyBucketMapper mapperBucket; + private final LoanAssembler loanAssembler; + + @Override + public Collection retrieveAllDelinquencyRanges() { + final List delinquencyRangeList = repositoryRange.findAll(); + return mapperRange.map(delinquencyRangeList); + } + + @Override + public DelinquencyRangeData retrieveDelinquencyRange(Long delinquencyRangeId) { + DelinquencyRange delinquencyRangeList = repositoryRange.getReferenceById(delinquencyRangeId); + return mapperRange.map(delinquencyRangeList); + } + + @Override + public Collection retrieveAllDelinquencyBuckets() { + final List delinquencyRangeList = repositoryBucket.findAll(); + return mapperBucket.map(delinquencyRangeList); + } + + @Override + public DelinquencyBucketData retrieveDelinquencyBucket(Long delinquencyBucketId) { + try { + final DelinquencyBucket delinquencyBucket = repositoryBucket.getReferenceById(delinquencyBucketId); + final DelinquencyBucketData delinquencyBucketData = mapperBucket.map(delinquencyBucket); + + final DelinquencyBucketMappingMapper rmMappings = new DelinquencyBucketMappingMapper(); + final String sql = "select " + rmMappings.schema() + " where buc.id = ? order by dran.min_age_days"; + List ranges = this.jdbcTemplate.query(sql, rmMappings, delinquencyBucketId); // NOSONAR + delinquencyBucketData.setRanges(ranges); + + return delinquencyBucketData; + } catch (final EmptyResultDataAccessException e) { + throw DelinquencyBucketNotFoundException.notFound(delinquencyBucketId, e); + } + } + + @Override + public DelinquencyRangeData retrieveCurrentDelinquencyTag(Long loanId) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + Optional optLoanDelinquencyTag = this.repositoryLoanDelinquencyTagHistory.findByLoanAndLiftedOnDate(loan, + null); + if (optLoanDelinquencyTag.isPresent()) { + return mapperRange.map(optLoanDelinquencyTag.get().getDelinquencyRange()); + } + return null; + } + + private static final class DelinquencyBucketMappingMapper implements RowMapper { + + public String schema() { + return " dran.id as id, dran.classification as classification, dran.min_age_days as minimumAgeDays, dran.max_age_days as maximumAgeDays from m_delinquency_bucket buc " + + " inner join m_delinquency_bucket_mappings dmap on dmap.delinquency_bucket_id = buc.id " + + " inner join m_delinquency_range dran on dran.id = dmap.delinquency_range_id "; + } + + @Override + public DelinquencyRangeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + + final Long id = rs.getLong("id"); + final String classification = rs.getString("classification"); + final Integer minimumAgeDays = rs.getInt("minimumAgeDays"); + final Integer maximumAgeDays = rs.getInt("maximumAgeDays"); + + return new DelinquencyRangeData(id, classification, minimumAgeDays, maximumAgeDays); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java new file mode 100644 index 000000000..e72bc38f8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java @@ -0,0 +1,40 @@ +/** + * 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.delinquency.service; + +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; + +public interface DelinquencyWritePlatformService { + + CommandProcessingResult createDelinquencyRange(JsonCommand command); + + CommandProcessingResult updateDelinquencyRange(Long delinquencyRangeId, JsonCommand command); + + CommandProcessingResult deleteDelinquencyRange(Long delinquencyRangeId, JsonCommand command); + + CommandProcessingResult setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId, JsonCommand command); + + CommandProcessingResult createDelinquencyBucket(JsonCommand command); + + CommandProcessingResult updateDelinquencyBucket(Long delinquencyBucketId, JsonCommand command); + + CommandProcessingResult deleteDelinquencyBucket(Long delinquencyBucketId, JsonCommand command); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java new file mode 100644 index 000000000..ed981a5a5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -0,0 +1,278 @@ +/** + * 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.delinquency.service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +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.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappings; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; +import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException; +import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException; +import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator; +import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlatformService { + + private final DelinquencyBucketParseAndValidator dataValidatorBucket; + private final DelinquencyRangeParseAndValidator dataValidatorRange; + + private final DelinquencyRangeRepository repositoryRange; + private final DelinquencyBucketRepository repositoryBucket; + private final DelinquencyBucketMappingsRepository repositoryBucketMappings; + private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository; + private final LoanRepositoryWrapper loanRepository; + private final LoanProductRepository loanProductRepository; + + @Override + public CommandProcessingResult createDelinquencyRange(JsonCommand command) { + DelinquencyRangeData data = dataValidatorRange.validateAndParseUpdate(command); + Map changes = new HashMap<>(); + DelinquencyRange delinquencyRange = createDelinquencyRange(data, changes); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).with(changes) + .build(); + } + + @Override + public CommandProcessingResult updateDelinquencyRange(Long delinquencyRangeId, JsonCommand command) { + DelinquencyRangeData data = dataValidatorRange.validateAndParseUpdate(command); + DelinquencyRange delinquencyRange = this.repositoryRange.getReferenceById(delinquencyRangeId); + Map changes = new HashMap<>(); + delinquencyRange = updateDelinquencyRange(delinquencyRange, data, changes); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).with(changes) + .build(); + } + + @Override + public CommandProcessingResult deleteDelinquencyRange(Long delinquencyRangeId, JsonCommand command) { + final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId); + if (delinquencyRange != null) { + final Long delinquencyRangeLinked = this.loanDelinquencyTagRepository.countByDelinquencyRange(delinquencyRange); + if (delinquencyRangeLinked > 0) { + throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.linked", + "Data integrity issue with resource: " + delinquencyRange.getId()); + } + repositoryRange.delete(delinquencyRange); + } + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyRange.getId()).build(); + } + + @Override + public CommandProcessingResult setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId, JsonCommand command) { + setLoanDelinquencyTag(loanId, delinquencyRangeId); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loanId).build(); + } + + @Override + public CommandProcessingResult createDelinquencyBucket(JsonCommand command) { + DelinquencyBucketData data = dataValidatorBucket.validateAndParseUpdate(command); + Map changes = new HashMap<>(); + DelinquencyBucket delinquencyBucket = createDelinquencyBucket(data, changes); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).with(changes) + .build(); + } + + @Override + public CommandProcessingResult updateDelinquencyBucket(Long delinquencyBucketId, JsonCommand command) { + DelinquencyBucketData data = dataValidatorBucket.validateAndParseUpdate(command); + DelinquencyBucket delinquencyBucket = this.repositoryBucket.getReferenceById(delinquencyBucketId); + + Map changes = new HashMap<>(); + delinquencyBucket = updateDelinquencyBucket(delinquencyBucket, data, changes); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).with(changes) + .build(); + } + + @Override + public CommandProcessingResult deleteDelinquencyBucket(Long delinquencyBucketId, JsonCommand command) { + final DelinquencyBucket delinquencyBucket = repositoryBucket.getReferenceById(delinquencyBucketId); + if (delinquencyBucket != null) { + Long delinquencyBucketLinked = this.loanProductRepository.countByDelinquencyBucket(delinquencyBucket); + if (delinquencyBucketLinked > 0) { + throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.linked", + "Data integrity issue with resource: " + delinquencyBucket.getId()); + } + repositoryBucket.delete(delinquencyBucket); + } + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).build(); + } + + private DelinquencyRange createDelinquencyRange(DelinquencyRangeData data, Map changes) { + Optional delinquencyRange = repositoryRange.findByClassification(data.getClassification()); + + if (delinquencyRange.isEmpty()) { + if (data.getMinimumAgeDays() > data.getMaximumAgeDays()) { + final String errorMessage = "The age days values are invalid, the maximum age days can't be lower than minimum age days"; + throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays()); + } + DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(), + data.getMaximumAgeDays()); + return repositoryRange.saveAndFlush(newDelinquencyRange); + } else { + throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated", + "Data integrity issue with resource: " + delinquencyRange.get().getId()); + } + } + + private DelinquencyRange updateDelinquencyRange(DelinquencyRange delinquencyRange, DelinquencyRangeData data, + Map changes) { + if (!data.getClassification().equalsIgnoreCase(delinquencyRange.getClassification())) { + delinquencyRange.setClassification(data.getClassification()); + changes.put(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, data.getClassification()); + } + if (!data.getMinimumAgeDays().equals(delinquencyRange.getMinimumAgeDays())) { + delinquencyRange.setMinimumAgeDays(data.getMinimumAgeDays()); + changes.put(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, data.getMinimumAgeDays()); + } + if (!data.getMaximumAgeDays().equals(delinquencyRange.getMaximumAgeDays())) { + delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays()); + changes.put(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, data.getMaximumAgeDays()); + } + if (!changes.isEmpty()) { + delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange); + } + return delinquencyRange; + } + + private DelinquencyBucket createDelinquencyBucket(DelinquencyBucketData data, Map changes) { + Optional delinquencyBucket = repositoryBucket.findByName(data.getName()); + + if (delinquencyBucket.isEmpty()) { + DelinquencyBucket newDelinquencyBucket = new DelinquencyBucket(data.getName()); + repositoryBucket.save(newDelinquencyBucket); + setDelinquencyBucketMappings(newDelinquencyBucket, data); + return newDelinquencyBucket; + } else { + throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated", + "Data integrity issue with resource: " + delinquencyBucket.get().getId()); + } + } + + private DelinquencyBucket updateDelinquencyBucket(DelinquencyBucket delinquencyBucket, DelinquencyBucketData data, + Map changes) { + if (!data.getName().equalsIgnoreCase(delinquencyBucket.getName())) { + delinquencyBucket.setName(data.getName()); + changes.put(DelinquencyApiConstants.NAME_PARAM_NAME, data.getName()); + } + if (!changes.isEmpty()) { + delinquencyBucket = repositoryBucket.save(delinquencyBucket); + } + setDelinquencyBucketMappings(delinquencyBucket, data); + return delinquencyBucket; + } + + private void setDelinquencyBucketMappings(DelinquencyBucket delinquencyBucket, DelinquencyBucketData data) { + List rangeIds = new ArrayList<>(); + data.getRanges().forEach(dataRange -> { + rangeIds.add(dataRange.getId()); + }); + + List ranges = repositoryRange.findAllById(rangeIds); + validateDelinquencyRanges(ranges); + List bucketMappings = repositoryBucketMappings.findByDelinquencyBucket(delinquencyBucket); + repositoryBucketMappings.deleteAll(bucketMappings); + bucketMappings.clear(); + for (DelinquencyRange delinquencyRange : ranges) { + bucketMappings.add(DelinquencyBucketMappings.instance(delinquencyBucket, delinquencyRange)); + } + repositoryBucketMappings.saveAllAndFlush(bucketMappings); + } + + private void validateDelinquencyRanges(List ranges) { + // Sort the ranges based on the minAgeDays + final Comparator orderByMinAge = new Comparator() { + + @Override + public int compare(DelinquencyRange o1, DelinquencyRange o2) { + return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays()); + } + }; + Collections.sort(ranges, orderByMinAge); + DelinquencyRange prevDelinquencyRange = null; + for (DelinquencyRange delinquencyRange : ranges) { + if (prevDelinquencyRange != null) { + if (isOverlaped(prevDelinquencyRange, delinquencyRange)) { + final String errorMessage = "The delinquency ranges age days values are overlaped"; + throw new DelinquencyBucketAgesOverlapedException(errorMessage, prevDelinquencyRange, delinquencyRange); + } + } + prevDelinquencyRange = delinquencyRange; + } + } + + private boolean isOverlaped(DelinquencyRange o1, DelinquencyRange o2) { + return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMaximumAgeDays()); + } + + public void setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId) { + final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); + + final LocalDate transactionDate = DateUtils.getLocalDateOfTenant(); + List loanDelinquencyTagHistory = this.loanDelinquencyTagRepository + .findByLoanOrderByAddedOnDateDesc(loan); + // The delinquencyRangeId in null means just goes out from Delinquency + if (delinquencyRangeId == null) { + // The Loan will go out from Delinquency + if (!loanDelinquencyTagHistory.isEmpty()) { + LoanDelinquencyTagHistory loanDelinquencyTagPrev = loanDelinquencyTagHistory.get(0); + loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); + this.loanDelinquencyTagRepository.saveAndFlush(loanDelinquencyTagPrev); + } + } else { + // The previous Loan Delinquency Tag will set as Lifted + if (!loanDelinquencyTagHistory.isEmpty()) { + LoanDelinquencyTagHistory loanDelinquencyTagPrev = loanDelinquencyTagHistory.get(0); + loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); + this.loanDelinquencyTagRepository.save(loanDelinquencyTagPrev); + } + + final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId); + LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null); + loanDelinquencyTagHistory.add(loanDelinquencyTag); + this.loanDelinquencyTagRepository.saveAndFlush(loanDelinquencyTag); + } + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java new file mode 100644 index 000000000..1e1c260bc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java @@ -0,0 +1,76 @@ +/** + * 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.delinquency.validator; + +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.validator.ParseAndValidator; +import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class DelinquencyBucketParseAndValidator extends ParseAndValidator { + + private final FromJsonHelper jsonHelper; + + public DelinquencyBucketData validateAndParseUpdate(@NotNull final JsonCommand command) { + final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("delinquencyBucket.create"); + JsonObject element = extractJsonObject(jsonHelper, command); + + DelinquencyBucketData result = validateAndParseUpdate(dataValidator, element, jsonHelper); + throwExceptionIfValidationWarningsExist(dataValidator); + + return result; + } + + private DelinquencyBucketData validateAndParseUpdate(final DataValidatorBuilder dataValidator, JsonObject element, + FromJsonHelper jsonHelper) { + if (element == null) { + return null; + } + + jsonHelper.checkForUnsupportedParameters(element, + List.of(DelinquencyApiConstants.NAME_PARAM_NAME, DelinquencyApiConstants.RANGES_PARAM_NAME)); + + final String name = jsonHelper.extractStringNamed(DelinquencyApiConstants.NAME_PARAM_NAME, element); + + dataValidator.reset().parameter(DelinquencyApiConstants.NAME_PARAM_NAME).value(name).notBlank(); + + final String[] rangeIds = jsonHelper.extractArrayNamed(DelinquencyApiConstants.RANGES_PARAM_NAME, element); + dataValidator.reset().parameter(DelinquencyApiConstants.RANGES_PARAM_NAME).value(rangeIds).notNull().arrayNotEmpty(); + + ArrayList ranges = new ArrayList<>(); + if (rangeIds != null) { + for (String rangeId : rangeIds) { + ranges.add(DelinquencyRangeData.reference(Long.parseLong(rangeId))); + } + } + return dataValidator.hasError() ? null : new DelinquencyBucketData(null, name, ranges); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java new file mode 100644 index 000000000..1bf105d30 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyRangeParseAndValidator.java @@ -0,0 +1,77 @@ +/** + * 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.delinquency.validator; + +import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.validator.ParseAndValidator; +import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class DelinquencyRangeParseAndValidator extends ParseAndValidator { + + private final FromJsonHelper jsonHelper; + + public DelinquencyRangeData validateAndParseUpdate(@NotNull final JsonCommand command) { + final DataValidatorBuilder dataValidator = new DataValidatorBuilder(new ArrayList<>()).resource("delinquencyrange.create"); + JsonObject element = extractJsonObject(jsonHelper, command); + + DelinquencyRangeData result = validateAndParseUpdate(dataValidator, element, jsonHelper); + throwExceptionIfValidationWarningsExist(dataValidator); + + return result; + } + + private DelinquencyRangeData validateAndParseUpdate(final DataValidatorBuilder dataValidator, JsonObject element, + FromJsonHelper jsonHelper) { + if (element == null) { + return null; + } + + jsonHelper.checkForUnsupportedParameters(element, + List.of(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, + DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.LOCALE_PARAM_NAME)); + + final String localeValue = jsonHelper.extractStringNamed(DelinquencyApiConstants.LOCALE_PARAM_NAME, element); + dataValidator.reset().parameter(DelinquencyApiConstants.LOCALE_PARAM_NAME).value(localeValue).notBlank(); + final Locale locale = jsonHelper.extractLocaleParameter(element); + + final String classification = jsonHelper.extractStringNamed(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, element); + final Integer minimumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, element, locale); + final Integer maximumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, element, locale); + dataValidator.reset().parameter(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME).value(classification).notBlank(); + dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME).value(minimumAge).notBlank() + .integerGreaterThanNumber(0); + dataValidator.reset().parameter(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME).value(maximumAge).ignoreIfNull() + .integerGreaterThanNumber(0); + + return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 4bc706a62..b35ab55bb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.accountdetails.data.LoanAccountSummaryData; import org.apache.fineract.portfolio.calendar.data.CalendarData; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.group.data.GroupGeneralData; @@ -239,6 +240,7 @@ public final class LoanAccountData { private LocalDate expectedDisbursementDate; private final CollectionData delinquent; + private final DelinquencyRangeData delinquencyRange; public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, @@ -426,6 +428,7 @@ public final class LoanAccountData { this.isRatesEnabled = false; this.fixedPrincipalPercentagePerInstallment = null; this.delinquent = null; + this.delinquencyRange = null; } public Integer getRowIndex() { @@ -592,6 +595,7 @@ public final class LoanAccountData { final Boolean isRatesEnabled = false; final BigDecimal fixedPrincipalPercentagePerInstallment = null; final CollectionData delinquent = CollectionData.template(); + final DelinquencyRangeData delinquencyRange = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, @@ -613,7 +617,7 @@ public final class LoanAccountData { interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquent); + fixedPrincipalPercentagePerInstallment, delinquent, delinquencyRange); } /** @@ -738,6 +742,7 @@ public final class LoanAccountData { final Boolean isRatesEnabled = false; final BigDecimal fixedPrincipalPercentagePerInstallment = null; final CollectionData delinquent = CollectionData.template(); + final DelinquencyRangeData delinquencyRange = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, @@ -759,7 +764,7 @@ public final class LoanAccountData { originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquent); + fixedPrincipalPercentagePerInstallment, delinquent, delinquencyRange); } public static LoanAccountData populateClientDefaults(final LoanAccountData acc, final LoanAccountData clientAcc) { @@ -790,7 +795,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } /** @@ -917,6 +922,7 @@ public final class LoanAccountData { final Boolean isRatesEnabled = false; final BigDecimal fixedPrincipalPercentagePerInstallment = null; final CollectionData delinquent = CollectionData.template(); + final DelinquencyRangeData delinquencyRange = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, loanProductId, loanProductName, loanProductDescription, isLoanProductLinkedToFloatingRate, fundId, fundName, @@ -938,7 +944,7 @@ public final class LoanAccountData { originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, delinquent); + fixedPrincipalPercentagePerInstallment, delinquent, delinquencyRange); } public static LoanAccountData populateGroupDefaults(final LoanAccountData acc, final LoanAccountData groupAcc) { @@ -968,7 +974,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } public static LoanAccountData loanProductWithTemplateDefaults(final LoanProductData product, @@ -1110,6 +1116,7 @@ public final class LoanAccountData { final List rates = null; final Boolean isRatesEnabled = false; final CollectionData delinquent = CollectionData.template(); + final DelinquencyRangeData delinquencyRange = null; return new LoanAccountData(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, group, loanType, product.getId(), product.getName(), product.getDescription(), product.isLinkedToFloatingInterestRates(), @@ -1136,7 +1143,7 @@ public final class LoanAccountData { product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, product.isEqualAmortization(), rates, isRatesEnabled, - product.getFixedPrincipalPercentagePerInstallment(), delinquent); + product.getFixedPrincipalPercentagePerInstallment(), delinquent, delinquencyRange); } public static LoanAccountData populateLoanProductDefaults(final LoanAccountData acc, final LoanProductData product) { @@ -1179,6 +1186,7 @@ public final class LoanAccountData { } } final CollectionData delinquent = CollectionData.template(); + final DelinquencyRangeData delinquencyRange = null; return new LoanAccountData(acc.id, acc.accountNo, acc.status, acc.externalId, acc.clientId, acc.clientAccountNo, acc.clientName, acc.clientOfficeId, acc.group, acc.loanType, product.getId(), product.getName(), product.getDescription(), @@ -1206,7 +1214,7 @@ public final class LoanAccountData { product.isVariableInstallmentsAllowed(), product.getMinimumGapBetweenInstallments(), product.getMaximumGapBetweenInstallments(), acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, product.isEqualAmortization(), acc.rates, acc.isRatesEnabled, - product.getFixedPrincipalPercentagePerInstallment(), delinquent); + product.getFixedPrincipalPercentagePerInstallment(), delinquent, delinquencyRange); } /* @@ -1237,7 +1245,8 @@ public final class LoanAccountData { final LoanInterestRecalculationData interestRecalculationData, final Boolean createStandingInstructionAtDisbursement, final Boolean isVariableInstallmentsAllowed, Integer minimumGap, Integer maximumGap, final EnumOptionData subStatus, final boolean canUseForTopup, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo, - final BigDecimal topupAmount, final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment) { + final BigDecimal topupAmount, final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment, + final DelinquencyRangeData delinquencyRange) { final LoanScheduleData repaymentSchedule = null; final Collection transactions = null; @@ -1297,7 +1306,7 @@ public final class LoanAccountData { isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, originalSchedule, createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, isVariableInstallmentsAllowed, minimumGap, maximumGap, subStatus, canUseForTopup, clientActiveLoanOptions, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, - isEqualAmortization, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquent); + isEqualAmortization, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquent, delinquencyRange); } /* @@ -1351,7 +1360,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, paidInAdvance, interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, rates, isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, delinquent); + acc.fixedPrincipalPercentagePerInstallment, delinquent, acc.delinquencyRange); } public static LoanAccountData associationsAndTemplate(final LoanAccountData acc, final Collection productOptions, @@ -1394,7 +1403,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } public static LoanAccountData associateMemberVariations(final LoanAccountData acc, final Map memberLoanCycle) { @@ -1460,7 +1469,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } public static LoanAccountData withInterestRecalculationCalendarData(final LoanAccountData acc, final CalendarData calendarData, @@ -1494,7 +1503,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } public static LoanAccountData withLoanCalendarData(final LoanAccountData acc, final CalendarData calendarData) { @@ -1523,7 +1532,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } public static LoanAccountData withOriginalSchedule(final LoanAccountData acc, final LoanScheduleData originalSchedule) { @@ -1553,7 +1562,7 @@ public final class LoanAccountData { acc.createStandingInstructionAtDisbursement, acc.paidInAdvance, acc.interestRatesPeriods, acc.isVariableInstallmentsAllowed, acc.minimumGap, acc.maximumGap, acc.subStatus, acc.canUseForTopup, acc.clientActiveLoanOptions, acc.isTopup, acc.closureLoanId, acc.closureLoanAccountNo, acc.topupAmount, acc.isEqualAmortization, acc.rates, acc.isRatesEnabled, - acc.fixedPrincipalPercentagePerInstallment, acc.delinquent); + acc.fixedPrincipalPercentagePerInstallment, acc.delinquent, acc.delinquencyRange); } private LoanAccountData(final Long id, // @@ -1606,7 +1615,8 @@ public final class LoanAccountData { final Integer minimumGap, final Integer maximumGap, final EnumOptionData subStatus, final Boolean canUseForTopup, final Collection clientActiveLoanOptions, final boolean isTopup, final Long closureLoanId, final String closureLoanAccountNo, final BigDecimal topupAmount, final boolean isEqualAmortization, final List rates, - final Boolean isRatesEnabled, final BigDecimal fixedPrincipalPercentagePerInstallment, final CollectionData delinquent) { + final Boolean isRatesEnabled, final BigDecimal fixedPrincipalPercentagePerInstallment, final CollectionData delinquent, + final DelinquencyRangeData delinquencyRange) { this.id = id; this.accountNo = accountNo; @@ -1794,6 +1804,7 @@ public final class LoanAccountData { this.rates = rates; this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment; this.delinquent = delinquent; + this.delinquencyRange = delinquencyRange; } public RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java index 8bf234fac..5684bb9c0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java @@ -151,7 +151,7 @@ public class RecalculateInterestForLoanTasklet implements Tasklet { List> posters = new ArrayList<>(); int fromIndex = 0; int size = loanIds.size(); - double toGetCeilValue = size / threadPoolSize; + double toGetCeilValue = size / ((double) threadPoolSize); batchSize = (int) Math.ceil(toGetCeilValue); if (batchSize == 0) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 03e335308..1c84f5d8b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -70,6 +70,8 @@ import org.apache.fineract.portfolio.client.domain.ClientEnumerations; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData; import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.apache.fineract.portfolio.fund.data.FundData; @@ -157,6 +159,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private final AccountDetailsReadPlatformService accountDetailsReadPlatformService; private final ColumnValidator columnValidator; private final DatabaseSpecificSQLGenerator sqlGenerator; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; @Autowired public LoanReadPlatformServiceImpl(final PlatformSecurityContext context, @@ -170,6 +173,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final FloatingRatesReadPlatformService floatingRatesReadPlatformService, final LoanUtilService loanUtilService, final ConfigurationDomainService configurationDomainService, + final DelinquencyReadPlatformService delinquencyReadPlatformService, final AccountDetailsReadPlatformService accountDetailsReadPlatformService, final LoanRepositoryWrapper loanRepositoryWrapper, final ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, PaginationHelper paginationHelper) { this.context = context; @@ -193,9 +197,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { this.configurationDomainService = configurationDomainService; this.accountDetailsReadPlatformService = accountDetailsReadPlatformService; this.columnValidator = columnValidator; - this.loaanLoanMapper = new LoanMapper(sqlGenerator); + this.loaanLoanMapper = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); this.sqlGenerator = sqlGenerator; this.paginationHelper = paginationHelper; + this.delinquencyReadPlatformService = delinquencyReadPlatformService; } @Override @@ -206,7 +211,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final String hierarchy = currentUser.getOffice().getHierarchy(); final String hierarchySearchString = hierarchy + "%"; - final LoanMapper rm = new LoanMapper(sqlGenerator); + final LoanMapper rm = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); final StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("select "); @@ -226,7 +231,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { // final AppUser currentUser = this.context.authenticatedUser(); this.context.authenticatedUser(); - final LoanMapper rm = new LoanMapper(sqlGenerator); + final LoanMapper rm = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); final String sql = "select " + rm.loanSchema() + " where l.account_no=?"; @@ -237,7 +242,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @Override public List retrieveGLIMChildLoansByGLIMParentAccount(String parentloanAccountNumber) { this.context.authenticatedUser(); - final LoanMapper rm = new LoanMapper(sqlGenerator); + final LoanMapper rm = new LoanMapper(sqlGenerator, delinquencyReadPlatformService); final String sql = "select " + rm.loanSchema() + " left join glim_parent_child_mapping as glim on glim.glim_child_account_id=l.account_no " @@ -490,8 +495,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { @Override public LoanTransactionData retrieveWaiveInterestDetails(final Long loanId) { - AppUser currentUser = this.context.authenticatedUser(); - // TODO - KW -OPTIMIZE - write simple sql query to fetch back overdue // interest that can be waived along with the date of repayment period // interest is overdue. @@ -582,9 +585,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { private static final class LoanMapper implements RowMapper { private final DatabaseSpecificSQLGenerator sqlGenerator; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; - LoanMapper(DatabaseSpecificSQLGenerator sqlGenerator) { + LoanMapper(DatabaseSpecificSQLGenerator sqlGenerator, DelinquencyReadPlatformService delinquencyReadPlatformService) { this.sqlGenerator = sqlGenerator; + this.delinquencyReadPlatformService = delinquencyReadPlatformService; } public String loanSchema() { @@ -989,6 +994,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { final Long closureLoanId = rs.getLong("closureLoanId"); final String closureLoanAccountNo = rs.getString("closureLoanAccountNo"); final BigDecimal topupAmount = rs.getBigDecimal("topupAmount"); + // Current Delinquency Range Data + DelinquencyRangeData delinquencyRange = this.delinquencyReadPlatformService.retrieveCurrentDelinquencyTag(id); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, @@ -1004,7 +1011,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { isNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, interestRecalculationData, createStandingInstructionAtDisbursement, isvariableInstallmentsAllowed, minimumGap, maximumGap, loanSubStatus, canUseForTopup, isTopup, closureLoanId, closureLoanAccountNo, topupAmount, isEqualAmortization, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, delinquencyRange); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 69dc2eb62..45b26be85 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -141,5 +141,6 @@ public interface LoanProductConstants { String ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED = "allowApprovedDisbursedAmountsOverApplied"; String OVER_APPLIED_CALCULATION_TYPE = "overAppliedCalculationType"; String OVER_APPLIED_NUMBER = "overAppliedNumber"; + String DELINQUENCY_BUCKET_PARAM_NAME = "delinquencyBucketId"; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index 0799ebf20..346fc68e3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -65,6 +65,8 @@ import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformSer import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData; import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlatformService; import org.apache.fineract.portfolio.fund.data.FundData; @@ -130,6 +132,7 @@ public class LoanProductsApiResource { private final FloatingRatesReadPlatformService floatingRateReadPlatformService; private final RateReadService rateReadService; private final ConfigurationDomainService configurationDomainService; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; @Autowired public LoanProductsApiResource(final PlatformSecurityContext context, final LoanProductReadPlatformService readPlatformService, @@ -145,7 +148,8 @@ public class LoanProductsApiResource { final DropdownReadPlatformService commonDropdownReadPlatformService, PaymentTypeReadPlatformService paymentTypeReadPlatformService, final FloatingRatesReadPlatformService floatingRateReadPlatformService, final RateReadService rateReadService, - final ConfigurationDomainService configurationDomainService) { + final ConfigurationDomainService configurationDomainService, + final DelinquencyReadPlatformService delinquencyReadPlatformService) { this.context = context; this.loanProductReadPlatformService = readPlatformService; this.chargeReadPlatformService = chargeReadPlatformService; @@ -164,6 +168,7 @@ public class LoanProductsApiResource { this.floatingRateReadPlatformService = floatingRateReadPlatformService; this.rateReadService = rateReadService; this.configurationDomainService = configurationDomainService; + this.delinquencyReadPlatformService = delinquencyReadPlatformService; } @POST @@ -339,6 +344,12 @@ public class LoanProductsApiResource { if (fundOptions.isEmpty()) { fundOptions = null; } + + Collection delinquencyBucketOptions = this.delinquencyReadPlatformService.retrieveAllDelinquencyBuckets(); + if (delinquencyBucketOptions.isEmpty()) { + delinquencyBucketOptions = null; + } + final Collection transactionProcessingStrategyOptions = this.dropdownReadPlatformService .retreiveTransactionProcessingStrategies(); @@ -372,7 +383,7 @@ public class LoanProductsApiResource { loanCycleValueConditionTypeOptions, daysInMonthTypeOptions, daysInYearTypeOptions, interestRecalculationCompoundingTypeOptions, rescheduleStrategyTypeOptions, interestRecalculationFrequencyTypeOptions, preCloseInterestCalculationStrategyOptions, floatingRateOptions, interestRecalculationNthDayTypeOptions, - interestRecalculationDayOfWeekTypeOptions, isRatesEnabled); + interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index 40077a8cb..5385884fa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; @@ -51,6 +52,7 @@ import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.rate.data.RateData; import org.springframework.util.CollectionUtils; +@SuppressWarnings("unused") /** * Immutable data object to represent loan products. */ @@ -140,9 +142,7 @@ public class LoanProductData implements Serializable { // template related private final Collection fundOptions; - @SuppressWarnings("unused") private final Collection paymentTypeOptions; - @SuppressWarnings("unused") private final Collection currencyOptions; private final List repaymentFrequencyTypeOptions; private final List interestRateFrequencyTypeOptions; @@ -152,32 +152,19 @@ public class LoanProductData implements Serializable { private final Collection transactionProcessingStrategyOptions; private final Collection chargeOptions; private final Collection rateOptions; - @SuppressWarnings("unused") private final Collection penaltyOptions; - @SuppressWarnings("unused") private final List accountingRuleOptions; - @SuppressWarnings("unused") private final Map> accountingMappingOptions; - @SuppressWarnings("unused") private final List valueConditionTypeOptions; - @SuppressWarnings("unused") private final List daysInMonthTypeOptions; - @SuppressWarnings("unused") private final List daysInYearTypeOptions; - @SuppressWarnings("unused") private final List interestRecalculationCompoundingTypeOptions; - @SuppressWarnings("unused") private final List interestRecalculationNthDayTypeOptions; - @SuppressWarnings("unused") private final List interestRecalculationDayOfWeekTypeOptions; - @SuppressWarnings("unused") private final List rescheduleStrategyTypeOptions; - @SuppressWarnings("unused") private final List preClosureInterestCalculationStrategyOptions; - @SuppressWarnings("unused") private final List interestRecalculationFrequencyTypeOptions; - @SuppressWarnings("unused") private final List floatingRateOptions; private final Boolean multiDisburseLoan; @@ -198,6 +185,10 @@ public class LoanProductData implements Serializable { private final boolean isEqualAmortization; private final BigDecimal fixedPrincipalPercentagePerInstallment; + // Delinquency Buckets + private final Collection delinquencyBucketOptions; + private final DelinquencyBucketData delinquencyBucket; + /** * Used when returning lookup information about loan product for dropdowns. */ @@ -282,6 +273,9 @@ public class LoanProductData implements Serializable { final Collection rateOptions = null; final Collection rates = null; final boolean isRatesEnabled = false; + final Collection delinquencyBucketOptions = null; + final DelinquencyBucketData delinquencyBucket = null; + return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, @@ -298,7 +292,7 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); } @@ -384,6 +378,8 @@ public class LoanProductData implements Serializable { final Collection rateOptions = null; final Collection rates = null; final boolean isRatesEnabled = false; + final Collection delinquencyBucketOptions = null; + final DelinquencyBucketData delinquencyBucket = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -401,7 +397,7 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); } @@ -494,6 +490,8 @@ public class LoanProductData implements Serializable { final Collection rateOptions = null; final Collection rates = null; final boolean isRatesEnabled = false; + final Collection delinquencyBucketOptions = null; + final DelinquencyBucketData delinquencyBucket = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -511,7 +509,7 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); } @@ -598,6 +596,8 @@ public class LoanProductData implements Serializable { final Collection rateOptions = null; final Collection rates = null; final boolean isRatesEnabled = false; + final Collection delinquencyBucketOptions = null; + final DelinquencyBucketData delinquencyBucket = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -615,7 +615,7 @@ public class LoanProductData implements Serializable { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment); + fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); } @@ -660,7 +660,8 @@ public class LoanProductData implements Serializable { final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments, final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization, Collection rateOptions, Collection rates, final boolean isRatesEnabled, - final BigDecimal fixedPrincipalPercentagePerInstallment) { + final BigDecimal fixedPrincipalPercentagePerInstallment, final Collection delinquencyBucketOptions, + final DelinquencyBucketData delinquencyBucket) { this.id = id; this.name = name; this.shortName = shortName; @@ -774,7 +775,8 @@ public class LoanProductData implements Serializable { this.syncExpectedWithDisbursementDate = syncExpectedWithDisbursementDate; this.canUseForTopup = canUseForTopup; this.isEqualAmortization = isEqualAmortization; - + this.delinquencyBucketOptions = delinquencyBucketOptions; + this.delinquencyBucket = delinquencyBucket; } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -790,7 +792,8 @@ public class LoanProductData implements Serializable { final List rescheduleStrategyTypeOptions, final List interestRecalculationFrequencyTypeOptions, final List preCloseInterestCalculationStrategyOptions, final List floatingRateOptions, final List interestRecalculationNthDayTypeOptions, - final List interestRecalculationDayOfWeekTypeOptions, final boolean isRatesEnabled) { + final List interestRecalculationDayOfWeekTypeOptions, final boolean isRatesEnabled, + final Collection delinquencyBucketOptions) { this.id = productData.id; this.name = productData.name; this.shortName = productData.shortName; @@ -921,6 +924,8 @@ public class LoanProductData implements Serializable { this.isEqualAmortization = productData.isEqualAmortization; this.rates = productData.rates; this.isRatesEnabled = isRatesEnabled; + this.delinquencyBucketOptions = delinquencyBucketOptions; + this.delinquencyBucket = productData.delinquencyBucket; } private Collection nullIfEmpty(final Collection charges) { @@ -1051,6 +1056,10 @@ public class LoanProductData implements Serializable { return this.fundOptions; } + public Collection getDelinquencyBucketOptions() { + return delinquencyBucketOptions; + } + public List getAmortizationTypeOptions() { return this.amortizationTypeOptions; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index d9a1ada88..07a84cdb4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -54,6 +54,7 @@ import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.apache.fineract.portfolio.floatingrates.data.FloatingRateDTO; import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; @@ -195,6 +196,10 @@ public class LoanProduct extends AbstractPersistableCustom { @Column(name = "over_applied_number", nullable = true) private Integer overAppliedNumber; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "delinquency_bucket_id", nullable = true) + private DelinquencyBucket delinquencyBucket; + public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate, final List productRates) { @@ -1001,6 +1006,15 @@ public class LoanProduct extends AbstractPersistableCustom { this.syncExpectedWithDisbursementDate = newValue; } + Long delinquencyBucketId = null; + if (this.delinquencyBucket != null) { + delinquencyBucketId = this.delinquencyBucket.getId(); + } + if (command.isChangeInLongParameterNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, delinquencyBucketId)) { + final Long newValue = command.longValueOfParameterNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME); + actualChanges.put(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, newValue); + } + /** * Update interest recalculation settings */ @@ -1578,4 +1592,12 @@ public class LoanProduct extends AbstractPersistableCustom { this.loanProducTrancheDetails = loanProducTrancheDetails; } + public DelinquencyBucket getDelinquencyBucket() { + return delinquencyBucket; + } + + public void setDelinquencyBucket(DelinquencyBucket delinquencyBucket) { + this.delinquencyBucket = delinquencyBucket; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java index 143802d33..071fa4524 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanproduct.domain; import java.util.List; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; @@ -28,4 +29,6 @@ public interface LoanProductRepository extends JpaRepository, @Query("select loanProduct from LoanProduct loanProduct, IN(loanProduct.charges) charge where charge.id = :chargeId") List retrieveLoanProductsByChargeId(@Param("chargeId") Long chargeId); + + Long countByDelinquencyBucket(DelinquencyBucket delinquencyBucket); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 5087aea6d..ca2b9442e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -111,7 +111,7 @@ public final class LoanProductDataValidator { LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME, LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, - LoanProductConstants.OVER_APPLIED_NUMBER)); + LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME)); private static final String[] supportedloanConfigurableAttributes = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyIdParamName, @@ -274,6 +274,13 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter("transactionProcessingStrategyId").value(transactionProcessingStrategyId).notNull() .integerGreaterThanZero(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element)) { + final Long delinquencyBucketId = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, + element); + baseDataValidator.reset().parameter(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME).value(delinquencyBucketId) + .ignoreIfNull().integerGreaterThanZero(); + } + // grace validation final Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element); baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment).zeroOrPositiveAmount(); @@ -1087,6 +1094,13 @@ public final class LoanProductDataValidator { .integerZeroOrGreater(); } + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element)) { + final Long delinquencyBucketId = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, + element); + baseDataValidator.reset().parameter(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME).value(delinquencyBucketId) + .ignoreIfNull().integerGreaterThanZero(); + } + Integer amortizationType = null; if (this.fromApiJsonHelper.parameterExists("amortizationType", element)) { amortizationType = this.fromApiJsonHelper.extractIntegerNamed("amortizationType", element, Locale.getDefault()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index bbb73decc..785e849bb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -36,6 +36,8 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.common.service.CommonEnumerations; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductGuaranteeData; @@ -60,18 +62,20 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo private final RateReadService rateReadService; private final DatabaseSpecificSQLGenerator sqlGenerator; private final FineractEntityAccessUtil fineractEntityAccessUtil; + private final DelinquencyReadPlatformService delinquencyReadPlatformService; @Autowired public LoanProductReadPlatformServiceImpl(final PlatformSecurityContext context, final ChargeReadPlatformService chargeReadPlatformService, final JdbcTemplate jdbcTemplate, final FineractEntityAccessUtil fineractEntityAccessUtil, final RateReadService rateReadService, - DatabaseSpecificSQLGenerator sqlGenerator) { + final DelinquencyReadPlatformService delinquencyReadPlatformService, final DatabaseSpecificSQLGenerator sqlGenerator) { this.context = context; this.chargeReadPlatformService = chargeReadPlatformService; this.jdbcTemplate = jdbcTemplate; this.fineractEntityAccessUtil = fineractEntityAccessUtil; this.rateReadService = rateReadService; this.sqlGenerator = sqlGenerator; + this.delinquencyReadPlatformService = delinquencyReadPlatformService; } @Override @@ -82,7 +86,9 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Collection rates = this.rateReadService.retrieveProductLoanRates(loanProductId); final Collection borrowerCycleVariationDatas = retrieveLoanProductBorrowerCycleVariations( loanProductId); - final LoanProductMapper rm = new LoanProductMapper(charges, borrowerCycleVariationDatas, rates); + final Collection delinquencyBucketOptions = this.delinquencyReadPlatformService + .retrieveAllDelinquencyBuckets(); + final LoanProductMapper rm = new LoanProductMapper(charges, borrowerCycleVariationDatas, rates, delinquencyBucketOptions); final String sql = "select " + rm.loanProductSchema() + " where lp.id = ?"; return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { loanProductId }); // NOSONAR @@ -104,7 +110,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo this.context.authenticatedUser(); - final LoanProductMapper rm = new LoanProductMapper(null, null, null); + final LoanProductMapper rm = new LoanProductMapper(null, null, null, null); String sql = "select " + rm.loanProductSchema(); @@ -183,11 +189,15 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo private final Collection rates; + private final Collection delinquencyBucketOptions; + LoanProductMapper(final Collection charges, - final Collection borrowerCycleVariationDatas, final Collection rates) { + final Collection borrowerCycleVariationDatas, final Collection rates, + final Collection delinquencyBucketOptions) { this.charges = charges; this.borrowerCycleVariationDatas = borrowerCycleVariationDatas; this.rates = rates; + this.delinquencyBucketOptions = delinquencyBucketOptions; } public String loanProductSchema() { @@ -235,7 +245,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lfr.max_differential_lending_rate as maxDifferentialLendingRate, " + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, " + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " - + "lvi.maximum_gap as maximumGap, " + + "lvi.maximum_gap as maximumGap, dbuc.id as delinquencyBucketId, dbuc.name as delinquencyBucketName, " + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization " + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " @@ -245,6 +255,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + " left join m_product_loan_floating_rates as lfr on lfr.loan_product_id = lp.id " + " left join m_floating_rates as fr on lfr.floating_rates_id = fr.id " + " left join m_product_loan_variable_installment_config as lvi on lvi.loan_product_id = lp.id " + + " left join m_delinquency_bucket as dbuc on dbuc.id = lp.delinquency_bucket_id " + " join m_currency curr on curr.code = lp.currency_code"; } @@ -465,6 +476,12 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Collection rateOptions = null; final boolean isRatesEnabled = false; + // Delinquency Buckets + final Long delinquencyBucketId = rs.getLong("delinquencyBucketId"); + final String delinquencyBucketName = rs.getString("delinquencyBucketName"); + final DelinquencyBucketData delinquencyBucket = new DelinquencyBucketData(delinquencyBucketId, delinquencyBucketName, + new ArrayList<>()); + return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, @@ -482,7 +499,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, - isRatesEnabled, fixedPrincipalPercentagePerInstallment); + isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket); } } @@ -558,7 +575,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo public Collection retrieveAllLoanProductsForCurrency(String currencyCode) { this.context.authenticatedUser(); - final LoanProductMapper rm = new LoanProductMapper(null, null, null); + final LoanProductMapper rm = new LoanProductMapper(null, null, null, null); String sql = "select " + rm.loanProductSchema() + " where lp.currency_code= ? "; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 8b6cff4cc..7f6377cb5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -38,6 +38,8 @@ import org.apache.fineract.portfolio.businessevent.domain.loan.product.LoanProdu import org.apache.fineract.portfolio.businessevent.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRateRepositoryWrapper; import org.apache.fineract.portfolio.fund.domain.Fund; @@ -83,6 +85,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro private final FloatingRateRepositoryWrapper floatingRateRepository; private final LoanRepositoryWrapper loanRepositoryWrapper; private final BusinessEventNotifierService businessEventNotifierService; + private final DelinquencyBucketRepository delinquencyBucketRepository; @Autowired public LoanProductWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, @@ -92,7 +95,8 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro final ChargeRepositoryWrapper chargeRepository, final RateRepositoryWrapper rateRepository, final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, final FineractEntityAccessUtil fineractEntityAccessUtil, final FloatingRateRepositoryWrapper floatingRateRepository, - final LoanRepositoryWrapper loanRepositoryWrapper, final BusinessEventNotifierService businessEventNotifierService) { + final LoanRepositoryWrapper loanRepositoryWrapper, final BusinessEventNotifierService businessEventNotifierService, + final DelinquencyBucketRepository delinquencyBucketRepository) { this.context = context; this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.loanProductRepository = loanProductRepository; @@ -106,6 +110,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro this.floatingRateRepository = floatingRateRepository; this.loanRepositoryWrapper = loanRepositoryWrapper; this.businessEventNotifierService = businessEventNotifierService; + this.delinquencyBucketRepository = delinquencyBucketRepository; } @Transactional @@ -215,6 +220,12 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro product.update(fund); } + if (changes.containsKey("delinquencyBucketId")) { + final Long delinquencyBucketId = (Long) changes.get("delinquencyBucketId"); + final DelinquencyBucket delinquencyBucket = this.delinquencyBucketRepository.getReferenceById(delinquencyBucketId); + product.setDelinquencyBucket(delinquencyBucket); + } + if (changes.containsKey("transactionProcessingStrategyId")) { final Long transactionProcessingStrategyId = (Long) changes.get("transactionProcessingStrategyId"); final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/generateadhocclientschhedule/GenerateAdhocClientScheduleTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/generateadhocclientschhedule/GenerateAdhocClientScheduleTasklet.java index 21d6ae015..7347dce5f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/generateadhocclientschhedule/GenerateAdhocClientScheduleTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/generateadhocclientschhedule/GenerateAdhocClientScheduleTasklet.java @@ -72,7 +72,7 @@ public class GenerateAdhocClientScheduleTasklet implements Tasklet { run = Math.toIntExact(ChronoUnit.YEARS.between(start, end)) >= 1; } case CUSTOM -> { - next = start.plusDays((int) (long) adhoc.getReportRunEvery()); + next = start.plusDays((long) adhoc.getReportRunEvery()); run = Math.toIntExact(ChronoUnit.DAYS.between(start, end)) >= adhoc.getReportRunEvery(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java index 9f298f5cd..4f5675fb4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java @@ -560,6 +560,10 @@ public class AppUser extends AbstractPersistableCustom implements PlatformUser { validateHasPermission("UPDATE", resourceType); } + public void validateHasDeletePermission(final String resourceType) { + validateHasPermission("DELETE", resourceType); + } + private void validateHasPermission(final String prefix, final String resourceType) { final String authorizationMessage = "User has no authority to " + prefix + " " + resourceType.toLowerCase() + "s"; final String matchPermission = prefix + "_" + resourceType.toUpperCase(); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 48f9e4577..8d208a8d5 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -48,4 +48,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0029_add_delinquency_buckets.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0029_add_delinquency_buckets.xml new file mode 100644 index 000000000..5b8a6ccbe --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0029_add_delinquency_buckets.xml @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java new file mode 100644 index 000000000..d08542194 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -0,0 +1,211 @@ +/** + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.ArrayList; +import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse; +import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse; +import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; +import org.apache.fineract.client.models.GetDelinquencyRangesResponse; +import org.apache.fineract.client.models.PostDelinquencyBucketResponse; +import org.apache.fineract.client.models.PostDelinquencyRangeResponse; +import org.apache.fineract.client.models.PutDelinquencyBucketResponse; +import org.apache.fineract.client.models.PutDelinquencyRangeResponse; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyRangesHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DelinquencyBucketsIntegrationTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + } + + @Test + public void testCreateDelinquencyRanges() { + // given + final String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + + // when + final PostDelinquencyRangeResponse delinquencyRangeResponse01 = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, + responseSpec, jsonRange); + final ArrayList ranges = DelinquencyRangesHelper.getDelinquencyRanges(requestSpec, responseSpec); + + // then + assertNotNull(delinquencyRangeResponse01); + assertNotNull(ranges); + assertEquals(1, ranges.get(0).getMinimumAgeDays(), "Expected Min Age Days to 1"); + assertEquals(3, ranges.get(0).getMaximumAgeDays(), "Expected Max Age Days to 3"); + } + + @Test + public void testUpdateDelinquencyRanges() { + // given + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + final PostDelinquencyRangeResponse delinquencyRangeResponse01 = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, + responseSpec, jsonRange); + jsonRange = DelinquencyRangesHelper.getAsJSON(1, 7); + assertNotNull(delinquencyRangeResponse01); + + // when + final PutDelinquencyRangeResponse delinquencyRangeResponse02 = DelinquencyRangesHelper.updateDelinquencyRange(requestSpec, + responseSpec, delinquencyRangeResponse01.getResourceId(), jsonRange); + final GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec, + delinquencyRangeResponse01.getResourceId()); + final DeleteDelinquencyRangeResponse deleteDelinquencyRangeResponse = DelinquencyRangesHelper.deleteDelinquencyRange(requestSpec, + responseSpec, delinquencyRangeResponse01.getResourceId()); + + // then + assertNotNull(delinquencyRangeResponse02); + assertNotNull(deleteDelinquencyRangeResponse); + assertNotNull(range); + assertNotEquals(3, range.getMaximumAgeDays()); + assertEquals(1, range.getMinimumAgeDays()); + assertEquals(7, range.getMaximumAgeDays()); + } + + @Test + public void testDelinquencyBuckets() { + // given + ArrayList rangeIds = new ArrayList<>(); + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30); + // Create + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, + responseSpec, jsonBucket); + // Update + jsonRange = DelinquencyRangesHelper.getAsJSON(31, 60); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PutDelinquencyBucketResponse updateDelinquencyBucketResponse = DelinquencyBucketsHelper.updateDelinquencyBucket(requestSpec, + responseSpec, delinquencyBucketResponse.getResourceId(), jsonBucket); + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + // Read + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketResponse.getResourceId()); + + // when + final ArrayList bucketList = DelinquencyBucketsHelper.getDelinquencyBuckets(requestSpec, + responseSpec); + + // then + assertNotNull(bucketList); + assertNotNull(delinquencyBucket); + assertEquals(2, delinquencyBucket.getRanges().size()); + assertNotNull(delinquencyBucketResponse); + assertNotNull(updateDelinquencyBucketResponse); + } + + @Test + public void testDelinquencyBucketDelete() { + // given + ArrayList rangeIds = new ArrayList<>(); + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30); + // Create + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, + responseSpec, jsonBucket); + // Delete + DeleteDelinquencyBucketResponse deleteDelinquencyBucketResponse = DelinquencyBucketsHelper.deleteDelinquencyBucket(requestSpec, + responseSpec, delinquencyBucketResponse.getResourceId()); + + // when + final ArrayList bucketList = DelinquencyBucketsHelper.getDelinquencyBuckets(requestSpec, + responseSpec); + + // then + assertNotNull(bucketList); + assertNotNull(delinquencyBucketResponse); + assertNotNull(deleteDelinquencyBucketResponse); + } + + @Test + public void testDelinquencyBucketsRangeAgeOverlaped() { + // Given + ArrayList rangeIds = new ArrayList<>(); + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonRange = DelinquencyRangesHelper.getAsJSON(3, 30); + // Create + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + final ResponseSpecification response403Spec = new ResponseSpecBuilder().expectStatusCode(403).build(); + + // When + DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, response403Spec, jsonBucket); + } + + @Test + public void testDelinquencyBucketsNameDuplication() { + // Given + ArrayList rangeIds = new ArrayList<>(); + String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3); + PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + jsonRange = DelinquencyRangesHelper.getAsJSON(4, 30); + // Create + delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange); + rangeIds.add(delinquencyRangeResponse.getResourceId()); + String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds); + final ResponseSpecification response403Spec = new ResponseSpecBuilder().expectStatusCode(403).build(); + + // When + DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, jsonBucket); + + // Then + DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, response403Spec, jsonBucket); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java index a331b42d4..44ccd7fdf 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java @@ -338,4 +338,9 @@ public final class Utils { .spec(responseSpec).log().ifError().when().get(getURL).andReturn().asString(); return templateLocation.substring(1, templateLocation.length() - 1); } + + public static String emptyJson() { + return "{}"; + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java new file mode 100644 index 000000000..7027e847a --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java @@ -0,0 +1,91 @@ +/** + * 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.products; + +import com.google.gson.Gson; +import com.linecorp.armeria.internal.shaded.guava.reflect.TypeToken; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse; +import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; +import org.apache.fineract.client.models.PostDelinquencyBucketResponse; +import org.apache.fineract.client.models.PutDelinquencyBucketResponse; +import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.common.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DelinquencyBucketsHelper { + + private static final String DELINQUENCY_BUCKETS_URL = "/fineract-provider/api/v1/delinquency/buckets"; + private static final Gson GSON = new JSON().getGson(); + + private static final Logger LOG = LoggerFactory.getLogger(DelinquencyBucketsHelper.class); + + protected DelinquencyBucketsHelper() {} + + public static ArrayList getDelinquencyBuckets(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec) { + String response = Utils.performServerGet(requestSpec, responseSpec, DELINQUENCY_BUCKETS_URL + "?" + Utils.TENANT_IDENTIFIER); + + Type delinquencyBucketListType = new TypeToken>() {}.getType(); + return GSON.fromJson(response, delinquencyBucketListType); + } + + public static GetDelinquencyBucketsResponse getDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId) { + String response = Utils.performServerGet(requestSpec, responseSpec, + DELINQUENCY_BUCKETS_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER); + return GSON.fromJson(response, GetDelinquencyBucketsResponse.class); + } + + public static PostDelinquencyBucketResponse createDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String json) { + LOG.info("JSON: {}", json); + final String response = Utils.performServerPost(requestSpec, responseSpec, DELINQUENCY_BUCKETS_URL + "?" + Utils.TENANT_IDENTIFIER, + json, null); + return GSON.fromJson(response, PostDelinquencyBucketResponse.class); + } + + public static PutDelinquencyBucketResponse updateDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId, final String json) { + LOG.info("JSON: {}", json); + final String response = Utils.performServerPut(requestSpec, responseSpec, + DELINQUENCY_BUCKETS_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER, json, null); + return GSON.fromJson(response, PutDelinquencyBucketResponse.class); + } + + public static DeleteDelinquencyBucketResponse deleteDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId) { + final String response = Utils.performServerDelete(requestSpec, responseSpec, + DELINQUENCY_BUCKETS_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER, Utils.emptyJson(), null); + return GSON.fromJson(response, DeleteDelinquencyBucketResponse.class); + } + + public static String getAsJSON(final ArrayList rangeIds) { + final HashMap map = new HashMap<>(); + map.put("name", Utils.randomNameGenerator("Delinquency_Bucket_", 4)); + map.put("ranges", rangeIds.toArray()); + return new Gson().toJson(map); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyRangesHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyRangesHelper.java new file mode 100644 index 000000000..0a7af2d48 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyRangesHelper.java @@ -0,0 +1,95 @@ +/** + * 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.products; + +import com.google.gson.Gson; +import com.linecorp.armeria.internal.shaded.guava.reflect.TypeToken; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse; +import org.apache.fineract.client.models.GetDelinquencyRangesResponse; +import org.apache.fineract.client.models.PostDelinquencyRangeResponse; +import org.apache.fineract.client.models.PutDelinquencyRangeResponse; +import org.apache.fineract.client.util.JSON; +import org.apache.fineract.integrationtests.common.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DelinquencyRangesHelper { + + private static final String DELINQUENCY_RANGES_URL = "/fineract-provider/api/v1/delinquency/ranges"; + private static final Gson GSON = new JSON().getGson(); + + private static final Logger LOG = LoggerFactory.getLogger(DelinquencyRangesHelper.class); + + protected DelinquencyRangesHelper() {} + + public static ArrayList getDelinquencyRanges(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec) { + String response = Utils.performServerGet(requestSpec, responseSpec, DELINQUENCY_RANGES_URL + "?" + Utils.TENANT_IDENTIFIER); + + Type delinquencyRangeListType = new TypeToken>() {}.getType(); + return GSON.fromJson(response, delinquencyRangeListType); + } + + public static GetDelinquencyRangesResponse getDelinquencyRange(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId) { + String response = Utils.performServerGet(requestSpec, responseSpec, + DELINQUENCY_RANGES_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER); + LOG.info("----- {}", response); + return GSON.fromJson(response, GetDelinquencyRangesResponse.class); + } + + public static PostDelinquencyRangeResponse createDelinquencyRange(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String json) { + final String response = Utils.performServerPost(requestSpec, responseSpec, DELINQUENCY_RANGES_URL + "?" + Utils.TENANT_IDENTIFIER, + json, null); + LOG.info("----- {}", response); + return GSON.fromJson(response, PostDelinquencyRangeResponse.class); + } + + public static PutDelinquencyRangeResponse updateDelinquencyRange(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId, final String json) { + final String response = Utils.performServerPut(requestSpec, responseSpec, + DELINQUENCY_RANGES_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER, json, null); + LOG.info("----- {}", response); + return GSON.fromJson(response, PutDelinquencyRangeResponse.class); + } + + public static DeleteDelinquencyRangeResponse deleteDelinquencyRange(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId) { + final String response = Utils.performServerDelete(requestSpec, responseSpec, + DELINQUENCY_RANGES_URL + "/" + resourceId + "?" + Utils.TENANT_IDENTIFIER, Utils.emptyJson(), null); + LOG.info("----- {}", response); + return GSON.fromJson(response, DeleteDelinquencyRangeResponse.class); + } + + public static String getAsJSON(int minimumAgeDays, int maximumAgeDays) { + final HashMap map = new HashMap<>(); + map.put("classification", Utils.randomNameGenerator("Delinquency_", 4)); + map.put("minimumAgeDays", minimumAgeDays); + map.put("maximumAgeDays", maximumAgeDays); + map.put("locale", "en"); + return new Gson().toJson(map); + } + +}