FINERACT-2081: Payments after loan charge - payment allocation
This commit is contained in:
committed by
Adam Saghy
parent
2d0c8db993
commit
31c9079ad9
+4
@@ -120,6 +120,10 @@ public enum ChargeCalculationType {
|
||||
|| isPercentageOfDisbursementAmount();
|
||||
}
|
||||
|
||||
public boolean hasInterest() {
|
||||
return isPercentageOfInterest() || isPercentageOfAmountAndInterest();
|
||||
}
|
||||
|
||||
public boolean isPercentageOfDisbursementAmount() {
|
||||
return this.value.equals(ChargeCalculationType.PERCENT_OF_DISBURSEMENT_AMOUNT.getValue());
|
||||
}
|
||||
|
||||
+4
@@ -338,6 +338,10 @@ public final class MathUtil {
|
||||
|
||||
// ----------------- Money -----------------
|
||||
|
||||
public static BigDecimal toBigDecimal(Money value) {
|
||||
return value == null ? null : value.getAmount();
|
||||
}
|
||||
|
||||
public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency) {
|
||||
return nullToDefault(value, Money.zero(currency));
|
||||
}
|
||||
|
||||
+30
-1
@@ -20,7 +20,36 @@ package org.apache.fineract.test.data.loanproduct;
|
||||
|
||||
public enum DefaultLoanProduct implements LoanProduct {
|
||||
|
||||
LP1, LP1_DUE_DATE, LP1_INTEREST_FLAT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_SAME_AS_PAYMENT, LP1_INTEREST_DECLINING_BALANCE_PERIOD_DAILY, LP1_1MONTH_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_MONTHLY, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_REDUCE_NR_INST, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_RESCH_NEXT_REP, LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE, LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_INTEREST_FLAT, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE, LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_INTEREST_FLAT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT, LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT_INTEREST, LP2_DOWNPAYMENT, LP2_DOWNPAYMENT_AUTO, LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION, LP2_DOWNPAYMENT_INTEREST, LP2_DOWNPAYMENT_INTEREST_AUTO, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_INSTALLMENT_LEVEL_DELINQUENCY, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION, LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH, LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE, LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY;
|
||||
LP1, //
|
||||
LP1_DUE_DATE, //
|
||||
LP1_INTEREST_FLAT, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_PERIOD_SAME_AS_PAYMENT, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_PERIOD_DAILY, //
|
||||
LP1_1MONTH_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_MONTHLY, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_REDUCE_NR_INST, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_RESCHEDULE_RESCH_NEXT_REP, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE, //
|
||||
LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB, //
|
||||
LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE, //
|
||||
LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_INTEREST_FLAT, //
|
||||
LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE, //
|
||||
LP1_PAYMENT_STRATEGY_DUE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_INTEREST_FLAT, //
|
||||
LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT, //
|
||||
LP1_INTEREST_FLAT_OVERDUE_FROM_AMOUNT_INTEREST, //
|
||||
LP2_DOWNPAYMENT, LP2_DOWNPAYMENT_AUTO, //
|
||||
LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION, //
|
||||
LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION, //
|
||||
LP2_DOWNPAYMENT_INTEREST, LP2_DOWNPAYMENT_INTEREST_AUTO, //
|
||||
LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
|
||||
LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_VERTICAL, //
|
||||
LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_INSTALLMENT_LEVEL_DELINQUENCY, //
|
||||
LP2_DOWNPAYMENT_ADV_PMT_ALLOC_PROG_SCHEDULE_HOR_INST_LVL_DELINQUENCY_CREDIT_ALLOCATION, //
|
||||
LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY, //
|
||||
LP2_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
|
||||
;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
|
||||
+110
@@ -770,6 +770,116 @@ public class LoanProductsRequestFactory {
|
||||
.incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));//
|
||||
}
|
||||
|
||||
public PostLoanProductsRequest defaultLoanProductsRequestLP2NoDown() {
|
||||
String name = Utils.randomNameGenerator(NAME_PREFIX, 4);
|
||||
String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX, 3);
|
||||
|
||||
List<Integer> principalVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<Integer> numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<Integer> interestRateVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<ChargeData> charges = new ArrayList<>();
|
||||
List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings = new ArrayList<>();
|
||||
List<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings = new ArrayList<>();
|
||||
|
||||
List<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings = new ArrayList<>();
|
||||
GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings();
|
||||
loanPaymentChannelToFundSourceMappings.fundSourceAccountId(accountTypeResolver.resolve(DefaultAccountType.FUND_RECEIVABLES));
|
||||
loanPaymentChannelToFundSourceMappings.paymentTypeId(paymentTypeResolver.resolve(DefaultPaymentType.MONEY_TRANSFER));
|
||||
paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings);
|
||||
|
||||
return new PostLoanProductsRequest()//
|
||||
.name(name)//
|
||||
.shortName(shortName)//
|
||||
.description(DESCRIPTION_LP2)//
|
||||
.fundId(FUND_ID)//
|
||||
.startDate(null)//
|
||||
.closeDate(null)//
|
||||
.includeInBorrowerCycle(false)//
|
||||
.currencyCode(CURRENCY_CODE)//
|
||||
.digitsAfterDecimal(2)//
|
||||
.inMultiplesOf(0)//
|
||||
.installmentAmountInMultiplesOf(1)//
|
||||
.useBorrowerCycle(false)//
|
||||
.minPrincipal(100.0)//
|
||||
.principal(1000.0)//
|
||||
.maxPrincipal(10000.0)//
|
||||
.minNumberOfRepayments(1)//
|
||||
.numberOfRepayments(1)//
|
||||
.maxNumberOfRepayments(30)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.minInterestRatePerPeriod((double) 0)//
|
||||
.interestRatePerPeriod((double) 0)//
|
||||
.maxInterestRatePerPeriod((double) 0)//
|
||||
.interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_MONTH)//
|
||||
.repaymentEvery(30)//
|
||||
.repaymentFrequencyType(REPAYMENT_FREQUENCY_TYPE_DAYS)//
|
||||
.principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)//
|
||||
.numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)//
|
||||
.interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)//
|
||||
.amortizationType(AMORTIZATION_TYPE)//
|
||||
.interestType(INTEREST_TYPE_DECLINING_BALANCE)//
|
||||
.isEqualAmortization(false)//
|
||||
.interestCalculationPeriodType(INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT)//
|
||||
.transactionProcessingStrategyCode(TRANSACTION_PROCESSING_STRATEGY_CODE)//
|
||||
.daysInYearType(DAYS_IN_YEAR_TYPE)//
|
||||
.daysInMonthType(DAYS_IN_MONTH_TYPE)//
|
||||
.canDefineInstallmentAmount(true)//
|
||||
.graceOnArrearsAgeing(3)//
|
||||
.overdueDaysForNPA(179)//
|
||||
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||
.principalThresholdForLastInstallment(50)//
|
||||
.allowVariableInstallments(false)//
|
||||
.canUseForTopup(false)//
|
||||
.isInterestRecalculationEnabled(false)//
|
||||
.holdGuaranteeFunds(false)//
|
||||
.multiDisburseLoan(true)//
|
||||
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||
.amortizationType(true)//
|
||||
.interestType(true)//
|
||||
.transactionProcessingStrategyCode(true)//
|
||||
.interestCalculationPeriodType(true)//
|
||||
.inArrearsTolerance(true)//
|
||||
.repaymentEvery(true)//
|
||||
.graceOnPrincipalAndInterestPayment(true)//
|
||||
.graceOnArrearsAgeing(true))//
|
||||
.allowPartialPeriodInterestCalcualtion(true)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.charges(charges)//
|
||||
.accountingRule(LOAN_ACCOUNTING_RULE)//
|
||||
.fundSourceAccountId(accountTypeResolver.resolve(DefaultAccountType.SUSPENSE_CLEARING_ACCOUNT))//
|
||||
.loanPortfolioAccountId(accountTypeResolver.resolve(DefaultAccountType.LOANS_RECEIVABLE))//
|
||||
.transfersInSuspenseAccountId(accountTypeResolver.resolve(DefaultAccountType.TRANSFER_IN_SUSPENSE_ACCOUNT))//
|
||||
.interestOnLoanAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME))//
|
||||
.incomeFromFeeAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_INCOME))//
|
||||
.incomeFromPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_INCOME))//
|
||||
.incomeFromRecoveryAccountId(accountTypeResolver.resolve(DefaultAccountType.RECOVERIES))//
|
||||
.writeOffAccountId(accountTypeResolver.resolve(DefaultAccountType.WRITTEN_OFF))//
|
||||
.overpaymentLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.OVERPAYMENT_ACCOUNT))//
|
||||
.receivableInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))//
|
||||
.receivableFeeAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))//
|
||||
.receivablePenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_FEE_RECEIVABLE))//
|
||||
.dateFormat(DATE_FORMAT)//
|
||||
.locale(LOCALE_EN)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType(OVER_APPLIED_CALCULATION_TYPE)//
|
||||
.overAppliedNumber(OVER_APPLIED_NUMBER)//
|
||||
.delinquencyBucketId(DELINQUENCY_BUCKET_ID.longValue())//
|
||||
.goodwillCreditAccountId(accountTypeResolver.resolve(DefaultAccountType.GOODWILL_EXPENSE_ACCOUNT))//
|
||||
.incomeFromGoodwillCreditInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME_CHARGE_OFF))//
|
||||
.incomeFromGoodwillCreditFeesAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))//
|
||||
.incomeFromGoodwillCreditPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))//
|
||||
.paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)//
|
||||
.penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)//
|
||||
.feeToIncomeAccountMappings(feeToIncomeAccountMappings)//
|
||||
.incomeFromChargeOffInterestAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME_CHARGE_OFF))//
|
||||
.incomeFromChargeOffFeesAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF))//
|
||||
.chargeOffExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT))//
|
||||
.chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))//
|
||||
.incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));//
|
||||
}
|
||||
|
||||
public PostLoanProductsRequest defaultLoanProductsRequestLP2InterestFlat() {
|
||||
String name = Utils.randomNameGenerator(NAME_PREFIX_INTEREST_FLAT_LP2, 4);
|
||||
String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX_INTEREST, 3);
|
||||
|
||||
+17
@@ -534,6 +534,23 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY,
|
||||
responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillRestFrequency);
|
||||
|
||||
String name40 = DefaultLoanProduct.LP2_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL.getName();
|
||||
PostLoanProductsRequest loanProductsRequestLP2AdvPmtAllocProgressiveLoanScheduleHorizontal = loanProductsRequestFactory
|
||||
.defaultLoanProductsRequestLP2NoDown()//
|
||||
.name(name40)//
|
||||
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
|
||||
.loanScheduleType("PROGRESSIVE") //
|
||||
.loanScheduleProcessingType("HORIZONTAL")//
|
||||
.paymentAllocation(List.of(//
|
||||
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
|
||||
createPaymentAllocation("GOODWILL_CREDIT", "NEXT_INSTALLMENT"), //
|
||||
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "NEXT_INSTALLMENT"), //
|
||||
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
|
||||
Response<PostLoanProductsResponse> responseLP2AdvPmtAllocProgressiveLoanScheduleHorizontal = loanProductsApi
|
||||
.createLoanProduct(loanProductsRequestLP2AdvPmtAllocProgressiveLoanScheduleHorizontal).execute();
|
||||
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL,
|
||||
responseLP2AdvPmtAllocProgressiveLoanScheduleHorizontal);
|
||||
}
|
||||
|
||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||
|
||||
+1
@@ -79,6 +79,7 @@ public abstract class TestContextKey {
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_ADV_PMT_ALLOC_FIXED_LENGTH = "loanProductCreateResponseLP2DownPaymentProgressiveLoanScheduleFixedLength";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationTillPreClose";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationTillRestFrequency";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL = "loanProductCreateResponseLP2ProgressiveLoanScheduleHorizontal";
|
||||
public static final String CHARGE_FOR_LOAN_PERCENT_LATE_CREATE_RESPONSE = "ChargeForLoanPercentLateCreateResponse";
|
||||
public static final String CHARGE_FOR_LOAN_PERCENT_LATE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanPercentLateAmountPlusInterestCreateResponse";
|
||||
public static final String CHARGE_FOR_LOAN_PERCENT_PROCESSING_CREATE_RESPONSE = "ChargeForLoanPercentProcessingCreateResponse";
|
||||
|
||||
@@ -3241,4 +3241,139 @@ Feature: LoanRepayment
|
||||
And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount
|
||||
When Admin sets the business date to "04 September 2023"
|
||||
#Run COB to check there is no accounting meltdown and accrual is handled properly
|
||||
When Admin runs inline COB job for Loan
|
||||
When Admin runs inline COB job for Loan
|
||||
|
||||
@AdvancedPaymentAllocation @ProgressiveLoanSchedule
|
||||
Scenario: Verify that payment allocation is correct in case of fee charged and payment is backdated
|
||||
# --- 7/23 - Loan Created & Approved ---
|
||||
When Admin sets the business date to "23 July 2024"
|
||||
When Admin creates a client with random data
|
||||
When Admin creates a fully customized loan with the following data:
|
||||
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||
| LP2_ADV_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 23 July 2024 | 150 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 1 | MONTHS | 1 | MONTHS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||
# --- 7/23 - Disbursement for 111.92 EUR ---
|
||||
And Admin successfully approves the loan on "23 July 2024" with "111.92" amount and expected disbursement date on "23 July 2024"
|
||||
When Admin successfully disburse the loan on "23 July 2024" with "111.92" EUR transaction amount
|
||||
# --- 8/8 - Partial merchant issued refund - 76.48 Eur ---
|
||||
When Admin sets the business date to "08 August 2024"
|
||||
When Customer makes "MERCHANT_ISSUED_REFUND" transaction with "AUTOPAY" payment type on "08 August 2024" with 76.48 EUR transaction amount and system-generated Idempotency key
|
||||
# --- 8/13 - Manual Repayment - 35.44 Eur (Account closed) ---
|
||||
When Admin sets the business date to "13 August 2024"
|
||||
And Customer makes "MONEY_TRANSFER" repayment on "13 August 2024" with 35.44 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan has 0 outstanding amount
|
||||
# --- 8/15 - Repayment reversed (Account reopened) ---
|
||||
When Admin sets the business date to "15 August 2024"
|
||||
When Customer undo "1"th "Repayment" transaction made on "13 August 2024"
|
||||
Then Loan status will be "ACTIVE"
|
||||
Then Loan has 35.44 outstanding amount
|
||||
# --- 8/22 - Autopay posted for 35.44 Eur ---
|
||||
When Admin sets the business date to "22 August 2024"
|
||||
And Customer makes "AUTOPAY" repayment on "22 August 2024" with 35.44 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan has 0 outstanding amount
|
||||
# --- 8/24 - Autopay reversed ---
|
||||
When Admin sets the business date to "24 August 2024"
|
||||
When Customer undo "1"th "Repayment" transaction made on "22 August 2024"
|
||||
Then Loan status will be "ACTIVE"
|
||||
Then Loan has 35.44 outstanding amount
|
||||
# --- 8/24 - Loan Charge created for 2.80 Eur ---
|
||||
When Admin adds "LOAN_NSF_FEE" due date charge with "24 August 2024" due date and 2.80 EUR transaction amount
|
||||
Then Loan status will be "ACTIVE"
|
||||
Then Loan has 38.24 outstanding amount
|
||||
# --- 8/24 (after COB) - Accrual created ---
|
||||
When Admin sets the business date to "25 August 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 2 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 23 August 2024 | | 0.0 | 111.92 | 0.0 | 0.0 | 0.0 | 111.92 | 76.48 | 76.48 | 0.0 | 35.44 |
|
||||
| 2 | 1 | 24 August 2024 | | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 111.92 | 0.0 | 0.0 | 2.8 | 114.72 | 76.48 | 76.48 | 0.0 | 38.24 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false |
|
||||
| 08 August 2024 | Merchant Issued Refund | 76.48 | 76.48 | 0.0 | 0.0 | 0.0 | 35.44 | false |
|
||||
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
# --- 8/28 - Backdated Autopay posted for 38.24 Eur (35.44 principal + 2.80 penalty) with transactionDate 8/22 ---
|
||||
When Admin sets the business date to "28 August 2024"
|
||||
And Customer makes "AUTOPAY" repayment on "22 August 2024" with 38.24 EUR transaction amount
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan has 0 outstanding amount
|
||||
# TODO check data
|
||||
Then Loan Repayment schedule has 2 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 23 August 2024 | 22 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 0.0 | 111.92 | 111.92 | 111.92 | 0.0 | 0.0 |
|
||||
| 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 111.92 | 0.0 | 0.0 | 2.8 | 114.72 | 114.72 | 114.72 | 0.0 | 0.0 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false |
|
||||
| 08 August 2024 | Merchant Issued Refund | 76.48 | 76.48 | 0.0 | 0.0 | 0.0 | 35.44 | false |
|
||||
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
When Customer makes "AUTOPAY" repayment on "23 August 2024" with 10 EUR transaction amount
|
||||
Then Loan status will be "OVERPAID"
|
||||
Then Loan has 0 outstanding amount
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false |
|
||||
| 08 August 2024 | Merchant Issued Refund | 76.48 | 76.48 | 0.0 | 0.0 | 0.0 | 35.44 | false |
|
||||
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
| 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false |
|
||||
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
Then Loan Repayment schedule has 2 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 23 August 2024 | 22 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 0.0 | 111.92 | 111.92 | 111.92 | 0.0 | 0.0 |
|
||||
| 2 | 1 | 24 August 2024 | 22 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 |
|
||||
When Admin sets the business date to "29 August 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
When Admin adds "LOAN_NSF_FEE" due date charge with "22 August 2024" due date and 5 EUR transaction amount
|
||||
Then Loan status will be "OVERPAID"
|
||||
Then Loan has 0 outstanding amount
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false |
|
||||
| 08 August 2024 | Merchant Issued Refund | 76.48 | 76.48 | 0.0 | 0.0 | 0.0 | 35.44 | false |
|
||||
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
| 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
|
||||
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
Then Loan Repayment schedule has 2 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 23 August 2024 | 23 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 5.0 | 116.92 | 116.92 | 114.72 | 0.0 | 0.0 |
|
||||
| 2 | 1 | 24 August 2024 | 23 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 2.8 | 2.8 | 2.8 | 2.8 | 0.0 | 0.0 |
|
||||
When Admin adds "LOAN_NSF_FEE" due date charge with "25 August 2024" due date and 5 EUR transaction amount
|
||||
When Admin sets the business date to "30 August 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
|
||||
Then Loan has 0 outstanding amount
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||
| 23 July 2024 | Disbursement | 111.92 | 0.0 | 0.0 | 0.0 | 0.0 | 111.92 | false |
|
||||
| 08 August 2024 | Merchant Issued Refund | 76.48 | 76.48 | 0.0 | 0.0 | 0.0 | 35.44 | false |
|
||||
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
|
||||
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
| 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false |
|
||||
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
|
||||
| 25 August 2024 | Accrual | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false |
|
||||
Then Loan Repayment schedule has 2 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 23 August 2024 | 23 August 2024 | 0.0 | 111.92 | 0.0 | 0.0 | 5.0 | 116.92 | 116.92 | 114.72 | 0.0 | 0.0 |
|
||||
| 2 | 2 | 25 August 2024 | 23 August 2024 | 0.0 | 0.0 | 0.0 | 0.0 | 7.8 | 7.8 | 7.8 | 7.8 | 0.0 | 0.0 |
|
||||
|
||||
+18
-14
@@ -704,8 +704,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.charges.add(loanCharge);
|
||||
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
|
||||
|
||||
// store Id's of existing loan transactions and existing reversed loan
|
||||
// transactions
|
||||
// store Id's of existing loan transactions and existing reversed loan transactions
|
||||
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge);
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -721,7 +720,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
@@ -729,6 +727,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail reprocessTransactionsWithPostTransactionChecks(LocalDate transactionDate) {
|
||||
ChangedTransactionDetail changedDetail = reprocessTransactions();
|
||||
doPostLoanTransactionChecks(transactionDate, loanLifecycleStateMachine);
|
||||
return changedDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The
|
||||
* newly created transaction is also added to the Loan on which this method is called.
|
||||
@@ -2499,7 +2503,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
boolean statusChanged = false;
|
||||
boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
|
||||
boolean isOverpaid = MathUtil.isGreaterThanZero(totalOverpaid);
|
||||
if (isOverpaid) {
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
||||
@@ -2510,7 +2514,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
} else {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
||||
}
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
}
|
||||
return statusChanged;
|
||||
@@ -2526,8 +2530,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.actualMaturityDate = transactionDate;
|
||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this);
|
||||
|
||||
} else if (LoanStatus.fromInt(this.loanStatus).isOverpaid()) {
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
} else if (getStatus().isOverpaid()) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
}
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
||||
@@ -4498,7 +4502,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
|
||||
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
|
||||
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
|
||||
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
|
||||
if (loanTransaction.isOverPaid()) {
|
||||
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
|
||||
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
|
||||
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
|
||||
@@ -4792,7 +4796,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate();
|
||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this);
|
||||
@@ -5497,12 +5501,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getLoanTransactions().stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
public LoanTransaction getLoanTransaction(Predicate<LoanTransaction> predicate) {
|
||||
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public LoanTransaction findChargedOffTransaction() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isNotReversed) //
|
||||
.filter(LoanTransaction::isChargeOff) //
|
||||
.findFirst() //
|
||||
.orElse(null);
|
||||
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
|
||||
}
|
||||
|
||||
public void handleMaturityDateActivate() {
|
||||
|
||||
+86
-23
@@ -38,9 +38,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
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.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@@ -303,6 +305,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
public LoanTransaction copyTransactionPropertiesAndMappings() {
|
||||
LoanTransaction newTransaction = copyTransactionProperties(this);
|
||||
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
||||
newTransaction.updateLoanChargePaidMappings(loanChargesPaid);
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
||||
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
|
||||
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
|
||||
@@ -920,37 +929,84 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
|
||||
public void updateLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> mappings) {
|
||||
Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings = new ArrayList<>();
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping : mappings) {
|
||||
updateMapingDetail(retainMappings, updatedrepaymentScheduleMapping);
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : mappings) {
|
||||
updateMappingDetail(retainMappings, updatedMapping, true);
|
||||
}
|
||||
this.loanTransactionToRepaymentScheduleMappings.retainAll(retainMappings);
|
||||
}
|
||||
|
||||
private boolean updateMapingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
||||
final LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping) {
|
||||
public void addLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> updatedMappings) {
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : updatedMappings) {
|
||||
updateMappingDetail(null, updatedMapping, false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateMappingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
||||
final LoanTransactionToRepaymentScheduleMapping updatedMapping, boolean overwrite) {
|
||||
boolean isMappingUpdated = false;
|
||||
for (LoanTransactionToRepaymentScheduleMapping repaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
||||
if (updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId() != null
|
||||
&& repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate()
|
||||
.equals(updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate())
|
||||
&& updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId()
|
||||
.equals(repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId())) {
|
||||
repaymentScheduleMapping.setComponents(updatedrepaymentScheduleMapping.getPrincipalPortion(),
|
||||
updatedrepaymentScheduleMapping.getInterestPortion(), updatedrepaymentScheduleMapping.getFeeChargesPortion(),
|
||||
updatedrepaymentScheduleMapping.getPenaltyChargesPortion());
|
||||
LoanRepaymentScheduleInstallment updatedInstallment = updatedMapping.getLoanRepaymentScheduleInstallment();
|
||||
for (LoanTransactionToRepaymentScheduleMapping existingMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
||||
LoanRepaymentScheduleInstallment existingInstallment = existingMapping.getLoanRepaymentScheduleInstallment();
|
||||
if (DateUtils.isEqual(existingInstallment.getDueDate(), updatedInstallment.getDueDate()) && updatedInstallment.getId() != null
|
||||
&& updatedInstallment.getId().equals(existingInstallment.getId())) {
|
||||
if (overwrite) {
|
||||
existingMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
} else {
|
||||
existingMapping.updateComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
}
|
||||
isMappingUpdated = true;
|
||||
retainMappings.add(repaymentScheduleMapping);
|
||||
if (retainMappings != null) {
|
||||
retainMappings.add(existingMapping);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isMappingUpdated) {
|
||||
updatedrepaymentScheduleMapping.setLoanTransaction(this);
|
||||
this.loanTransactionToRepaymentScheduleMappings.add(updatedrepaymentScheduleMapping);
|
||||
retainMappings.add(updatedrepaymentScheduleMapping);
|
||||
LoanTransactionToRepaymentScheduleMapping newMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(this,
|
||||
updatedInstallment, null, null, null, null);
|
||||
newMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
this.loanTransactionToRepaymentScheduleMappings.add(newMapping);
|
||||
if (retainMappings != null) {
|
||||
retainMappings.add(newMapping);
|
||||
}
|
||||
}
|
||||
return isMappingUpdated;
|
||||
}
|
||||
|
||||
public void updateLoanChargePaidMappings(final Collection<LoanChargePaidBy> updatedMappings) {
|
||||
Collection<LoanChargePaidBy> retainMappings = new ArrayList<>();
|
||||
for (LoanChargePaidBy updatedMapping : updatedMappings) {
|
||||
updateLoanChargePaid(retainMappings, updatedMapping);
|
||||
}
|
||||
this.loanChargesPaid.retainAll(retainMappings);
|
||||
}
|
||||
|
||||
private boolean updateLoanChargePaid(final Collection<LoanChargePaidBy> retainMappings, final LoanChargePaidBy updatedMapping) {
|
||||
boolean updated = false;
|
||||
LoanCharge updatedCharge = updatedMapping.getLoanCharge();
|
||||
Integer updatedInstallment = updatedMapping.getInstallmentNumber();
|
||||
for (LoanChargePaidBy existingMapping : loanChargesPaid) {
|
||||
LoanCharge existingCharge = existingMapping.getLoanCharge();
|
||||
Integer existingInstallment = existingMapping.getInstallmentNumber();
|
||||
if (existingCharge.equals(updatedCharge)
|
||||
&& (existingInstallment == null ? updatedInstallment == null : existingInstallment.equals(updatedInstallment))) {
|
||||
existingMapping.setAmount(updatedMapping.getAmount());
|
||||
updated = true;
|
||||
retainMappings.add(existingMapping);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
LoanChargePaidBy newMapping = new LoanChargePaidBy(this, updatedCharge, updatedMapping.getAmount(), updatedInstallment);
|
||||
this.loanChargesPaid.add(newMapping);
|
||||
retainMappings.add(newMapping);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
|
||||
return this.loanTransactionToRepaymentScheduleMappings;
|
||||
}
|
||||
@@ -989,18 +1045,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
}
|
||||
|
||||
public boolean hasLoanTransactionRelations() {
|
||||
return (loanTransactionRelations != null && loanTransactionRelations.size() > 0);
|
||||
}
|
||||
|
||||
public boolean hasChargebackLoanTransactionRelations() {
|
||||
return (loanTransactionRelations != null
|
||||
&& loanTransactionRelations.stream().anyMatch(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())));
|
||||
return !loanTransactionRelations.isEmpty();
|
||||
}
|
||||
|
||||
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
|
||||
return loanTransactionRelations;
|
||||
}
|
||||
|
||||
public List<LoanTransactionRelation> getLoanTransactionRelations(Predicate<LoanTransactionRelation> predicate) {
|
||||
return loanTransactionRelations.stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
public boolean hasChargebackLoanTransactionRelations() {
|
||||
return !getLoanTransactionRelations(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())).isEmpty();
|
||||
}
|
||||
|
||||
public void copyLoanTransactionRelations(Set<LoanTransactionRelation> sourceLoanTransactionRelations) {
|
||||
for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) {
|
||||
loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(),
|
||||
@@ -1055,6 +1114,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return chargeRefundChargeType;
|
||||
}
|
||||
|
||||
public boolean isOverPaid() {
|
||||
return MathUtil.isGreaterThanZero(overPaymentPortion);
|
||||
}
|
||||
|
||||
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
||||
// this is never stored in a Collection.
|
||||
}
|
||||
|
||||
+16
-14
@@ -24,8 +24,10 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@@ -77,15 +79,11 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
final Money feeChargesPortion, final Money penaltyChargesPortion) {
|
||||
return new LoanTransactionToRepaymentScheduleMapping(loanTransaction, installment, defaultToNullIfZero(principalPortion),
|
||||
defaultToNullIfZero(interestPortion), defaultToNullIfZero(feeChargesPortion), defaultToNullIfZero(penaltyChargesPortion),
|
||||
defaultToNullIfZero(principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion)));
|
||||
defaultToNullIfZero(MathUtil.plus(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)));
|
||||
}
|
||||
|
||||
private static BigDecimal defaultToNullIfZero(final Money value) {
|
||||
BigDecimal result = value.getAmount();
|
||||
if (value.isZero()) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
return (value == null || value.isZero()) ? null : value.getAmount();
|
||||
}
|
||||
|
||||
private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
|
||||
@@ -100,10 +98,15 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
return this.installment;
|
||||
}
|
||||
|
||||
public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = principal.getCurrency();
|
||||
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal));
|
||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest));
|
||||
public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges,
|
||||
@NotNull Money penaltyCharges) {
|
||||
updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
|
||||
}
|
||||
|
||||
void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
|
||||
final BigDecimal penaltyCharges) {
|
||||
this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), principal));
|
||||
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), interest));
|
||||
updateChargesComponents(feeCharges, penaltyCharges);
|
||||
updateAmount();
|
||||
}
|
||||
@@ -122,10 +125,9 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
updateAmount();
|
||||
}
|
||||
|
||||
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = feeCharges.getCurrency();
|
||||
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges));
|
||||
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges));
|
||||
private void updateChargesComponents(final BigDecimal feeCharges, final BigDecimal penaltyCharges) {
|
||||
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), feeCharges));
|
||||
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), penaltyCharges));
|
||||
}
|
||||
|
||||
public Money getPrincipalPortion(final MonetaryCurrency currency) {
|
||||
|
||||
+11
-1
@@ -28,7 +28,9 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleProcessingType {
|
||||
|
||||
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
|
||||
HORIZONTAL("Horizontal"), //
|
||||
VERTICAL("Vertical"), //
|
||||
;
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
@@ -39,4 +41,12 @@ public enum LoanScheduleProcessingType {
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
|
||||
public boolean isHorizontal() {
|
||||
return this == HORIZONTAL;
|
||||
}
|
||||
|
||||
public boolean isVertical() {
|
||||
return this == VERTICAL;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -50,4 +50,8 @@ public enum PaymentAllocationTransactionType {
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
return this == DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
+260
-139
@@ -26,10 +26,9 @@ import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FE
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE;
|
||||
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -91,8 +90,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocat
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -180,20 +177,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, installments, mc);
|
||||
|
||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
|
||||
changedTransactionDetail, scheduleModel);
|
||||
|
||||
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
|
||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, ctx));
|
||||
chargeOrTransaction.getLoanCharge()
|
||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
||||
if (chargeOrTransaction.isTransaction()) {
|
||||
LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get();
|
||||
processSingleTransaction(transaction, ctx);
|
||||
transaction = getProcessedTransaction(changedTransactionDetail, transaction);
|
||||
if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT
|
||||
overpaidTransactions.add(transaction);
|
||||
}
|
||||
} else {
|
||||
LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get();
|
||||
processSingleCharge(loanCharge, currency, installments, disbursementDate);
|
||||
if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) {
|
||||
overpaidTransactions = processOverpaidTransactions(overpaidTransactions, currency, installments, charges,
|
||||
changedTransactionDetail, overpaymentHolder, scheduleModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<Long, LoanTransaction> newTransactionMappings = changedTransactionDetail.getNewTransactionMappings();
|
||||
for (Long oldTransactionId : newTransactionMappings.keySet()) {
|
||||
LoanTransaction oldTransaction = loanTransactions.stream().filter(e -> oldTransactionId.equals(e.getId())).findFirst().get();
|
||||
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
|
||||
createNewTransaction(oldTransaction, newTransaction, ctx);
|
||||
}
|
||||
List<LoanTransaction> txs = chargeOrTransactions.stream() //
|
||||
.map(ChargeOrTransaction::getLoanTransaction) //
|
||||
.filter(Optional::isPresent) //
|
||||
.map(Optional::get).toList();
|
||||
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx, true);
|
||||
List<LoanTransaction> txs = chargeOrTransactions.stream() //
|
||||
.filter(ChargeOrTransaction::isTransaction) //
|
||||
.map(e -> e.getLoanTransaction().get()).toList();
|
||||
reprocessInstallments(disbursementDate, txs, installments, currency);
|
||||
return Pair.of(changedTransactionDetail, scheduleModel);
|
||||
}
|
||||
@@ -204,6 +218,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) {
|
||||
LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId());
|
||||
return newTransaction == null ? transaction : newTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
@@ -283,36 +303,33 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
||||
}
|
||||
|
||||
protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
if (loanTransaction.getId() != null) { // this the normal case without reverse-replay
|
||||
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
||||
.anyMatch(this.hasMatchingToLoanTransaction(loanTransaction.getId(), CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (originalTransaction.isEmpty()) {
|
||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||
protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) {
|
||||
ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||
Long chargebackId = chargebackTransaction.getId(); // this the normal case without reverse-replay
|
||||
if (changedTransactionDetail != null) {
|
||||
if (chargebackId == null) {
|
||||
// the chargeback transaction was changed, so we need to look it up from the ctx.
|
||||
chargebackId = changedTransactionDetail.getCurrentTransactionToOldId().get(chargebackTransaction);
|
||||
}
|
||||
return originalTransaction.get();
|
||||
} else { // when there is no id, then it might be that the original transaction is changed, so we need to look
|
||||
// it up from the Ctx.
|
||||
Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
|
||||
Collection<LoanTransaction> updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values();
|
||||
Optional<LoanTransaction> updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations()
|
||||
.stream().anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK))).findFirst();
|
||||
|
||||
if (updatedTransaction.isPresent()) {
|
||||
return updatedTransaction.get();
|
||||
} else { // if it is not there, then it simply means that this has not changed during reverse replay
|
||||
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
||||
.anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (originalTransaction.isEmpty()) {
|
||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||
}
|
||||
return originalTransaction.get();
|
||||
Long toId = chargebackId;
|
||||
Collection<LoanTransaction> updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values();
|
||||
Optional<LoanTransaction> fromTransaction = updatedTransactions.stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (fromTransaction.isPresent()) {
|
||||
return fromTransaction.get();
|
||||
}
|
||||
}
|
||||
Long toId = chargebackId;
|
||||
// if the original transaction is not in the ctx, then it means that it has not changed during reverse replay
|
||||
Optional<LoanTransaction> fromTransaction = chargebackTransaction.getLoan().getLoanTransactions().stream()
|
||||
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(toId, CHARGEBACK)))
|
||||
.findFirst();
|
||||
if (fromTransaction.isEmpty()) {
|
||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
||||
}
|
||||
return fromTransaction.get();
|
||||
}
|
||||
|
||||
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
@@ -331,7 +348,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (transactionAmount.isGreaterThanZero()) {
|
||||
if (loanTransaction.isChargeback()) {
|
||||
LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx);
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||
// get the original allocation from the opriginal transaction
|
||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
|
||||
ctx.getCurrency());
|
||||
@@ -420,8 +437,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
List<LoanTransaction> chargebacks = allTransactions.stream().filter(LoanTransaction::isChargeback).toList();
|
||||
|
||||
// let's figure out the original transaction for these chargebacks, and order them by ascending order
|
||||
Comparator<LoanTransaction> comparator = loanTransactionDateComparator();
|
||||
List<LoanTransaction> chargebacksForTheSameOriginal = chargebacks.stream()
|
||||
.filter(tr -> findOriginalTransaction(tr, ctx) == originalTransaction).sorted(loanTransactionDateComparator()).toList();
|
||||
.filter(tr -> findChargebackOriginalTransaction(tr, ctx) == originalTransaction
|
||||
&& comparator.compare(tr, chargeBackTransaction) < 0)
|
||||
.sorted(comparator).toList();
|
||||
|
||||
Map<AllocationType, Money> allocation = new HashMap<>(originalAllocation);
|
||||
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
|
||||
@@ -518,16 +538,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
LoanPaymentAllocationRule paymentAllocationRule = getAllocationRule(loanTransaction);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
List<PaymentAllocationType> paymentAllocationTypes;
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
|
||||
if (DEFAULT.equals(paymentAllocationRule.getTransactionType())) {
|
||||
if (paymentAllocationRule.getTransactionType().isDefault()) {
|
||||
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
|
||||
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
|
||||
Collections.reverse(paymentAllocationTypes);
|
||||
@@ -536,8 +551,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
|
||||
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
}
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
LoanScheduleProcessingType scheduleProcessingType = loan.getLoanProductRelatedDetail().getLoanScheduleProcessingType();
|
||||
if (scheduleProcessingType.isHorizontal()) {
|
||||
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationTypes.stream().collect(
|
||||
Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList())));
|
||||
|
||||
@@ -549,8 +565,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
} else if (scheduleProcessingType.isVertical()) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
|
||||
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
|
||||
@@ -567,55 +582,152 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private void processSingleTransaction(LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
|
||||
final MonetaryCurrency currency = ctx.getCurrency();
|
||||
final ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, ctx);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
loanTransaction.adjustInterestComponent(currency);
|
||||
}
|
||||
boolean isNew = loanTransaction.getId() == null;
|
||||
LoanTransaction processTransaction = loanTransaction;
|
||||
if (!isNew) {
|
||||
// For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
|
||||
// changed.
|
||||
processTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(processTransaction, loanTransaction.getId());
|
||||
}
|
||||
// Reset derived component of new loan transaction and re-process transaction
|
||||
processLatestTransaction(processTransaction, ctx);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
processTransaction.adjustInterestComponent(ctx.getCurrency());
|
||||
}
|
||||
if (isNew) {
|
||||
checkRegisteredNewTransaction(loanTransaction, ctx);
|
||||
} else {
|
||||
/*
|
||||
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
|
||||
* changed.<br>
|
||||
*/
|
||||
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(newLoanTransaction, loanTransaction.getId());
|
||||
|
||||
// Reset derived component of new loan transaction and
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction, ctx);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
}
|
||||
/*
|
||||
* Check if the transaction amounts have changed or was there any transaction for the same date which was
|
||||
* reverse-replayed. If so, reverse the original transaction and update changedTransactionDetail accordingly
|
||||
*/
|
||||
boolean aTransactionWasAlreadyReplayedForTheSameDate = changedTransactionDetail.getNewTransactionMappings().values().stream()
|
||||
.anyMatch(lt -> lt.getTransactionDate().equals(loanTransaction.getTransactionDate()));
|
||||
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)
|
||||
&& !aTransactionWasAlreadyReplayedForTheSameDate) {
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
|
||||
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
|
||||
} else {
|
||||
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
|
||||
checkAndUpdateReplayedChargebackRelationWithReplayedTransaction(loanTransaction, newLoanTransaction, ctx);
|
||||
}
|
||||
updateOrRegisterNewTransaction(loanTransaction, processTransaction, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndUpdateReplayedChargebackRelationWithReplayedTransaction(LoanTransaction loanTransaction,
|
||||
LoanTransaction newLoanTransaction, TransactionCtx ctx) {
|
||||
// if chargeback is getting reverse-replayed
|
||||
// find replayed transaction with CHARGEBACK relation with reversed chargeback transaction
|
||||
// for replayed transaction, add relation to point to new Chargeback transaction
|
||||
if (loanTransaction.getTypeOf().isChargeback()) {
|
||||
LoanTransaction originalTransaction = findOriginalTransaction(newLoanTransaction, ctx);
|
||||
originalTransaction.getLoanTransactionRelations()
|
||||
.add(LoanTransactionRelation.linkToTransaction(originalTransaction, newLoanTransaction, CHARGEBACK));
|
||||
private List<LoanTransaction> processOverpaidTransactions(List<LoanTransaction> overpaidTransactions, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
|
||||
MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
List<LoanTransaction> remainingTransactions = new ArrayList<>(overpaidTransactions);
|
||||
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail,
|
||||
scheduleModel);
|
||||
Money zero = Money.zero(currency);
|
||||
for (LoanTransaction transaction : overpaidTransactions) {
|
||||
Money overpayment = transaction.getOverPaymentPortion(currency);
|
||||
Money ctxOverpayment = overpaymentHolder.getMoneyObject();
|
||||
Money processAmount = MathUtil.min(ctxOverpayment, overpayment, false);
|
||||
if (MathUtil.isEmpty(processAmount)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LoanTransaction processTransaction = transaction;
|
||||
boolean isNew = transaction.getId() == null;
|
||||
if (!isNew) {
|
||||
processTransaction = transaction.copyTransactionPropertiesAndMappings();
|
||||
ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().put(processTransaction, transaction.getId());
|
||||
}
|
||||
processTransaction.setOverPayments(overpayment = MathUtil.minus(overpayment, processAmount));
|
||||
overpaymentHolder.setMoneyObject(ctxOverpayment = MathUtil.minus(ctxOverpayment, processAmount));
|
||||
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
|
||||
Money unprocessed = processPeriods(processTransaction, processAmount, charges, transactionMappings, balances, ctx);
|
||||
|
||||
processTransaction.setOverPayments(MathUtil.plus(overpayment, unprocessed));
|
||||
overpaymentHolder.setMoneyObject(MathUtil.plus(ctxOverpayment, unprocessed));
|
||||
|
||||
processTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
processTransaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
if (processTransaction.isInterestWaiver()) {
|
||||
processTransaction.adjustInterestComponent(currency);
|
||||
}
|
||||
if (isNew) {
|
||||
processTransaction = checkRegisteredNewTransaction(transaction, ctx);
|
||||
} else {
|
||||
processTransaction = updateOrRegisterNewTransaction(transaction, processTransaction, ctx);
|
||||
}
|
||||
remainingTransactions.remove(transaction);
|
||||
if (processTransaction.isOverPaid()) {
|
||||
remainingTransactions.add(processTransaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return remainingTransactions;
|
||||
}
|
||||
|
||||
private LoanTransaction checkRegisteredNewTransaction(LoanTransaction newTransaction, TransactionCtx ctx) {
|
||||
ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||
Long oldTransactionId = changedTransactionDetail.getCurrentTransactionToOldId().get(newTransaction);
|
||||
if (oldTransactionId != null) {
|
||||
LoanTransaction oldTransaction = newTransaction.getLoan().getLoanTransaction(e -> oldTransactionId.equals(e.getId()));
|
||||
LoanTransaction applicableTransaction = useOldTransactionIfApplicable(oldTransaction, newTransaction, ctx);
|
||||
if (applicableTransaction != null) {
|
||||
return applicableTransaction;
|
||||
}
|
||||
}
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
private LoanTransaction updateOrRegisterNewTransaction(LoanTransaction oldTransaction, LoanTransaction newTransaction,
|
||||
TransactionCtx ctx) {
|
||||
LoanTransaction applicableTransaction = useOldTransactionIfApplicable(oldTransaction, newTransaction, ctx);
|
||||
if (applicableTransaction != null) {
|
||||
return applicableTransaction;
|
||||
}
|
||||
|
||||
newTransaction.copyLoanTransactionRelations(oldTransaction.getLoanTransactionRelations());
|
||||
|
||||
ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||
changedTransactionDetail.getNewTransactionMappings().put(oldTransaction.getId(), newTransaction);
|
||||
changedTransactionDetail.getCurrentTransactionToOldId().put(newTransaction, oldTransaction.getId());
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static LoanTransaction useOldTransactionIfApplicable(LoanTransaction oldTransaction, LoanTransaction newTransaction,
|
||||
TransactionCtx ctx) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||
Map<Long, LoanTransaction> newTransactionMappings = changedTransactionDetail.getNewTransactionMappings();
|
||||
/*
|
||||
* Check if the transaction amounts have changed or was there any transaction for the same date which was
|
||||
* reverse-replayed. If so, reverse the original transaction and update changedTransactionDetail accordingly to
|
||||
* keep the original order of the transactions.
|
||||
*/
|
||||
boolean alreadyProcessed = newTransactionMappings.values().stream()
|
||||
.anyMatch(lt -> !lt.equals(newTransaction) && lt.getTransactionDate().equals(oldTransaction.getTransactionDate()));
|
||||
boolean amountMatch = LoanTransaction.transactionAmountsMatch(currency, oldTransaction, newTransaction);
|
||||
if (!alreadyProcessed && amountMatch) {
|
||||
if (!oldTransaction.getTypeOf().isWaiveCharges()) { // WAIVE_CHARGES is not reprocessed
|
||||
oldTransaction
|
||||
.updateLoanTransactionToRepaymentScheduleMappings(newTransaction.getLoanTransactionToRepaymentScheduleMappings());
|
||||
oldTransaction.updateLoanChargePaidMappings(newTransaction.getLoanChargesPaid());
|
||||
}
|
||||
changedTransactionDetail.getCurrentTransactionToOldId().remove(newTransaction);
|
||||
newTransactionMappings.remove(oldTransaction.getId());
|
||||
return oldTransaction;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void createNewTransaction(LoanTransaction oldTransaction, LoanTransaction newTransaction, TransactionCtx ctx) {
|
||||
oldTransaction.updateExternalId(null);
|
||||
oldTransaction.getLoanChargesPaid().clear();
|
||||
// Adding Replayed relation from newly created transaction to reversed transaction
|
||||
oldTransaction.getLoanTransactionRelations()
|
||||
.add(LoanTransactionRelation.linkToTransaction(newTransaction, oldTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
|
||||
|
||||
// if chargeback is getting reverse-replayed, find the original transaction with CHARGEBACK relation and point
|
||||
// the relation to the new chargeback transaction
|
||||
if (oldTransaction.getTypeOf().isChargeback()) {
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(newTransaction, ctx);
|
||||
Set<LoanTransactionRelation> relations = originalTransaction.getLoanTransactionRelations();
|
||||
List<LoanTransactionRelation> oldChargebackRelations = originalTransaction.getLoanTransactionRelations(
|
||||
e -> CHARGEBACK.equals(e.getRelationType()) && e.getToTransaction().equals(oldTransaction));
|
||||
oldChargebackRelations.forEach(relations::remove);
|
||||
relations.add(LoanTransactionRelation.linkToTransaction(originalTransaction, newTransaction, CHARGEBACK));
|
||||
}
|
||||
oldTransaction.reverse();
|
||||
}
|
||||
|
||||
private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||
@@ -738,32 +850,21 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject().isGreaterThanZero()) {
|
||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject()
|
||||
.isGreaterThan(loanTransaction.getAmount(transactionCtx.getCurrency()))) {
|
||||
loanTransaction.setOverPayments(loanTransaction.getAmount(transactionCtx.getCurrency()));
|
||||
} else {
|
||||
loanTransaction.setOverPayments(transactionCtx.getOverpaymentHolder().getMoneyObject());
|
||||
}
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
MoneyHolder overpaymentHolder = transactionCtx.getOverpaymentHolder();
|
||||
Money overpayment = overpaymentHolder.getMoneyObject();
|
||||
if (overpayment.isGreaterThanZero()) {
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||
loanTransaction.setOverPayments(MathUtil.min(transactionAmount, overpayment, false));
|
||||
|
||||
Money transactionAmountUnprocessed = transactionCtx.getOverpaymentHolder().getMoneyObject();
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Money zero = Money.zero(currency);
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings,
|
||||
Set.of(), balances);
|
||||
}
|
||||
transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
|
||||
LoanPaymentAllocationRule defaultAllocationRule = getDefaultAllocationRule(loanTransaction.getLoan());
|
||||
Money transactionAmountUnprocessed = processPeriods(loanTransaction, overpayment, defaultAllocationRule, Set.of(),
|
||||
transactionMappings, balances, transactionCtx);
|
||||
|
||||
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
}
|
||||
}
|
||||
@@ -811,7 +912,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean isLastRecalculation,
|
||||
LoanRepaymentScheduleInstallment currentInstallment, Money overduePrincipal, ProgressiveTransactionCtx ctx) {
|
||||
|
||||
LocalDate fromDate = currentInstallment.getFromDate();
|
||||
boolean hasUpdate = false;
|
||||
|
||||
@@ -1209,27 +1309,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private void processTransaction(LoanTransaction loanTransaction, TransactionCtx transactionCtx, Money transactionAmountUnprocessed) {
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
|
||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
||||
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
|
||||
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
|
||||
.orElse(defaultPaymentAllocationRule);
|
||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||
Balances balances = new Balances(zero, zero, zero, zero);
|
||||
transactionAmountUnprocessed = processPeriods(loanTransaction, transactionAmountUnprocessed, transactionCtx.getCharges(),
|
||||
transactionMappings, balances, transactionCtx);
|
||||
|
||||
if (LoanScheduleProcessingType.HORIZONTAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||
paymentAllocationRule, transactionMappings, transactionCtx.getCharges(), balances);
|
||||
} else if (LoanScheduleProcessingType.VERTICAL
|
||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings,
|
||||
transactionCtx.getCharges(), balances);
|
||||
}
|
||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
@@ -1237,6 +1322,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
|
||||
}
|
||||
|
||||
private Money processPeriods(LoanTransaction transaction, Money processAmount, Set<LoanCharge> charges,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances, TransactionCtx transactionCtx) {
|
||||
LoanPaymentAllocationRule allocationRule = getAllocationRule(transaction);
|
||||
return processPeriods(transaction, processAmount, allocationRule, charges, transactionMappings, balances, transactionCtx);
|
||||
}
|
||||
|
||||
private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule,
|
||||
Set<LoanCharge> charges, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances,
|
||||
TransactionCtx transactionCtx) {
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail()
|
||||
.getLoanScheduleProcessingType();
|
||||
if (scheduleProcessingType.isHorizontal()) {
|
||||
return processPeriodsHorizontally(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges,
|
||||
balances);
|
||||
}
|
||||
if (scheduleProcessingType.isVertical()) {
|
||||
return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule,
|
||||
transactionMappings, charges, balances);
|
||||
}
|
||||
return processAmount;
|
||||
}
|
||||
|
||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
||||
Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||
@@ -1606,4 +1714,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
default -> throw new UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static LoanPaymentAllocationRule getAllocationRule(LoanTransaction loanTransaction) {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
return loan.getPaymentAllocationRules().stream()
|
||||
.filter(e -> loanTransaction.getTypeOf() == e.getTransactionType().getLoanTransactionType()).findFirst()
|
||||
.orElse(getDefaultAllocationRule(loan));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static LoanPaymentAllocationRule getDefaultAllocationRule(Loan loan) {
|
||||
return loan.getPaymentAllocationRules().stream().filter(e -> e.getTransactionType().isDefault()).findFirst().orElseThrow();
|
||||
}
|
||||
}
|
||||
|
||||
+17
-10
@@ -23,6 +23,7 @@ import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -43,6 +44,14 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
||||
this.loanCharge = Optional.empty();
|
||||
}
|
||||
|
||||
public boolean isTransaction() {
|
||||
return loanTransaction.isPresent();
|
||||
}
|
||||
|
||||
public boolean isCharge() {
|
||||
return loanCharge.isPresent();
|
||||
}
|
||||
|
||||
private LocalDate getEffectiveDate() {
|
||||
if (loanCharge.isPresent()) {
|
||||
if (isBackdatedCharge()) {
|
||||
@@ -58,11 +67,11 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
||||
}
|
||||
|
||||
private boolean isAccrualActivity() {
|
||||
return loanTransaction.isPresent() && loanTransaction.get().isAccrualActivity();
|
||||
return isTransaction() && loanTransaction.get().isAccrualActivity();
|
||||
}
|
||||
|
||||
private boolean isBackdatedCharge() {
|
||||
return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate());
|
||||
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
|
||||
}
|
||||
|
||||
private LocalDate getSubmittedOnDate() {
|
||||
@@ -88,17 +97,15 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
||||
@Override
|
||||
@SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals")
|
||||
public int compareTo(@NotNull ChargeOrTransaction o) {
|
||||
int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate());
|
||||
int datePortion = DateUtils.compare(this.getEffectiveDate(), o.getEffectiveDate());
|
||||
if (datePortion == 0) {
|
||||
if (this.isAccrualActivity() && !o.isAccrualActivity()) {
|
||||
return 1;
|
||||
boolean isAccrual = isAccrualActivity();
|
||||
if (isAccrual != o.isAccrualActivity()) {
|
||||
return isAccrual ? 1 : -1;
|
||||
}
|
||||
if (!this.isAccrualActivity() && o.isAccrualActivity()) {
|
||||
return -1;
|
||||
}
|
||||
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
|
||||
int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate());
|
||||
if (submittedDate == 0) {
|
||||
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
|
||||
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime());
|
||||
}
|
||||
return submittedDate;
|
||||
}
|
||||
|
||||
+5
-5
@@ -252,7 +252,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
||||
assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(currency).getAmount()));
|
||||
Mockito.verify(loan, times(1)).getPaymentAllocationRules();
|
||||
Mockito.verify(loan, times(2)).getPaymentAllocationRules();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -531,7 +531,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackTransaction, ctx);
|
||||
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackTransaction, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
@@ -554,7 +554,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
// when + then
|
||||
RuntimeException runtimeException = Assertions.assertThrows(RuntimeException.class,
|
||||
() -> underTest.findOriginalTransaction(chargebackTransaction, ctx));
|
||||
() -> underTest.findChargebackOriginalTransaction(chargebackTransaction, ctx));
|
||||
Assertions.assertEquals("Chargeback transaction must have an original transaction", runtimeException.getMessage());
|
||||
}
|
||||
|
||||
@@ -580,7 +580,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
@@ -611,7 +611,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
|
||||
|
||||
// when
|
||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
||||
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx);
|
||||
|
||||
// then
|
||||
Assertions.assertEquals(originalTransaction, repayment2);
|
||||
|
||||
+26
-19
@@ -209,29 +209,34 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
boolean isAppliedOnBackDate = false;
|
||||
LoanCharge loanCharge = null;
|
||||
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
||||
LocalDate transactionDate = null;
|
||||
if (chargeDefinition.isPercentageOfDisbursementAmount()) {
|
||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
|
||||
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
|
||||
boolean needToGenerateNewExternalId = false;
|
||||
boolean isFirst = true;
|
||||
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
|
||||
if (disbursementDetail.actualDisbursementDate() == null) {
|
||||
// If multiple charges to be applied, only the first one will get the provided externalId, for the
|
||||
// rest we generate new ones (if needed)
|
||||
if (needToGenerateNewExternalId) {
|
||||
if (!isFirst) {
|
||||
externalId = externalIdFactory.create();
|
||||
}
|
||||
LocalDate dueDate = disbursementDetail.expectedDisbursementDateAsLocalDate();
|
||||
loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null,
|
||||
null, disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null, externalId);
|
||||
null, dueDate, null, null, externalId);
|
||||
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail);
|
||||
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge));
|
||||
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
||||
addCharge(loan, chargeDefinition, loanCharge);
|
||||
isAppliedOnBackDate = true;
|
||||
if (DateUtils.isAfter(recalculateFrom, disbursementDetail.expectedDisbursementDateAsLocalDate())) {
|
||||
recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate();
|
||||
if (DateUtils.isAfter(recalculateFrom, dueDate)) {
|
||||
recalculateFrom = dueDate;
|
||||
}
|
||||
needToGenerateNewExternalId = true;
|
||||
if (isFirst) {
|
||||
transactionDate = loanCharge.getEffectiveDueDate();
|
||||
}
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
if (loanCharge == null) {
|
||||
@@ -246,35 +251,38 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
|
||||
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
||||
isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
|
||||
if (loanCharge.getDueLocalDate() == null || DateUtils.isAfter(recalculateFrom, loanCharge.getDueLocalDate())) {
|
||||
if (DateUtils.isAfter(recalculateFrom, loanCharge.getDueLocalDate())) {
|
||||
isAppliedOnBackDate = true;
|
||||
recalculateFrom = loanCharge.getDueLocalDate();
|
||||
}
|
||||
transactionDate = loanCharge.getEffectiveDueDate();
|
||||
}
|
||||
|
||||
boolean reprocessRequired = true;
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
// overpaid transactions will be reprocessed and pay this charge
|
||||
boolean overpaidReprocess = !loanCharge.isDueAtDisbursement() && !loanCharge.isPaid() && loan.getStatus().isOverpaid();
|
||||
if (!overpaidReprocess && loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
|
||||
|
||||
loan = runScheduleRecalculation(loan, recalculateFrom);
|
||||
reprocessRequired = false;
|
||||
}
|
||||
this.loanWritePlatformService.updateOriginalSchedule(loan);
|
||||
}
|
||||
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
|
||||
// date, if yes trigger reprocess else no reprocessing
|
||||
if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(loan.transactionProcessingStrategy())) {
|
||||
if (!overpaidReprocess && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||
.equals(loan.transactionProcessingStrategy())) {
|
||||
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
|
||||
// date, if yes trigger reprocess else no reprocessing
|
||||
LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing();
|
||||
if (lastPaymentTransaction != null) {
|
||||
if (loanCharge.getEffectiveDueDate() != null
|
||||
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
|
||||
reprocessRequired = false;
|
||||
}
|
||||
if (lastPaymentTransaction != null
|
||||
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
|
||||
reprocessRequired = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (reprocessRequired) {
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
|
||||
ChangedTransactionDetail changedTransactionDetail = overpaidReprocess
|
||||
? loan.reprocessTransactionsWithPostTransactionChecks(transactionDate)
|
||||
: loan.reprocessTransactions();
|
||||
if (changedTransactionDetail != null) {
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
|
||||
@@ -284,7 +292,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
|
||||
}
|
||||
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
}
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
|
||||
+1
-1
@@ -1355,7 +1355,7 @@ public class ClientLoanIntegrationTest {
|
||||
* amount
|
||||
*/
|
||||
@Test
|
||||
public void loanWithCahargesOfTypeAmountPercentageAndCashBasedAccountingEnabled() {
|
||||
public void loanWithChargesOfTypeAmountPercentageAndCashBasedAccountingEnabled() {
|
||||
|
||||
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
|
||||
ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);
|
||||
|
||||
Reference in New Issue
Block a user