FINERACT-1806: Add charge-off reasons to loan product response
This commit is contained in:
committed by
Adam Saghy
parent
6286d1f821
commit
628f27bf32
+2
-1
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.infrastructure.codes.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
|
||||
/**
|
||||
@@ -40,7 +41,7 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
*/
|
||||
public interface CodeValueReadPlatformService {
|
||||
|
||||
Collection<CodeValueData> retrieveCodeValuesByCode(String code);
|
||||
List<CodeValueData> retrieveCodeValuesByCode(String code);
|
||||
|
||||
Collection<CodeValueData> retrieveAllCodeValues(Long codeId);
|
||||
|
||||
|
||||
+6
@@ -53,6 +53,7 @@ import org.apache.fineract.client.services.SchedulerApi;
|
||||
import org.apache.fineract.client.services.SchedulerJobApi;
|
||||
import org.apache.fineract.client.services.UsersApi;
|
||||
import org.apache.fineract.client.util.FineractClient;
|
||||
import org.apache.fineract.test.stepdef.loan.LoanProductsCustomApi;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -98,6 +99,11 @@ public class ApiConfiguration {
|
||||
return fineractClient.createService(LoanProductsApi.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LoanProductsCustomApi loanProductsCustomApi() {
|
||||
return fineractClient.createService(LoanProductsCustomApi.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SavingsProductApi savingsProductApi() {
|
||||
return fineractClient.createService(SavingsProductApi.class);
|
||||
|
||||
+16
@@ -24,6 +24,7 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.client.models.BatchResponse;
|
||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
@@ -888,4 +889,19 @@ public final class ErrorMessageHelper {
|
||||
public static String downpaymentDisabledOnProductErrorCodeMsg() {
|
||||
return "The Loan can not override the downpayment properties because in the Loan Product the downpayment is disabled";
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInChargeOffReasonOptions(final int line, final List<List<String>> actual,
|
||||
final List<String> expected) {
|
||||
final String actualValues = actual.stream().map(List::toString).collect(Collectors.joining(System.lineSeparator()));
|
||||
|
||||
return String.format(
|
||||
"%nWrong value in Loan Charge-Off Reason Options line %s. %nActual values in line: %s %nExpected values in line: %s", line,
|
||||
actualValues, expected);
|
||||
}
|
||||
|
||||
public static String wrongNumberOfLinesInChargeOffReasonOptions(final int actual, final int expected) {
|
||||
return String.format(
|
||||
"Number of lines in loan charge-off reason options is not correct. Actual value is: %d - Expected value is: %d", actual,
|
||||
expected);
|
||||
}
|
||||
}
|
||||
|
||||
+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.test.stepdef.loan;
|
||||
|
||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.GET;
|
||||
|
||||
public interface LoanProductsCustomApi {
|
||||
|
||||
@GET("v1/loanproducts/{productId}")
|
||||
Call<GetLoanProductsProductIdResponse> retrieveLoanProductDetails(@retrofit2.http.Path("productId") Long productId,
|
||||
@retrofit2.http.Query("template") String isTemplate);
|
||||
}
|
||||
+90
@@ -52,7 +52,9 @@ import org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1;
|
||||
import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1;
|
||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsChargeOffReasonOptions;
|
||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsTemplateResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdLoanChargePaidByData;
|
||||
@@ -158,6 +160,9 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
@Autowired
|
||||
private LoanProductsApi loanProductsApi;
|
||||
|
||||
@Autowired
|
||||
private LoanProductsCustomApi loanProductsCustomApi;
|
||||
|
||||
@Autowired
|
||||
private EventStore eventStore;
|
||||
|
||||
@@ -2550,6 +2555,66 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
assertTrue(relationshipOptional.isPresent(), "Missed relationship between transactions");
|
||||
}
|
||||
|
||||
@Then("Loan Product Charge-Off reasons options from loan product template have {int} options, with the following data:")
|
||||
public void loanProductTemplateChargeOffReasonOptionsCheck(final int linesExpected, final DataTable table) throws IOException {
|
||||
final Response<GetLoanProductsTemplateResponse> loanProductDetails = loanProductsApi.retrieveTemplate11(false).execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanProductDetails);
|
||||
|
||||
assertNotNull(loanProductDetails.body());
|
||||
final List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions = loanProductDetails.body().getChargeOffReasonOptions();
|
||||
assertNotNull(chargeOffReasonOptions);
|
||||
|
||||
final List<List<String>> data = table.asLists();
|
||||
final int linesActual = chargeOffReasonOptions.size();
|
||||
data.stream().skip(1) // skip headers
|
||||
.forEach(expectedValues -> {
|
||||
final List<List<String>> actualValuesList = chargeOffReasonOptions.stream()
|
||||
.map(chargeOffReason -> fetchValuesOfLoanChargeOffReasonOptions(data.get(0), chargeOffReason))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final boolean containsExpectedValues = actualValuesList.stream()
|
||||
.anyMatch(actualValues -> actualValues.equals(expectedValues));
|
||||
assertThat(containsExpectedValues).as(ErrorMessageHelper
|
||||
.wrongValueInLineInChargeOffReasonOptions(data.indexOf(expectedValues), actualValuesList, expectedValues))
|
||||
.isTrue();
|
||||
|
||||
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInChargeOffReasonOptions(linesActual, linesExpected))
|
||||
.isEqualTo(linesExpected);
|
||||
});
|
||||
}
|
||||
|
||||
@Then("Loan Product {string} Charge-Off reasons options from specific loan product have {int} options, with the following data:")
|
||||
public void specificLoanProductChargeOffReasonOptionsCheck(final String loanProductName, final int linesExpected, final DataTable table)
|
||||
throws IOException {
|
||||
final DefaultLoanProduct product = DefaultLoanProduct.valueOf(loanProductName);
|
||||
final Long loanProductId = loanProductResolver.resolve(product);
|
||||
final Response<GetLoanProductsProductIdResponse> loanProductDetails = loanProductsCustomApi
|
||||
.retrieveLoanProductDetails(loanProductId, "true").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanProductDetails);
|
||||
|
||||
assertNotNull(loanProductDetails.body());
|
||||
final List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions = loanProductDetails.body().getChargeOffReasonOptions();
|
||||
assertNotNull(chargeOffReasonOptions);
|
||||
|
||||
final List<List<String>> data = table.asLists();
|
||||
final int linesActual = chargeOffReasonOptions.size();
|
||||
data.stream().skip(1) // skip headers
|
||||
.forEach(expectedValues -> {
|
||||
final List<List<String>> actualValuesList = chargeOffReasonOptions.stream()
|
||||
.map(chargeOffReason -> fetchValuesOfLoanChargeOffReasonOptions(data.get(0), chargeOffReason))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final boolean containsExpectedValues = actualValuesList.stream()
|
||||
.anyMatch(actualValues -> actualValues.equals(expectedValues));
|
||||
assertThat(containsExpectedValues).as(ErrorMessageHelper
|
||||
.wrongValueInLineInChargeOffReasonOptions(data.indexOf(expectedValues), actualValuesList, expectedValues))
|
||||
.isTrue();
|
||||
|
||||
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInChargeOffReasonOptions(linesActual, linesExpected))
|
||||
.isEqualTo(linesExpected);
|
||||
});
|
||||
}
|
||||
|
||||
private void createCustomizedLoan(final List<String> loanData, final boolean withEmi) throws IOException {
|
||||
final String loanProduct = loanData.get(0);
|
||||
final String submitDate = loanData.get(1);
|
||||
@@ -2928,4 +2993,29 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
}
|
||||
return actualValues;
|
||||
}
|
||||
|
||||
@SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
|
||||
private List<String> fetchValuesOfLoanChargeOffReasonOptions(final List<String> header,
|
||||
final GetLoanProductsChargeOffReasonOptions chargeOffReasonOption) {
|
||||
final List<String> actualValues = new ArrayList<>();
|
||||
for (String headerName : header) {
|
||||
switch (headerName) {
|
||||
case "Charge-Off Reason Name" ->
|
||||
actualValues.add(chargeOffReasonOption.getName() == null ? null : chargeOffReasonOption.getName());
|
||||
case "Description" -> {
|
||||
assertNotNull(chargeOffReasonOption.getDescription());
|
||||
actualValues
|
||||
.add(chargeOffReasonOption.getDescription().isEmpty() || chargeOffReasonOption.getDescription() == null ? null
|
||||
: chargeOffReasonOption.getDescription());
|
||||
}
|
||||
case "Position" -> actualValues
|
||||
.add(chargeOffReasonOption.getPosition() == null ? null : String.valueOf(chargeOffReasonOption.getPosition()));
|
||||
case "Is Active" ->
|
||||
actualValues.add(chargeOffReasonOption.getActive() == null ? null : String.valueOf(chargeOffReasonOption.getActive()));
|
||||
case "Is Mandatory" -> actualValues
|
||||
.add(chargeOffReasonOption.getMandatory() == null ? null : String.valueOf(chargeOffReasonOption.getMandatory()));
|
||||
}
|
||||
}
|
||||
return actualValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,3 +181,21 @@ Feature: LoanProduct
|
||||
Then Loan has 500 outstanding amount
|
||||
When Refund undo happens on "1 July 2022"
|
||||
Then Loan has 1000 outstanding amount
|
||||
|
||||
Scenario: As a user I would like to verify Charge-Off reasons options in loan product template response
|
||||
When Admin sets the business date to "12 December 2021"
|
||||
When Admin creates a client with random data
|
||||
And Admin successfully creates a new customised Loan submitted on date: "12 December 2021", with Principal: "1000", a loanTermFrequency: 1 months, and numberOfRepayments: 1
|
||||
Then Loan Product Charge-Off reasons options from loan product template have 2 options, with the following data:
|
||||
| Charge-Off Reason Name | Description | Position | Is Active | Is Mandatory |
|
||||
| debit_card | | 0 | true | false |
|
||||
| credit_card | | 1 | true | false |
|
||||
|
||||
Scenario: As a user I would like to verify Charge-Off reasons options in specific loan product response
|
||||
When Admin sets the business date to "12 December 2021"
|
||||
When Admin creates a client with random data
|
||||
And Admin successfully creates a new customised Loan submitted on date: "12 December 2021", with Principal: "1000", a loanTermFrequency: 1 months, and numberOfRepayments: 1
|
||||
Then Loan Product "LP1" Charge-Off reasons options from specific loan product have 2 options, with the following data:
|
||||
| Charge-Off Reason Name | Description | Position | Is Active | Is Mandatory |
|
||||
| debit_card | | 0 | true | false |
|
||||
| credit_card | | 1 | true | false |
|
||||
+20
@@ -1016,6 +1016,24 @@ final class LoanProductsApiResourceSwagger {
|
||||
public String description;
|
||||
}
|
||||
|
||||
static final class GetLoanProductsChargeOffReasonOptions {
|
||||
|
||||
private GetLoanProductsChargeOffReasonOptions() {}
|
||||
|
||||
@Schema(example = "2")
|
||||
public Long id;
|
||||
@Schema(example = "debit_card")
|
||||
public String name;
|
||||
@Schema(example = "2")
|
||||
public Integer position;
|
||||
@Schema(example = "Charge-Off reason description")
|
||||
public String description;
|
||||
@Schema(example = "true")
|
||||
public Boolean active;
|
||||
@Schema(example = "false")
|
||||
public Boolean mandatory;
|
||||
}
|
||||
|
||||
@Schema(example = "false")
|
||||
public Boolean includeInBorrowerCycle;
|
||||
@Schema(example = "false")
|
||||
@@ -1068,6 +1086,7 @@ final class LoanProductsApiResourceSwagger {
|
||||
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypes;
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypesOptions;
|
||||
public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
|
||||
}
|
||||
|
||||
@Schema(description = "GetLoanProductsProductIdResponse")
|
||||
@@ -1342,6 +1361,7 @@ final class LoanProductsApiResourceSwagger {
|
||||
@Schema(example = "false")
|
||||
public Boolean enableAccrualActivityPosting;
|
||||
public List<StringEnumOptionData> supportedInterestRefundTypes;
|
||||
public List<GetLoanProductsTemplateResponse.GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions;
|
||||
}
|
||||
|
||||
@Schema(description = "PutLoanProductsProductIdRequest")
|
||||
|
||||
+6
-1
@@ -32,6 +32,7 @@ import org.apache.fineract.accounting.common.AccountingRuleType;
|
||||
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
@@ -212,6 +213,8 @@ public class LoanProductData implements Serializable {
|
||||
private final boolean isEqualAmortization;
|
||||
private final BigDecimal fixedPrincipalPercentagePerInstallment;
|
||||
|
||||
private final List<CodeValueData> chargeOffReasonOptions;
|
||||
|
||||
// Delinquency Buckets
|
||||
private final Collection<DelinquencyBucketData> delinquencyBucketOptions;
|
||||
private final DelinquencyBucketData delinquencyBucket;
|
||||
@@ -913,6 +916,7 @@ public class LoanProductData implements Serializable {
|
||||
this.enableAccrualActivityPosting = enableAccrualActivityPosting;
|
||||
this.supportedInterestRefundTypes = supportedInterestRefundTypes;
|
||||
this.supportedInterestRefundTypesOptions = null;
|
||||
this.chargeOffReasonOptions = null;
|
||||
}
|
||||
|
||||
public LoanProductData(final LoanProductData productData, final Collection<ChargeData> chargeOptions,
|
||||
@@ -935,7 +939,7 @@ public class LoanProductData implements Serializable {
|
||||
final List<EnumOptionData> advancedPaymentAllocationTypes, final List<EnumOptionData> loanScheduleTypeOptions,
|
||||
final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<EnumOptionData> creditAllocationTransactionTypes,
|
||||
final List<EnumOptionData> creditAllocationAllocationTypes,
|
||||
final List<StringEnumOptionData> supportedInterestRefundTypesOptions) {
|
||||
final List<StringEnumOptionData> supportedInterestRefundTypesOptions, final List<CodeValueData> chargeOffReasonOptions) {
|
||||
|
||||
this.id = productData.id;
|
||||
this.name = productData.name;
|
||||
@@ -1092,6 +1096,7 @@ public class LoanProductData implements Serializable {
|
||||
this.enableAccrualActivityPosting = productData.enableAccrualActivityPosting;
|
||||
this.supportedInterestRefundTypesOptions = supportedInterestRefundTypesOptions;
|
||||
this.supportedInterestRefundTypes = productData.supportedInterestRefundTypes;
|
||||
this.chargeOffReasonOptions = chargeOffReasonOptions;
|
||||
}
|
||||
|
||||
private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
|
||||
|
||||
+1
-1
@@ -300,7 +300,7 @@ public class BulkImportWorkbookPopulatorServiceImpl implements BulkImportWorkboo
|
||||
private List<CodeValueData> fetchCodeValuesByCodeName(String codeName) {
|
||||
List<CodeValueData> codeValues = null;
|
||||
if (codeName != null) {
|
||||
codeValues = (List<CodeValueData>) codeValueReadPlatformService.retrieveCodeValuesByCode(codeName);
|
||||
codeValues = codeValueReadPlatformService.retrieveCodeValuesByCode(codeName);
|
||||
} else {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.codes.service;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.exception.CodeValueNotFoundException;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
@@ -66,7 +67,7 @@ public class CodeValueReadPlatformServiceImpl implements CodeValueReadPlatformSe
|
||||
|
||||
@Override
|
||||
@Cacheable(value = "code_values", key = "T(org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat(#code+'cv')")
|
||||
public Collection<CodeValueData> retrieveCodeValuesByCode(final String code) {
|
||||
public List<CodeValueData> retrieveCodeValuesByCode(final String code) {
|
||||
|
||||
this.context.authenticatedUser();
|
||||
|
||||
|
||||
+6
-1
@@ -54,6 +54,8 @@ import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToG
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiParameterHelper;
|
||||
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
|
||||
@@ -150,6 +152,7 @@ public class LoanProductsApiResource {
|
||||
private final RateReadService rateReadService;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
|
||||
private final CodeValueReadPlatformService codeValueReadPlatformService;
|
||||
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@@ -425,6 +428,8 @@ public class LoanProductsApiResource {
|
||||
final List<EnumOptionData> creditAllocationAllocationTypes = AllocationType.getValuesAsEnumOptionDataList();
|
||||
final List<StringEnumOptionData> supportedInterestRefundTypesOptions = LoanSupportedInterestRefundTypes
|
||||
.getValuesAsStringEnumOptionDataList();
|
||||
final List<CodeValueData> chargeOffReasonOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS);
|
||||
|
||||
return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions,
|
||||
interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions,
|
||||
@@ -436,7 +441,7 @@ public class LoanProductsApiResource {
|
||||
advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules,
|
||||
advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(),
|
||||
LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes,
|
||||
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions);
|
||||
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffReasonOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user