FINERACT-2311: Add buydown fees to loan

This commit is contained in:
budaidev
2025-07-01 20:14:43 +02:00
committed by Adam Saghy
parent cb6a15c3d2
commit 98b7328508
23 changed files with 729 additions and 7 deletions
@@ -145,6 +145,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
if (transactionType.isCapitalizedIncomeAmortizationAdjustment()) {
createJournalEntriesForCapitalizedIncomeAmortizationAdjustment(loanDTO, loanTransactionDTO, office);
}
// Handle Buy Down Fee
if (transactionType.isBuyDownFee()) {
createJournalEntriesForBuyDownFee(loanDTO, loanTransactionDTO, office);
}
}
}
@@ -412,6 +416,26 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
private void createJournalEntriesForBuyDownFee(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 amount = loanTransactionDTO.getAmount();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
if (MathUtil.isGreaterThanZero(amount)) {
this.helper.createJournalEntriesForLoan(office, currencyCode, AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), loanProductId, paymentTypeId, loanId, transactionId,
transactionDate, amount);
}
}
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
@@ -596,6 +596,8 @@ public class LoanTransactionsApiResource {
commandRequest = builder.undoReAmortize(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, CAPITALIZED_INCOME)) {
commandRequest = builder.addCapitalizedIncome(resolvedLoanId).build();
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) {
commandRequest = builder.makeLoanBuyDownFee(resolvedLoanId).build();
}
if (commandRequest == null) {
@@ -684,6 +686,8 @@ public class LoanTransactionsApiResource {
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId,
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, transactionId);
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.BUY_DOWN_FEE_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanTransactionTemplate(resolvedLoanId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -103,6 +103,8 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionVa
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.BuyDownFeeWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
@@ -149,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator;
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
@@ -158,6 +161,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.note.service.NoteWritePlatformService;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadPlatformService;
import org.apache.fineract.portfolio.rate.service.RateAssembler;
@@ -372,6 +376,16 @@ public class LoanAccountConfiguration {
workingDaysRepository, loanScheduleFactory, floatingRatesReadPlatformService, calendarReadPlatformService, noteRepository);
}
@Bean
@ConditionalOnMissingBean(BuyDownFeePlatformService.class)
public BuyDownFeePlatformService buyDownFeePlatformService(ProgressiveLoanTransactionValidator loanTransactionValidator,
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory);
}
@Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
@@ -407,7 +421,7 @@ public class LoanAccountConfiguration {
LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper loanMapper,
LoanTransactionProcessingService loanTransactionProcessingService, final LoanBalanceService loanBalanceService,
LoanTransactionService loanTransactionService) {
LoanTransactionService loanTransactionService, BuyDownFeePlatformService buyDownFeePlatformService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
@@ -175,7 +175,7 @@ public class LoanProductsApiResource {
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonToExpenseAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType,enableBuyDownFee\n\n"
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
@@ -200,7 +200,8 @@ public final class LoanProductDataValidator {
LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME,
LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_TYPE_PARAM_NAME,
LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME,
LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME));
LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME,
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue()));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -204,4 +204,5 @@
<include file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" relativeToChangelogFile="true" />
<include file="parts/0184_add_document_event_configuration.xml" relativeToChangelogFile="true" />
<include file="parts/0185_loan_buy_down_fee.xml" relativeToChangelogFile="true" />
<include file="parts/0186_add_buy_down_fee_to_loan_product.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,44 @@
<?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.1.xsd">
<changeSet author="fineract" id="1">
<insert tableName="r_enum_value">
<column name="enum_name" value="transaction_type_enum"/>
<column name="enum_id" valueNumeric="40"/>
<column name="enum_message_property" value="Buy Down Fee"/>
<column name="enum_value" value="Buy Down Fee"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="r_enum_value">
<column name="enum_name" value="transaction_type_enum"/>
<column name="enum_id" valueNumeric="41"/>
<column name="enum_message_property" value="Buy Down Fee Adjustment"/>
<column name="enum_value" value="Buy Down Fee Adjustment"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>