FINERACT-2178: Ability to retrieve a loan's balance at a certain point in time in the past
This commit is contained in:
committed by
Arnold Gálovics
parent
986dceea1c
commit
0f1be3c913
@@ -93,6 +93,7 @@ import org.apache.fineract.client.services.LoanProductsApi;
|
|||||||
import org.apache.fineract.client.services.LoanReschedulingApi;
|
import org.apache.fineract.client.services.LoanReschedulingApi;
|
||||||
import org.apache.fineract.client.services.LoanTransactionsApi;
|
import org.apache.fineract.client.services.LoanTransactionsApi;
|
||||||
import org.apache.fineract.client.services.LoansApi;
|
import org.apache.fineract.client.services.LoansApi;
|
||||||
|
import org.apache.fineract.client.services.LoansPointInTimeApi;
|
||||||
import org.apache.fineract.client.services.MakerCheckerOr4EyeFunctionalityApi;
|
import org.apache.fineract.client.services.MakerCheckerOr4EyeFunctionalityApi;
|
||||||
import org.apache.fineract.client.services.MappingFinancialActivitiesToAccountsApi;
|
import org.apache.fineract.client.services.MappingFinancialActivitiesToAccountsApi;
|
||||||
import org.apache.fineract.client.services.MixMappingApi;
|
import org.apache.fineract.client.services.MixMappingApi;
|
||||||
@@ -229,6 +230,7 @@ public final class FineractClient {
|
|||||||
public final LoanCollateralApi loanCollaterals;
|
public final LoanCollateralApi loanCollaterals;
|
||||||
public final LoanProductsApi loanProducts;
|
public final LoanProductsApi loanProducts;
|
||||||
public final LoanReschedulingApi loanSchedules;
|
public final LoanReschedulingApi loanSchedules;
|
||||||
|
public final LoansPointInTimeApi loansPointInTimeApi;
|
||||||
public final LoansApi loans;
|
public final LoansApi loans;
|
||||||
public final LoanDisbursementDetailsApi loanDisbursementDetails;
|
public final LoanDisbursementDetailsApi loanDisbursementDetails;
|
||||||
public final LoanTransactionsApi loanTransactions;
|
public final LoanTransactionsApi loanTransactions;
|
||||||
@@ -355,6 +357,7 @@ public final class FineractClient {
|
|||||||
loanCollaterals = retrofit.create(LoanCollateralApi.class);
|
loanCollaterals = retrofit.create(LoanCollateralApi.class);
|
||||||
loanProducts = retrofit.create(LoanProductsApi.class);
|
loanProducts = retrofit.create(LoanProductsApi.class);
|
||||||
loanSchedules = retrofit.create(LoanReschedulingApi.class);
|
loanSchedules = retrofit.create(LoanReschedulingApi.class);
|
||||||
|
loansPointInTimeApi = retrofit.create(LoansPointInTimeApi.class);
|
||||||
loans = retrofit.create(LoansApi.class);
|
loans = retrofit.create(LoansApi.class);
|
||||||
loanDisbursementDetails = retrofit.create(LoanDisbursementDetailsApi.class);
|
loanDisbursementDetails = retrofit.create(LoanDisbursementDetailsApi.class);
|
||||||
loanTransactions = retrofit.create(LoanTransactionsApi.class);
|
loanTransactions = retrofit.create(LoanTransactionsApi.class);
|
||||||
|
|||||||
+16
@@ -30,6 +30,7 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import net.fortuna.ical4j.model.property.RRule;
|
import net.fortuna.ical4j.model.property.RRule;
|
||||||
import net.fortuna.ical4j.validate.ValidationException;
|
import net.fortuna.ical4j.validate.ValidationException;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -616,6 +617,21 @@ public class DataValidatorBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DataValidatorBuilder listNotEmpty() {
|
||||||
|
if (this.value == null && this.ignoreNullValue) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Object> list = (List<Object>) this.value;
|
||||||
|
if (CollectionUtils.isEmpty(list)) {
|
||||||
|
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".cannot.be.empty";
|
||||||
|
String defaultEnglishMessage = "The parameter `" + this.parameter + "` cannot be empty. You must select at least one.";
|
||||||
|
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter);
|
||||||
|
this.dataValidationErrors.add(error);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public DataValidatorBuilder jsonArrayNotEmpty() {
|
public DataValidatorBuilder jsonArrayNotEmpty() {
|
||||||
if (this.value == null && this.ignoreNullValue) {
|
if (this.value == null && this.ignoreNullValue) {
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
+7
@@ -18,6 +18,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.core.service;
|
package org.apache.fineract.infrastructure.core.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
@@ -35,6 +37,11 @@ public class ExternalIdFactory {
|
|||||||
return StringUtils.isBlank(value) ? ExternalId.empty() : new ExternalId(value);
|
return StringUtils.isBlank(value) ? ExternalId.empty() : new ExternalId(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<ExternalId> produce(List<String> values) {
|
||||||
|
Objects.requireNonNull(values, "values must not be null");
|
||||||
|
return values.stream().map(ExternalIdFactory::produce).toList();
|
||||||
|
}
|
||||||
|
|
||||||
public ExternalId createFromCommand(JsonCommand command, final String externalIdKey) {
|
public ExternalId createFromCommand(JsonCommand command, final String externalIdKey) {
|
||||||
String externalIdStr = null;
|
String externalIdStr = null;
|
||||||
if (command.parsedJson() != null) {
|
if (command.parsedJson() != null) {
|
||||||
|
|||||||
+10
@@ -21,6 +21,8 @@ package org.apache.fineract.organisation.monetary.data;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable data object representing currency.
|
* Immutable data object representing currency.
|
||||||
@@ -87,4 +89,12 @@ public class CurrencyData implements Serializable {
|
|||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
default CurrencyData map(MonetaryCurrency source) {
|
||||||
|
return source.toData();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -19,6 +19,9 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.data;
|
package org.apache.fineract.portfolio.loanaccount.data;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable data object represent loan status enumerations.
|
* Immutable data object represent loan status enumerations.
|
||||||
@@ -51,4 +54,12 @@ public class LoanStatusEnumData {
|
|||||||
this.closed = this.closedObligationsMet || this.closedWrittenOff || this.closedRescheduled;
|
this.closed = this.closedObligationsMet || this.closedWrittenOff || this.closedRescheduled;
|
||||||
this.overpaid = Long.valueOf(700).equals(this.id);
|
this.overpaid = Long.valueOf(700).equals(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
default LoanStatusEnumData map(Loan source) {
|
||||||
|
return LoanEnumerations.status(source.getPlainStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -2693,11 +2693,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return changedTransactionDetail;
|
return changedTransactionDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Probably this is buggy when a chargeback transaction happens
|
||||||
|
*/
|
||||||
public void processPostDisbursementTransactions() {
|
public void processPostDisbursementTransactions() {
|
||||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||||
final List<LoanTransaction> copyTransactions = new ArrayList<>();
|
final List<LoanTransaction> copyTransactions = new ArrayList<>();
|
||||||
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
|
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
|
||||||
|
// TODO: Probably this is not needed and can be eliminated, make sure to double check it
|
||||||
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
|
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
|
||||||
copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
|
copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
|
||||||
}
|
}
|
||||||
@@ -3507,6 +3511,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
|
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeCharges(Predicate<LoanCharge> predicate) {
|
||||||
|
charges.removeIf(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasDelinquencyBucket() {
|
public boolean hasDelinquencyBucket() {
|
||||||
return (getLoanProduct().getDelinquencyBucket() != null);
|
return (getLoanProduct().getDelinquencyBucket() != null);
|
||||||
}
|
}
|
||||||
@@ -3537,6 +3545,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeLoanTransactions(Predicate<LoanTransaction> predicate) {
|
||||||
|
loanTransactions.removeIf(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
public LoanTransaction findChargedOffTransaction() {
|
public LoanTransaction findChargedOffTransaction() {
|
||||||
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
|
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -85,6 +85,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
|||||||
|
|
||||||
String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId";
|
String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId";
|
||||||
|
|
||||||
|
String FIND_IDS_BY_EXTERNAL_IDS = "SELECT loan.id FROM Loan loan WHERE loan.externalId IN :externalIds ORDER BY loan.id ASC";
|
||||||
|
|
||||||
// should follow the logic of `FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE` query
|
// should follow the logic of `FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE` query
|
||||||
String FIND_OLDEST_COB_PROCESSED_LOAN = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.loanStatus in (100,200,300,303,304) and loan.lastClosedBusinessDate = (select min(l.lastClosedBusinessDate) from Loan l where l"
|
String FIND_OLDEST_COB_PROCESSED_LOAN = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.loanStatus in (100,200,300,303,304) and loan.lastClosedBusinessDate = (select min(l.lastClosedBusinessDate) from Loan l where l"
|
||||||
+ ".loanStatus in (100,200,300,303,304) and l.lastClosedBusinessDate < :cobBusinessDate)";
|
+ ".loanStatus in (100,200,300,303,304) and l.lastClosedBusinessDate < :cobBusinessDate)";
|
||||||
@@ -217,6 +219,9 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
|||||||
@Query(FIND_ID_BY_EXTERNAL_ID)
|
@Query(FIND_ID_BY_EXTERNAL_ID)
|
||||||
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
|
||||||
|
|
||||||
|
@Query(FIND_IDS_BY_EXTERNAL_IDS)
|
||||||
|
List<Long> findIdsByExternalIds(@Param("externalIds") List<ExternalId> externalIds);
|
||||||
|
|
||||||
@Query(FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS)
|
@Query(FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS)
|
||||||
List<LoanIdAndLastClosedBusinessDate> findAllNonClosedLoansBehindByLoanIds(@Param("cobBusinessDate") LocalDate cobBusinessDate,
|
List<LoanIdAndLastClosedBusinessDate> findAllNonClosedLoansBehindByLoanIds(@Param("cobBusinessDate") LocalDate cobBusinessDate,
|
||||||
@Param("loanIds") List<Long> loanIds);
|
@Param("loanIds") List<Long> loanIds);
|
||||||
|
|||||||
+3
@@ -285,4 +285,7 @@ public class LoanRepositoryWrapper {
|
|||||||
return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges);
|
return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Long> findIdByExternalIds(List<ExternalId> externalIds) {
|
||||||
|
return repository.findIdsByExternalIds(externalIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ public class LoanScheduleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
|
public void recalculateSchedule(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
|
||||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargedOff()) {
|
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isChargedOff()) {
|
||||||
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+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.api.pointintime;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.DateParam;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeExternalIdsRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
|
|
||||||
|
public interface LoansPointInTimeApi {
|
||||||
|
|
||||||
|
LoanPointInTimeData retrieveLoanPointInTime(Long loanId, DateParam dateParam, String dateFormat, String locale);
|
||||||
|
|
||||||
|
LoanPointInTimeData retrieveLoanPointInTimeByExternalId(String loanExternalId, DateParam dateParam, String dateFormat, String locale);
|
||||||
|
|
||||||
|
List<LoanPointInTimeData> retrieveLoansPointInTime(RetrieveLoansPointInTimeRequest request);
|
||||||
|
|
||||||
|
List<LoanPointInTimeData> retrieveLoansPointInTimeByExternalIds(RetrieveLoansPointInTimeExternalIdsRequest request);
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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.api.pointintime;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.DateParam;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DateFormat;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeExternalIdsRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanPointInTimeService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LoansPointInTimeApiDelegate implements LoansPointInTimeApi {
|
||||||
|
|
||||||
|
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
|
||||||
|
|
||||||
|
private final LoanPointInTimeService loanPointInTimeService;
|
||||||
|
private final LoanReadPlatformService loanReadPlatformService;
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoanPointInTimeData retrieveLoanPointInTime(Long loanId, DateParam dateParam, String dateFormat, String locale) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
return getLoanPointInTime(loanId, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoanPointInTimeData retrieveLoanPointInTimeByExternalId(String loanExternalIdStr, DateParam dateParam, String dateFormat,
|
||||||
|
String locale) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr);
|
||||||
|
Long loanId = resolveExternalId(loanExternalId);
|
||||||
|
|
||||||
|
return getLoanPointInTime(loanId, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LoanPointInTimeData> retrieveLoansPointInTime(RetrieveLoansPointInTimeRequest request) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
List<Long> loanIds = request.getLoanIds();
|
||||||
|
DateParam dateParam = request.getDate();
|
||||||
|
String dateFormat = request.getDateFormat();
|
||||||
|
String locale = request.getLocale();
|
||||||
|
|
||||||
|
return getLoansPointInTime(loanIds, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LoanPointInTimeData> retrieveLoansPointInTimeByExternalIds(RetrieveLoansPointInTimeExternalIdsRequest request) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
List<String> loanExternalIds = request.getExternalIds();
|
||||||
|
DateParam dateParam = request.getDate();
|
||||||
|
String dateFormat = request.getDateFormat();
|
||||||
|
String locale = request.getLocale();
|
||||||
|
|
||||||
|
List<ExternalId> externalIds = ExternalIdFactory.produce(loanExternalIds);
|
||||||
|
List<Long> loanIds = resolveExternalIds(externalIds);
|
||||||
|
|
||||||
|
return getLoansPointInTime(loanIds, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanPointInTimeData> getLoansPointInTime(List<Long> loanIds, DateParam dateParam, String dateFormat, String locale) {
|
||||||
|
DateFormat df = StringUtils.isBlank(dateFormat) ? null : new DateFormat(dateFormat);
|
||||||
|
LocalDate date = dateParam.getDate("date", df, locale);
|
||||||
|
return loanPointInTimeService.retrieveAt(loanIds, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanPointInTimeData getLoanPointInTime(Long loanId, DateParam dateParam, String dateFormat, String locale) {
|
||||||
|
DateFormat df = StringUtils.isBlank(dateFormat) ? null : new DateFormat(dateFormat);
|
||||||
|
LocalDate date = dateParam.getDate("date", df, locale);
|
||||||
|
return loanPointInTimeService.retrieveAt(loanId, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> resolveExternalIds(List<ExternalId> loanExternalIds) {
|
||||||
|
loanExternalIds.forEach(ExternalId::throwExceptionIfEmpty);
|
||||||
|
return loanReadPlatformService.retrieveLoanIdsByExternalIds(loanExternalIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long resolveExternalId(ExternalId loanExternalId) {
|
||||||
|
loanExternalId.throwExceptionIfEmpty();
|
||||||
|
Long resolvedLoanId = loanReadPlatformService.retrieveLoanIdByExternalId(loanExternalId);
|
||||||
|
if (resolvedLoanId == null) {
|
||||||
|
throw new LoanNotFoundException(loanExternalId);
|
||||||
|
}
|
||||||
|
return resolvedLoanId;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.api.pointintime;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
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.QueryParam;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.DateParam;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeExternalIdsRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.api.pointintime.data.RetrieveLoansPointInTimeRequest;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Path("/v1/loans/at-date")
|
||||||
|
@Component
|
||||||
|
@Tag(name = "LoansPointInTime", description = "API to enable clients to retrieve Loan states in a specific point in time")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LoansPointInTimeApiResource implements LoansPointInTimeApi {
|
||||||
|
|
||||||
|
private final LoansPointInTimeApiDelegate delegate;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{loanId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Override
|
||||||
|
public LoanPointInTimeData retrieveLoanPointInTime(@PathParam("loanId") @Parameter(description = "loanId", required = true) Long loanId,
|
||||||
|
@QueryParam("date") @Parameter(description = "date", required = true) DateParam dateParam,
|
||||||
|
@QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat,
|
||||||
|
@QueryParam("locale") @Parameter(description = "locale") final String locale) {
|
||||||
|
return delegate.retrieveLoanPointInTime(loanId, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("external-id/{loanExternalId}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Override
|
||||||
|
public LoanPointInTimeData retrieveLoanPointInTimeByExternalId(
|
||||||
|
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) String loanExternalIdStr,
|
||||||
|
@QueryParam("date") @Parameter(description = "date", required = true) DateParam dateParam,
|
||||||
|
@QueryParam("dateFormat") @Parameter(description = "dateFormat") final String dateFormat,
|
||||||
|
@QueryParam("locale") @Parameter(description = "locale") final String locale) {
|
||||||
|
return delegate.retrieveLoanPointInTimeByExternalId(loanExternalIdStr, dateParam, dateFormat, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("search")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Override
|
||||||
|
public List<LoanPointInTimeData> retrieveLoansPointInTime(@Parameter RetrieveLoansPointInTimeRequest request) {
|
||||||
|
return delegate.retrieveLoansPointInTime(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("search/external-id")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Override
|
||||||
|
public List<LoanPointInTimeData> retrieveLoansPointInTimeByExternalIds(@Parameter RetrieveLoansPointInTimeExternalIdsRequest request) {
|
||||||
|
return delegate.retrieveLoansPointInTimeByExternalIds(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+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.portfolio.loanaccount.api.pointintime.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.DateParam;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RetrieveLoansPointInTimeExternalIdsRequest {
|
||||||
|
|
||||||
|
private List<String> externalIds;
|
||||||
|
private DateParam date;
|
||||||
|
private String dateFormat;
|
||||||
|
private String locale;
|
||||||
|
}
|
||||||
+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.portfolio.loanaccount.api.pointintime.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.DateParam;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RetrieveLoansPointInTimeRequest {
|
||||||
|
|
||||||
|
private List<Long> loanIds;
|
||||||
|
private DateParam date;
|
||||||
|
private String dateFormat;
|
||||||
|
private String locale;
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.loanaccount.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanFeeData {
|
||||||
|
|
||||||
|
private final BigDecimal feeChargesCharged;
|
||||||
|
private final BigDecimal feeAdjustments;
|
||||||
|
private final BigDecimal feeChargesDueAtDisbursementCharged;
|
||||||
|
private final BigDecimal feeChargesPaid;
|
||||||
|
private final BigDecimal feeChargesWaived;
|
||||||
|
private final BigDecimal feeChargesWrittenOff;
|
||||||
|
private final BigDecimal feeChargesOutstanding;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "totalFeeChargesCharged", target = "feeChargesCharged")
|
||||||
|
@Mapping(source = "totalFeeChargesDueAtDisbursement", target = "feeChargesDueAtDisbursementCharged")
|
||||||
|
@Mapping(source = "totalFeeAdjustments", target = "feeAdjustments")
|
||||||
|
@Mapping(source = "totalFeeChargesRepaid", target = "feeChargesPaid")
|
||||||
|
@Mapping(source = "totalFeeChargesWaived", target = "feeChargesWaived")
|
||||||
|
@Mapping(source = "totalFeeChargesWrittenOff", target = "feeChargesWrittenOff")
|
||||||
|
@Mapping(source = "totalFeeChargesOutstanding", target = "feeChargesOutstanding")
|
||||||
|
LoanFeeData map(LoanSummary source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.loanaccount.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanInterestData {
|
||||||
|
|
||||||
|
private final BigDecimal interestCharged;
|
||||||
|
private final BigDecimal interestPaid;
|
||||||
|
private final BigDecimal interestWaived;
|
||||||
|
private final BigDecimal interestWrittenOff;
|
||||||
|
private final BigDecimal interestOutstanding;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "totalInterestCharged", target = "interestCharged")
|
||||||
|
@Mapping(source = "totalInterestRepaid", target = "interestPaid")
|
||||||
|
@Mapping(source = "totalInterestWaived", target = "interestWaived")
|
||||||
|
@Mapping(source = "totalInterestWrittenOff", target = "interestWrittenOff")
|
||||||
|
@Mapping(source = "totalInterestOutstanding", target = "interestOutstanding")
|
||||||
|
LoanInterestData map(LoanSummary source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* 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.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanPenaltyData {
|
||||||
|
|
||||||
|
private final BigDecimal penaltyChargesCharged;
|
||||||
|
private final BigDecimal penaltyAdjustments;
|
||||||
|
private final BigDecimal penaltyChargesPaid;
|
||||||
|
private final BigDecimal penaltyChargesWaived;
|
||||||
|
private final BigDecimal penaltyChargesWrittenOff;
|
||||||
|
private final BigDecimal penaltyChargesOutstanding;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "totalPenaltyChargesCharged", target = "penaltyChargesCharged")
|
||||||
|
@Mapping(source = "totalPenaltyAdjustments", target = "penaltyAdjustments")
|
||||||
|
@Mapping(source = "totalPenaltyChargesRepaid", target = "penaltyChargesPaid")
|
||||||
|
@Mapping(source = "totalPenaltyChargesWaived", target = "penaltyChargesWaived")
|
||||||
|
@Mapping(source = "totalPenaltyChargesWrittenOff", target = "penaltyChargesWrittenOff")
|
||||||
|
@Mapping(source = "totalPenaltyChargesOutstanding", target = "penaltyChargesOutstanding")
|
||||||
|
LoanPenaltyData map(LoanSummary source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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.data;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanPointInTimeData {
|
||||||
|
|
||||||
|
// Loan attributes
|
||||||
|
private Long id;
|
||||||
|
private String accountNo;
|
||||||
|
private LoanStatusEnumData status;
|
||||||
|
private String externalId;
|
||||||
|
private CurrencyData currency;
|
||||||
|
private LoanPrincipalData principal;
|
||||||
|
private LoanInterestData interest;
|
||||||
|
private LoanFeeData fee;
|
||||||
|
private LoanPenaltyData penalty;
|
||||||
|
private LoanTotalAmountData total;
|
||||||
|
|
||||||
|
// Client attributes
|
||||||
|
private Long clientId;
|
||||||
|
private String clientAccountNo;
|
||||||
|
private String clientExternalId;
|
||||||
|
private String clientDisplayName;
|
||||||
|
private Long clientOfficeId;
|
||||||
|
|
||||||
|
// Loan product attributes
|
||||||
|
private Long loanProductId;
|
||||||
|
private String loanProductName;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyData.Mapper.class,
|
||||||
|
LoanPrincipalData.Mapper.class, LoanInterestData.Mapper.class, LoanFeeData.Mapper.class, LoanPenaltyData.Mapper.class,
|
||||||
|
LoanTotalAmountData.Mapper.class })
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "accountNumber", target = "accountNo")
|
||||||
|
@Mapping(source = "source", target = "status")
|
||||||
|
@Mapping(source = "client.id", target = "clientId")
|
||||||
|
@Mapping(source = "client.accountNumber", target = "clientAccountNo")
|
||||||
|
@Mapping(source = "client.externalId", target = "clientExternalId")
|
||||||
|
@Mapping(source = "client.displayName", target = "clientDisplayName")
|
||||||
|
@Mapping(source = "client.office.id", target = "clientOfficeId")
|
||||||
|
@Mapping(source = "summary", target = "principal")
|
||||||
|
@Mapping(source = "summary", target = "interest")
|
||||||
|
@Mapping(source = "summary", target = "fee")
|
||||||
|
@Mapping(source = "summary", target = "penalty")
|
||||||
|
@Mapping(source = "summary", target = "total")
|
||||||
|
@Mapping(source = "loanProduct.id", target = "loanProductId")
|
||||||
|
@Mapping(source = "loanProduct.name", target = "loanProductName")
|
||||||
|
LoanPointInTimeData map(Loan source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.loanaccount.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanPrincipalData {
|
||||||
|
|
||||||
|
private final BigDecimal principalDisbursed;
|
||||||
|
private final BigDecimal principalAdjustments;
|
||||||
|
private final BigDecimal principalPaid;
|
||||||
|
private final BigDecimal principalWrittenOff;
|
||||||
|
private final BigDecimal principalOutstanding;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "totalPrincipalDisbursed", target = "principalDisbursed")
|
||||||
|
@Mapping(source = "totalPrincipalAdjustments", target = "principalAdjustments")
|
||||||
|
@Mapping(source = "totalPrincipalRepaid", target = "principalPaid")
|
||||||
|
@Mapping(source = "totalPrincipalWrittenOff", target = "principalWrittenOff")
|
||||||
|
@Mapping(source = "totalPrincipalOutstanding", target = "principalOutstanding")
|
||||||
|
LoanPrincipalData map(LoanSummary source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.loanaccount.data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanTotalAmountData {
|
||||||
|
|
||||||
|
private final BigDecimal totalExpectedRepayment;
|
||||||
|
private final BigDecimal totalRepayment;
|
||||||
|
private final BigDecimal totalExpectedCostOfLoan;
|
||||||
|
private final BigDecimal totalCostOfLoan;
|
||||||
|
private final BigDecimal totalWaived;
|
||||||
|
private final BigDecimal totalWrittenOff;
|
||||||
|
private final BigDecimal totalOutstanding;
|
||||||
|
|
||||||
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
|
||||||
|
public interface Mapper {
|
||||||
|
|
||||||
|
@Mapping(source = "totalExpectedRepayment", target = "totalExpectedRepayment")
|
||||||
|
@Mapping(source = "totalRepayment", target = "totalRepayment")
|
||||||
|
@Mapping(source = "totalExpectedCostOfLoan", target = "totalExpectedCostOfLoan")
|
||||||
|
@Mapping(source = "totalCostOfLoan", target = "totalCostOfLoan")
|
||||||
|
@Mapping(source = "totalWaived", target = "totalWaived")
|
||||||
|
@Mapping(source = "totalWrittenOff", target = "totalWrittenOff")
|
||||||
|
@Mapping(source = "totalOutstanding", target = "totalOutstanding")
|
||||||
|
LoanTotalAmountData map(LoanSummary source);
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
|
|
||||||
|
public interface LoanPointInTimeService {
|
||||||
|
|
||||||
|
LoanPointInTimeData retrieveAt(Long loanId, LocalDate date);
|
||||||
|
|
||||||
|
List<LoanPointInTimeData> retrieveAt(List<Long> loanIds, LocalDate date);
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* 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.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
|
||||||
|
|
||||||
|
private final LoanUtilService loanUtilService;
|
||||||
|
private final LoanScheduleService loanScheduleService;
|
||||||
|
private final LoanAssembler loanAssembler;
|
||||||
|
private final LoanPointInTimeData.Mapper dataMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
|
||||||
|
validateSingularRetrieval(loanId, date);
|
||||||
|
|
||||||
|
// Note: since everything is running in a readOnly transaction
|
||||||
|
// whatever we modify on the loan is not going to be propagated to the DB
|
||||||
|
// Note2: Interest is always calculated against the current date of the system so we need to roll time back
|
||||||
|
HashMap<BusinessDateType, LocalDate> originalBDs = ThreadLocalContextUtil.getBusinessDates();
|
||||||
|
try {
|
||||||
|
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, date)));
|
||||||
|
|
||||||
|
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
|
removeAfterDateTransactions(loan, date);
|
||||||
|
removeAfterDateCharges(loan, date);
|
||||||
|
|
||||||
|
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
|
||||||
|
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
|
||||||
|
|
||||||
|
return dataMapper.map(loan);
|
||||||
|
} finally {
|
||||||
|
ThreadLocalContextUtil.setBusinessDates(originalBDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAfterDateCharges(Loan loan, LocalDate date) {
|
||||||
|
loan.removeCharges(c -> DateUtils.isAfter(c.getEffectiveDueDate(), date));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeAfterDateTransactions(Loan loan, LocalDate date) {
|
||||||
|
loan.removeLoanTransactions(tx -> DateUtils.isAfter(tx.getTransactionDate(), date));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSingularRetrieval(Long loanId, LocalDate date) {
|
||||||
|
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
|
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
|
||||||
|
baseDataValidator.reset().parameter("loanId").value(loanId).notNull();
|
||||||
|
baseDataValidator.reset().parameter("date").value(date).notNull().notBlank();
|
||||||
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<LoanPointInTimeData> retrieveAt(List<Long> loanIds, LocalDate date) {
|
||||||
|
validateBulkRetrieval(loanIds, date);
|
||||||
|
return loanIds.stream().map(loanId -> retrieveAt(loanId, date)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateBulkRetrieval(List<Long> loanIds, LocalDate date) {
|
||||||
|
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||||
|
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
|
||||||
|
baseDataValidator.reset().parameter("loanIds").value(loanIds).notNull().listNotEmpty();
|
||||||
|
baseDataValidator.reset().parameter("date").value(date).notNull().notBlank();
|
||||||
|
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwExceptionIfValidationWarningsExist(List<ApiParameterError> dataValidationErrors) {
|
||||||
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||||
|
dataValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -141,4 +141,6 @@ public interface LoanReadPlatformService {
|
|||||||
Long retrieveLoanTransactionIdByExternalId(ExternalId externalId);
|
Long retrieveLoanTransactionIdByExternalId(ExternalId externalId);
|
||||||
|
|
||||||
Long retrieveLoanIdByExternalId(ExternalId externalId);
|
Long retrieveLoanIdByExternalId(ExternalId externalId);
|
||||||
|
|
||||||
|
List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -2172,4 +2172,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
return loanRepositoryWrapper.findIdByExternalId(externalId);
|
return loanRepositoryWrapper.findIdByExternalId(externalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds) {
|
||||||
|
return loanRepositoryWrapper.findIdByExternalIds(externalIds);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -3816,7 +3816,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
&& DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate))
|
&& DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate))
|
||||||
|| DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate));
|
|| DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate));
|
||||||
reverseExistingTransactionsTillLastDisbursal(loan, lastDisbursalTransaction);
|
reverseExistingTransactionsTillLastDisbursal(loan, lastDisbursalTransaction);
|
||||||
loanScheduleService.recalculateScheduleFromLastTransaction(loan, scheduleGeneratorDTO);
|
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
|
||||||
actualChanges.put("undolastdisbursal", "true");
|
actualChanges.put("undolastdisbursal", "true");
|
||||||
actualChanges.put("disbursedAmount", loan.getDisbursedAmount());
|
actualChanges.put("disbursedAmount", loan.getDisbursedAmount());
|
||||||
loan.updateLoanSummaryDerivedFields();
|
loan.updateLoanSummaryDerivedFields();
|
||||||
|
|||||||
+2
-1
@@ -1088,7 +1088,8 @@ public final class LoanProductDataValidator {
|
|||||||
&& !LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
|
&& !LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
|
||||||
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
|
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
|
||||||
"reschedule.strategy.method.not.supported.for.loan.schedule.type.progressive",
|
"reschedule.strategy.method.not.supported.for.loan.schedule.type.progressive",
|
||||||
loanScheduleType.toString() + " reschedule strategy type is not supported for Progressive loan schedule type");
|
loanRescheduleStrategyMethod.toString()
|
||||||
|
+ " reschedule strategy type is not supported for Progressive loan schedule type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -67,6 +67,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdStatus;
|
|||||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
|
||||||
import org.apache.fineract.client.models.JournalEntryTransactionItem;
|
import org.apache.fineract.client.models.JournalEntryTransactionItem;
|
||||||
|
import org.apache.fineract.client.models.LoanPointInTimeData;
|
||||||
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
||||||
import org.apache.fineract.client.models.PostChargesResponse;
|
import org.apache.fineract.client.models.PostChargesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
@@ -80,7 +81,9 @@ import org.apache.fineract.client.models.PostLoansRequest;
|
|||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
|
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
|
||||||
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
|
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
|
||||||
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||||
|
import org.apache.fineract.client.util.Calls;
|
||||||
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
||||||
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
||||||
import org.apache.fineract.integrationtests.common.BatchHelper;
|
import org.apache.fineract.integrationtests.common.BatchHelper;
|
||||||
@@ -706,6 +709,26 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
loanTransactionHelper.undoLastDisbursalLoan(loanId, new PostLoansLoanIdRequest());
|
loanTransactionHelper.undoLastDisbursalLoan(loanId, new PostLoansLoanIdRequest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected LoanPointInTimeData getPointInTimeData(Long loanId, String date) {
|
||||||
|
return Calls.ok(fineractClient().loansPointInTimeApi.retrieveLoanPointInTime(loanId, date, DATETIME_PATTERN, "en"));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<LoanPointInTimeData> getPointInTimeData(List<Long> loanIds, String date) {
|
||||||
|
RetrieveLoansPointInTimeRequest request = new RetrieveLoansPointInTimeRequest().loanIds(loanIds).date(date)
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en");
|
||||||
|
return Calls.ok(fineractClient().loansPointInTimeApi.retrieveLoansPointInTime(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verifyOutstanding(LoanPointInTimeData loan, OutstandingAmounts outstanding) {
|
||||||
|
assertThat(BigDecimal.valueOf(outstanding.principalOutstanding))
|
||||||
|
.isEqualByComparingTo(loan.getPrincipal().getPrincipalOutstanding());
|
||||||
|
assertThat(BigDecimal.valueOf(outstanding.interestOutstanding)).isEqualByComparingTo(loan.getInterest().getInterestOutstanding());
|
||||||
|
assertThat(BigDecimal.valueOf(outstanding.feeOutstanding)).isEqualByComparingTo(loan.getFee().getFeeChargesOutstanding());
|
||||||
|
assertThat(BigDecimal.valueOf(outstanding.penaltyOutstanding))
|
||||||
|
.isEqualByComparingTo(loan.getPenalty().getPenaltyChargesOutstanding());
|
||||||
|
assertThat(BigDecimal.valueOf(outstanding.totalOutstanding)).isEqualByComparingTo(loan.getTotal().getTotalOutstanding());
|
||||||
|
}
|
||||||
|
|
||||||
// Note: this is buggy because if multiple journal entries are for the same account, amount and type, the
|
// Note: this is buggy because if multiple journal entries are for the same account, amount and type, the
|
||||||
// verification will pass
|
// verification will pass
|
||||||
// not all journal entries have been validated - since there might be duplicates
|
// not all journal entries have been validated - since there might be duplicates
|
||||||
@@ -1158,8 +1181,8 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
loanBalance);
|
loanBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected OutstandingAmounts outstanding(double principal, double fee, double penalty, double total) {
|
protected OutstandingAmounts outstanding(double principal, double interestOutstanding, double fee, double penalty, double total) {
|
||||||
return new OutstandingAmounts(principal, fee, penalty, total);
|
return new OutstandingAmounts(principal, interestOutstanding, fee, penalty, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected BatchRequestBuilder batchRequest() {
|
protected BatchRequestBuilder batchRequest() {
|
||||||
@@ -1321,6 +1344,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
public static class OutstandingAmounts {
|
public static class OutstandingAmounts {
|
||||||
|
|
||||||
Double principalOutstanding;
|
Double principalOutstanding;
|
||||||
|
Double interestOutstanding;
|
||||||
Double feeOutstanding;
|
Double feeOutstanding;
|
||||||
Double penaltyOutstanding;
|
Double penaltyOutstanding;
|
||||||
Double totalOutstanding;
|
Double totalOutstanding;
|
||||||
|
|||||||
+3
-3
@@ -597,7 +597,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
installment(312.0, 0, 0, 0, 0, true, "01 March 2023"), //
|
installment(312.0, 0, 0, 0, 0, true, "01 March 2023"), //
|
||||||
installment(312.0, 0, 0, 0, 0, true, "01 April 2023"), //
|
installment(312.0, 0, 0, 0, 0, true, "01 April 2023"), //
|
||||||
installment(314.0, 0, 0, 0, 0, true, "01 May 2023"), //
|
installment(314.0, 0, 0, 0, 0, true, "01 May 2023"), //
|
||||||
installment(100.0, 0, 0, 0, outstanding(50.0, 0d, 0d, 50.0), false, "02 May 2023") //
|
installment(100.0, 0, 0, 0, outstanding(50.0, 0.0, 0d, 0d, 50.0), false, "02 May 2023") //
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -659,7 +659,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
// DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ...
|
// DEFAULT payment allocation is ..., DUE_PENALTY, DUE_FEE, DUE_PRINCIPAL, ...
|
||||||
verifyRepaymentSchedule(loanId, //
|
verifyRepaymentSchedule(loanId, //
|
||||||
installment(1250.0, null, "01 January 2023"), //
|
installment(1250.0, null, "01 January 2023"), //
|
||||||
installment(342.0, 0, 100, 40, outstanding(30.0, 20.0, 0.0, 50.0), false, "01 February 2023"), //
|
installment(342.0, 0, 100, 40, outstanding(30.0, 0.0, 20.0, 0.0, 50.0), false, "01 February 2023"), //
|
||||||
installment(312.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
installment(312.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
||||||
installment(312.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
installment(312.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
||||||
installment(314.0, 0, 0, 0, 0.0, true, "01 May 2023") //
|
installment(314.0, 0, 0, 0, 0.0, true, "01 May 2023") //
|
||||||
@@ -726,7 +726,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
// DEFAULT payment allocation is ..., DUE_PRINCIPAL, DUE_FEE, DUE_PENALTY ...
|
// DEFAULT payment allocation is ..., DUE_PRINCIPAL, DUE_FEE, DUE_PENALTY ...
|
||||||
verifyRepaymentSchedule(loanId, //
|
verifyRepaymentSchedule(loanId, //
|
||||||
installment(1250.0, null, "01 January 2023"), //
|
installment(1250.0, null, "01 January 2023"), //
|
||||||
installment(342.0, 0, 100, 40, outstanding(0.0, 30.0, 20.0, 50.0), false, "01 February 2023"), //
|
installment(342.0, 0, 100, 40, outstanding(0.0, 0.0, 30.0, 20.0, 50.0), false, "01 February 2023"), //
|
||||||
installment(312.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
installment(312.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
|
||||||
installment(312.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
installment(312.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
|
||||||
installment(314.0, 0, 0, 0, 0.0, true, "01 May 2023") //
|
installment(314.0, 0, 0, 0, 0.0, true, "01 May 2023") //
|
||||||
|
|||||||
+659
@@ -0,0 +1,659 @@
|
|||||||
|
/**
|
||||||
|
* 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.loan.pointintime;
|
||||||
|
|
||||||
|
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.apache.fineract.client.models.ChargeData;
|
||||||
|
import org.apache.fineract.client.models.LoanPointInTimeData;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansRequestChargeData;
|
||||||
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
|
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeDataWorks_ForPrincipalOutstandingCalculation() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
// Create charges
|
||||||
|
double charge1Amount = 1.0;
|
||||||
|
double charge2Amount = 1.5;
|
||||||
|
Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
|
||||||
|
Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||||
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false)//
|
||||||
|
.disallowExpectedDisbursements(false)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.multiDisburseLoan(null)//
|
||||||
|
.charges(List.of(new ChargeData().id(charge1Id), new ChargeData().id(charge2Id)));//
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
double amount = 5000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||||
|
.repaymentEvery(repaymentEvery)//
|
||||||
|
.loanTermFrequency(numberOfRepayments)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.charges(List.of(//
|
||||||
|
new PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)), //
|
||||||
|
new PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
|
||||||
|
));//
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 January 2023"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("09 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "09 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "07 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "09 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4000.0, 0.0, 0.0, 0.0, 4000.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4000.0, 0.0, 0.0, 0.0, 4000.0));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(3500.0, 0.0, 0.0, 0.0, 3500.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeDataWorks_ForAllOutstandingCalculation_WhenLoanIsCumulative_AndInterestIsEnabled() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||||
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestRatePerPeriod(10.0)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false)//
|
||||||
|
.disallowExpectedDisbursements(false)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.multiDisburseLoan(null);//
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
double amount = 5000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||||
|
.repaymentEvery(repaymentEvery)//
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(10.0))//
|
||||||
|
.loanTermFrequency(numberOfRepayments)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);//
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 January 2023"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
Long chargeId = createCharge(100.0).getResourceId();
|
||||||
|
addLoanCharge(loanId, chargeId, "02 February 2023", 100.0);
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 250.0, "01 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(5000.0, 767.70, 0.0, 0.0, 5767.70));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("09 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "09 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(5000.0, 767.70, 0.0, 0.0, 5767.70));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "07 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(5000.0, 800.22, 100.0, 0.0, 5900.22));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "09 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4759.59, 551.47, 100.0, 0.0, 5411.06));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(5000.0, 767.70, 0.0, 0.0, 5767.70));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4759.59, 556.02, 100.0, 0.0, 5415.61));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4259.59, 642.46, 100.0, 0.0, 5002.05));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("05 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay full loan
|
||||||
|
addRepaymentForLoan(loanId, 5032.52, "05 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(5000.0, 767.70, 0.0, 0.0, 5767.70));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4759.59, 556.02, 100.0, 0.0, 5415.61));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4259.59, 642.46, 100.0, 0.0, 5002.05));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "05 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0, 0.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023"), //
|
||||||
|
transaction(5032.52, "Repayment", "05 March 2023"), //
|
||||||
|
transaction(1282.52, "Accrual", "05 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeDataWorks_ForAllOutstandingCalculation_WhenLoanIsProgressive_AndInterestIsEnabled() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
PostLoanProductsRequest product = create4IProgressive() //
|
||||||
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestRatePerPeriod(10.0)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false)//
|
||||||
|
.disallowExpectedDisbursements(false)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.currencyCode("USD").multiDisburseLoan(null);//
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
double amount = 5000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||||
|
.repaymentEvery(repaymentEvery)//
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).interestRatePerPeriod(BigDecimal.valueOf(10.0))//
|
||||||
|
.loanTermFrequency(numberOfRepayments)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);//
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 January 2023"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
Long chargeId = createCharge(100.0).getResourceId();
|
||||||
|
addLoanCharge(loanId, chargeId, "02 February 2023", 100.0);
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 250.0, "01 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4750.0, 83.56, 0.0, 0.0, 4833.56));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("09 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "09 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4750.0, 83.56, 0.0, 0.0, 4833.56));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "07 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4750.0, 86.10, 100.0, 0.0, 4936.10));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "09 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4250.0, 86.93, 100.0, 0.0, 4436.93));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4750.0, 83.56, 0.0, 0.0, 4833.56));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4250.0, 87.20, 100.0, 0.0, 4437.20));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(3750.0, 92.36, 100.0, 0.0, 3942.36));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("05 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// repay full loan
|
||||||
|
addRepaymentForLoan(loanId, 3942.36, "05 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4750.0, 83.56, 0.0, 0.0, 4833.56));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(4250.0, 87.20, 100.0, 0.0, 4437.20));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(3750.0, 92.36, 100.0, 0.0, 3942.36));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(loanId, "05 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0, 0.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(250.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "09 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023"), //
|
||||||
|
transaction(3942.36, "Repayment", "05 March 2023"), //
|
||||||
|
transaction(182.31, "Accrual", "05 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoansPointInTimeDataWorks_ForPrincipalOutstandingCalculation() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
AtomicReference<Long> aLoanId2 = new AtomicReference<>();
|
||||||
|
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
// Create charges
|
||||||
|
double charge1Amount = 1.0;
|
||||||
|
double charge2Amount = 1.5;
|
||||||
|
Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
|
||||||
|
Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
|
||||||
|
.numberOfRepayments(numberOfRepayments) //
|
||||||
|
.repaymentEvery(repaymentEvery) //
|
||||||
|
.installmentAmountInMultiplesOf(null) //
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false)//
|
||||||
|
.disallowExpectedDisbursements(false)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.multiDisburseLoan(null)//
|
||||||
|
.charges(List.of(new ChargeData().id(charge1Id), new ChargeData().id(charge2Id)));//
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
double amount = 5000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||||
|
.repaymentEvery(repaymentEvery)//
|
||||||
|
.loanTermFrequency(numberOfRepayments)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.charges(List.of(//
|
||||||
|
new PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)), //
|
||||||
|
new PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
|
||||||
|
));//
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
PostLoansResponse postLoansResponse2 = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 January 2023"));
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult2 = loanTransactionHelper.approveLoan(postLoansResponse2.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 January 2023"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
aLoanId2.getAndSet(approvedLoanResult2.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
Long loanId2 = aLoanId2.get();
|
||||||
|
|
||||||
|
// disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||||
|
disburseLoan(loanId2, BigDecimal.valueOf(5000.0), "01 January 2023");
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023") //
|
||||||
|
);
|
||||||
|
verifyTransactions(loanId2, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 February 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
Long loanId2 = aLoanId2.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 February 2023");
|
||||||
|
addRepaymentForLoan(loanId2, 500.0, "01 February 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
List<LoanPointInTimeData> pointInTimeData = getPointInTimeData(List.of(loanId, loanId2), "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData.get(0), outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
verifyOutstanding(pointInTimeData.get(1), outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023") //
|
||||||
|
);
|
||||||
|
verifyTransactions(loanId2, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
Long loanId2 = aLoanId2.get();
|
||||||
|
|
||||||
|
// repay 500
|
||||||
|
addRepaymentForLoan(loanId, 500.0, "01 March 2023");
|
||||||
|
addRepaymentForLoan(loanId2, 500.0, "01 March 2023");
|
||||||
|
|
||||||
|
// check point in time data
|
||||||
|
List<LoanPointInTimeData> pointInTimeData = getPointInTimeData(List.of(loanId, loanId2), "01 February 2023");
|
||||||
|
verifyOutstanding(pointInTimeData.get(0), outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
verifyOutstanding(pointInTimeData.get(1), outstanding(4500.0, 0.0, 0.0, 0.0, 4500.0));
|
||||||
|
|
||||||
|
pointInTimeData = getPointInTimeData(List.of(loanId, loanId2), "01 March 2023");
|
||||||
|
verifyOutstanding(pointInTimeData.get(0), outstanding(4000.0, 0.0, 0.0, 0.0, 4000.0));
|
||||||
|
verifyOutstanding(pointInTimeData.get(1), outstanding(4000.0, 0.0, 0.0, 0.0, 4000.0));
|
||||||
|
|
||||||
|
// verify transactions
|
||||||
|
verifyTransactions(loanId, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023") //
|
||||||
|
);
|
||||||
|
verifyTransactions(loanId2, //
|
||||||
|
transaction(5000.0, "Disbursement", "01 January 2023"), //
|
||||||
|
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 February 2023"), //
|
||||||
|
transaction(500.0, "Repayment", "01 March 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user