FINERACT-2374: Advance Accounting rule for classification type

This commit is contained in:
Jose Alberto Hernandez
2025-09-30 12:27:45 +02:00
committed by Adam Saghy
parent 6d3e5fa150
commit cb70555485
31 changed files with 1023 additions and 66 deletions
@@ -48,4 +48,6 @@ public class LoanDTO {
private Long chargeOffReasonCodeValue;
private boolean markedAsWrittenOff;
private boolean merchantBuyDownFee;
private List<AdvancedMappingtDTO> buydownFeeAdvancedMappingData;
private List<AdvancedMappingtDTO> capitalizedIncomeAdvancedMappingData;
}
@@ -35,6 +35,7 @@ import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsFor
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForShares;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
@@ -171,7 +172,9 @@ public class AccountingProcessorHelper {
return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee);
chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff, merchantBuyDownFee,
accountingBridgeData.getBuydownFeeClassificationCodeValue(),
accountingBridgeData.getCapitalizedIncomeClassificationCodeValue());
}
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
@@ -179,6 +182,16 @@ public class AccountingProcessorHelper {
return accountMappingRepository.findChargeOffReasonMapping(loanProductId, productType.getValue(), chargeOffReasonId);
}
public ProductToGLAccountMapping getClassificationMappingByCodeValue(Long loanProductId, PortfolioProductType productType,
final Long classificationId, final String classificationType) {
if (LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue().equals(classificationType)) {
return accountMappingRepository.findBuydownFeeClassificationMapping(loanProductId, productType.getValue(), classificationId);
} else {
return accountMappingRepository.findCapitalizedIncomeClassificationMapping(loanProductId, productType.getValue(),
classificationId);
}
}
public SavingsDTO populateSavingsDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
final boolean accrualBasedAccountingEnabled) {
final Long loanId = (Long) accountingBridgeData.get("savingsId");
@@ -472,6 +485,14 @@ public class AccountingProcessorHelper {
transactionId, transactionDate, amount);
}
public void createJournalEntriesForLoan(final Office office, final String currencyCode, final Integer accountTypeToBeDebited,
final GLAccount accountToBeCredited, final Long loanProductId, final Long paymentTypeId, final Long loanId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
int accountTypeToDebitId = accountTypeToBeDebited;
createJournalEntriesForLoan(office, currencyCode, accountTypeToDebitId, accountToBeCredited, loanProductId, paymentTypeId, loanId,
transactionId, transactionDate, amount);
}
public void createSplitJournalEntriesForLoan(Office office, String currencyCode, List<JournalAmountHolder> splitAccountsHolder,
JournalAmountHolder totalAccountHolder, Long loanProductId, Long paymentTypeId, Long loanId, String transactionId,
LocalDate transactionDate) {
@@ -534,6 +555,14 @@ public class AccountingProcessorHelper {
createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount);
}
private void createJournalEntriesForLoan(final Office office, final String currencyCode, final int accountTypeToDebitId,
final GLAccount creditAccount, final Long loanProductId, final Long paymentTypeId, final Long loanId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final GLAccount debitAccount = getLinkedGLAccountForLoanProduct(loanProductId, accountTypeToDebitId, paymentTypeId);
createDebitJournalEntryForLoan(office, currencyCode, debitAccount, loanId, transactionId, transactionDate, amount);
createCreditJournalEntryForLoan(office, currencyCode, creditAccount, loanId, transactionId, transactionDate, amount);
}
private void createJournalEntriesForSavings(final Office office, final String currencyCode, final int accountTypeToDebitId,
final int accountTypeToCreditId, final Long savingsProductId, final Long paymentTypeId, final Long savingsId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
@@ -29,7 +29,9 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO;
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
@@ -296,17 +298,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final List<AdvancedMappingtDTO> classificationCodeValues = loanDTO.getCapitalizedIncomeAdvancedMappingData();
// interest payment
final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
: AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION;
if (MathUtil.isGreaterThanZero(interestAmount)) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
if (classificationCodeValues.isEmpty()) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
} else {
classificationCodeValues.stream().forEach(classificationCodeValue -> {
ProductToGLAccountMapping mapping = null;
if (classificationCodeValue.getReferenceValueId() != null) {
mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(),
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue());
}
if (mapping == null) {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId,
creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
});
}
}
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
if (classificationCodeValues.isEmpty()) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
} else {
classificationCodeValues.stream().forEach(classificationCodeValue -> {
ProductToGLAccountMapping mapping = null;
if (classificationCodeValue.getReferenceValueId() != null) {
mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(),
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue());
}
if (mapping == null) {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId,
creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
});
}
}
// create credit entries
@@ -327,6 +369,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
private ProductToGLAccountMapping fetchAdvanceAccountingMappingForCodeValue(final Long loanProductId, final Long codeValueId,
final String codeName) {
return helper.getClassificationMappingByCodeValue(loanProductId, PortfolioProductType.LOAN, codeValueId, codeName);
}
private void createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
@@ -493,17 +540,57 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final List<AdvancedMappingtDTO> classificationCodeValues = loanDTO.getBuydownFeeAdvancedMappingData();
// interest payment
final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
: AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN;
if (MathUtil.isGreaterThanZero(interestAmount)) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
if (classificationCodeValues.isEmpty()) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
} else {
classificationCodeValues.stream().forEach(classificationCodeValue -> {
ProductToGLAccountMapping mapping = null;
if (classificationCodeValue.getReferenceValueId() != null) {
mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(),
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue());
}
if (mapping == null) {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId,
creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
});
}
}
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
if (classificationCodeValues.isEmpty()) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
} else {
classificationCodeValues.stream().forEach(classificationCodeValue -> {
ProductToGLAccountMapping mapping = null;
if (classificationCodeValue.getReferenceValueId() != null) {
mapping = fetchAdvanceAccountingMappingForCodeValue(loanProductId, classificationCodeValue.getReferenceValueId(),
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue());
}
if (mapping == null) {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId,
creditAccountType.getValue(), AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, classificationCodeValue.getAmount(), paymentTypeId, mapping.getGlAccount(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
});
}
}
// create credit entries
@@ -800,6 +887,17 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
private void populateCreditDebitMaps(Long loanProductId, BigDecimal transactionPartAmount, Long paymentTypeId, GLAccount accountCredit,
Integer debitAccountType, GLAccountBalanceHolder glAccountBalanceHolder) {
if (MathUtil.isGreaterThanZero(transactionPartAmount)) {
// Resolve Credit
glAccountBalanceHolder.addToCredit(accountCredit, transactionPartAmount);
// Resolve Debit
GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, debitAccountType, paymentTypeId);
glAccountBalanceHolder.addToDebit(accountDebit, transactionPartAmount);
}
}
private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
if (isMarkedAsChargeOff) {
@@ -44,6 +44,7 @@ import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformSer
import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams;
import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand;
import org.apache.fineract.accounting.journalentry.command.SingleDebitOrCreditEntryCommand;
import org.apache.fineract.accounting.journalentry.data.AdvancedMappingtDTO;
import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.SavingsDTO;
@@ -61,6 +62,7 @@ import org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry;
import org.apache.fineract.accounting.rule.domain.AccountingRule;
import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository;
import org.apache.fineract.accounting.rule.exception.AccountingRuleNotFoundException;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -90,11 +92,14 @@ import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
@@ -128,6 +133,8 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
private final ConfigurationReadPlatformService configurationReadPlatformService;
private final AccountingService accountingService;
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
private final LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository;
private final LoanTransactionRepository loanTransactionRepository;
@Transactional
@Override
@@ -857,11 +864,39 @@ public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements Journa
}
}
List<AdvancedMappingtDTO> buydownFeeAdvancedMappingData = null;
List<AdvancedMappingtDTO> capitalizedIncomeAdvancedMappingData = null;
if (loanTransaction.isBuyDownFeeAmortization()) {
buydownFeeAdvancedMappingData = getLoanTransactionClassificationId(loanTransaction);
} else if (loanTransaction.isCapitalizedIncomeAmortization()) {
capitalizedIncomeAdvancedMappingData = getLoanTransactionClassificationId(loanTransaction);
}
return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode,
loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
isAccountTransfer, wasChargedOffAtTransactionTime, loan.isFraud(), loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
transactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee());
transactions, loan.getLoanProductRelatedDetail().isMerchantBuyDownFee(), buydownFeeAdvancedMappingData,
capitalizedIncomeAdvancedMappingData);
}
private List<AdvancedMappingtDTO> getLoanTransactionClassificationId(final LoanTransaction loanTransaction) {
List<AdvancedMappingtDTO> advancedMappingData = new ArrayList<AdvancedMappingtDTO>();
if (loanTransaction.isCapitalizedIncomeAmortization() || loanTransaction.isBuyDownFeeAmortization()) {
final List<LoanAmortizationAllocationMapping> loanTransactionAllocations = loanAmortizationAllocationMappingRepository
.fetchLoanTransactionAllocationByAmortizationLoanTransactionId(loanTransaction.getId(),
loanTransaction.getLoan().getId());
loanTransactionAllocations.stream().forEach(loanTransactionAllocation -> {
final CodeValue classification = loanTransactionRepository
.fetchClassificationCodeValueByTransactionId(loanTransactionAllocation.getBaseLoanTransactionId());
if (classification != null) {
advancedMappingData.add(new AdvancedMappingtDTO(classification.getId(), loanTransactionAllocation.getAmount()));
} else {
advancedMappingData.add(new AdvancedMappingtDTO(null, loanTransactionAllocation.getAmount()));
}
});
}
return advancedMappingData;
}
/**
@@ -49,6 +49,8 @@ import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.service.OfficeReadPlatformService;
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAmortizationAllocationMappingRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@@ -94,12 +96,14 @@ public class AccountingJournalEntryConfiguration {
FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper,
CashBasedAccountingProcessorForClientTransactions accountingProcessorForClientTransactions,
ConfigurationReadPlatformService configurationReadPlatformService, AccountingService accountingService,
ExternalAssetOwnerRepository externalAssetOwnerRepository) {
ExternalAssetOwnerRepository externalAssetOwnerRepository,
LoanAmortizationAllocationMappingRepository loanAmortizationAllocationMappingRepository,
LoanTransactionRepository loanTransactionRepository) {
return new JournalEntryWritePlatformServiceJpaRepositoryImpl(glClosureRepository, glAccountRepository, glJournalEntryRepository,
officeRepositoryWrapper, accountingProcessorForLoanFactory, accountingProcessorForSavingsFactory,
accountingProcessorForSharesFactory, helper, fromApiJsonDeserializer, accountingRuleRepository,
glAccountReadPlatformService, organisationCurrencyRepository, context, paymentDetailWritePlatformService,
financialActivityAccountRepositoryWrapper, accountingProcessorForClientTransactions, configurationReadPlatformService,
accountingService, externalAssetOwnerRepository);
accountingService, externalAssetOwnerRepository, loanAmortizationAllocationMappingRepository, loanTransactionRepository);
}
}
@@ -140,6 +140,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
loanProductId, null);
break;
case ACCRUAL_UPFRONT:
// Fall Through
@@ -233,6 +237,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
loanProductId, null);
break;
}
}
@@ -411,6 +419,10 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
changes);
this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId,
changes);
this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
loanProductId, changes);
}
return changes;
}
@@ -1,130 +0,0 @@
/**
* 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.infrastructure.codes.api;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Created by sanyam on 30/7/17.
*/
final class CodeValuesApiResourceSwagger {
private CodeValuesApiResourceSwagger() {
}
@Schema(description = "GetCodeValuesDataResponse")
public static final class GetCodeValuesDataResponse {
private GetCodeValuesDataResponse() {
}
@Schema(example = "1")
public Long id;
@Schema(example = "Passport")
public String name;
@Schema(example = "Passport information")
public String description;
@Schema(example = "0")
public Integer position;
}
@Schema(description = "PostCodeValuesDataRequest")
public static final class PostCodeValuesDataRequest {
private PostCodeValuesDataRequest() {
}
@Schema(example = "Passport")
public String name;
@Schema(example = "true")
public Boolean isActive;
@Schema(example = "Passport information")
public String description;
@Schema(example = "0")
public Integer position;
}
@Schema(description = "PostCodeValueDataResponse")
public static final class PostCodeValueDataResponse {
private PostCodeValueDataResponse() {
}
@Schema(example = "4")
public Long resourceId;
@Schema(example = "4")
public Long subResourceId;
}
@Schema(description = "PutCodeValuesDataRequest")
public static final class PutCodeValuesDataRequest {
private PutCodeValuesDataRequest() {
}
@Schema(example = "Passport")
public String name;
@Schema(example = "Passport information")
public String description;
@Schema(example = "0")
public Integer position;
@Schema(example = "true")
public Boolean isActive;
}
@Schema(description = "PutCodeValueDataResponse")
public static final class PutCodeValueDataResponse {
private PutCodeValueDataResponse() {
}
private static final class PutCodeValuechangesSwagger {
private PutCodeValuechangesSwagger() {}
@Schema(example = "Passport")
public String name;
@Schema(example = "Passport information")
public String description;
@Schema(example = "0")
public Integer position;
}
@Schema(example = "4")
public Long resourceId;
public PutCodeValuechangesSwagger changes;
}
@Schema(description = "DeleteCodeValueDataResponse")
public static final class DeleteCodeValueDataResponse {
private DeleteCodeValueDataResponse() {
}
@Schema(example = "4")
public Long resourceId;
}
}
@@ -1,110 +0,0 @@
/**
* 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.infrastructure.codes.api;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* Created by sanyam on 29/7/17.
*/
final class CodesApiResourceSwagger {
private CodesApiResourceSwagger() {
// this class is only for Swagger Live Documentation
}
@Schema(description = "GetCodesResponse")
public static final class GetCodesResponse {
private GetCodesResponse() {
}
@Schema(example = "1")
public Long id;
@Schema(example = "Education")
public String name;
@Schema(example = "true")
public boolean systemDefined;
}
@Schema(description = "PostCodesRequest")
public static final class PostCodesRequest {
private PostCodesRequest() {
}
@Schema(example = "MyNewCode")
public String name;
}
@Schema(description = "PostCodesResponse")
public static final class PostCodesResponse {
private PostCodesResponse() {
}
@Schema(example = "4")
public Long resourceId;
}
@Schema(description = "PutCodesRequest")
public static final class PutCodesRequest {
private PutCodesRequest() {
}
@Schema(example = "MyNewCode(changed)")
public String name;
}
@Schema(description = "PutCodesResponse")
public static final class PutCodesResponse {
private PutCodesResponse() {
}
private static final class PutCodesApichangesSwagger {
private PutCodesApichangesSwagger() {}
@Schema(example = "MyNewCode(changed)")
public String name;
}
@Schema(example = "4")
public Long resourceId;
public PutCodesApichangesSwagger changes;
}
@Schema(description = "DeleteCodesResponse")
public static final class DeleteCodesResponse {
private DeleteCodesResponse() {
}
@Schema(example = "4")
public Long resourceId;
}
}
@@ -46,10 +46,12 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.common.AccountingDropdownReadPlatformService;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService;
import org.apache.fineract.commands.domain.CommandWrapper;
@@ -82,6 +84,7 @@ import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlat
import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -352,6 +355,8 @@ public class LoanProductsApiResource {
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
if (loanProduct.hasAccountingEnabled()) {
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
loanProduct.getAccountingRule().getId().intValue());
@@ -362,8 +367,14 @@ public class LoanProductsApiResource {
.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId);
chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService
.fetchChargeOffReasonMappingsForLoanProduct(productId);
capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService
.fetchClassificationMappingsForLoanProduct(productId,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
buydowFeeClassificationToGLAccountMappings = accountMappingReadPlatformService.fetchClassificationMappingsForLoanProduct(
productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings,
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings);
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings,
capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings);
}
if (settings.isTemplate()) {
@@ -464,6 +475,10 @@ public class LoanProductsApiResource {
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
final List<CodeValueData> buydownFeeClassificationOptions = codeValueReadPlatformService
.retrieveCodeValuesByCode(LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE);
return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions,
interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions,
@@ -477,7 +492,8 @@ public class LoanProductsApiResource {
LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes,
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions,
daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions,
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions);
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions,
capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
}
}
@@ -49,6 +49,7 @@ import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanTransactionApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -202,7 +203,10 @@ public final class LoanProductDataValidator {
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(),
LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME));
LoanProductConstants.MERCHANT_BUY_DOWN_FEE_PARAM_NAME,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue(), //
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS.getValue() //
));
private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName,
@@ -744,7 +748,10 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(baseDataValidator, element);
validateClassificationToIncomeMappings(baseDataValidator, element,
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
validateClassificationToIncomeMappings(baseDataValidator, element,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
}
if (AccountingValidations.isAccrualBasedAccounting(accountingRuleType)) {
@@ -1877,6 +1884,10 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(baseDataValidator, element);
validateClassificationToIncomeMappings(baseDataValidator, element,
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
validateClassificationToIncomeMappings(baseDataValidator, element,
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
validateMinMaxConstraints(element, baseDataValidator, loanProduct);
@@ -2109,6 +2120,65 @@ public final class LoanProductDataValidator {
}
}
private void validateClassificationToIncomeMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element,
final LoanProductAccountingParams classificationParameter) {
String parameterName = classificationParameter.getValue();
if (this.fromApiJsonHelper.parameterExists(parameterName, element)) {
final JsonArray classificationToIncomeMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
if (classificationToIncomeMappingArray != null && classificationToIncomeMappingArray.size() > 0) {
Map<Long, Set<Long>> classificationToAccounts = new HashMap<>();
List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method
int i = 0;
do {
final JsonObject jsonObject = classificationToIncomeMappingArray.get(i).getAsJsonObject();
final Long incomeGlAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(), jsonObject);
final Long classificationCodeValueId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue(), jsonObject);
// Validate parameters locally
baseDataValidator.reset()
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
+ LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue())
.value(incomeGlAccountId).notNull().integerGreaterThanZero();
baseDataValidator.reset()
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
+ LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue())
.value(classificationCodeValueId).notNull().integerGreaterThanZero();
// Handle duplicate classification and GL Account validation
classificationToAccounts.putIfAbsent(classificationCodeValueId, new HashSet<>());
Set<Long> associatedAccounts = classificationToAccounts.get(classificationCodeValueId);
if (associatedAccounts.contains(incomeGlAccountId)) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("duplicate.classification.and.glAccount");
}
associatedAccounts.add(incomeGlAccountId);
if (associatedAccounts.size() > 1) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("multiple.glAccounts.for.classification");
}
// Collect mapping for additional validations
processedMappings.add(jsonObject);
i++;
} while (i < classificationToIncomeMappingArray.size());
// Call the new validation method for additional checks
final String dataCodeName = classificationParameter
.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)
? LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE
: LoanTransactionApiConstants.BUY_DOWN_FEE_CLASSIFICATION_CODE;
productToGLAccountMappingHelper.validateClassificationMappingsInDatabase(processedMappings, dataCodeName);
}
}
}
public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator,
final LoanProduct loanProduct) {
validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator);
@@ -216,4 +216,5 @@
<include file="parts/0195_create_loan_amortization_allocation_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/0196_add_deleted_and_closed_to_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0197_add_deleted_and_closed_to_capitalized_income_balance.xml" relativeToChangelogFile="true" />
<include file="parts/0198_add_classification_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,52 @@
<?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 id="1" author="fineract">
<addColumn tableName="acc_product_mapping">
<column name="capitalized_income_classification_id" type="INT" defaultValueNumeric="NULL">
<constraints nullable="true"/>
</column>
</addColumn>
<addForeignKeyConstraint
baseTableName="acc_product_mapping"
baseColumnNames="capitalized_income_classification_id"
referencedTableName="m_code_value"
referencedColumnNames="id"
constraintName="fk_acc_product_mapping_capitalized_income_classification"/>
</changeSet>
<changeSet id="2" author="fineract">
<addColumn tableName="acc_product_mapping">
<column name="buydown_fee_classification_id" type="INT" defaultValueNumeric="NULL">
<constraints nullable="true"/>
</column>
</addColumn>
<addForeignKeyConstraint
baseTableName="acc_product_mapping"
baseColumnNames="buydown_fee_classification_id"
referencedTableName="m_code_value"
referencedColumnNames="id"
constraintName="fk_acc_product_mapping_buydown_fee_classification"/>
</changeSet>
</databaseChangeLog>
@@ -73,7 +73,7 @@ class CreateJournalEntriesForChargeOffLoanTest {
Collections.emptyList(), false, "", null, null, null, null);
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasonId, false,
false);
false, null, null);
}
@Test