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.PeriodicAccrualAccountingApi;
|
||||||
import org.apache.fineract.client.services.PermissionsApi;
|
import org.apache.fineract.client.services.PermissionsApi;
|
||||||
import org.apache.fineract.client.services.PocketApi;
|
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.ProvisioningCategoryApi;
|
||||||
import org.apache.fineract.client.services.ProvisioningCriteriaApi;
|
import org.apache.fineract.client.services.ProvisioningCriteriaApi;
|
||||||
import org.apache.fineract.client.services.ProvisioningEntriesApi;
|
import org.apache.fineract.client.services.ProvisioningEntriesApi;
|
||||||
@@ -294,6 +295,7 @@ public final class FineractClient {
|
|||||||
public final UsersApi users;
|
public final UsersApi users;
|
||||||
public final WorkingDaysApi workingDays;
|
public final WorkingDaysApi workingDays;
|
||||||
public final LoanInterestPauseApi loanInterestPauseApi;
|
public final LoanInterestPauseApi loanInterestPauseApi;
|
||||||
|
public final ProgressiveLoanApi progressiveLoanApi;
|
||||||
|
|
||||||
public final ExternalAssetOwnersApi externalAssetOwners;
|
public final ExternalAssetOwnersApi externalAssetOwners;
|
||||||
public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes;
|
public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes;
|
||||||
@@ -421,6 +423,7 @@ public final class FineractClient {
|
|||||||
users = retrofit.create(UsersApi.class);
|
users = retrofit.create(UsersApi.class);
|
||||||
workingDays = retrofit.create(WorkingDaysApi.class);
|
workingDays = retrofit.create(WorkingDaysApi.class);
|
||||||
loanInterestPauseApi = retrofit.create(LoanInterestPauseApi.class);
|
loanInterestPauseApi = retrofit.create(LoanInterestPauseApi.class);
|
||||||
|
progressiveLoanApi = retrofit.create(ProgressiveLoanApi.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
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.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
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.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -39,6 +40,7 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
|||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class InterestPeriod implements Comparable<InterestPeriod> {
|
public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
private final RepaymentPeriod repaymentPeriod;
|
private final RepaymentPeriod repaymentPeriod;
|
||||||
@Setter
|
@Setter
|
||||||
@NotNull
|
@NotNull
|
||||||
@@ -58,9 +60,21 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
private Money balanceCorrectionAmount;
|
private Money balanceCorrectionAmount;
|
||||||
private Money outstandingLoanBalance;
|
private Money outstandingLoanBalance;
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
private final MathContext mc;
|
private final MathContext mc;
|
||||||
private final boolean isPaused;
|
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) {
|
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
|
||||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getChargebackPrincipal(),
|
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.BiFunction;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
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.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -50,14 +52,18 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaym
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Accessors(fluent = true)
|
@Accessors(fluent = true)
|
||||||
|
@AllArgsConstructor
|
||||||
public class ProgressiveLoanInterestScheduleModel {
|
public class ProgressiveLoanInterestScheduleModel {
|
||||||
|
|
||||||
private final List<RepaymentPeriod> repaymentPeriods;
|
private final List<RepaymentPeriod> repaymentPeriods;
|
||||||
private final TreeSet<InterestRate> interestRates;
|
private final TreeSet<InterestRate> interestRates;
|
||||||
|
@JsonExclude
|
||||||
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
|
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
|
||||||
private final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations;
|
private final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations;
|
||||||
private final Integer installmentAmountInMultiplesOf;
|
private final Integer installmentAmountInMultiplesOf;
|
||||||
|
@JsonExclude
|
||||||
private final MathContext mc;
|
private final MathContext mc;
|
||||||
|
@JsonExclude
|
||||||
private final Money zero;
|
private final Money zero;
|
||||||
private final Map<LoanInterestScheduleModelModifiers, Boolean> modifiers;
|
private final Map<LoanInterestScheduleModelModifiers, Boolean> modifiers;
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.util.Memo;
|
import org.apache.fineract.portfolio.util.Memo;
|
||||||
@@ -39,6 +40,7 @@ import org.apache.fineract.portfolio.util.Memo;
|
|||||||
@EqualsAndHashCode(exclude = { "previous" })
|
@EqualsAndHashCode(exclude = { "previous" })
|
||||||
public final class RepaymentPeriod {
|
public final class RepaymentPeriod {
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
private final RepaymentPeriod previous;
|
private final RepaymentPeriod previous;
|
||||||
@Getter
|
@Getter
|
||||||
private final LocalDate fromDate;
|
private final LocalDate fromDate;
|
||||||
@@ -59,11 +61,16 @@ public final class RepaymentPeriod {
|
|||||||
@Getter
|
@Getter
|
||||||
private Money paidInterest;
|
private Money paidInterest;
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
private final MathContext mc;
|
private final MathContext mc;
|
||||||
|
|
||||||
|
@JsonExclude
|
||||||
private Memo<BigDecimal> rateFactorPlus1Calculation;
|
private Memo<BigDecimal> rateFactorPlus1Calculation;
|
||||||
|
@JsonExclude
|
||||||
private Memo<Money> calculatedDueInterestCalculation;
|
private Memo<Money> calculatedDueInterestCalculation;
|
||||||
|
@JsonExclude
|
||||||
private Memo<Money> dueInterestCalculation;
|
private Memo<Money> dueInterestCalculation;
|
||||||
|
@JsonExclude
|
||||||
private Memo<Money> outstandingBalanceCalculation;
|
private Memo<Money> outstandingBalanceCalculation;
|
||||||
|
|
||||||
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
|
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
|
||||||
@@ -79,6 +86,10 @@ public final class RepaymentPeriod {
|
|||||||
this.mc = mc;
|
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) {
|
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) {
|
||||||
final Money zero = emi.zero();
|
final Money zero = emi.zero();
|
||||||
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, 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);
|
repaymentPeriod.paidInterest, mc);
|
||||||
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
||||||
newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod));
|
newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
|
||||||
}
|
}
|
||||||
return newRepaymentPeriod;
|
return newRepaymentPeriod;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -23,4 +23,5 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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">
|
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 -->
|
<!-- 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>
|
</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.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
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.InterestPeriod;
|
||||||
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
|
||||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||||
@@ -70,6 +71,7 @@ class ProgressiveEMICalculatorTest {
|
|||||||
|
|
||||||
private static List<LoanRepaymentScheduleInstallment> periods;
|
private static List<LoanRepaymentScheduleInstallment> periods;
|
||||||
private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
|
private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
|
||||||
|
private final ProgressiveLoanInterestScheduleModelParserServiceGsonImpl interestScheduleModelService = new ProgressiveLoanInterestScheduleModelParserServiceGsonImpl();
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void init() {
|
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);
|
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) {
|
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
|
||||||
final Money zeroAmount = Money.zero(currency);
|
final Money zeroAmount = Money.zero(currency);
|
||||||
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
|
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
|
||||||
@@ -3202,4 +3277,24 @@ class ProgressiveEMICalculatorTest {
|
|||||||
private static Money toMoney(final double value) {
|
private static Money toMoney(final double value) {
|
||||||
return Money.of(currency, BigDecimal.valueOf(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/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"/>
|
<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"/>
|
<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 -->
|
<!-- Scripts to run after the modules were initialized -->
|
||||||
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
|
<include file="tenant/final-changelog-tenant.xml" relativeToChangelogFile="true" context="tenant_db AND !initial_switch"/>
|
||||||
</databaseChangeLog>
|
</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