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.LoanTransactionsApi;
|
||||
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.MappingFinancialActivitiesToAccountsApi;
|
||||
import org.apache.fineract.client.services.MixMappingApi;
|
||||
@@ -229,6 +230,7 @@ public final class FineractClient {
|
||||
public final LoanCollateralApi loanCollaterals;
|
||||
public final LoanProductsApi loanProducts;
|
||||
public final LoanReschedulingApi loanSchedules;
|
||||
public final LoansPointInTimeApi loansPointInTimeApi;
|
||||
public final LoansApi loans;
|
||||
public final LoanDisbursementDetailsApi loanDisbursementDetails;
|
||||
public final LoanTransactionsApi loanTransactions;
|
||||
@@ -355,6 +357,7 @@ public final class FineractClient {
|
||||
loanCollaterals = retrofit.create(LoanCollateralApi.class);
|
||||
loanProducts = retrofit.create(LoanProductsApi.class);
|
||||
loanSchedules = retrofit.create(LoanReschedulingApi.class);
|
||||
loansPointInTimeApi = retrofit.create(LoansPointInTimeApi.class);
|
||||
loans = retrofit.create(LoansApi.class);
|
||||
loanDisbursementDetails = retrofit.create(LoanDisbursementDetailsApi.class);
|
||||
loanTransactions = retrofit.create(LoanTransactionsApi.class);
|
||||
|
||||
+16
@@ -30,6 +30,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import net.fortuna.ical4j.model.property.RRule;
|
||||
import net.fortuna.ical4j.validate.ValidationException;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
@@ -616,6 +617,21 @@ public class DataValidatorBuilder {
|
||||
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() {
|
||||
if (this.value == null && this.ignoreNullValue) {
|
||||
return this;
|
||||
|
||||
+7
@@ -18,6 +18,8 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
@@ -35,6 +37,11 @@ public class ExternalIdFactory {
|
||||
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) {
|
||||
String externalIdStr = null;
|
||||
if (command.parsedJson() != null) {
|
||||
|
||||
+10
@@ -21,6 +21,8 @@ package org.apache.fineract.organisation.monetary.data;
|
||||
import java.io.Serializable;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
|
||||
/**
|
||||
* Immutable data object representing currency.
|
||||
@@ -87,4 +89,12 @@ public class CurrencyData implements Serializable {
|
||||
|
||||
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;
|
||||
|
||||
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.
|
||||
@@ -51,4 +54,12 @@ public class LoanStatusEnumData {
|
||||
this.closed = this.closedObligationsMet || this.closedWrittenOff || this.closedRescheduled;
|
||||
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;
|
||||
}
|
||||
|
||||
/*
|
||||
* Probably this is buggy when a chargeback transaction happens
|
||||
*/
|
||||
public void processPostDisbursementTransactions() {
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
final List<LoanTransaction> copyTransactions = new ArrayList<>();
|
||||
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
|
||||
// TODO: Probably this is not needed and can be eliminated, make sure to double check it
|
||||
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
|
||||
copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
|
||||
}
|
||||
@@ -3507,6 +3511,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
|
||||
}
|
||||
|
||||
public void removeCharges(Predicate<LoanCharge> predicate) {
|
||||
charges.removeIf(predicate);
|
||||
}
|
||||
|
||||
public boolean hasDelinquencyBucket() {
|
||||
return (getLoanProduct().getDelinquencyBucket() != null);
|
||||
}
|
||||
@@ -3537,6 +3545,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public void removeLoanTransactions(Predicate<LoanTransaction> predicate) {
|
||||
loanTransactions.removeIf(predicate);
|
||||
}
|
||||
|
||||
public LoanTransaction findChargedOffTransaction() {
|
||||
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_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
|
||||
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)";
|
||||
@@ -217,6 +219,9 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
||||
@Query(FIND_ID_BY_EXTERNAL_ID)
|
||||
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)
|
||||
List<LoanIdAndLastClosedBusinessDate> findAllNonClosedLoansBehindByLoanIds(@Param("cobBusinessDate") LocalDate cobBusinessDate,
|
||||
@Param("loanIds") List<Long> loanIds);
|
||||
|
||||
+3
@@ -285,4 +285,7 @@ public class LoanRepositoryWrapper {
|
||||
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()) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
|
||||
} 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 retrieveLoanIdByExternalId(ExternalId externalId);
|
||||
|
||||
List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds);
|
||||
}
|
||||
|
||||
+5
@@ -2172,4 +2172,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
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.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate));
|
||||
reverseExistingTransactionsTillLastDisbursal(loan, lastDisbursalTransaction);
|
||||
loanScheduleService.recalculateScheduleFromLastTransaction(loan, scheduleGeneratorDTO);
|
||||
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
|
||||
actualChanges.put("undolastdisbursal", "true");
|
||||
actualChanges.put("disbursedAmount", loan.getDisbursedAmount());
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
+2
-1
@@ -1088,7 +1088,8 @@ public final class LoanProductDataValidator {
|
||||
&& !LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
|
||||
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
|
||||
"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.GetLoansLoanIdTransactionsTemplateResponse;
|
||||
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.PostChargesResponse;
|
||||
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.PutGlobalConfigurationsRequest;
|
||||
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.Calls;
|
||||
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
|
||||
import org.apache.fineract.integrationtests.client.IntegrationTest;
|
||||
import org.apache.fineract.integrationtests.common.BatchHelper;
|
||||
@@ -706,6 +709,26 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
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
|
||||
// verification will pass
|
||||
// not all journal entries have been validated - since there might be duplicates
|
||||
@@ -1158,8 +1181,8 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
loanBalance);
|
||||
}
|
||||
|
||||
protected OutstandingAmounts outstanding(double principal, double fee, double penalty, double total) {
|
||||
return new OutstandingAmounts(principal, fee, penalty, total);
|
||||
protected OutstandingAmounts outstanding(double principal, double interestOutstanding, double fee, double penalty, double total) {
|
||||
return new OutstandingAmounts(principal, interestOutstanding, fee, penalty, total);
|
||||
}
|
||||
|
||||
protected BatchRequestBuilder batchRequest() {
|
||||
@@ -1321,6 +1344,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
public static class OutstandingAmounts {
|
||||
|
||||
Double principalOutstanding;
|
||||
Double interestOutstanding;
|
||||
Double feeOutstanding;
|
||||
Double penaltyOutstanding;
|
||||
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 April 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, ...
|
||||
verifyRepaymentSchedule(loanId, //
|
||||
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 April 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 ...
|
||||
verifyRepaymentSchedule(loanId, //
|
||||
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 April 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