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) {
|
||||
super("error.msg." + paramName + ".invalid.account.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);
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -3822,4 +3822,13 @@ public class CommandWrapperBuilder {
|
||||
this.href = "/v1/loans/external-id/" + loanExternalId + "/interest-pauses/" + variationId;
|
||||
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_ACC_MATUR_CHARGE_OFF, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATE_LAST_INSTALLMENT, //
|
||||
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME, //
|
||||
;
|
||||
|
||||
@Override
|
||||
|
||||
+117
@@ -1500,4 +1500,121 @@ public class LoanProductsRequestFactory {
|
||||
.chargeOffFraudExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.CREDIT_LOSS_BAD_DEBT_FRAUD))//
|
||||
.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)
|
||||
.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(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATELAST_INSTALLMENT,
|
||||
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,
|
||||
|
||||
+19
@@ -3863,4 +3863,23 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
assertNotNull(targetProduct.getInterestRecognitionOnDisbursementDate());
|
||||
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 = "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_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_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";
|
||||
@@ -203,4 +204,5 @@ public abstract class TestContextKey {
|
||||
public static final String LOAN_WRITE_OFF_RESPONSE = "loanWriteOffResponse";
|
||||
public static final String LOAN_DELINQUENCY_ACTION_RESPONSE = "loanDelinquencyActionResponse";
|
||||
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, //
|
||||
interestRefund, //
|
||||
accrualAdjustment, //
|
||||
capitalizedIncome, //
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -306,6 +306,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
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() {
|
||||
LoanTransaction newTransaction = copyTransactionProperties(this);
|
||||
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 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()) {
|
||||
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 REAMORTIZE = "reAmortize";
|
||||
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",
|
||||
LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME, LoanApiConstants.REVERSED_ON_DATE_PARAMNAME));
|
||||
|
||||
@@ -526,6 +527,7 @@ public class LoanTransactionsApiResource {
|
||||
case accrualActivity -> LoanTransactionType.ACCRUAL_ACTIVITY;
|
||||
case interestRefund -> LoanTransactionType.INTEREST_REFUND;
|
||||
case accrualAdjustment -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
|
||||
case capitalizedIncome -> LoanTransactionType.CAPITALIZED_INCOME;
|
||||
default ->
|
||||
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
||||
};
|
||||
@@ -583,6 +585,8 @@ public class LoanTransactionsApiResource {
|
||||
commandRequest = builder.reAmortize(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, UNDO_REAMORTIZE)) {
|
||||
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) {
|
||||
commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build();
|
||||
}
|
||||
|
||||
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.ScheduleGeneratorDTO;
|
||||
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.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
|
||||
@@ -105,6 +107,7 @@ public final class LoanTransactionValidator {
|
||||
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
|
||||
private final CalendarInstanceRepository calendarInstanceRepository;
|
||||
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
@@ -1034,4 +1037,91 @@ public final class LoanTransactionValidator {
|
||||
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();
|
||||
}
|
||||
|
||||
@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) {
|
||||
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user