FINERACT-2347: Introduce classification field on loan transactions

This commit is contained in:
Jose Alberto Hernandez
2025-09-03 11:43:06 +02:00
committed by Adam Saghy
parent 9ba17bf79a
commit 65538495aa
27 changed files with 442 additions and 21 deletions
@@ -263,6 +263,14 @@
"null",
"boolean"
]
},
{
"default": null,
"name": "classification",
"type": [
"null",
"org.apache.fineract.avro.generic.v1.CodeValueDataV1"
]
}
]
}
@@ -60,4 +60,8 @@ public class CodeHelper {
return codesApi.retrieveCodes().execute().body().stream().filter(r -> name.equals(r.getName())).findAny()
.orElseThrow(() -> new IllegalArgumentException("Code with name " + name + " has not been found"));
}
public Response<PostCodeValueDataResponse> createCodeValue(Long codeId, PostCodeValuesDataRequest request) throws IOException {
return codeValuesApi.createCodeValue(codeId, request).execute();
}
}
@@ -67,6 +67,8 @@ import org.apache.fineract.client.models.BuyDownFeeAmortizationDetails;
import org.apache.fineract.client.models.CapitalizedIncomeDetails;
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;
@@ -90,6 +92,8 @@ 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;
@@ -108,6 +112,7 @@ import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountResp
import org.apache.fineract.client.models.PutLoansLoanIdRequest;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.services.BusinessDateManagementApi;
import org.apache.fineract.client.services.CodeValuesApi;
import org.apache.fineract.client.services.LoanBuyDownFeesApi;
import org.apache.fineract.client.services.LoanCapitalizedIncomeApi;
import org.apache.fineract.client.services.LoanCobCatchUpApi;
@@ -236,6 +241,9 @@ public class LoanStepDef extends AbstractStepDef {
@Autowired
private CodeHelper codeHelper;
@Autowired
private CodeValuesApi codeValuesApi;
@Autowired
private LoanInterestPauseApi loanInterestPauseApi;
@@ -4771,6 +4779,35 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(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) throws IOException {
final Response<PostLoansLoanIdTransactionsResponse> capitalizedIncomeResponse = addCapitalizedIncomeWithClassification(
transactionPaymentType, transactionDate, amount, classificationCodeName);
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_RESPONSE, capitalizedIncomeResponse);
ErrorHelper.checkSuccessfulApiCall(capitalizedIncomeResponse);
}
public Response<PostLoansLoanIdTransactionsResponse> addCapitalizedIncomeWithClassification(final String transactionPaymentType,
final String transactionDate, final String amount, final String classificationCodeName) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final DefaultPaymentType paymentType = DefaultPaymentType.valueOf(transactionPaymentType);
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
// Get classification code value
final Long classificationId = getClassificationCodeValueId(classificationCodeName);
final PostLoansLoanIdTransactionsRequest capitalizedIncomeRequest = LoanRequestFactory.defaultCapitalizedIncomeRequest()
.transactionDate(transactionDate).transactionAmount(Double.valueOf(amount)).paymentTypeId(paymentTypeValue)
.externalId("EXT-CAP-INC-" + UUID.randomUUID()).classificationId(classificationId);
final Response<PostLoansLoanIdTransactionsResponse> capitalizedIncomeResponse = loanTransactionsApi
.executeLoanTransaction(loanId, capitalizedIncomeRequest, "capitalizedIncome").execute();
return capitalizedIncomeResponse;
}
public Response<PostLoansLoanIdTransactionsResponse> adjustCapitalizedIncome(final String transactionPaymentType,
final String transactionDate, final String amount, final Long transactionId) throws IOException {
@@ -5167,6 +5204,35 @@ public class LoanStepDef extends AbstractStepDef {
ErrorHelper.checkSuccessfulApiCall(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) throws IOException {
final Response<PostLoansLoanIdTransactionsResponse> buyDownFeesIncomeResponse = addBuyDownFeeWithClassification(
transactionPaymentType, transactionDate, amount, classificationCodeName);
testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_RESPONSE, buyDownFeesIncomeResponse);
ErrorHelper.checkSuccessfulApiCall(buyDownFeesIncomeResponse);
}
public Response<PostLoansLoanIdTransactionsResponse> addBuyDownFeeWithClassification(final String transactionPaymentType,
final String transactionDate, final String amount, final String classificationCodeName) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final DefaultPaymentType paymentType = DefaultPaymentType.valueOf(transactionPaymentType);
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
// Get classification code value
final Long classificationId = getClassificationCodeValueId(classificationCodeName);
final PostLoansLoanIdTransactionsRequest buyDownFeeRequest = LoanRequestFactory.defaultBuyDownFeeIncomeRequest()
.transactionDate(transactionDate).transactionAmount(Double.valueOf(amount)).paymentTypeId(paymentTypeValue)
.externalId("EXT-BUY-DOWN-FEE" + UUID.randomUUID()).classificationId(classificationId);
final Response<PostLoansLoanIdTransactionsResponse> buyDownFeeResponse = loanTransactionsApi
.executeLoanTransaction(loanId, buyDownFeeRequest, "buyDownFee").execute();
return buyDownFeeResponse;
}
@And("Admin adds buy down fee adjustment with {string} payment type to the loan on {string} with {string} EUR transaction amount")
public void adminAddsBuyDownFeesAdjustmentToTheLoan(final String transactionPaymentType, final String transactionDate,
final String amount) throws IOException {
@@ -5553,4 +5619,50 @@ public class LoanStepDef extends AbstractStepDef {
eventAssertion.assertEventRaised(LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.class,
buyDownFeeAmortizationAdjustmentTransactionId);
}
@And("Loan Transactions tab has a {string} transaction with date {string} which has classification code value {string}")
public void loanTransactionHasClassification(String transactionType, String expectedDate, String expectedClassification)
throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId();
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
GetLoansLoanIdTransactions transaction = transactions.stream()
.filter(t -> transactionType.equals(t.getType().getValue()) && expectedDate.equals(FORMATTER.format(t.getDate())))
.findFirst().orElseThrow(
() -> new IllegalStateException(String.format("No %s transaction found on %s", transactionType, expectedDate)));
// Get detailed transaction information including classification
Response<GetLoansLoanIdTransactionsTransactionIdResponse> transactionDetailsResponse = loanTransactionsApi
.retrieveTransaction(loanId, transaction.getId(), null).execute();
ErrorHelper.checkSuccessfulApiCall(transactionDetailsResponse);
GetLoansLoanIdTransactionsTransactionIdResponse transactionDetails = transactionDetailsResponse.body();
assertThat(transactionDetails.getClassification()).as(String.format("%s transaction should have classification", transactionType))
.isNotNull();
assertThat(transactionDetails.getClassification().getName()).as("Classification name should match expected value")
.isEqualTo(expectedClassification);
}
private Long getClassificationCodeValueId(String classificationName) throws IOException {
final GetCodesResponse code = codeHelper.retrieveCodeByName(classificationName);
// Delete all existing code values for this classification
List<GetCodeValuesDataResponse> existingCodeValues = codeValuesApi.retrieveAllCodeValues(code.getId()).execute().body();
for (GetCodeValuesDataResponse codeValue : existingCodeValues) {
codeValuesApi.deleteCodeValue(code.getId(), codeValue.getId()).execute();
}
// Create a new code value under the classification code
PostCodeValuesDataRequest codeValueRequest = new PostCodeValuesDataRequest().name(classificationName + "_value").isActive(true)
.position(1);
Response<PostCodeValueDataResponse> response = codeHelper.createCodeValue(code.getId(), codeValueRequest);
ErrorHelper.checkSuccessfulApiCall(response);
return response.body().getSubResourceId();
}
}
@@ -3204,3 +3204,39 @@ Feature:Feature: Buy Down Fees
| Date | Fee Amount | Amortized Amount | Not Yet Amortized Amount | Adjusted Amount | Charged Off Amount |
| 01 January 2024 | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 |
And LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent is created on "01 April 2024"
@TestRailId:C4004
Scenario: Verify buy down fee transaction creation with classification field set
When Admin sets the business date to "01 January 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES | 01 January 2024 | 100 | 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 "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
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 | | | |
| 1 | 31 | 01 February 2024 | | 66.86 | 33.14 | 0.58 | 0.0 | 0.0 | 33.72 | 0.0 | 0.0 | 0.0 | 33.72 |
| 2 | 29 | 01 March 2024 | | 33.53 | 33.33 | 0.39 | 0.0 | 0.0 | 33.72 | 0.0 | 0.0 | 0.0 | 33.72 |
| 3 | 31 | 01 April 2024 | | 0.0 | 33.53 | 0.2 | 0.0 | 0.0 | 33.73 | 0.0 | 0.0 | 0.0 | 33.73 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 1.17 | 0.0 | 0.0 | 101.17 | 0.0 | 0.0 | 0.0 | 101.17 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
| 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
And Loan Transactions tab has a "BUY_DOWN_FEE" transaction with date "01 January 2024" which has the following Journal entries:
| 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 "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 |
And LoanBuyDownFeeTransactionCreatedBusinessEvent is created on "01 January 2024"
And Admin adds buy down fee adjustment with "AUTOPAY" payment type to the loan on "01 January 2024" with "25" EUR transaction amount
And Loan Transactions tab has a "Buy Down Fee Adjustment" transaction with date "01 January 2024" which has classification code value "buydown_fee_transaction_classification_value"
@@ -7529,3 +7529,40 @@ Feature: Capitalized Income
When Admin successfully disburse the loan on "2 January 2024" with "300" EUR transaction amount
# Available amount = 1200 - 900 - 300 - 0 = 0
Then Loan's available disbursement amount is "0.0"
@TestRailId:C4005
Scenario: Verify capitalized income transaction creation with classification field set
When Admin sets the business date to "1 January 2024"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME | 1 January 2024 | 1000.0 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 30 | DAYS | 30 | DAYS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "1 January 2024" with "1000" amount and expected disbursement date on "1 January 2024"
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
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 | | | |
| 1 | 0 | 01 January 2024 | 01 January 2024 | 675.0 | 225.0 | 0.0 | 0.0 | 0.0 | 225.0 | 225.0 | 0.0 | 0.0 | 0.0 |
| | | 02 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
| 2 | 30 | 31 January 2024 | | 0.0 | 775.0 | 0.0 | 0.0 | 0.0 | 775.0 | 0.0 | 0.0 | 0.0 | 775.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 1000.0 | 0.0 | 0.0 | 0.0 | 1000.0 | 225.0 | 0.0 | 0.0 | 775.0 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
| 01 January 2024 | Down Payment | 225.0 | 225.0 | 0.0 | 0.0 | 0.0 | 675.0 | false |
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 775.0 | false |
And Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "02 January 2024" which has the following Journal entries:
| Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 100.0 | |
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
And Loan Transactions tab has a "Capitalized Income" transaction with date "02 January 2024" which has classification code value "capitalized_income_transaction_classification_value"
And Deferred Capitalized Income contains the following data:
| Amount | Amortized Amount | Unrecognized Amount | Adjusted Amount | Charged Off Amount |
| 100.0 | 0.0 | 100.0 | 0.0 | 0.0 |
And Admin adds capitalized income adjustment with "AUTOPAY" payment type to the loan on "02 January 2024" with "50" EUR transaction amount
And Loan Transactions tab has a "Capitalized Income Adjustment" transaction with date "02 January 2024" which has classification code value "capitalized_income_transaction_classification_value"
@@ -60,4 +60,8 @@ public interface LoanTransactionApiConstants {
buyDownFeeAmortization, //
buyDownFeeAmortizationAdjustment, //
}
String TRANSACTION_CLASSIFICATIONID_PARAMNAME = "classificationId";
String CAPITALIZED_INCOME_CLASSIFICATION_CODE = "capitalized_income_transaction_classification";
String BUY_DOWN_FEE_CLASSIFICATION_CODE = "buydown_fee_transaction_classification";
}
@@ -32,6 +32,20 @@ final class LoanTransactionsApiResourceSwagger {
private LoanTransactionsApiResourceSwagger() {}
static final class GetCodeValuesDataResponse {
private GetCodeValuesDataResponse() {}
@Schema(example = "1")
public Long id;
@Schema(example = "Passport")
public String name;
@Schema(example = "Passport information")
public String description;
@Schema(example = "0")
public Integer position;
}
@Schema(description = "GetLoansLoanIdTransactionsTemplateResponse")
public static final class GetLoansLoanIdTransactionsTemplateResponse {
@@ -110,6 +124,8 @@ final class LoanTransactionsApiResourceSwagger {
public List<GetPaymentTypeOptions> paymentTypeOptions;
@Schema(example = "200.000000")
public Double netDisbursalAmount;
public List<GetCodeValuesDataResponse> classificationOptions;
}
public static final class GetLoanCurrency {
@@ -263,6 +279,7 @@ final class LoanTransactionsApiResourceSwagger {
public Set<GetLoanTransactionRelation> transactionRelations;
public Set<GetLoansLoanIdLoanChargePaidByData> loanChargePaidByList;
public PaymentDetailData paymentDetailData;
public GetCodeValuesDataResponse classification;
static final class PaymentDetailData {
@@ -354,6 +371,8 @@ final class LoanTransactionsApiResourceSwagger {
// command=reAge END
@Schema(description = "Optional. Controls whether Interest Refund transaction should be created for this refund. If not provided, loan product config is used.", example = "false")
public Boolean interestRefundCalculation;
@Schema(example = "1")
public Long classificationId;
}
@Schema(description = "PostLoansLoanIdTransactionsResponse")
@@ -113,6 +113,8 @@ public class LoanTransactionData implements Serializable {
private List<LoanTransactionRelationData> transactionRelations;
private Collection<CodeValueData> chargeOffReasonOptions = null;
private Collection<CodeValueData> classificationOptions = null;
private CodeValueData classification;
public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId,
Integer rowIndex, String locale, String dateFormat) {
@@ -162,10 +164,10 @@ public class LoanTransactionData implements Serializable {
public static LoanTransactionData loanTransactionDataForCreditTemplate(final LoanTransactionEnumData transactionType,
final LocalDate transactionDate, final BigDecimal transactionAmount, final Collection<PaymentTypeData> paymentOptions,
final CurrencyData currency) {
final CurrencyData currency, List<CodeValueData> classificationOptions) {
return builder().type(transactionType).date(transactionDate).amount(transactionAmount).paymentTypeOptions(paymentOptions)
.currency(currency).externalLoanId(ExternalId.empty()).externalId(ExternalId.empty()).reversalExternalId(ExternalId.empty())
.manuallyReversed(false).build();
.manuallyReversed(false).classificationOptions(classificationOptions).build();
}
public static LoanTransactionData loanTransactionDataForDisbursalTemplate(final LoanTransactionEnumData transactionType,
@@ -39,6 +39,7 @@ import java.util.Set;
import java.util.function.Predicate;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -144,6 +145,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
private LoanReAgeParameter loanReAgeParameter;
@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "classification_cv_id")
private CodeValue classification;
protected LoanTransaction() {}
public static LoanTransaction incomePosting(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
@@ -312,6 +318,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
if (LoanTransactionType.REAGE.equals(loanTransaction.getTypeOf())) {
newTransaction.setLoanReAgeParameter(loanTransaction.getLoanReAgeParameter().getCopy(newTransaction));
}
newTransaction.setClassification(loanTransaction.getClassification());
return newTransaction;
}
@@ -33,6 +33,7 @@ public interface LoanTransactionMapper {
@Mapping(target = "loanRepaymentScheduleInstallments", ignore = true)
@Mapping(target = "writeOffReasonOptions", ignore = true)
@Mapping(target = "chargeOffReasonOptions", ignore = true)
@Mapping(target = "classificationOptions", ignore = true)
@Mapping(target = "paymentTypeOptions", ignore = true)
@Mapping(target = "overpaymentPortion", ignore = true)
@Mapping(target = "transfer", ignore = true)
@@ -63,5 +64,6 @@ public interface LoanTransactionMapper {
@Mapping(target = "bankNumber", ignore = true)
@Mapping(target = "accountId", ignore = true)
@Mapping(target = "transactionAmount", ignore = true)
@Mapping(target = "classification", expression = "java(loanTransaction.getClassification() != null ? loanTransaction.getClassification().toData() : null)")
LoanTransactionData mapLoanTransaction(LoanTransaction loanTransaction);
}
@@ -96,4 +96,6 @@ public interface LoanTransactionValidator {
void validateReversalExternalId(DataValidatorBuilder baseDataValidator, JsonElement element);
void validateManualInterestRefundTransaction(String json);
void validateClassificationCodeValue(String codeName, Long transactionClassificationId, DataValidatorBuilder baseDataValidator);
}
@@ -54,4 +54,5 @@
<include relativeToChangelogFile="true" file="parts/1029_add_installment_amount_in_multiples_of_to_loan.xml"/>
<include relativeToChangelogFile="true" file="parts/1030_add_loan_undo_contract_termination_event.xml"/>
<include relativeToChangelogFile="true" file="parts/1031_loan_merchant_buy_down_fee.xml"/>
<include relativeToChangelogFile="true" file="parts/1032_add_classification_to_loan_transaction.xml"/>
</databaseChangeLog>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan_transaction">
<column defaultValueComputed="NULL" name="classification_cv_id" type="BIGINT"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="m_code">
<column name="code_name" value="capitalized_income_transaction_classification"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
<insert tableName="m_code">
<column name="code_name" value="buydown_fee_transaction_classification"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -280,10 +280,11 @@ public class LoanTransactionDataTest {
BigDecimal transactionAmount = new BigDecimal("3000.00");
Collection<PaymentTypeData> paymentOptions = mock(Collection.class);
CurrencyData currency = new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD");
List<CodeValueData> classificationOptions = mock(List.class);
// When
LoanTransactionData result = LoanTransactionData.loanTransactionDataForCreditTemplate(transactionType, transactionDate,
transactionAmount, paymentOptions, currency);
transactionAmount, paymentOptions, currency, classificationOptions);
// Then
assertEquals(transactionType, result.getType());
@@ -295,6 +296,7 @@ public class LoanTransactionDataTest {
assertEquals(ExternalId.empty(), result.getExternalId());
assertEquals(ExternalId.empty(), result.getExternalLoanId());
assertEquals(ExternalId.empty(), result.getReversalExternalId());
assertEquals(classificationOptions, result.getClassificationOptions());
}
@Test
@@ -26,6 +26,7 @@ import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -40,6 +41,7 @@ import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -65,6 +67,7 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
private final ExternalIdFactory externalIdFactory;
private final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository;
@Transactional
@Override
@@ -93,6 +96,9 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
// Add to loan (NO schedule recalculation as per requirements)
loan.addLoanTransaction(buyDownFeeTransaction);
// Add Loan Transaction classification
addClassificationCodeToTransaction(command, LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE, buyDownFeeTransaction);
// Save transaction
loanTransactionRepository.saveAndFlush(buyDownFeeTransaction);
@@ -150,6 +156,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
buyDownFeeAdjustment.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(buyDownFeeAdjustment,
originalBuyDownFee.get(), LoanTransactionRelationTypeEnum.ADJUSTMENT));
// Inherit from the target transaction the classification
buyDownFeeAdjustment.setClassification(originalBuyDownFee.get().getClassification());
// Add transaction to loan
loan.addLoanTransaction(buyDownFeeAdjustment);
@@ -203,4 +211,12 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer
buyDownFeeBalance.setUnrecognizedAmount(buyDownFeeTransaction.getAmount());
loanBuyDownFeeBalanceRepository.saveAndFlush(buyDownFeeBalance);
}
private void addClassificationCodeToTransaction(final JsonCommand command, final String codeName, LoanTransaction loanTransaction) {
final Long transactionClassificationId = command
.longValueOfParameterNamed(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME);
if (transactionClassificationId != null) {
loanTransaction.setClassification(codeValueRepository.findByCodeNameAndId(codeName, transactionClassificationId));
}
}
}
@@ -25,6 +25,7 @@ import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -37,6 +38,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
@@ -66,6 +68,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
private final LoanBalanceService loanBalanceService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final BusinessEventNotifierService businessEventNotifierService;
private final CodeValueRepository codeValueRepository;
@Transactional
@Override
@@ -86,6 +89,9 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
transactionDate, txnExternalId);
// Update loan with capitalized income
loan.addLoanTransaction(capitalizedIncomeTransaction);
// Add Loan Transaction classification
addClassificationCodeToTransaction(command, LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE,
capitalizedIncomeTransaction);
// Recalculate loan transactions
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
// Save and flush (PK is set)
@@ -137,6 +143,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
Money.of(loan.getCurrency(), transactionAmount), paymentDetail, transactionDate, txnExternalId);
capitalizedIncomeAdjustment.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(capitalizedIncomeAdjustment,
capitalizedIncome.get(), LoanTransactionRelationTypeEnum.ADJUSTMENT));
capitalizedIncomeAdjustment.setClassification(capitalizedIncome.get().getClassification());
loan.addLoanTransaction(capitalizedIncomeAdjustment);
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeAdjustment);
LoanTransaction savedCapitalizedIncomeAdjustment = loanTransactionRepository.saveAndFlush(capitalizedIncomeAdjustment);
@@ -205,4 +212,11 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
capitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance);
}
private void addClassificationCodeToTransaction(final JsonCommand command, final String codeName, LoanTransaction loanTransaction) {
final Long transactionClassificationId = command
.longValueOfParameterNamed(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME);
if (transactionClassificationId != null) {
loanTransaction.setClassification(codeValueRepository.findByCodeNameAndId(codeName, transactionClassificationId));
}
}
}
@@ -45,6 +45,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
@@ -149,6 +150,11 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
}
}
final Long transactionClassificationId = fromApiJsonHelper
.extractLongNamed(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME, element);
loanTransactionValidator.validateClassificationCodeValue(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE,
transactionClassificationId, baseDataValidator);
validatePaymentDetails(baseDataValidator, element);
validateNote(baseDataValidator, element);
validateExternalId(baseDataValidator, element);
@@ -276,7 +282,8 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
}
private static final List<String> BUY_DOWN_FEE_TRANSACTION_SUPPORTED_PARAMETERS = List
.of(new String[] { "transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId" });
.of(new String[] { "transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId",
LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME });
@Override
public void validateBuyDownFee(JsonCommand command, Long loanId) {
@@ -313,6 +320,11 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
final Long transactionClassificationId = fromApiJsonHelper
.extractLongNamed(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME, element);
loanTransactionValidator.validateClassificationCodeValue(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE,
transactionClassificationId, baseDataValidator);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@@ -576,9 +588,15 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
loanTransactionValidator.validateManualInterestRefundTransaction(json);
}
@Override
public void validateClassificationCodeValue(final String codeName, final Long transactionClassificationId,
DataValidatorBuilder baseDataValidator) {
loanTransactionValidator.validateClassificationCodeValue(codeName, transactionClassificationId, baseDataValidator);
}
private Set<String> getCapitalizedIncomeParameters() {
return new HashSet<>(
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
return new HashSet<>(Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note",
"externalId", LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME));
}
private Set<String> getCapitalizedIncomeAdjustmentParameters() {
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.loanaccount.starter;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
@@ -57,11 +58,12 @@ public class ProgressiveLoanAccountConfiguration {
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService) {
LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository) {
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine,
businessEventNotifierService);
businessEventNotifierService, codeValueRepository);
}
@Bean
@@ -89,4 +91,5 @@ public class ProgressiveLoanAccountConfiguration {
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) {
return new CapitalizedIncomeBalanceReadServiceImpl(loanRepository, loanCapitalizedIncomeBalanceRepository);
}
}
@@ -123,6 +123,23 @@ public class CodesApiResource {
return this.toApiJsonSerializer.serialize(settings, code, RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("name/{codeName}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a Code", description = "Returns the details of a Code.\n" + "\n" + "Example Requests:\n" + "\n"
+ "codes/1")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = CodesApiResourceSwagger.GetCodesResponse.class))) })
public String retrieveCodeByName(@PathParam("codeName") @Parameter(description = "codeName") final String codeName,
@Context final UriInfo uriInfo) {
final CodeData code = this.readPlatformService.retriveCode(codeName);
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializer.serialize(settings, code, RESPONSE_DATA_PARAMETERS);
}
@PUT
@Path("{codeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -93,7 +93,7 @@ public class LoanTransactionsApiResource {
public static final String CAPITALIZED_INCOME = "capitalizedIncome";
public static final String INTEREST_REFUND_COMMAND_VALUE = "interest-refund";
private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME, "classification"));
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
@@ -35,6 +35,8 @@ import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
@@ -67,6 +69,7 @@ import org.apache.fineract.portfolio.common.service.Validator;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -107,6 +110,7 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
private final LoanDisbursementValidator loanDisbursementValidator;
private final CodeValueRepository codeValueRepository;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
@@ -1120,4 +1124,18 @@ public final class LoanTransactionValidatorImpl implements LoanTransactionValida
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
@Override
public void validateClassificationCodeValue(final String codeName, final Long transactionClassificationId,
DataValidatorBuilder baseDataValidator) {
baseDataValidator.reset().parameter(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME)
.value(transactionClassificationId).ignoreIfNull().positiveAmount();
if (transactionClassificationId != null) {
final CodeValue codeValue = codeValueRepository.findByCodeNameAndId(codeName, transactionClassificationId);
if (codeValue == null) {
baseDataValidator.reset().parameter(LoanTransactionApiConstants.TRANSACTION_CLASSIFICATIONID_PARAMNAME)
.failWithCode("code.value.classification.not.exists", "Code value does not exists in the code " + codeName);
}
}
}
}
@@ -94,6 +94,7 @@ import org.apache.fineract.portfolio.group.data.GroupGeneralData;
import org.apache.fineract.portfolio.group.data.GroupRoleData;
import org.apache.fineract.portfolio.group.service.GroupReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApplicationTimelineData;
@@ -485,6 +486,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
LoanTransactionData loanTransactionData = null;
Collection<PaymentTypeData> paymentOptions = null;
List<CodeValueData> classificationOptions = null;
BigDecimal transactionAmount = BigDecimal.ZERO;
switch (transactionType) {
case CAPITALIZED_INCOME:
@@ -499,15 +501,19 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
: loan.getApprovedPrincipal();
transactionAmount = transactionAmount.subtract(loan.getDisbursedAmount()).subtract(capitalizedIncomeBalance);
paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
classificationOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId));
paymentOptions, retriveLoanCurrencyData(loanId), classificationOptions);
break;
case BUY_DOWN_FEE:
paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
classificationOptions = this.codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE);
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId));
paymentOptions, retriveLoanCurrencyData(loanId), classificationOptions);
break;
case CAPITALIZED_INCOME_ADJUSTMENT:
final LoanCapitalizedIncomeBalance loanCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
@@ -518,7 +524,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
.subtract(MathUtil.nullToZero(loanCapitalizedIncomeBalance.getAmountAdjustment()));
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId));
paymentOptions, retriveLoanCurrencyData(loanId), classificationOptions);
break;
case BUY_DOWN_FEE_ADJUSTMENT:
final LoanBuyDownFeeBalance loanBuyDownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
@@ -528,7 +534,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
: loanBuyDownFeeBalance.getAmount().subtract(MathUtil.nullToZero(loanBuyDownFeeBalance.getAmountAdjustment()));
loanTransactionData = LoanTransactionData.loanTransactionDataForCreditTemplate(
LoanEnumerations.transactionType(transactionType), DateUtils.getBusinessLocalDate(), transactionAmount,
paymentOptions, retriveLoanCurrencyData(loanId));
paymentOptions, retriveLoanCurrencyData(loanId), classificationOptions);
break;
default:
loanTransactionData = LoanTransactionData.templateOnTop(retrieveLoanTransactionTemplate(loanId),
@@ -1667,13 +1673,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " fromtran.transaction_date as fromTransferDate, fromtran.amount as fromTransferAmount,"
+ " fromtran.description as fromTransferDescription, "
+ " totran.id as toTransferId, totran.is_reversed as toTransferReversed, "
+ " totran.transaction_date as toTransferDate, totran.amount as toTransferAmount,"
+ " totran.transaction_date as toTransferDate, totran.amount as toTransferAmount, "
+ " clcv.id as classCodeId, clcv.code_value as classCodeValue, "
+ " totran.description as toTransferDescription from m_loan l join m_loan_transaction tr on tr.loan_id = l.id "
+ " join m_currency rc on rc." + sqlGenerator.escape("code") + " = l.currency_code "
+ " left JOIN m_payment_detail pd ON tr.payment_detail_id = pd.id"
+ " left join m_payment_type pt on pd.payment_type_id = pt.id left join m_office office on office.id=tr.office_id"
+ " left join m_account_transfer_transaction fromtran on fromtran.from_loan_transaction_id = tr.id "
+ " left join m_account_transfer_transaction totran on totran.to_loan_transaction_id = tr.id ";
+ " left join m_account_transfer_transaction totran on totran.to_loan_transaction_id = tr.id "
+ " left join m_code_value clcv on clcv.id = tr.classification_cv_id ";
}
@Override
@@ -1700,6 +1708,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
PaymentDetailData paymentDetailData = null;
final CodeValueData classificationData = CodeValueData.instance(rs.getLong("classCodeId"), rs.getString("classCodeValue"));
final Long paymentTypeId = JdbcSupport.getLong(rs, "paymentType");
if (paymentTypeId != null) {
final String typeName = rs.getString("paymentTypeName");
@@ -1759,7 +1769,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
.overpaymentPortion(overPaymentPortion).unrecognizedIncomePortion(unrecognizedIncomePortion).externalId(externalId)
.transfer(transfer).outstandingLoanBalance(outstandingLoanBalance).submittedOnDate(submittedOnDate)
.manuallyReversed(manuallyReversed).reversalExternalId(reversalExternalId).reversedOnDate(reversedOnDate).loanId(loanId)
.externalLoanId(externalLoanId).build();
.externalLoanId(externalLoanId).classification(classificationData).build();
}
}
@@ -2491,8 +2501,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadPlatformService.retrieveAllPaymentTypes();
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.INTEREST_REFUND);
return LoanTransactionData.loanTransactionDataForCreditTemplate(transactionType, targetTxn.getTransactionDate(),
interestRefundAmount, paymentTypeOptions, loan.getCurrency().toData());
return LoanTransactionData.builder().transactionType(LoanTransactionType.INTEREST_REFUND.name()).type(transactionType)
.date(targetTxn.getTransactionDate()).amount(interestRefundAmount).paymentTypeOptions(paymentTypeOptions)
.currency(loan.getCurrency().toData()).build();
}
@Override
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
import org.apache.fineract.cob.service.LoanAccountLockService;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
@@ -391,10 +392,11 @@ public class LoanAccountConfiguration {
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService) {
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService,
CodeValueRepository codeValueRepository) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory,
loanBuyDownFeeBalanceRepository, businessEventNotifierService);
loanBuyDownFeeBalanceRepository, businessEventNotifierService, codeValueRepository);
}
@Bean
@@ -29,10 +29,14 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.CapitalizedIncomeDetails;
import org.apache.fineract.client.models.GetCodesResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.LoanCapitalizedIncomeData;
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.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
@@ -45,6 +49,7 @@ import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -185,6 +190,11 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
final GetCodesResponse code = codeHelper.retrieveCodeByName(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
final PostCodeValueDataResponse classificationCode = codeHelper.createCodeValue(code.getId(),
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10));
final Long classificationId = classificationCode.getSubResourceId();
runAt("1 April 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
500.0, 7.0, 3, null);
@@ -192,9 +202,15 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
"1 January 2024", 50.0);
"1 January 2024", 50.0, classificationId);
capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
// Validate Loan Transaction classification set value
GetLoansLoanIdTransactionsTransactionIdResponse transactionDetails = loanTransactionHelper.getLoanTransactionDetails(loanId,
capitalizedIncomeIdRef.get());
assertNotNull(transactionDetails.getClassification());
assertEquals(classificationId, transactionDetails.getClassification().getId());
PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustmentResponse = loanTransactionHelper
.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "1 April 2024", 50.0);
assertNotNull(capitalizedIncomeAdjustmentResponse.getLoanId());
@@ -222,6 +238,12 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
journalEntry(49.71, loansReceivableAccount, "CREDIT"), //
journalEntry(0.29, interestReceivableAccount, "CREDIT") //
);
// Validate Loan Transaction classification Inherit
transactionDetails = loanTransactionHelper.getLoanTransactionDetails(loanId,
capitalizedIncomeAdjustmentResponse.getResourceId());
assertNotNull(transactionDetails.getClassification());
assertEquals(classificationId, transactionDetails.getClassification().getId());
});
}
@@ -26,15 +26,20 @@ import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetCodesResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
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.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.TransactionType;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -103,6 +108,9 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
@Test
public void testGetLoanTransactionTemplateForCapitalizedIncomeWithOverAppliedAmount() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final GetCodesResponse code = codeHelper.retrieveCodeByName(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
final PostCodeValueDataResponse classificationCode = codeHelper.createCodeValue(code.getId(),
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10));
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
@@ -127,6 +135,7 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
assertEquals("loanTransactionType." + capitalizedIncomeCommand, transactionTemplate.getType().getCode());
assertEquals(transactionTemplate.getAmount(), 415);
assertThat(transactionTemplate.getPaymentTypeOptions().size() > 0);
assertThat(transactionTemplate.getClassificationOptions().size() > 0);
});
}
@@ -242,6 +251,9 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
@Test
public void testGetLoanTransactionTemplateForBuyDownFee() {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final GetCodesResponse code = codeHelper.retrieveCodeByName(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE);
final PostCodeValueDataResponse classificationCode = codeHelper.createCodeValue(code.getId(),
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("CLASS_", 6)).isActive(true).position(10));
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
@@ -266,6 +278,7 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
assertEquals("loanTransactionType." + buyDownFeeCommand, transactionTemplate.getType().getCode());
assertEquals(transactionTemplate.getAmount(), 0);
assertThat(transactionTemplate.getPaymentTypeOptions().size() > 0);
assertThat(transactionTemplate.getClassificationOptions().size() > 0);
});
}
}
@@ -930,6 +930,12 @@ public class LoanTransactionHelper {
.transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en"));
}
public PostLoansLoanIdTransactionsResponse addCapitalizedIncome(final Long loanId, final String transactionDate, final double amount,
final Long classificationId) {
return addCapitalizedIncome(loanId, new PostLoansLoanIdTransactionsRequest().transactionAmount(amount)
.transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en").classificationId(classificationId));
}
public Response<CommandProcessingResult> createInterestPause(Long loanId, String startDate, String endDate) {
log.info("Creating interest pause for Loan {} from {} to {}", loanId, startDate, endDate);
return Calls.executeU(FineractClientHelper.getFineractClient().loanInterestPauseApi.createInterestPause(loanId,
@@ -388,4 +388,8 @@ public final class CodeHelper {
public List<GetCodesResponse> retrieveCodes() {
return Calls.ok(FineractClientHelper.getFineractClient().codes.retrieveCodes());
}
public GetCodesResponse retrieveCodeByName(final String codeName) {
return Calls.ok(FineractClientHelper.getFineractClient().codes.retrieveCodeByName(codeName));
}
}