Skip to content

Commit

Permalink
FINERACT-2178: Ability to retrieve a loan's balance at a certain poin…
Browse files Browse the repository at this point in the history
…t in time in the past
  • Loading branch information
galovics committed Feb 17, 2025
1 parent 986dcee commit 0f1be3c
Show file tree
Hide file tree
Showing 29 changed files with 1,523 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -355,6 +357,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -616,6 +617,21 @@ public DataValidatorBuilder arrayNotEmpty() {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +37,11 @@ public static ExternalId produce(String 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) {
String externalIdStr = null;
if (command.parsedJson() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
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.
Expand Down Expand Up @@ -87,4 +89,12 @@ private String generateDisplayLabel() {

return builder.toString();
}

@org.mapstruct.Mapper(config = MapstructMapperConfig.class)
public interface Mapper {

default CurrencyData map(MonetaryCurrency source) {
return source.toData();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -51,4 +54,12 @@ public LoanStatusEnumData(final Long id, final String code, final String value)
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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2693,11 +2693,15 @@ public ChangedTransactionDetail processTransactions() {
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));
}
Expand Down Expand Up @@ -3507,6 +3511,10 @@ public Collection<LoanCharge> getCharges() {
return Optional.ofNullable(this.charges).orElse(new HashSet<>());
}

public void removeCharges(Predicate<LoanCharge> predicate) {
charges.removeIf(predicate);
}

public boolean hasDelinquencyBucket() {
return (getLoanProduct().getDelinquencyBucket() != null);
}
Expand Down Expand Up @@ -3537,6 +3545,10 @@ public LoanTransaction getLoanTransaction(Predicate<LoanTransaction> predicate)
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down Expand Up @@ -217,6 +219,9 @@ List<Loan> findByGroupOfficeIdsAndLoanStatus(@Param("officeIds") Collection<Long
@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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,7 @@ public List<Loan> findLoansForAddAccrual(Integer accountingType, LocalDate tillD
return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges);
}

public List<Long> findIdByExternalIds(List<ExternalId> externalIds) {
return repository.findIdsByExternalIds(externalIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGenerator
}
}

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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 0f1be3c

Please sign in to comment.