FINERACT-2220: Make ProgressiveLoanInterestScheduleModel serializable and deserializable
This commit is contained in:
@@ -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() {
|
||||
|
||||
+32
@@ -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 {}
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+43
@@ -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<LocalDate> {
|
||||
|
||||
@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);
|
||||
|
||||
}
|
||||
}
|
||||
+43
@@ -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<Money> {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -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<Money> {
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Money money, Type type, JsonSerializationContext jsonSerializationContext) {
|
||||
return new JsonPrimitive(money.getAmount());
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -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<Long> {
|
||||
|
||||
@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;
|
||||
|
||||
}
|
||||
+30
@@ -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<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
|
||||
|
||||
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
||||
}
|
||||
+33
@@ -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<ProgressiveLoanInterestScheduleModel> readProgressiveLoanInterestScheduleModel(Long loanId,
|
||||
LoanProductMinimumRepaymentScheduleRelatedDetail detail, Integer installmentAmountInMultipliesOf);
|
||||
|
||||
}
|
||||
+68
@@ -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<ProgressiveLoanInterestScheduleModel> 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)); //
|
||||
}
|
||||
}
|
||||
+66
@@ -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);
|
||||
}
|
||||
}
|
||||
+129
@@ -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<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing();
|
||||
LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate();
|
||||
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
|
||||
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
ProgressiveLoanInterestScheduleModel model = changedTransactionDetailProgressiveLoanInterestScheduleModelPair.getRight();
|
||||
final List<Long> 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);
|
||||
}
|
||||
}
|
||||
+126
@@ -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<RepaymentPeriod> repaymentPeriods;
|
||||
@Schema(example = "[]")
|
||||
public TreeSet<InterestRate> interestRates;
|
||||
@Schema(example = "{}")
|
||||
public Map<Integer, List<LoanTermVariationsData>> loanTermVariations;
|
||||
@Schema(example = "1")
|
||||
public Integer installmentAmountInMultiplesOf;
|
||||
@Schema(example = "{}")
|
||||
public Map<Integer, Boolean> 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<InterestPeriod> 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;
|
||||
}
|
||||
}
|
||||
+36
@@ -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();
|
||||
}
|
||||
}
|
||||
+37
@@ -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);
|
||||
}
|
||||
+90
@@ -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<InterestPeriod>) ctx::createInterestPeriodInstance)
|
||||
.registerTypeAdapter(ProgressiveLoanInterestScheduleModel.class,
|
||||
(InstanceCreator<ProgressiveLoanInterestScheduleModel>) ctx::createProgressiveLoanInterestScheduleModelInstance)
|
||||
.registerTypeAdapter(RepaymentPeriod.class, (InstanceCreator<RepaymentPeriod>) 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -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<InterestPeriod> {
|
||||
|
||||
@JsonExclude
|
||||
private final RepaymentPeriod repaymentPeriod;
|
||||
@Setter
|
||||
@NotNull
|
||||
@@ -58,9 +60,21 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
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(),
|
||||
|
||||
+6
@@ -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<RepaymentPeriod> repaymentPeriods;
|
||||
private final TreeSet<InterestRate> interestRates;
|
||||
@JsonExclude
|
||||
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
|
||||
private final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations;
|
||||
private final Integer installmentAmountInMultiplesOf;
|
||||
@JsonExclude
|
||||
private final MathContext mc;
|
||||
@JsonExclude
|
||||
private final Money zero;
|
||||
private final Map<LoanInterestScheduleModelModifiers, Boolean> modifiers;
|
||||
|
||||
|
||||
+12
-1
@@ -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<BigDecimal> rateFactorPlus1Calculation;
|
||||
@JsonExclude
|
||||
private Memo<Money> calculatedDueInterestCalculation;
|
||||
@JsonExclude
|
||||
private Memo<Money> dueInterestCalculation;
|
||||
@JsonExclude
|
||||
private Memo<Money> outstandingBalanceCalculation;
|
||||
|
||||
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> 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;
|
||||
}
|
||||
|
||||
+1
@@ -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">
|
||||
<!-- Sequence is starting from 5000 to make it easier to move existing liquibase changesets here -->
|
||||
<include file="parts/5001_create_progressive_loan_model.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
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.31.xsd"
|
||||
objectQuotingStrategy="QUOTE_ONLY_RESERVED_WORDS">
|
||||
<changeSet id="1744116182-1" author="fineract">
|
||||
<createTable tableName="m_loan_progressive_model">
|
||||
<column autoIncrement="true" name="id" type="BIGINT">
|
||||
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_progressive_model"/>
|
||||
</column>
|
||||
<column name="version" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="loan_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="json_model" type="TEXT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="business_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet id="1744116182-2" author="fineract">
|
||||
<addUniqueConstraint columnNames="loan_id" constraintName="uc_m_loan_progressive_model_loan"
|
||||
tableName="m_loan_progressive_model"/>
|
||||
</changeSet>
|
||||
<changeSet id="1744116182-3" author="fineract">
|
||||
<addForeignKeyConstraint baseColumnNames="loan_id" baseTableName="m_loan_progressive_model"
|
||||
constraintName="FK_M_LOAN_PROGRESSIVE_MODEL_ON_LOAN" referencedColumnNames="id"
|
||||
referencedTableName="m_loan"/>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="1744116182-4-my" context="mysql">
|
||||
<addColumn tableName="m_loan_progressive_model">
|
||||
<column name="last_modified_on_utc" type="DATETIME(6)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="1744116182
|
||||
-4-pg" context="postgresql">
|
||||
<addColumn tableName="m_loan_progressive_model">
|
||||
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+95
@@ -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<LoanRepaymentScheduleInstallment> 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<LoanScheduleModelRepaymentPeriod> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<include file="db/changelog/tenant/module/loan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<include file="db/changelog/tenant/module/investor/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<includeAll path="db/custom-changelog" errorIfMissingOrEmpty="false" context="tenant_db AND !initial_switch AND custom_changelog"/>
|
||||
<include file="/db/changelog/tenant/module/progressiveloan/module-changelog-master.xml" context="tenant_db AND !initial_switch"/>
|
||||
<!-- Scripts to run after the modules were initialized -->
|
||||
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+54
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user