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();
|
|| isPercentageOfDisbursementAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasInterest() {
|
||||||
|
return isPercentageOfInterest() || isPercentageOfAmountAndInterest();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPercentageOfDisbursementAmount() {
|
public boolean isPercentageOfDisbursementAmount() {
|
||||||
return this.value.equals(ChargeCalculationType.PERCENT_OF_DISBURSEMENT_AMOUNT.getValue());
|
return this.value.equals(ChargeCalculationType.PERCENT_OF_DISBURSEMENT_AMOUNT.getValue());
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -338,6 +338,10 @@ public final class MathUtil {
|
|||||||
|
|
||||||
// ----------------- Money -----------------
|
// ----------------- Money -----------------
|
||||||
|
|
||||||
|
public static BigDecimal toBigDecimal(Money value) {
|
||||||
|
return value == null ? null : value.getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency) {
|
public static Money nullToZero(Money value, @NotNull MonetaryCurrency currency) {
|
||||||
return nullToDefault(value, Money.zero(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 {
|
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
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
|||||||
+110
@@ -770,6 +770,116 @@ public class LoanProductsRequestFactory {
|
|||||||
.incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));//
|
.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() {
|
public PostLoanProductsRequest defaultLoanProductsRequestLP2InterestFlat() {
|
||||||
String name = Utils.randomNameGenerator(NAME_PREFIX_INTEREST_FLAT_LP2, 4);
|
String name = Utils.randomNameGenerator(NAME_PREFIX_INTEREST_FLAT_LP2, 4);
|
||||||
String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX_INTEREST, 3);
|
String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX_INTEREST, 3);
|
||||||
|
|||||||
+17
@@ -534,6 +534,23 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
|||||||
TestContext.INSTANCE.set(
|
TestContext.INSTANCE.set(
|
||||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY,
|
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_REST_FREQUENCY,
|
||||||
responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillRestFrequency);
|
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,
|
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_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_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_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_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_LATE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanPercentLateAmountPlusInterestCreateResponse";
|
||||||
public static final String CHARGE_FOR_LOAN_PERCENT_PROCESSING_CREATE_RESPONSE = "ChargeForLoanPercentProcessingCreateResponse";
|
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
|
And Customer makes "AUTOPAY" repayment on "01 September 2023" with 250 EUR transaction amount
|
||||||
When Admin sets the business date to "04 September 2023"
|
When Admin sets the business date to "04 September 2023"
|
||||||
#Run COB to check there is no accounting meltdown and accrual is handled properly
|
#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.charges.add(loanCharge);
|
||||||
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
|
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
|
||||||
|
|
||||||
// store Id's of existing loan transactions and existing reversed loan
|
// store Id's of existing loan transactions and existing reversed loan transactions
|
||||||
// transactions
|
|
||||||
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||||
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge);
|
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge);
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
@@ -721,7 +720,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||||
|
|
||||||
mapEntry.getValue().updateLoan(this);
|
mapEntry.getValue().updateLoan(this);
|
||||||
}
|
}
|
||||||
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||||
@@ -729,6 +727,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return changedTransactionDetail;
|
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
|
* 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.
|
* 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,
|
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
|
||||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||||
boolean statusChanged = false;
|
boolean statusChanged = false;
|
||||||
boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
|
boolean isOverpaid = MathUtil.isGreaterThanZero(totalOverpaid);
|
||||||
if (isOverpaid) {
|
if (isOverpaid) {
|
||||||
// FIXME - kw - update account balance to negative amount.
|
// FIXME - kw - update account balance to negative amount.
|
||||||
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
||||||
@@ -2510,7 +2514,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
} else {
|
} else {
|
||||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
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;
|
this.overpaidOnDate = null;
|
||||||
}
|
}
|
||||||
return statusChanged;
|
return statusChanged;
|
||||||
@@ -2526,8 +2530,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
this.actualMaturityDate = transactionDate;
|
this.actualMaturityDate = transactionDate;
|
||||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this);
|
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this);
|
||||||
|
|
||||||
} else if (LoanStatus.fromInt(this.loanStatus).isOverpaid()) {
|
} else if (getStatus().isOverpaid()) {
|
||||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||||
this.overpaidOnDate = null;
|
this.overpaidOnDate = null;
|
||||||
}
|
}
|
||||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
||||||
@@ -4498,7 +4502,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
|
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
|
||||||
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
|
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
|
||||||
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
|
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
|
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
|
||||||
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
|
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
|
||||||
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
|
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
|
||||||
@@ -4792,7 +4796,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
|
|
||||||
updateLoanSummaryDerivedFields();
|
updateLoanSummaryDerivedFields();
|
||||||
|
|
||||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||||
this.overpaidOnDate = null;
|
this.overpaidOnDate = null;
|
||||||
this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate();
|
this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate();
|
||||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this);
|
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this);
|
||||||
@@ -5497,12 +5501,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
return getLoanTransactions().stream().filter(predicate).toList();
|
return getLoanTransactions().stream().filter(predicate).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoanTransaction getLoanTransaction(Predicate<LoanTransaction> predicate) {
|
||||||
|
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
public LoanTransaction findChargedOffTransaction() {
|
public LoanTransaction findChargedOffTransaction() {
|
||||||
return getLoanTransactions().stream() //
|
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
|
||||||
.filter(LoanTransaction::isNotReversed) //
|
|
||||||
.filter(LoanTransaction::isChargeOff) //
|
|
||||||
.findFirst() //
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleMaturityDateActivate() {
|
public void handleMaturityDateActivate() {
|
||||||
|
|||||||
+86
-23
@@ -38,9 +38,11 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -303,6 +305,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
return newTransaction;
|
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,
|
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 Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
|
||||||
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
|
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) {
|
public void updateLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> mappings) {
|
||||||
Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings = new ArrayList<>();
|
Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings = new ArrayList<>();
|
||||||
for (LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping : mappings) {
|
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : mappings) {
|
||||||
updateMapingDetail(retainMappings, updatedrepaymentScheduleMapping);
|
updateMappingDetail(retainMappings, updatedMapping, true);
|
||||||
}
|
}
|
||||||
this.loanTransactionToRepaymentScheduleMappings.retainAll(retainMappings);
|
this.loanTransactionToRepaymentScheduleMappings.retainAll(retainMappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean updateMapingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
public void addLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> updatedMappings) {
|
||||||
final LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping) {
|
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : updatedMappings) {
|
||||||
|
updateMappingDetail(null, updatedMapping, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateMappingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
||||||
|
final LoanTransactionToRepaymentScheduleMapping updatedMapping, boolean overwrite) {
|
||||||
boolean isMappingUpdated = false;
|
boolean isMappingUpdated = false;
|
||||||
for (LoanTransactionToRepaymentScheduleMapping repaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
LoanRepaymentScheduleInstallment updatedInstallment = updatedMapping.getLoanRepaymentScheduleInstallment();
|
||||||
if (updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId() != null
|
for (LoanTransactionToRepaymentScheduleMapping existingMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
||||||
&& repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate()
|
LoanRepaymentScheduleInstallment existingInstallment = existingMapping.getLoanRepaymentScheduleInstallment();
|
||||||
.equals(updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate())
|
if (DateUtils.isEqual(existingInstallment.getDueDate(), updatedInstallment.getDueDate()) && updatedInstallment.getId() != null
|
||||||
&& updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId()
|
&& updatedInstallment.getId().equals(existingInstallment.getId())) {
|
||||||
.equals(repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId())) {
|
if (overwrite) {
|
||||||
repaymentScheduleMapping.setComponents(updatedrepaymentScheduleMapping.getPrincipalPortion(),
|
existingMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||||
updatedrepaymentScheduleMapping.getInterestPortion(), updatedrepaymentScheduleMapping.getFeeChargesPortion(),
|
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||||
updatedrepaymentScheduleMapping.getPenaltyChargesPortion());
|
} else {
|
||||||
|
existingMapping.updateComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||||
|
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||||
|
}
|
||||||
isMappingUpdated = true;
|
isMappingUpdated = true;
|
||||||
retainMappings.add(repaymentScheduleMapping);
|
if (retainMappings != null) {
|
||||||
|
retainMappings.add(existingMapping);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isMappingUpdated) {
|
if (!isMappingUpdated) {
|
||||||
updatedrepaymentScheduleMapping.setLoanTransaction(this);
|
LoanTransactionToRepaymentScheduleMapping newMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(this,
|
||||||
this.loanTransactionToRepaymentScheduleMappings.add(updatedrepaymentScheduleMapping);
|
updatedInstallment, null, null, null, null);
|
||||||
retainMappings.add(updatedrepaymentScheduleMapping);
|
newMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||||
|
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||||
|
this.loanTransactionToRepaymentScheduleMappings.add(newMapping);
|
||||||
|
if (retainMappings != null) {
|
||||||
|
retainMappings.add(newMapping);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return isMappingUpdated;
|
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() {
|
public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
|
||||||
return this.loanTransactionToRepaymentScheduleMappings;
|
return this.loanTransactionToRepaymentScheduleMappings;
|
||||||
}
|
}
|
||||||
@@ -989,18 +1045,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasLoanTransactionRelations() {
|
public boolean hasLoanTransactionRelations() {
|
||||||
return (loanTransactionRelations != null && loanTransactionRelations.size() > 0);
|
return !loanTransactionRelations.isEmpty();
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasChargebackLoanTransactionRelations() {
|
|
||||||
return (loanTransactionRelations != null
|
|
||||||
&& loanTransactionRelations.stream().anyMatch(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
|
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
|
||||||
return loanTransactionRelations;
|
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) {
|
public void copyLoanTransactionRelations(Set<LoanTransactionRelation> sourceLoanTransactionRelations) {
|
||||||
for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) {
|
for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) {
|
||||||
loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(),
|
loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(),
|
||||||
@@ -1055,6 +1114,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
return chargeRefundChargeType;
|
return chargeRefundChargeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isOverPaid() {
|
||||||
|
return MathUtil.isGreaterThanZero(overPaymentPortion);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
||||||
// this is never stored in a Collection.
|
// this is never stored in a Collection.
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -24,8 +24,10 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.JoinColumn;
|
import jakarta.persistence.JoinColumn;
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
|
|
||||||
@@ -77,15 +79,11 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
|||||||
final Money feeChargesPortion, final Money penaltyChargesPortion) {
|
final Money feeChargesPortion, final Money penaltyChargesPortion) {
|
||||||
return new LoanTransactionToRepaymentScheduleMapping(loanTransaction, installment, defaultToNullIfZero(principalPortion),
|
return new LoanTransactionToRepaymentScheduleMapping(loanTransaction, installment, defaultToNullIfZero(principalPortion),
|
||||||
defaultToNullIfZero(interestPortion), defaultToNullIfZero(feeChargesPortion), defaultToNullIfZero(penaltyChargesPortion),
|
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) {
|
private static BigDecimal defaultToNullIfZero(final Money value) {
|
||||||
BigDecimal result = value.getAmount();
|
return (value == null || value.isZero()) ? null : value.getAmount();
|
||||||
if (value.isZero()) {
|
|
||||||
result = null;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
|
private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
|
||||||
@@ -100,10 +98,15 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
|||||||
return this.installment;
|
return this.installment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
|
public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges,
|
||||||
final MonetaryCurrency currency = principal.getCurrency();
|
@NotNull Money penaltyCharges) {
|
||||||
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal));
|
updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
|
||||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest));
|
}
|
||||||
|
|
||||||
|
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);
|
updateChargesComponents(feeCharges, penaltyCharges);
|
||||||
updateAmount();
|
updateAmount();
|
||||||
}
|
}
|
||||||
@@ -122,10 +125,9 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
|||||||
updateAmount();
|
updateAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
|
private void updateChargesComponents(final BigDecimal feeCharges, final BigDecimal penaltyCharges) {
|
||||||
final MonetaryCurrency currency = feeCharges.getCurrency();
|
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), feeCharges));
|
||||||
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges));
|
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), penaltyCharges));
|
||||||
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Money getPrincipalPortion(final MonetaryCurrency currency) {
|
public Money getPrincipalPortion(final MonetaryCurrency currency) {
|
||||||
|
|||||||
+11
-1
@@ -28,7 +28,9 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public enum LoanScheduleProcessingType {
|
public enum LoanScheduleProcessingType {
|
||||||
|
|
||||||
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
|
HORIZONTAL("Horizontal"), //
|
||||||
|
VERTICAL("Vertical"), //
|
||||||
|
;
|
||||||
|
|
||||||
private final String humanReadableName;
|
private final String humanReadableName;
|
||||||
|
|
||||||
@@ -39,4 +41,12 @@ public enum LoanScheduleProcessingType {
|
|||||||
public EnumOptionData asEnumOptionData() {
|
public EnumOptionData asEnumOptionData() {
|
||||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
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() {
|
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
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.INTEREST;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
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.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.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
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.LoanPreClosureInterestCalculationStrategy;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -180,20 +177,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
||||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
|
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
|
||||||
installmentAmountInMultiplesOf, installments, mc);
|
installmentAmountInMultiplesOf, installments, mc);
|
||||||
|
|
||||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
|
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
|
||||||
changedTransactionDetail, scheduleModel);
|
changedTransactionDetail, scheduleModel);
|
||||||
|
|
||||||
|
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
|
||||||
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
|
||||||
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, ctx));
|
if (chargeOrTransaction.isTransaction()) {
|
||||||
chargeOrTransaction.getLoanCharge()
|
LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get();
|
||||||
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
|
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);
|
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx, true);
|
||||||
|
List<LoanTransaction> txs = chargeOrTransactions.stream() //
|
||||||
|
.filter(ChargeOrTransaction::isTransaction) //
|
||||||
|
.map(e -> e.getLoanTransaction().get()).toList();
|
||||||
reprocessInstallments(disbursementDate, txs, installments, currency);
|
reprocessInstallments(disbursementDate, txs, installments, currency);
|
||||||
return Pair.of(changedTransactionDetail, scheduleModel);
|
return Pair.of(changedTransactionDetail, scheduleModel);
|
||||||
}
|
}
|
||||||
@@ -204,6 +218,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
|
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
|
@Override
|
||||||
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
switch (loanTransaction.getTypeOf()) {
|
switch (loanTransaction.getTypeOf()) {
|
||||||
@@ -283,36 +303,33 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) {
|
||||||
if (loanTransaction.getId() != null) { // this the normal case without reverse-replay
|
ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
||||||
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
Long chargebackId = chargebackTransaction.getId(); // this the normal case without reverse-replay
|
||||||
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
if (changedTransactionDetail != null) {
|
||||||
.anyMatch(this.hasMatchingToLoanTransaction(loanTransaction.getId(), CHARGEBACK)))
|
if (chargebackId == null) {
|
||||||
.findFirst();
|
// the chargeback transaction was changed, so we need to look it up from the ctx.
|
||||||
if (originalTransaction.isEmpty()) {
|
chargebackId = changedTransactionDetail.getCurrentTransactionToOldId().get(chargebackTransaction);
|
||||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
|
||||||
}
|
}
|
||||||
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()) {
|
Long toId = chargebackId;
|
||||||
return updatedTransaction.get();
|
Collection<LoanTransaction> updatedTransactions = changedTransactionDetail.getNewTransactionMappings().values();
|
||||||
} else { // if it is not there, then it simply means that this has not changed during reverse replay
|
Optional<LoanTransaction> fromTransaction = updatedTransactions.stream()
|
||||||
Optional<LoanTransaction> originalTransaction = loanTransaction.getLoan().getLoanTransactions().stream()
|
.filter(tr -> tr.getLoanTransactionRelations().stream().anyMatch(hasMatchingToLoanTransaction(toId, CHARGEBACK)))
|
||||||
.filter(tr -> tr.getLoanTransactionRelations().stream()
|
.findFirst();
|
||||||
.anyMatch(this.hasMatchingToLoanTransaction(originalChargebackTransactionId, CHARGEBACK)))
|
if (fromTransaction.isPresent()) {
|
||||||
.findFirst();
|
return fromTransaction.get();
|
||||||
if (originalTransaction.isEmpty()) {
|
|
||||||
throw new RuntimeException("Chargeback transaction must have an original transaction");
|
|
||||||
}
|
|
||||||
return originalTransaction.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) {
|
protected void processCreditTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
@@ -331,7 +348,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
|
|
||||||
if (transactionAmount.isGreaterThanZero()) {
|
if (transactionAmount.isGreaterThanZero()) {
|
||||||
if (loanTransaction.isChargeback()) {
|
if (loanTransaction.isChargeback()) {
|
||||||
LoanTransaction originalTransaction = findOriginalTransaction(loanTransaction, ctx);
|
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||||
// get the original allocation from the opriginal transaction
|
// get the original allocation from the opriginal transaction
|
||||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
|
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
|
||||||
ctx.getCurrency());
|
ctx.getCurrency());
|
||||||
@@ -420,8 +437,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanTransaction> chargebacks = allTransactions.stream().filter(LoanTransaction::isChargeback).toList();
|
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
|
// 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()
|
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);
|
Map<AllocationType, Money> allocation = new HashMap<>(originalAllocation);
|
||||||
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
|
for (LoanTransaction loanTransaction : chargebacksForTheSameOriginal) {
|
||||||
@@ -518,16 +538,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||||
|
|
||||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
LoanPaymentAllocationRule paymentAllocationRule = getAllocationRule(loanTransaction);
|
||||||
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);
|
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
Balances balances = new Balances(zero, zero, zero, zero);
|
||||||
List<PaymentAllocationType> paymentAllocationTypes;
|
List<PaymentAllocationType> paymentAllocationTypes;
|
||||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule;
|
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
|
// if the allocation rule is not defined then the reverse order of the default allocation rule will be used
|
||||||
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
|
paymentAllocationTypes = new ArrayList<>(paymentAllocationRule.getAllocationTypes());
|
||||||
Collections.reverse(paymentAllocationTypes);
|
Collections.reverse(paymentAllocationTypes);
|
||||||
@@ -536,8 +551,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
|
paymentAllocationTypes = paymentAllocationRule.getAllocationTypes();
|
||||||
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||||
}
|
}
|
||||||
if (LoanScheduleProcessingType.HORIZONTAL
|
Loan loan = loanTransaction.getLoan();
|
||||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
LoanScheduleProcessingType scheduleProcessingType = loan.getLoanProductRelatedDetail().getLoanScheduleProcessingType();
|
||||||
|
if (scheduleProcessingType.isHorizontal()) {
|
||||||
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationTypes.stream().collect(
|
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationTypes.stream().collect(
|
||||||
Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList())));
|
Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList())));
|
||||||
|
|
||||||
@@ -549,8 +565,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (LoanScheduleProcessingType.VERTICAL
|
} else if (scheduleProcessingType.isVertical()) {
|
||||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
|
||||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||||
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
|
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
|
||||||
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
|
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
|
||||||
@@ -567,55 +582,152 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processSingleTransaction(LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
|
private void processSingleTransaction(LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
|
||||||
final MonetaryCurrency currency = ctx.getCurrency();
|
boolean isNew = loanTransaction.getId() == null;
|
||||||
final ChangedTransactionDetail changedTransactionDetail = ctx.getChangedTransactionDetail();
|
LoanTransaction processTransaction = loanTransaction;
|
||||||
|
if (!isNew) {
|
||||||
if (loanTransaction.getId() == null) {
|
// For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
|
||||||
processLatestTransaction(loanTransaction, ctx);
|
// changed.
|
||||||
if (loanTransaction.isInterestWaiver()) {
|
processTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
|
||||||
loanTransaction.adjustInterestComponent(currency);
|
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 {
|
} else {
|
||||||
/*
|
updateOrRegisterNewTransaction(loanTransaction, processTransaction, ctx);
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkAndUpdateReplayedChargebackRelationWithReplayedTransaction(LoanTransaction loanTransaction,
|
private List<LoanTransaction> processOverpaidTransactions(List<LoanTransaction> overpaidTransactions, MonetaryCurrency currency,
|
||||||
LoanTransaction newLoanTransaction, TransactionCtx ctx) {
|
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
|
||||||
// if chargeback is getting reverse-replayed
|
MoneyHolder overpaymentHolder, ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||||
// find replayed transaction with CHARGEBACK relation with reversed chargeback transaction
|
List<LoanTransaction> remainingTransactions = new ArrayList<>(overpaidTransactions);
|
||||||
// for replayed transaction, add relation to point to new Chargeback transaction
|
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail,
|
||||||
if (loanTransaction.getTypeOf().isChargeback()) {
|
scheduleModel);
|
||||||
LoanTransaction originalTransaction = findOriginalTransaction(newLoanTransaction, ctx);
|
Money zero = Money.zero(currency);
|
||||||
originalTransaction.getLoanTransactionRelations()
|
for (LoanTransaction transaction : overpaidTransactions) {
|
||||||
.add(LoanTransactionRelation.linkToTransaction(originalTransaction, newLoanTransaction, CHARGEBACK));
|
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,
|
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) {
|
private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject().isGreaterThanZero()) {
|
MoneyHolder overpaymentHolder = transactionCtx.getOverpaymentHolder();
|
||||||
if (transactionCtx.getOverpaymentHolder().getMoneyObject()
|
Money overpayment = overpaymentHolder.getMoneyObject();
|
||||||
.isGreaterThan(loanTransaction.getAmount(transactionCtx.getCurrency()))) {
|
if (overpayment.isGreaterThanZero()) {
|
||||||
loanTransaction.setOverPayments(loanTransaction.getAmount(transactionCtx.getCurrency()));
|
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||||
} else {
|
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||||
loanTransaction.setOverPayments(transactionCtx.getOverpaymentHolder().getMoneyObject());
|
loanTransaction.setOverPayments(MathUtil.min(transactionAmount, overpayment, false));
|
||||||
}
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
|
||||||
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
|
|
||||||
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
|
|
||||||
.filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
|
|
||||||
|
|
||||||
Money transactionAmountUnprocessed = transactionCtx.getOverpaymentHolder().getMoneyObject();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
Money zero = Money.zero(currency);
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
Balances balances = new Balances(zero, zero, zero, zero);
|
||||||
if (LoanScheduleProcessingType.HORIZONTAL
|
LoanPaymentAllocationRule defaultAllocationRule = getDefaultAllocationRule(loanTransaction.getLoan());
|
||||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
Money transactionAmountUnprocessed = processPeriods(loanTransaction, overpayment, defaultAllocationRule, Set.of(),
|
||||||
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
transactionMappings, balances, transactionCtx);
|
||||||
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
|
|
||||||
} else if (LoanScheduleProcessingType.VERTICAL
|
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
||||||
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
|
|
||||||
transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(),
|
|
||||||
transactionCtx.getInstallments(), transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings,
|
|
||||||
Set.of(), balances);
|
|
||||||
}
|
|
||||||
transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed);
|
|
||||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,7 +912,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
|
|
||||||
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean isLastRecalculation,
|
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean isLastRecalculation,
|
||||||
LoanRepaymentScheduleInstallment currentInstallment, Money overduePrincipal, ProgressiveTransactionCtx ctx) {
|
LoanRepaymentScheduleInstallment currentInstallment, Money overduePrincipal, ProgressiveTransactionCtx ctx) {
|
||||||
|
|
||||||
LocalDate fromDate = currentInstallment.getFromDate();
|
LocalDate fromDate = currentInstallment.getFromDate();
|
||||||
boolean hasUpdate = false;
|
boolean hasUpdate = false;
|
||||||
|
|
||||||
@@ -1209,27 +1309,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processTransaction(LoanTransaction loanTransaction, TransactionCtx transactionCtx, Money transactionAmountUnprocessed) {
|
private void processTransaction(LoanTransaction loanTransaction, TransactionCtx transactionCtx, Money transactionAmountUnprocessed) {
|
||||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
|
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||||
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);
|
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
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(),
|
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||||
@@ -1237,6 +1322,29 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
|
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,
|
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
||||||
Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule,
|
Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule,
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||||
@@ -1606,4 +1714,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
default -> throw new UnsupportedOperationException(reAgeParameter.getFrequencyType().getCode());
|
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.time.OffsetDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import lombok.Getter;
|
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.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -43,6 +44,14 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
|||||||
this.loanCharge = Optional.empty();
|
this.loanCharge = Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTransaction() {
|
||||||
|
return loanTransaction.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCharge() {
|
||||||
|
return loanCharge.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
private LocalDate getEffectiveDate() {
|
private LocalDate getEffectiveDate() {
|
||||||
if (loanCharge.isPresent()) {
|
if (loanCharge.isPresent()) {
|
||||||
if (isBackdatedCharge()) {
|
if (isBackdatedCharge()) {
|
||||||
@@ -58,11 +67,11 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAccrualActivity() {
|
private boolean isAccrualActivity() {
|
||||||
return loanTransaction.isPresent() && loanTransaction.get().isAccrualActivity();
|
return isTransaction() && loanTransaction.get().isAccrualActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBackdatedCharge() {
|
private boolean isBackdatedCharge() {
|
||||||
return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate());
|
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate getSubmittedOnDate() {
|
private LocalDate getSubmittedOnDate() {
|
||||||
@@ -88,17 +97,15 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
|
|||||||
@Override
|
@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")
|
@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) {
|
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 (datePortion == 0) {
|
||||||
if (this.isAccrualActivity() && !o.isAccrualActivity()) {
|
boolean isAccrual = isAccrualActivity();
|
||||||
return 1;
|
if (isAccrual != o.isAccrualActivity()) {
|
||||||
|
return isAccrual ? 1 : -1;
|
||||||
}
|
}
|
||||||
if (!this.isAccrualActivity() && o.isAccrualActivity()) {
|
int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate());
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
|
|
||||||
if (submittedDate == 0) {
|
if (submittedDate == 0) {
|
||||||
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
|
return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime());
|
||||||
}
|
}
|
||||||
return submittedDate;
|
return submittedDate;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -252,7 +252,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
assertEquals(0, chargeAmount.compareTo(installment.getFeeChargesCharged(currency).getAmount()));
|
||||||
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
assertEquals(0, BigDecimal.ZERO.compareTo(installment.getFeeChargesOutstanding(currency).getAmount()));
|
||||||
assertEquals(0, BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(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
|
@Test
|
||||||
@@ -531,7 +531,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
TransactionCtx ctx = mock(TransactionCtx.class);
|
TransactionCtx ctx = mock(TransactionCtx.class);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackTransaction, ctx);
|
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackTransaction, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Assertions.assertEquals(originalTransaction, repayment2);
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
@@ -554,7 +554,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
|
|
||||||
// when + then
|
// when + then
|
||||||
RuntimeException runtimeException = Assertions.assertThrows(RuntimeException.class,
|
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());
|
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(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of(122L, repayment1, 121L, repayment2));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Assertions.assertEquals(originalTransaction, repayment2);
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
@@ -611,7 +611,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
|
when(changedTransactionDetail.getNewTransactionMappings()).thenReturn(Map.of());
|
||||||
|
|
||||||
// when
|
// when
|
||||||
LoanTransaction originalTransaction = underTest.findOriginalTransaction(chargebackReplayed, ctx);
|
LoanTransaction originalTransaction = underTest.findChargebackOriginalTransaction(chargebackReplayed, ctx);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
Assertions.assertEquals(originalTransaction, repayment2);
|
Assertions.assertEquals(originalTransaction, repayment2);
|
||||||
|
|||||||
+26
-19
@@ -209,29 +209,34 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
boolean isAppliedOnBackDate = false;
|
boolean isAppliedOnBackDate = false;
|
||||||
LoanCharge loanCharge = null;
|
LoanCharge loanCharge = null;
|
||||||
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
||||||
|
LocalDate transactionDate = null;
|
||||||
if (chargeDefinition.isPercentageOfDisbursementAmount()) {
|
if (chargeDefinition.isPercentageOfDisbursementAmount()) {
|
||||||
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
|
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
|
||||||
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
|
ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId");
|
||||||
boolean needToGenerateNewExternalId = false;
|
boolean isFirst = true;
|
||||||
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
|
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
|
||||||
if (disbursementDetail.actualDisbursementDate() == null) {
|
if (disbursementDetail.actualDisbursementDate() == null) {
|
||||||
// If multiple charges to be applied, only the first one will get the provided externalId, for the
|
// If multiple charges to be applied, only the first one will get the provided externalId, for the
|
||||||
// rest we generate new ones (if needed)
|
// rest we generate new ones (if needed)
|
||||||
if (needToGenerateNewExternalId) {
|
if (!isFirst) {
|
||||||
externalId = externalIdFactory.create();
|
externalId = externalIdFactory.create();
|
||||||
}
|
}
|
||||||
|
LocalDate dueDate = disbursementDetail.expectedDisbursementDateAsLocalDate();
|
||||||
loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null,
|
loanCharge = loanChargeAssembler.createNewWithoutLoan(chargeDefinition, disbursementDetail.principal(), null, null,
|
||||||
null, disbursementDetail.expectedDisbursementDateAsLocalDate(), null, null, externalId);
|
null, dueDate, null, null, externalId);
|
||||||
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail);
|
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, disbursementDetail);
|
||||||
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
|
||||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge));
|
businessEventNotifierService.notifyPreBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge));
|
||||||
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
||||||
addCharge(loan, chargeDefinition, loanCharge);
|
addCharge(loan, chargeDefinition, loanCharge);
|
||||||
isAppliedOnBackDate = true;
|
isAppliedOnBackDate = true;
|
||||||
if (DateUtils.isAfter(recalculateFrom, disbursementDetail.expectedDisbursementDateAsLocalDate())) {
|
if (DateUtils.isAfter(recalculateFrom, dueDate)) {
|
||||||
recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate();
|
recalculateFrom = dueDate;
|
||||||
}
|
}
|
||||||
needToGenerateNewExternalId = true;
|
if (isFirst) {
|
||||||
|
transactionDate = loanCharge.getEffectiveDueDate();
|
||||||
|
}
|
||||||
|
isFirst = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (loanCharge == null) {
|
if (loanCharge == null) {
|
||||||
@@ -246,35 +251,38 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
|
|
||||||
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
|
||||||
isAppliedOnBackDate = addCharge(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;
|
isAppliedOnBackDate = true;
|
||||||
recalculateFrom = loanCharge.getDueLocalDate();
|
recalculateFrom = loanCharge.getDueLocalDate();
|
||||||
}
|
}
|
||||||
|
transactionDate = loanCharge.getEffectiveDueDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean reprocessRequired = true;
|
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()) {
|
if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
|
||||||
|
|
||||||
loan = runScheduleRecalculation(loan, recalculateFrom);
|
loan = runScheduleRecalculation(loan, recalculateFrom);
|
||||||
reprocessRequired = false;
|
reprocessRequired = false;
|
||||||
}
|
}
|
||||||
this.loanWritePlatformService.updateOriginalSchedule(loan);
|
this.loanWritePlatformService.updateOriginalSchedule(loan);
|
||||||
}
|
}
|
||||||
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
|
if (!overpaidReprocess && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY
|
||||||
// date, if yes trigger reprocess else no reprocessing
|
.equals(loan.transactionProcessingStrategy())) {
|
||||||
if (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();
|
LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing();
|
||||||
if (lastPaymentTransaction != null) {
|
if (lastPaymentTransaction != null
|
||||||
if (loanCharge.getEffectiveDueDate() != null
|
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
|
||||||
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
|
reprocessRequired = false;
|
||||||
reprocessRequired = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reprocessRequired) {
|
if (reprocessRequired) {
|
||||||
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
|
ChangedTransactionDetail changedTransactionDetail = overpaidReprocess
|
||||||
|
? loan.reprocessTransactionsWithPostTransactionChecks(transactionDate)
|
||||||
|
: loan.reprocessTransactions();
|
||||||
if (changedTransactionDetail != null) {
|
if (changedTransactionDetail != null) {
|
||||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||||
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
|
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
|
||||||
@@ -284,7 +292,6 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
|
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
|
||||||
}
|
}
|
||||||
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|||||||
+1
-1
@@ -1355,7 +1355,7 @@ public class ClientLoanIntegrationTest {
|
|||||||
* amount
|
* amount
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void loanWithCahargesOfTypeAmountPercentageAndCashBasedAccountingEnabled() {
|
public void loanWithChargesOfTypeAmountPercentageAndCashBasedAccountingEnabled() {
|
||||||
|
|
||||||
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
|
final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
|
||||||
ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);
|
ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);
|
||||||
|
|||||||
Reference in New Issue
Block a user