diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc index d9df8b18c..2b59fd23a 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanTransactionDataV1.avsc @@ -263,6 +263,14 @@ "null", "boolean" ] + }, + { + "default": null, + "name": "classification", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CodeValueDataV1" + ] } ] } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/CodeHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/CodeHelper.java index 8d6203cc2..d0a75faf7 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/CodeHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/CodeHelper.java @@ -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 createCodeValue(Long codeId, PostCodeValuesDataRequest request) throws IOException { + return codeValuesApi.createCodeValue(codeId, request).execute(); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index 919344083..6273fac69 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -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 capitalizedIncomeResponse = addCapitalizedIncomeWithClassification( + transactionPaymentType, transactionDate, amount, classificationCodeName); + testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_RESPONSE, capitalizedIncomeResponse); + ErrorHelper.checkSuccessfulApiCall(capitalizedIncomeResponse); + } + + public Response addCapitalizedIncomeWithClassification(final String transactionPaymentType, + final String transactionDate, final String amount, final String classificationCodeName) throws IOException { + final Response 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 capitalizedIncomeResponse = loanTransactionsApi + .executeLoanTransaction(loanId, capitalizedIncomeRequest, "capitalizedIncome").execute(); + return capitalizedIncomeResponse; + } + public Response 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 buyDownFeesIncomeResponse = addBuyDownFeeWithClassification( + transactionPaymentType, transactionDate, amount, classificationCodeName); + testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_RESPONSE, buyDownFeesIncomeResponse); + ErrorHelper.checkSuccessfulApiCall(buyDownFeesIncomeResponse); + } + + public Response addBuyDownFeeWithClassification(final String transactionPaymentType, + final String transactionDate, final String amount, final String classificationCodeName) throws IOException { + final Response 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 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 loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + long loanId = loanCreateResponse.body().getLoanId(); + + Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + List 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 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 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 response = codeHelper.createCodeValue(code.getId(), codeValueRequest); + ErrorHelper.checkSuccessfulApiCall(response); + + return response.body().getSubResourceId(); + } } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature index bc2442f80..2c469309b 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature @@ -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" diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature index b7951e395..0350b40e5 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature @@ -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" diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java index 6617def50..258635d95 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java @@ -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"; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java index 8929d7b43..dd0bb22c1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java @@ -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 paymentTypeOptions; @Schema(example = "200.000000") public Double netDisbursalAmount; + + public List classificationOptions; } public static final class GetLoanCurrency { @@ -263,6 +279,7 @@ final class LoanTransactionsApiResourceSwagger { public Set transactionRelations; public Set 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") diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java index 0e669052f..c0a4ab510 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java @@ -113,6 +113,8 @@ public class LoanTransactionData implements Serializable { private List transactionRelations; private Collection chargeOffReasonOptions = null; + private Collection 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 paymentOptions, - final CurrencyData currency) { + final CurrencyData currency, List 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, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 327e491ac..230d3f3dd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -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 + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1032_add_classification_to_loan_transaction.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1032_add_classification_to_loan_transaction.xml new file mode 100644 index 000000000..2b4e8e91b --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1032_add_classification_to_loan_transaction.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java index 9b690341f..4ee6a17f0 100644 --- a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionDataTest.java @@ -280,10 +280,11 @@ public class LoanTransactionDataTest { BigDecimal transactionAmount = new BigDecimal("3000.00"); Collection paymentOptions = mock(Collection.class); CurrencyData currency = new CurrencyData("USD", "US Dollar", 2, 0, "$", "USD"); + List 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 diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java index 2a623ae50..35c850fcd 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeWritePlatformServiceImpl.java @@ -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)); + } + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java index fe4112a1c..431f01b19 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -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)); + } + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java index 51688ae08..085904d2c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanTransactionValidatorImpl.java @@ -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 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 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 getCapitalizedIncomeAdjustmentParameters() { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java index 291b02b8f..eb70e364e 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java @@ -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); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResource.java index 9fc63d868..409bf3bf1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/codes/api/CodesApiResource.java @@ -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 }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java index 22ed1e911..f8c45fcc5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java @@ -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 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"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java index f75e69daf..42ff8fefc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java @@ -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 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); + } + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index f9e609b25..44a6badd4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -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 paymentOptions = null; + List 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 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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index 7a1e2f263..c55a94cfd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -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 diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java index 9c95110d2..e0e1ef1ee 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -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()); }); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java index 7e24d12c5..df505ccc2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionTest.java @@ -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); }); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 39a75fcce..276713ad5 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -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 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, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java index a7951c169..fe063ec80 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/system/CodeHelper.java @@ -388,4 +388,8 @@ public final class CodeHelper { public List retrieveCodes() { return Calls.ok(FineractClientHelper.getFineractClient().codes.retrieveCodes()); } + + public GetCodesResponse retrieveCodeByName(final String codeName) { + return Calls.ok(FineractClientHelper.getFineractClient().codes.retrieveCodeByName(codeName)); + } }