FINERACT-2421: Rework code value handling in E2E

This commit is contained in:
Adam Saghy
2026-01-16 17:59:14 +01:00
parent 0f73ae1eb7
commit 0891adb267
18 changed files with 328 additions and 148 deletions
@@ -1257,6 +1257,14 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder createCodeValue(final Long codeId, String codeName) {
this.actionName = "CREATE";
this.entityName = "CODEVALUE";
this.entityId = codeId;
this.href = "/codes/name/" + codeName + "/codevalues/template";
return this;
}
public CommandWrapperBuilder updateCodeValue(final Long codeId, final Long codeValueId) {
this.actionName = "UPDATE";
this.entityName = "CODEVALUE";
@@ -1266,6 +1274,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder updateCodeValue(String codeName, Long codeId, Long codeValueId) {
this.actionName = "UPDATE";
this.entityName = "CODEVALUE";
this.entityId = codeId;
this.subentityId = codeValueId;
this.href = "/codes/name/" + codeName + "/codevalues/" + codeValueId;
return this;
}
public CommandWrapperBuilder deleteCodeValue(final Long codeId, final Long codeValueId) {
this.actionName = "DELETE";
this.entityName = "CODEVALUE";
@@ -1275,6 +1292,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder deleteCodeValue(String codeName, Long codeId, Long codeValueId) {
this.actionName = "DELETE";
this.entityName = "CODEVALUE";
this.entityId = codeId;
this.subentityId = codeValueId;
this.href = "/codes/name/" + codeName + "/codevalues/" + codeValueId;
return this;
}
public CommandWrapperBuilder createGLClosure() {
this.actionName = "CREATE";
this.entityName = "GLCLOSURE";
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.codes.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -26,4 +27,6 @@ public interface CodeValueRepository extends JpaRepository<CodeValue, Long>, Jpa
CodeValue findByCodeNameAndId(String codeName, Long id);
CodeValue findByCodeNameAndLabel(String codeName, String label);
List<CodeValue> findByCodeName(String codeName);
}
@@ -27,5 +27,5 @@ public interface CodeReadPlatformService {
CodeData retrieveCode(Long codeId);
CodeData retriveCode(String codeName);
CodeData retrieveCode(String codeName);
}
@@ -46,4 +46,8 @@ public interface CodeValueReadPlatformService {
Collection<CodeValueData> retrieveAllCodeValues(Long codeId);
CodeValueData retrieveCodeValue(Long codeValueId);
List<CodeValueData> retrieveAllCodeValues(String codeName);
CodeValueData retrieveCodeValue(String codeName, Long codeValueId);
}
@@ -0,0 +1,58 @@
/**
* 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.data.codevalue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum CodeNames {
FINANCIAL_INSTRUMENT("financial_instrument"), //
TRANSACTION_TYPE("transaction_type"), //
BANKRUPTCY_TAG("bankruptcy_tag"), //
PENDING_FRAUD_TAG("pending_fraud_tag"), //
PENDING_DECEASED_TAG("pending_deceased_tag"), //
HARDSHIP_TAG("hardship_tag"), //
ACTIVE_DUTY_TAG("active_duty_tag"), //
ADDRESS_TYPE("ADDRESS_TYPE"), //
COUNTRY("COUNTRY"), //
STATE("STATE"), //
CHARGE_OFF("ChargeOffReasons"), //
CUSTOMER_IDENTIFIER("Customer Identifier"), //
GENDER("Gender"), //
CLIENT_TYPE("ClientType"), //
CLIENT_CLASSIFICATION("ClientClassification"), //
FAMILY_MEMBER_RELATIONSHIP("RELATIONSHIP"), //
FAMILY_MEMBER_PROFESSION("PROFESSION"), //
FAMILY_MARITAL_STATUS("MARITAL STATUS"), //
CONSTITUTION("Constitution"), //
LOAN_RESCHEDULE_REASON("LoanRescheduleReason"), //
WRITE_OFF_REASON("WriteOffReasons"), //
BUYDOWN_FEE_TRANSACTION_CLASSIFICATION("buydown_fee_transaction_classification"), //
CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION("capitalized_income_transaction_classification"); //
private final String value;
@Override
public String toString() {
return value;
}
}
@@ -36,14 +36,26 @@ public class CodeValueResolver {
private final FineractFeignClient fineractClient;
@Cacheable(key = "#codeValue.getName()", value = "codeValuesByName")
@Cacheable(key = "#codeId + '-' + #codeValue", value = "codeValuesByName")
public long resolve(Long codeId, CodeValue codeValue) {
String codeValueName = codeValue.getName();
log.debug("Resolving code value by code id and name [{}]", codeValue);
List<GetCodeValuesDataResponse> codeValuesResponses = ok(() -> fineractClient.codeValues().retrieveAllCodeValues(codeId, Map.of()));
GetCodeValuesDataResponse foundPtr = codeValuesResponses.stream().filter(ptr -> codeValueName.equals(ptr.getName())).findAny()
.orElseThrow(() -> new IllegalArgumentException("Payment type [%s] not found".formatted(codeValueName)));
.orElseThrow(
() -> new IllegalArgumentException("Code Value [%s] not found for Code [%s]".formatted(codeValueName, codeId)));
return foundPtr.getId();
}
@Cacheable(key = "#codeName + '-' + #codeValue", value = "codeValuesByName")
public long resolve(String codeName, String codeValue) {
log.debug("Resolving code value by code id and name [{}]", codeValue);
List<GetCodeValuesDataResponse> codeValuesResponses = ok(
() -> fineractClient.codeValues().retrieveAllCodeValues1(codeName, Map.of()));
GetCodeValuesDataResponse foundPtr = codeValuesResponses.stream().filter(ptr -> codeValue.equals(ptr.getName())).findAny()
.orElseThrow(() -> new IllegalArgumentException("Code Value [%s] not found for Code [%s]".formatted(codeValue, codeName)));
return foundPtr.getId();
}
@@ -23,7 +23,10 @@ public enum DefaultCodeValue implements CodeValue {
// Charge-off reason
FRAUD("Fraud"), //
DELINQUENT("Delinquent"), //
OTHER("Other"), BAD_DEBT("Bad Debt"), FORGIVEN("Forgiven"), TEST("Test"); //
OTHER("Other"), //
BAD_DEBT("Bad Debt"), //
FORGIVEN("Forgiven"), //
TEST("Test"); //
private final String customName;
@@ -79,7 +79,6 @@ import org.apache.fineract.client.models.CommandProcessingResult;
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
import org.apache.fineract.client.models.DisbursementDetail;
import org.apache.fineract.client.models.GetCodeValuesDataResponse;
import org.apache.fineract.client.models.GetCodesResponse;
import org.apache.fineract.client.models.GetLoanProductsChargeOffReasonOptions;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoanProductsResponse;
@@ -105,8 +104,6 @@ import org.apache.fineract.client.models.OldestCOBProcessedLoanDTO;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostAddAndDeleteDisbursementDetailRequest;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCodeValueDataResponse;
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
import org.apache.fineract.client.models.PostLoansDisbursementData;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
@@ -131,6 +128,7 @@ import org.apache.fineract.test.data.LoanTermFrequencyType;
import org.apache.fineract.test.data.RepaymentFrequencyType;
import org.apache.fineract.test.data.TransactionProcessingStrategyCode;
import org.apache.fineract.test.data.TransactionType;
import org.apache.fineract.test.data.codevalue.CodeNames;
import org.apache.fineract.test.data.codevalue.CodeValue;
import org.apache.fineract.test.data.codevalue.CodeValueResolver;
import org.apache.fineract.test.data.codevalue.DefaultCodeValue;
@@ -4879,14 +4877,6 @@ public class LoanStepDef extends AbstractStepDef {
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_RESPONSE, capitalizedIncomeResponse);
}
@And("Admin adds capitalized income with {string} payment type to the loan on {string} with {string} EUR transaction amount and classification: scheduled_payment")
public void adminAddsCapitalizedIncomeToTheLoanOnWithEURTransactionAmountWithClassificationScheduledPayment(
final String transactionPaymentType, final String transactionDate, final String amount) {
final PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = addCapitalizedIncomeToTheLoanOnWithEURTransactionAmountWithClassificationScheduledPayment(
transactionPaymentType, transactionDate, amount);
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_RESPONSE, capitalizedIncomeResponse);
}
@And("Admin adds capitalized income with {string} payment type to the loan on {string} with {string} EUR transaction amount and {string} classification")
public void adminAddsCapitalizedIncomeWithClassification(final String transactionPaymentType, final String transactionDate,
final String amount, final String classificationCodeName) {
@@ -4904,7 +4894,8 @@ public class LoanStepDef extends AbstractStepDef {
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
// Get classification code value
final Long classificationId = getClassificationCodeValueId(classificationCodeName);
final Long classificationId = getClassificationCodeValueId(CodeNames.CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION.getValue(),
classificationCodeName);
final PostLoansLoanIdTransactionsRequest capitalizedIncomeRequest = LoanRequestFactory.defaultCapitalizedIncomeRequest()
.transactionDate(transactionDate).transactionAmount(Double.valueOf(amount)).paymentTypeId(paymentTypeValue)
@@ -5373,24 +5364,16 @@ public class LoanStepDef extends AbstractStepDef {
testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_RESPONSE, buyDownFeesIncomeResponse);
}
@And("Admin adds buy down fee with {string} payment type to the loan on {string} with {string} EUR transaction amount and classification: pending_bankruptcy")
public void adminAddsBuyDownFeesToTheLoanOnWithEURTransactionAmountWithClassification(final String transactionPaymentType,
final String transactionDate, final String amount) {
final PostLoansLoanIdTransactionsResponse buyDownFeesIncomeResponse = addBuyDownFeeToTheLoanOnWithEURTransactionAmountWithClassification(
transactionPaymentType, transactionDate, amount);
testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_RESPONSE, buyDownFeesIncomeResponse);
}
@When("Admin adds buy down fee with {string} payment type to the loan on {string} with {string} EUR transaction amount and {string} classification")
public void adminAddsBuyDownFeeWithClassification(final String transactionPaymentType, final String transactionDate,
final String amount, final String classificationCodeName) {
final String amount, final String classificationCodeValueName) {
final PostLoansLoanIdTransactionsResponse buyDownFeesIncomeResponse = addBuyDownFeeWithClassification(transactionPaymentType,
transactionDate, amount, classificationCodeName);
transactionDate, amount, classificationCodeValueName);
testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_RESPONSE, buyDownFeesIncomeResponse);
}
public PostLoansLoanIdTransactionsResponse addBuyDownFeeWithClassification(final String transactionPaymentType,
final String transactionDate, final String amount, final String classificationCodeName) {
final String transactionDate, final String amount, final String classificationCodeValueName) {
final PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.getLoanId();
@@ -5398,7 +5381,8 @@ public class LoanStepDef extends AbstractStepDef {
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
// Get classification code value
final Long classificationId = getClassificationCodeValueId(classificationCodeName);
final Long classificationId = getClassificationCodeValueId(CodeNames.BUYDOWN_FEE_TRANSACTION_CLASSIFICATION.getValue(),
classificationCodeValueName);
final PostLoansLoanIdTransactionsRequest buyDownFeeRequest = LoanRequestFactory.defaultBuyDownFeeIncomeRequest()
.transactionDate(transactionDate).transactionAmount(Double.valueOf(amount)).paymentTypeId(paymentTypeValue)
@@ -5740,27 +5724,18 @@ public class LoanStepDef extends AbstractStepDef {
.isEqualTo(expectedClassification);
}
private Long getClassificationCodeValueId(String classificationName) {
final GetCodesResponse code = codeHelper.retrieveCodeByName(classificationName);
private Long getClassificationCodeValueId(String codeName, String codeValueName) {
// Check if code value already exists
List<GetCodeValuesDataResponse> existingCodeValues = fineractClient.codeValues().retrieveAllCodeValues(code.getId());
String codeValueName = classificationName + "_value";
List<GetCodeValuesDataResponse> existingCodeValues = fineractClient.codeValues().retrieveAllCodeValues1(codeName);
// Try to find existing code value with the same name
for (GetCodeValuesDataResponse codeValue : existingCodeValues) {
if (codeValueName.equals(codeValue.getName())) {
log.info("Reusing existing code value: {}", codeValueName);
log.debug("Reusing existing code value: {}", codeValueName);
return codeValue.getId();
}
}
// If not found, create a new code value
PostCodeValuesDataRequest codeValueRequest = new PostCodeValuesDataRequest().name(codeValueName).isActive(true).position(1);
PostCodeValueDataResponse response = codeHelper.createCodeValue(code.getId(), codeValueRequest);
return response.getSubResourceId();
throw new IllegalStateException(String.format("Code [%s] with code value [%s] cannot be found", codeName, codeValueName));
}
@And("Loan Amortization Allocation Mapping for {string} transaction created on {string} contains the following data:")
@@ -30,6 +30,7 @@ import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
import org.apache.fineract.client.models.PostCodesRequest;
import org.apache.fineract.client.models.PutCodeValuesDataRequest;
import org.apache.fineract.test.data.codevalue.CodeNames;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@@ -40,44 +41,26 @@ import org.springframework.stereotype.Component;
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep {
public static final String CODE_NAME_FINANCIAL_INSTRUMENT = "financial_instrument";
public static final String CODE_NAME_TRANSACTION_TYPE = "transaction_type";
public static final String CODE_NAME_BANKRUPTCY_TAG = "bankruptcy_tag";
public static final String CODE_NAME_PENDING_FRAUD_TAG = "pending_fraud_tag";
public static final String CODE_NAME_PENDING_DECEASED_TAG = "pending_deceased_tag";
public static final String CODE_NAME_HARDSHIP_TAG = "hardship_tag";
public static final String CODE_NAME_ACTIVE_DUTY_TAG = "active_duty_tag";
public static final Long CODE_VALUE_ADDRESS_TYPE_ID = 29L;
public static final String CODE_VALUE_ADDRESS_TYPE_RESIDENTIAL = "Residential address";
public static final String CODE_VALUE_ADDRESS_TYPE_OFFICE = "Office address";
public static final Long CODE_VALUE_COUNTRY_ID = 28L;
public static final String CODE_VALUE_COUNTRY_GERMANY = "Germany";
public static final Long CODE_VALUE_STATE_ID = 27L;
public static final String CODE_VALUE_STATE_BERLIN = "Berlin";
public static final Long CODE_VALUE_FINANCIAL_INSTRUMENT_ID = 39L;
public static final String CODE_VALUE_FINANCIAL_INSTRUMENT_DEBIT = "debit_card";
public static final String CODE_VALUE_FINANCIAL_INSTRUMENT_CREDIT = "credit_card";
public static final String CODE_VALUE_FINANCIAL_INSTRUMENT_FRAUD = "Fraud";
public static final String CODE_VALUE_FINANCIAL_INSTRUMENT_DELINQUENT = "Delinquent";
public static final String CODE_VALUE_FINANCIAL_INSTRUMENT_OTHER = "Other";
public static final Long CODE_VALUE_TRANSACTION_TYPE_ID = 40L;
public static final String CODE_VALUE_CHARGE_OFF_REASON_FRAUD = "Fraud";
public static final String CODE_VALUE_CHARGE_OFF_REASON_DELINQUENT = "Delinquent";
public static final String CODE_VALUE_CHARGE_OFF_REASON_OTHER = "Other";
public static final String CODE_VALUE_TRANSACTION_TYPE_SCHEDULED_PAYMENT = "scheduled_payment";
public static final Long CODE_VALUE_BANKRUPTCY_TAG_ID = 41L;
public static final String CODE_VALUE_BANKRUPTCY_TAG_PENDING = "pending_bankruptcy";
public static final String CODE_VALUE_BANKRUPTCY_TAG_BANKRUPTCY = "bankruptcy";
public static final Long CODE_VALUE_PENDING_FRAUD_TAG_ID = 42L;
public static final String CODE_VALUE_PENDING_FRAUD_TAG_PENDING = "pending_fraud";
public static final String CODE_VALUE_PENDING_FRAUD_TAG_FRAUD = "fraud";
public static final Long CODE_VALUE_PENDING_DECEASED_TAG_ID = 43L;
public static final String CODE_VALUE_PENDING_DECEASED_TAG_PENDING = "pending_deceased";
public static final String CODE_VALUE_PENDING_DECEASED_TAG_DECEASED = "deceased";
public static final Long CODE_VALUE_HARDSHIP_TAG_ID = 44L;
public static final String CODE_VALUE_HARDSHIP_TAG_ACTIVE = "active";
public static final String CODE_VALUE_HARDSHIP_TAG_INACTIVE = "inactive";
public static final Long CODE_VALUE_ACTIVE_DUTY_TAG_ID = 45L;
public static final String CODE_VALUE_ACTIVE_DUTY_TAG_ACTIVE = "active";
public static final String CODE_VALUE_ACTIVE_DUTY_TAG_INACTIVE = "inactive";
public static final Long CODE_VALUE_CUSTOMER_IDENTIFIERS_ID = 1L;
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_1 = "Passport";
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_2 = "Id";
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_3 = "Drivers License";
@@ -86,37 +69,30 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_6 = "TIN";
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_7 = "ITIN";
public static final String CODE_VALUE_CUSTOMER_IDENTIFIERS_8 = "EIN";
public static final Long CODE_VALUE_GENDER_ID = 4L;
public static final String CODE_VALUE_GENDER_FEMALE = "Female";
public static final String CODE_VALUE_GENDER_MALE = "Male";
public static final Long CODE_VALUE_CLIENT_TYPE_ID = 16L;
public static final String CODE_VALUE_CLIENT_TYPE_CORPORATE = "Corporate";
public static final String CODE_VALUE_CLIENT_TYPE_LEGAL = "Legal";
public static final String CODE_VALUE_CLIENT_TYPE_NON_LEGAL = "Non-legal";
public static final Long CODE_VALUE_CLIENT_CLASSIFICATION_ID = 17L;
public static final String CODE_VALUE_CLIENT_CLASSIFICATION_LAWYER = "Lawyer";
public static final String CODE_VALUE_CLIENT_CLASSIFICATION_DIRECTOR = "Director";
public static final String CODE_VALUE_CLIENT_CLASSIFICATION_NONE = "None";
public static final Long CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_ID = 31L;
public static final String CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_SPOUSE = "Spouse";
public static final String CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_FATHER = "Father";
public static final String CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_MOTHER = "Mother";
public static final String CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_CHILD = "Child";
public static final Long CODE_VALUE_FAMILY_MEMBER_PROFESSION_ID = 32L;
public static final String CODE_VALUE_FAMILY_MEMBER_PROFESSION_EMPLOYEE = "Employee";
public static final String CODE_VALUE_FAMILY_MEMBER_PROFESSION_SELF_EMPLOYED = "Self-Employed";
public static final Long CODE_VALUE_FAMILY_MARITAL_STATUS_ID = 30L;
public static final String CODE_VALUE_FAMILY_MARITAL_STATUS_MARRIED = "Married";
public static final String CODE_VALUE_FAMILY_MARITAL_STATUS_SINGLE = "Single";
public static final String CODE_VALUE_FAMILY_MARITAL_STATUS_WIDOWED = "Widowed";
public static final Long CODE_VALUE_CONSTITUTION_ID = 24L;
public static final String CODE_VALUE_CONSTITUTION_TEST = "Test";
public static final Long CODE_VALUE_RESCHEDULE_REASON_ID = 23L;
public static final String CODE_VALUE_RESCHEDULE_REASON_TEST = "Test";
public static final Long CODE_VALUE_WRITE_OFF_REASON_ID = 26L;
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_1 = "Bad Debt";
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_2 = "Forgiven";
public static final String CODE_VALUE_WRITE_OFF_REASON_TEST_3 = "Test";
public static final String BUYDOWN_FEE_TRANSACTION_CLASSIFICATION_VALUE = "buydown_fee_transaction_classification_value";
public static final String CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION_VALUE = "capitalized_income_transaction_classification_value";
private final FineractFeignClient fineractClient;
@@ -131,61 +107,64 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
List<String> addressNames = new ArrayList<>();
addressNames.add(CODE_VALUE_ADDRESS_TYPE_RESIDENTIAL);
addressNames.add(CODE_VALUE_ADDRESS_TYPE_OFFICE);
createCodeValues(CODE_VALUE_ADDRESS_TYPE_ID, addressNames);
createCodeValues(CodeNames.ADDRESS_TYPE.getValue(), addressNames);
// Country
List<String> countryNames = new ArrayList<>();
countryNames.add(CODE_VALUE_COUNTRY_GERMANY);
createCodeValues(CODE_VALUE_COUNTRY_ID, countryNames);
createCodeValues(CodeNames.COUNTRY.getValue(), countryNames);
// State
List<String> stateNames = new ArrayList<>();
stateNames.add(CODE_VALUE_STATE_BERLIN);
createCodeValues(CODE_VALUE_STATE_ID, stateNames);
createCodeValues(CodeNames.STATE.getValue(), stateNames);
// financial instrument
List<String> financialInstrumentNames = new ArrayList<>();
financialInstrumentNames.add(CODE_VALUE_FINANCIAL_INSTRUMENT_DEBIT);
financialInstrumentNames.add(CODE_VALUE_FINANCIAL_INSTRUMENT_CREDIT);
financialInstrumentNames.add(CODE_VALUE_FINANCIAL_INSTRUMENT_FRAUD);
financialInstrumentNames.add(CODE_VALUE_FINANCIAL_INSTRUMENT_DELINQUENT);
financialInstrumentNames.add(CODE_VALUE_FINANCIAL_INSTRUMENT_OTHER);
createCodeValues(CODE_VALUE_FINANCIAL_INSTRUMENT_ID, financialInstrumentNames);
createCodeValues(CodeNames.FINANCIAL_INSTRUMENT.getValue(), financialInstrumentNames);
List<String> chargeOffReasonNames = new ArrayList<>();
chargeOffReasonNames.add(CODE_VALUE_CHARGE_OFF_REASON_FRAUD);
chargeOffReasonNames.add(CODE_VALUE_CHARGE_OFF_REASON_DELINQUENT);
chargeOffReasonNames.add(CODE_VALUE_CHARGE_OFF_REASON_OTHER);
createCodeValues(CodeNames.CHARGE_OFF.getValue(), chargeOffReasonNames);
// transaction type
List<String> transactionTypeNames = new ArrayList<>();
transactionTypeNames.add(CODE_VALUE_TRANSACTION_TYPE_SCHEDULED_PAYMENT);
createCodeValues(CODE_VALUE_TRANSACTION_TYPE_ID, transactionTypeNames);
createCodeValues(CodeNames.TRANSACTION_TYPE.getValue(), transactionTypeNames);
// bankruptcy tag
List<String> bankruptcyTagNames = new ArrayList<>();
bankruptcyTagNames.add(CODE_VALUE_BANKRUPTCY_TAG_PENDING);
bankruptcyTagNames.add(CODE_VALUE_BANKRUPTCY_TAG_BANKRUPTCY);
createCodeValues(CODE_VALUE_BANKRUPTCY_TAG_ID, bankruptcyTagNames);
createCodeValues(CodeNames.BANKRUPTCY_TAG.getValue(), bankruptcyTagNames);
// pending fraud tag
List<String> pendingFraudTagNames = new ArrayList<>();
pendingFraudTagNames.add(CODE_VALUE_PENDING_FRAUD_TAG_PENDING);
pendingFraudTagNames.add(CODE_VALUE_PENDING_FRAUD_TAG_FRAUD);
createCodeValues(CODE_VALUE_PENDING_FRAUD_TAG_ID, pendingFraudTagNames);
createCodeValues(CodeNames.PENDING_FRAUD_TAG.getValue(), pendingFraudTagNames);
// pending deceased tag
List<String> pendingDeceasedTagNames = new ArrayList<>();
pendingDeceasedTagNames.add(CODE_VALUE_PENDING_DECEASED_TAG_PENDING);
pendingDeceasedTagNames.add(CODE_VALUE_PENDING_DECEASED_TAG_DECEASED);
createCodeValues(CODE_VALUE_PENDING_DECEASED_TAG_ID, pendingDeceasedTagNames);
createCodeValues(CodeNames.PENDING_DECEASED_TAG.getValue(), pendingDeceasedTagNames);
// hardship tag
List<String> hardshipTagNames = new ArrayList<>();
hardshipTagNames.add(CODE_VALUE_HARDSHIP_TAG_ACTIVE);
hardshipTagNames.add(CODE_VALUE_HARDSHIP_TAG_INACTIVE);
createCodeValues(CODE_VALUE_HARDSHIP_TAG_ID, hardshipTagNames);
createCodeValues(CodeNames.HARDSHIP_TAG.getValue(), hardshipTagNames);
// active duty tag
List<String> activeDutyTagNames = new ArrayList<>();
activeDutyTagNames.add(CODE_VALUE_ACTIVE_DUTY_TAG_ACTIVE);
activeDutyTagNames.add(CODE_VALUE_ACTIVE_DUTY_TAG_INACTIVE);
createCodeValues(CODE_VALUE_ACTIVE_DUTY_TAG_ID, activeDutyTagNames);
createCodeValues(CodeNames.ACTIVE_DUTY_TAG.getValue(), activeDutyTagNames);
// customer identifiers put/post
List<String> customerIdentifierNamesPut = new ArrayList<>();
@@ -193,34 +172,34 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
customerIdentifierNamesPut.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_2);
customerIdentifierNamesPut.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_3);
customerIdentifierNamesPut.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_4);
updateCodeValues(CODE_VALUE_CUSTOMER_IDENTIFIERS_ID, customerIdentifierNamesPut);
updateCodeValues(CodeNames.CUSTOMER_IDENTIFIER.getValue(), customerIdentifierNamesPut);
List<String> customerIdentifierNamesPost = new ArrayList<>();
customerIdentifierNamesPost.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_5);
customerIdentifierNamesPost.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_6);
customerIdentifierNamesPost.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_7);
customerIdentifierNamesPost.add(CODE_VALUE_CUSTOMER_IDENTIFIERS_8);
createCodeValues(CODE_VALUE_CUSTOMER_IDENTIFIERS_ID, customerIdentifierNamesPost);
createCodeValues(CodeNames.CUSTOMER_IDENTIFIER.getValue(), customerIdentifierNamesPost);
// gender
List<String> genderNames = new ArrayList<>();
genderNames.add(CODE_VALUE_GENDER_FEMALE);
genderNames.add(CODE_VALUE_GENDER_MALE);
createCodeValues(CODE_VALUE_GENDER_ID, genderNames);
createCodeValues(CodeNames.GENDER.getValue(), genderNames);
// client type
List<String> clientTypeNames = new ArrayList<>();
clientTypeNames.add(CODE_VALUE_CLIENT_TYPE_CORPORATE);
clientTypeNames.add(CODE_VALUE_CLIENT_TYPE_LEGAL);
clientTypeNames.add(CODE_VALUE_CLIENT_TYPE_NON_LEGAL);
createCodeValues(CODE_VALUE_CLIENT_TYPE_ID, clientTypeNames);
createCodeValues(CodeNames.CLIENT_TYPE.getValue(), clientTypeNames);
// client classification
List<String> clientClassificationNames = new ArrayList<>();
clientClassificationNames.add(CODE_VALUE_CLIENT_CLASSIFICATION_LAWYER);
clientClassificationNames.add(CODE_VALUE_CLIENT_CLASSIFICATION_DIRECTOR);
clientClassificationNames.add(CODE_VALUE_CLIENT_CLASSIFICATION_NONE);
createCodeValues(CODE_VALUE_CLIENT_CLASSIFICATION_ID, clientClassificationNames);
createCodeValues(CodeNames.CLIENT_CLASSIFICATION.getValue(), clientClassificationNames);
// add family member - relationship
List<String> familyMemberRelationshipNames = new ArrayList<>();
@@ -228,40 +207,51 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
familyMemberRelationshipNames.add(CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_FATHER);
familyMemberRelationshipNames.add(CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_MOTHER);
familyMemberRelationshipNames.add(CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_CHILD);
createCodeValues(CODE_VALUE_FAMILY_MEMBER_RELATIONSHIP_ID, familyMemberRelationshipNames);
createCodeValues(CodeNames.FAMILY_MEMBER_RELATIONSHIP.getValue(), familyMemberRelationshipNames);
// add family member - profession
List<String> familyMemberProfessionNames = new ArrayList<>();
familyMemberProfessionNames.add(CODE_VALUE_FAMILY_MEMBER_PROFESSION_EMPLOYEE);
familyMemberProfessionNames.add(CODE_VALUE_FAMILY_MEMBER_PROFESSION_SELF_EMPLOYED);
createCodeValues(CODE_VALUE_FAMILY_MEMBER_PROFESSION_ID, familyMemberProfessionNames);
createCodeValues(CodeNames.FAMILY_MEMBER_PROFESSION.getValue(), familyMemberProfessionNames);
// add family member - marital status
List<String> familyMemberMaritalStatusNames = new ArrayList<>();
familyMemberMaritalStatusNames.add(CODE_VALUE_FAMILY_MARITAL_STATUS_MARRIED);
familyMemberMaritalStatusNames.add(CODE_VALUE_FAMILY_MARITAL_STATUS_SINGLE);
familyMemberMaritalStatusNames.add(CODE_VALUE_FAMILY_MARITAL_STATUS_WIDOWED);
createCodeValues(CODE_VALUE_FAMILY_MARITAL_STATUS_ID, familyMemberMaritalStatusNames);
createCodeValues(CodeNames.FAMILY_MARITAL_STATUS.getValue(), familyMemberMaritalStatusNames);
// add constitution (for client creation as Entity)
List<String> constitutionNames = new ArrayList<>();
constitutionNames.add(CODE_VALUE_CONSTITUTION_TEST);
createCodeValues(CODE_VALUE_CONSTITUTION_ID, constitutionNames);
createCodeValues(CodeNames.CONSTITUTION.getValue(), constitutionNames);
// add LoanRescheduleReason
List<String> rescheduleReasonNames = new ArrayList<>();
rescheduleReasonNames.add(CODE_VALUE_RESCHEDULE_REASON_TEST);
createCodeValues(CODE_VALUE_RESCHEDULE_REASON_ID, rescheduleReasonNames);
createCodeValues(CodeNames.LOAN_RESCHEDULE_REASON.getValue(), rescheduleReasonNames);
// add Write-off reasons
List<String> writeOffReasonNames = new ArrayList<>();
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_1);
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_2);
writeOffReasonNames.add(CODE_VALUE_WRITE_OFF_REASON_TEST_3);
createCodeValues(CODE_VALUE_WRITE_OFF_REASON_ID, writeOffReasonNames);
createCodeValues(CodeNames.WRITE_OFF_REASON.getValue(), writeOffReasonNames);
// Add buy down fee transaction classification
List<String> buydownFeeTransactionClassificationName = new ArrayList<>();
buydownFeeTransactionClassificationName.add(BUYDOWN_FEE_TRANSACTION_CLASSIFICATION_VALUE);
createCodeValues(CodeNames.BUYDOWN_FEE_TRANSACTION_CLASSIFICATION.getValue(), buydownFeeTransactionClassificationName);
// Add capitalized income transaction classification
List<String> capitalizedIncomeTransactionClassificationName = new ArrayList<>();
capitalizedIncomeTransactionClassificationName.add(CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION_VALUE);
createCodeValues(CodeNames.CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION.getValue(),
capitalizedIncomeTransactionClassificationName);
}
public void createCodeValues(Long codeId, List<String> codeValueNames) {
public void createCodeValues(String codeName, List<String> codeValueNames) {
codeValueNames.forEach(name -> {
Integer position = codeValueNames.indexOf(name);
PostCodeValuesDataRequest postCodeValuesDataRequest = new PostCodeValuesDataRequest();
@@ -270,7 +260,7 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
postCodeValuesDataRequest.position(position);
try {
executeVoid(() -> fineractClient.codeValues().createCodeValue(codeId, postCodeValuesDataRequest, Map.of()));
executeVoid(() -> fineractClient.codeValues().createCodeValue1(codeName, postCodeValuesDataRequest, Map.of()));
log.debug("Code value '{}' created successfully", name);
} catch (CallFailedRuntimeException e) {
if (e.getStatus() == 403 && e.getDeveloperMessage() != null && e.getDeveloperMessage().contains("already exists")) {
@@ -282,7 +272,7 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
});
}
public void updateCodeValues(Long codeId, List<String> codeValueNames) {
public void updateCodeValues(String codeName, List<String> codeValueNames) {
codeValueNames.forEach(name -> {
int position = codeValueNames.indexOf(name) + 1;
PutCodeValuesDataRequest putCodeValuesDataRequest = new PutCodeValuesDataRequest();
@@ -290,19 +280,19 @@ public class CodeGlobalInitializerStep implements FineractGlobalInitializerStep
putCodeValuesDataRequest.name(name);
putCodeValuesDataRequest.position(position);
executeVoid(() -> fineractClient.codeValues().updateCodeValue(codeId, (long) position, putCodeValuesDataRequest, Map.of()));
executeVoid(() -> fineractClient.codeValues().updateCodeValue1(codeName, (long) position, putCodeValuesDataRequest, Map.of()));
});
}
private void createCodeNames() {
List<String> codesNameList = new ArrayList<>();
codesNameList.add(CODE_NAME_FINANCIAL_INSTRUMENT);
codesNameList.add(CODE_NAME_TRANSACTION_TYPE);
codesNameList.add(CODE_NAME_BANKRUPTCY_TAG);
codesNameList.add(CODE_NAME_PENDING_FRAUD_TAG);
codesNameList.add(CODE_NAME_PENDING_DECEASED_TAG);
codesNameList.add(CODE_NAME_HARDSHIP_TAG);
codesNameList.add(CODE_NAME_ACTIVE_DUTY_TAG);
codesNameList.add(CodeNames.FINANCIAL_INSTRUMENT.getValue());
codesNameList.add(CodeNames.TRANSACTION_TYPE.getValue());
codesNameList.add(CodeNames.BANKRUPTCY_TAG.getValue());
codesNameList.add(CodeNames.PENDING_FRAUD_TAG.getValue());
codesNameList.add(CodeNames.PENDING_DECEASED_TAG.getValue());
codesNameList.add(CodeNames.HARDSHIP_TAG.getValue());
codesNameList.add(CodeNames.ACTIVE_DUTY_TAG.getValue());
codesNameList.forEach(codeName -> {
try {
@@ -62,6 +62,7 @@ import org.apache.fineract.test.data.OverAppliedCalculationType;
import org.apache.fineract.test.data.PreClosureInterestCalculationRule;
import org.apache.fineract.test.data.RecalculationRestFrequencyType;
import org.apache.fineract.test.data.TransactionProcessingStrategyCode;
import org.apache.fineract.test.data.codevalue.CodeNames;
import org.apache.fineract.test.data.codevalue.CodeValue;
import org.apache.fineract.test.data.codevalue.CodeValueResolver;
import org.apache.fineract.test.data.codevalue.DefaultCodeValue;
@@ -3697,7 +3698,9 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
List<PostClassificationToIncomeAccountMappings> buydownfeeClassificationToIncomeAccountMappings = new ArrayList<>();
PostClassificationToIncomeAccountMappings classificationToIncomeAccountMappings = new PostClassificationToIncomeAccountMappings();
classificationToIncomeAccountMappings.setClassificationCodeValueId(25L);
long classificationId = codeValueResolver.resolve(CodeNames.BUYDOWN_FEE_TRANSACTION_CLASSIFICATION.getValue(),
CodeGlobalInitializerStep.BUYDOWN_FEE_TRANSACTION_CLASSIFICATION_VALUE);
classificationToIncomeAccountMappings.setClassificationCodeValueId(classificationId);
classificationToIncomeAccountMappings.setIncomeAccountId(10L);
buydownfeeClassificationToIncomeAccountMappings.add(classificationToIncomeAccountMappings);
@@ -3736,7 +3739,10 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
List<PostClassificationToIncomeAccountMappings> capitalizedIncomeClassificationToIncomeAccountMappings = new ArrayList<>();
PostClassificationToIncomeAccountMappings classificationToIncomeAccountMappingsCapitalizedIncome = new PostClassificationToIncomeAccountMappings();
classificationToIncomeAccountMappingsCapitalizedIncome.setClassificationCodeValueId(24L);
long capitalizedIncomeClassificationId = codeValueResolver.resolve(
CodeNames.CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION.getValue(),
CodeGlobalInitializerStep.CAPITALIZED_INCOME_TRANSACTION_CLASSIFICATION_VALUE);
classificationToIncomeAccountMappingsCapitalizedIncome.setClassificationCodeValueId(capitalizedIncomeClassificationId);
classificationToIncomeAccountMappingsCapitalizedIncome.setIncomeAccountId(15L);
capitalizedIncomeClassificationToIncomeAccountMappings.add(classificationToIncomeAccountMappingsCapitalizedIncome);
@@ -3232,7 +3232,7 @@ Feature:Feature: Buy Down Fees
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan status will be "ACTIVE"
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "buydown_fee_transaction_classification" classification
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "buydown_fee_transaction_classification_value" classification
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -3841,7 +3841,7 @@ Feature:Feature: Buy Down Fees
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES_CLASSIFICATION_INCOME_MAP | 01 January 2024 | 350 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "350" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and classification: pending_bankruptcy
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "buydown_fee_transaction_classification_value" classification
And Admin sets the business date to "02 January 2024"
And Admin runs inline COB job for Loan
Then Loan Transactions tab has a "BUY_DOWN_FEE" transaction with date "01 January 2024" which has the following Journal entries:
@@ -3868,7 +3868,7 @@ Feature:Feature: Buy Down Fees
And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount
And Admin sets the business date to "02 January 2024"
And Admin runs inline COB job for Loan
And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "02 January 2024" with "20" EUR transaction amount and classification: pending_bankruptcy
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "02 January 2024" with "20" EUR transaction amount and "buydown_fee_transaction_classification_value" classification
And Admin sets the business date to "03 January 2024"
And Admin runs inline COB job for Loan
Then Loan Transactions tab has a "BUY_DOWN_FEE" transaction with date "01 January 2024" which has the following Journal entries:
@@ -3902,7 +3902,7 @@ Feature:Feature: Buy Down Fees
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan status will be "ACTIVE"
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and classification: pending_bankruptcy
When Admin adds buy down fee with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "buydown_fee_transaction_classification_value" classification
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -3920,7 +3920,7 @@ Feature:Feature: Buy Down Fees
| Type | Account code | Account name | Debit | Credit |
| EXPENSE | 450280 | Buy Down Expense | 50.0 | |
| LIABILITY | 145024 | Deferred Capitalized Income | | 50.0 |
And Loan Transactions tab has a "Buy Down Fee" transaction with date "01 January 2024" which has classification code value "pending_bankruptcy"
And Loan Transactions tab has a "Buy Down Fee" transaction with date "01 January 2024" which has classification code value "buydown_fee_transaction_classification_value"
And Buy down fee contains the following data:
| Date | Fee Amount | Amortized Amount | Not Yet Amortized Amount | Adjusted Amount | Charged Off Amount |
| 01 January 2024 | 50.0 | 0.0 | 50.0 | 0.0 | 0.0 |
@@ -7673,7 +7673,7 @@ Feature: Capitalized Income
And Admin successfully disburse the loan on "1 January 2024" with "900" EUR transaction amount
Then Loan status will be "ACTIVE"
When Admin sets the business date to "2 January 2024"
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount and "capitalized_income_transaction_classification" classification
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount and "capitalized_income_transaction_classification_value" classification
Then Loan Repayment schedule has 2 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 900.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -8277,7 +8277,7 @@ Feature: Capitalized Income
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@TestRailId:C4096
Scenario: Verify GL entries for Capitalized Income Amortization - UC2: Amortization for Capitalized Income with classification rule: pending_bankruptcy
Scenario: Verify GL entries for Capitalized Income Amortization - UC2: Amortization for Capitalized Income with classification rule: capitalized_income_transaction_classification_value
When Admin sets the business date to "01 January 2024"
And Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
@@ -8285,7 +8285,7 @@ Feature: Capitalized Income
| LP2_PROGRESSIVE_ADV_PMNT_ALLOCATION_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC_CLASSIFICATION_INCOME_MAP | 01 January 2024 | 350 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "350" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and classification: scheduled_payment
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "capitalized_income_transaction_classification_value" classification
When Admin sets the business date to "02 January 2024"
And Admin runs inline COB job for Loan
Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "01 January 2024" which has the following Journal entries:
@@ -8301,7 +8301,7 @@ Feature: Capitalized Income
Then Loan is closed with zero outstanding balance and it's all installments have obligations met
@TestRailId:C4097
Scenario: Verify GL entries for Capitalized Income Amortization - UC3: Amortization for Capitalized Incomes with NO classification and with classification rule: pending_bankruptcy
Scenario: Verify GL entries for Capitalized Income Amortization - UC3: Amortization for Capitalized Incomes with NO classification and with classification rule: capitalized_income_transaction_classification_value
When Admin sets the business date to "01 January 2024"
And Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
@@ -8312,7 +8312,7 @@ Feature: Capitalized Income
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount
When Admin sets the business date to "02 January 2024"
And Admin runs inline COB job for Loan
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "02 January 2024" with "20" EUR transaction amount and classification: scheduled_payment
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "02 January 2024" with "20" EUR transaction amount and "capitalized_income_transaction_classification_value" classification
When Admin sets the business date to "03 January 2024"
And Admin runs inline COB job for Loan
Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "01 January 2024" which has the following Journal entries:
@@ -8346,7 +8346,7 @@ Feature: Capitalized Income
And Admin successfully approves the loan on "01 January 2024" with "200" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan status will be "ACTIVE"
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and classification: scheduled_payment
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "01 January 2024" with "50" EUR transaction amount and "capitalized_income_transaction_classification_value" classification
Then Loan Repayment schedule has 3 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -8365,7 +8365,7 @@ Feature: Capitalized Income
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 50.0 | |
| LIABILITY | 145024 | Deferred Capitalized Income | | 50.0 |
And Loan Transactions tab has a "Capitalized Income" transaction with date "01 January 2024" which has classification code value "scheduled_payment"
And Loan Transactions tab has a "Capitalized Income" transaction with date "01 January 2024" which has classification code value "capitalized_income_transaction_classification_value"
Then LoanCapitalizedIncomeTransactionCreatedBusinessEvent is raised on "01 January 2024"
And Admin adds capitalized income adjustment with "AUTOPAY" payment type to the loan on "01 January 2024" with "25" EUR transaction amount
Then Loan Transactions tab has the following data:
@@ -187,26 +187,22 @@ Feature: LoanProduct
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 5 options, with the following data:
Then Loan Product Charge-Off reasons options from loan product template have 3 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 |
| Fraud | | 2 | true | false |
| Delinquent | | 3 | true | false |
| Other | | 4 | true | false |
| Fraud | | 0 | true | false |
| Delinquent | | 1 | true | false |
| Other | | 2 | true | false |
@TestRailId:C3482
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 5 options, with the following data:
Then Loan Product "LP1" Charge-Off reasons options from specific loan product have 3 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 |
| Fraud | | 2 | true | false |
| Delinquent | | 3 | true | false |
| Other | | 4 | true | false |
| Fraud | | 0 | true | false |
| Delinquent | | 1 | true | false |
| Other | | 2 | true | false |
@TestRailId:C3587
Scenario: As a user I would like to verify interestRecognitionOnDisbursementDate=false flag in loan product response
@@ -41,6 +41,7 @@ import jakarta.ws.rs.core.UriInfo;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
@@ -49,6 +50,7 @@ import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformS
import org.apache.fineract.infrastructure.codes.CodeConstants.CodevalueJSONinputParams;
import org.apache.fineract.infrastructure.codes.data.CodeData;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeReadPlatformService;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -57,7 +59,7 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.stereotype.Component;
@Path("/v1/codes/{codeId}/codevalues")
@Path("/v1/codes")
@Component
@Tag(name = "Code Values", description = "Code and code values: Codes represent a specific category of data, their code values are a specific instance of that category.\n"
+ "\n"
@@ -76,11 +78,13 @@ public class CodeValuesApiResource {
private final PlatformSecurityContext context;
private final CodeValueReadPlatformService readPlatformService;
private final CodeReadPlatformService codeReadPlatformService;
private final DefaultToApiJsonSerializer<CodeValueData> toApiJsonSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@GET
@Path("{codeId}/codevalues")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Code Values", description = "Returns the list of Code Values for a given Code\n" + "\n"
@@ -99,7 +103,7 @@ public class CodeValuesApiResource {
}
@GET
@Path("{codeValueId}")
@Path("{codeId}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Code description", description = "Returns the details of a Code Value\n" + "\n" + "Example Requests:\n"
@@ -119,6 +123,7 @@ public class CodeValuesApiResource {
}
@POST
@Path("{codeId}/codevalues")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a Code description", description = "")
@@ -137,7 +142,7 @@ public class CodeValuesApiResource {
}
@PUT
@Path("{codeValueId}")
@Path("{codeId}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Code description", description = "Updates the details of a code description.")
@@ -157,7 +162,7 @@ public class CodeValuesApiResource {
}
@DELETE
@Path("{codeValueId}")
@Path("{codeId}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Code description", description = "Deletes a code description")
@@ -172,4 +177,89 @@ public class CodeValuesApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@GET
@Path("name/{codeName}/codevalues")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List Code Values", description = "Returns the list of Code Values for a given Code\n" + "\n"
+ "Example Requests:\n" + "\n" + "codes/1/codevalues", parameters = @Parameter(name = "codeId", description = "co"))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "A List of code values for a given code", content = @Content(array = @ArraySchema(schema = @Schema(implementation = CodeValuesApiResourceSwagger.GetCodeValuesDataResponse.class)))) })
public List<CodeValueData> retrieveAllCodeValues(@Context final UriInfo uriInfo,
@PathParam("codeName") @Parameter(description = "codeName") final String codeName) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.readPlatformService.retrieveAllCodeValues(codeName);
}
@GET
@Path("name/{codeName}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Code description", description = "Returns the details of a Code Value\n" + "\n" + "Example Requests:\n"
+ "\n" + "codes/name/ADDRESS_TYPE/codevalues/1")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.GetCodeValuesDataResponse.class))) })
public CodeValueData retrieveCodeValue(@Context final UriInfo uriInfo,
@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@PathParam("codeValueId") @Parameter(description = "codeValueId") final Long codeValueId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return this.readPlatformService.retrieveCodeValue(codeName, codeValueId);
}
@POST
@Path("name/{codeName}/codevalues")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a Code description", description = "")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.PostCodeValuesDataRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.PostCodeValueDataResponse.class))) })
public CommandProcessingResult createCodeValue(@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
CodeData code = codeReadPlatformService.retrieveCode(codeName);
final CommandWrapper commandRequest = new CommandWrapperBuilder().createCodeValue(code.getId(), codeName)
.withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@PUT
@Path("name/{codeName}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a Code description", description = "Updates the details of a code description.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.PutCodeValuesDataRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.PutCodeValueDataResponse.class))) })
public CommandProcessingResult updateCodeValue(@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@PathParam("codeValueId") @Parameter(description = "codeValueId") final Long codeValueId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
CodeData code = codeReadPlatformService.retrieveCode(codeName);
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateCodeValue(codeName, code.getId(), codeValueId)
.withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
@DELETE
@Path("name/{codeName}/codevalues/{codeValueId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a Code description", description = "Deletes a code description")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CodeValuesApiResourceSwagger.DeleteCodeValueDataResponse.class))) })
public CommandProcessingResult deleteCodeValue(@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@PathParam("codeValueId") @Parameter(description = "codeValueId") final Long codeValueId) {
CodeData code = codeReadPlatformService.retrieveCode(codeName);
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteCodeValue(codeName, code.getId(), codeValueId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
}
@@ -134,7 +134,7 @@ public class CodesApiResource {
public String retrieveCodeByName(@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@Context final UriInfo uriInfo) {
final CodeData code = this.readPlatformService.retriveCode(codeName);
final CodeData code = this.readPlatformService.retrieveCode(codeName);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, code, RESPONSE_DATA_PARAMETERS);
@@ -86,7 +86,7 @@ public class CodeReadPlatformServiceImpl implements CodeReadPlatformService {
}
@Override
public CodeData retriveCode(final String codeName) {
public CodeData retrieveCode(final String codeName) {
try {
this.context.authenticatedUser();
@@ -22,10 +22,12 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.codes.exception.CodeValueNotFoundException;
import org.apache.fineract.infrastructure.codes.mapper.CodeValueMapper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -33,16 +35,13 @@ import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CodeValueReadPlatformServiceImpl implements CodeValueReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
@Autowired
public CodeValueReadPlatformServiceImpl(final PlatformSecurityContext context, final JdbcTemplate jdbcTemplate) {
this.context = context;
this.jdbcTemplate = jdbcTemplate;
}
private final CodeValueRepository codeValueRepository;
private final CodeValueMapper codeValueMapper;
private static final class CodeValueDataMapper implements RowMapper<CodeValueData> {
@@ -105,4 +104,22 @@ public class CodeValueReadPlatformServiceImpl implements CodeValueReadPlatformSe
}
}
@Override
public List<CodeValueData> retrieveAllCodeValues(String codeName) {
// TODO: review whether it is needed.
this.context.authenticatedUser();
return codeValueMapper.map(codeValueRepository.findByCodeName(codeName));
}
@Override
public CodeValueData retrieveCodeValue(String codeName, Long codeValueId) {
// TODO: review whether it is needed.
this.context.authenticatedUser();
CodeValueData codeValueData = codeValueMapper.map(codeValueRepository.findByCodeNameAndId(codeName, codeValueId));
if (codeValueData == null) {
throw new CodeValueNotFoundException(codeName, codeValueId);
}
return codeValueData;
}
}
@@ -689,7 +689,7 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
if (StringUtils.isNotBlank(code)) {
if (isConstraintApproach) {
codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId());
codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retrieveCode(code).getId());
String fkName = "fk_" + dataTableNameAlias + "_" + name;
constrainBuilder.append(", CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (")
.append(sqlGenerator.escape(name)).append(") ").append(REFERENCES_CLAUSE)
@@ -753,7 +753,7 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
if (StringUtils.isNotBlank(code)) {
if (isConstraintApproach) {
String fkName = "fk_" + dataTableNameAlias + "_" + name;
codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retriveCode(code).getId());
codeMappings.put(dataTableNameAlias + "_" + name, this.codeReadPlatformService.retrieveCode(code).getId());
constrainBuilder.append(",ADD CONSTRAINT ").append(sqlGenerator.escape(fkName)).append(" ").append("FOREIGN KEY (")
.append(sqlGenerator.escape(name)).append(") ").append(REFERENCES_CLAUSE)
.append(sqlGenerator.escape(CODE_VALUES_TABLE)).append(" (").append(TABLE_FIELD_ID).append(")");
@@ -823,7 +823,7 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
}
}
if (newCode != null) {
codeMappings.put(dataTableNameAlias + "_" + newName, this.codeReadPlatformService.retriveCode(newCode).getId());
codeMappings.put(dataTableNameAlias + "_" + newName, this.codeReadPlatformService.retrieveCode(newCode).getId());
if (code == null || !StringUtils.equalsIgnoreCase(oldName, newName)) {
constrainBuilder.append(", ADD CONSTRAINT ").append(sqlGenerator.escape(newFkName)).append(" ")
.append("FOREIGN KEY (").append(sqlGenerator.escape(newName)).append(") ").append(REFERENCES_CLAUSE)