diff --git a/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml b/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml index d1e6be5a80f..23ade62e2fa 100644 --- a/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml +++ b/fineract-accounting/src/main/resources/jpa/static-weaving/module/fineract-accounting/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml index ee2b5fcfef2..18fae21beff 100644 --- a/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml +++ b/fineract-branch/src/main/resources/jpa/static-weaving/module/fineract-branch/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml b/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml index 7482e322cdd..94c1a2f8dec 100644 --- a/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml +++ b/fineract-charge/src/main/resources/jpa/static-weaving/module/fineract-charge/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml index 946e34b7079..d7fdec20372 100644 --- a/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml +++ b/fineract-cob/src/main/resources/jpa/static-weaving/module/fineract-cob/persistence.xml @@ -47,6 +47,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java index 6bda1952377..ec44dfeabf0 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java @@ -18,12 +18,15 @@ */ package org.apache.fineract.portfolio.delinquency.domain; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import jakarta.persistence.Version; @@ -48,6 +51,13 @@ public class DelinquencyBucket extends AbstractAuditableWithUTCDateTimeCustom ranges; + @OneToOne(mappedBy = "bucket", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + private DelinquencyMinimumPaymentPeriodAndRule minimumPaymentPeriodAndRule; + + @Enumerated + @Column(name = "bucket_type") + private DelinquencyBucketType bucketType; + @Version private Long version; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketType.java new file mode 100644 index 00000000000..443537b2c53 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketType.java @@ -0,0 +1,47 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.delinquency.domain; + +import java.util.Arrays; +import lombok.Getter; + +public enum DelinquencyBucketType { + + REGULAR(1L, "bucketType.regular"), // + WORKING_CAPITAL(2L, "bucketType.workingCapital"); + + @Getter + private final Long value; + + @Getter + private final String code; + + DelinquencyBucketType(Long value, String code) { + this.value = value; + this.code = code; + } + + public static DelinquencyBucketType fromLong(Long value) { + if (value == null) { + return null; + } + return Arrays.stream(DelinquencyBucketType.values()).filter(v -> v.getValue().equals(value)).findAny().orElse(REGULAR); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketTypeConverter.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketTypeConverter.java new file mode 100644 index 00000000000..650955fd4d9 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucketTypeConverter.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.domain; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.Optional; + +@Converter(autoApply = true) +public class DelinquencyBucketTypeConverter implements AttributeConverter { + + @Override + public Long convertToDatabaseColumn(DelinquencyBucketType delinquencyBucketType) { + return Optional.ofNullable(delinquencyBucketType).map(DelinquencyBucketType::getValue).orElse(null); + } + + @Override + public DelinquencyBucketType convertToEntityAttribute(Long aLong) { + return DelinquencyBucketType.fromLong(aLong); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyType.java new file mode 100644 index 00000000000..67d42992bc3 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyType.java @@ -0,0 +1,50 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.delinquency.domain; + +import java.util.Arrays; +import lombok.Getter; + +public enum DelinquencyFrequencyType { + + DAYS(0, "delinquencyFrequencyType.days"), // + WEEKS(1, "delinquencyFrequencyType.weeks"), // + MONTHS(2, "delinquencyFrequencyType.months"), // + YEARS(3, "delinquencyFrequencyType.years"); + + @Getter + private final Integer value; + @Getter + private final String code; + + DelinquencyFrequencyType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public static DelinquencyFrequencyType fromInt(final Integer v) { + if (v == null) { + return null; + } + return Arrays.stream(values()).filter(e -> e.getValue().equals(v)).findAny() + .orElseThrow(() -> new IllegalArgumentException("Invalid value of DelinquencyFrequencyType: " + v)); + } + +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyTypeConverter.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyTypeConverter.java new file mode 100644 index 00000000000..960daf2844a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyFrequencyTypeConverter.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 jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.Optional; + +@Converter(autoApply = true) +public class DelinquencyFrequencyTypeConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(DelinquencyFrequencyType delinquencyFrequencyType) { + return Optional.ofNullable(delinquencyFrequencyType).map(DelinquencyFrequencyType::getValue).orElse(null); + } + + @Override + public DelinquencyFrequencyType convertToEntityAttribute(Integer intValue) { + return DelinquencyFrequencyType.fromInt(intValue); + + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPayment.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPayment.java new file mode 100644 index 00000000000..788156856f5 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPayment.java @@ -0,0 +1,45 @@ +/** + * 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.Arrays; +import lombok.Getter; + +public enum DelinquencyMinimumPayment { + + PERCENTAGE(1L, "delinquencyMinimumPayment.percentage"), // + FLAT(2L, "delinquencyMinimumPayment.flat"); + + @Getter + private final Long value; + @Getter + private final String code; + + DelinquencyMinimumPayment(Long value, String code) { + this.value = value; + this.code = code; + } + + public static DelinquencyMinimumPayment fromLong(Long value) { + if (value == null) { + return null; + } + return Arrays.stream(DelinquencyMinimumPayment.values()).filter(v -> v.getValue().equals(value)).findAny().orElse(null); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentConverter.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentConverter.java new file mode 100644 index 00000000000..2aa3ee89378 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentConverter.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.domain; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.Optional; + +@Converter(autoApply = true) +public class DelinquencyMinimumPaymentConverter implements AttributeConverter { + + @Override + public Long convertToDatabaseColumn(DelinquencyMinimumPayment delinquencyMinimumPayment) { + return Optional.ofNullable(delinquencyMinimumPayment).map(DelinquencyMinimumPayment::getValue).orElse(null); + } + + @Override + public DelinquencyMinimumPayment convertToEntityAttribute(Long aLong) { + return DelinquencyMinimumPayment.fromLong(aLong); + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRule.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRule.java new file mode 100644 index 00000000000..ca1ee989cd7 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRule.java @@ -0,0 +1,61 @@ +/** + * 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 jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.io.Serial; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_delinquency_payment_rule") +public class DelinquencyMinimumPaymentPeriodAndRule extends AbstractAuditableWithUTCDateTimeCustom { + + @Serial + private static final long serialVersionUID = -9204385885041120403L; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "bucket_id", nullable = false, unique = true) + private DelinquencyBucket bucket; + + @Column(name = "frequency", nullable = false) + private Long frequency; + + @Column(name = "frequency_type", nullable = false) + private DelinquencyFrequencyType frequencyType; + + @Column(name = "minimum_payment", nullable = false) + private BigDecimal minimumPayment; + + @Column(name = "minimum_payment_type", nullable = false) + private DelinquencyMinimumPayment minimumPaymentType; +} diff --git a/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml b/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml index 4883cc15e81..dda569b6f16 100644 --- a/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml +++ b/fineract-core/src/main/resources/jpa/static-weaving/module/fineract-core/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java index 1590115c84a..b687dad6e31 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalStepDef.java @@ -25,6 +25,7 @@ import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -33,10 +34,15 @@ import org.apache.fineract.client.feign.services.WorkingCapitalLoanProductsApi; import org.apache.fineract.client.feign.util.CallFailedRuntimeException; import org.apache.fineract.client.models.DeleteWorkingCapitalLoanProductsProductIdResponse; +import org.apache.fineract.client.models.DelinquencyBucketData; +import org.apache.fineract.client.models.DelinquencyBucketRequest; +import org.apache.fineract.client.models.DelinquencyRangeData; import org.apache.fineract.client.models.GetConfigurableAttributes; import org.apache.fineract.client.models.GetPaymentAllocation; import org.apache.fineract.client.models.GetWorkingCapitalLoanProductsProductIdResponse; +import org.apache.fineract.client.models.MinimumPaymentPeriodAndRule; import org.apache.fineract.client.models.PostAllowAttributeOverrides; +import org.apache.fineract.client.models.PostDelinquencyBucketResponse; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsRequest; import org.apache.fineract.client.models.PostWorkingCapitalLoanProductsResponse; import org.apache.fineract.client.models.PutWorkingCapitalLoanProductsProductIdRequest; @@ -805,4 +811,62 @@ public void checkWorkingCapitalLoanProductDeleteFailure(Long productId) { assertThat(exception.getDeveloperMessage()) .contains(ErrorMessageHelper.workingCapitalLoanProductIdentifiedDoesNotExistFailure(String.valueOf(productId))); } + + @When("Admin Calls Delinquency Template") + public void adminCallsDelinquencyTemplate() { + DelinquencyBucketData template = ok(() -> fineractFeignClient.delinquencyRangeAndBucketsManagement().getTemplate3()); + log.info("Template DelinquencyBucketData: {}", template); + } + + @When("Admin creates WC Delinquency Bucket With Values") + public void adminCreatesWCDelinquencyBucketWithValues() { + DelinquencyBucketRequest delinquencyBucketRequest = new DelinquencyBucketRequest() // + .name("DB-" + System.currentTimeMillis()) // + .bucketType("2").ranges(List.of(1L)).minimumPaymentPeriodAndRule(new MinimumPaymentPeriodAndRule().frequency(1L) + .frequencyType(1).minimumPayment(BigDecimal.valueOf(1.23D)).minimumPaymentType(1L)); + PostDelinquencyBucketResponse ok = ok( + () -> fineractFeignClient.delinquencyRangeAndBucketsManagement().createDelinquencyBucket(delinquencyBucketRequest)); + assertThat(ok).isNotNull(); + assertThat(ok.getResourceId()).isNotNull(); + testContext().get().put("DELINQUENCY_BUCKET_ID", ok.getResourceId()); + } + + @Then("Get Delinquency Bucket has the following values") + public void getDelinquencyBucketHasTheFollowingValues() { + Long id = (Long) testContext().get().get("DELINQUENCY_BUCKET_ID"); + DelinquencyBucketData delinquencyBucketData = ok( + () -> fineractFeignClient.delinquencyRangeAndBucketsManagement().getDelinquencyBucket(id)); + log.info("Get DelinquencyBucketData: {}", delinquencyBucketData); + } + + @Then("Get Delinquency Bucket With Template has the following values") + public void getDelinquencyBucketWithTemplateHasTheFollowingValues() { + Long id = (Long) testContext().get().get("DELINQUENCY_BUCKET_ID"); + DelinquencyBucketData delinquencyBucketData = ok(() -> fineractFeignClient.delinquencyRangeAndBucketsManagement() + .getDelinquencyBucketUniversal(id, Map.of("template", "true"))); + log.info("Get With Template DelinquencyBucketData: {}", delinquencyBucketData); + } + + @When("Admin modifies WC Delinquency Bucket With Values") + public void adminModifiesWCDelinquencyBucketWithValues() { + Long id = (Long) testContext().get().get("DELINQUENCY_BUCKET_ID"); + DelinquencyBucketData delinquencyBucketData = ok( + () -> fineractFeignClient.delinquencyRangeAndBucketsManagement().getDelinquencyBucket(id)); + DelinquencyBucketRequest request = new DelinquencyBucketRequest() // + .ranges(delinquencyBucketData.getRanges().stream().map(DelinquencyRangeData::getId).toList()) + // 1 Regular + // 2 Working Capital + .name(delinquencyBucketData.getName()).bucketType("2").minimumPaymentPeriodAndRule(new MinimumPaymentPeriodAndRule() // + .minimumPayment(BigDecimal.valueOf(7.89D)).minimumPaymentType(2L) // + .frequencyType(3) // + .frequency(4L) // + ); + ok(() -> fineractFeignClient.delinquencyRangeAndBucketsManagement().updateDelinquencyBucket(id, request)); + } + + @When("Admin deletes WC Delinquency Bucket With Values") + public void adminDeletesWCDelinquencyBucketWithValues() { + Long id = (Long) testContext().get().get("DELINQUENCY_BUCKET_ID"); + ok(() -> fineractFeignClient.delinquencyRangeAndBucketsManagement().deleteDelinquencyBucket(id)); + } } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyConfiguration.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyConfiguration.feature new file mode 100644 index 00000000000..c6192122173 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalDelinquencyConfiguration.feature @@ -0,0 +1,12 @@ +@WCDelinquencyCFeature @WC +Feature: Working Capital Delinquency Configuration + + @TestRailId:CXXXX + Scenario: Verify Working Capital Delinquency Configuration CRUD + When Admin Calls Delinquency Template + When Admin creates WC Delinquency Bucket With Values + Then Get Delinquency Bucket has the following values + Then Get Delinquency Bucket With Template has the following values + When Admin modifies WC Delinquency Bucket With Values + Then Get Delinquency Bucket has the following values + When Admin deletes WC Delinquency Bucket With Values \ No newline at end of file diff --git a/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml b/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml index 3ecc879408b..fd2d3078acb 100644 --- a/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml +++ b/fineract-investor/src/main/resources/jpa/static-weaving/module/fineract-investor/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java index d4bd7d4108a..b6d9b34228d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiConstants.java @@ -25,6 +25,12 @@ private DelinquencyApiConstants() { } public static final String NAME_PARAM_NAME = "name"; + public static final String BUCKET_TYPE_PARAM_NAME = "bucketType"; + public static final String MINIMUM_PAYMENT_PERIOD_AND_RULE_PARAM_NAME = "minimumPaymentPeriodAndRule"; + public static final String FREQUENCY_PARAM_NAME = "frequency"; + public static final String FREQUENCY_TYPE_PARAM_NAME = "frequencyType"; + public static final String MINIMUM_PAYMENT_PARAM_NAME = "minimumPayment"; + public static final String MINIMUM_PAYMENT_TYPE_PARAM_NAME = "minimumPaymentType"; public static final String CLASSIFICATION_PARAM_NAME = "classification"; public static final String RANGES_PARAM_NAME = "ranges"; public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java index 811c6b539fd..71b24adaa00 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResource.java @@ -33,17 +33,26 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.Arrays; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +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.domain.DelinquencyBucketType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyFrequencyType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPayment; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.springframework.stereotype.Component; @@ -58,6 +67,7 @@ public class DelinquencyApiResource { private final DefaultToApiJsonSerializer jsonSerializerRange; private final DelinquencyReadPlatformService readPlatformService; private final PortfolioCommandSourceWritePlatformService commandWritePlatformService; + private final ApiRequestParameterHelper apiRequestParameterHelper; public static final String DELINQUENCY_BUCKET = "DELINQUENCY_BUCKET"; @GET @@ -127,6 +137,16 @@ public CommandProcessingResult deleteDelinquencyRange( return commandWritePlatformService.logCommandSource(commandRequest); } + @GET + @Path("buckets/template") + @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Template for Delinquency Buckets", description = "") + public DelinquencyBucketData getTemplate() { + securityContext.authenticatedUser().validateHasReadPermission(DELINQUENCY_BUCKET); + return handleTemplate(null); + } + @GET @Path("buckets") @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON }) @@ -143,9 +163,27 @@ public List getDelinquencyBuckets() { @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Retrieve a specific Delinquency Bucket based on the Id", description = "") public DelinquencyBucketData getDelinquencyBucket( - @PathParam("delinquencyBucketId") @Parameter(description = "delinquencyBucketId") final Long delinquencyBucketId) { + @PathParam("delinquencyBucketId") @Parameter(description = "delinquencyBucketId") final Long delinquencyBucketId, + @Context final UriInfo uriInfo) { securityContext.authenticatedUser().validateHasReadPermission(DELINQUENCY_BUCKET); - return this.readPlatformService.retrieveDelinquencyBucket(delinquencyBucketId); + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + DelinquencyBucketData delinquencyBucketData = this.readPlatformService.retrieveDelinquencyBucket(delinquencyBucketId); + return settings.isTemplate() ? handleTemplate(delinquencyBucketData) : delinquencyBucketData; + } + + private DelinquencyBucketData handleTemplate(DelinquencyBucketData delinquencyBucketData) { + if (delinquencyBucketData == null) { + delinquencyBucketData = DelinquencyBucketData.getDataInstance(null, null, List.of(), DelinquencyBucketType.REGULAR.getValue(), + null); + } + delinquencyBucketData.setRangesOptions(this.readPlatformService.retrieveAllDelinquencyRanges()); + delinquencyBucketData.setBucketTypeOptions( + Arrays.stream(DelinquencyBucketType.values()).map(e -> CodeValueData.instance(e.getValue(), e.getCode())).toList()); + delinquencyBucketData.setFrequencyTypeOptions(Arrays.stream(DelinquencyFrequencyType.values()) + .map(e -> CodeValueData.instance(e.getValue().longValue(), e.getCode())).toList()); + delinquencyBucketData.setMinimumPaymentOptions( + Arrays.stream(DelinquencyMinimumPayment.values()).map(e -> CodeValueData.instance(e.getValue(), e.getCode())).toList()); + return delinquencyBucketData; } @POST diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyBucketRequest.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyBucketRequest.java index ed50e91709a..67fcf117fa9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyBucketRequest.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyBucketRequest.java @@ -20,6 +20,7 @@ import java.io.Serial; import java.io.Serializable; +import java.math.BigDecimal; import java.util.List; import lombok.Getter; import lombok.NoArgsConstructor; @@ -35,4 +36,20 @@ public class DelinquencyBucketRequest implements Serializable { private String name; private List ranges; + private String bucketType; + private MinimumPaymentPeriodAndRule minimumPaymentPeriodAndRule; + + @Setter + @Getter + @NoArgsConstructor + public static class MinimumPaymentPeriodAndRule implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long frequency; + private Integer frequencyType; + private BigDecimal minimumPayment; + private Long minimumPaymentType; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java index b078343cf4b..1f14c529081 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyBucketData.java @@ -19,11 +19,13 @@ package org.apache.fineract.portfolio.delinquency.data; import java.io.Serializable; +import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; @ToString @AllArgsConstructor @@ -34,5 +36,17 @@ public class DelinquencyBucketData implements Serializable { private Long id; private String name; private List ranges; + private Long bucketType; + private DelinquencyMinimumPaymentPeriodAndRuleData minimumPaymentPeriodAndRule; + + public static DelinquencyBucketData getDataInstance(Long id, String name, List ranges, Long bucketType, + DelinquencyMinimumPaymentPeriodAndRuleData minimumPaymentPeriodAndRule) { + return new DelinquencyBucketData(id, name, ranges, bucketType, minimumPaymentPeriodAndRule, null, null, null, null); + } + + private List rangesOptions = new ArrayList<>(); + private List bucketTypeOptions = new ArrayList<>(); + private List frequencyTypeOptions = new ArrayList<>(); + private List minimumPaymentOptions = new ArrayList<>(); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyMinimumPaymentPeriodAndRuleData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyMinimumPaymentPeriodAndRuleData.java new file mode 100644 index 00000000000..6eb2fda0013 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/data/DelinquencyMinimumPaymentPeriodAndRuleData.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.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@ToString +@AllArgsConstructor +@Getter +@Setter +public class DelinquencyMinimumPaymentPeriodAndRuleData { + + private Long frequency; + private Integer frequencyType; + private BigDecimal minimumPayment; + private Long minimumPaymentType; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRuleRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRuleRepository.java new file mode 100644 index 00000000000..a2524032779 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyMinimumPaymentPeriodAndRuleRepository.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fineract.portfolio.delinquency.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; + +public interface DelinquencyMinimumPaymentPeriodAndRuleRepository extends JpaRepository, + JpaSpecificationExecutor, CrudRepository { + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java index 4b207c91aaa..454e6bd4d27 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyBucketMapper.java @@ -22,13 +22,23 @@ import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketType; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; @Mapper(config = MapstructMapperConfig.class) -public interface DelinquencyBucketMapper { +public interface DelinquencyBucketMapper extends DelinquencyMinimumPaymentPeriodAndRuleMapper { + @Mapping(target = "rangesOptions", ignore = true) + @Mapping(target = "bucketTypeOptions", ignore = true) + @Mapping(target = "frequencyTypeOptions", ignore = true) + @Mapping(target = "minimumPaymentOptions", ignore = true) DelinquencyBucketData map(DelinquencyBucket source); List map(List sources); + default Long map(DelinquencyBucketType value) { + return value.getValue(); + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyMinimumPaymentPeriodAndRuleMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyMinimumPaymentPeriodAndRuleMapper.java new file mode 100644 index 00000000000..d69b7bbf312 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/DelinquencyMinimumPaymentPeriodAndRuleMapper.java @@ -0,0 +1,41 @@ +/** + * 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 org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.delinquency.data.DelinquencyMinimumPaymentPeriodAndRuleData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPayment; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule; +import org.mapstruct.Mapper; + +@Mapper(config = MapstructMapperConfig.class) +public interface DelinquencyMinimumPaymentPeriodAndRuleMapper { + + DelinquencyMinimumPaymentPeriodAndRuleData map(DelinquencyMinimumPaymentPeriodAndRule source); + + default Integer map(PeriodFrequencyType periodFrequencyType) { + return periodFrequencyType.getValue(); + } + + default Long map(DelinquencyMinimumPayment value) { + return value.getValue(); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 392e2ce814b..5803a916525 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -42,6 +42,11 @@ 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.DelinquencyBucketType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyFrequencyType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPayment; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRuleRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; @@ -84,6 +89,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final BusinessEventNotifierService businessEventNotifierService; private final DelinquencyWritePlatformServiceHelper delinquencyHelper; + private final DelinquencyMinimumPaymentPeriodAndRuleRepository delinquencyMinimumPaymentPeriodAndRuleRepository; @Override public CommandProcessingResult createDelinquencyRange(JsonCommand command) { @@ -338,7 +344,22 @@ private DelinquencyBucket createDelinquencyBucket(DelinquencyBucketData data, Ma if (delinquencyBucket.isEmpty()) { DelinquencyBucket newDelinquencyBucket = new DelinquencyBucket(data.getName()); + newDelinquencyBucket.setBucketType( + data.getBucketType() != null ? DelinquencyBucketType.fromLong(data.getBucketType()) : DelinquencyBucketType.REGULAR); + if (DelinquencyBucketType.WORKING_CAPITAL.equals(newDelinquencyBucket.getBucketType())) { + // data.getMinimumPaymentPeriodAndRule() + newDelinquencyBucket.setMinimumPaymentPeriodAndRule(new DelinquencyMinimumPaymentPeriodAndRule()); + newDelinquencyBucket.getMinimumPaymentPeriodAndRule().setBucket(newDelinquencyBucket); + newDelinquencyBucket.getMinimumPaymentPeriodAndRule().setFrequency(data.getMinimumPaymentPeriodAndRule().getFrequency()); + newDelinquencyBucket.getMinimumPaymentPeriodAndRule().setMinimumPaymentType( + DelinquencyMinimumPayment.fromLong(data.getMinimumPaymentPeriodAndRule().getMinimumPaymentType())); + newDelinquencyBucket.getMinimumPaymentPeriodAndRule() + .setFrequencyType(DelinquencyFrequencyType.fromInt(data.getMinimumPaymentPeriodAndRule().getFrequencyType())); + newDelinquencyBucket.getMinimumPaymentPeriodAndRule() + .setMinimumPayment(data.getMinimumPaymentPeriodAndRule().getMinimumPayment()); + } repositoryBucket.save(newDelinquencyBucket); + setDelinquencyBucketMappings(newDelinquencyBucket, data); return newDelinquencyBucket; } else { @@ -353,6 +374,52 @@ private DelinquencyBucket updateDelinquencyBucket(DelinquencyBucket delinquencyB delinquencyBucket.setName(data.getName()); changes.put(DelinquencyApiConstants.NAME_PARAM_NAME, data.getName()); } + if (!data.getBucketType().equals(delinquencyBucket.getBucketType().getValue())) { + changes.put(DelinquencyApiConstants.BUCKET_TYPE_PARAM_NAME, data.getBucketType()); + delinquencyBucket.setBucketType(DelinquencyBucketType.fromLong(data.getBucketType())); + } + if (delinquencyBucket.getBucketType().equals(DelinquencyBucketType.WORKING_CAPITAL)) { + if (delinquencyBucket.getMinimumPaymentPeriodAndRule() == null) { + delinquencyBucket.setMinimumPaymentPeriodAndRule(new DelinquencyMinimumPaymentPeriodAndRule()); + delinquencyBucket.getMinimumPaymentPeriodAndRule().setBucket(delinquencyBucket); + } + if (!data.getMinimumPaymentPeriodAndRule().getFrequency() + .equals(delinquencyBucket.getMinimumPaymentPeriodAndRule().getFrequency())) { + delinquencyBucket.getMinimumPaymentPeriodAndRule().setFrequency(data.getMinimumPaymentPeriodAndRule().getFrequency()); + changes.put(DelinquencyApiConstants.FREQUENCY_PARAM_NAME, + delinquencyBucket.getMinimumPaymentPeriodAndRule().getFrequency()); + } + if (!data.getMinimumPaymentPeriodAndRule().getFrequencyType() + .equals(delinquencyBucket.getMinimumPaymentPeriodAndRule().getFrequencyType().getValue())) { + delinquencyBucket.getMinimumPaymentPeriodAndRule() + .setFrequencyType(DelinquencyFrequencyType.fromInt(data.getMinimumPaymentPeriodAndRule().getFrequencyType())); + changes.put(DelinquencyApiConstants.FREQUENCY_TYPE_PARAM_NAME, + delinquencyBucket.getMinimumPaymentPeriodAndRule().getFrequencyType().getValue()); + } + if (!data.getMinimumPaymentPeriodAndRule().getMinimumPaymentType() + .equals(delinquencyBucket.getMinimumPaymentPeriodAndRule().getMinimumPaymentType().getValue())) { + changes.put(DelinquencyApiConstants.MINIMUM_PAYMENT_TYPE_PARAM_NAME, + delinquencyBucket.getMinimumPaymentPeriodAndRule().getMinimumPaymentType().getValue()); + delinquencyBucket.getMinimumPaymentPeriodAndRule().setMinimumPaymentType( + DelinquencyMinimumPayment.fromLong(data.getMinimumPaymentPeriodAndRule().getMinimumPaymentType())); + } + if (data.getMinimumPaymentPeriodAndRule().getMinimumPayment() + .compareTo(delinquencyBucket.getMinimumPaymentPeriodAndRule().getMinimumPayment()) != 0) { + changes.put(DelinquencyApiConstants.MINIMUM_PAYMENT_PARAM_NAME, + delinquencyBucket.getMinimumPaymentPeriodAndRule().getMinimumPayment()); + delinquencyBucket.getMinimumPaymentPeriodAndRule() + .setMinimumPayment(data.getMinimumPaymentPeriodAndRule().getMinimumPayment()); + } + } else { + if (delinquencyBucket.getMinimumPaymentPeriodAndRule() != null) { + changes.put(DelinquencyApiConstants.MINIMUM_PAYMENT_PERIOD_AND_RULE_PARAM_NAME, null); + DelinquencyMinimumPaymentPeriodAndRule minimumPaymentPeriodAndRule = delinquencyBucket.getMinimumPaymentPeriodAndRule(); + minimumPaymentPeriodAndRule.setBucket(null); + delinquencyBucket.setMinimumPaymentPeriodAndRule(null); + delinquencyMinimumPaymentPeriodAndRuleRepository.save(minimumPaymentPeriodAndRule); + delinquencyMinimumPaymentPeriodAndRuleRepository.delete(minimumPaymentPeriodAndRule); + } + } if (!changes.isEmpty()) { delinquencyBucket = repositoryBucket.save(delinquencyBucket); } @@ -363,7 +430,7 @@ private DelinquencyBucket updateDelinquencyBucket(DelinquencyBucket delinquencyB private void setDelinquencyBucketMappings(DelinquencyBucket delinquencyBucket, DelinquencyBucketData data) { List rangeIds = new ArrayList<>(); data.getRanges().forEach(dataRange -> rangeIds.add(dataRange.getId())); - + delinquencyBucket.setBucketType(DelinquencyBucketType.fromLong(data.getBucketType())); List ranges = repositoryRange.findAllById(rangeIds); validateDelinquencyRanges(ranges); List bucketMappings = repositoryBucketMappings.findByDelinquencyBucket(delinquencyBucket); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java index 410f05e1958..022e3c4e47f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java @@ -22,6 +22,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRuleRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; @@ -83,12 +84,13 @@ public DelinquencyWritePlatformService delinquencyWritePlatformService(Delinquen DelinquencyReadPlatformService delinquencyReadPlatformService, LoanDelinquencyActionRepository loanDelinquencyActionRepository, DelinquencyActionParseAndValidator delinquencyActionParseAndValidator, DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, - DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper) { + DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper, + DelinquencyMinimumPaymentPeriodAndRuleRepository delinquencyMinimumPaymentPeriodAndRuleRepository) { return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket, repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository, delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService, - delinquencyWritePlatformServiceHelper); + delinquencyWritePlatformServiceHelper, delinquencyMinimumPaymentPeriodAndRuleRepository); } @Bean diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java index 93888f2962e..ddd7e2bbc13 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyBucketParseAndValidator.java @@ -20,8 +20,10 @@ import com.google.gson.JsonObject; import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -29,7 +31,9 @@ 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.DelinquencyMinimumPaymentPeriodAndRuleData; import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketType; import org.springframework.stereotype.Component; @RequiredArgsConstructor @@ -55,7 +59,9 @@ private DelinquencyBucketData validateAndParseUpdate(final DataValidatorBuilder } jsonHelper.checkForUnsupportedParameters(element, - List.of(DelinquencyApiConstants.NAME_PARAM_NAME, DelinquencyApiConstants.RANGES_PARAM_NAME)); + List.of(DelinquencyApiConstants.NAME_PARAM_NAME, DelinquencyApiConstants.RANGES_PARAM_NAME, + DelinquencyApiConstants.BUCKET_TYPE_PARAM_NAME, + DelinquencyApiConstants.MINIMUM_PAYMENT_PERIOD_AND_RULE_PARAM_NAME)); final String name = jsonHelper.extractStringNamed(DelinquencyApiConstants.NAME_PARAM_NAME, element); @@ -70,7 +76,49 @@ private DelinquencyBucketData validateAndParseUpdate(final DataValidatorBuilder ranges.add(DelinquencyRangeData.reference(Long.parseLong(rangeId))); } } - return dataValidator.hasError() ? null : new DelinquencyBucketData(null, name, ranges); + + final Long bucketTypeParam = jsonHelper.extractLongNamed(DelinquencyApiConstants.BUCKET_TYPE_PARAM_NAME, element); + dataValidator.reset().parameter(DelinquencyApiConstants.BUCKET_TYPE_PARAM_NAME).value(bucketTypeParam).ignoreIfNull() + .isOneOfTheseValues(DelinquencyBucketType.REGULAR.getValue(), DelinquencyBucketType.WORKING_CAPITAL.getValue()); + Long bucketType = bucketTypeParam == null ? DelinquencyBucketType.REGULAR.getValue() : bucketTypeParam; + + DelinquencyMinimumPaymentPeriodAndRuleData minimumPaymentPeriodAndRule = null; + if (DelinquencyBucketType.WORKING_CAPITAL.getValue().equals(bucketType)) { + JsonObject minimumPaymentPeriodAndRuleElement = jsonHelper + .extractJsonObjectNamed(DelinquencyApiConstants.MINIMUM_PAYMENT_PERIOD_AND_RULE_PARAM_NAME, element); + minimumPaymentPeriodAndRule = validateAndParseUpdateMinimumPaymentPeriodAndRule(dataValidator, + minimumPaymentPeriodAndRuleElement, jsonHelper); + + } + + return dataValidator.hasError() ? null + : DelinquencyBucketData.getDataInstance(null, name, ranges, bucketType, minimumPaymentPeriodAndRule); + } + + private DelinquencyMinimumPaymentPeriodAndRuleData validateAndParseUpdateMinimumPaymentPeriodAndRule(DataValidatorBuilder dataValidator, + JsonObject element, FromJsonHelper jsonHelper) { + dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUM_PAYMENT_PERIOD_AND_RULE_PARAM_NAME).value(element).notNull(); + if (element != null) { + Long frequency = jsonHelper.extractLongNamed(DelinquencyApiConstants.FREQUENCY_PARAM_NAME, element); + dataValidator.reset().parameter(DelinquencyApiConstants.FREQUENCY_PARAM_NAME).value(frequency).notNull(); + + Integer frequencyType = Math + .toIntExact(jsonHelper.extractLongNamed(DelinquencyApiConstants.FREQUENCY_TYPE_PARAM_NAME, element)); + dataValidator.reset().parameter(DelinquencyApiConstants.FREQUENCY_TYPE_PARAM_NAME).value(frequencyType).notNull() + .inMinMaxRange(0, 3); + + BigDecimal minimumPayment = jsonHelper.extractBigDecimalNamed(DelinquencyApiConstants.MINIMUM_PAYMENT_PARAM_NAME, element, + Locale.US); + dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUM_PAYMENT_PARAM_NAME).value(minimumPayment).notNull() + .zeroOrPositiveAmount(); + + Long minimumPaymentType = jsonHelper.extractLongNamed(DelinquencyApiConstants.MINIMUM_PAYMENT_TYPE_PARAM_NAME, element); + dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUM_PAYMENT_TYPE_PARAM_NAME).value(minimumPaymentType).notNull() + .inMinMaxRange(1, 2); + + return new DelinquencyMinimumPaymentPeriodAndRuleData(frequency, frequencyType, minimumPayment, minimumPaymentType); + } + return null; } } diff --git a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml index 8eb34963c21..32f739f46dd 100644 --- a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml +++ b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml index 27208079e4a..08ef888d56d 100644 --- a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml +++ b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory 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 895221d578a..b9abd911dcd 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 @@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketType; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType; @@ -548,8 +549,8 @@ public LoanProductData mapRow(@NonNull final ResultSet rs, @SuppressWarnings("un // Delinquency Buckets final Long delinquencyBucketId = JdbcSupport.getLong(rs, "delinquencyBucketId"); final String delinquencyBucketName = rs.getString("delinquencyBucketName"); - final DelinquencyBucketData delinquencyBucket = new DelinquencyBucketData(delinquencyBucketId, delinquencyBucketName, - new ArrayList<>()); + final DelinquencyBucketData delinquencyBucket = DelinquencyBucketData.getDataInstance(delinquencyBucketId, + delinquencyBucketName, new ArrayList<>(), DelinquencyBucketType.REGULAR.getValue(), null); final String loanScheduleTypeStr = rs.getString("loanScheduleType"); final LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loanScheduleTypeStr); 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 6ce8d437d8e..b66c93a6683 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 @@ -238,4 +238,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0220_delinquency_for_working_capital_loans.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0220_delinquency_for_working_capital_loans.xml new file mode 100644 index 00000000000..2c38991bd2a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0220_delinquency_for_working_capital_loans.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml index 6ffd51d1ff3..6a2dc3b5837 100644 --- a/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml +++ b/fineract-provider/src/main/resources/jpa/static-weaving/module/fineract-provider/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index d692d1d9672..e74b9e70fd4 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -57,6 +57,7 @@ import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRuleRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; @@ -130,6 +131,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { private LoanDelinquencyActionRepository loanDelinquencyActionRepository; @Mock private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; + @Mock + private DelinquencyMinimumPaymentPeriodAndRuleRepository delinquencyMinimumPaymentPeriodAndRuleRepository; private DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper; @@ -153,7 +156,7 @@ public void setUp() { repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository, delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService, - delinquencyWritePlatformServiceHelper); + delinquencyWritePlatformServiceHelper, delinquencyMinimumPaymentPeriodAndRuleRepository); } @AfterAll diff --git a/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml b/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml index 61addd405f4..cd0da3f2429 100644 --- a/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml +++ b/fineract-rates/src/main/resources/jpa/static-weaving/module/fineract-rates/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml b/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml index 4883cc15e81..dda569b6f16 100644 --- a/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml +++ b/fineract-report/src/main/resources/jpa/static-weaving/module/fineract-report/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml b/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml index 2c9e1ba7ba1..4997a4dc594 100644 --- a/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml +++ b/fineract-savings/src/main/resources/jpa/static-weaving/module/fineract-savings/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml b/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml index 4a482aecacb..0c5eb540a9a 100644 --- a/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml +++ b/fineract-tax/src/main/resources/jpa/static-weaving/module/fineract-tax/persistence.xml @@ -48,6 +48,7 @@ org.apache.fineract.portfolio.calendar.domain.Calendar org.apache.fineract.portfolio.calendar.domain.CalendarHistory org.apache.fineract.portfolio.client.domain.ClientIdentifier + org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange org.apache.fineract.portfolio.group.domain.StaffAssignmentHistory diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DelinquencyRangeScheduleBusinessStep.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DelinquencyRangeScheduleBusinessStep.java new file mode 100644 index 00000000000..5a4dfcb9290 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/cob/workingcapitalloan/businessstep/DelinquencyRangeScheduleBusinessStep.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.cob.workingcapitalloan.businessstep; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WcLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.service.WcLoanDelinquencyRangeScheduleService; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Component +public class DelinquencyRangeScheduleBusinessStep extends WorkingCapitalLoanCOBBusinessStep { + + private final WcLoanDelinquencyRangeScheduleService rangeScheduleService; + private final WcLoanDelinquencyRangeScheduleRepository rangeScheduleRepository; + + @Override + public WorkingCapitalLoan execute(WorkingCapitalLoan input) { + if (input.getActualDisbursementDate() == null) { + log.debug("Skipping delinquency range schedule for WC loan {} - not yet disbursed", input.getId()); + return input; + } + + java.time.LocalDate businessDate = DateUtils.getBusinessLocalDate(); + + boolean hasSchedule = rangeScheduleRepository.findTopByLoanIdOrderByPeriodNumberDesc(input.getId()).isPresent(); + if (!hasSchedule) { + rangeScheduleService.generateInitialPeriod(input); + } + + rangeScheduleService.generateNextPeriodIfNeeded(input, businessDate); + rangeScheduleService.evaluateExpiredPeriods(input, businessDate); + + return input; + } + + @Override + public String getEnumStyledName() { + return "WC_DELINQUENCY_RANGE_SCHEDULE"; + } + + @Override + public String getHumanReadableName() { + return "WC Delinquency Range Schedule"; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WcLoanDelinquencyRangeScheduleApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WcLoanDelinquencyRangeScheduleApiResource.java new file mode 100644 index 00000000000..c5ef37b54f8 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WcLoanDelinquencyRangeScheduleApiResource.java @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +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 jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.workingcapitalloan.data.WcLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.service.WcLoanDelinquencyRangeScheduleService; +import org.springframework.stereotype.Component; + +@Path("/v1/working-capital-loans/{loanId}/delinquency-range-schedule") +@Component +@Tag(name = "Working Capital Loan Delinquency Range Schedule", description = "Manages delinquency range schedule periods for Working Capital loans") +@RequiredArgsConstructor +public class WcLoanDelinquencyRangeScheduleApiResource { + + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "WORKINGCAPITALLOAN"; + + private final PlatformSecurityContext context; + private final WcLoanDelinquencyRangeScheduleService rangeScheduleService; + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Delinquency Range Schedule", description = "Retrieves the delinquency range schedule periods for a Working Capital loan") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = WcLoanDelinquencyRangeScheduleData.class)))) }) + public List retrieveDelinquencyRangeSchedule( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId) { + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + return rangeScheduleService.retrieveRangeSchedule(loanId); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WcLoanDelinquencyRangeScheduleData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WcLoanDelinquencyRangeScheduleData.java new file mode 100644 index 00000000000..ef4dc087938 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/WcLoanDelinquencyRangeScheduleData.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class WcLoanDelinquencyRangeScheduleData { + + private Long id; + private Long loanId; + private Integer periodNumber; + private LocalDate fromDate; + private LocalDate toDate; + private BigDecimal expectedAmount; + private BigDecimal paidAmount; + private BigDecimal outstandingAmount; + private Boolean minPaymentCriteriaMet; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WcLoanDelinquencyRangeSchedule.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WcLoanDelinquencyRangeSchedule.java new file mode 100644 index 00000000000..2e45ae55b08 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WcLoanDelinquencyRangeSchedule.java @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; + +@Getter +@Setter +@NoArgsConstructor +@Entity +@Table(name = "m_wc_loan_delinquency_range_schedule", uniqueConstraints = { + @UniqueConstraint(columnNames = { "wc_loan_id", "period_number" }, name = "uc_wc_delinquency_range_schedule_loan_period") }) +public class WcLoanDelinquencyRangeSchedule extends AbstractAuditableWithUTCDateTimeCustom { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wc_loan_id", nullable = false) + private WorkingCapitalLoan loan; + + @Column(name = "period_number", nullable = false) + private Integer periodNumber; + + @Column(name = "from_date", nullable = false) + private LocalDate fromDate; + + @Column(name = "to_date", nullable = false) + private LocalDate toDate; + + @Column(name = "expected_amount", scale = 6, precision = 19) + private BigDecimal expectedAmount; + + @Column(name = "paid_amount", scale = 6, precision = 19) + private BigDecimal paidAmount; + + @Column(name = "outstanding_amount", scale = 6, precision = 19) + private BigDecimal outstandingAmount; + + @Column(name = "min_payment_criteria_met") + private Boolean minPaymentCriteriaMet; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WcLoanDelinquencyRangeScheduleMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WcLoanDelinquencyRangeScheduleMapper.java new file mode 100644 index 00000000000..c031c74ce94 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/WcLoanDelinquencyRangeScheduleMapper.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.workingcapitalloan.mapper; + +import java.util.List; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.workingcapitalloan.data.WcLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WcLoanDelinquencyRangeSchedule; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface WcLoanDelinquencyRangeScheduleMapper { + + @Mapping(target = "loanId", source = "loan.id") + WcLoanDelinquencyRangeScheduleData toData(WcLoanDelinquencyRangeSchedule entity); + + List toDataList(List entities); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WcLoanDelinquencyRangeScheduleRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WcLoanDelinquencyRangeScheduleRepository.java new file mode 100644 index 00000000000..8760b348230 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WcLoanDelinquencyRangeScheduleRepository.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.workingcapitalloan.repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WcLoanDelinquencyRangeSchedule; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface WcLoanDelinquencyRangeScheduleRepository extends JpaRepository { + + List findByLoanIdOrderByPeriodNumberAsc(Long loanId); + + Optional findTopByLoanIdOrderByPeriodNumberDesc(Long loanId); + + Optional findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(Long loanId, LocalDate date, + LocalDate date2); + + List findByLoanIdAndToDateLessThanAndMinPaymentCriteriaMetIsNull(Long loanId, LocalDate businessDate); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleService.java new file mode 100644 index 00000000000..74bafb844c9 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleService.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.workingcapitalloan.service; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.portfolio.workingcapitalloan.data.WcLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; + +public interface WcLoanDelinquencyRangeScheduleService { + + void generateInitialPeriod(WorkingCapitalLoan loan); + + void generateNextPeriodIfNeeded(WorkingCapitalLoan loan, LocalDate businessDate); + + void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount); + + void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDate); + + List retrieveRangeSchedule(Long loanId); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleServiceImpl.java new file mode 100644 index 00000000000..18ff8af0d3c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WcLoanDelinquencyRangeScheduleServiceImpl.java @@ -0,0 +1,172 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyFrequencyType; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyMinimumPaymentPeriodAndRule; +import org.apache.fineract.portfolio.workingcapitalloan.data.WcLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WcLoanDelinquencyRangeSchedule; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WcLoanDelinquencyRangeScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WcLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloanproduct.domain.WorkingCapitalLoanProduct; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Slf4j +@Service +public class WcLoanDelinquencyRangeScheduleServiceImpl implements WcLoanDelinquencyRangeScheduleService { + + private final WcLoanDelinquencyRangeScheduleRepository repository; + private final WcLoanDelinquencyRangeScheduleMapper mapper; + + @Override + public void generateInitialPeriod(WorkingCapitalLoan loan) { + DelinquencyMinimumPaymentPeriodAndRule rule = getMinimumPaymentRule(loan); + if (rule == null) { + return; + } + + LocalDate fromDate = loan.getActualDisbursementDate(); + LocalDate toDate = calculateToDate(fromDate, rule.getFrequency(), rule.getFrequencyType()); + BigDecimal expectedAmount = calculateExpectedAmount(loan, rule); + + WcLoanDelinquencyRangeSchedule period = new WcLoanDelinquencyRangeSchedule(); + period.setLoan(loan); + period.setPeriodNumber(1); + period.setFromDate(fromDate); + period.setToDate(toDate); + period.setExpectedAmount(expectedAmount); + period.setPaidAmount(BigDecimal.ZERO); + period.setOutstandingAmount(expectedAmount); + period.setMinPaymentCriteriaMet(null); + + repository.saveAndFlush(period); + log.debug("Generated initial delinquency range schedule period for WC loan {}", loan.getId()); + } + + @Override + public void generateNextPeriodIfNeeded(WorkingCapitalLoan loan, LocalDate businessDate) { + DelinquencyMinimumPaymentPeriodAndRule rule = getMinimumPaymentRule(loan); + if (rule == null) { + return; + } + + Optional latestPeriodOpt = repository.findTopByLoanIdOrderByPeriodNumberDesc(loan.getId()); + if (latestPeriodOpt.isEmpty()) { + return; + } + + WcLoanDelinquencyRangeSchedule latestPeriod = latestPeriodOpt.get(); + while (latestPeriod.getToDate().isBefore(businessDate)) { + LocalDate newFromDate = latestPeriod.getToDate().plusDays(1); + LocalDate newToDate = calculateToDate(newFromDate, rule.getFrequency(), rule.getFrequencyType()); + BigDecimal expectedAmount = calculateExpectedAmount(loan, rule); + + WcLoanDelinquencyRangeSchedule nextPeriod = new WcLoanDelinquencyRangeSchedule(); + nextPeriod.setLoan(loan); + nextPeriod.setPeriodNumber(latestPeriod.getPeriodNumber() + 1); + nextPeriod.setFromDate(newFromDate); + nextPeriod.setToDate(newToDate); + nextPeriod.setExpectedAmount(expectedAmount); + nextPeriod.setPaidAmount(BigDecimal.ZERO); + nextPeriod.setOutstandingAmount(expectedAmount); + nextPeriod.setMinPaymentCriteriaMet(null); + + latestPeriod = repository.saveAndFlush(nextPeriod); + log.debug("Generated next delinquency range schedule period {} for WC loan {}", nextPeriod.getPeriodNumber(), loan.getId()); + } + } + + @Override + public void applyRepayment(Long loanId, LocalDate transactionDate, BigDecimal amount) { + Optional periodOpt = repository + .findByLoanIdAndFromDateLessThanEqualAndToDateGreaterThanEqual(loanId, transactionDate, transactionDate); + if (periodOpt.isPresent()) { + WcLoanDelinquencyRangeSchedule period = periodOpt.get(); + BigDecimal newPaidAmount = period.getPaidAmount().add(amount); + period.setPaidAmount(newPaidAmount); + period.setOutstandingAmount(period.getExpectedAmount().subtract(newPaidAmount).max(BigDecimal.ZERO)); + repository.saveAndFlush(period); + log.debug("Applied repayment of {} to delinquency range schedule period {} for WC loan {}", amount, period.getPeriodNumber(), + loanId); + } + } + + @Override + public void evaluateExpiredPeriods(WorkingCapitalLoan loan, LocalDate businessDate) { + List unevaluatedPeriods = repository + .findByLoanIdAndToDateLessThanAndMinPaymentCriteriaMetIsNull(loan.getId(), businessDate); + for (WcLoanDelinquencyRangeSchedule period : unevaluatedPeriods) { + boolean criteriaMet = period.getPaidAmount().compareTo(period.getExpectedAmount()) >= 0; + period.setMinPaymentCriteriaMet(criteriaMet); + repository.saveAndFlush(period); + log.debug("Evaluated delinquency range schedule period {} for WC loan {}: criteriaMet={}", period.getPeriodNumber(), + loan.getId(), criteriaMet); + } + } + + @Override + public List retrieveRangeSchedule(Long loanId) { + List periods = repository.findByLoanIdOrderByPeriodNumberAsc(loanId); + return mapper.toDataList(periods); + } + + private DelinquencyMinimumPaymentPeriodAndRule getMinimumPaymentRule(WorkingCapitalLoan loan) { + WorkingCapitalLoanProduct product = loan.getProduct(); + if (product == null) { + return null; + } + DelinquencyBucket bucket = product.getDelinquencyBucket(); + if (bucket == null) { + return null; + } + return bucket.getMinimumPaymentPeriodAndRule(); + } + + private LocalDate calculateToDate(LocalDate fromDate, Long frequency, DelinquencyFrequencyType frequencyType) { + return switch (frequencyType) { + case DAYS -> fromDate.plusDays(frequency - 1); + case WEEKS -> fromDate.plusWeeks(frequency).minusDays(1); + case MONTHS -> fromDate.plusMonths(frequency).minusDays(1); + case YEARS -> fromDate.plusYears(frequency).minusDays(1); + }; + } + + private BigDecimal calculateExpectedAmount(WorkingCapitalLoan loan, DelinquencyMinimumPaymentPeriodAndRule rule) { + BigDecimal principal = loan.getApprovedPrincipal(); + if (principal == null) { + return BigDecimal.ZERO; + } + BigDecimal minimumPayment = rule.getMinimumPayment(); + if (minimumPayment == null) { + return BigDecimal.ZERO; + } + return principal.multiply(minimumPayment).divide(BigDecimal.valueOf(100), MathContext.DECIMAL128); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java index 8544302098c..91e4808ffb0 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloanproduct/domain/WorkingCapitalLoan.java @@ -21,9 +21,13 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import jakarta.persistence.Version; +import java.math.BigDecimal; import java.time.LocalDate; import lombok.AccessLevel; import lombok.Getter; @@ -63,4 +67,13 @@ public class WorkingCapitalLoan extends AbstractAuditableWithUTCDateTimeCustom + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0005_delinquency_range_schedule.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0005_delinquency_range_schedule.xml new file mode 100644 index 00000000000..8730a083762 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0005_delinquency_range_schedule.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission WHERE code = 'READ_WORKINGCAPITALLOAN' + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/WcLoanDelinquencyRangeScheduleIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WcLoanDelinquencyRangeScheduleIntegrationTest.java new file mode 100644 index 00000000000..416292568ce --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/WcLoanDelinquencyRangeScheduleIntegrationTest.java @@ -0,0 +1,145 @@ +/** + * 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.assertNotNull; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +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.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse; +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.util.JSON; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.apache.fineract.integrationtests.common.products.DelinquencyRangesHelper; +import org.apache.fineract.integrationtests.common.workingcapitalloan.WcLoanDelinquencyRangeScheduleHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@Slf4j +@ExtendWith(LoanTestLifecycleExtension.class) +@Order(1) +public class WcLoanDelinquencyRangeScheduleIntegrationTest { + + private static final Gson GSON = new JSON().getGson(); + + 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 testDelinquencyBucketWithMinPaymentValue() { + // given - Create delinquency ranges + final String jsonRange1 = DelinquencyRangesHelper.getAsJSON(1, 30); + final PostDelinquencyRangeResponse rangeResponse1 = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange1); + assertNotNull(rangeResponse1); + + final String jsonRange2 = DelinquencyRangesHelper.getAsJSON(31, 60); + final PostDelinquencyRangeResponse rangeResponse2 = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, + jsonRange2); + assertNotNull(rangeResponse2); + + final List rangeIds = new ArrayList<>(); + rangeIds.add(Math.toIntExact(rangeResponse1.getResourceId())); + rangeIds.add(Math.toIntExact(rangeResponse2.getResourceId())); + + // when - Create a WC delinquency bucket with minimumPayment percentage + final BigDecimal initialMinPayment = new BigDecimal("3"); + final PostDelinquencyBucketResponse createResponse = WcLoanDelinquencyRangeScheduleHelper.createWcDelinquencyBucket(requestSpec, + responseSpec, rangeIds, 30, 0, initialMinPayment, 1); + + // then - Verify creation was successful + assertNotNull(createResponse); + assertNotNull(createResponse.getResourceId()); + log.info("Created WC delinquency bucket with id: {}", createResponse.getResourceId()); + + // Retrieve the bucket and verify minimumPaymentValue is persisted + final String bucketJson = Utils.performServerGet(requestSpec, responseSpec, + "/fineract-provider/api/v1/delinquency/buckets/" + createResponse.getResourceId() + "?" + Utils.TENANT_IDENTIFIER); + assertNotNull(bucketJson); + log.info("Retrieved bucket JSON: {}", bucketJson); + + final Type mapType = new TypeToken>() {}.getType(); + final Map bucketMap = GSON.fromJson(bucketJson, mapType); + + assertEquals(2.0, bucketMap.get("bucketType"), "Bucket type should be WORKING_CAPITAL (2)"); + + @SuppressWarnings("unchecked") + final Map ruleMap = (Map) bucketMap.get("minimumPaymentPeriodAndRule"); + assertNotNull(ruleMap, "minimumPaymentPeriodAndRule should not be null"); + assertEquals(30.0, ruleMap.get("frequency"), "Frequency should be 30"); + assertEquals(0.0, ruleMap.get("frequencyType"), "Frequency type should be DAYS (0)"); + assertEquals(3.0, ruleMap.get("minimumPayment"), "Minimum payment should be 3"); + assertEquals(1.0, ruleMap.get("minimumPaymentType"), "Minimum payment type should be PERCENTAGE (1)"); + + // when - Update the bucket with a different minimumPayment + final BigDecimal updatedMinPayment = new BigDecimal("5"); + final PutDelinquencyBucketResponse updateResponse = WcLoanDelinquencyRangeScheduleHelper.updateWcDelinquencyBucket(requestSpec, + responseSpec, Math.toIntExact(createResponse.getResourceId()), rangeIds, 30, 0, updatedMinPayment, 1); + + // then - Verify update was successful + assertNotNull(updateResponse); + log.info("Updated WC delinquency bucket with id: {}", updateResponse.getResourceId()); + + // Retrieve the bucket again and verify the updated minimumPaymentValue + final String updatedBucketJson = Utils.performServerGet(requestSpec, responseSpec, + "/fineract-provider/api/v1/delinquency/buckets/" + createResponse.getResourceId() + "?" + Utils.TENANT_IDENTIFIER); + assertNotNull(updatedBucketJson); + log.info("Retrieved updated bucket JSON: {}", updatedBucketJson); + + final Map updatedBucketMap = GSON.fromJson(updatedBucketJson, mapType); + + @SuppressWarnings("unchecked") + final Map updatedRuleMap = (Map) updatedBucketMap.get("minimumPaymentPeriodAndRule"); + assertNotNull(updatedRuleMap, "minimumPaymentPeriodAndRule should not be null after update"); + assertEquals(5.0, updatedRuleMap.get("minimumPayment"), "minimumPayment should be updated to 5"); + + // Cleanup - Delete the bucket + final DeleteDelinquencyBucketResponse deleteResponse = DelinquencyBucketsHelper.deleteDelinquencyBucket(requestSpec, responseSpec, + Math.toIntExact(createResponse.getResourceId())); + assertNotNull(deleteResponse); + log.info("Deleted WC delinquency bucket with id: {}", deleteResponse.getResourceId()); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WcLoanDelinquencyRangeScheduleHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WcLoanDelinquencyRangeScheduleHelper.java new file mode 100644 index 00000000000..75d6b25f4d1 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/workingcapitalloan/WcLoanDelinquencyRangeScheduleHelper.java @@ -0,0 +1,86 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.common.workingcapitalloan; + +import com.google.gson.Gson; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +public final class WcLoanDelinquencyRangeScheduleHelper { + + private static final String DELINQUENCY_BUCKETS_URL = "/fineract-provider/api/v1/delinquency/buckets"; + private static final String WC_LOAN_DELINQUENCY_RANGE_SCHEDULE_URL = "/fineract-provider/api/v1/working-capital-loans"; + private static final Gson GSON = new JSON().getGson(); + + private WcLoanDelinquencyRangeScheduleHelper() {} + + public static String getDelinquencyRangeSchedule(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final Long loanId) { + final String url = WC_LOAN_DELINQUENCY_RANGE_SCHEDULE_URL + "/" + loanId + "/delinquency-range-schedule?" + Utils.TENANT_IDENTIFIER; + log.info("GET {}", url); + return Utils.performServerGet(requestSpec, responseSpec, url); + } + + public static PostDelinquencyBucketResponse createWcDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final List rangeIds, final int frequency, final int frequencyType, + final BigDecimal minimumPayment, final int minimumPaymentType) { + final String json = getWcBucketAsJSON(rangeIds, frequency, frequencyType, minimumPayment, minimumPaymentType); + 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 updateWcDelinquencyBucket(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer resourceId, final List rangeIds, final int frequency, + final int frequencyType, final BigDecimal minimumPayment, final int minimumPaymentType) { + final String json = getWcBucketAsJSON(rangeIds, frequency, frequencyType, minimumPayment, minimumPaymentType); + 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 String getWcBucketAsJSON(final List rangeIds, final int frequency, final int frequencyType, + final BigDecimal minimumPayment, final int minimumPaymentType) { + final HashMap map = new HashMap<>(); + map.put("name", Utils.uniqueRandomStringGenerator("WC_Delinquency_Bucket_", 4)); + map.put("ranges", rangeIds.toArray()); + map.put("bucketType", 2); + + final HashMap ruleMap = new HashMap<>(); + ruleMap.put("frequency", frequency); + ruleMap.put("frequencyType", frequencyType); + ruleMap.put("minimumPayment", minimumPayment); + ruleMap.put("minimumPaymentType", minimumPaymentType); + ruleMap.put("locale", "en"); + map.put("minimumPaymentPeriodAndRule", ruleMap); + + return GSON.toJson(map); + } +}