diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index 2237d8ec8..624c73b44 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -107,6 +107,7 @@ import org.apache.fineract.client.services.PaymentTypeApi; import org.apache.fineract.client.services.PeriodicAccrualAccountingApi; import org.apache.fineract.client.services.PermissionsApi; import org.apache.fineract.client.services.PocketApi; +import org.apache.fineract.client.services.ProgressiveLoanApi; import org.apache.fineract.client.services.ProvisioningCategoryApi; import org.apache.fineract.client.services.ProvisioningCriteriaApi; import org.apache.fineract.client.services.ProvisioningEntriesApi; @@ -294,6 +295,7 @@ public final class FineractClient { public final UsersApi users; public final WorkingDaysApi workingDays; public final LoanInterestPauseApi loanInterestPauseApi; + public final ProgressiveLoanApi progressiveLoanApi; public final ExternalAssetOwnersApi externalAssetOwners; public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes; @@ -421,6 +423,7 @@ public final class FineractClient { users = retrofit.create(UsersApi.class); workingDays = retrofit.create(WorkingDaysApi.class); loanInterestPauseApi = retrofit.create(LoanInterestPauseApi.class); + progressiveLoanApi = retrofit.create(ProgressiveLoanApi.class); } public static Builder builder() { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExclude.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExclude.java new file mode 100644 index 000000000..2a0b8c1bc --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExclude.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.serialization.gson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Exclude field from json serialization when using Gson with custom + * {@link JsonExcludeAnnotationBasedExclusionStrategy}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface JsonExclude {} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExcludeAnnotationBasedExclusionStrategy.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExcludeAnnotationBasedExclusionStrategy.java new file mode 100644 index 000000000..c1c1981eb --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/JsonExcludeAnnotationBasedExclusionStrategy.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.serialization.gson; + +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; + +/** + * Exclusion strategy to handle {@link JsonExclude} annotations to exclude fields from serialization and + * deserialization. + */ +public class JsonExcludeAnnotationBasedExclusionStrategy implements ExclusionStrategy { + + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + JsonExclude annotation = fieldAttributes.getAnnotation(JsonExclude.class); + return annotation != null; + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/LocalDateAdapter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/LocalDateAdapter.java new file mode 100644 index 000000000..5a06665ba --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/gson/LocalDateAdapter.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.core.serialization.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class LocalDateAdapter extends TypeAdapter { + + @Override + public void write(final JsonWriter jsonWriter, final LocalDate localDate) throws IOException { + + jsonWriter.value(localDate.format(DateTimeFormatter.ISO_LOCAL_DATE)); + + } + + @Override + public LocalDate read(final JsonReader jsonReader) throws IOException { + + return LocalDate.parse(jsonReader.nextString(), DateTimeFormatter.ISO_LOCAL_DATE); + + } +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java new file mode 100644 index 000000000..5214d239a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneyDeserializer.java @@ -0,0 +1,43 @@ +/** + * 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.organisation.monetary.serialization.gson; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import java.math.MathContext; +import lombok.AllArgsConstructor; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; + +@AllArgsConstructor +public class MoneyDeserializer implements JsonDeserializer { + + private final MathContext mc; + private final MonetaryCurrency currency; + + @Override + public Money deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { + return Money.of(currency, jsonElement.getAsBigDecimal(), mc); + } + +} diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java new file mode 100644 index 000000000..b5375857a --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/serialization/gson/MoneySerializer.java @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.organisation.monetary.serialization.gson; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import org.apache.fineract.organisation.monetary.domain.Money; + +public class MoneySerializer implements JsonSerializer { + + @Override + public JsonElement serialize(Money money, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(money.getAmount()); + } + +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java new file mode 100644 index 000000000..c8aec9b2f --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/ProgressiveLoanModel.java @@ -0,0 +1,55 @@ +/** + * 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.loanaccount.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "m_loan_progressive_model") +@Getter +@Setter +public class ProgressiveLoanModel extends AbstractPersistableCustom { + + @Version + int version; + + @OneToOne + @JoinColumn(name = "loan_id", nullable = false) + private Loan loan; + + @Column(name = "json_model", columnDefinition = "text", nullable = false) + private String jsonModel; + + @Column(name = "business_date", nullable = false) + private LocalDate businessDate; + + @Column(name = "last_modified_on_utc", nullable = false) + private OffsetDateTime lastModifiedDate; + +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java new file mode 100644 index 000000000..b49beb9d2 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/ProgressiveLoanModelRepository.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.repository; + +import java.util.Optional; +import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ProgressiveLoanModelRepository + extends JpaSpecificationExecutor, JpaRepository { + + Optional findOneByLoanId(Long loanId); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java new file mode 100644 index 000000000..76dfd8a0c --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapper.java @@ -0,0 +1,33 @@ +/** + * 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.loanaccount.service; + +import java.util.Optional; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; + +public interface InterestScheduleModelRepositoryWrapper { + + String writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model); + + Optional readProgressiveLoanInterestScheduleModel(Long loanId, + LoanProductMinimumRepaymentScheduleRelatedDetail detail, Integer installmentAmountInMultipliesOf); + +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java new file mode 100644 index 000000000..7be230c5b --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java @@ -0,0 +1,68 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import jakarta.transaction.Transactional; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel; +import org.apache.fineract.portfolio.loanaccount.repository.ProgressiveLoanModelRepository; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class InterestScheduleModelRepositoryWrapperImpl implements InterestScheduleModelRepositoryWrapper { + + private final ProgressiveLoanModelRepository loanModelRepository; + private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; + + @Transactional + @Override + public String writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model) { + if (model == null) { + return null; + } + String jsonModel = progressiveLoanInterestScheduleModelParserService.toJson(model); + ProgressiveLoanModel progressiveLoanModel = loanModelRepository.findOneByLoanId(loan.getId()).orElseGet(() -> { + ProgressiveLoanModel plm = new ProgressiveLoanModel(); + plm.setLoan(loan); + return plm; + }); + progressiveLoanModel.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); + progressiveLoanModel.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); + progressiveLoanModel.setJsonModel(jsonModel); + loanModelRepository.save(progressiveLoanModel); + return jsonModel; + } + + @Override + public Optional readProgressiveLoanInterestScheduleModel(final Long loanId, + final LoanProductMinimumRepaymentScheduleRelatedDetail detail, final Integer installmentAmountInMultipliesOf) { + return loanModelRepository.findOneByLoanId(loanId) // + .map(ProgressiveLoanModel::getJsonModel) // + .map(jsonModel -> progressiveLoanInterestScheduleModelParserService.fromJson(jsonModel, detail, + MoneyHelper.getMathContext(), installmentAmountInMultipliesOf)); // + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java new file mode 100644 index 000000000..39de64dfe --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.lang.reflect.Type; +import java.math.MathContext; +import java.util.ArrayList; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; + +@Data +@RequiredArgsConstructor +public class InterestScheduleModelServiceGsonContext { + + private final MonetaryCurrency currency; + private final MathContext mc; + private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; + private RepaymentPeriod prev = null; + private final Integer installmentAmountInMultipliesOf; + + public RepaymentPeriod createRepaymentPeriodInstance(Type type) { + if (type == RepaymentPeriod.class) { + + setPrev(RepaymentPeriod.empty(getPrev(), getMc())); + return getPrev(); + } + throw new IllegalArgumentException("Unsupported RepaymentPeriod type: " + type); + } + + public InterestPeriod createInterestPeriodInstance(Type type) { + if (type == InterestPeriod.class) { + return InterestPeriod.empty(getPrev(), getMc()); + } + throw new IllegalArgumentException("Unsupported InterestPeriod type: " + type); + } + + public ProgressiveLoanInterestScheduleModel createProgressiveLoanInterestScheduleModelInstance(Type type) { + if (type == ProgressiveLoanInterestScheduleModel.class) { + setPrev(null); + return new ProgressiveLoanInterestScheduleModel(new ArrayList<>(), getLoanProductRelatedDetail(), new ArrayList<>(), + installmentAmountInMultipliesOf, getMc()); + } + throw new IllegalArgumentException("Unsupported ProgressiveLoanInterestScheduleModel type: " + type); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java new file mode 100644 index 000000000..8e7febf15 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResource.java @@ -0,0 +1,129 @@ +/** + * 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.loanaccount.service; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.infrastructure.core.boot.FineractProfiles; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Profile(FineractProfiles.TEST) +@Component +@Path("v1/internal/loan/progressive") +@Tag(name = "Progressive Loan", description = "internal loan testing API. This API should be disabled in production!!!") +public class InternalProgressiveLoanApiResource implements InitializingBean { + + private final LoanRepositoryWrapper loanRepository; + private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor; + private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService; + private final InterestScheduleModelRepositoryWrapper writePlatformService; + + @Override + @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") + public void afterPropertiesSet() throws Exception { + log.warn("------------------------------------------------------------"); + log.warn(" "); + log.warn("DO NOT USE THIS IN PRODUCTION!"); + log.warn("Internal client services mode is enabled"); + log.warn("DO NOT USE THIS IN PRODUCTION!"); + log.warn(" "); + log.warn("------------------------------------------------------------"); + } + + @GET + @Produces({ MediaType.APPLICATION_JSON }) + @Path("{loanId}/model") + @Operation(summary = "Fetch ProgressiveLoanInterestScheduleModel", description = "DO NOT USE THIS IN PRODUCTION!") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = InternalProgressiveLoanApiResourceSwagger.ProgressiveLoanInterestScheduleModel.class))) }) + public String fetchModel(@PathParam("loanId") @Parameter(description = "loanId") long loanId) { + Loan loan = loanRepository.findOneWithNotFoundDetection(loanId); + if (!loan.isProgressiveSchedule()) { + throw new IllegalArgumentException("The loan is not progressive."); + } + + return writePlatformService + .readProgressiveLoanInterestScheduleModel(loanId, loan.getLoanRepaymentScheduleDetail(), + loan.getLoanProduct().getInstallmentAmountInMultiplesOf()) + .map(progressiveLoanInterestScheduleModelParserService::toJson).orElse(null); + } + + private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(Loan loan) { + List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing(); + LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate(); + Pair changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight(); + final List replayedTransactions = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getLeft() + .getTransactionChanges().stream().filter(change -> change.getOldTransaction() != null) + .map(change -> change.getNewTransaction().getId()).filter(Objects::nonNull).toList(); + + if (!replayedTransactions.isEmpty()) { + log.warn("Reprocessed transactions show differences: There are unsaved changes of the following transactions: {}", + replayedTransactions); + } + return model; + } + + @POST + @Path("{loanId}/model") + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Update and Save ProgressiveLoanInterestScheduleModel", description = "DO NOT USE THIS IN PRODUCTION!") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = InternalProgressiveLoanApiResourceSwagger.ProgressiveLoanInterestScheduleModel.class))) }) + public String updateModel(@PathParam("loanId") @Parameter(description = "loanId") long loanId) { + Loan loan = loanRepository.findOneWithNotFoundDetection(loanId); + if (!loan.isProgressiveSchedule()) { + throw new IllegalArgumentException("The loan is not progressive."); + } + ProgressiveLoanInterestScheduleModel model = reprocessTransactionsAndGetModel(loan); + + return writePlatformService.writeInterestScheduleModel(loan, model); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResourceSwagger.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResourceSwagger.java new file mode 100644 index 000000000..9c249db68 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InternalProgressiveLoanApiResourceSwagger.java @@ -0,0 +1,126 @@ +/** + * 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.loanaccount.service; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +final class InternalProgressiveLoanApiResourceSwagger { + + @Schema(description = "ProgressiveLoanInterestScheduleModel") + static final class ProgressiveLoanInterestScheduleModel { + + private ProgressiveLoanInterestScheduleModel() {} + + @Schema(example = "[]") + public List repaymentPeriods; + @Schema(example = "[]") + public TreeSet interestRates; + @Schema(example = "{}") + public Map> loanTermVariations; + @Schema(example = "1") + public Integer installmentAmountInMultiplesOf; + @Schema(example = "{}") + public Map modifiers; + } + + @Schema(description = "Interest Period") + static final class InterestPeriod { + + private InterestPeriod() {} + + @Schema(example = "01/01/2024") + public String fromDate; + @Schema(example = "01/09/2024") + public String dueDate; + @Schema(example = "0.9636548454") + public BigDecimal rateFactor; + @Schema(example = "0.9456878987") + public BigDecimal rateFactorTillPeriodDueDate; + @Schema(example = "0.0") + public BigDecimal chargebackPrincipal; + @Schema(example = "0.0") + public BigDecimal chargebackInterest; + @Schema(example = "1000.0") + public BigDecimal disbursementAmount; + @Schema(example = "3.38") + public BigDecimal balanceCorrectionAmount; + @Schema(example = "865.71") + public BigDecimal outstandingLoanBalance; + @Schema(example = "false") + public boolean isPaused; + } + + @Schema(description = "Repayment Period") + static final class RepaymentPeriod { + + private RepaymentPeriod() {} + + @Schema(example = "01/01/2024") + public String fromDate; + @Schema(example = "01/02/2024") + public String dueDate; + @Schema(example = "[]") + public List interestPeriods; + @Schema(example = "127.04") + public BigDecimal emi; + @Schema(example = "127.04") + public BigDecimal originalEmi; + @Schema(example = "104.04") + public BigDecimal paidPrincipal; + @Schema(example = "23.00") + public BigDecimal paidInterest; + } + + @Schema(description = "Interest Rate") + static final class InterestRate { + + private InterestRate() {} + + @Schema(example = "21/12/2024") + public String effectiveFrom; + @Schema(example = "7.963") + public BigDecimal interestRate; + } + + @Schema(description = "Loan Term Variations Data") + static final class LoanTermVariationsData { + + private LoanTermVariationsData() {} + + @Schema(example = "12345") + public Long id; + @Schema(example = "null") + public EnumOptionData termType; + @Schema(example = "21/12/2024") + public String termVariationApplicableFrom; + @Schema(example = "1.20") + public BigDecimal decimalValue; + @Schema(example = "21/12/2024") + public String dateValue; + @Schema(example = "true") + public boolean isSpecificToInstallment; + @Schema(example = "true") + public Boolean isProcessed; + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanConfiguration.java new file mode 100644 index 000000000..08b25899e --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanConfiguration.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.loanaccount.service; + +import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) +public class ProgressiveLoanConfiguration { + + @Bean + @ConditionalOnMissingBean + public ProgressiveLoanInterestScheduleModelParserService interestScheduleModelService() { + return new ProgressiveLoanInterestScheduleModelParserServiceGsonImpl(); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserService.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserService.java new file mode 100644 index 000000000..e148ae23a --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserService.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.MathContext; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; + +public interface ProgressiveLoanInterestScheduleModelParserService { + + /** + * Json stringify a ProgressiveLoanInterestScheduleModel model. + */ + String toJson(ProgressiveLoanInterestScheduleModel model); + + /** + * Restore a ProgressiveLoanInterestScheduleModel from a JSON string. + */ + ProgressiveLoanInterestScheduleModel fromJson(String s, LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, + MathContext mc, Integer installmentAmountInMultipliesOf); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java new file mode 100644 index 000000000..067d66b01 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestScheduleModelParserServiceGsonImpl.java @@ -0,0 +1,90 @@ +/** + * 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.loanaccount.service; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.ToNumberPolicy; +import jakarta.validation.constraints.NotNull; +import java.math.MathContext; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExcludeAnnotationBasedExclusionStrategy; +import org.apache.fineract.infrastructure.core.serialization.gson.LocalDateAdapter; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.serialization.gson.MoneyDeserializer; +import org.apache.fineract.organisation.monetary.serialization.gson.MoneySerializer; +import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; +import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail; + +@Slf4j +@RequiredArgsConstructor +public class ProgressiveLoanInterestScheduleModelParserServiceGsonImpl implements ProgressiveLoanInterestScheduleModelParserService { + + private final Gson gsonSerializer = createSerializer(); + + private Gson createSerializer() { + return new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) + .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL).registerTypeAdapter(Money.class, new MoneySerializer()) + .addDeserializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) + .addSerializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()).create(); + } + + private Gson createDeserializer(LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, MathContext mc, + Integer installmentAmountInMultipliesOf) { + InterestScheduleModelServiceGsonContext ctx = new InterestScheduleModelServiceGsonContext( + new MonetaryCurrency(loanProductRelatedDetail.getCurrencyData()), mc, loanProductRelatedDetail, + installmentAmountInMultipliesOf); + return new GsonBuilder().registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) + .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) + .registerTypeAdapter(Money.class, new MoneyDeserializer(ctx.getMc(), ctx.getCurrency())) + .registerTypeAdapter(InterestPeriod.class, (InstanceCreator) ctx::createInterestPeriodInstance) + .registerTypeAdapter(ProgressiveLoanInterestScheduleModel.class, + (InstanceCreator) ctx::createProgressiveLoanInterestScheduleModelInstance) + .registerTypeAdapter(RepaymentPeriod.class, (InstanceCreator) ctx::createRepaymentPeriodInstance) + .addDeserializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) + .addSerializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()).create(); + } + + @Override + public String toJson(@NotNull ProgressiveLoanInterestScheduleModel model) { + return gsonSerializer.toJson(model); + } + + @Override + public ProgressiveLoanInterestScheduleModel fromJson(String s, + @NotNull LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, @NotNull MathContext mc, + Integer installmentAmountInMultipliesOf) { + if (s == null) { + return null; + } + try { + Gson gson = createDeserializer(loanProductRelatedDetail, mc, installmentAmountInMultipliesOf); + return gson.fromJson(s, ProgressiveLoanInterestScheduleModel.class); + } catch (Exception e) { + log.warn("Failed to parse ProgressiveLoanInterestScheduleModel json. Falling back to default value.", e); + return null; + } + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index 0f461fe20..71431d457 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; @@ -39,6 +40,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; @AllArgsConstructor(access = AccessLevel.PRIVATE) public class InterestPeriod implements Comparable { + @JsonExclude private final RepaymentPeriod repaymentPeriod; @Setter @NotNull @@ -58,9 +60,21 @@ public class InterestPeriod implements Comparable { private Money balanceCorrectionAmount; private Money outstandingLoanBalance; + @JsonExclude private final MathContext mc; private final boolean isPaused; + public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod, MathContext mc) { + return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), + interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(), + interestPeriod.getChargebackInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), + interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused()); + } + + public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) { + return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, mc, false); + } + public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) { return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(), diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index 3a74d32f2..58debfb91 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -39,8 +39,10 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.stream.Collectors; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.experimental.Accessors; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; @@ -50,14 +52,18 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaym @Data @Accessors(fluent = true) +@AllArgsConstructor public class ProgressiveLoanInterestScheduleModel { private final List repaymentPeriods; private final TreeSet interestRates; + @JsonExclude private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail; private final Map> loanTermVariations; private final Integer installmentAmountInMultiplesOf; + @JsonExclude private final MathContext mc; + @JsonExclude private final Money zero; private final Map modifiers; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 5390c1705..e4dd3025d 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.util.Memo; @@ -39,6 +40,7 @@ import org.apache.fineract.portfolio.util.Memo; @EqualsAndHashCode(exclude = { "previous" }) public final class RepaymentPeriod { + @JsonExclude private final RepaymentPeriod previous; @Getter private final LocalDate fromDate; @@ -59,11 +61,16 @@ public final class RepaymentPeriod { @Getter private Money paidInterest; + @JsonExclude private final MathContext mc; + @JsonExclude private Memo rateFactorPlus1Calculation; + @JsonExclude private Memo calculatedDueInterestCalculation; + @JsonExclude private Memo dueInterestCalculation; + @JsonExclude private Memo outstandingBalanceCalculation; private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List interestPeriods, @@ -79,6 +86,10 @@ public final class RepaymentPeriod { this.mc = mc; } + public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc) { + return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, mc); + } + public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) { final Money zero = emi.zero(); final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero, @@ -94,7 +105,7 @@ public final class RepaymentPeriod { repaymentPeriod.paidInterest, mc); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { - newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod)); + newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc)); } return newRepaymentPeriod; } diff --git a/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml similarity index 92% rename from fineract-progressive-loan/src/main/resources/jpa/progressiveloan/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml rename to fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml index baf21a9ba..60b1a5e14 100644 --- a/fineract-progressive-loan/src/main/resources/jpa/progressiveloan/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml +++ b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml @@ -23,4 +23,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + diff --git a/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5001_create_progressive_loan_model.xml b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5001_create_progressive_loan_model.xml new file mode 100644 index 000000000..de328cf03 --- /dev/null +++ b/fineract-progressive-loan/src/main/resources/db/changelog/tenant/module/progressiveloan/parts/5001_create_progressive_loan_model.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 33c1b341e..5bb211460 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl; import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; @@ -70,6 +71,7 @@ class ProgressiveEMICalculatorTest { private static List periods; private final BigDecimal interestRate = BigDecimal.valueOf(0.094822); + private final ProgressiveLoanInterestScheduleModelParserServiceGsonImpl interestScheduleModelService = new ProgressiveLoanInterestScheduleModelParserServiceGsonImpl(); @BeforeAll public static void init() { @@ -3114,6 +3116,79 @@ class ProgressiveEMICalculatorTest { checkPeriod(interestSchedule, 5, 0, 17.05, 0.005833333333, 0.0988749999945, 0.10, 16.95, 0.0); } + @Test + public void test_interest_schedule_model_service_serialization() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), + repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), + repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), + repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)), + repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)), + repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestScheduleExpected = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + ProgressiveLoanInterestScheduleModel interestScheduleActual = copyJson(interestScheduleExpected); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + emiCalculator.addDisbursement(interestScheduleExpected, LocalDate.of(2024, 1, 1), disbursedAmount); + emiCalculator.addDisbursement(interestScheduleActual, LocalDate.of(2024, 1, 1), disbursedAmount); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + interestScheduleActual = copyJson(interestScheduleExpected); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + + // repay 1st period + LocalDate txnDate = LocalDate.of(2024, 2, 1); + emiCalculator.payPrincipal(interestScheduleExpected, txnDate, txnDate, toMoney(16.43)); + emiCalculator.payInterest(interestScheduleExpected, txnDate, txnDate, toMoney(0.58)); + emiCalculator.payPrincipal(interestScheduleActual, txnDate, txnDate, toMoney(16.43)); + emiCalculator.payInterest(interestScheduleActual, txnDate, txnDate, toMoney(0.58)); + + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + interestScheduleActual = copyJson(interestScheduleExpected); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + + // repay 2nd period + txnDate = LocalDate.of(2024, 3, 1); + emiCalculator.payPrincipal(interestScheduleExpected, txnDate, txnDate, toMoney(16.52)); + emiCalculator.payInterest(interestScheduleExpected, txnDate, txnDate, toMoney(0.49)); + emiCalculator.payPrincipal(interestScheduleActual, txnDate, txnDate, toMoney(16.52)); + emiCalculator.payInterest(interestScheduleActual, txnDate, txnDate, toMoney(0.49)); + + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + interestScheduleActual = copyJson(interestScheduleExpected); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + + // chargeback + txnDate = LocalDate.of(2024, 3, 15); + emiCalculator.chargebackPrincipal(interestScheduleExpected, txnDate, toMoney(16.52)); + emiCalculator.chargebackInterest(interestScheduleExpected, txnDate, toMoney(0.49)); + emiCalculator.chargebackPrincipal(interestScheduleActual, txnDate, toMoney(16.52)); + emiCalculator.chargebackInterest(interestScheduleActual, txnDate, toMoney(0.49)); + + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + interestScheduleActual = copyJson(interestScheduleExpected); + verifyAllPeriods(interestScheduleExpected, interestScheduleActual); + + PeriodDueDetails dueAmountsExpected = emiCalculator.getDueAmounts(interestScheduleExpected, LocalDate.of(2024, 4, 1), txnDate); + PeriodDueDetails dueAmountsActual = emiCalculator.getDueAmounts(interestScheduleActual, LocalDate.of(2024, 4, 1), txnDate); + + Assertions.assertEquals(toDouble(dueAmountsExpected.getDuePrincipal()), toDouble(dueAmountsActual.getDuePrincipal())); + Assertions.assertEquals(toDouble(dueAmountsExpected.getDueInterest()), toDouble(dueAmountsActual.getDueInterest())); + + } + private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) { final Money zeroAmount = Money.zero(currency); return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount, @@ -3202,4 +3277,24 @@ class ProgressiveEMICalculatorTest { private static Money toMoney(final double value) { return Money.of(currency, BigDecimal.valueOf(value)); } + + private static void verifyAllPeriods(final ProgressiveLoanInterestScheduleModel expectedModel, + final ProgressiveLoanInterestScheduleModel actualModel) { + for (int repInd = 0; repInd < expectedModel.repaymentPeriods().size(); repInd++) { + RepaymentPeriod repaymentPeriod = expectedModel.repaymentPeriods().get(repInd); + for (int interestPeriodIndex = 0; interestPeriodIndex < repaymentPeriod.getInterestPeriods().size(); interestPeriodIndex++) { + final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestPeriodIndex); + checkPeriod(actualModel, repInd, interestPeriodIndex, toDouble(repaymentPeriod.getEmi()), + toDouble(applyMathContext(interestPeriod.getRateFactor())), toDouble(interestPeriod.getCalculatedDueInterest()), + toDouble(repaymentPeriod.getDueInterest()), toDouble(repaymentPeriod.getDuePrincipal()), + toDouble(repaymentPeriod.getOutstandingLoanBalance())); + } + } + } + + private ProgressiveLoanInterestScheduleModel copyJson(ProgressiveLoanInterestScheduleModel toCopy) { + String json = interestScheduleModelService.toJson(toCopy); + return interestScheduleModelService.fromJson(json, toCopy.loanProductRelatedDetail(), toCopy.mc(), + toCopy.installmentAmountInMultiplesOf()); + } } diff --git a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml index fd9f869f1..f44f38478 100644 --- a/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml +++ b/fineract-provider/src/main/resources/db/changelog/db.changelog-master.xml @@ -35,6 +35,7 @@ + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java new file mode 100644 index 000000000..3e6aa001c --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanModelIntegrationTest.java @@ -0,0 +1,54 @@ +/** + * 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 org.apache.fineract.client.models.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.Test; + +public class ProgressiveLoanModelIntegrationTest extends BaseLoanIntegrationTest { + + private final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + private final Long loanProductId = loanProductHelper.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true)) + .getResourceId(); + + @Test + public void testModelReturnsNullAndThenSaveThenNotNull() { + runAt("1 January 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "1 January 2024", 1000.0, 96.32, 6, null); + loanTransactionHelper.disburseLoan(loanId, "1 January 2024", 1000.0); + + // Model not saved, fetching it. It should return null + ProgressiveLoanInterestScheduleModel progressiveLoanInterestScheduleModelResponse1 = ok( + fineractClient().progressiveLoanApi.fetchModel(loanId)); + + assertThat(progressiveLoanInterestScheduleModelResponse1).isNull(); + + // Forcing Model recalculation and save to database. It should return the actual model. + ProgressiveLoanInterestScheduleModel ok = ok(fineractClient().progressiveLoanApi.updateModel(loanId)); + assertThat(ok).isNotNull(); + + // Model saved in previous step. API should return the previous model. + ProgressiveLoanInterestScheduleModel progressiveLoanInterestScheduleModelResponse2 = ok( + fineractClient().progressiveLoanApi.fetchModel(loanId)); + assertThat(progressiveLoanInterestScheduleModelResponse2).isNotNull(); + }); + } + +}