FINERACT-2348: Calculate and store Capitalized income / Buydown fee amortization allocation schedule

This commit is contained in:
mariiaKraievska
2025-09-08 14:00:06 +02:00
committed by Adam Saghy
parent 9790f59e4a
commit cd0ecfcda0
34 changed files with 2094 additions and 91 deletions
@@ -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<PostLoansResponse> 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<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
assert loanDetailsResponse.body() != null;
final List<GetLoansLoanIdTransactions> 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<PostLoansLoanIdTransactionsResponse> 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<PostLoansResponse> 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<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanResponse.body().getLoanId();
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
final List<GetLoansLoanIdTransactions> 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<PostLoansLoanIdTransactionsResponse> 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<PostLoansResponse> 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<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
final long loanId = loanCreateResponse.body().getLoanId();
final String resourceId = String.valueOf(loanId);
final Response<GetLoansLoanIdResponse> 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<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
assert transactions != null;
final List<GetLoansLoanIdTransactions> 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> 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<List<String>> data = table.asLists();
for (int i = 1; i < data.size(); i++) {
final List<String> expectedValues = data.get(i);
assert amortizationAllocationResponse.getAmortizationMappings() != null;
final boolean found = amortizationAllocationResponse.getAmortizationMappings().stream().anyMatch(t -> {
final List<String> 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<String> fetchValuesOfAmortizationAllocationMapping(final List<String> header, final AmortizationMappingData t) {
final List<String> 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;
}
}
@@ -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 |
@@ -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 |
@@ -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<AmortizationMappingData> 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;
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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<AmortizationMappingData> 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;
}
}
@@ -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<AmortizationType> {
AM("amortizationTransaction.type.amortization", "Amortization"), //
AM_ADJ("amortizationTransaction.type.amortizationAdjustment", "Amortization Adjustment"); //
private final String code;
private final String humanReadableName;
}
@@ -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<Long> {
@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;
}
@@ -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<LoanAmortizationAllocationMapping, Long>, JpaSpecificationExecutor<LoanAmortizationAllocationMapping> {
@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<AmortizationAllocationMappingDTO> findAmortizationMappingsByBaseTransactionAndLoan(
@Param("baseLoanTransactionId") Long baseLoanTransactionId, @Param("loanId") Long loanId);
}
@@ -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);
}
@@ -163,4 +163,6 @@ public interface LoanReadPlatformService {
Long getResolvedLoanId(ExternalId loanExternalId);
Long getResolvedLoanTransactionId(Long transactionId, ExternalId externalTransactionId);
}
@@ -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<String> 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<LoanAmortizationAllocationData> 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);
}
}
@@ -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<String> 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<LoanAmortizationAllocationData> 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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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<LoanBuyDownFeeBalance, Long>, JpaSpecificationExecutor<LoanBuyDownFeeBalance> {
List<LoanBuyDownFeeBalance> 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<LoanBuyDownFeeBalance> 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<LoanBuyDownFeeBalance> 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);
}
@@ -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<LoanCapitalizedIncomeBalance, Long>,
JpaSpecificationExecutor<LoanCapitalizedIncomeBalance>, 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<LoanCapitalizedIncomeBalance> 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<LoanCapitalizedIncomeBalance> 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<LoanCapitalizedIncomeBalance> 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<LoanTransactionRepaymentPeriodData> 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);
}
@@ -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<LoanBuyDownFeeBalance> buyDownFeeBalances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loanId);
final List<LoanBuyDownFeeBalance> buyDownFeeBalances = loanBuyDownFeeBalanceRepository
.findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId);
return buyDownFeeBalances.stream().map(this::mapToLoanBuyDownFeeAmortizationData).collect(Collectors.toList());
}
@@ -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
@@ -43,7 +43,8 @@ public class CapitalizedIncomeBalanceReadServiceImpl implements CapitalizedIncom
if (loanRepository.isEnabledCapitalizedIncome(loanId)) {
List<CapitalizedIncomeDetails> capitalizedIncomeData = new ArrayList<>();
List<LoanCapitalizedIncomeBalance> capitalizedIncomeBalances = capitalizedIncomeBalanceRepository.findAllByLoanId(loanId);
List<LoanCapitalizedIncomeBalance> 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<CapitalizedIncomeDetails> capitalizedIncomeData = new ArrayList<>();
List<LoanCapitalizedIncomeBalance> capitalizedIncomeBalances = capitalizedIncomeBalanceRepository.findAllByLoanId(loanId);
List<LoanCapitalizedIncomeBalance> capitalizedIncomeBalances = capitalizedIncomeBalanceRepository
.findAllByLoanIdAndDeletedFalseAndClosedFalse(loanId);
for (final LoanCapitalizedIncomeBalance capitalizedIncomeBalance : capitalizedIncomeBalances) {
final BigDecimal amortizedAmount = capitalizedIncomeBalance.getAmount() //
.subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getUnrecognizedAmount())) //
@@ -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(
@@ -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.");
@@ -123,6 +123,7 @@
<class>org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement</class>
<class>org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain</class>
<class>org.apache.fineract.portfolio.collateral.domain.LoanCollateral</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping</class>
<!-- Loan Module Converters -->
<class>org.apache.fineract.portfolio.loanproduct.domain.AllocationTypeListConverter</class>
@@ -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<AmortizationAllocationMappingDTO> amortizationMappings = loanAmortizationAllocationMappingRepository
.findAmortizationMappingsByBaseTransactionAndLoan(loanTransactionId, loanId);
final List<AmortizationMappingData> 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);
}
}
@@ -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<LoanBuyDownFeeBalance> balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId());
final List<LoanBuyDownFeeBalance> 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<LoanAmortizationAllocationMapping> loanAmortizationAllocationMappings = new ArrayList<>();
Money totalAmortization = Money.zero(loan.getCurrency());
final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan);
BigDecimal totalAmortizedAccumulator = totalAmortized;
for (LoanBuyDownFeeBalance balance : balances) {
List<LoanTransaction> 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<LoanTransaction> 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<LoanTransaction> createBuyDownFeeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
final boolean isChargeOff, final LoanTransaction chargeOffTransaction) {
ExternalId externalId = externalIdFactory.create();
final ExternalId externalId = externalIdFactory.create();
final List<LoanBuyDownFeeBalance> balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId());
final List<LoanBuyDownFeeBalance> balances = loanBuyDownFeeBalanceRepository.findAllByLoanIdAndClosedFalse(loan.getId());
final List<LoanAmortizationAllocationMapping> 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<LoanTransaction> 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<LoanTransaction> 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);
}
@@ -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<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository
.findAllByLoanIdAndClosedFalse(loan.getId());
final List<LoanAmortizationAllocationMapping> 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<LoanTransaction> 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<LoanTransaction> 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<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
final List<LoanCapitalizedIncomeBalance> 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<LoanAmortizationAllocationMapping> loanAmortizationAllocationMappings = new ArrayList<>();
Money totalAmortization = Money.zero(loan.getCurrency());
final BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
BigDecimal totalAmortizedAccumulator = totalAmortized;
for (LoanCapitalizedIncomeBalance balance : balances) {
List<LoanTransaction> 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<LoanTransaction> 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);
@@ -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();
@@ -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()) {
@@ -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);
}
}
@@ -213,4 +213,7 @@
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
<include file="parts/0193_add_column_ip.xml" relativeToChangelogFile="true" />
<include file="parts/0194_fix_missing_permission.xml" relativeToChangelogFile="true" />
<include file="parts/0195_create_loan_amortization_allocation_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<createTable tableName="m_loan_amortization_allocation_mapping">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_amortization_allocation_mapping"/>
</column>
<column name="loan_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="base_loan_transaction_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="amortization_loan_transaction_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="amortization_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="timestamp with time zone"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="timestamp with time zone"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="2" context="mysql">
<createTable tableName="m_loan_amortization_allocation_mapping">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="pk_m_loan_amortization_allocation_mapping"/>
</column>
<column name="loan_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="base_loan_transaction_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="amortization_loan_transaction_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="amortization_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="amount" type="DECIMAL(19, 6)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="DATETIME"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="DATETIME"/>
</createTable>
</changeSet>
<changeSet author="fineract" id="3">
<addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan_amortization_allocation_mapping"
constraintName="FK_laam_created_by" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan_amortization_allocation_mapping"
constraintName="FK_laam_last_modified_by" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_appuser" validate="true"/>
<addForeignKeyConstraint baseColumnNames="loan_id" baseTableName="m_loan_amortization_allocation_mapping"
constraintName="FK_laam_loan_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_loan" validate="true"/>
<addForeignKeyConstraint baseColumnNames="base_loan_transaction_id" baseTableName="m_loan_amortization_allocation_mapping"
constraintName="FK_laam_base_transaction_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_loan_transaction" validate="true"/>
<addForeignKeyConstraint baseColumnNames="amortization_loan_transaction_id" baseTableName="m_loan_amortization_allocation_mapping"
constraintName="FK_laam_amortization_transaction_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="m_loan_transaction" validate="true"/>
</changeSet>
</databaseChangeLog>
@@ -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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan_buy_down_fee_balance">
<column defaultValueBoolean="false" name="is_deleted" type="boolean">
<constraints nullable="false"/>
</column>
<column defaultValueBoolean="false" name="is_closed" type="boolean">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_loan_capitalized_income_balance">
<column defaultValueBoolean="false" name="is_deleted" type="boolean">
<constraints nullable="false"/>
</column>
<column defaultValueBoolean="false" name="is_closed" type="boolean">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>