FINERACT-2330: Buy-Down fees Accounting for non merchant product

This commit is contained in:
Jose Alberto Hernandez
2025-08-15 22:54:36 +02:00
committed by Arnold Gálovics
parent 125c8b4db2
commit 6bbc4bd3f3
32 changed files with 233 additions and 65 deletions
@@ -30,7 +30,8 @@ public interface ProductToGLAccountMappingWritePlatformService {
void createSavingProductToGLAccountMapping(Long savingProductId, JsonCommand command, DepositAccountType accountType);
Map<String, Object> updateLoanProductToGLAccountMapping(Long loanProductId, JsonCommand command, boolean accountingRuleChanged,
AccountingRuleType accountingRuleTypeId, boolean enableIncomeCapitalization, boolean enableBuyDownFee);
AccountingRuleType accountingRuleTypeId, boolean enableIncomeCapitalization, boolean enableBuyDownFee,
boolean merchantBuyDownFee);
Map<String, Object> updateSavingsProductToGLAccountMapping(Long savingsProductId, JsonCommand command, boolean accountingRuleChanged,
int accountingRuleTypeId, DepositAccountType accountType);
@@ -1751,6 +1751,7 @@ public class LoanProductsRequestFactory {
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)//
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.INTEREST)//
.enableBuyDownFee(true)//
.merchantBuyDownFee(true)//
.buyDownExpenseAccountId(accountTypeResolver.resolve(DefaultAccountType.BUY_DOWN_EXPENSE))//
.incomeFromBuyDownAccountId(accountTypeResolver.resolve(DefaultAccountType.INCOME_FROM_BUY_DOWN));//
}
@@ -256,7 +256,7 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
*/
public void handleChangesToLoanProductToGLAccountMappings(final Long loanProductId, final Map<String, Object> changes,
final JsonElement element, final AccountingRuleType accountingRuleType, final boolean enableIncomeCapitalization,
final boolean enableBuyDownFee) {
final boolean enableBuyDownFee, final boolean merchantBuyDownFee) {
switch (accountingRuleType) {
case NONE:
break;
@@ -399,9 +399,11 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
deleteProductToGLAccountMapping(loanProductId, PortfolioProductType.LOAN,
AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue());
} else {
mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), loanProductId,
AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(), AccrualAccountsForLoan.BUY_DOWN_EXPENSE.toString(),
changes);
if (merchantBuyDownFee) {
mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(),
loanProductId, AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(),
AccrualAccountsForLoan.BUY_DOWN_EXPENSE.toString(), changes);
}
}
// liabilities
@@ -46,5 +46,6 @@ public class AccountingBridgeDataDTO {
private Long chargeOffReasonCodeValue;
private boolean isWrittenOff;
private List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new ArrayList<>();
private boolean merchantBuyDownFee;
}
@@ -288,6 +288,7 @@ public class LoanAccountData {
private StringEnumOptionData buyDownFeeCalculationType;
private StringEnumOptionData buyDownFeeStrategy;
private StringEnumOptionData buyDownFeeIncomeType;
private Boolean merchantBuyDownFee;
public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId,
Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments,
@@ -483,7 +484,8 @@ public class LoanAccountData {
final boolean enableIncomeCapitalization, final StringEnumOptionData capitalizedIncomeCalculationType,
final StringEnumOptionData capitalizedIncomeStrategy, StringEnumOptionData capitalizedIncomeType,
final boolean enableBuyDownFee, final StringEnumOptionData buyDownFeeCalculationType,
final StringEnumOptionData buyDownFeeStrategy, final StringEnumOptionData buyDownFeeIncomeType) {
final StringEnumOptionData buyDownFeeStrategy, final StringEnumOptionData buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final CollectionData delinquent = CollectionData.template();
@@ -533,7 +535,8 @@ public class LoanAccountData {
.setCapitalizedIncomeCalculationType(capitalizedIncomeCalculationType)
.setCapitalizedIncomeStrategy(capitalizedIncomeStrategy).setCapitalizedIncomeType(capitalizedIncomeType)
.setEnableBuyDownFee(enableBuyDownFee).setBuyDownFeeCalculationType(buyDownFeeCalculationType)
.setBuyDownFeeStrategy(buyDownFeeStrategy).setBuyDownFeeIncomeType(buyDownFeeIncomeType);
.setBuyDownFeeStrategy(buyDownFeeStrategy).setBuyDownFeeIncomeType(buyDownFeeIncomeType)
.setMerchantBuyDownFee(merchantBuyDownFee);
}
/*
@@ -246,6 +246,7 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeCalculationType buyDownFeeCalculationType;
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
private LoanApplicationTerms(Builder builder) {
this.currency = builder.currency;
@@ -290,6 +291,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeCalculationType = builder.buyDownFeeCalculationType;
this.buyDownFeeStrategy = builder.buyDownFeeStrategy;
this.buyDownFeeIncomeType = builder.buyDownFeeIncomeType;
this.merchantBuyDownFee = builder.merchantBuyDownFee;
this.interestMethod = builder.interestMethod;
this.allowPartialPeriodInterestCalcualtion = builder.allowPartialPeriodInterestCalculation;
}
@@ -330,6 +332,7 @@ public final class LoanApplicationTerms {
private LoanBuyDownFeeCalculationType buyDownFeeCalculationType;
private LoanBuyDownFeeStrategy buyDownFeeStrategy;
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
private boolean allowPartialPeriodInterestCalculation;
public Builder interestMethod(InterestMethod interestMethod) {
@@ -492,6 +495,11 @@ public final class LoanApplicationTerms {
return this;
}
public Builder merchantBuyDownFee(boolean value) {
this.merchantBuyDownFee = value;
return this;
}
public LoanApplicationTerms build() {
return new LoanApplicationTerms(this);
}
@@ -570,7 +578,8 @@ public final class LoanApplicationTerms {
final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType,
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType) {
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -592,7 +601,7 @@ public final class LoanApplicationTerms {
fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
@@ -670,7 +679,8 @@ public final class LoanApplicationTerms {
loanProductRelatedDetail.isEnableIncomeCapitalization(), loanProductRelatedDetail.getCapitalizedIncomeCalculationType(),
loanProductRelatedDetail.getCapitalizedIncomeStrategy(), loanProductRelatedDetail.getCapitalizedIncomeType(),
loanProductRelatedDetail.isEnableBuyDownFee(), loanProductRelatedDetail.getBuyDownFeeCalculationType(),
loanProductRelatedDetail.getBuyDownFeeStrategy(), loanProductRelatedDetail.getBuyDownFeeIncomeType());
loanProductRelatedDetail.getBuyDownFeeStrategy(), loanProductRelatedDetail.getBuyDownFeeIncomeType(),
loanProductRelatedDetail.isMerchantBuyDownFee());
}
private LoanApplicationTerms(final CurrencyData currency, final Integer loanTermFrequency,
@@ -705,7 +715,8 @@ public final class LoanApplicationTerms {
final boolean enableIncomeCapitalization, final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType,
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType) {
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
this.currency = currency;
this.loanTermFrequency = loanTermFrequency;
@@ -815,6 +826,7 @@ public final class LoanApplicationTerms {
this.buyDownFeeCalculationType = buyDownFeeCalculationType;
this.buyDownFeeStrategy = buyDownFeeStrategy;
this.buyDownFeeIncomeType = buyDownFeeIncomeType;
this.merchantBuyDownFee = merchantBuyDownFee;
}
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1679,7 +1691,7 @@ public final class LoanApplicationTerms {
this.chargeOffBehaviour, this.interestRecognitionOnDisbursementDate, this.daysInYearCustomStrategy,
this.enableIncomeCapitalization, this.capitalizedIncomeCalculationType, this.capitalizedIncomeStrategy,
this.capitalizedIncomeType, this.installmentAmountInMultiplesOf, this.enableBuyDownFee, this.buyDownFeeCalculationType,
this.buyDownFeeStrategy, this.buyDownFeeIncomeType);
this.buyDownFeeStrategy, this.buyDownFeeIncomeType, this.merchantBuyDownFee);
}
public LoanProductMinimumRepaymentScheduleRelatedDetail toLoanProductRelatedDetailMinimumData() {
@@ -59,13 +59,13 @@ public class LoanAccountingBridgeMapper {
currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, false, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
newLoanTransactionsBeforeChargeOff);
newLoanTransactionsBeforeChargeOff, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee());
AccountingBridgeDataDTO afterChargeOff = new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(),
currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, true, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
newLoanTransactionsAfterChargeOff);
newLoanTransactionsAfterChargeOff, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee());
List<AccountingBridgeDataDTO> result = new ArrayList<>();
result.add(beforeChargeOff);
@@ -86,7 +86,7 @@ public class LoanAccountingBridgeMapper {
loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, loan.isChargedOff(), loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
newLoanTransactions);
newLoanTransactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee());
}
public AccountingBridgeLoanTransactionDTO mapToLoanTransactionData(final LoanTransaction transaction, final String currencyCode) {
@@ -183,4 +183,5 @@ public interface LoanProductConstants {
String BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME = "buyDownFeeCalculationType";
String BUY_DOWN_FEE_STRATEGY_PARAM_NAME = "buyDownFeeStrategy";
String BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME = "buyDownFeeIncomeType";
String MERCHANT_BUY_DOWN_FEE_PARAM_NAME = "merchantBuyDownFee";
}
@@ -213,6 +213,8 @@ public final class LoanProductsApiResourceSwagger {
public String buyDownFeeStrategy;
@Schema(example = "FEE", allowableValues = { "FEE", "INTEREST" })
public String buyDownFeeIncomeType;
@Schema(example = "false")
public Boolean merchantBuyDownFee;
// Interest Recalculation
@Schema(example = "false")
@@ -693,6 +695,8 @@ public final class LoanProductsApiResourceSwagger {
public StringEnumOptionData buyDownFeeStrategy;
@Schema(example = "FEE")
public StringEnumOptionData buyDownFeeIncomeType;
@Schema(example = "false")
public Boolean merchantBuyDownFee;
}
@Schema(description = "GetLoanProductsTemplateResponse")
@@ -1157,6 +1161,8 @@ public final class LoanProductsApiResourceSwagger {
public StringEnumOptionData buyDownFeeCalculationType;
@Schema(example = "EQUAL_AMORTIZATION")
public StringEnumOptionData buyDownFeeStrategy;
@Schema(example = "false")
public Boolean merchantBuyDownFee;
@Schema(example = "FEE")
public StringEnumOptionData buyDownFeeIncomeType;
public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions;
@@ -1505,6 +1511,8 @@ public final class LoanProductsApiResourceSwagger {
public StringEnumOptionData buyDownFeeStrategy;
@Schema(example = "FEE")
public StringEnumOptionData buyDownFeeIncomeType;
@Schema(example = "false")
public Boolean merchantBuyDownFee;
public List<StringEnumOptionData> buyDownFeeCalculationTypeOptions;
public List<StringEnumOptionData> buyDownFeeStrategyOptions;
public List<StringEnumOptionData> buyDownFeeIncomeTypeOptions;
@@ -1787,6 +1795,8 @@ public final class LoanProductsApiResourceSwagger {
public String buyDownFeeStrategy;
@Schema(example = "FEE", allowableValues = { "FEE", "INTEREST" })
public String buyDownFeeIncomeType;
@Schema(example = "false")
public Boolean merchantBuyDownFee;
}
public static final class AdvancedPaymentData {
@@ -256,6 +256,7 @@ public class LoanProductData implements Serializable {
private StringEnumOptionData buyDownFeeCalculationType;
private StringEnumOptionData buyDownFeeStrategy;
private StringEnumOptionData buyDownFeeIncomeType;
private boolean merchantBuyDownFee;
private List<StringEnumOptionData> buyDownFeeCalculationTypeOptions;
private List<StringEnumOptionData> buyDownFeeStrategyOptions;
private List<StringEnumOptionData> buyDownFeeIncomeTypeOptions;
@@ -371,6 +372,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeCalculationType = null;
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -394,7 +396,7 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
@@ -507,6 +509,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeCalculationType = null;
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -530,7 +533,7 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
@@ -650,6 +653,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeCalculationType = null;
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -673,7 +677,7 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
@@ -787,6 +791,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeCalculationType = null;
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -810,7 +815,7 @@ public class LoanProductData implements Serializable {
loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour,
interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -869,7 +874,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy,
final StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee,
final StringEnumOptionData buyDownFeeCalculationType, final StringEnumOptionData buyDownFeeStrategy,
final StringEnumOptionData buyDownFeeIncomeType) {
final StringEnumOptionData buyDownFeeIncomeType, final boolean merchantBuyDownFee) {
this.id = id;
this.name = name;
this.shortName = shortName;
@@ -934,6 +939,7 @@ public class LoanProductData implements Serializable {
this.buyDownFeeCalculationType = buyDownFeeCalculationType;
this.buyDownFeeStrategy = buyDownFeeStrategy;
this.buyDownFeeIncomeType = buyDownFeeIncomeType;
this.merchantBuyDownFee = merchantBuyDownFee;
this.chargeOptions = null;
this.penaltyOptions = null;
@@ -1238,6 +1244,8 @@ public class LoanProductData implements Serializable {
this.buyDownFeeCalculationTypeOptions = buyDownFeeCalculationTypeOptions;
this.buyDownFeeStrategyOptions = buyDownFeeStrategyOptions;
this.buyDownFeeIncomeTypeOptions = buyDownFeeIncomeTypeOptions;
this.merchantBuyDownFee = productData.isMerchantBuyDownFee();
}
private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
@@ -287,7 +287,8 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType,
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final boolean enableBuyDownFee, final LoanBuyDownFeeCalculationType buyDownFeeCalculationType,
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType) {
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LoanBuyDownFeeIncomeType buyDownFeeIncomeType,
final boolean merchantBuyDownFee) {
this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
@@ -339,7 +340,8 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting,
supportedInterestRefundTypes, chargeOffBehaviour, isInterestRecognitionOnDisbursementDate, daysInYearCustomStrategy,
enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType,
installmentAmountInMultiplesOf, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
installmentAmountInMultiplesOf, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee);
this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal,
defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments,
@@ -204,6 +204,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
@Column(name = "buy_down_fee_income_type")
private LoanBuyDownFeeIncomeType buyDownFeeIncomeType;
@Column(name = "is_merchant_buy_down_fee")
private boolean merchantBuyDownFee = true;
@Column(name = "installment_amount_in_multiples_of")
private Integer installmentAmountInMultiplesOf;
@@ -226,7 +229,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final Integer installmentAmountInMultiplesOf, final boolean enableBuyDownFee,
final LoanBuyDownFeeCalculationType buyDownFeeCalculationType, final LoanBuyDownFeeStrategy buyDownFeeStrategy,
final LoanBuyDownFeeIncomeType buyDownFeeIncomeType) {
final LoanBuyDownFeeIncomeType buyDownFeeIncomeType, final boolean merchantBuyDownFee) {
final MonetaryCurrency currency = MonetaryCurrency.fromCurrencyData(currencyData);
return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType,
@@ -238,7 +241,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes,
chargeOffBehaviour, interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, installmentAmountInMultiplesOf,
enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
protected LoanProductRelatedDetail() {
@@ -264,7 +267,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LoanCapitalizedIncomeType capitalizedIncomeType,
final Integer installmentAmountInMultiplesOf, final boolean enableBuyDownFee,
final LoanBuyDownFeeCalculationType buyDownFeeCalculationType, final LoanBuyDownFeeStrategy buyDownFeeStrategy,
final LoanBuyDownFeeIncomeType buyDownFeeIncomeType) {
final LoanBuyDownFeeIncomeType buyDownFeeIncomeType, final boolean merchantBuyDownFee) {
this.currency = currency;
this.principal = defaultPrincipal;
this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod;
@@ -311,6 +314,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche
this.buyDownFeeCalculationType = buyDownFeeCalculationType;
this.buyDownFeeStrategy = buyDownFeeStrategy;
this.buyDownFeeIncomeType = buyDownFeeIncomeType;
this.merchantBuyDownFee = merchantBuyDownFee;
}
private Integer defaultToNullIfZero(final Integer value) {
@@ -53,4 +53,5 @@
<include relativeToChangelogFile="true" file="parts/1028_remove_suboptimal_indexes.xml"/>
<include relativeToChangelogFile="true" file="parts/1029_add_installment_amount_in_multiples_of_to_loan.xml"/>
<include relativeToChangelogFile="true" file="parts/1030_add_loan_undo_contract_termination_event.xml"/>
<include relativeToChangelogFile="true" file="parts/1031_loan_merchant_buy_down_fee.xml"/>
</databaseChangeLog>
@@ -0,0 +1,37 @@
<?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">
<addColumn tableName="m_loan">
<column name="is_merchant_buy_down_fee" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="m_product_loan">
<column name="is_merchant_buy_down_fee" type="boolean" defaultValueBoolean="true">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -47,4 +47,5 @@ public class LoanDTO {
private boolean markedAsFraud;
private Long chargeOffReasonCodeValue;
private boolean markedAsWrittenOff;
private boolean merchantBuyDownFee;
}
@@ -111,6 +111,7 @@ public class AccountingProcessorHelper {
final boolean cashBasedAccountingEnabled = accountingBridgeData.isCashBasedAccountingEnabled();
final boolean upfrontAccrualBasedAccountingEnabled = accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled();
final boolean periodicAccrualBasedAccountingEnabled = accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled();
final boolean merchantBuyDownFee = accountingBridgeData.isMerchantBuyDownFee();
final List<AccountingBridgeLoanTransactionDTO> loanTransactionDTOs = accountingBridgeData.getNewLoanTransactions();
@@ -170,7 +171,7 @@ public class AccountingProcessorHelper {
return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff);
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee);
}
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
@@ -435,8 +435,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final BigDecimal amount = loanTransactionDTO.getAmount();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final AccrualAccountsForLoan debitAccountType = loanDTO.isMerchantBuyDownFee() ? AccrualAccountsForLoan.BUY_DOWN_EXPENSE
: AccrualAccountsForLoan.FUND_SOURCE;
if (MathUtil.isGreaterThanZero(amount)) {
this.helper.createJournalEntriesForLoan(office, currencyCode, AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue(),
this.helper.createJournalEntriesForLoan(office, currencyCode, debitAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), loanProductId, paymentTypeId, loanId, transactionId,
transactionDate, amount);
}
@@ -41,6 +41,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.service.SavingsPro
import org.apache.fineract.accounting.producttoaccountmapping.service.ShareProductToGLAccountMappingHelper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +62,14 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
final JsonElement element = this.fromApiJsonHelper.parse(command.json());
final Integer accountingRuleTypeId = this.fromApiJsonHelper.extractIntegerNamed("accountingRule", element, Locale.getDefault());
final AccountingRuleType accountingRuleType = AccountingRuleType.fromInt(accountingRuleTypeId);
boolean merchantBuyDownFee = true;
if (fromApiJsonHelper.parameterExists(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME, element)) {
final Boolean merchantBuyDownFeeParamValue = fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME, element);
if (merchantBuyDownFeeParamValue != null) {
merchantBuyDownFee = merchantBuyDownFeeParamValue;
}
}
switch (accountingRuleType) {
case NONE:
@@ -207,9 +216,11 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), loanProductId,
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue());
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), loanProductId,
AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue());
if (merchantBuyDownFee) {
this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element,
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), loanProductId,
AccrualAccountsForLoan.BUY_DOWN_EXPENSE.getValue());
}
// liabilities
this.loanProductToGLAccountMappingHelper.saveLoanToLiabilityAccountMapping(element,
@@ -370,7 +381,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
@Transactional
public Map<String, Object> updateLoanProductToGLAccountMapping(final Long loanProductId, final JsonCommand command,
final boolean accountingRuleChanged, final AccountingRuleType accountingRuleType, final boolean enableIncomeCapitalization,
final boolean enableBuyDownFee) {
final boolean enableBuyDownFee, final boolean merchantBuyDownFee) {
/***
* Variable tracks all accounting mapping properties that have been updated
***/
@@ -390,7 +401,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
} /*** else examine and update individual changes ***/
else {
this.loanProductToGLAccountMappingHelper.handleChangesToLoanProductToGLAccountMappings(loanProductId, changes, element,
accountingRuleType, enableIncomeCapitalization, enableBuyDownFee);
accountingRuleType, enableIncomeCapitalization, enableBuyDownFee, merchantBuyDownFee);
this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
@@ -554,7 +554,8 @@ public class LoanScheduleAssembler {
loanProduct.getLoanProductRelatedDetail().isEnableBuyDownFee(),
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeCalculationType(),
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeStrategy(),
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeIncomeType());
loanProduct.getLoanProductRelatedDetail().getBuyDownFeeIncomeType(),
loanProduct.getLoanProductRelatedDetail().isMerchantBuyDownFee());
}
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -445,7 +445,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
loan.getOfficeId(), loan.getCurrencyCode(), loan.getSummary().getTotalInterestCharged(),
loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), false,
false, false, null, false, newTransactionDTOs);
false, false, null, false, newTransactionDTOs, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee());
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
@@ -327,6 +327,8 @@ public class LoanProductAssembler {
.enumValueOfParameterNamed(LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanBuyDownFeeStrategy.class);
final LoanBuyDownFeeIncomeType buyDownFeeIncomeType = command
.enumValueOfParameterNamed(LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME, LoanBuyDownFeeIncomeType.class);
final boolean merchantBuyDownFee = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME);
return new LoanProduct(fund, loanTransactionProcessingStrategy, loanProductPaymentAllocationRules, loanProductCreditAllocationRules,
name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod,
@@ -349,7 +351,8 @@ public class LoanProductAssembler {
repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength,
enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate,
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
capitalizedIncomeType, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee);
}
@@ -359,6 +359,13 @@ public class LoanProductRelatedDetailUpdateUtil {
loanRepaymentScheduleDetail.setBuyDownFeeIncomeType(newValue);
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME,
loanRepaymentScheduleDetail.isMerchantBuyDownFee())) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME);
actualChanges.put(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME, newValue);
loanRepaymentScheduleDetail.setMerchantBuyDownFee(newValue);
}
return actualChanges;
}
@@ -847,9 +847,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " l.enable_income_capitalization as enableIncomeCapitalization, "
+ " l.capitalized_income_calculation_type as capitalizedIncomeCalculationType, "
+ " l.capitalized_income_strategy as capitalizedIncomeStrategy, "
+ " l.capitalized_income_type as capitalizedIncomeType, " //
+ " l.enable_buy_down_fee as enableBuyDownFee, " + " l.buy_down_fee_calculation_type as buyDownFeeCalculationType, "
+ " l.buy_down_fee_strategy as buyDownFeeStrategy, " + " l.buy_down_fee_income_type as buyDownFeeIncomeType, "
+ " l.capitalized_income_type as capitalizedIncomeType, l.is_merchant_buy_down_fee as merchantBuyDownFee, " //
+ " l.enable_buy_down_fee as enableBuyDownFee, l.buy_down_fee_calculation_type as buyDownFeeCalculationType, "
+ " l.buy_down_fee_strategy as buyDownFeeStrategy, l.buy_down_fee_income_type as buyDownFeeIncomeType, "
+ " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement, "
+ " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap, "
+ " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, "
@@ -1246,6 +1246,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
rs.getString("buyDownFeeStrategy"));
final StringEnumOptionData buyDownFeeIncomeType = ApiFacingEnum.getStringEnumOptionData(LoanBuyDownFeeIncomeType.class,
rs.getString("buyDownFeeIncomeType"));
final boolean merchantBuyDownFee = rs.getBoolean("merchantBuyDownFee");
return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName,
clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription,
@@ -1267,7 +1268,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
loanScheduleProcessingType.asEnumOptionData(), fixedLength, chargeOffBehaviour.getValueAsStringEnumOptionData(),
interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization,
capitalizedIncomeCalculationType, capitalizedIncomeStrategy, capitalizedIncomeType, enableBuyDownFee,
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType, merchantBuyDownFee);
}
}
@@ -201,7 +201,8 @@ public final class LoanProductDataValidator {
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,
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue()));
LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(),
LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -2838,14 +2839,21 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter(LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME).value(buyDownFeeIncomeType)
.isOneOfEnumValues(LoanBuyDownFeeIncomeType.class)
.cantBeBlankWhenParameterProvidedIs(LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, true);
Boolean merchantBuyDownFee = this.fromApiJsonHelper
.extractBooleanNamed(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME, element);
baseDataValidator.reset().parameter(LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME).value(merchantBuyDownFee)
.ignoreIfNull().validateForBooleanValue();
// Accounting
if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) {
final Long deferredIncomeLiabilityAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue())
.value(deferredIncomeLiabilityAccountId).notNull()
.cantBeBlankWhenParameterProvidedIs(LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, true)
.integerGreaterThanZero();
if (merchantBuyDownFee) {
final Long deferredIncomeLiabilityAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue())
.value(deferredIncomeLiabilityAccountId).notNull()
.cantBeBlankWhenParameterProvidedIs(LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, true)
.integerGreaterThanZero();
}
final Long incomeFromDeferredIncomeAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_BUY_DOWN.getValue())
@@ -299,7 +299,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lp.enable_income_capitalization as enableIncomeCapitalization, " //
+ "lp.capitalized_income_calculation_type as capitalizedIncomeCalculationType, " //
+ "lp.capitalized_income_strategy as capitalizedIncomeStrategy, " //
+ "lp.capitalized_income_type as capitalizedIncomeType, " //
+ "lp.capitalized_income_type as capitalizedIncomeType, lp.is_merchant_buy_down_fee as merchantBuyDownFee, " //
+ "lp.enable_buy_down_fee as enableBuyDownFee, " + "lp.buy_down_fee_calculation_type as buyDownFeeCalculationType, "
+ "lp.buy_down_fee_strategy as buyDownFeeStrategy, " + "lp.buy_down_fee_income_type as buyDownFeeIncomeType "
+ " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id "
@@ -579,6 +579,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
rs.getString("buyDownFeeStrategy"));
final StringEnumOptionData buyDownFeeIncomeType = ApiFacingEnum.getStringEnumOptionData(LoanBuyDownFeeIncomeType.class,
rs.getString("buyDownFeeIncomeType"));
final boolean merchantBuyDownFee = rs.getBoolean("merchantBuyDownFee");
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -604,7 +605,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes,
loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate,
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
capitalizedIncome, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType);
capitalizedIncome, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
merchantBuyDownFee);
}
}
@@ -263,6 +263,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
enableIncomeCapitalization = (boolean) changes.get(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME);
}
boolean enableBuyDownFee = product.getLoanProductRelatedDetail().isEnableBuyDownFee();
boolean merchantBuyDownFee = product.getLoanProductRelatedDetail().isMerchantBuyDownFee();
if (changes.containsKey(LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME)) {
enableBuyDownFee = (boolean) changes.get(LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME);
}
@@ -271,7 +272,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
final boolean accountingTypeChanged = changes.containsKey("accountingRule");
final Map<String, Object> accountingMappingChanges = this.accountMappingWritePlatformService
.updateLoanProductToGLAccountMapping(product.getId(), command, accountingTypeChanged, product.getAccountingRule(),
enableIncomeCapitalization, enableBuyDownFee);
enableIncomeCapitalization, enableBuyDownFee, merchantBuyDownFee);
changes.putAll(accountingMappingChanges);
if (changes.containsKey(LoanProductConstants.RATES_PARAM_NAME)) {
@@ -72,7 +72,8 @@ class CreateJournalEntriesForChargeOffLoanTest {
transactionType, new BigDecimal("500.00"), new BigDecimal("500.00"), null, null, null, null, false, Collections.emptyList(),
Collections.emptyList(), false, "", null, null, null, null);
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false);
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false,
false);
}
@Test
@@ -91,7 +91,7 @@ public class DefaultScheduledDateGeneratorTest {
DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO,
EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE,
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false, null, false, null, null,
null, false, null, null, null);
null, false, null, null, null, false);
// when
List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate,
@@ -172,7 +172,7 @@ public class DefaultScheduledDateGeneratorTest {
EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null,
null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false,
null, false, DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null,
false, null, false, null, null, null, false, null, null, null);
false, null, false, null, null, null, false, null, null, null, false);
}
private HolidayDetailDTO createHolidayDTO() {
@@ -271,7 +271,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()).loanScheduleType("PROGRESSIVE")
.loanScheduleProcessingType("HORIZONTAL").enableBuyDownFee(true)
.loanScheduleProcessingType("HORIZONTAL").enableBuyDownFee(true).merchantBuyDownFee(true)
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).locale("en").dateFormat("dd MMMM yyyy");
@@ -496,7 +496,7 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
.enableBuyDownFee(true).buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()));
@@ -865,4 +865,44 @@ public class LoanBuyDownFeeTest extends BaseLoanIntegrationTest {
});
}
@Test
public void testBuyDownFeeForNonMerchant() {
runAt("10 September 2024", () -> {
deleteAllExternalEvents();
// Add initial buy down fee
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(createProgressiveLoanProductWithBuyDownFee().merchantBuyDownFee(false));
// Apply for the loan with proper progressive loan settings
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId,
loanProductsResponse.getResourceId(), "01 September 2024", 1000.0, 10.0, 12, null));
loanId = postLoansResponse.getLoanId();
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 September 2024"));
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 September 2024");
final Long buyDownFeeTransactionId = addBuyDownFeeForLoan(loanId, 400.0, "10 September 2024");
assertNotNull(buyDownFeeTransactionId);
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 September 2024"), //
transaction(400.0, "Buy Down Fee", "10 September 2024"));
// Verify initial buy down fee (non merchant) accounting entries
verifyTRJournalEntries(buyDownFeeTransactionId, debit(fundSource, 400.0), credit(deferredIncomeLiabilityAccount, 400.0));
// Reverse Buy Down Fee (non merchant)
PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.reverseLoanTransaction(loanId,
buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN)
.transactionDate("10 September 2024").note("Buy Down Fee reversed").transactionAmount(0.0).locale("en"));
Assertions.assertNotNull(transactionsResponse);
Assertions.assertNotNull(transactionsResponse.getResourceId());
Assertions.assertEquals(transactionsResponse.getResourceId(), buyDownFeeTransactionId);
// Verify initial buy down fee (non merchant) reversed accounting entries
verifyTRJournalEntries(buyDownFeeTransactionId, debit(fundSource, 400.0), credit(deferredIncomeLiabilityAccount, 400.0),
credit(fundSource, 400.0), debit(deferredIncomeLiabilityAccount, 400.0));
});
}
}
@@ -259,7 +259,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
.enableBuyDownFee(true).buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()));
final GetLoanProductsProductIdResponse loanProductsProductIdResponse = loanProductHelper
@@ -339,7 +339,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
.enableBuyDownFee(true).buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()));
final GetLoanProductsProductIdResponse loanProductsProductIdResponse = loanProductHelper
@@ -404,7 +404,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())));
}
@@ -414,7 +414,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
() -> loanProductHelper.createLoanProduct(create4IProgressive().enableBuyDownFee(true)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())));
}
@@ -424,7 +424,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
() -> loanProductHelper.createLoanProduct(create4IProgressive().enableBuyDownFee(true)
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())));
}
@@ -434,7 +434,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
() -> loanProductHelper.createLoanProduct(create4IProgressive().enableBuyDownFee(true)
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())));
}
@@ -444,7 +444,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
() -> loanProductHelper.createLoanProduct(create4IProgressive().enableBuyDownFee(true)
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).merchantBuyDownFee(true)
.incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue())));
}
@@ -454,7 +454,7 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
() -> loanProductHelper.createLoanProduct(create4IProgressive().enableBuyDownFee(true)
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)
.buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE)
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE).merchantBuyDownFee(true)
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())));
}
}
@@ -206,7 +206,7 @@ public class LoanTransactionTest extends BaseLoanIntegrationTest {
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()
.enableBuyDownFee(true).buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION)//
.buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT)//
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.INTEREST)//
.buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.INTEREST).merchantBuyDownFee(true)//
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) //
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) //
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE) //
@@ -167,6 +167,7 @@ public class LoanProductTestBuilder {
private String chargeOffBehaviour;
private boolean interestRecognitionOnDisbursementDate = false;
private Boolean enableBuyDownFee = false;
private Boolean merchantBuyDownFee = false;
private String buyDownFeeCalculationType;
public String build() {
@@ -347,8 +348,8 @@ public class LoanProductTestBuilder {
map.put("enableBuyDownFee", this.enableBuyDownFee);
}
if (this.buyDownFeeCalculationType != null) {
map.put("buyDownFeeCalculationType", this.buyDownFeeCalculationType);
if (this.merchantBuyDownFee != null) {
map.put("merchantBuyDownFee", this.merchantBuyDownFee);
}
return map;
@@ -921,6 +922,11 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder withMerchantBuyDownFee(final Boolean merchantBuyDownFee) {
this.merchantBuyDownFee = merchantBuyDownFee;
return this;
}
public LoanProductTestBuilder withBuyDownFeeCalculationType(final String buyDownFeeCalculationType) {
this.buyDownFeeCalculationType = buyDownFeeCalculationType;
return this;