FINERACT-2232: Capitalized income - Add Capitalized income to Loan
This commit is contained in:
committed by
Adam Saghy
parent
bb0bdb9670
commit
e10542838e
+1
-1
@@ -29,7 +29,7 @@ public class ProductToGLAccountMappingInvalidException extends AbstractPlatformD
|
|||||||
final String actualAccountCategory, final String expectedAccountCategory) {
|
final String actualAccountCategory, final String expectedAccountCategory) {
|
||||||
super("error.msg." + paramName + ".invalid.account.type",
|
super("error.msg." + paramName + ".invalid.account.type",
|
||||||
"Passed in GLAccount " + paramName + " with Id " + accountId + "maps to the account " + accountName + " of type "
|
"Passed in GLAccount " + paramName + " with Id " + accountId + "maps to the account " + accountName + " of type "
|
||||||
+ actualAccountCategory + ", the expected account type was one among" + expectedAccountCategory,
|
+ actualAccountCategory + ", the expected account type was one among " + expectedAccountCategory,
|
||||||
paramName, accountId, accountName, actualAccountCategory, expectedAccountCategory);
|
paramName, accountId, accountName, actualAccountCategory, expectedAccountCategory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -3822,4 +3822,13 @@ public class CommandWrapperBuilder {
|
|||||||
this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses/" + variationId;
|
this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses/" + variationId;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder addCapitalizedIncome(final Long loanId) {
|
||||||
|
this.actionName = "CAPITALIZEDINCOME";
|
||||||
|
this.entityName = "LOAN";
|
||||||
|
this.entityId = loanId;
|
||||||
|
this.loanId = loanId;
|
||||||
|
this.href = "/loans/" + loanId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -120,6 +120,7 @@ public enum DefaultLoanProduct implements LoanProduct {
|
|||||||
LP2_ADV_PYMNT_INT_DAILY_EMI_ACTUAL_ACTUAL_NO_INTEREST_RECALC_INT_REFUND_FULL_ZERO_INT_CHARGE_OFF, //
|
LP2_ADV_PYMNT_INT_DAILY_EMI_ACTUAL_ACTUAL_NO_INTEREST_RECALC_INT_REFUND_FULL_ZERO_INT_CHARGE_OFF, //
|
||||||
LP2_ADV_PYMNT_INT_DAILY_EMI_ACTUAL_ACTUAL_NO_INTEREST_RECALC_INT_REFUND_FULL_ACC_MATUR_CHARGE_OFF, //
|
LP2_ADV_PYMNT_INT_DAILY_EMI_ACTUAL_ACTUAL_NO_INTEREST_RECALC_INT_REFUND_FULL_ACC_MATUR_CHARGE_OFF, //
|
||||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATE_LAST_INSTALLMENT, //
|
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATE_LAST_INSTALLMENT, //
|
||||||
|
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME, //
|
||||||
;
|
;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+117
@@ -1500,4 +1500,121 @@ public class LoanProductsRequestFactory {
|
|||||||
.chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))//
|
.chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))//
|
||||||
.incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));//
|
.incomeFromChargeOffPenaltyAccountId(accountTypeResolver.resolve(DefaultAccountType.FEE_CHARGE_OFF));//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PostLoanProductsRequest defaultLoanProductsRequestLP2CapitalizedIncome() {
|
||||||
|
final String name = Utils.randomNameGenerator(NAME_PREFIX_LP2, 4);
|
||||||
|
final String shortName = Utils.randomNameGenerator(SHORT_NAME_PREFIX, 3);
|
||||||
|
final List<Integer> principalVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
final List<Integer> numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
final List<Integer> interestRateVariationsForBorrowerCycle = new ArrayList<>();
|
||||||
|
final List<LoanProductChargeData> charges = new ArrayList<>();
|
||||||
|
final List<LoanProductChargeToGLAccountMapper> penaltyToIncomeAccountMappings = new ArrayList<>();
|
||||||
|
final List<LoanProductChargeToGLAccountMapper> feeToIncomeAccountMappings = new ArrayList<>();
|
||||||
|
final List<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings = new ArrayList<>();
|
||||||
|
final 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)//
|
||||||
|
.enableDownPayment(true)//
|
||||||
|
.enableAutoRepaymentForDownPayment(true)//
|
||||||
|
.disbursedAmountPercentageForDownPayment(new BigDecimal(25))//
|
||||||
|
.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(3)//
|
||||||
|
.maxNumberOfRepayments(30)//
|
||||||
|
.isLinkedToFloatingInterestRates(false)//
|
||||||
|
.minInterestRatePerPeriod((double) 0)//
|
||||||
|
.interestRatePerPeriod((double) 0)//
|
||||||
|
.maxInterestRatePerPeriod((double) 0)//
|
||||||
|
.interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_MONTH)//
|
||||||
|
.repaymentEvery(15)//
|
||||||
|
.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))//
|
||||||
|
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
|
||||||
|
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
|
||||||
|
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.INTEREST)
|
||||||
|
.deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.OVERPAYMENT_ACCOUNT))
|
||||||
|
.incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME))
|
||||||
|
.enableIncomeCapitalization(true);//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -285,4 +285,9 @@ public class LoanRequestFactory {
|
|||||||
return new InterestPauseRequestDto().dateFormat(DATE_FORMAT).locale(DEFAULT_LOCALE).startDate(DEFAULT_TRANSACTION_DATE)
|
return new InterestPauseRequestDto().dateFormat(DATE_FORMAT).locale(DEFAULT_LOCALE).startDate(DEFAULT_TRANSACTION_DATE)
|
||||||
.endDate(DEFAULT_TRANSACTION_DATE);
|
.endDate(DEFAULT_TRANSACTION_DATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static PostLoansLoanIdTransactionsRequest defaultCapitalizedIncomeRequest() {
|
||||||
|
return new PostLoansLoanIdTransactionsRequest().transactionDate(DEFAULT_TRANSACTION_DATE).dateFormat(DATE_FORMAT)
|
||||||
|
.locale(DEFAULT_LOCALE).note("Capitalized Income");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -2405,6 +2405,22 @@ 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_IR_DAILY_TILL_REST_FREQUENCY_DATELAST_INSTALLMENT,
|
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATELAST_INSTALLMENT,
|
||||||
responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcDailyTillRestFrequencyDateLastInstallment);
|
responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcDailyTillRestFrequencyDateLastInstallment);
|
||||||
|
|
||||||
|
final String name97 = DefaultLoanProduct.LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME.getName();
|
||||||
|
final PostLoanProductsRequest loanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome = loanProductsRequestFactory
|
||||||
|
.defaultLoanProductsRequestLP2CapitalizedIncome()//
|
||||||
|
.name(name97)//
|
||||||
|
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
|
||||||
|
.loanScheduleType("PROGRESSIVE") //
|
||||||
|
.paymentAllocation(List.of(//
|
||||||
|
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
|
||||||
|
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
|
||||||
|
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
|
||||||
|
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
|
||||||
|
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome = loanProductsApi
|
||||||
|
.createLoanProduct(loanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome).execute();
|
||||||
|
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME,
|
||||||
|
responseLoanProductsRequestLP2ProgressiveAdvPaymentCapitalizedIncome);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||||
|
|||||||
+19
@@ -3863,4 +3863,23 @@ public class LoanStepDef extends AbstractStepDef {
|
|||||||
assertNotNull(targetProduct.getInterestRecognitionOnDisbursementDate());
|
assertNotNull(targetProduct.getInterestRecognitionOnDisbursementDate());
|
||||||
assertThat(targetProduct.getInterestRecognitionOnDisbursementDate().toString()).isEqualTo(expectedValue);
|
assertThat(targetProduct.getInterestRecognitionOnDisbursementDate().toString()).isEqualTo(expectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@And("Admin adds capitalized income with {string} payment type to the loan on {string} with {string} EUR transaction amount")
|
||||||
|
public void adminAddsCapitalizedIncomeToTheLoanOnWithEURTransactionAmount(final String transactionPaymentType,
|
||||||
|
final String transactionDate, final String amount) throws IOException {
|
||||||
|
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||||
|
final long loanId = loanResponse.body().getLoanId();
|
||||||
|
|
||||||
|
final DefaultPaymentType paymentType = DefaultPaymentType.valueOf(transactionPaymentType);
|
||||||
|
final Long paymentTypeValue = paymentTypeResolver.resolve(paymentType);
|
||||||
|
|
||||||
|
final PostLoansLoanIdTransactionsRequest capitalizedIncomeRequest = LoanRequestFactory.defaultCapitalizedIncomeRequest()
|
||||||
|
.transactionDate(transactionDate).transactionAmount(Double.valueOf(amount)).paymentTypeId(paymentTypeValue)
|
||||||
|
.externalId("EXT-CAP-INC-" + UUID.randomUUID());
|
||||||
|
|
||||||
|
final Response<PostLoansLoanIdTransactionsResponse> capitalizedIncomeResponse = loanTransactionsApi
|
||||||
|
.executeLoanTransaction(loanId, capitalizedIncomeRequest, "capitalizedIncome").execute();
|
||||||
|
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_RESPONSE, capitalizedIncomeResponse);
|
||||||
|
ErrorHelper.checkSuccessfulApiCall(capitalizedIncomeResponse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -145,6 +145,7 @@ public abstract class TestContextKey {
|
|||||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_EXPECT_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseExpectTranche";
|
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_EXPECT_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseExpectTranche";
|
||||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedPaymentInterestRecalculationDailyEmi36030Multidisburse";
|
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedPaymentInterestRecalculationDailyEmi36030Multidisburse";
|
||||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CASH_ACCOUNTING_DISBURSEMENT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyCashBasedDisbursementCharge";
|
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CASH_ACCOUNTING_DISBURSEMENT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyCashBasedDisbursementCharge";
|
||||||
|
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentCapitalizedIncome";
|
||||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
|
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
|
||||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
|
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
|
||||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
|
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
|
||||||
@@ -203,4 +204,5 @@ public abstract class TestContextKey {
|
|||||||
public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse";
|
public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse";
|
||||||
public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse";
|
public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse";
|
||||||
public static final String LOAN_TRANSACTION_RESPONSE = "loanTransactionResponse";
|
public static final String LOAN_TRANSACTION_RESPONSE = "loanTransactionResponse";
|
||||||
|
public static final String LOAN_CAPITALIZED_INCOME_RESPONSE = "loanCapitalizedIncomeResponse";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@CapitalizedIncomeFeature
|
||||||
|
Feature: Capitalized Income
|
||||||
|
|
||||||
|
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement
|
||||||
|
When Admin sets the business date to "1 January 2024"
|
||||||
|
And Admin creates a client with random data
|
||||||
|
When Admin creates a fully customized loan with the following data:
|
||||||
|
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||||
|
| LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME | 1 January 2024 | 1000.0 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 30 | DAYS | 30 | DAYS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||||
|
And Admin successfully approves the loan on "1 January 2024" with "1000" amount and expected disbursement date on "1 January 2024"
|
||||||
|
And Admin successfully disburse the loan on "1 January 2024" with "900" EUR transaction amount
|
||||||
|
Then Loan status will be "ACTIVE"
|
||||||
|
When Admin sets the business date to "2 January 2024"
|
||||||
|
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "2 January 2024" with "100" EUR transaction amount
|
||||||
|
Then Loan Transactions tab has the following data:
|
||||||
|
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted |
|
||||||
|
| 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false |
|
||||||
|
| 01 January 2024 | Down Payment | 225.0 | 225.0 | 0.0 | 0.0 | 0.0 | 675.0 | false |
|
||||||
|
| 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 775.0 | false |
|
||||||
|
Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with date "02 January 2024" which has the following Journal entries:
|
||||||
|
| Type | Account code | Account name | Debit | Credit |
|
||||||
|
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
||||||
|
| LIABILITY | 145024 | Deferred Income | | 100.0 |
|
||||||
+1
@@ -52,5 +52,6 @@ public interface LoanTransactionApiConstants {
|
|||||||
accrualActivity, //
|
accrualActivity, //
|
||||||
interestRefund, //
|
interestRefund, //
|
||||||
accrualAdjustment, //
|
accrualAdjustment, //
|
||||||
|
capitalizedIncome, //
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -306,6 +306,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
return newTransaction;
|
return newTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LoanTransaction capitalizedIncome(final Loan loan, final Money amount, final PaymentDetail paymentDetail,
|
||||||
|
final LocalDate transactionDate, final ExternalId externalId) {
|
||||||
|
return new LoanTransaction(null, loan.getOffice(), LoanTransactionType.CAPITALIZED_INCOME, paymentDetail, amount.getAmount(),
|
||||||
|
transactionDate, externalId);
|
||||||
|
}
|
||||||
|
|
||||||
public LoanTransaction copyTransactionPropertiesAndMappings() {
|
public LoanTransaction copyTransactionPropertiesAndMappings() {
|
||||||
LoanTransaction newTransaction = copyTransactionProperties(this);
|
LoanTransaction newTransaction = copyTransactionProperties(this);
|
||||||
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.loanaccount.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.orm.jpa.JpaSystemException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CommandType(entity = "LOAN", action = "CAPITALIZEDINCOME")
|
||||||
|
public class AddCapitalizedIncomeCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final LoanWritePlatformService writePlatformService;
|
||||||
|
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.writePlatformService.addCapitalizedIncome(command.getLoanId(), command);
|
||||||
|
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||||
|
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.capitalized.income",
|
||||||
|
"Capitalized Income");
|
||||||
|
return CommandProcessingResult.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -125,4 +125,6 @@ public interface LoanWritePlatformService {
|
|||||||
CommandProcessingResult undoChargeOff(JsonCommand command);
|
CommandProcessingResult undoChargeOff(JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command);
|
CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult addCapitalizedIncome(Long loanId, JsonCommand command);
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
or more contributor license agreements. See the NOTICE file
|
||||||
|
distributed with this work for additional information
|
||||||
|
regarding copyright ownership. The ASF licenses this file
|
||||||
|
to you under the Apache License, Version 2.0 (the
|
||||||
|
"License"); you may not use this file except in compliance
|
||||||
|
with the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing,
|
||||||
|
software distributed under the License is distributed on an
|
||||||
|
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, either express or implied. See the License for the
|
||||||
|
specific language governing permissions and limitations
|
||||||
|
under the License.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||||
|
<changeSet author="fineract" id="1">
|
||||||
|
<insert tableName="r_enum_value">
|
||||||
|
<column name="enum_name" value="transaction_type_enum"/>
|
||||||
|
<column name="enum_id" valueNumeric="35"/>
|
||||||
|
<column name="enum_message_property" value="Capitalized Income"/>
|
||||||
|
<column name="enum_value" value="Capitalized Income"/>
|
||||||
|
<column name="enum_type" valueBoolean="false"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+41
@@ -123,6 +123,47 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterestRefund()) {
|
else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterestRefund()) {
|
||||||
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO, loanTransactionDTO, office);
|
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO, loanTransactionDTO, office);
|
||||||
}
|
}
|
||||||
|
// Handle Capitalized Income
|
||||||
|
if (transactionType.isCapitalizedIncome()) {
|
||||||
|
createJournalEntriesForCapitalizedIncome(loanDTO, loanTransactionDTO, office);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createJournalEntriesForCapitalizedIncome(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||||
|
final Office office) {
|
||||||
|
// loan properties
|
||||||
|
final Long loanProductId = loanDTO.getLoanProductId();
|
||||||
|
final Long loanId = loanDTO.getLoanId();
|
||||||
|
final String currencyCode = loanDTO.getCurrencyCode();
|
||||||
|
// transaction properties
|
||||||
|
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||||
|
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||||
|
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
|
||||||
|
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||||
|
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
|
||||||
|
|
||||||
|
if (MathUtil.isGreaterThanZero(principalAmount)) {
|
||||||
|
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
|
||||||
|
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
||||||
|
glAccountBalanceHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create credit entries
|
||||||
|
for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
|
||||||
|
if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
|
||||||
|
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
|
||||||
|
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
|
||||||
|
creditEntry.getValue(), glAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create debit entries
|
||||||
|
for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
|
||||||
|
if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
|
||||||
|
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
|
||||||
|
this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
|
||||||
|
debitEntry.getValue(), glAccount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -90,6 +90,7 @@ public class LoanTransactionsApiResource {
|
|||||||
public static final String REAGE = "reAge";
|
public static final String REAGE = "reAge";
|
||||||
public static final String REAMORTIZE = "reAmortize";
|
public static final String REAMORTIZE = "reAmortize";
|
||||||
public static final String UNDO_REAMORTIZE = "undoReAmortize";
|
public static final String UNDO_REAMORTIZE = "undoReAmortize";
|
||||||
|
public static final String CAPITALIZED_INCOME = "capitalizedIncome";
|
||||||
private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
|
private final Set<String> responseDataParameters = new HashSet<>(Arrays.asList("id", "type", "date", "currency", "amount", "externalId",
|
||||||
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
|
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
|
||||||
|
|
||||||
@@ -526,6 +527,7 @@ public class LoanTransactionsApiResource {
|
|||||||
case accrualActivity -> LoanTransactionType.ACCRUAL_ACTIVITY;
|
case accrualActivity -> LoanTransactionType.ACCRUAL_ACTIVITY;
|
||||||
case interestRefund -> LoanTransactionType.INTEREST_REFUND;
|
case interestRefund -> LoanTransactionType.INTEREST_REFUND;
|
||||||
case accrualAdjustment -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
|
case accrualAdjustment -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
|
||||||
|
case capitalizedIncome -> LoanTransactionType.CAPITALIZED_INCOME;
|
||||||
default ->
|
default ->
|
||||||
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
||||||
};
|
};
|
||||||
@@ -583,6 +585,8 @@ public class LoanTransactionsApiResource {
|
|||||||
commandRequest = builder.reAmortize(resolvedLoanId).build();
|
commandRequest = builder.reAmortize(resolvedLoanId).build();
|
||||||
} else if (CommandParameterUtil.is(commandParam, UNDO_REAMORTIZE)) {
|
} else if (CommandParameterUtil.is(commandParam, UNDO_REAMORTIZE)) {
|
||||||
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
|
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
|
||||||
|
} else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) {
|
||||||
|
commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandRequest == null) {
|
if (commandRequest == null) {
|
||||||
|
|||||||
+90
@@ -69,6 +69,8 @@ import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||||
@@ -105,6 +107,7 @@ public final class LoanTransactionValidator {
|
|||||||
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
||||||
private final CalendarInstanceRepository calendarInstanceRepository;
|
private final CalendarInstanceRepository calendarInstanceRepository;
|
||||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||||
|
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||||
|
|
||||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||||
if (!dataValidationErrors.isEmpty()) {
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
@@ -1034,4 +1037,91 @@ public final class LoanTransactionValidator {
|
|||||||
loan.getRescheduledOnDate());
|
loan.getRescheduledOnDate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void validateCapitalizedIncome(final JsonCommand command, final Long loanId) {
|
||||||
|
final String json = command.json();
|
||||||
|
if (StringUtils.isBlank(json)) {
|
||||||
|
throw new InvalidJsonException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonElement element = this.fromApiJsonHelper.parse(json);
|
||||||
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeParameters());
|
||||||
|
|
||||||
|
LoanApplicationValidator.validateOrThrow("loan.capitalized.income", baseDataValidator -> {
|
||||||
|
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||||
|
validateLoanClientIsActive(loan);
|
||||||
|
validateLoanGroupIsActive(loan);
|
||||||
|
|
||||||
|
// Validate that loan is disbursed
|
||||||
|
if (!loan.isDisbursed()) {
|
||||||
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("capitalized.income.only.after.disbursement",
|
||||||
|
"Capitalized income can be added to the loan only after Disbursement");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate loan is progressive
|
||||||
|
if (!loan.isProgressiveSchedule()) {
|
||||||
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.progressive.loan");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate income capitalization is enabled
|
||||||
|
if (!loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()) {
|
||||||
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("income.capitalization.not.enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate loan is active
|
||||||
|
if (!loan.getStatus().isActive()) {
|
||||||
|
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("not.active");
|
||||||
|
}
|
||||||
|
|
||||||
|
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
|
||||||
|
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
|
||||||
|
|
||||||
|
// Validate transaction date is not before disbursement date
|
||||||
|
if (transactionDate != null && loan.getDisbursementDate() != null && transactionDate.isBefore(loan.getDisbursementDate())) {
|
||||||
|
baseDataValidator.reset().parameter("transactionDate").failWithCode("before.disbursement.date",
|
||||||
|
"Transaction date cannot be before disbursement date");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||||
|
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
||||||
|
|
||||||
|
// Validate total disbursement + capitalized income <= approved amount
|
||||||
|
if (transactionAmount != null) {
|
||||||
|
final BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||||
|
final BigDecimal existingCapitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loanId).stream()
|
||||||
|
.map(LoanCapitalizedIncomeBalance::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
final BigDecimal approvedAmount = loan.getApprovedPrincipal();
|
||||||
|
final BigDecimal newTotal = totalDisbursed.add(existingCapitalizedIncomeBalance).add(transactionAmount);
|
||||||
|
|
||||||
|
if (newTotal.compareTo(approvedAmount) > 0) {
|
||||||
|
baseDataValidator.reset().parameter("transactionAmount").failWithCode("exceeds.approved.amount",
|
||||||
|
"Sum of disbursed amount and capitalized income cannot exceed approved amount");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePaymentDetails(baseDataValidator, element);
|
||||||
|
validateNote(baseDataValidator, element);
|
||||||
|
validateExternalId(baseDataValidator, element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateNote(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||||
|
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
|
||||||
|
if (StringUtils.isNotBlank(note)) {
|
||||||
|
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateExternalId(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||||
|
final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
|
||||||
|
if (StringUtils.isNotBlank(externalId)) {
|
||||||
|
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getCapitalizedIncomeParameters() {
|
||||||
|
return new HashSet<>(
|
||||||
|
Arrays.asList("transactionDate", "dateFormat", "locale", "transactionAmount", "paymentTypeId", "note", "externalId"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -2996,6 +2996,53 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult addCapitalizedIncome(final Long loanId, final JsonCommand command) {
|
||||||
|
loanTransactionValidator.validateCapitalizedIncome(command, loanId);
|
||||||
|
|
||||||
|
final Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
|
final AppUser currentUser = getAppUserIfPresent();
|
||||||
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Create payment details
|
||||||
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
|
|
||||||
|
// Validate cash payment if applicable
|
||||||
|
if (paymentDetail != null && paymentDetail.getPaymentType() != null && paymentDetail.getPaymentType().getIsCashPayment()) {
|
||||||
|
BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
this.cashierTransactionDataValidator.validateOnLoanDisbursal(currentUser, loan.getCurrencyCode(), transactionAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract transaction details
|
||||||
|
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||||
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, "externalId");
|
||||||
|
|
||||||
|
// Create capitalized income transaction
|
||||||
|
final Money capitalizedIncomeAmount = Money.of(loan.getCurrency(), transactionAmount);
|
||||||
|
final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail,
|
||||||
|
transactionDate, txnExternalId);
|
||||||
|
|
||||||
|
// Update loan with capitalized income
|
||||||
|
capitalizedIncomeTransaction.updateLoan(loan);
|
||||||
|
loan.addLoanTransaction(capitalizedIncomeTransaction);
|
||||||
|
loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction);
|
||||||
|
|
||||||
|
// Update loan counters and save
|
||||||
|
loan.updateLoanScheduleDependentDerivedFields();
|
||||||
|
|
||||||
|
// Create note if provided
|
||||||
|
createNote(loan, command, changes);
|
||||||
|
|
||||||
|
// Post journal entries
|
||||||
|
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
|
||||||
|
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
|
||||||
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder().withEntityId(loan.getId()).withEntityExternalId(loan.getExternalId()).build();
|
||||||
|
}
|
||||||
|
|
||||||
public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) {
|
public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) {
|
||||||
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
|
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user