FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans
This commit is contained in:
+2
-2
@@ -43,8 +43,8 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createLoanNote(Long loanId, String note) {
|
public void createLoanTransactionNote(Long loanTransactionId, String note) {
|
||||||
throw new UnsupportedOperationException("createLoanNote() is not yet implemented.");
|
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -458,6 +458,30 @@
|
|||||||
"null",
|
"null",
|
||||||
"bigdecimal"
|
"bigdecimal"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": null,
|
||||||
|
"name": "totalPrincipal",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"bigdecimal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": null,
|
||||||
|
"name": "totalCapitalizedIncome",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"bigdecimal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": null,
|
||||||
|
"name": "totalCapitalizedIncomeAdjustment",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"bigdecimal"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -310,6 +310,12 @@ public final class DateUtils {
|
|||||||
return isBeforeBusinessDate(localDate);
|
return isBeforeBusinessDate(localDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDateInRangeFromInclusiveToExclusive(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
|
||||||
|
final LocalDate target) {
|
||||||
|
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
|
||||||
|
&& DateUtils.isBefore(target, upToNotInclusive);
|
||||||
|
}
|
||||||
|
|
||||||
public static int compare(LocalDate first, LocalDate second) {
|
public static int compare(LocalDate first, LocalDate second) {
|
||||||
return compare(first, second, true);
|
return compare(first, second, true);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ public interface NoteWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult createNote(JsonCommand command);
|
CommandProcessingResult createNote(JsonCommand command);
|
||||||
|
|
||||||
void createLoanNote(Long loanId, String note);
|
void createLoanTransactionNote(Long loanTransactionId, String note);
|
||||||
|
|
||||||
CommandProcessingResult updateNote(JsonCommand command);
|
CommandProcessingResult updateNote(JsonCommand command);
|
||||||
|
|
||||||
|
|||||||
+39
-39
@@ -188,12 +188,12 @@ Feature: Capitalized Income
|
|||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
||||||
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
|
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
|
||||||
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 900.17 EUR transaction amount and system-generated Idempotency key
|
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000.17 EUR transaction amount and system-generated Idempotency key
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 02 January 2024 | Repayment | 900.17 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false |
|
| 02 January 2024 | Repayment | 1000.17| 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
||||||
@@ -222,12 +222,12 @@ Feature: Capitalized Income
|
|||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
||||||
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
|
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
|
||||||
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000 EUR transaction amount and system-generated Idempotency key
|
When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1100 EUR transaction amount and system-generated Idempotency key
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 02 January 2024 | Repayment | 1000.0 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false |
|
| 02 January 2024 | Repayment | 1100.0 | 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
| 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
||||||
@@ -262,7 +262,7 @@ Feature: Capitalized Income
|
|||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 02 January 2024 | Close (as written-off) | 905.25 | 900.0 | 5.25 | 0.0 | 0.0 | 100.0 | false |
|
| 02 January 2024 | Close (as written-off) | 1005.81| 1000.0 | 5.81 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
@@ -555,16 +555,16 @@ Feature: Capitalized Income
|
|||||||
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
||||||
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
@@ -597,16 +597,16 @@ Feature: Capitalized Income
|
|||||||
And Admin does charge-off the loan on "03 February 2024"
|
And Admin does charge-off the loan on "03 February 2024"
|
||||||
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||||
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 900.0 | |
|
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 1000.0| |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
@@ -639,16 +639,16 @@ Feature: Capitalized Income
|
|||||||
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
||||||
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
@@ -681,16 +681,16 @@ Feature: Capitalized Income
|
|||||||
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024"
|
||||||
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false |
|
||||||
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
@@ -700,20 +700,20 @@ Feature: Capitalized Income
|
|||||||
Then Admin does a charge-off undo the loan
|
Then Admin does a charge-off undo the loan
|
||||||
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||||
| ASSET | 112601 | Loans Receivable | 900.0 | |
|
| ASSET | 112601 | Loans Receivable | 1000.0| |
|
||||||
| ASSET | 112603 | Interest/Fee Receivable | 4.0 | |
|
| ASSET | 112603 | Interest/Fee Receivable | 4.58 | |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 900.0 |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 |
|
||||||
| INCOME | 404001 | Interest Income Charge Off | | 4.0 |
|
| INCOME | 404001 | Interest Income Charge Off | | 4.58 |
|
||||||
Then Loan Transactions tab has the following data:
|
Then Loan Transactions tab has the following data:
|
||||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false |
|
||||||
| 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false |
|
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||||
| 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | true |
|
| 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | true |
|
||||||
Then Reversed loan capitalized income amortization transaction has the following Journal entries:
|
Then Reversed loan capitalized income amortization transaction has the following Journal entries:
|
||||||
| Type | Account code | Account name | Debit | Credit |
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
||||||
|
|||||||
+2
-3
@@ -30,7 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
|||||||
*/
|
*/
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public final class DisbursementData implements Comparable<DisbursementData> {
|
public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable<DisbursementData> {
|
||||||
|
|
||||||
private final Long id;
|
private final Long id;
|
||||||
private final LocalDate expectedDisbursementDate;
|
private final LocalDate expectedDisbursementDate;
|
||||||
@@ -102,8 +102,7 @@ public final class DisbursementData implements Comparable<DisbursementData> {
|
|||||||
|
|
||||||
private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
|
private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
|
||||||
final LocalDate target) {
|
final LocalDate target) {
|
||||||
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
|
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromInclusive, upToNotInclusive, target);
|
||||||
&& DateUtils.isBefore(target, upToNotInclusive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal getWaivedChargeAmount() {
|
public BigDecimal getWaivedChargeAmount() {
|
||||||
|
|||||||
+3
@@ -34,6 +34,9 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
|||||||
public class LoanSummaryData {
|
public class LoanSummaryData {
|
||||||
|
|
||||||
private final CurrencyData currency;
|
private final CurrencyData currency;
|
||||||
|
private final BigDecimal totalPrincipal;
|
||||||
|
private final BigDecimal totalCapitalizedIncome;
|
||||||
|
private final BigDecimal totalCapitalizedIncomeAdjustment;
|
||||||
private final BigDecimal principalDisbursed;
|
private final BigDecimal principalDisbursed;
|
||||||
private final BigDecimal principalAdjustments;
|
private final BigDecimal principalAdjustments;
|
||||||
private final BigDecimal principalPaid;
|
private final BigDecimal principalPaid;
|
||||||
|
|||||||
+20
-5
@@ -38,6 +38,15 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
|||||||
public class LoanSummary {
|
public class LoanSummary {
|
||||||
|
|
||||||
// derived totals fields
|
// derived totals fields
|
||||||
|
@Column(name = "total_principal_derived", scale = 6, precision = 19)
|
||||||
|
private BigDecimal totalPrincipal;
|
||||||
|
|
||||||
|
@Column(name = "capitalized_income_derived", scale = 6, precision = 19)
|
||||||
|
private BigDecimal totalCapitalizedIncome;
|
||||||
|
|
||||||
|
@Column(name = "capitalized_income_adjustment_derived", scale = 6, precision = 19)
|
||||||
|
private BigDecimal totalCapitalizedIncomeAdjustment;
|
||||||
|
|
||||||
@Column(name = "principal_disbursed_derived", scale = 6, precision = 19)
|
@Column(name = "principal_disbursed_derived", scale = 6, precision = 19)
|
||||||
private BigDecimal totalPrincipalDisbursed;
|
private BigDecimal totalPrincipalDisbursed;
|
||||||
|
|
||||||
@@ -166,6 +175,9 @@ public class LoanSummary {
|
|||||||
this.totalPenaltyChargesWaived = BigDecimal.ZERO;
|
this.totalPenaltyChargesWaived = BigDecimal.ZERO;
|
||||||
this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO;
|
this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO;
|
||||||
this.totalPrincipalAdjustments = BigDecimal.ZERO;
|
this.totalPrincipalAdjustments = BigDecimal.ZERO;
|
||||||
|
this.totalPrincipal = BigDecimal.ZERO;
|
||||||
|
this.totalCapitalizedIncome = BigDecimal.ZERO;
|
||||||
|
this.totalCapitalizedIncomeAdjustment = BigDecimal.ZERO;
|
||||||
this.totalPrincipalDisbursed = BigDecimal.ZERO;
|
this.totalPrincipalDisbursed = BigDecimal.ZERO;
|
||||||
this.totalPrincipalOutstanding = BigDecimal.ZERO;
|
this.totalPrincipalOutstanding = BigDecimal.ZERO;
|
||||||
this.totalPrincipalRepaid = BigDecimal.ZERO;
|
this.totalPrincipalRepaid = BigDecimal.ZERO;
|
||||||
@@ -176,17 +188,20 @@ public class LoanSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateSummary(final MonetaryCurrency currency, final Money principal,
|
public void updateSummary(final MonetaryCurrency currency, final Money principal,
|
||||||
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges) {
|
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges, Money capitalizedIncome,
|
||||||
|
Money capitalizedIncomeAdjustment) {
|
||||||
this.totalPrincipalDisbursed = principal.getAmount();
|
this.totalPrincipalDisbursed = principal.getAmount();
|
||||||
|
this.totalCapitalizedIncome = capitalizedIncome.getAmount();
|
||||||
|
this.totalCapitalizedIncomeAdjustment = capitalizedIncomeAdjustment.getAmount();
|
||||||
|
this.totalPrincipal = principal.plus(capitalizedIncome).getAmount();
|
||||||
this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||||
this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||||
this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||||
this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
|
this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
|
||||||
this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount();
|
this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount();
|
||||||
|
|
||||||
this.totalPrincipalOutstanding = principal.plus(this.totalPrincipalAdjustments).minus(this.totalPrincipalRepaid)
|
this.totalPrincipalOutstanding = principal.plus(capitalizedIncome).plus(this.totalPrincipalAdjustments)
|
||||||
.minus(this.totalPrincipalWrittenOff).getAmount();
|
.minus(this.totalPrincipalRepaid).minus(this.totalPrincipalWrittenOff).getAmount();
|
||||||
|
|
||||||
final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency);
|
final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency);
|
||||||
this.totalInterestCharged = totalInterestCharged.getAmount();
|
this.totalInterestCharged = totalInterestCharged.getAmount();
|
||||||
@@ -225,7 +240,7 @@ public class LoanSummary {
|
|||||||
this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid)
|
this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid)
|
||||||
.minus(this.totalPenaltyChargesWaived).minus(this.totalPenaltyChargesWrittenOff).getAmount();
|
.minus(this.totalPenaltyChargesWaived).minus(this.totalPenaltyChargesWrittenOff).getAmount();
|
||||||
|
|
||||||
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipalDisbursed).plus(this.totalInterestCharged)
|
final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipal).plus(this.totalInterestCharged)
|
||||||
.plus(this.totalFeeChargesCharged).plus(this.totalPenaltyChargesCharged);
|
.plus(this.totalFeeChargesCharged).plus(this.totalPenaltyChargesCharged);
|
||||||
this.totalExpectedRepayment = totalExpectedRepayment.getAmount();
|
this.totalExpectedRepayment = totalExpectedRepayment.getAmount();
|
||||||
|
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPrincipalRelatedDataHolder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable data object representing a subset of loan transaction data.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class LoanTransactionRepaymentPeriodData implements LoanPrincipalRelatedDataHolder, Serializable {
|
||||||
|
|
||||||
|
private final Long transactionId;
|
||||||
|
private final Long loanId;
|
||||||
|
private final LocalDate date;
|
||||||
|
private final boolean reversed;
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final BigDecimal unrecognizedAmount;
|
||||||
|
private final BigDecimal feeChargesPortion;
|
||||||
|
|
||||||
|
public LoanTransactionRepaymentPeriodData(Long transactionId, Long loanId, LocalDate date, boolean reversed, BigDecimal amount,
|
||||||
|
BigDecimal unrecognizedAmount, BigDecimal feeChargesPortion) {
|
||||||
|
this.transactionId = transactionId;
|
||||||
|
this.loanId = loanId;
|
||||||
|
this.date = date;
|
||||||
|
this.reversed = reversed;
|
||||||
|
this.amount = amount;
|
||||||
|
this.unrecognizedAmount = unrecognizedAmount;
|
||||||
|
this.feeChargesPortion = feeChargesPortion;
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -104,7 +104,10 @@ public final class LoanRefundValidator {
|
|||||||
if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) {
|
if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) {
|
||||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
|
||||||
|
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome);
|
||||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
|
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
|
||||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
|
public interface CapitalizedIncomeBalanceService {
|
||||||
|
|
||||||
|
Money calculateCapitalizedIncome(Loan loan);
|
||||||
|
|
||||||
|
Money calculateCapitalizedIncomeAdjustment(Loan loan);
|
||||||
|
}
|
||||||
+8
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -35,8 +36,11 @@ import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransact
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class LoanBalanceService {
|
public class LoanBalanceService {
|
||||||
|
|
||||||
|
private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService;
|
||||||
|
|
||||||
public Money calculateTotalOverpayment(final Loan loan) {
|
public Money calculateTotalOverpayment(final Loan loan) {
|
||||||
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
|
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
|
||||||
|
|
||||||
@@ -97,7 +101,10 @@ public class LoanBalanceService {
|
|||||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||||
|
|
||||||
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||||
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges());
|
final Money capitalizedIncome = capitalizedIncomeBalanceService.calculateCapitalizedIncome(loan);
|
||||||
|
final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan);
|
||||||
|
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(),
|
||||||
|
capitalizedIncome, capitalizedIncomeAdjustment);
|
||||||
loan.updateLoanOutstandingBalances();
|
loan.updateLoanOutstandingBalances();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -173,7 +173,10 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
|||||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||||
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||||
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream()
|
||||||
|
.filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome);
|
||||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
|
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
|
||||||
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
||||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||||
|
|||||||
+3
-1
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||||
@@ -50,7 +51,8 @@ public interface LoanReadPlatformService {
|
|||||||
LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData);
|
LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData);
|
||||||
|
|
||||||
LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData,
|
LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData,
|
||||||
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
|
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
|
||||||
|
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType);
|
||||||
|
|
||||||
Collection<LoanTransactionData> retrieveLoanTransactions(Long loanId);
|
Collection<LoanTransactionData> retrieveLoanTransactions(Long loanId);
|
||||||
|
|
||||||
|
|||||||
+71
-18
@@ -344,6 +344,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
||||||
case REAGE -> handleReAge(loanTransaction, ctx);
|
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
|
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
|
||||||
|
case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx);
|
||||||
// TODO: Cover rest of the transaction types
|
// TODO: Cover rest of the transaction types
|
||||||
default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||||
}
|
}
|
||||||
@@ -1199,18 +1200,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
|
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
|
||||||
|
|
||||||
disbursementTransaction.resetDerivedComponents();
|
disbursementTransaction.resetDerivedComponents();
|
||||||
if (amortizableAmount.isGreaterThanZero()) {
|
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||||
model.repaymentPeriods().forEach(rm -> {
|
|
||||||
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
|
||||||
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
if (installment != null) {
|
|
||||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
|
||||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
|
||||||
installment.updateObligationsMet(currency, transactionDate);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,7 +1212,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||||
.toList();
|
.toList();
|
||||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
|
||||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||||
Money downPaymentAmount = Money.zero(currency);
|
Money downPaymentAmount = Money.zero(currency);
|
||||||
@@ -1240,7 +1229,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
|
|
||||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||||
|
|
||||||
|
recalculateRepaymentInstallmentsWithoutEMICalculation(disbursementTransaction, amortizableAmount, candidateRepaymentInstallments,
|
||||||
|
currency, installmentAmountInMultiplesOf);
|
||||||
|
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||||
|
if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) {
|
||||||
|
handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||||
|
} else {
|
||||||
|
handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCapitalizedIncomeWithEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||||
|
ProgressiveLoanInterestScheduleModel model;
|
||||||
|
if (!(transactionCtx instanceof ProgressiveTransactionCtx)
|
||||||
|
|| (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) {
|
||||||
|
throw new IllegalStateException("TransactionCtx has no model");
|
||||||
|
}
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||||
|
LocalDate transactionDate = capitalizedIncomeTransaction.getTransactionDate();
|
||||||
|
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||||
|
|
||||||
|
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
||||||
|
emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount);
|
||||||
|
|
||||||
|
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||||
|
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCapitalizedIncomeWithoutEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) {
|
||||||
|
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||||
|
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||||
|
i -> i.getDueDate().isAfter(capitalizedIncomeTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||||
|
.toList();
|
||||||
|
Integer installmentAmountInMultiplesOf = capitalizedIncomeTransaction.getLoan().getLoanProduct()
|
||||||
|
.getInstallmentAmountInMultiplesOf();
|
||||||
|
|
||||||
|
Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency);
|
||||||
|
|
||||||
|
recalculateRepaymentInstallmentsWithoutEMICalculation(capitalizedIncomeTransaction, amortizableAmount,
|
||||||
|
candidateRepaymentInstallments, currency, installmentAmountInMultiplesOf);
|
||||||
|
allocateOverpayment(capitalizedIncomeTransaction, transactionCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model,
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate, MonetaryCurrency currency) {
|
||||||
if (amortizableAmount.isGreaterThanZero()) {
|
if (amortizableAmount.isGreaterThanZero()) {
|
||||||
|
model.repaymentPeriods().forEach(rm -> {
|
||||||
|
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
||||||
|
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
if (installment != null) {
|
||||||
|
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||||
|
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||||
|
installment.updateObligationsMet(currency, transactionDate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recalculateRepaymentInstallmentsWithoutEMICalculation(LoanTransaction loanTransaction, Money amortizableAmount,
|
||||||
|
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments, MonetaryCurrency currency,
|
||||||
|
Integer installmentAmountInMultiplesOf) {
|
||||||
|
if (amortizableAmount.isGreaterThanZero()) {
|
||||||
|
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
||||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||||
|
|
||||||
@@ -1252,14 +1307,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
i.updatePrincipal(newPrincipal.getAmount());
|
i.updatePrincipal(newPrincipal.getAmount());
|
||||||
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
|
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
|
||||||
i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate());
|
i.updateObligationsMet(currency, loanTransaction.getTransactionDate());
|
||||||
});
|
});
|
||||||
// Hence the rounding, we might need to amend the last installment amount
|
// Hence the rounding, we might need to amend the last installment amount
|
||||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1).addToPrincipal(loanTransaction.getTransactionDate(),
|
||||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject());
|
moneyHolder.getMoneyObject());
|
||||||
}
|
}
|
||||||
|
|
||||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||||
|
|||||||
+14
@@ -18,15 +18,29 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.repository;
|
package org.apache.fineract.portfolio.loanaccount.repository;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
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.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
public interface LoanCapitalizedIncomeBalanceRepository
|
public interface LoanCapitalizedIncomeBalanceRepository
|
||||||
extends JpaRepository<LoanCapitalizedIncomeBalance, Long>, JpaSpecificationExecutor<LoanCapitalizedIncomeBalance> {
|
extends JpaRepository<LoanCapitalizedIncomeBalance, Long>, JpaSpecificationExecutor<LoanCapitalizedIncomeBalance> {
|
||||||
|
|
||||||
|
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);
|
List<LoanCapitalizedIncomeBalance> findAllByLoanId(Long loanId);
|
||||||
|
|
||||||
LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId);
|
LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId);
|
||||||
|
|
||||||
|
@Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id = :loanId")
|
||||||
|
List<LoanTransactionRepaymentPeriodData> findRepaymentPeriodDataByLoanId(Long loanId);
|
||||||
|
|
||||||
|
@Query("SELECT SUM(lcib.amount) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId")
|
||||||
|
BigDecimal calculateCapitalizedIncome(Long loanId);
|
||||||
|
|
||||||
|
@Query("SELECT SUM(lcib.amountAdjustment) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId")
|
||||||
|
BigDecimal calculateCapitalizedIncomeAdjustment(Long loanId);
|
||||||
}
|
}
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 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 lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CapitalizedIncomeBalanceServiceImpl implements CapitalizedIncomeBalanceService {
|
||||||
|
|
||||||
|
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Money calculateCapitalizedIncome(Loan loan) {
|
||||||
|
BigDecimal balance = capitalizedIncomeBalanceRepository.calculateCapitalizedIncome(loan.getId());
|
||||||
|
return Money.of(loan.getCurrency(), balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Money calculateCapitalizedIncomeAdjustment(Loan loan) {
|
||||||
|
BigDecimal balance = capitalizedIncomeBalanceRepository.calculateCapitalizedIncomeAdjustment(loan.getId());
|
||||||
|
return Money.of(loan.getCurrency(), balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-4
@@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
|||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
@@ -54,6 +55,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
|||||||
private final NoteWritePlatformService noteWritePlatformService;
|
private final NoteWritePlatformService noteWritePlatformService;
|
||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
||||||
|
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||||
|
private final LoanBalanceService loanBalanceService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
@@ -74,21 +77,24 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
|||||||
final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount);
|
final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount);
|
||||||
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
|
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
|
||||||
transactionDate, txnExternalId);
|
transactionDate, txnExternalId);
|
||||||
// TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story)
|
|
||||||
// Update loan with capitalized income
|
// Update loan with capitalized income
|
||||||
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
||||||
|
// Recalculate loan transactions
|
||||||
|
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
|
||||||
// Create capitalized income balances
|
// Create capitalized income balances
|
||||||
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||||
// Save and flush (PK is set)
|
// Save and flush (PK is set)
|
||||||
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
|
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
|
||||||
|
// Create capitalized income balances
|
||||||
|
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||||
// Update loan counters and save
|
// Update loan counters and save
|
||||||
loan.updateLoanScheduleDependentDerivedFields();
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
// Update outstanding loan balances
|
// Update loan summary data
|
||||||
loan.updateLoanOutstandingBalances();
|
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||||
// Create a note if provided
|
// Create a note if provided
|
||||||
final String noteText = command.stringValueOfParameterNamed("note");
|
final String noteText = command.stringValueOfParameterNamed("note");
|
||||||
if (noteText != null && !noteText.isEmpty()) {
|
if (noteText != null && !noteText.isEmpty()) {
|
||||||
noteWritePlatformService.createLoanNote(loanId, noteText);
|
noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText);
|
||||||
}
|
}
|
||||||
// Post journal entries
|
// Post journal entries
|
||||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
@@ -99,6 +105,14 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction capitalizedIncomeTransaction) {
|
||||||
|
if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) {
|
||||||
|
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||||
|
} else {
|
||||||
|
reprocessLoanTransactionsService.processLatestTransaction(capitalizedIncomeTransaction, loan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resetBalance(final Long loanId) {
|
public void resetBalance(final Long loanId) {
|
||||||
capitalizedIncomeBalanceRepository
|
capitalizedIncomeBalanceRepository
|
||||||
|
|||||||
+14
-2
@@ -24,12 +24,16 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeWritePlatformServiceImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
|
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
|
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||||
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
@@ -45,10 +49,11 @@ public class ProgressiveLoanAccountConfiguration {
|
|||||||
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
|
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
|
||||||
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
|
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
|
||||||
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
|
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
|
||||||
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) {
|
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
|
||||||
|
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService) {
|
||||||
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
|
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
|
||||||
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
|
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
|
||||||
capitalizedIncomeBalanceRepository);
|
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -59,4 +64,11 @@ public class ProgressiveLoanAccountConfiguration {
|
|||||||
return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper,
|
return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper,
|
||||||
loanCapitalizedIncomeBalanceRepository);
|
loanCapitalizedIncomeBalanceRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(CapitalizedIncomeBalanceService.class)
|
||||||
|
public CapitalizedIncomeBalanceService capitalizedIncomeBalanceService(
|
||||||
|
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) {
|
||||||
|
return new CapitalizedIncomeBalanceServiceImpl(loanCapitalizedIncomeBalanceRepository);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -65,6 +65,12 @@ public interface EMICalculator {
|
|||||||
*/
|
*/
|
||||||
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the capitalized income transaction on the interest model. This method recalculates the EMI amounts from
|
||||||
|
* the action date.
|
||||||
|
*/
|
||||||
|
void addCapitalizedIncome(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDueDate, Money transactionAmount);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
|
* Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action
|
||||||
* date.
|
* date.
|
||||||
|
|||||||
+31
-8
@@ -119,7 +119,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
scheduleModel
|
scheduleModel
|
||||||
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
||||||
scheduleModel.zero())
|
scheduleModel.zero(), scheduleModel.zero())
|
||||||
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
|
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
|
||||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
||||||
operation));
|
operation));
|
||||||
@@ -140,6 +140,24 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
return changedRepaymentPeriod.getDueDate();
|
return changedRepaymentPeriod.getDueDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add capitalized income to Interest Period
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate transactionDueDate,
|
||||||
|
final Money transactionAmount) {
|
||||||
|
addCapitalizedIncome(scheduleModel, EmiChangeOperation.capitalizedIncome(transactionDueDate, transactionAmount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
|
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(),
|
||||||
|
scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> {
|
||||||
|
calculateEMIValueAndRateFactors(
|
||||||
|
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
||||||
|
operation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
|
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
|
||||||
final BigDecimal newInterestRate) {
|
final BigDecimal newInterestRate) {
|
||||||
@@ -151,7 +169,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
|
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
|
||||||
scheduleModel
|
scheduleModel
|
||||||
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
|
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
|
||||||
scheduleModel.zero())
|
scheduleModel.zero(), scheduleModel.zero())
|
||||||
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
|
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
|
||||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
|
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
|
||||||
operation));
|
operation));
|
||||||
@@ -160,8 +178,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
@Override
|
@Override
|
||||||
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
|
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
|
||||||
Money balanceCorrectionAmount) {
|
Money balanceCorrectionAmount) {
|
||||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount)
|
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount,
|
||||||
.ifPresent(repaymentPeriod -> {
|
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
|
||||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||||
calculateOutstandingBalance(scheduleModel);
|
calculateOutstandingBalance(scheduleModel);
|
||||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||||
@@ -237,7 +255,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
|
|
||||||
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
|
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
|
||||||
Money creditedInterestAmount) {
|
Money creditedInterestAmount) {
|
||||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount)
|
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
|
||||||
.ifPresent(repaymentPeriod -> {
|
.ifPresent(repaymentPeriod -> {
|
||||||
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
|
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
|
||||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||||
@@ -440,10 +458,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48
|
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48
|
||||||
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
|
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
|
||||||
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
|
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
|
||||||
.plus(scheduleModel.getTotalCreditedPrincipal(), mc); //
|
Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream()
|
||||||
|
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal))
|
||||||
|
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
|
||||||
|
|
||||||
Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc);
|
Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(scheduleModel.getTotalCreditedPrincipal(), mc)
|
||||||
|
.plus(totalDueInterest, mc).minus(totalEMI, mc);
|
||||||
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||||
.reduce((first, second) -> second);
|
.reduce((first, second) -> second);
|
||||||
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
|
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
|
||||||
@@ -809,7 +830,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
|
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
|
||||||
|
|
||||||
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
|
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
|
||||||
|
addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount());
|
||||||
|
|
||||||
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
||||||
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
||||||
|
|||||||
+8
-2
@@ -31,7 +31,9 @@ public class EmiChangeOperation {
|
|||||||
|
|
||||||
public enum Action { //
|
public enum Action { //
|
||||||
DISBURSEMENT, //
|
DISBURSEMENT, //
|
||||||
INTEREST_RATE_CHANGE //
|
INTEREST_RATE_CHANGE, //
|
||||||
|
CAPITALIZED_INCOME, //
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Action action;
|
private final Action action;
|
||||||
@@ -48,8 +50,12 @@ public class EmiChangeOperation {
|
|||||||
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
|
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) {
|
||||||
|
return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null);
|
||||||
|
}
|
||||||
|
|
||||||
public EmiChangeOperation withZeroAmount() {
|
public EmiChangeOperation withZeroAmount() {
|
||||||
if (action == Action.DISBURSEMENT) {
|
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
|
||||||
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
|
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+15
-7
@@ -62,6 +62,8 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
private Money balanceCorrectionAmount;
|
private Money balanceCorrectionAmount;
|
||||||
private Money outstandingLoanBalance;
|
private Money outstandingLoanBalance;
|
||||||
|
|
||||||
|
private Money capitalizedIncomePrincipal;
|
||||||
|
|
||||||
@JsonExclude
|
@JsonExclude
|
||||||
private final MathContext mc;
|
private final MathContext mc;
|
||||||
private final boolean isPaused;
|
private final boolean isPaused;
|
||||||
@@ -70,38 +72,38 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||||
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||||
interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused());
|
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), mc, interestPeriod.isPaused());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
|
public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) {
|
||||||
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, mc, false);
|
return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null,null, mc, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
|
public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) {
|
||||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||||
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
|
||||||
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused());
|
interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||||
LocalDate dueDate) {
|
LocalDate dueDate) {
|
||||||
final Money zero = repaymentPeriod.getEmi().zero();
|
final Money zero = repaymentPeriod.getEmi().zero();
|
||||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||||
zero.getMc(), false);
|
zero.getMc(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate,
|
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate,
|
||||||
boolean isPaused) {
|
boolean isPaused) {
|
||||||
final Money zero = repaymentPeriod.getEmi().zero();
|
final Money zero = repaymentPeriod.getEmi().zero();
|
||||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||||
zero.getMc(), isPaused);
|
zero.getMc(), isPaused);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||||
LocalDate dueDate) {
|
LocalDate dueDate) {
|
||||||
final Money zero = repaymentPeriod.getEmi().zero();
|
final Money zero = repaymentPeriod.getEmi().zero();
|
||||||
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero,
|
return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero,
|
||||||
zero.getMc(), true);
|
zero.getMc(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +128,10 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc);
|
this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addCapitalizedIncomePrincipalAmount(final Money capitalizedIncomePrincipal) {
|
||||||
|
this.capitalizedIncomePrincipal = MathUtil.plus(this.capitalizedIncomePrincipal, capitalizedIncomePrincipal, mc);
|
||||||
|
}
|
||||||
|
|
||||||
public BigDecimal getCalculatedDueInterest() {
|
public BigDecimal getCalculatedDueInterest() {
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
return creditedInterest.getAmount();
|
return creditedInterest.getAmount();
|
||||||
@@ -156,6 +162,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
|
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
|
||||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()//
|
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()//
|
||||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
|
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
|
||||||
|
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)//
|
||||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
|
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
|
||||||
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
|
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
|
||||||
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);//
|
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);//
|
||||||
@@ -165,6 +172,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
|
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
|
||||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() //
|
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() //
|
||||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||||
|
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)); //
|
.plus(previousInterestPeriod.getDisbursementAmount(), mc)); //
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
|||||||
* Include principal like amounts (all disbursement amount + credited principal)
|
* Include principal like amounts (all disbursement amount + credited principal)
|
||||||
*/
|
*/
|
||||||
public Money getCreditedAmounts() {
|
public Money getCreditedAmounts() {
|
||||||
return getDisbursementAmount().plus(getCreditedPrincipal(), mc);
|
return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFirstInterestPeriod() {
|
public boolean isFirstInterestPeriod() {
|
||||||
|
|||||||
+8
-5
@@ -175,9 +175,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
|
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
|
||||||
final Money disbursedAmount, final Money correctionAmount) {
|
final Money disbursedAmount, final Money correctionAmount, final Money capitalizedIncomePrincipal) {
|
||||||
return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()//
|
return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()//
|
||||||
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
|
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount,
|
||||||
|
capitalizedIncomePrincipal))//
|
||||||
.findFirst();//
|
.findFirst();//
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +215,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
|
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||||
final Money correctionAmount) {
|
final Money correctionAmount, final Money capitalizedIncomePrincipal) {
|
||||||
return repaymentPeriod -> {
|
return repaymentPeriod -> {
|
||||||
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
|
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
|
||||||
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
|
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
|
||||||
@@ -222,9 +223,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
isChangeOnMaturityDate);
|
isChangeOnMaturityDate);
|
||||||
if (interestPeriodOptional.isPresent()) {
|
if (interestPeriodOptional.isPresent()) {
|
||||||
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
|
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
|
||||||
|
interestPeriodOptional.get().addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||||
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
|
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
|
||||||
} else {
|
} else {
|
||||||
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
|
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount, capitalizedIncomePrincipal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -246,7 +248,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount,
|
void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||||
final Money correctionAmount) {
|
final Money correctionAmount, Money capitalizedIncomePrincipal) {
|
||||||
final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate);
|
final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate);
|
||||||
final LocalDate originalDueDate = previousInterestPeriod.getDueDate();
|
final LocalDate originalDueDate = previousInterestPeriod.getDueDate();
|
||||||
final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate);
|
final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate);
|
||||||
@@ -254,6 +256,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
|
|
||||||
previousInterestPeriod.setDueDate(newDueDate);
|
previousInterestPeriod.setDueDate(newDueDate);
|
||||||
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
|
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
|
||||||
|
previousInterestPeriod.addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||||
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
|
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
|
||||||
|
|
||||||
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate, isPaused);
|
final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate, isPaused);
|
||||||
|
|||||||
+16
-1
@@ -225,6 +225,17 @@ public final class RepaymentPeriod {
|
|||||||
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
|
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sum of capitalized income principals
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Money getCapitalizedIncomePrincipal() {
|
||||||
|
return MathUtil.negativeToZero(interestPeriods.stream() //
|
||||||
|
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
|
||||||
|
.reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); //
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives back due principal + credited principal or paid principal
|
* Gives back due principal + credited principal or paid principal
|
||||||
*
|
*
|
||||||
@@ -278,6 +289,7 @@ public final class RepaymentPeriod {
|
|||||||
InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
|
InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
|
||||||
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
|
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
|
||||||
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||||
|
.plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||||
.plus(lastInterestPeriod.getDisbursementAmount(), mc) //
|
.plus(lastInterestPeriod.getDisbursementAmount(), mc) //
|
||||||
.minus(getDuePrincipal(), mc)//
|
.minus(getDuePrincipal(), mc)//
|
||||||
.plus(getPaidPrincipal(), mc);//
|
.plus(getPaidPrincipal(), mc);//
|
||||||
@@ -305,7 +317,10 @@ public final class RepaymentPeriod {
|
|||||||
Money totalDisbursedAmount = getInterestPeriods().stream() //
|
Money totalDisbursedAmount = getInterestPeriods().stream() //
|
||||||
.map(InterestPeriod::getDisbursementAmount) //
|
.map(InterestPeriod::getDisbursementAmount) //
|
||||||
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
||||||
return initialBalance.add(totalDisbursedAmount, mc);
|
Money totalCapitalizedIncomeAmount = getInterestPeriods().stream() //
|
||||||
|
.map(InterestPeriod::getCapitalizedIncomePrincipal) //
|
||||||
|
.reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); //
|
||||||
|
return initialBalance.add(totalDisbursedAmount, mc).add(totalCapitalizedIncomeAmount, mc);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money getZero(MathContext mc) {
|
private Money getZero(MathContext mc) {
|
||||||
|
|||||||
+1
@@ -464,6 +464,7 @@ public class LoanImportHandler implements ImportHandler {
|
|||||||
JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject();
|
JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject();
|
||||||
loanRepaymentJsonob.remove("manuallyReversed");
|
loanRepaymentJsonob.remove("manuallyReversed");
|
||||||
loanRepaymentJsonob.remove("numberOfRepayments");
|
loanRepaymentJsonob.remove("numberOfRepayments");
|
||||||
|
loanRepaymentJsonob.remove("reversed");
|
||||||
String payload = loanRepaymentJsonob.toString();
|
String payload = loanRepaymentJsonob.toString();
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||||
.loanRepaymentTransaction(result.getLoanId()) //
|
.loanRepaymentTransaction(result.getLoanId()) //
|
||||||
|
|||||||
+9
-2
@@ -139,6 +139,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeTyp
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException;
|
import org.apache.fineract.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException;
|
||||||
@@ -150,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
|||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
|
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||||
@@ -297,6 +299,7 @@ public class LoansApiResource {
|
|||||||
private final ClientReadPlatformService clientReadPlatformService;
|
private final ClientReadPlatformService clientReadPlatformService;
|
||||||
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
||||||
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
|
private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
|
||||||
|
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
|
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
|
||||||
@@ -498,11 +501,13 @@ public class LoansApiResource {
|
|||||||
loanBasicDetails.getPageItems().forEach(i -> {
|
loanBasicDetails.getPageItems().forEach(i -> {
|
||||||
if (i.getSummary() != null) {
|
if (i.getSummary() != null) {
|
||||||
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
|
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
|
||||||
|
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||||
|
.findRepaymentPeriodDataByLoanId(i.getId());
|
||||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||||
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
|
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
|
||||||
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
|
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
|
||||||
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
|
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
|
||||||
repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(),
|
repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(),
|
||||||
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
|
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
|
||||||
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
||||||
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
|
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
|
||||||
@@ -981,13 +986,15 @@ public class LoansApiResource {
|
|||||||
|
|
||||||
if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
|
if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
|
||||||
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
|
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
|
||||||
|
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||||
|
.findRepaymentPeriodDataByLoanId(resolvedLoanId);
|
||||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||||
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
|
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
|
||||||
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
|
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
|
||||||
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
|
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
|
||||||
loanBasicDetails.getFeeChargesAtDisbursementCharged());
|
loanBasicDetails.getFeeChargesAtDisbursementCharged());
|
||||||
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
|
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
|
||||||
disbursementData, loanBasicDetails.isInterestRecalculationEnabled(),
|
disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(),
|
||||||
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
|
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
|
||||||
|
|
||||||
if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName)
|
if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName)
|
||||||
|
|||||||
+4
@@ -517,7 +517,11 @@ final class LoansApiResourceSwagger {
|
|||||||
|
|
||||||
public GetLoansLoanIdCurrency currency;
|
public GetLoansLoanIdCurrency currency;
|
||||||
@Schema(example = "1000000.000000")
|
@Schema(example = "1000000.000000")
|
||||||
|
public BigDecimal totalPrincipal;
|
||||||
|
@Schema(example = "1000000.000000")
|
||||||
public BigDecimal principalDisbursed;
|
public BigDecimal principalDisbursed;
|
||||||
|
@Schema(example = "1000000.000000")
|
||||||
|
public BigDecimal totalCapitalizedIncome;
|
||||||
@Schema(example = "0.000000")
|
@Schema(example = "0.000000")
|
||||||
public BigDecimal principalPaid;
|
public BigDecimal principalPaid;
|
||||||
@Schema(example = "0.00")
|
@Schema(example = "0.00")
|
||||||
|
|||||||
+3
-1
@@ -97,7 +97,9 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
|
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
|
||||||
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed())
|
.principalDisbursed(defaultSummaryData.getPrincipalDisbursed()).totalPrincipal(defaultSummaryData.getTotalPrincipal())
|
||||||
|
.totalCapitalizedIncome(defaultSummaryData.getTotalCapitalizedIncome())
|
||||||
|
.totalCapitalizedIncomeAdjustment(defaultSummaryData.getTotalCapitalizedIncomeAdjustment())
|
||||||
.principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid())
|
.principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid())
|
||||||
.principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff())
|
.principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff())
|
||||||
.principalOutstanding(defaultSummaryData.getPrincipalOutstanding())
|
.principalOutstanding(defaultSummaryData.getPrincipalOutstanding())
|
||||||
|
|||||||
+19
-9
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
@@ -51,9 +53,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||||
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
|
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||||
businessEventNotifierService
|
false);
|
||||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService
|
||||||
|
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,13 +70,16 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
transactionDate = DateUtils.getBusinessLocalDate();
|
transactionDate = DateUtils.getBusinessLocalDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
|
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
true);
|
||||||
businessEventNotifierService
|
if (amortizationTransaction.isPresent()) {
|
||||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
businessEventNotifierService.notifyPostBusinessEvent(
|
||||||
|
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
private Optional<LoanTransaction> createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||||
final boolean isChargeOff) {
|
final boolean isChargeOff) {
|
||||||
ExternalId externalId = ExternalId.empty();
|
ExternalId externalId = ExternalId.empty();
|
||||||
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
||||||
@@ -92,13 +98,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
||||||
transactionDate, totalUnrecognizedAmount, externalId);
|
transactionDate, totalUnrecognizedAmount, externalId);
|
||||||
|
|
||||||
loan.addLoanTransaction(amortizationTransaction);
|
loan.addLoanTransaction(amortizationTransaction);
|
||||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||||
|
|
||||||
return amortizationTransaction;
|
return Optional.of(amortizationTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+113
-27
@@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanSchedulePeriodDataWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||||
@@ -118,6 +119,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||||
@@ -249,8 +251,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
accountData.getFeeChargesAtDisbursementCharged());
|
accountData.getFeeChargesAtDisbursementCharged());
|
||||||
|
|
||||||
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
|
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
|
||||||
|
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
|
||||||
|
.findRepaymentPeriodDataByLoanId(accountData.getId());
|
||||||
final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData,
|
final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData,
|
||||||
disbursementData, accountData.isInterestRecalculationEnabled(),
|
disbursementData, capitalizedIncomeData, accountData.isInterestRecalculationEnabled(),
|
||||||
LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType()));
|
LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType()));
|
||||||
accountData.setRepaymentSchedule(repaymentSchedule);
|
accountData.setRepaymentSchedule(repaymentSchedule);
|
||||||
return accountData;
|
return accountData;
|
||||||
@@ -259,13 +263,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
@Override
|
@Override
|
||||||
public LoanScheduleData retrieveRepaymentSchedule(final Long loanId,
|
public LoanScheduleData retrieveRepaymentSchedule(final Long loanId,
|
||||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
||||||
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
||||||
|
LoanScheduleType loanScheduleType) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.context.authenticatedUser();
|
this.context.authenticatedUser();
|
||||||
|
|
||||||
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
|
final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor(
|
||||||
repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled, loanScheduleType);
|
repaymentScheduleRelatedLoanData, disbursementData, capitalizedIncomeData, isInterestRecalculationEnabled,
|
||||||
|
loanScheduleType);
|
||||||
final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment";
|
final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment";
|
||||||
|
|
||||||
return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR
|
return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR
|
||||||
@@ -749,7 +755,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
+ " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname,"
|
+ " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname,"
|
||||||
+ " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, "
|
+ " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, "
|
||||||
+ " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate, "
|
+ " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate, "
|
||||||
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
|
+ " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.total_principal_derived as totalPrincipal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery,"
|
||||||
+ " l.grace_on_principal_periods as graceOnPrincipalPayment, l.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged,l.grace_on_arrears_ageing as graceOnArrearsAgeing,"
|
+ " l.grace_on_principal_periods as graceOnPrincipalPayment, l.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged,l.grace_on_arrears_ageing as graceOnArrearsAgeing,"
|
||||||
+ " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, "
|
+ " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, "
|
||||||
+ " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, "
|
+ " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, "
|
||||||
@@ -764,7 +770,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
+ sqlGenerator.escape("name")
|
+ sqlGenerator.escape("name")
|
||||||
+ " as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode, "
|
+ " as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode, "
|
||||||
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, "
|
+ " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, "
|
||||||
+ " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid,"
|
+ " l.principal_disbursed_derived as principalDisbursed, l.capitalized_income_derived as totalCapitalizedIncome, l.capitalized_income_adjustment_derived as totalCapitalizedIncomeAdjustment, l.principal_repaid_derived as principalPaid,"
|
||||||
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff,"
|
+ " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff,"
|
||||||
+ " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
|
+ " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
|
||||||
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
|
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
|
||||||
@@ -1023,6 +1029,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
|
|
||||||
// loan summary
|
// loan summary
|
||||||
final BigDecimal principalDisbursed = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDisbursed");
|
final BigDecimal principalDisbursed = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDisbursed");
|
||||||
|
final BigDecimal totalPrincipal = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPrincipal");
|
||||||
|
final BigDecimal totalCapitalizedIncome = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalCapitalizedIncome");
|
||||||
|
final BigDecimal totalCapitalizedIncomeAdjustment = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs,
|
||||||
|
"totalCapitalizedIncomeAdjustment");
|
||||||
final BigDecimal principalAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAdjustments");
|
final BigDecimal principalAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAdjustments");
|
||||||
final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
|
final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
|
||||||
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
|
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
|
||||||
@@ -1066,12 +1076,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
inArrears = (overdueSinceDate != null);
|
inArrears = (overdueSinceDate != null);
|
||||||
|
|
||||||
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
|
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
|
||||||
.principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
|
.totalPrincipal(totalPrincipal).totalCapitalizedIncome(totalCapitalizedIncome)
|
||||||
.principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
|
.totalCapitalizedIncomeAdjustment(totalCapitalizedIncomeAdjustment).principalAdjustments(principalAdjustments)
|
||||||
.interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
|
.principalPaid(principalPaid).principalWrittenOff(principalWrittenOff).principalOutstanding(principalOutstanding)
|
||||||
.interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged)
|
.principalOverdue(principalOverdue).interestCharged(interestCharged).interestPaid(interestPaid)
|
||||||
.feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged)
|
.interestWaived(interestWaived).interestWrittenOff(interestWrittenOff).interestOutstanding(interestOutstanding)
|
||||||
.feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
.interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged).feeAdjustments(feeAdjustments)
|
||||||
|
.feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged).feeChargesPaid(feeChargesPaid)
|
||||||
|
.feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
||||||
.feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue)
|
.feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue)
|
||||||
.penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments)
|
.penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments)
|
||||||
.penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived)
|
.penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived)
|
||||||
@@ -1271,15 +1283,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
private final DisbursementData disbursement;
|
private final DisbursementData disbursement;
|
||||||
private final BigDecimal totalFeeChargesDueAtDisbursement;
|
private final BigDecimal totalFeeChargesDueAtDisbursement;
|
||||||
private final Collection<DisbursementData> disbursementData;
|
private final Collection<DisbursementData> disbursementData;
|
||||||
|
private final Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData;
|
||||||
private final LoanScheduleType loanScheduleType;
|
private final LoanScheduleType loanScheduleType;
|
||||||
private LocalDate lastDueDate;
|
private LocalDate lastDueDate;
|
||||||
private BigDecimal outstandingLoanPrincipalBalance;
|
private BigDecimal outstandingLoanPrincipalBalance;
|
||||||
private boolean excludePastUnDisbursed;
|
private boolean excludePastUnDisbursed;
|
||||||
|
|
||||||
LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
|
LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData,
|
||||||
Collection<DisbursementData> disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
Collection<DisbursementData> disbursementData, Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData,
|
||||||
|
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||||
this.currency = repaymentScheduleRelatedLoanData.getCurrency();
|
this.currency = repaymentScheduleRelatedLoanData.getCurrency();
|
||||||
this.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
|
this.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
|
||||||
|
this.capitalizedIncomeData = capitalizedIncomeData;
|
||||||
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
|
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
|
||||||
this.lastDueDate = this.disbursement.disbursementDate();
|
this.lastDueDate = this.disbursement.disbursementDate();
|
||||||
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
|
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
|
||||||
@@ -1359,10 +1374,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
||||||
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
|
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
|
||||||
final boolean complete = rs.getBoolean("complete");
|
final boolean complete = rs.getBoolean("complete");
|
||||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds,
|
List<LoanSchedulePeriodDataWrapper> combinedDataList = new ArrayList<>();
|
||||||
disbursementChargeAmount, waivedChargeAmount, periods);
|
combinedDataList.addAll(
|
||||||
|
collectEligibleDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds));
|
||||||
|
combinedDataList.addAll(collectEligibleCapitalizedIncomeData(fromDate, dueDate, disbursementPeriodIds));
|
||||||
|
combinedDataList.sort(this::sortPeriodDataHolders);
|
||||||
|
fillLoanSchedulePeriodData(periods, combinedDataList, disbursementChargeAmount, waivedChargeAmount);
|
||||||
|
|
||||||
|
BigDecimal disbursedAmount = calculateDisbursedAmount(combinedDataList);
|
||||||
|
|
||||||
// Add the Charge back or Credits to the initial amount to avoid negative balance
|
// Add the Charge back or Credits to the initial amount to avoid negative balance
|
||||||
final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits");
|
final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits");
|
||||||
@@ -1473,26 +1493,82 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
|
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal processDisbursementData(LoanScheduleType loanScheduleType, Collection<DisbursementData> disbursementData,
|
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
||||||
LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds, BigDecimal disbursementChargeAmount,
|
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
|
||||||
BigDecimal waivedChargeAmount, List<LoanSchedulePeriodData> periods) {
|
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
||||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
// Collect eligible disbursement data
|
||||||
for (final DisbursementData data : disbursementData) {
|
for (final DisbursementData data : disbursementData) {
|
||||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||||
if (((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||||
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||||
&& !disbursementPeriodIds.contains(data.getId())) {
|
&& !disbursementPeriodIds.contains(data.getId());
|
||||||
disbursedAmount = disbursedAmount.add(data.getPrincipal());
|
|
||||||
LoanSchedulePeriodData periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
|
if (isEligible) {
|
||||||
periods.add(periodData);
|
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
|
||||||
disbursementPeriodIds.add(data.getId());
|
disbursementPeriodIds.add(data.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return disbursementDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanSchedulePeriodDataWrapper> collectEligibleCapitalizedIncomeData(LocalDate fromDate, LocalDate dueDate,
|
||||||
|
Set<Long> disbursementPeriodIds) {
|
||||||
|
List<LoanSchedulePeriodDataWrapper> capitalizedIncomeDataList = new ArrayList<>();
|
||||||
|
// Collect eligible capitalized income data
|
||||||
|
for (LoanTransactionRepaymentPeriodData data : capitalizedIncomeData) {
|
||||||
|
boolean isEligible = canAddCapitalizedIncomeData(data, fromDate, dueDate)
|
||||||
|
&& !disbursementPeriodIds.contains(data.getTransactionId());
|
||||||
|
|
||||||
|
if (isEligible) {
|
||||||
|
capitalizedIncomeDataList.add(new LoanSchedulePeriodDataWrapper(data, data.getDate(), false));
|
||||||
|
disbursementPeriodIds.add(data.getTransactionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return capitalizedIncomeDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillLoanSchedulePeriodData(List<LoanSchedulePeriodData> periods, List<LoanSchedulePeriodDataWrapper> combinedDataList,
|
||||||
|
BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount) {
|
||||||
|
// Process all collected data in chronological order
|
||||||
|
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
|
||||||
|
LoanSchedulePeriodData periodData;
|
||||||
|
if (dataItem.isDisbursement()) {
|
||||||
|
// Process disbursement data
|
||||||
|
DisbursementData data = (DisbursementData) dataItem.getData();
|
||||||
|
periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
|
||||||
|
} else {
|
||||||
|
// Process capitalized income data
|
||||||
|
LoanTransactionRepaymentPeriodData data = (LoanTransactionRepaymentPeriodData) dataItem.getData();
|
||||||
|
periodData = createLoanSchedulePeriodData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common processing for both data types
|
||||||
|
periods.add(periodData);
|
||||||
|
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateDisbursedAmount(List<LoanSchedulePeriodDataWrapper> combinedDataList) {
|
||||||
|
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||||
|
for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) {
|
||||||
|
if (dataItem.isDisbursement()) {
|
||||||
|
DisbursementData data = (DisbursementData) dataItem.getData();
|
||||||
|
disbursedAmount = disbursedAmount.add(data.getPrincipal());
|
||||||
|
}
|
||||||
|
}
|
||||||
return disbursedAmount;
|
return disbursedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int sortPeriodDataHolders(LoanSchedulePeriodDataWrapper item1, LoanSchedulePeriodDataWrapper item2) {
|
||||||
|
int dateComparison = item1.getDate().compareTo(item2.getDate());
|
||||||
|
if (dateComparison == 0 && item1.isDisbursement() != item2.isDisbursement()) {
|
||||||
|
// If dates are equal, prioritize disbursement data
|
||||||
|
return item1.isDisbursement() ? -1 : 1;
|
||||||
|
}
|
||||||
|
return dateComparison;
|
||||||
|
}
|
||||||
|
|
||||||
private LoanSchedulePeriodData createLoanSchedulePeriodData(final DisbursementData data, BigDecimal disbursementChargeAmount,
|
private LoanSchedulePeriodData createLoanSchedulePeriodData(final DisbursementData data, BigDecimal disbursementChargeAmount,
|
||||||
BigDecimal waivedChargeAmount) {
|
BigDecimal waivedChargeAmount) {
|
||||||
BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount
|
BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount
|
||||||
@@ -1501,11 +1577,20 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
data.isDisbursed());
|
data.isDisbursed());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoanSchedulePeriodData createLoanSchedulePeriodData(final LoanTransactionRepaymentPeriodData data) {
|
||||||
|
BigDecimal feeCharges = Objects.isNull(data.getFeeChargesPortion()) ? BigDecimal.ZERO : data.getFeeChargesPortion();
|
||||||
|
return LoanSchedulePeriodData.disbursementOnlyPeriod(data.getDate(), data.getAmount(), feeCharges, !data.isReversed());
|
||||||
|
}
|
||||||
|
|
||||||
private boolean canAddDisbursementData(DisbursementData data, boolean isDueForDisbursement, boolean excludePastUnDisbursed) {
|
private boolean canAddDisbursementData(DisbursementData data, boolean isDueForDisbursement, boolean excludePastUnDisbursed) {
|
||||||
return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate()))
|
return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate()))
|
||||||
&& isDueForDisbursement;
|
&& isDueForDisbursement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean canAddCapitalizedIncomeData(LoanTransactionRepaymentPeriodData data, LocalDate fromDate, LocalDate dueDate) {
|
||||||
|
return !data.isReversed() && DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, dueDate, data.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
|
private static final class LoanTransactionsMapper implements RowMapper<LoanTransactionData> {
|
||||||
@@ -2168,7 +2253,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
|
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
|
||||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||||
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
|
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
|
||||||
final Boolean isReversed = false;
|
final Boolean isManuallyReversed = false;
|
||||||
|
|
||||||
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
|
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
|
||||||
|
|
||||||
@@ -2178,7 +2263,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
|||||||
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
|
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
|
||||||
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
|
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
|
||||||
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion,
|
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion,
|
||||||
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isReversed, loanId, loan.getExternalId());
|
paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isManuallyReversed, loanId,
|
||||||
|
loan.getExternalId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CurrencyMapper implements RowMapper<CurrencyData> {
|
private static final class CurrencyMapper implements RowMapper<CurrencyData> {
|
||||||
|
|||||||
+9
-2
@@ -64,6 +64,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||||
import org.apache.fineract.portfolio.note.domain.Note;
|
import org.apache.fineract.portfolio.note.domain.Note;
|
||||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
@@ -93,6 +94,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
|||||||
private final LoanChargeValidator loanChargeValidator;
|
private final LoanChargeValidator loanChargeValidator;
|
||||||
private final LoanJournalEntryPoster journalEntryPoster;
|
private final LoanJournalEntryPoster journalEntryPoster;
|
||||||
private final LoanBalanceService loanBalanceService;
|
private final LoanBalanceService loanBalanceService;
|
||||||
|
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
||||||
@@ -245,8 +247,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
|||||||
transactionForAdjustment.getTransactionDate());
|
transactionForAdjustment.getTransactionDate());
|
||||||
|
|
||||||
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
|
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
|
||||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) {
|
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()
|
||||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted.";
|
&& !transactionForAdjustment.isCapitalizedIncome()) {
|
||||||
|
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, capitalized income, accrual or credit balance refund can be adjusted.";
|
||||||
throw new InvalidLoanTransactionTypeException("transaction",
|
throw new InvalidLoanTransactionTypeException("transaction",
|
||||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||||
}
|
}
|
||||||
@@ -285,6 +288,10 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
|||||||
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment,
|
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment,
|
||||||
scheduleGeneratorDTO);
|
scheduleGeneratorDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) {
|
||||||
|
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
|
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
|
||||||
|
|||||||
+7
-3
@@ -204,9 +204,13 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createLoanNote(final Long loanId, final String note) {
|
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
|
||||||
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
|
||||||
final Note newNote = Note.loanNote(loan, note);
|
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
|
||||||
|
|
||||||
|
final Loan loan = loanTransaction.getLoan();
|
||||||
|
|
||||||
|
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
|
||||||
|
|
||||||
this.noteRepository.save(newNote);
|
this.noteRepository.save(newNote);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,4 +196,5 @@
|
|||||||
<include file="parts/0175_add_fk_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
<include file="parts/0175_add_fk_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
|
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
|
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||||
|
<changeSet author="fineract" id="1">
|
||||||
|
<addColumn tableName="m_loan">
|
||||||
|
<column defaultValueNumeric="0.000000" name="capitalized_income_derived" type="DECIMAL(19, 6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column defaultValueNumeric="0.000000" name="capitalized_income_adjustment_derived" type="DECIMAL(19, 6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
<column defaultValueNumeric="0.000000" name="total_principal_derived" type="DECIMAL(19, 6)">
|
||||||
|
<constraints nullable="false"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+3
-3
@@ -78,7 +78,7 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
transaction(100.0, "Disbursement", "01 January 2024"), //
|
transaction(100.0, "Disbursement", "01 January 2024"), //
|
||||||
transaction(50.0, "Capitalized Income", "01 January 2024"), //
|
transaction(50.0, "Capitalized Income", "01 January 2024"), //
|
||||||
transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), //
|
transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), //
|
||||||
transaction(0.02, "Accrual", "02 January 2024"), //
|
transaction(0.03, "Accrual", "02 January 2024"), //
|
||||||
transaction(0.55, "Capitalized Income Amortization", "02 January 2024") //
|
transaction(0.55, "Capitalized Income Amortization", "02 January 2024") //
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
|
journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
|
||||||
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||||
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
|
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(0.02, interestReceivableAccount, "DEBIT"), //
|
journalEntry(0.03, interestReceivableAccount, "DEBIT"), //
|
||||||
journalEntry(0.02, interestIncomeAccount, "CREDIT"), //
|
journalEntry(0.03, interestIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||||
journalEntry(0.55, feeIncomeAccount, "CREDIT") //
|
journalEntry(0.55, feeIncomeAccount, "CREDIT") //
|
||||||
);
|
);
|
||||||
|
|||||||
+35
@@ -19,7 +19,9 @@
|
|||||||
package org.apache.fineract.integrationtests;
|
package org.apache.fineract.integrationtests;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
@@ -248,4 +250,37 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCapitalizedIncomeExistsInRepaymentScheduleAndModifiesPrincipal() {
|
||||||
|
runAt("01 March 2023", () -> {
|
||||||
|
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
|
||||||
|
.createLoanProduct(create4IProgressiveWithCapitalizedIncome());
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId,
|
||||||
|
loanProductsResponse.getResourceId(), "01 March 2023", 10000.00, 12.00, 4, null));
|
||||||
|
loanId = postLoansResponse.getLoanId();
|
||||||
|
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.00, "01 March 2023"));
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 March 2023");
|
||||||
|
});
|
||||||
|
runAt("02 March 2023", () -> {
|
||||||
|
loanTransactionHelper.addCapitalizedIncome(loanId, "02 March 2023", 100.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
BigDecimal thousand = BigDecimal.valueOf(1000.0);
|
||||||
|
BigDecimal hundred = BigDecimal.valueOf(100.0);
|
||||||
|
BigDecimal thousandOneHundred = BigDecimal.valueOf(1100.0);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
Assertions.assertEquals(thousand, loanDetails.getPrincipal().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
Assertions.assertEquals(thousand, loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
Assertions.assertEquals(hundred, loanDetails.getSummary().getTotalCapitalizedIncome().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getPrincipalOutstanding().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
|
||||||
|
List<GetLoansLoanIdRepaymentPeriod> periods = loanDetails.getRepaymentSchedule().getPeriods();
|
||||||
|
Assertions.assertEquals(6, periods.size());
|
||||||
|
Assertions.assertEquals(thousand, periods.get(0).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
Assertions.assertEquals(hundred, periods.get(1).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user