From cd0ecfcda0b831987ca5b73cf431d40b0429f5cd Mon Sep 17 00:00:00 2001 From: mariiaKraievska Date: Thu, 28 Aug 2025 16:59:49 +0300 Subject: [PATCH] FINERACT-2348: Calculate and store Capitalized income / Buydown fee amortization allocation schedule --- .../test/stepdef/loan/LoanStepDef.java | 140 ++++++++ .../features/LoanBuyDownFees.feature | 314 +++++++++++++++++ .../features/LoanCapitalizedIncome.feature | 317 ++++++++++++++++++ ...rtizationAllocationApiResourceSwagger.java | 79 +++++ ...rtizationAllocationBaseTransactionDTO.java | 42 +++ .../AmortizationAllocationMappingDTO.java | 40 +++ .../data/LoanAmortizationAllocationData.java | 62 ++++ .../loanaccount/domain/AmortizationType.java | 34 ++ .../LoanAmortizationAllocationMapping.java | 67 ++++ ...ortizationAllocationMappingRepository.java | 43 +++ .../LoanAmortizationAllocationService.java | 44 +++ .../service/LoanReadPlatformService.java | 2 + .../api/LoanBuyDownFeeApiResource.java | 108 ++++++ .../api/LoanCapitalizedIncomeApiResource.java | 108 ++++++ .../domain/LoanBuyDownFeeBalance.java | 5 + .../domain/LoanCapitalizedIncomeBalance.java | 6 + .../LoanBuyDownFeeBalanceRepository.java | 24 +- ...oanCapitalizedIncomeBalanceRepository.java | 29 +- .../BuyDownFeeReadPlatformServiceImpl.java | 3 +- .../BuyDownFeeWritePlatformServiceImpl.java | 4 +- ...pitalizedIncomeBalanceReadServiceImpl.java | 6 +- ...talizedIncomeWritePlatformServiceImpl.java | 4 +- ...ogressiveLoanTransactionValidatorImpl.java | 6 +- .../fineract-progressive-loan/persistence.xml | 1 + ...LoanAmortizationAllocationServiceImpl.java | 124 +++++++ ...nFeeAmortizationProcessingServiceImpl.java | 167 +++++++-- ...comeAmortizationProcessingServiceImpl.java | 170 ++++++++-- .../service/LoanReadPlatformServiceImpl.java | 23 +- .../adjustment/LoanAdjustmentServiceImpl.java | 12 +- .../starter/LoanAccountConfiguration.java | 23 +- .../db/changelog/tenant/changelog-tenant.xml | 3 + ...e_loan_amortization_allocation_mapping.xml | 107 ++++++ ...ted_and_closed_to_buy_down_fee_balance.xml | 34 ++ ...d_closed_to_capitalized_income_balance.xml | 34 ++ 34 files changed, 2094 insertions(+), 91 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanAmortizationAllocationApiResourceSwagger.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationBaseTransactionDTO.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationMappingDTO.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAmortizationAllocationData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/AmortizationType.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMapping.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0195_create_loan_amortization_allocation_mapping.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml 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 6273fac69..983696b9a 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 @@ -62,6 +62,7 @@ import org.apache.fineract.avro.loan.v1.LoanChargePaidByDataV1; import org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.AmortizationMappingData; import org.apache.fineract.client.models.BusinessDateResponse; import org.apache.fineract.client.models.BuyDownFeeAmortizationDetails; import org.apache.fineract.client.models.CapitalizedIncomeDetails; @@ -78,6 +79,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdDisbursementDetails; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargePaidByData; import org.apache.fineract.client.models.GetLoansLoanIdLoanTermVariations; +import org.apache.fineract.client.models.GetLoansLoanIdLoanTransactionEnumData; import org.apache.fineract.client.models.GetLoansLoanIdLoanTransactionRelation; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; @@ -87,6 +89,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.IsCatchUpRunningDTO; +import org.apache.fineract.client.models.LoanAmortizationAllocationResponse; import org.apache.fineract.client.models.LoanProductChargeData; import org.apache.fineract.client.models.OldestCOBProcessedLoanDTO; import org.apache.fineract.client.models.PaymentAllocationOrder; @@ -4955,6 +4958,41 @@ public class LoanStepDef extends AbstractStepDef { log.debug("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); } + @And("Admin adds capitalized income adjustment of capitalized income transaction made on {string} with {string} payment type to the loan on {string} with {string} EUR transaction amount") + public void adminAddsCapitalizedIncomeAdjustmentToTheLoan(final String originalTransactionDate, final String transactionPaymentType, + final String transactionDate, final String amount) throws IOException { + final Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanResponse.body().getLoanId(); + + // Get current business date to ensure we're not creating backdated transactions + String currentBusinessDate = businessDateHelper.getBusinessDate(); + log.debug("Current business date: {}, Transaction date: {}", currentBusinessDate, transactionDate); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + assert loanDetailsResponse.body() != null; + final List transactions = loanDetailsResponse.body().getTransactions(); + assert transactions != null; + final GetLoansLoanIdTransactions capitalizedIncomeTransaction = transactions.stream().filter(t -> { + assert t.getType() != null; + if (!"Capitalized Income".equals(t.getType().getValue())) { + return false; + } + assert t.getDate() != null; + return FORMATTER.format(t.getDate()).equals(originalTransactionDate); + }).findFirst().orElseThrow(() -> new IllegalStateException("No Capitalized Income transaction found for loan " + loanId)); + + final Response adjustmentResponse = adjustCapitalizedIncome(transactionPaymentType, + transactionDate, amount, capitalizedIncomeTransaction.getId()); + + testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_ADJUSTMENT_RESPONSE, adjustmentResponse); + ErrorHelper.checkSuccessfulApiCall(adjustmentResponse); + + assert adjustmentResponse.body() != null; + log.debug("Capitalized Income Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); + } + @Then("Loan's available disbursement amount is {string}") public void verifyAvailableDisbursementAmount(String expectedAmount) throws IOException { Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); @@ -5256,6 +5294,34 @@ public class LoanStepDef extends AbstractStepDef { log.debug("BuyDown Fee Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); } + @And("Admin adds buy down fee adjustment of buy down fee transaction made on {string} with {string} payment type to the loan on {string} with {string} EUR transaction amount") + public void adminAddsBuyDownFeesAdjustmentToTheLoan(final String originalTransactionDate, final String transactionPaymentType, + final String transactionDate, final String amount) throws IOException { + final Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanResponse.body().getLoanId(); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + final List transactions = loanDetailsResponse.body().getTransactions(); + final GetLoansLoanIdTransactions buyDownFeeTransaction = transactions.stream().filter(t -> { + assert t.getType() != null; + if (!"Buy Down Fee".equals(t.getType().getValue())) { + return false; + } + assert t.getDate() != null; + return FORMATTER.format(t.getDate()).equals(originalTransactionDate); + }).findFirst().orElseThrow(() -> new IllegalStateException("No Buy Down Fee transaction found for loan " + loanId)); + + final Response adjustmentResponse = adjustBuyDownFee(transactionPaymentType, transactionDate, + amount, buyDownFeeTransaction.getId()); + + testContext().set(TestContextKey.LOAN_BUY_DOWN_FEE_ADJUSTMENT_RESPONSE, adjustmentResponse); + ErrorHelper.checkSuccessfulApiCall(adjustmentResponse); + + log.debug("BuyDown Fee Adjustment created: Transaction ID {}", adjustmentResponse.body().getResourceId()); + } + @And("Buy down fee contains the following data:") public void checkBuyDownFeeData(DataTable table) throws IOException { Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); @@ -5665,4 +5731,78 @@ public class LoanStepDef extends AbstractStepDef { return response.body().getSubResourceId(); } + + @And("Loan Amortization Allocation Mapping for {string} transaction created on {string} contains the following data:") + public void checkLoanAmortizationAllocationMapping(final String transactionType, final String transactionDate, DataTable table) + throws IOException { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + final Response loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanCreateResponse.body().getLoanId(); + final String resourceId = String.valueOf(loanId); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + final TransactionType transactionType1 = TransactionType.valueOf(transactionType); + final String transactionTypeExpected = transactionType1.getValue(); + + assert loanDetailsResponse.body() != null; + final List transactions = loanDetailsResponse.body().getTransactions(); + assert transactions != null; + final List transactionsMatch = transactions.stream().filter(t -> { + assert t.getDate() != null; + if (!transactionDate.equals(formatter.format(t.getDate()))) { + return false; + } + assert t.getType() != null; + assert t.getType().getCode() != null; + return transactionTypeExpected.equals(t.getType().getCode().substring(20)); + }).toList(); + + final Response loanAmortizationAllocationResponse = transactionsMatch.getFirst().getType() + .getCode().substring(20).equals(GetLoansLoanIdLoanTransactionEnumData.SERIALIZED_NAME_CAPITALIZED_INCOME) + ? loanCapitalizedIncomeApi.retrieveCapitalizedIncomeAllocationData(loanId, transactionsMatch.getFirst().getId()) + .execute() + : loanBuyDownFeesApi.retrieveBuyDownFeesAllocationData(loanId, transactionsMatch.getFirst().getId()).execute(); + ErrorHelper.checkSuccessfulApiCall(loanAmortizationAllocationResponse); + + checkLoanAmortizationAllocationMappingData(resourceId, loanAmortizationAllocationResponse.body(), table); + } + + private void checkLoanAmortizationAllocationMappingData(final String resourceId, + final LoanAmortizationAllocationResponse amortizationAllocationResponse, final DataTable table) { + final List> data = table.asLists(); + for (int i = 1; i < data.size(); i++) { + final List expectedValues = data.get(i); + assert amortizationAllocationResponse.getAmortizationMappings() != null; + final boolean found = amortizationAllocationResponse.getAmortizationMappings().stream().anyMatch(t -> { + final List actualValues = fetchValuesOfAmortizationAllocationMapping(table.row(0), t); + return actualValues.equals(expectedValues); + }); + + assertThat(found).as(ErrorMessageHelper.wrongValueInLineInDeferredIncomeTab(resourceId, i, + amortizationAllocationResponse.getAmortizationMappings().stream() + .map(t -> fetchValuesOfAmortizationAllocationMapping(table.row(0), t)).collect(Collectors.toList()), + expectedValues)).isTrue(); + } + assertThat(amortizationAllocationResponse.getAmortizationMappings().size()) + .as(ErrorMessageHelper.nrOfLinesWrongInDeferredIncomeTab(resourceId, + amortizationAllocationResponse.getAmortizationMappings().size(), data.size() - 1)) + .isEqualTo(data.size() - 1); + } + + private List fetchValuesOfAmortizationAllocationMapping(final List header, final AmortizationMappingData t) { + final List actualValues = new ArrayList<>(); + for (String headerName : header) { + switch (headerName) { + case "Date" -> actualValues.add(t.getDate() == null ? null : FORMATTER.format(t.getDate())); + case "Type" -> actualValues.add(t.getType() == null ? null : t.getType()); + case "Amount" -> + actualValues.add(t.getAmount() == null ? new Utils.DoubleFormatter(new BigDecimal("0.0").doubleValue()).format() + : new Utils.DoubleFormatter(t.getAmount().doubleValue()).format()); + default -> throw new IllegalStateException(String.format("Header name %s cannot be found", headerName)); + } + } + return actualValues; + } } 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 2c469309b..e1b86ba3e 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanBuyDownFees.feature @@ -3240,3 +3240,317 @@ Feature:Feature: Buy Down Fees 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" + + @TestRailId:C4009 + Scenario: Verify Buy Down Fee amortization allocation mappings + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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 | 250 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "250" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then 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 Admin adds buy down fee with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + + @TestRailId:C4019 + Scenario: Verify Buy Down Fee amortization allocation mappings when buy down fee transaction is reversed + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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 | 350 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "350" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then 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 Admin adds buy down fee with "AUTOPAY" payment type to the loan on "2 January 2024" with "200" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + When Customer undo "1"th "Buy Down Fee" transaction made on "01 January 2024" + When Admin sets the business date to "4 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 1.12 | 0.0 | 1.12 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 1.1 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + | 03 January 2024 | AM | 2.22 | + When Admin sets the business date to "5 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | true | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 1.12 | 0.0 | 1.12 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Amortization | 2.23 | 0.0 | 2.23 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 1.1 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + | 03 January 2024 | AM | 2.22 | + | 04 January 2024 | AM | 2.23 | + + @TestRailId:C4022 + Scenario: Verify Buy Down Fee amortization allocation mappings when buy down fee adjustment occurs + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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 | 350 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "350" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds buy down fee with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then 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 Admin adds buy down fee with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + When Admin adds buy down fee adjustment with "AUTOPAY" payment type to the loan on "02 January 2024" with "40" EUR transaction amount + When Admin sets the business date to "4 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Adjustment | 40.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + When Admin sets the business date to "5 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Adjustment | 40.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + And Admin adds buy down fee adjustment of buy down fee transaction made on "02 January 2024" with "AUTOPAY" payment type to the loan on "04 January 2024" with "60" EUR transaction amount + When Admin sets the business date to "6 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Adjustment | 40.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Adjustment | 60.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Buy Down Fee Amortization Adjustment | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + | 05 January 2024 | AM | 0.11 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + | 05 January 2024 | AM_ADJ | 0.25 | + When Admin adds buy down fee adjustment with "AUTOPAY" payment type to the loan on "05 January 2024" with "10" EUR transaction amount + When Admin sets the business date to "7 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Buy Down Fee | 50.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 01 January 2024 | Buy Down Fee Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Buy Down Fee Adjustment | 40.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Buy Down Fee Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Buy Down Fee Adjustment | 60.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Buy Down Fee Amortization Adjustment | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Buy Down Fee Adjustment | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Buy Down Fee Amortization Adjustment | 0.54 | 0.0 | 0.54 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + | 05 January 2024 | AM | 0.11 | + | 06 January 2024 | AM_ADJ | 0.97 | + And Loan Amortization Allocation Mapping for "BUY_DOWN_FEE" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + | 05 January 2024 | AM_ADJ | 0.25 | + | 06 January 2024 | AM | 0.43 | \ No newline at end of file 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 0350b40e5..b539eef60 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature @@ -7566,3 +7566,320 @@ Feature: Capitalized Income | 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" + + @TestRailId:C4008 + Scenario: Verify Capitalized income amortization allocation mappings + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC | 01 January 2024 | 250 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "250" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | | | | + | | | 01 January 2024 | | 50.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 100.29 | 49.71 | 0.87 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 2 | 29 | 01 March 2024 | | 50.3 | 49.99 | 0.59 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 3 | 31 | 01 April 2024 | | 0.0 | 50.3 | 0.29 | 0.0 | 0.0 | 50.59 | 0.0 | 0.0 | 0.0 | 50.59 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 150.0 | 1.75 | 0.0 | 0.0 | 151.75 | 0.0 | 0.0 | 0.0 | 151.75 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "01 January 2024" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 50.0 | | + | LIABILITY | 145024 | Deferred Capitalized Income | | 50.0 | + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + + @TestRailId:C4020 + Scenario: Verify Capitalized income amortization allocation mappings when capitalized income transaction is reversed + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC | 01 January 2024 | 350 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "350" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | | | | + | | | 01 January 2024 | | 50.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 100.29 | 49.71 | 0.87 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 2 | 29 | 01 March 2024 | | 50.3 | 49.99 | 0.59 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 3 | 31 | 01 April 2024 | | 0.0 | 50.3 | 0.29 | 0.0 | 0.0 | 50.59 | 0.0 | 0.0 | 0.0 | 50.59 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 150.0 | 1.75 | 0.0 | 0.0 | 151.75 | 0.0 | 0.0 | 0.0 | 151.75 | + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "01 January 2024" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 50.0 | | + | LIABILITY | 145024 | Deferred Capitalized Income | | 50.0 | + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "200" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 350.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + When Customer undo "1"th "Capitalized Income" transaction made on "01 January 2024" + When Admin sets the business date to "4 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | true | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 300.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.05 | 0.0 | 0.05 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 1.12 | 0.0 | 1.12 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 1.1 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + | 03 January 2024 | AM | 2.22 | + When Admin sets the business date to "5 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | true | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 300.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 2.77 | 0.0 | 2.77 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Accrual | 0.05 | 0.0 | 0.05 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 1.12 | 0.0 | 1.12 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.05 | 0.0 | 0.05 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Amortization | 2.23 | 0.0 | 2.23 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 1.1 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 2.22 | + | 03 January 2024 | AM | 2.22 | + | 04 January 2024 | AM | 2.23 | + + @TestRailId:C4021 + Scenario: Verify Capitalized income amortization allocation mappings when capitalized income adjustment occurs + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When 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_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC | 01 January 2024 | 250 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "1 January 2024" with "250" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + Then Loan status will be "ACTIVE" + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "1 January 2024" with "50" EUR transaction amount + When Admin sets the business date to "2 January 2024" + And Admin runs inline COB job for Loan + 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 | | | | + | | | 01 January 2024 | | 50.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | | 100.29 | 49.71 | 0.87 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 2 | 29 | 01 March 2024 | | 50.3 | 49.99 | 0.59 | 0.0 | 0.0 | 50.58 | 0.0 | 0.0 | 0.0 | 50.58 | + | 3 | 31 | 01 April 2024 | | 0.0 | 50.3 | 0.29 | 0.0 | 0.0 | 50.59 | 0.0 | 0.0 | 0.0 | 50.59 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 150.0 | 1.75 | 0.0 | 0.0 | 151.75 | 0.0 | 0.0 | 0.0 | 151.75 | + Then Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "01 January 2024" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 50.0 | | + | LIABILITY | 145024 | Deferred Capitalized Income | | 50.0 | + And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "3 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + When Admin adds capitalized income adjustment with "AUTOPAY" payment type to the loan on "02 January 2024" with "40" EUR transaction amount + When Admin sets the business date to "4 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Adjustment | 40.0 | 40.0 | 0.0 | 0.0 | 0.0 | 210.0 | false | false | + | 03 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + When Admin sets the business date to "5 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Adjustment | 40.0 | 40.0 | 0.0 | 0.0 | 0.0 | 210.0 | false | false | + | 03 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + And Admin adds capitalized income adjustment of capitalized income transaction made on "02 January 2024" with "AUTOPAY" payment type to the loan on "04 January 2024" with "60" EUR transaction amount + When Admin sets the business date to "6 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Adjustment | 40.0 | 40.0 | 0.0 | 0.0 | 0.0 | 210.0 | false | false | + | 03 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Adjustment | 60.0 | 59.89 | 0.11 | 0.0 | 0.0 | 150.11 | false | false | + | 05 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Capitalized Income Amortization Adjustment | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + | 05 January 2024 | AM | 0.11 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + | 05 January 2024 | AM_ADJ | 0.25 | + When Admin adds capitalized income adjustment with "AUTOPAY" payment type to the loan on "05 January 2024" with "10" EUR transaction amount + When Admin sets the business date to "7 January 2024" + And Admin runs inline COB job for Loan + And Loan Transactions tab has the following data: + | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | + | 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false | + | 01 January 2024 | Capitalized Income | 50.0 | 50.0 | 0.0 | 0.0 | 0.0 | 150.0 | false | false | + | 01 January 2024 | Capitalized Income Amortization | 0.55 | 0.0 | 0.55 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 250.0 | false | false | + | 02 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Amortization | 1.66 | 0.0 | 1.66 | 0.0 | 0.0 | 0.0 | false | false | + | 02 January 2024 | Capitalized Income Adjustment | 40.0 | 40.0 | 0.0 | 0.0 | 0.0 | 210.0 | false | false | + | 03 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 03 January 2024 | Capitalized Income Amortization | 0.77 | 0.0 | 0.77 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Accrual | 0.04 | 0.0 | 0.04 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Amortization | 1.21 | 0.0 | 1.21 | 0.0 | 0.0 | 0.0 | false | false | + | 04 January 2024 | Capitalized Income Adjustment | 60.0 | 59.89 | 0.11 | 0.0 | 0.0 | 150.11 | false | false | + | 05 January 2024 | Accrual | 0.03 | 0.0 | 0.03 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Capitalized Income Amortization Adjustment | 0.14 | 0.0 | 0.14 | 0.0 | 0.0 | 0.0 | false | false | + | 05 January 2024 | Capitalized Income Adjustment | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 140.11 | false | false | + | 06 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | + | 06 January 2024 | Capitalized Income Amortization Adjustment | 0.54 | 0.0 | 0.54 | 0.0 | 0.0 | 0.0 | false | false | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "01 January 2024" contains the following data: + | Date | Type | Amount | + | 01 January 2024 | AM | 0.55 | + | 02 January 2024 | AM | 0.55 | + | 03 January 2024 | AM_ADJ | 0.34 | + | 04 January 2024 | AM | 0.1 | + | 05 January 2024 | AM | 0.11 | + | 06 January 2024 | AM_ADJ | 0.97 | + And Loan Amortization Allocation Mapping for "CAPITALIZED_INCOME" transaction created on "02 January 2024" contains the following data: + | Date | Type | Amount | + | 02 January 2024 | AM | 1.11 | + | 03 January 2024 | AM | 1.11 | + | 04 January 2024 | AM | 1.11 | + | 05 January 2024 | AM_ADJ | 0.25 | + | 06 January 2024 | AM | 0.43 | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanAmortizationAllocationApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanAmortizationAllocationApiResourceSwagger.java new file mode 100644 index 000000000..e8081bdb0 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanAmortizationAllocationApiResourceSwagger.java @@ -0,0 +1,79 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.api; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * Swagger documentation for Loan Amortization Allocation API + */ +final class LoanAmortizationAllocationApiResourceSwagger { + + private LoanAmortizationAllocationApiResourceSwagger() {} + + /** + * Common response class for all loan amortization allocation APIs Used for both Capitalized Income and Buydown Fee + * endpoints + */ + @Schema(description = "LoanAmortizationAllocationResponse") + public static final class LoanAmortizationAllocationResponse { + + private LoanAmortizationAllocationResponse() {} + + @Schema(example = "1") + public Long loanId; + @Schema(example = "loan-ext-123") + public String loanExternalId; + @Schema(example = "123") + public Long baseLoanTransactionId; + @Schema(example = "2024-01-15") + public LocalDate baseLoanTransactionDate; + @Schema(example = "1000.00") + public BigDecimal baseLoanTransactionAmount; + @Schema(example = "50.00") + public BigDecimal unrecognizedAmount; + @Schema(example = "0.00") + public BigDecimal chargedOffAmount; + @Schema(example = "25.00") + public BigDecimal adjustmentAmount; + public List amortizationMappings; + + /** + * Data transfer object for amortization mapping details + */ + static final class AmortizationMappingData { + + private AmortizationMappingData() {} + + @Schema(example = "789") + public Long amortizationLoanTransactionId; + @Schema(example = "amort-txn-ext-789") + public String amortizationLoanTransactionExternalId; + @Schema(example = "2024-01-15") + public LocalDate date; + @Schema(example = "AM", description = "AM for amortization, AM_ADJ for amortization adjustment") + public String type; + @Schema(example = "10.00") + public BigDecimal amount; + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationBaseTransactionDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationBaseTransactionDTO.java new file mode 100644 index 000000000..ec9d2c259 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationBaseTransactionDTO.java @@ -0,0 +1,42 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.ExternalId; + +/** + * DTO for base transaction information for AmortizationAllocationMapping + */ +@Getter +@AllArgsConstructor +public class AmortizationAllocationBaseTransactionDTO { + + private Long loanId; + private ExternalId loanExternalId; + private Long baseLoanTransactionId; + private LocalDate baseLoanTransactionDate; + private BigDecimal baseLoanTransactionAmount; + private BigDecimal unrecognizedAmount; + private BigDecimal chargedOffAmount; + private BigDecimal adjustmentAmount; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationMappingDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationMappingDTO.java new file mode 100644 index 000000000..df7809f6e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AmortizationAllocationMappingDTO.java @@ -0,0 +1,40 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; + +/** + * DTO for amortization allocation mapping data + */ +@Getter +@AllArgsConstructor +public class AmortizationAllocationMappingDTO { + + private Long amortizationLoanTransactionId; + private ExternalId amortizationLoanTransactionExternalId; + private LocalDate amortizationDate; + private AmortizationType amortizationType; + private BigDecimal amount; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAmortizationAllocationData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAmortizationAllocationData.java new file mode 100644 index 000000000..56400783c --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAmortizationAllocationData.java @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; + +/** + * Data transfer object for loan amortization allocation information + */ +@Getter +@Builder +@AllArgsConstructor +public class LoanAmortizationAllocationData { + + private Long loanId; + private ExternalId loanExternalId; + private Long baseLoanTransactionId; + private LocalDate baseLoanTransactionDate; + private BigDecimal baseLoanTransactionAmount; + private BigDecimal unrecognizedAmount; + private BigDecimal chargedOffAmount; + private BigDecimal adjustmentAmount; + private List amortizationMappings; + + /** + * Data transfer object for amortization mapping details + */ + @Getter + @Builder + @AllArgsConstructor + public static class AmortizationMappingData { + + private Long amortizationLoanTransactionId; + private ExternalId amortizationLoanTransactionExternalId; + private LocalDate date; + private AmortizationType type; // AM or AM_ADJ + private BigDecimal amount; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/AmortizationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/AmortizationType.java new file mode 100644 index 000000000..1c22645aa --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/AmortizationType.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; + +@Getter +@RequiredArgsConstructor +public enum AmortizationType implements ApiFacingEnum { + + AM("amortizationTransaction.type.amortization", "Amortization"), // + AM_ADJ("amortizationTransaction.type.amortizationAdjustment", "Amortization Adjustment"); // + + private final String code; + private final String humanReadableName; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMapping.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMapping.java new file mode 100644 index 000000000..5e19538c5 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMapping.java @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; + +/** + * Entity to store the mapping between base loan transactions (Capitalized Income or Buy Down Fee) and their + * amortization allocation schedule. This mapping table stores which base loan transaction amortization occurs, on which + * date and what was the calculated amount. + */ +@Entity +@Table(name = "m_loan_amortization_allocation_mapping") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class LoanAmortizationAllocationMapping extends AbstractAuditableWithUTCDateTimeCustom { + + @Column(name = "loan_id", nullable = false) + private Long loanId; + + @Column(name = "base_loan_transaction_id", nullable = false) + private Long baseLoanTransactionId; + + @Column(name = "date", nullable = false) + private LocalDate date; + + @Column(name = "amortization_loan_transaction_id", nullable = false) + private Long amortizationLoanTransactionId; + + /** + * Type of the amortization transaction (AM - amortization, AM_ADJ - amortization adjustment) + */ + @Enumerated(EnumType.STRING) + @Column(name = "amortization_type", nullable = false) + private AmortizationType amortizationType; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java new file mode 100644 index 000000000..7507a75ea --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAmortizationAllocationMappingRepository.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain; + +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationMappingDTO; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +/** + * Repository for LoanAmortizationAllocationMapping entity + */ +@Repository +public interface LoanAmortizationAllocationMappingRepository + extends JpaRepository, JpaSpecificationExecutor { + + @Query(value = "SELECT new org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationMappingDTO(" + + "laam.amortizationLoanTransactionId, at.externalId, laam.date, laam.amortizationType, laam.amount) " + + "FROM LoanAmortizationAllocationMapping laam " + "JOIN LoanTransaction at ON laam.amortizationLoanTransactionId = at.id " + + "WHERE laam.baseLoanTransactionId = :baseLoanTransactionId AND laam.loanId = :loanId " + + "ORDER BY laam.date, laam.amortizationLoanTransactionId") + List findAmortizationMappingsByBaseTransactionAndLoan( + @Param("baseLoanTransactionId") Long baseLoanTransactionId, @Param("loanId") Long loanId); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationService.java new file mode 100644 index 000000000..70b2b62a8 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationService.java @@ -0,0 +1,44 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; + +public interface LoanAmortizationAllocationService { + + /** + * Retrieve amortization allocation data for a specific buy down fee loan transaction + */ + LoanAmortizationAllocationData retrieveLoanAmortizationAllocationsForBuyDownFeeTransaction(Long loanTransactionId, Long loanId); + + /** + * Retrieve amortization allocation data for a specific capitalized income loan transaction + */ + LoanAmortizationAllocationData retrieveLoanAmortizationAllocationsForCapitalizedIncomeTransaction(Long loanTransactionId, Long loanId); + + LoanAmortizationAllocationMapping createAmortizationAllocationMappingWithBaseLoanTransaction(LoanTransaction loanTransaction, + BigDecimal amount, AmortizationType amortizationType); + + void setAmortizationTransactionDataAndSaveAmortizationAllocationMapping(LoanAmortizationAllocationMapping amortizationAllocationMapping, + LoanTransaction amortizationTransaction); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 7878ed215..05cb824a2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -163,4 +163,6 @@ public interface LoanReadPlatformService { Long getResolvedLoanId(ExternalId loanExternalId); + Long getResolvedLoanTransactionId(Long transactionId, ExternalId externalTransactionId); + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanBuyDownFeeApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanBuyDownFeeApiResource.java index 915b2d732..e18466c80 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanBuyDownFeeApiResource.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanBuyDownFeeApiResource.java @@ -31,14 +31,24 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.data.BuyDownFeeAmortizationDetails; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeeReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAmortizationAllocationService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.stereotype.Component; @@ -48,9 +58,15 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class LoanBuyDownFeeApiResource { + private static final Set RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList("loanId", "loanExternalId", "baseLoanTransactionId", "baseLoanTransactionDate", "baseLoanTransactionAmount", + "unrecognizedAmount", "chargedOffAmount", "adjustmentAmount", "amortizationMappings")); private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN"; private final PlatformSecurityContext context; private final BuyDownFeeReadPlatformService buyDownFeeReadPlatformService; + private final LoanAmortizationAllocationService loanAmortizationAllocationService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; private final LoanReadPlatformService loanReadPlatformService; @Path("/{loanId}/buydown-fees") @@ -84,4 +100,96 @@ public class LoanBuyDownFeeApiResource { return this.buyDownFeeReadPlatformService.retrieveLoanBuyDownFeeAmortizationDetails(resolvedLoanId); } + /** + * Get BuyDown Fees allocation data by loan ID and transaction ID + */ + @GET + @Path("{loanId}/buydown-fees/{loanTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a BuyDown Fees allocation data", description = "Retrieves BuyDown Fees allocation data according to the Loan ID and Loan Transaction ID" + + "Example Requests:\n" + "\n" + "/loans/1/buydown-fees/1\n" + "\n" + "\n" + + "/loans/1/buydown-fees/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String retrieveBuyDownFeesAllocationData(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanTransactionId") @Parameter(description = "loanTransactionId") final Long loanTransactionId, + @Context final UriInfo uriInfo) { + return retrieveBuyDownFeesAllocationData(loanId, null, loanTransactionId, null, uriInfo); + } + + /** + * Get BuyDown Fees allocation data by loan external ID and transaction ID + */ + @GET + @Path("external-id/{loanExternalId}/buydown-fees/{loanTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a BuyDown Fees allocation data", description = "Retrieves BuyDown Fees allocation data according to the Loan external ID and Loan Transaction ID" + + "Example Requests:\n" + "\n" + "/loans/external-id/1/buydown-fees/1\n" + "\n" + "\n" + + "/loans/external-id/1/buydown-fees/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getBuyDownFeesAllocationDataByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanTransactionId") @Parameter(description = "loanTransactionId") final Long loanTransactionId, + @Context final UriInfo uriInfo) { + return retrieveBuyDownFeesAllocationData(null, loanExternalId, loanTransactionId, null, uriInfo); + } + + /** + * Get BuyDown Fees allocation data by loan ID and transaction external ID + */ + @GET + @Path("{loanId}/buydown-fees/external-id/{loanTransactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a BuyDown Fees allocation data", description = "Retrieves BuyDown Fees allocation data according to the Loan ID and Loan Transaction external ID" + + "Example Requests:\n" + "\n" + "/loans/1/buydown-fees/external-id/1\n" + "\n" + "\n" + + "/loans/1/buydown-fees/external-id/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getBuyDownFeesAllocationDataByTransactionExternalId( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanTransactionExternalId") @Parameter(description = "loanTransactionExternalId") final String loanTransactionExternalId, + @Context final UriInfo uriInfo) { + return retrieveBuyDownFeesAllocationData(loanId, null, null, loanTransactionExternalId, uriInfo); + } + + /** + * Get BuyDown Fees allocation data by loan external ID and transaction external ID + */ + @GET + @Path("external-id/{loanExternalId}/buydown-fees/external-id/{loanTransactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a BuyDown Fees allocation data", description = "Retrieves BuyDown Fees allocation data according to the Loan external ID and Loan Transaction external ID" + + "Example Requests:\n" + "\n" + "/loans/external-id/1/buydown-fees/1\n" + "\n" + "\n" + + "/loans/external-id/1/buydown-fees/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getBuyDownFeesAllocationDataByExternalIds( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanTransactionExternalId") @Parameter(description = "loanTransactionExternalId") final String loanTransactionExternalId, + @Context final UriInfo uriInfo) { + return retrieveBuyDownFeesAllocationData(null, loanExternalId, null, loanTransactionExternalId, uriInfo); + } + + private String retrieveBuyDownFeesAllocationData(final Long loanId, final String loanExternalIdStr, final Long loanTransactionId, + final String loanTransactionExternalIdStr, final UriInfo uriInfo) { + context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + + final ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + final ExternalId loanTransactionExternalId = ExternalIdFactory.produce(loanTransactionExternalIdStr); + + final Long resolvedLoanId = loanId == null ? loanReadPlatformService.getResolvedLoanId(loanExternalId) : loanId; + final Long resolvedLoanTransactionId = loanReadPlatformService.getResolvedLoanTransactionId(loanTransactionId, + loanTransactionExternalId); + + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForBuyDownFeeTransaction(resolvedLoanTransactionId, resolvedLoanId); + + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return toApiJsonSerializer.serialize(settings, loanAmortizationAllocationData, RESPONSE_DATA_PARAMETERS); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanCapitalizedIncomeApiResource.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanCapitalizedIncomeApiResource.java index 584c1a095..6ae53ffc7 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanCapitalizedIncomeApiResource.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanCapitalizedIncomeApiResource.java @@ -30,15 +30,25 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.data.CapitalizedIncomeDetails; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; import org.apache.fineract.portfolio.loanaccount.data.LoanCapitalizedIncomeData; import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceReadService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAmortizationAllocationService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.stereotype.Component; @@ -48,9 +58,15 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class LoanCapitalizedIncomeApiResource { + private static final Set RESPONSE_DATA_PARAMETERS = new HashSet<>( + Arrays.asList("loanId", "loanExternalId", "baseLoanTransactionId", "baseLoanTransactionDate", "baseLoanTransactionAmount", + "unrecognizedAmount", "chargedOffAmount", "adjustmentAmount", "amortizationMappings")); private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN"; private final PlatformSecurityContext context; private final CapitalizedIncomeBalanceReadService capitalizedIncomeBalanceReadService; + private final LoanAmortizationAllocationService loanAmortizationAllocationService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final ApiRequestParameterHelper apiRequestParameterHelper; private final LoanReadPlatformService loanReadPlatformService; @Path("/{loanId}/deferredincome") @@ -109,4 +125,96 @@ public class LoanCapitalizedIncomeApiResource { return this.capitalizedIncomeBalanceReadService.fetchLoanCapitalizedIncomeDetails(resolvedLoanId); } + /** + * Get capitalized income allocation data by loan ID and transaction ID + */ + @GET + @Path("{loanId}/capitalized-incomes/{loanTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a capitalized income allocation data", description = "Retrieves capitalized income allocation data according to the Loan ID and Loan Transaction ID" + + "Example Requests:\n" + "\n" + "/loans/1/capitalized-incomes/1\n" + "\n" + "\n" + + "/loans/1/capitalized-incomes/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String retrieveCapitalizedIncomeAllocationData(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanTransactionId") @Parameter(description = "loanTransactionId") final Long loanTransactionId, + @Context final UriInfo uriInfo) { + return retrieveCapitalizedIncomeAllocationData(loanId, null, loanTransactionId, null, uriInfo); + } + + /** + * Get capitalized income allocation data by loan external ID and transaction ID + */ + @GET + @Path("external-id/{loanExternalId}/capitalized-incomes/{loanTransactionId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a capitalized income allocation data", description = "Retrieves capitalized income allocation data according to the Loan external ID and Loan Transaction ID" + + "Example Requests:\n" + "\n" + "/loans/external-id/1/capitalized-incomes/1\n" + "\n" + "\n" + + "/loans/external-id/1/capitalized-incomes/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getCapitalizedIncomeAllocationDataByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanTransactionId") @Parameter(description = "loanTransactionId") final Long loanTransactionId, + @Context final UriInfo uriInfo) { + return retrieveCapitalizedIncomeAllocationData(null, loanExternalId, loanTransactionId, null, uriInfo); + } + + /** + * Get capitalized income allocation data by loan ID and transaction external ID + */ + @GET + @Path("{loanId}/capitalized-incomes/external-id/{loanTransactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a capitalized income allocation data", description = "Retrieves capitalized income allocation data according to the Loan ID and Loan Transaction external ID" + + "Example Requests:\n" + "\n" + "/loans/1/capitalized-incomes/external-id/1\n" + "\n" + "\n" + + "/loans/1/capitalized-incomes/external-id/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getCapitalizedIncomeAllocationDataByTransactionExternalId( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("loanTransactionExternalId") @Parameter(description = "loanTransactionExternalId") final String loanTransactionExternalId, + @Context final UriInfo uriInfo) { + return retrieveCapitalizedIncomeAllocationData(loanId, null, null, loanTransactionExternalId, uriInfo); + } + + /** + * Get capitalized income allocation data by loan external ID and transaction external ID + */ + @GET + @Path("external-id/{loanExternalId}/capitalized-incomes/external-id/{loanTransactionExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve a capitalized income allocation data", description = "Retrieves capitalized income allocation data according to the Loan external ID and Loan Transaction external ID" + + "Example Requests:\n" + "\n" + "/loans/external-id/1/capitalized-incomes/1\n" + "\n" + "\n" + + "/loans/external-id/1/capitalized-incomes/1?fields=baseLoanTransaction,unrecognizedAmount") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanAmortizationAllocationApiResourceSwagger.LoanAmortizationAllocationResponse.class))) }) + public String getCapitalizedIncomeAllocationDataByExternalIds( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("loanTransactionExternalId") @Parameter(description = "loanTransactionExternalId") final String loanTransactionExternalId, + @Context final UriInfo uriInfo) { + return retrieveCapitalizedIncomeAllocationData(null, loanExternalId, null, loanTransactionExternalId, uriInfo); + } + + private String retrieveCapitalizedIncomeAllocationData(final Long loanId, final String loanExternalIdStr, final Long loanTransactionId, + final String loanTransactionExternalIdStr, final UriInfo uriInfo) { + context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); + + final ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr); + final ExternalId loanTransactionExternalId = ExternalIdFactory.produce(loanTransactionExternalIdStr); + + final Long resolvedLoanId = loanId == null ? loanReadPlatformService.getResolvedLoanId(loanExternalId) : loanId; + final Long resolvedLoanTransactionId = loanReadPlatformService.getResolvedLoanTransactionId(loanTransactionId, + loanTransactionExternalId); + + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForCapitalizedIncomeTransaction(resolvedLoanTransactionId, resolvedLoanId); + + final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return toApiJsonSerializer.serialize(settings, loanAmortizationAllocationData, RESPONSE_DATA_PARAMETERS); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuyDownFeeBalance.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuyDownFeeBalance.java index 679ee0ebc..6d4786e30 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuyDownFeeBalance.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuyDownFeeBalance.java @@ -62,4 +62,9 @@ public class LoanBuyDownFeeBalance extends AbstractAuditableWithUTCDateTimeCusto @Column(name = "amount_adjustment", scale = 6, precision = 19) private BigDecimal amountAdjustment; + @Column(name = "is_deleted", nullable = false) + private boolean deleted = false; + + @Column(name = "is_closed", nullable = false) + private boolean closed = false; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java index 5e8b2c128..fd8f383af 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeBalance.java @@ -61,4 +61,10 @@ public class LoanCapitalizedIncomeBalance extends AbstractAuditableWithUTCDateTi @Column(name = "amount_adjustment", scale = 6, precision = 19) private BigDecimal amountAdjustment; + + @Column(name = "is_deleted", nullable = false) + private boolean deleted = false; + + @Column(name = "is_closed", nullable = false) + private boolean closed = false; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeeBalanceRepository.java index a9baa5c68..08e126f30 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeeBalanceRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanBuyDownFeeBalanceRepository.java @@ -19,18 +19,34 @@ package org.apache.fineract.portfolio.loanaccount.repository; import java.util.List; +import org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationBaseTransactionDTO; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LoanBuyDownFeeBalanceRepository extends JpaRepository, JpaSpecificationExecutor { - List findAllByLoanId(Long loanId); + @Query("SELECT lbdfb FROM LoanBuyDownFeeBalance lbdfb WHERE lbdfb.loan.id=:loanId AND lbdfb.closed = FALSE ORDER BY lbdfb.date, lbdfb.createdDate") + List findAllByLoanIdAndClosedFalse(@Param("loanId") Long loanId); - LoanBuyDownFeeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId); + @Query("SELECT lbdfb FROM LoanBuyDownFeeBalance lbdfb WHERE lbdfb.loan.id=:loanId AND lbdfb.closed = FALSE AND lbdfb.deleted = FALSE ORDER BY lbdfb.date, lbdfb.createdDate") + List findAllByLoanIdAndDeletedFalseAndClosedFalse(@Param("loanId") Long loanId); - @Query("SELECT lbdfb FROM LoanBuyDownFeeBalance lbdfb, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lbdfb.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lbdfb.loanTransaction.id = lt.id") - LoanBuyDownFeeBalance findBalanceForAdjustment(Long transactionId); + LoanBuyDownFeeBalance findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(Long loanId, Long transactionId); + + @Query("SELECT lbdfb FROM LoanBuyDownFeeBalance lbdfb, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lbdfb.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lbdfb.loanTransaction.id = lt.id AND lbdfb.deleted = false AND lbdfb.closed = false") + LoanBuyDownFeeBalance findBalanceForAdjustment(@Param("transactionId") Long transactionId); + + @Query(""" + SELECT new org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationBaseTransactionDTO( + l.id, l.externalId, lbdfb.loanTransaction.id, lbdfb.date, lbdfb.amount, + lbdfb.unrecognizedAmount, lbdfb.chargedOffAmount, lbdfb.amountAdjustment + ) FROM LoanBuyDownFeeBalance lbdfb JOIN lbdfb.loan l JOIN lbdfb.loanTransaction bt + WHERE lbdfb.loanTransaction.id = :loanTransactionId AND l.id = :loanId + """) + AmortizationAllocationBaseTransactionDTO findBaseTransactionInfo(@Param("loanTransactionId") Long loanTransactionId, + @Param("loanId") Long loanId); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java index a5f70f029..f7a096fb1 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java @@ -20,30 +20,47 @@ package org.apache.fineract.portfolio.loanaccount.repository; import java.math.BigDecimal; import java.util.List; +import org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationBaseTransactionDTO; import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LoanCapitalizedIncomeBalanceRepository extends JpaRepository, JpaSpecificationExecutor, CustomizedLoanCapitalizedIncomeBalanceRepository { String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lcib.loanTransaction.id, lcib.loan.id, lcib.loanTransaction.dateOf, lcib.loanTransaction.reversed, lcib.amount, lcib.unrecognizedAmount, lcib.loanTransaction.feeChargesPortion) FROM LoanCapitalizedIncomeBalance lcib "; - List findAllByLoanId(Long loanId); + @Query("SELECT lcib FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id=:loanId AND lcib.closed = FALSE ORDER BY lcib.date, lcib.createdDate") + List findAllByLoanIdAndClosedFalse(@Param("loanId") Long loanId); - LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId); + @Query("SELECT lcib FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id=:loanId AND lcib.closed = FALSE AND lcib.deleted = FALSE ORDER BY lcib.date, lcib.createdDate") + List findAllByLoanIdAndDeletedFalseAndClosedFalse(@Param("loanId") Long loanId); - @Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id = :loanId") + LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(Long loanId, Long transactionId); + + @Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + + " WHERE lcib.loan.id = :loanId AND lcib.deleted = false AND lcib.closed = false ORDER BY lcib.date, lcib.createdDate") List findRepaymentPeriodDataByLoanId(Long loanId); - @Query("SELECT SUM(lcib.amount) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId") + @Query("SELECT SUM(lcib.amount) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId AND lcib.deleted = false AND lcib.closed = false") BigDecimal calculateCapitalizedIncome(Long loanId); - @Query("SELECT SUM(lcib.amountAdjustment) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId") + @Query("SELECT SUM(lcib.amountAdjustment) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId AND lcib.deleted = false AND lcib.closed = false") BigDecimal calculateCapitalizedIncomeAdjustment(Long loanId); - @Query("SELECT lcib FROM LoanCapitalizedIncomeBalance lcib, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lcib.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lcib.loanTransaction.id = lt.id") + @Query("SELECT lcib FROM LoanCapitalizedIncomeBalance lcib, LoanTransaction lt, LoanTransactionRelation ltr WHERE lt.loan.id = lcib.loan.id AND ltr.fromTransaction.id =:transactionId AND ltr.toTransaction.id=lt.id AND lcib.loanTransaction.id = lt.id AND lcib.deleted = false AND lcib.closed = false") LoanCapitalizedIncomeBalance findBalanceForAdjustment(Long transactionId); + + @Query(""" + SELECT new org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationBaseTransactionDTO( + l.id, l.externalId, lcib.loanTransaction.id, lcib.date, lcib.amount, + lcib.unrecognizedAmount, lcib.chargedOffAmount, lcib.amountAdjustment + ) FROM LoanCapitalizedIncomeBalance lcib JOIN lcib.loan l JOIN lcib.loanTransaction bt + WHERE lcib.loanTransaction.id = :loanTransactionId AND l.id = :loanId + """) + AmortizationAllocationBaseTransactionDTO findBaseTransactionInfo(@Param("loanTransactionId") Long loanTransactionId, + @Param("loanId") Long loanId); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeReadPlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeReadPlatformServiceImpl.java index 7748abad2..49ea50a6b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeReadPlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/BuyDownFeeReadPlatformServiceImpl.java @@ -45,7 +45,8 @@ public class BuyDownFeeReadPlatformServiceImpl implements BuyDownFeeReadPlatform throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.enabled.buydown.fee", "Loan: %s is not enabled Buydown fee feature".formatted(loanId), loanId); } - final List buyDownFeeBalances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loanId); + final List buyDownFeeBalances = loanBuyDownFeeBalanceRepository + .findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId); return buyDownFeeBalances.stream().map(this::mapToLoanBuyDownFeeAmortizationData).collect(Collectors.toList()); } 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 35c850fcd..213265bd0 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 @@ -165,8 +165,8 @@ public class BuyDownFeeWritePlatformServiceImpl implements BuyDownFeePlatformSer LoanTransaction savedBuyDownFeeAdjustment = loanTransactionRepository.saveAndFlush(buyDownFeeAdjustment); // Update buy down fee balance - LoanBuyDownFeeBalance buydownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId, - buyDownFeeTransactionId); + LoanBuyDownFeeBalance buydownFeeBalance = loanBuyDownFeeBalanceRepository + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, buyDownFeeTransactionId); if (buydownFeeBalance != null) { buydownFeeBalance.setAmountAdjustment(MathUtil.nullToZero(buydownFeeBalance.getAmountAdjustment()).add(transactionAmount)); buydownFeeBalance diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceReadServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceReadServiceImpl.java index 523931f57..1dbfc44e7 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceReadServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceReadServiceImpl.java @@ -43,7 +43,8 @@ public class CapitalizedIncomeBalanceReadServiceImpl implements CapitalizedIncom if (loanRepository.isEnabledCapitalizedIncome(loanId)) { List capitalizedIncomeData = new ArrayList<>(); - List capitalizedIncomeBalances = capitalizedIncomeBalanceRepository.findAllByLoanId(loanId); + List capitalizedIncomeBalances = capitalizedIncomeBalanceRepository + .findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId); for (final LoanCapitalizedIncomeBalance capitalizedIncomeBalance : capitalizedIncomeBalances) { final BigDecimal amortizedAmount = capitalizedIncomeBalance.getAmount() // .subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getUnrecognizedAmount())) // @@ -67,7 +68,8 @@ public class CapitalizedIncomeBalanceReadServiceImpl implements CapitalizedIncom if (loanRepository.isEnabledCapitalizedIncome(loanId)) { List capitalizedIncomeData = new ArrayList<>(); - List capitalizedIncomeBalances = capitalizedIncomeBalanceRepository.findAllByLoanId(loanId); + List capitalizedIncomeBalances = capitalizedIncomeBalanceRepository + .findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId); for (final LoanCapitalizedIncomeBalance capitalizedIncomeBalance : capitalizedIncomeBalances) { final BigDecimal amortizedAmount = capitalizedIncomeBalance.getAmount() // .subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getUnrecognizedAmount())) // 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 431f01b19..23fb66ac9 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 @@ -159,8 +159,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc // Create journal entries immediately for this transaction journalEntryPoster.postJournalEntriesForLoanTransaction(savedCapitalizedIncomeAdjustment, false, false); - LoanCapitalizedIncomeBalance capitalizedIncomeBalance = capitalizedIncomeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId, - capitalizedIncomeTransactionId); + LoanCapitalizedIncomeBalance capitalizedIncomeBalance = capitalizedIncomeBalanceRepository + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, capitalizedIncomeTransactionId); capitalizedIncomeBalance .setAmountAdjustment(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment()).add(transactionAmount)); capitalizedIncomeBalance.setUnrecognizedAmount( 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 085904d2c..385d07220 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 @@ -224,7 +224,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT } if (transactionAmount != null) { LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository - .findByLoanIdAndLoanTransactionId(loanId, capitalizedIncomeTransactionId); + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, capitalizedIncomeTransactionId); if (MathUtil.isLessThan(capitalizedIncomeBalance.getAmount() .subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment())), transactionAmount)) { baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).failWithCode( @@ -401,8 +401,8 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT } if (transactionAmount != null) { - LoanBuyDownFeeBalance buydownFeeBalance = loanBuydownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId, - buyDownFeeTransactionId); + LoanBuyDownFeeBalance buydownFeeBalance = loanBuydownFeeBalanceRepository + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, buyDownFeeTransactionId); if (buydownFeeBalance == null) { baseDataValidator.reset().parameter("buyDownFeeTransactionId").failWithCode("buydown.fee.balance.not.found", "Buy down fee balance not found for the specified transaction."); diff --git a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml index 8146f21db..72a418ca9 100644 --- a/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml +++ b/fineract-progressive-loan/src/main/resources/jpa/static-weaving/module/fineract-progressive-loan/persistence.xml @@ -123,6 +123,7 @@ org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain org.apache.fineract.portfolio.collateral.domain.LoanCollateral + org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping org.apache.fineract.portfolio.loanproduct.domain.AllocationTypeListConverter diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationServiceImpl.java new file mode 100644 index 000000000..6932d26cb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAmortizationAllocationServiceImpl.java @@ -0,0 +1,124 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationBaseTransactionDTO; +import org.apache.fineract.portfolio.loanaccount.data.AmortizationAllocationMappingDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData.AmortizationMappingData; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; +import org.springframework.dao.EmptyResultDataAccessException; + +@RequiredArgsConstructor +public class LoanAmortizationAllocationServiceImpl implements LoanAmortizationAllocationService { + + private final LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository; + private final LoanTransactionRepository loanTransactionRepository; + private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; + private final LoanBuyDownFeeBalanceRepository buyDownFeeBalanceRepository; + + @Override + public LoanAmortizationAllocationData retrieveLoanAmortizationAllocationsForBuyDownFeeTransaction(final Long loanTransactionId, + final Long loanId) { + final LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(loanTransactionId, loanId) + .orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId)); + if (!LoanTransactionType.BUY_DOWN_FEE.equals(loanTransaction.getTypeOf())) { + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.buydown.fee.transaction", + "Transaction with ID " + loanTransactionId + " is not a Buy Down Fee transaction"); + } + return retrieveLoanAmortizationAllocationData(loanTransaction, loanId); + } + + @Override + public LoanAmortizationAllocationData retrieveLoanAmortizationAllocationsForCapitalizedIncomeTransaction(final Long loanTransactionId, + final Long loanId) { + final LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(loanTransactionId, loanId) + .orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId)); + if (!LoanTransactionType.CAPITALIZED_INCOME.equals(loanTransaction.getTypeOf())) { + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.capitalized.income.transaction", + "Transaction with ID " + loanTransactionId + " is not a Capitalized Income transaction"); + } + return retrieveLoanAmortizationAllocationData(loanTransaction, loanId); + } + + private LoanAmortizationAllocationData retrieveLoanAmortizationAllocationData(final LoanTransaction loanTransaction, + final Long loanId) { + try { + final Long loanTransactionId = loanTransaction.getId(); + final AmortizationAllocationBaseTransactionDTO baseTransactionInfo = getBaseTransactionInfo(loanTransaction, loanId); + if (baseTransactionInfo == null) { + throw new LoanTransactionNotFoundException(loanTransactionId); + } + + final List amortizationMappings = loanAmortizationAllocationMappingRepository + .findAmortizationMappingsByBaseTransactionAndLoan(loanTransactionId, loanId); + + final List mappings = amortizationMappings.stream() + .map(dto -> AmortizationMappingData.builder().amortizationLoanTransactionId(dto.getAmortizationLoanTransactionId()) + .amortizationLoanTransactionExternalId(dto.getAmortizationLoanTransactionExternalId()) + .date(dto.getAmortizationDate()).type(dto.getAmortizationType()).amount(dto.getAmount()).build()) + .toList(); + + return new LoanAmortizationAllocationData(baseTransactionInfo.getLoanId(), baseTransactionInfo.getLoanExternalId(), + baseTransactionInfo.getBaseLoanTransactionId(), baseTransactionInfo.getBaseLoanTransactionDate(), + baseTransactionInfo.getBaseLoanTransactionAmount(), baseTransactionInfo.getUnrecognizedAmount(), + baseTransactionInfo.getChargedOffAmount(), baseTransactionInfo.getAdjustmentAmount(), mappings); + } catch (final EmptyResultDataAccessException e) { + throw new LoanTransactionNotFoundException(loanTransaction.getId(), e); + } + } + + private AmortizationAllocationBaseTransactionDTO getBaseTransactionInfo(final LoanTransaction loanTransaction, final Long loanId) { + if (loanTransaction.isBuyDownFee()) { + return buyDownFeeBalanceRepository.findBaseTransactionInfo(loanTransaction.getId(), loanId); + } else { + return capitalizedIncomeBalanceRepository.findBaseTransactionInfo(loanTransaction.getId(), loanId); + } + } + + @Override + public LoanAmortizationAllocationMapping createAmortizationAllocationMappingWithBaseLoanTransaction( + final LoanTransaction loanTransaction, final BigDecimal amount, final AmortizationType amortizationType) { + return new LoanAmortizationAllocationMapping(loanTransaction.getLoan().getId(), loanTransaction.getId(), null, null, + amortizationType, amount); + } + + @Override + public void setAmortizationTransactionDataAndSaveAmortizationAllocationMapping( + final LoanAmortizationAllocationMapping amortizationAllocationMapping, final LoanTransaction amortizationTransaction) { + final LoanAmortizationAllocationMapping updatedMapping = new LoanAmortizationAllocationMapping( + amortizationAllocationMapping.getLoanId(), amortizationAllocationMapping.getBaseLoanTransactionId(), + amortizationTransaction.getTransactionDate(), amortizationTransaction.getId(), + amortizationAllocationMapping.getAmortizationType(), amortizationAllocationMapping.getAmount()); + loanAmortizationAllocationMappingRepository.save(updatedMapping); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java index 5a2d3f304..7bf31ef0b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBuyDownFeeAmortizationProcessingServiceImpl.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -33,7 +34,10 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; @@ -54,34 +58,83 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD private final BusinessEventNotifierService businessEventNotifierService; private final LoanJournalEntryPoster journalEntryPoster; private final ExternalIdFactory externalIdFactory; + private final LoanAmortizationAllocationService loanAmortizationAllocationService; @Override @Transactional public void processBuyDownFeeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate, boolean addJournal) { - List balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId()); + final List balances = loanBuyDownFeeBalanceRepository.findAllByLoanIdAndClosedFalse(loan.getId()); - LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() + final LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : getFinalBuyDownFeeAmortizationTransactionDate(loan); LocalDate tillDatePlusOne = tillDate.plusDays(1); if (tillDatePlusOne.isAfter(maturityDate)) { tillDatePlusOne = maturityDate; } + final List loanAmortizationAllocationMappings = new ArrayList<>(); Money totalAmortization = Money.zero(loan.getCurrency()); + final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan); + BigDecimal totalAmortizedAccumulator = totalAmortized; for (LoanBuyDownFeeBalance balance : balances) { - List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); - Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, maturityDate, - loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), tillDatePlusOne, loan.getCurrency()); - totalAmortization = totalAmortization.add(amortizationTillDate); - - balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) - .subtract(amortizationTillDate.getAmount())); + BigDecimal amortizationAmount; + AmortizationType amortizationType; + if (!balance.isDeleted()) { + final List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); + final Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, + maturityDate, loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), tillDatePlusOne, loan.getCurrency()); + totalAmortization = totalAmortization.add(amortizationTillDate); + if (!adjustments.isEmpty()) { + BigDecimal alreadyAmortizedAmount = balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(MathUtil.nullToZero(balance.getChargedOffAmount())).subtract(balance.getUnrecognizedAmount()); + if (alreadyAmortizedAmount.compareTo(BigDecimal.ZERO) == 0) { + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForBuyDownFeeTransaction(balance.getLoanTransaction().getId(), + loan.getId()); + BigDecimal amortizedAmount = BigDecimal.ZERO; + for (LoanAmortizationAllocationData.AmortizationMappingData loanAmortizationMapping : loanAmortizationAllocationData + .getAmortizationMappings()) { + if (AmortizationType.AM.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.add(loanAmortizationMapping.getAmount()); + } else if (AmortizationType.AM_ADJ.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.subtract(loanAmortizationMapping.getAmount()); + } + } + amortizationAmount = amortizedAmount; + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else if (alreadyAmortizedAmount.compareTo(amortizationTillDate.getAmount()) > 0) { + amortizationAmount = alreadyAmortizedAmount.subtract(amortizationTillDate.getAmount()); + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else { + amortizationAmount = amortizationTillDate.getAmount().subtract(alreadyAmortizedAmount); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + } else { + amortizationAmount = totalAmortization.getAmount().subtract(totalAmortizedAccumulator); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(amortizationTillDate.getAmount())); + } else { + amortizationAmount = balance.getAmount().subtract(balance.getUnrecognizedAmount()); + amortizationType = AmortizationType.AM_ADJ; + balance.setClosed(true); + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } + if (amortizationAmount.compareTo(BigDecimal.ZERO) > 0) { + final LoanAmortizationAllocationMapping loanAmortizationAllocationMapping = loanAmortizationAllocationService + .createAmortizationAllocationMappingWithBaseLoanTransaction(balance.getLoanTransaction(), amortizationAmount, + amortizationType); + loanAmortizationAllocationMappings.add(loanAmortizationAllocationMapping); + } } loanBuyDownFeeBalanceRepository.saveAll(balances); - - BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan); - BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized); + final BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized); if (!MathUtil.isZero(totalAmortizationAmount)) { LoanTransaction transaction = MathUtil.isGreaterThanZero(totalAmortizationAmount) @@ -91,14 +144,17 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD Money.of(loan.getCurrency(), MathUtil.negate(totalAmortizationAmount)), tillDate, externalIdFactory.create()); loan.addLoanTransaction(transaction); - transaction = loanTransactionRepository.save(transaction); - loanTransactionRepository.flush(); + transaction = loanTransactionRepository.saveAndFlush(transaction); + final LoanTransaction finalTransaction = transaction; + loanAmortizationAllocationMappings.forEach(loanAmortizationAllocationMapping -> loanAmortizationAllocationService + .setAmortizationTransactionDataAndSaveAmortizationAllocationMapping(loanAmortizationAllocationMapping, + finalTransaction)); if (addJournal) { journalEntryPoster.postJournalEntriesForLoanTransaction(transaction, false, false); } - BusinessEvent event = MathUtil.isGreaterThanZero(totalAmortizationAmount) + final BusinessEvent event = MathUtil.isGreaterThanZero(totalAmortizationAmount) ? new LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(transaction) : new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(transaction); businessEventNotifierService.notifyPostBusinessEvent(event); @@ -165,7 +221,7 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); }); - for (LoanBuyDownFeeBalance balance : loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId())) { + for (LoanBuyDownFeeBalance balance : loanBuyDownFeeBalanceRepository.findAllByLoanIdAndDeletedFalseAndClosedFalse(loan.getId())) { balance.setUnrecognizedAmount(balance.getChargedOffAmount()); balance.setChargedOffAmount(BigDecimal.ZERO); } @@ -173,25 +229,75 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD private Optional createBuyDownFeeAmortizationTransaction(final Loan loan, final LocalDate transactionDate, final boolean isChargeOff, final LoanTransaction chargeOffTransaction) { - ExternalId externalId = externalIdFactory.create(); + final ExternalId externalId = externalIdFactory.create(); - final List balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId()); + final List balances = loanBuyDownFeeBalanceRepository.findAllByLoanIdAndClosedFalse(loan.getId()); + final List loanAmortizationAllocationMappings = new ArrayList<>(); - BigDecimal totalAmortizationAmount = BigDecimal.ZERO; + BigDecimal totalAmortization = BigDecimal.ZERO; + final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan); + BigDecimal totalAmortizedAccumulator = totalAmortized; for (LoanBuyDownFeeBalance balance : balances) { - List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); - LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate; - final Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, - maturityDate, loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), maturityDate, loan.getCurrency()); - totalAmortizationAmount = totalAmortizationAmount.add(amortizationTillDate.getAmount()); - if (isChargeOff) { - balance.setChargedOffAmount(balance.getUnrecognizedAmount()); + BigDecimal amortizationAmount; + AmortizationType amortizationType; + if (!balance.isDeleted()) { + final List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); + final LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate; + final Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, + maturityDate, loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), maturityDate, loan.getCurrency()); + totalAmortization = totalAmortization.add(amortizationTillDate.getAmount()); + if (!adjustments.isEmpty()) { + BigDecimal alreadyAmortizedAmount = balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(MathUtil.nullToZero(balance.getChargedOffAmount())).subtract(balance.getUnrecognizedAmount()); + if (alreadyAmortizedAmount.compareTo(BigDecimal.ZERO) == 0) { + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForBuyDownFeeTransaction(balance.getLoanTransaction().getId(), + loan.getId()); + BigDecimal amortizedAmount = BigDecimal.ZERO; + for (LoanAmortizationAllocationData.AmortizationMappingData loanAmortizationMapping : loanAmortizationAllocationData + .getAmortizationMappings()) { + if (AmortizationType.AM.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.add(loanAmortizationMapping.getAmount()); + } else if (AmortizationType.AM_ADJ.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.subtract(loanAmortizationMapping.getAmount()); + } + } + amortizationAmount = amortizedAmount; + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else if (alreadyAmortizedAmount.compareTo(amortizationTillDate.getAmount()) > 0) { + amortizationAmount = alreadyAmortizedAmount.subtract(amortizationTillDate.getAmount()); + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else { + amortizationAmount = amortizationTillDate.getAmount().subtract(alreadyAmortizedAmount); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + } else { + amortizationAmount = totalAmortization.subtract(totalAmortizedAccumulator); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + if (isChargeOff) { + balance.setChargedOffAmount(balance.getUnrecognizedAmount()); + } + balance.setUnrecognizedAmount(BigDecimal.ZERO); + } else { + amortizationAmount = balance.getAmount().subtract(balance.getUnrecognizedAmount()); + amortizationType = AmortizationType.AM_ADJ; + balance.setClosed(true); + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } + if (amortizationAmount.compareTo(BigDecimal.ZERO) > 0) { + final LoanAmortizationAllocationMapping loanAmortizationAllocationMapping = loanAmortizationAllocationService + .createAmortizationAllocationMappingWithBaseLoanTransaction(balance.getLoanTransaction(), amortizationAmount, + amortizationType); + loanAmortizationAllocationMappings.add(loanAmortizationAllocationMapping); } - balance.setUnrecognizedAmount(BigDecimal.ZERO); } - BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan); - BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount); + final BigDecimal totalUnrecognizedAmount = totalAmortization.subtract(totalAmortized); if (MathUtil.isZero(totalUnrecognizedAmount)) { return Optional.empty(); } @@ -207,6 +313,9 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD loan.addLoanTransaction(amortizationTransaction); loanTransactionRepository.saveAndFlush(amortizationTransaction); + loanAmortizationAllocationMappings.forEach(loanAmortizationAllocationMapping -> loanAmortizationAllocationService + .setAmortizationTransactionDataAndSaveAmortizationAllocationMapping(loanAmortizationAllocationMapping, + amortizationTransaction)); return Optional.of(amortizationTransaction); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java index e9ed6b495..045227e1a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -34,7 +35,10 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.LoanAmortizationAllocationData; +import org.apache.fineract.portfolio.loanaccount.domain.AmortizationType; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; @@ -56,6 +60,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L private final BusinessEventNotifierService businessEventNotifierService; private final LoanJournalEntryPoster journalEntryPoster; private final ExternalIdFactory externalIdFactory; + private final LoanAmortizationAllocationService loanAmortizationAllocationService; @Override @Transactional @@ -108,23 +113,75 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L externalId = ExternalId.generate(); } - final List balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId()); + final List balances = loanCapitalizedIncomeBalanceRepository + .findAllByLoanIdAndClosedFalse(loan.getId()); + final List loanAmortizationAllocationMappings = new ArrayList<>(); - BigDecimal totalAmortizationAmount = BigDecimal.ZERO; + BigDecimal totalAmortization = BigDecimal.ZERO; + final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan); + BigDecimal totalAmortizedAccumulator = totalAmortized; for (LoanCapitalizedIncomeBalance balance : balances) { - List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); - LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate; - final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, - maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency()); - totalAmortizationAmount = totalAmortizationAmount.add(amortizationTillDate.getAmount()); - if (isChargeOff) { - balance.setChargedOffAmount(balance.getUnrecognizedAmount()); + BigDecimal amortizationAmount; + AmortizationType amortizationType; + if (!balance.isDeleted()) { + final List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); + final LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate; + final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, + adjustments, maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, + loan.getCurrency()); + totalAmortization = totalAmortization.add(amortizationTillDate.getAmount()); + if (!adjustments.isEmpty()) { + BigDecimal alreadyAmortizedAmount = balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(MathUtil.nullToZero(balance.getChargedOffAmount())).subtract(balance.getUnrecognizedAmount()); + if (alreadyAmortizedAmount.compareTo(BigDecimal.ZERO) == 0) { + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForCapitalizedIncomeTransaction(balance.getLoanTransaction().getId(), + loan.getId()); + BigDecimal amortizedAmount = BigDecimal.ZERO; + for (LoanAmortizationAllocationData.AmortizationMappingData loanAmortizationMapping : loanAmortizationAllocationData + .getAmortizationMappings()) { + if (AmortizationType.AM.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.add(loanAmortizationMapping.getAmount()); + } else if (AmortizationType.AM_ADJ.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.subtract(loanAmortizationMapping.getAmount()); + } + } + amortizationAmount = amortizedAmount; + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else if (alreadyAmortizedAmount.compareTo(amortizationTillDate.getAmount()) > 0) { + amortizationAmount = alreadyAmortizedAmount.subtract(amortizationTillDate.getAmount()); + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else { + amortizationAmount = amortizationTillDate.getAmount().subtract(alreadyAmortizedAmount); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + } else { + amortizationAmount = totalAmortization.subtract(totalAmortizedAccumulator); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + if (isChargeOff) { + balance.setChargedOffAmount(balance.getUnrecognizedAmount()); + } + balance.setUnrecognizedAmount(BigDecimal.ZERO); + } else { + amortizationAmount = balance.getAmount().subtract(balance.getUnrecognizedAmount()); + amortizationType = AmortizationType.AM_ADJ; + balance.setClosed(true); + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } + if (amortizationAmount.compareTo(BigDecimal.ZERO) > 0) { + final LoanAmortizationAllocationMapping loanAmortizationAllocationMapping = loanAmortizationAllocationService + .createAmortizationAllocationMappingWithBaseLoanTransaction(balance.getLoanTransaction(), amortizationAmount, + amortizationType); + loanAmortizationAllocationMappings.add(loanAmortizationAllocationMapping); } - balance.setUnrecognizedAmount(BigDecimal.ZERO); } - BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan); - BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount); + final BigDecimal totalUnrecognizedAmount = totalAmortization.subtract(totalAmortized); if (MathUtil.isZero(totalUnrecognizedAmount)) { return Optional.empty(); } @@ -141,6 +198,9 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L loan.addLoanTransaction(amortizationTransaction); loanTransactionRepository.saveAndFlush(amortizationTransaction); + loanAmortizationAllocationMappings.forEach(loanAmortizationAllocationMapping -> loanAmortizationAllocationService + .setAmortizationTransactionDataAndSaveAmortizationAllocationMapping(loanAmortizationAllocationMapping, + amortizationTransaction)); return Optional.of(amortizationTransaction); } @@ -162,7 +222,8 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L journalEntryPoster.postJournalEntriesForLoanTransaction(transaction, false, false); }); - for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) { + for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository + .findAllByLoanIdAndDeletedFalseAndClosedFalse(loan.getId())) { balance.setUnrecognizedAmount(balance.getChargedOffAmount()); balance.setChargedOffAmount(BigDecimal.ZERO); } @@ -172,30 +233,80 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L @Transactional public void processCapitalizedIncomeAmortizationTillDate(@NonNull final Loan loan, @NonNull final LocalDate tillDate, final boolean addJournal) { - List balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId()); + final List balances = loanCapitalizedIncomeBalanceRepository + .findAllByLoanIdAndClosedFalse(loan.getId()); - LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() + final LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : getFinalCapitalizedIncomeAmortizationTransactionDate(loan); LocalDate tillDatePlusOne = tillDate.plusDays(1); if (tillDatePlusOne.isAfter(maturityDate)) { tillDatePlusOne = maturityDate; } + final List loanAmortizationAllocationMappings = new ArrayList<>(); Money totalAmortization = Money.zero(loan.getCurrency()); + final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan); + BigDecimal totalAmortizedAccumulator = totalAmortized; for (LoanCapitalizedIncomeBalance balance : balances) { - List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); - Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, - maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency()); - totalAmortization = totalAmortization.add(amortizationTillDate); - - balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) - .subtract(amortizationTillDate.getAmount())); + BigDecimal amortizationAmount; + AmortizationType amortizationType; + if (!balance.isDeleted()) { + final List adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction()); + final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, + adjustments, maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, + loan.getCurrency()); + totalAmortization = totalAmortization.add(amortizationTillDate); + if (!adjustments.isEmpty()) { + BigDecimal alreadyAmortizedAmount = balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(MathUtil.nullToZero(balance.getChargedOffAmount())).subtract(balance.getUnrecognizedAmount()); + if (alreadyAmortizedAmount.compareTo(BigDecimal.ZERO) == 0) { + final LoanAmortizationAllocationData loanAmortizationAllocationData = loanAmortizationAllocationService + .retrieveLoanAmortizationAllocationsForCapitalizedIncomeTransaction(balance.getLoanTransaction().getId(), + loan.getId()); + BigDecimal amortizedAmount = BigDecimal.ZERO; + for (LoanAmortizationAllocationData.AmortizationMappingData loanAmortizationMapping : loanAmortizationAllocationData + .getAmortizationMappings()) { + if (AmortizationType.AM.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.add(loanAmortizationMapping.getAmount()); + } else if (AmortizationType.AM_ADJ.equals(loanAmortizationMapping.getType())) { + amortizedAmount = amortizedAmount.subtract(loanAmortizationMapping.getAmount()); + } + } + amortizationAmount = amortizedAmount; + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else if (alreadyAmortizedAmount.compareTo(amortizationTillDate.getAmount()) > 0) { + amortizationAmount = alreadyAmortizedAmount.subtract(amortizationTillDate.getAmount()); + amortizationType = AmortizationType.AM_ADJ; + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } else { + amortizationAmount = amortizationTillDate.getAmount().subtract(alreadyAmortizedAmount); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + } else { + amortizationAmount = totalAmortization.getAmount().subtract(totalAmortizedAccumulator); + amortizationType = AmortizationType.AM; + totalAmortizedAccumulator = totalAmortizedAccumulator.add(amortizationAmount); + } + balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment())) + .subtract(amortizationTillDate.getAmount())); + } else { + amortizationAmount = balance.getAmount().subtract(balance.getUnrecognizedAmount()); + amortizationType = AmortizationType.AM_ADJ; + balance.setClosed(true); + totalAmortizedAccumulator = totalAmortizedAccumulator.subtract(amortizationAmount); + } + if (amortizationAmount.compareTo(BigDecimal.ZERO) > 0) { + final LoanAmortizationAllocationMapping loanAmortizationAllocationMapping = loanAmortizationAllocationService + .createAmortizationAllocationMappingWithBaseLoanTransaction(balance.getLoanTransaction(), amortizationAmount, + amortizationType); + loanAmortizationAllocationMappings.add(loanAmortizationAllocationMapping); + } } loanCapitalizedIncomeBalanceRepository.saveAll(balances); - - BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan); - BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized); + final BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized); if (!MathUtil.isZero(totalAmortizationAmount)) { LoanTransaction transaction = MathUtil.isGreaterThanZero(totalAmortizationAmount) @@ -205,14 +316,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L Money.of(loan.getCurrency(), MathUtil.negate(totalAmortizationAmount)), tillDate, externalIdFactory.create()); loan.addLoanTransaction(transaction); - transaction = loanTransactionRepository.save(transaction); - loanTransactionRepository.flush(); + transaction = loanTransactionRepository.saveAndFlush(transaction); + final LoanTransaction finalTransaction = transaction; + loanAmortizationAllocationMappings.forEach(loanAmortizationAllocationMapping -> loanAmortizationAllocationService + .setAmortizationTransactionDataAndSaveAmortizationAllocationMapping(loanAmortizationAllocationMapping, + finalTransaction)); if (addJournal) { journalEntryPoster.postJournalEntriesForLoanTransaction(transaction, false, false); } - BusinessEvent event = MathUtil.isGreaterThanZero(totalAmortizationAmount) + final BusinessEvent event = MathUtil.isGreaterThanZero(totalAmortizationAmount) ? new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(transaction) : new LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent(transaction); businessEventNotifierService.notifyPostBusinessEvent(event); 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 1e6309757..4f4d1f1e3 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 @@ -493,8 +493,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); BigDecimal capitalizedIncomeBalance = BigDecimal.ZERO; if (loan.getLoanProduct().getLoanProductRelatedDetail().isEnableIncomeCapitalization()) { - capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream() - .map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId) + .stream().map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); } transactionAmount = loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied() ? loanMaximumAmountCalculator.getOverAppliedMax(loan) @@ -517,7 +517,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa break; case CAPITALIZED_INCOME_ADJUSTMENT: final LoanCapitalizedIncomeBalance loanCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository - .findByLoanIdAndLoanTransactionId(loanId, transactionId); + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, transactionId); transactionAmount = (loanCapitalizedIncomeBalance == null) ? BigDecimal.ZERO : loanCapitalizedIncomeBalance.getAmount() @@ -527,8 +527,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa paymentOptions, retriveLoanCurrencyData(loanId), classificationOptions); break; case BUY_DOWN_FEE_ADJUSTMENT: - final LoanBuyDownFeeBalance loanBuyDownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId, - transactionId); + final LoanBuyDownFeeBalance loanBuyDownFeeBalance = loanBuyDownFeeBalanceRepository + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loanId, transactionId); transactionAmount = (loanBuyDownFeeBalance == null) ? BigDecimal.ZERO : loanBuyDownFeeBalance.getAmount().subtract(MathUtil.nullToZero(loanBuyDownFeeBalance.getAmountAdjustment())); @@ -2522,6 +2522,19 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa return resolvedLoanId; } + @Override + public Long getResolvedLoanTransactionId(final Long transactionId, final ExternalId externalTransactionId) { + Long resolvedLoanTransactionId = transactionId; + if (resolvedLoanTransactionId == null) { + externalTransactionId.throwExceptionIfEmpty(); + resolvedLoanTransactionId = retrieveLoanTransactionIdByExternalId(externalTransactionId); + if (resolvedLoanTransactionId == null) { + throw new LoanTransactionNotFoundException(externalTransactionId); + } + } + return resolvedLoanTransactionId; + } + private LoanTransaction deriveDefaultInterestWaiverTransaction(final Loan loan) { final Money totalInterestOutstanding = loan.getTotalInterestOutstandingOnLoan(); Money possibleInterestToWaive = totalInterestOutstanding.copy(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java index 14c0c21f1..b94727832 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java @@ -147,12 +147,13 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { } LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository - .findByLoanIdAndLoanTransactionId(loan.getId(), transactionToAdjust.getId()); + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loan.getId(), transactionToAdjust.getId()); if (MathUtil.isGreaterThanZero(capitalizedIncomeBalance.getAmountAdjustment())) { throw new InvalidLoanTransactionTypeException("transaction", "capitalizedIncome.cannot.be.reversed.when.adjusted", "Capitalized income transaction cannot be reversed when non-reversed adjustment exists for it."); } - loanCapitalizedIncomeBalanceRepository.delete(capitalizedIncomeBalance); + capitalizedIncomeBalance.setDeleted(true); + loanCapitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance); } if (transactionToAdjust.isCapitalizedIncomeAdjustment()) { if (newTransactionDetail.isNotZero()) { @@ -174,14 +175,15 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { "Buy down fee transaction cannot be adjusted"); } - LoanBuyDownFeeBalance buyDownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loan.getId(), - transactionToAdjust.getId()); + LoanBuyDownFeeBalance buyDownFeeBalance = loanBuyDownFeeBalanceRepository + .findByLoanIdAndLoanTransactionIdAndDeletedFalseAndClosedFalse(loan.getId(), transactionToAdjust.getId()); if (MathUtil.isGreaterThanZero(buyDownFeeBalance.getAmountAdjustment())) { throw new InvalidLoanTransactionTypeException("transaction", "buy.down.fee.cannot.be.reversed.when.adjusted", "Buy down fee transaction cannot be reversed when non-reversed adjustment exists for it."); } - loanBuyDownFeeBalanceRepository.delete(buyDownFeeBalance); + buyDownFeeBalance.setDeleted(true); + loanBuyDownFeeBalanceRepository.saveAndFlush(buyDownFeeBalance); } if (transactionToAdjust.isBuyDownFeeAdjustment()) { if (newTransactionDetail.isNotZero()) { 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 c55a94cfd..f7dfa1329 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 @@ -70,6 +70,7 @@ import org.apache.fineract.portfolio.interestpauses.service.InterestPauseWritePl import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; @@ -118,6 +119,8 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualEventService import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAmortizationAllocationService; +import org.apache.fineract.portfolio.loanaccount.service.LoanAmortizationAllocationServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService; @@ -566,9 +569,10 @@ public class LoanAccountConfiguration { final ConfigurationDomainService configurationDomainService, final LoanTransactionRepository loanTransactionRepository, final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository, final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster, - final ExternalIdFactory externalIdFactory) { + final ExternalIdFactory externalIdFactory, final LoanAmortizationAllocationService loanAmortizationAllocationService) { return new LoanCapitalizedIncomeAmortizationProcessingServiceImpl(configurationDomainService, loanTransactionRepository, - loanCapitalizedIncomeBalanceRepository, businessEventNotifierService, journalEntryPoster, externalIdFactory); + loanCapitalizedIncomeBalanceRepository, businessEventNotifierService, journalEntryPoster, externalIdFactory, + loanAmortizationAllocationService); } @Bean @@ -577,9 +581,9 @@ public class LoanAccountConfiguration { final LoanTransactionRepository loanTransactionRepository, final LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster, - final ExternalIdFactory externalIdFactory) { + final ExternalIdFactory externalIdFactory, final LoanAmortizationAllocationService loanAmortizationAllocationService) { return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeeBalanceRepository, - businessEventNotifierService, journalEntryPoster, externalIdFactory); + businessEventNotifierService, journalEntryPoster, externalIdFactory, loanAmortizationAllocationService); } @Bean @@ -589,4 +593,15 @@ public class LoanAccountConfiguration { LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService) { return new LoanBuyDownFeeAmortizationEventService(businessEventNotifierService, loanBuyDownFeeAmortizationProcessingService); } + + @Bean + @ConditionalOnMissingBean(LoanAmortizationAllocationService.class) + public LoanAmortizationAllocationService loanAmortizationAllocationService( + final LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository, + final LoanTransactionRepository loanTransactionRepository, + final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository, + final LoanBuyDownFeeBalanceRepository buyDownFeeBalanceRepository) { + return new LoanAmortizationAllocationServiceImpl(loanAmortizationAllocationMappingRepository, loanTransactionRepository, + capitalizedIncomeBalanceRepository, buyDownFeeBalanceRepository); + } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 94ffeb11b..2cd6e8dea 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -213,4 +213,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0195_create_loan_amortization_allocation_mapping.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0195_create_loan_amortization_allocation_mapping.xml new file mode 100644 index 000000000..31ee2c26a --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0195_create_loan_amortization_allocation_mapping.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml new file mode 100644 index 000000000..70b9f0d45 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml new file mode 100644 index 000000000..298fdb8a0 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + +