FINERACT-2113: Advanced Charge-off Expense Accounting - Add new "Advanced Accounting Rule"

This commit is contained in:
Andrii Kulminskyi
2024-11-21 17:18:06 +01:00
committed by Adam Saghy
parent 75fa740ff6
commit 4ccb33a95e
18 changed files with 621 additions and 14 deletions
@@ -130,6 +130,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
// advanced accounting mappings
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
break;
case ACCRUAL_UPFRONT:
// Fall Through
@@ -208,6 +209,7 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
// advanced accounting mappings
this.loanProductToGLAccountMappingHelper.savePaymentChannelToFundSourceMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargesToIncomeAccountMappings(command, element, loanProductId, null);
this.loanProductToGLAccountMappingHelper.saveChargeOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
break;
}
}
@@ -379,6 +381,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
accountingRuleType);
this.loanProductToGLAccountMappingHelper.updatePaymentChannelToFundSourceMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
changes);
}
return changes;
}
@@ -160,7 +160,7 @@ public class LoanProductsApiResource {
@Operation(summary = "Create a Loan Product", description = "Depending of the Accounting Rule (accountingRule) selected, additional fields with details of the appropriate Ledger Account identifiers would need to be passed in.\n"
+ "\n" + "Refer MifosX Accounting Specs Draft for more details regarding the significance of the selected accounting rule\n\n"
+ "Mandatory Fields: name, shortName, currencyCode, digitsAfterDecimal, inMultiplesOf, principal, numberOfRepayments, repaymentEvery, repaymentFrequencyType, interestRatePerPeriod, interestRateFrequencyType, amortizationType, interestType, interestCalculationPeriodType, transactionProcessingStrategyCode, accountingRule, isInterestRecalculationEnabled, daysInYearType, daysInMonthType\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
+ "Optional Fields: inArrearsTolerance, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, graceOnArrearsAgeing, charges, paymentChannelToFundSourceMappings, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, chargeOffReasonsToExpenseMappings, includeInBorrowerCycle, useBorrowerCycle,principalVariationsForBorrowerCycle, numberOfRepaymentVariationsForBorrowerCycle, interestRateVariationsForBorrowerCycle, multiDisburseLoan,maxTrancheCount, outstandingLoanBalance,overdueDaysForNPA,holdGuaranteeFunds, principalThresholdForLastInstalment, accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineInstallmentAmount, installmentAmountInMultiplesOf, allowAttributeOverrides, allowPartialPeriodInterestCalcualtion,dueDaysForRepaymentEvent,overDueDaysForRepaymentEvent,enableDownPayment,disbursedAmountPercentageDownPayment,enableAutoRepaymentForDownPayment,repaymentStartDateType\n\n"
+ "Additional Mandatory Fields for Cash(2) based accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields for periodic (3) and upfront (4)accrual accounting: fundSourceAccountId, loanPortfolioAccountId, interestOnLoanAccountId, incomeFromFeeAccountId, incomeFromPenaltyAccountId, writeOffAccountId, receivableInterestAccountId, receivableFeeAccountId, receivablePenaltyAccountId, transfersInSuspenseAccountId, overpaymentLiabilityAccountId\n\n"
+ "Additional Mandatory Fields if interest recalculation is enabled(true): interestRecalculationCompoundingMethod, rescheduleStrategyMethod, recalculationRestFrequencyType\n\n"
@@ -336,6 +336,7 @@ public class LoanProductsApiResource {
Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
List<ChargeToGLAccountMapper> chargeOffReasonsToExpenseMappings;
if (loanProduct.hasAccountingEnabled()) {
accountingMappings = this.accountMappingReadPlatformService.fetchAccountMappingDetailsForLoanProduct(productId,
loanProduct.getAccountingRule().getId().intValue());
@@ -27,6 +27,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -36,6 +37,7 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.common.AccountingValidations;
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingHelper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
@@ -141,7 +143,9 @@ public final class LoanProductDataValidator {
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(),
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(),
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(),
LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME,
LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue(),
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(),
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME,
LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
LoanProductConstants.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME,
LoanProductConstants.NUMBER_OF_REPAYMENT_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.SHORT_NAME,
@@ -195,6 +199,7 @@ public final class LoanProductDataValidator {
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
private final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
private final ProductToGLAccountMappingHelper productToGLAccountMappingHelper;
public void validateForCreate(final JsonCommand command) {
String json = command.json();
@@ -718,6 +723,7 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(baseDataValidator, element);
}
@@ -1791,6 +1797,7 @@ public final class LoanProductDataValidator {
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
validateChargeToIncomeAccountMappings(baseDataValidator, element);
validateChargeOffToExpenseMappings(baseDataValidator, element);
validateMinMaxConstraints(element, baseDataValidator, loanProduct);
@@ -1964,6 +1971,60 @@ public final class LoanProductDataValidator {
}
}
private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASONS_TO_EXPENSE.getValue();
if (this.fromApiJsonHelper.parameterExists(parameterName, element)) {
final JsonArray chargeOffToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
if (chargeOffToExpenseMappingArray != null && chargeOffToExpenseMappingArray.size() > 0) {
Map<Long, Set<Long>> chargeOffReasonToAccounts = new HashMap<>();
List<JsonObject> processedMappings = new ArrayList<>(); // Collect processed mappings for the new method
int i = 0;
do {
final JsonObject jsonObject = chargeOffToExpenseMappingArray.get(i).getAsJsonObject();
final Long expenseGlAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
final Long chargeOffReasonCodeValueId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(), jsonObject);
// Validate parameters locally
baseDataValidator.reset()
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
+ LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue())
.value(expenseGlAccountId).notNull().integerGreaterThanZero();
baseDataValidator.reset()
.parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT
+ LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue())
.value(chargeOffReasonCodeValueId).notNull().integerGreaterThanZero();
// Handle duplicate charge-off reason and GL Account validation
chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>());
Set<Long> associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId);
if (associatedAccounts.contains(expenseGlAccountId)) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("duplicate.chargeOffReason.and.glAccount");
}
associatedAccounts.add(expenseGlAccountId);
if (associatedAccounts.size() > 1) {
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
.failWithCode("multiple.glAccounts.for.chargeOffReason");
}
// Collect mapping for additional validations
processedMappings.add(jsonObject);
i++;
} while (i < chargeOffToExpenseMappingArray.size());
// Call the new validation method for additional checks
productToGLAccountMappingHelper.validateChargeOffMappingsInDatabase(processedMappings);
}
}
}
public void validateMinMaxConstraints(final JsonElement element, final DataValidatorBuilder baseDataValidator,
final LoanProduct loanProduct) {
validatePrincipalMinMaxConstraint(element, loanProduct, baseDataValidator);
@@ -171,4 +171,5 @@
<include file="parts/0150_transaction_summary_with_asset_owner_report_interest_waiver_interest_refund_added.xml" relativeToChangelogFile="true" />
<include file="parts/0151_interest_refund_business_events.xml" relativeToChangelogFile="true" />
<include file="parts/0152_update_password_validation_policy.xml" relativeToChangelogFile="true" />
<include file="parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,40 @@
<?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="charge_off_reason_id" type="INT" defaultValueNumeric="NULL">
<constraints nullable="true"/>
</column>
</addColumn>
<addForeignKeyConstraint
baseTableName="acc_product_mapping"
baseColumnNames="charge_off_reason_id"
referencedTableName="m_code_value"
referencedColumnNames="id"
constraintName="fk_acc_product_mapping_charge_off_reason"/>
</changeSet>
</databaseChangeLog>