FINERACT-2178: Ability to retrieve a loan's balance at a certain point in time in the past

This commit is contained in:
Arnold Galovics
2025-02-17 21:48:05 +01:00
committed by Arnold Gálovics
parent 986dceea1c
commit 0f1be3c913
29 changed files with 1523 additions and 8 deletions
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
}
@@ -141,4 +141,6 @@ public interface LoanReadPlatformService {
Long retrieveLoanTransactionIdByExternalId(ExternalId externalId);
Long retrieveLoanIdByExternalId(ExternalId externalId);
List<Long> retrieveLoanIdsByExternalIds(List<ExternalId> externalIds);
}
@@ -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);
}
}
@@ -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();
@@ -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");
}
}