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
|
||||
public void createLoanNote(Long loanId, String note) {
|
||||
throw new UnsupportedOperationException("createLoanNote() is not yet implemented.");
|
||||
public void createLoanTransactionNote(Long loanTransactionId, String note) {
|
||||
throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -458,6 +458,30 @@
|
||||
"null",
|
||||
"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);
|
||||
}
|
||||
|
||||
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) {
|
||||
return compare(first, second, true);
|
||||
}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ public interface NoteWritePlatformService {
|
||||
|
||||
CommandProcessingResult createNote(JsonCommand command);
|
||||
|
||||
void createLoanNote(Long loanId, String note);
|
||||
void createLoanTransactionNote(Long loanTransactionId, String note);
|
||||
|
||||
CommandProcessingResult updateNote(JsonCommand command);
|
||||
|
||||
|
||||
+39
-39
@@ -188,12 +188,12 @@ Feature: Capitalized Income
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | 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:
|
||||
| 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 |
|
||||
| 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 | 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:
|
||||
@@ -222,12 +222,12 @@ Feature: Capitalized Income
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | 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:
|
||||
| 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 |
|
||||
| 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 | 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:
|
||||
@@ -262,7 +262,7 @@ Feature: Capitalized Income
|
||||
| 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 |
|
||||
| 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 |
|
||||
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 |
|
||||
@@ -555,16 +555,16 @@ Feature: Capitalized Income
|
||||
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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
||||
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||
| 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 | Charge-off | 904.0 | 900.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 | 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 |
|
||||
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 |
|
||||
@@ -597,16 +597,16 @@ Feature: Capitalized Income
|
||||
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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
||||
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 900.0 | |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
||||
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 1000.0| |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||
| 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 | Charge-off | 904.0 | 900.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 | 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 |
|
||||
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 |
|
||||
@@ -639,16 +639,16 @@ Feature: Capitalized Income
|
||||
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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
||||
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||
| 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 | Charge-off | 904.0 | 900.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 | 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 |
|
||||
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 |
|
||||
@@ -681,16 +681,16 @@ Feature: Capitalized Income
|
||||
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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
||||
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||
| 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 | Charge-off | 904.0 | 900.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 | 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 |
|
||||
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 |
|
||||
@@ -700,20 +700,20 @@ Feature: Capitalized Income
|
||||
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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 900.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.0 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.0 | |
|
||||
| ASSET | 112601 | Loans Receivable | 900.0 | |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 4.0 | |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 900.0 |
|
||||
| INCOME | 404001 | Interest Income Charge Off | | 4.0 |
|
||||
| ASSET | 112601 | Loans Receivable | | 1000.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 4.58 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| |
|
||||
| INCOME | 404001 | Interest Income Charge Off | 4.58 | |
|
||||
| ASSET | 112601 | Loans Receivable | 1000.0| |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 4.58 | |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 |
|
||||
| INCOME | 404001 | Interest Income Charge Off | | 4.58 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||
| 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 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false |
|
||||
| 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:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
||||
|
||||
+2
-3
@@ -30,7 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public final class DisbursementData implements Comparable<DisbursementData> {
|
||||
public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable<DisbursementData> {
|
||||
|
||||
private final Long id;
|
||||
private final LocalDate expectedDisbursementDate;
|
||||
@@ -102,8 +102,7 @@ public final class DisbursementData implements Comparable<DisbursementData> {
|
||||
|
||||
private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive,
|
||||
final LocalDate target) {
|
||||
return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive))
|
||||
&& DateUtils.isBefore(target, upToNotInclusive);
|
||||
return DateUtils.isDateInRangeFromInclusiveToExclusive(fromInclusive, upToNotInclusive, target);
|
||||
}
|
||||
|
||||
public BigDecimal getWaivedChargeAmount() {
|
||||
|
||||
+3
@@ -34,6 +34,9 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
public class LoanSummaryData {
|
||||
|
||||
private final CurrencyData currency;
|
||||
private final BigDecimal totalPrincipal;
|
||||
private final BigDecimal totalCapitalizedIncome;
|
||||
private final BigDecimal totalCapitalizedIncomeAdjustment;
|
||||
private final BigDecimal principalDisbursed;
|
||||
private final BigDecimal principalAdjustments;
|
||||
private final BigDecimal principalPaid;
|
||||
|
||||
+20
-5
@@ -38,6 +38,15 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
public class LoanSummary {
|
||||
|
||||
// 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)
|
||||
private BigDecimal totalPrincipalDisbursed;
|
||||
|
||||
@@ -166,6 +175,9 @@ public class LoanSummary {
|
||||
this.totalPenaltyChargesWaived = BigDecimal.ZERO;
|
||||
this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO;
|
||||
this.totalPrincipalAdjustments = BigDecimal.ZERO;
|
||||
this.totalPrincipal = BigDecimal.ZERO;
|
||||
this.totalCapitalizedIncome = BigDecimal.ZERO;
|
||||
this.totalCapitalizedIncomeAdjustment = BigDecimal.ZERO;
|
||||
this.totalPrincipalDisbursed = BigDecimal.ZERO;
|
||||
this.totalPrincipalOutstanding = BigDecimal.ZERO;
|
||||
this.totalPrincipalRepaid = BigDecimal.ZERO;
|
||||
@@ -176,17 +188,20 @@ public class LoanSummary {
|
||||
}
|
||||
|
||||
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.totalCapitalizedIncome = capitalizedIncome.getAmount();
|
||||
this.totalCapitalizedIncomeAdjustment = capitalizedIncomeAdjustment.getAmount();
|
||||
this.totalPrincipal = principal.plus(capitalizedIncome).getAmount();
|
||||
this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount();
|
||||
this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount();
|
||||
|
||||
this.totalPrincipalOutstanding = principal.plus(this.totalPrincipalAdjustments).minus(this.totalPrincipalRepaid)
|
||||
.minus(this.totalPrincipalWrittenOff).getAmount();
|
||||
this.totalPrincipalOutstanding = principal.plus(capitalizedIncome).plus(this.totalPrincipalAdjustments)
|
||||
.minus(this.totalPrincipalRepaid).minus(this.totalPrincipalWrittenOff).getAmount();
|
||||
|
||||
final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency);
|
||||
this.totalInterestCharged = totalInterestCharged.getAmount();
|
||||
@@ -225,7 +240,7 @@ public class LoanSummary {
|
||||
this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid)
|
||||
.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);
|
||||
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) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
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) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoanBalanceService {
|
||||
|
||||
private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService;
|
||||
|
||||
public Money calculateTotalOverpayment(final Loan loan) {
|
||||
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
|
||||
|
||||
@@ -97,7 +101,10 @@ public class LoanBalanceService {
|
||||
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -173,7 +173,10 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
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
|
||||
&& loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
||||
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.RepaymentScheduleRelatedLoanData;
|
||||
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.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||
@@ -50,7 +51,8 @@ public interface LoanReadPlatformService {
|
||||
LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
+71
-18
@@ -344,6 +344,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
||||
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
|
||||
case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx);
|
||||
// TODO: Cover rest of the transaction types
|
||||
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);
|
||||
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency);
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
@@ -1222,7 +1212,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
@@ -1240,7 +1229,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
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()) {
|
||||
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());
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
@@ -1252,14 +1307,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
i.updatePrincipal(newPrincipal.getAmount());
|
||||
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
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
||||
.addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject());
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1).addToPrincipal(loanTransaction.getTransactionDate(),
|
||||
moneyHolder.getMoneyObject());
|
||||
}
|
||||
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
|
||||
+14
@@ -18,15 +18,29 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface LoanCapitalizedIncomeBalanceRepository
|
||||
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);
|
||||
|
||||
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.CommandProcessingResultBuilder;
|
||||
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.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
@@ -54,6 +55,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
private final NoteWritePlatformService noteWritePlatformService;
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -74,21 +77,24 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount);
|
||||
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
|
||||
transactionDate, txnExternalId);
|
||||
// TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story)
|
||||
// Update loan with capitalized income
|
||||
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
||||
// Recalculate loan transactions
|
||||
recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction);
|
||||
// Create capitalized income balances
|
||||
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||
// Save and flush (PK is set)
|
||||
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
|
||||
// Create capitalized income balances
|
||||
createCapitalizedIncomeBalance(capitalizedIncomeTransaction);
|
||||
// Update loan counters and save
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
// Update outstanding loan balances
|
||||
loan.updateLoanOutstandingBalances();
|
||||
// Update loan summary data
|
||||
loanBalanceService.updateLoanSummaryDerivedFields(loan);
|
||||
// Create a note if provided
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
if (noteText != null && !noteText.isEmpty()) {
|
||||
noteWritePlatformService.createLoanNote(loanId, noteText);
|
||||
noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText);
|
||||
}
|
||||
// Post journal entries
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
@@ -99,6 +105,14 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
||||
.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
|
||||
public void resetBalance(final Long loanId) {
|
||||
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.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
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.CapitalizedIncomeWritePlatformServiceImpl;
|
||||
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.ProgressiveLoanTransactionValidator;
|
||||
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.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -45,10 +49,11 @@ public class ProgressiveLoanAccountConfiguration {
|
||||
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
|
||||
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
|
||||
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
|
||||
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) {
|
||||
LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository,
|
||||
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService) {
|
||||
return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
|
||||
paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory,
|
||||
capitalizedIncomeBalanceRepository);
|
||||
capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -59,4 +64,11 @@ public class ProgressiveLoanAccountConfiguration {
|
||||
return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper,
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* date.
|
||||
|
||||
+31
-8
@@ -119,7 +119,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||
scheduleModel
|
||||
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
|
||||
scheduleModel.zero())
|
||||
scheduleModel.zero(), scheduleModel.zero())
|
||||
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(
|
||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel,
|
||||
operation));
|
||||
@@ -140,6 +140,24 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
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
|
||||
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
|
||||
final BigDecimal newInterestRate) {
|
||||
@@ -151,7 +169,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate());
|
||||
scheduleModel
|
||||
.changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(),
|
||||
scheduleModel.zero())
|
||||
scheduleModel.zero(), scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors(
|
||||
getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel,
|
||||
operation));
|
||||
@@ -160,8 +178,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
@Override
|
||||
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
|
||||
Money balanceCorrectionAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount)
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount,
|
||||
scheduleModel.zero()).ifPresent(repaymentPeriod -> {
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
@@ -237,7 +255,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
|
||||
private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount,
|
||||
Money creditedInterestAmount) {
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount)
|
||||
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero())
|
||||
.ifPresent(repaymentPeriod -> {
|
||||
addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount);
|
||||
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
|
||||
@@ -440,10 +458,13 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48
|
||||
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100
|
||||
.plus(scheduleModel.getTotalCreditedPrincipal(), mc); //
|
||||
.reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100
|
||||
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())
|
||||
.reduce((first, second) -> second);
|
||||
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
|
||||
@@ -809,7 +830,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return;
|
||||
}
|
||||
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
|
||||
|
||||
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
|
||||
addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount());
|
||||
|
||||
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
||||
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
||||
|
||||
+8
-2
@@ -31,7 +31,9 @@ public class EmiChangeOperation {
|
||||
|
||||
public enum Action { //
|
||||
DISBURSEMENT, //
|
||||
INTEREST_RATE_CHANGE //
|
||||
INTEREST_RATE_CHANGE, //
|
||||
CAPITALIZED_INCOME, //
|
||||
;
|
||||
}
|
||||
|
||||
private final Action action;
|
||||
@@ -48,8 +50,12 @@ public class EmiChangeOperation {
|
||||
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() {
|
||||
if (action == Action.DISBURSEMENT) {
|
||||
if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) {
|
||||
return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null);
|
||||
}
|
||||
return null;
|
||||
|
||||
+15
-7
@@ -62,6 +62,8 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
private Money balanceCorrectionAmount;
|
||||
private Money outstandingLoanBalance;
|
||||
|
||||
private Money capitalizedIncomePrincipal;
|
||||
|
||||
@JsonExclude
|
||||
private final MathContext mc;
|
||||
private final boolean isPaused;
|
||||
@@ -70,38 +72,38 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||
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) {
|
||||
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) {
|
||||
return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(),
|
||||
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,
|
||||
LocalDate dueDate) {
|
||||
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);
|
||||
}
|
||||
|
||||
public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate,
|
||||
boolean isPaused) {
|
||||
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);
|
||||
}
|
||||
|
||||
public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate,
|
||||
LocalDate dueDate) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -126,6 +128,10 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
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() {
|
||||
if (isPaused) {
|
||||
return creditedInterest.getAmount();
|
||||
@@ -156,6 +162,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
|
||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()//
|
||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
|
||||
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)//
|
||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
|
||||
.minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)//
|
||||
.plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);//
|
||||
@@ -165,6 +172,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
|
||||
this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() //
|
||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||
.plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)); //
|
||||
}
|
||||
}
|
||||
@@ -173,7 +181,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
* Include principal like amounts (all disbursement amount + credited principal)
|
||||
*/
|
||||
public Money getCreditedAmounts() {
|
||||
return getDisbursementAmount().plus(getCreditedPrincipal(), mc);
|
||||
return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc);
|
||||
}
|
||||
|
||||
public boolean isFirstInterestPeriod() {
|
||||
|
||||
+8
-5
@@ -175,9 +175,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
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()//
|
||||
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
|
||||
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount,
|
||||
capitalizedIncomePrincipal))//
|
||||
.findFirst();//
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
}
|
||||
|
||||
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
|
||||
final Money correctionAmount) {
|
||||
final Money correctionAmount, final Money capitalizedIncomePrincipal) {
|
||||
return repaymentPeriod -> {
|
||||
final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod)
|
||||
&& balanceChangeDate.isEqual(repaymentPeriod.getDueDate());
|
||||
@@ -222,9 +223,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
isChangeOnMaturityDate);
|
||||
if (interestPeriodOptional.isPresent()) {
|
||||
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
|
||||
interestPeriodOptional.get().addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
|
||||
} 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,
|
||||
final Money correctionAmount) {
|
||||
final Money correctionAmount, Money capitalizedIncomePrincipal) {
|
||||
final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate);
|
||||
final LocalDate originalDueDate = previousInterestPeriod.getDueDate();
|
||||
final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate);
|
||||
@@ -254,6 +256,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
|
||||
previousInterestPeriod.setDueDate(newDueDate);
|
||||
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
|
||||
previousInterestPeriod.addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal);
|
||||
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
|
||||
|
||||
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); //
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -278,6 +289,7 @@ public final class RepaymentPeriod {
|
||||
InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
|
||||
Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() //
|
||||
.plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) //
|
||||
.plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) //
|
||||
.plus(lastInterestPeriod.getDisbursementAmount(), mc) //
|
||||
.minus(getDuePrincipal(), mc)//
|
||||
.plus(getPaidPrincipal(), mc);//
|
||||
@@ -305,7 +317,10 @@ public final class RepaymentPeriod {
|
||||
Money totalDisbursedAmount = getInterestPeriods().stream() //
|
||||
.map(InterestPeriod::getDisbursementAmount) //
|
||||
.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) {
|
||||
|
||||
+1
@@ -464,6 +464,7 @@ public class LoanImportHandler implements ImportHandler {
|
||||
JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject();
|
||||
loanRepaymentJsonob.remove("manuallyReversed");
|
||||
loanRepaymentJsonob.remove("numberOfRepayments");
|
||||
loanRepaymentJsonob.remove("reversed");
|
||||
String payload = loanRepaymentJsonob.toString();
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||
.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.LoanSummaryBalancesRepository;
|
||||
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.LoanTemplateTypeRequiredException;
|
||||
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.service.LoanScheduleCalculationPlatformService;
|
||||
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.service.GLIMAccountInfoReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||
@@ -297,6 +299,7 @@ public class LoansApiResource {
|
||||
private final ClientReadPlatformService clientReadPlatformService;
|
||||
private final LoanTermVariationsRepository loanTermVariationsRepository;
|
||||
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
|
||||
@@ -498,11 +501,13 @@ public class LoansApiResource {
|
||||
loanBasicDetails.getPageItems().forEach(i -> {
|
||||
if (i.getSummary() != null) {
|
||||
Collection<DisbursementData> disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId());
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(i.getId());
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||
i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(),
|
||||
i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged());
|
||||
final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(),
|
||||
repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(),
|
||||
repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType()));
|
||||
LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate
|
||||
.resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode());
|
||||
@@ -981,13 +986,15 @@ public class LoansApiResource {
|
||||
|
||||
if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
|
||||
mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(resolvedLoanId);
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
|
||||
loanBasicDetails.getTimeline().getExpectedDisbursementDate(),
|
||||
loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(),
|
||||
loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(),
|
||||
loanBasicDetails.getFeeChargesAtDisbursementCharged());
|
||||
repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData,
|
||||
disbursementData, loanBasicDetails.isInterestRecalculationEnabled(),
|
||||
disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType()));
|
||||
|
||||
if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName)
|
||||
|
||||
+4
@@ -517,7 +517,11 @@ final class LoansApiResourceSwagger {
|
||||
|
||||
public GetLoansLoanIdCurrency currency;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal totalPrincipal;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal principalDisbursed;
|
||||
@Schema(example = "1000000.000000")
|
||||
public BigDecimal totalCapitalizedIncome;
|
||||
@Schema(example = "0.000000")
|
||||
public BigDecimal principalPaid;
|
||||
@Schema(example = "0.00")
|
||||
|
||||
+3
-1
@@ -97,7 +97,9 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
|
||||
}
|
||||
|
||||
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())
|
||||
.principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff())
|
||||
.principalOutstanding(defaultSummaryData.getPrincipalOutstanding())
|
||||
|
||||
+19
-9
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
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.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
@@ -51,9 +53,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
@Transactional
|
||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||
false);
|
||||
amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,13 +70,16 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
transactionDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||
true);
|
||||
if (amortizationTransaction.isPresent()) {
|
||||
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) {
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
||||
@@ -92,13 +98,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
||||
transactionDate, totalUnrecognizedAmount, externalId);
|
||||
|
||||
loan.addLoanTransaction(amortizationTransaction);
|
||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||
|
||||
return amortizationTransaction;
|
||||
return Optional.of(amortizationTransaction);
|
||||
}
|
||||
|
||||
@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.LoanInterestRecalculationData;
|
||||
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.LoanSummaryData;
|
||||
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.LoanSubStatus;
|
||||
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.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||
@@ -249,8 +251,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
accountData.getFeeChargesAtDisbursementCharged());
|
||||
|
||||
final Collection<DisbursementData> disbursementData = retrieveLoanDisbursementDetails(accountData.getId());
|
||||
List<LoanTransactionRepaymentPeriodData> capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository
|
||||
.findRepaymentPeriodDataByLoanId(accountData.getId());
|
||||
final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData,
|
||||
disbursementData, accountData.isInterestRecalculationEnabled(),
|
||||
disbursementData, capitalizedIncomeData, accountData.isInterestRecalculationEnabled(),
|
||||
LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType()));
|
||||
accountData.setRepaymentSchedule(repaymentSchedule);
|
||||
return accountData;
|
||||
@@ -259,13 +263,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
@Override
|
||||
public LoanScheduleData retrieveRepaymentSchedule(final Long loanId,
|
||||
final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection<DisbursementData> disbursementData,
|
||||
boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) {
|
||||
Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData, boolean isInterestRecalculationEnabled,
|
||||
LoanScheduleType loanScheduleType) {
|
||||
|
||||
try {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
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";
|
||||
|
||||
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.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.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.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, "
|
||||
@@ -764,7 +770,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
+ sqlGenerator.escape("name")
|
||||
+ " 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.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.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments,"
|
||||
+ " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged,"
|
||||
@@ -1023,6 +1029,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
|
||||
// loan summary
|
||||
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 principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid");
|
||||
final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff");
|
||||
@@ -1066,12 +1076,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
inArrears = (overdueSinceDate != null);
|
||||
|
||||
loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed)
|
||||
.principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff)
|
||||
.principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged)
|
||||
.interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff)
|
||||
.interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged)
|
||||
.feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged)
|
||||
.feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
||||
.totalPrincipal(totalPrincipal).totalCapitalizedIncome(totalCapitalizedIncome)
|
||||
.totalCapitalizedIncomeAdjustment(totalCapitalizedIncomeAdjustment).principalAdjustments(principalAdjustments)
|
||||
.principalPaid(principalPaid).principalWrittenOff(principalWrittenOff).principalOutstanding(principalOutstanding)
|
||||
.principalOverdue(principalOverdue).interestCharged(interestCharged).interestPaid(interestPaid)
|
||||
.interestWaived(interestWaived).interestWrittenOff(interestWrittenOff).interestOutstanding(interestOutstanding)
|
||||
.interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged).feeAdjustments(feeAdjustments)
|
||||
.feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged).feeChargesPaid(feeChargesPaid)
|
||||
.feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff)
|
||||
.feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue)
|
||||
.penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments)
|
||||
.penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived)
|
||||
@@ -1271,15 +1283,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
private final DisbursementData disbursement;
|
||||
private final BigDecimal totalFeeChargesDueAtDisbursement;
|
||||
private final Collection<DisbursementData> disbursementData;
|
||||
private final Collection<LoanTransactionRepaymentPeriodData> capitalizedIncomeData;
|
||||
private final LoanScheduleType loanScheduleType;
|
||||
private LocalDate lastDueDate;
|
||||
private BigDecimal outstandingLoanPrincipalBalance;
|
||||
private boolean excludePastUnDisbursed;
|
||||
|
||||
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.disbursement = repaymentScheduleRelatedLoanData.disbursementData();
|
||||
this.capitalizedIncomeData = capitalizedIncomeData;
|
||||
this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement();
|
||||
this.lastDueDate = this.disbursement.disbursementDate();
|
||||
this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal();
|
||||
@@ -1359,10 +1374,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
||||
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
|
||||
final boolean complete = rs.getBoolean("complete");
|
||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||
|
||||
disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds,
|
||||
disbursementChargeAmount, waivedChargeAmount, periods);
|
||||
List<LoanSchedulePeriodDataWrapper> combinedDataList = new ArrayList<>();
|
||||
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
|
||||
final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits");
|
||||
@@ -1473,26 +1493,82 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount());
|
||||
}
|
||||
|
||||
private BigDecimal processDisbursementData(LoanScheduleType loanScheduleType, Collection<DisbursementData> disbursementData,
|
||||
LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds, BigDecimal disbursementChargeAmount,
|
||||
BigDecimal waivedChargeAmount, List<LoanSchedulePeriodData> periods) {
|
||||
BigDecimal disbursedAmount = BigDecimal.ZERO;
|
||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
||||
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
|
||||
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
||||
// Collect eligible disbursement data
|
||||
for (final DisbursementData data : disbursementData) {
|
||||
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))
|
||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||
&& !disbursementPeriodIds.contains(data.getId())) {
|
||||
disbursedAmount = disbursedAmount.add(data.getPrincipal());
|
||||
LoanSchedulePeriodData periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount);
|
||||
periods.add(periodData);
|
||||
this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed());
|
||||
&& !disbursementPeriodIds.contains(data.getId());
|
||||
|
||||
if (isEligible) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
BigDecimal waivedChargeAmount) {
|
||||
BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount
|
||||
@@ -1501,11 +1577,20 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
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) {
|
||||
return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate()))
|
||||
&& 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> {
|
||||
@@ -2168,7 +2253,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT);
|
||||
final Collection<PaymentTypeData> paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount();
|
||||
final Boolean isReversed = false;
|
||||
final Boolean isManuallyReversed = false;
|
||||
|
||||
final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency);
|
||||
|
||||
@@ -2178,7 +2263,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(),
|
||||
loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(),
|
||||
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> {
|
||||
|
||||
+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.LoanJournalEntryPoster;
|
||||
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.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
@@ -93,6 +94,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
private final LoanBalanceService loanBalanceService;
|
||||
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter,
|
||||
@@ -245,8 +247,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType()
|
||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) {
|
||||
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted.";
|
||||
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()
|
||||
&& !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",
|
||||
"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,
|
||||
scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) {
|
||||
reprocessLoanTransactionsService.reprocessTransactions(loan);
|
||||
}
|
||||
}
|
||||
|
||||
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
|
||||
|
||||
+7
-3
@@ -204,9 +204,13 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLoanNote(final Long loanId, final String note) {
|
||||
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
||||
final Note newNote = Note.loanNote(loan, note);
|
||||
public void createLoanTransactionNote(final Long loanTransactionId, final String note) {
|
||||
final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId)
|
||||
.orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId));
|
||||
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
|
||||
final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note);
|
||||
|
||||
this.noteRepository.save(newNote);
|
||||
}
|
||||
|
||||
@@ -196,4 +196,5 @@
|
||||
<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/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
|
||||
</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(50.0, "Capitalized Income", "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") //
|
||||
);
|
||||
|
||||
@@ -89,8 +89,8 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
||||
journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
|
||||
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
|
||||
journalEntry(0.02, interestReceivableAccount, "DEBIT"), //
|
||||
journalEntry(0.02, interestIncomeAccount, "CREDIT"), //
|
||||
journalEntry(0.03, interestReceivableAccount, "DEBIT"), //
|
||||
journalEntry(0.03, interestIncomeAccount, "CREDIT"), //
|
||||
journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||
journalEntry(0.55, feeIncomeAccount, "CREDIT") //
|
||||
);
|
||||
|
||||
+35
@@ -19,7 +19,9 @@
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
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