FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason
This commit is contained in:
+4
@@ -140,6 +140,7 @@ 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.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
|
||||
loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
@@ -237,6 +238,7 @@ 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.saveWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveBuyDownFeeClassificationToIncomeAccountMappings(command, element,
|
||||
loanProductId, null);
|
||||
this.loanProductToGLAccountMappingHelper.saveCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
@@ -419,6 +421,8 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc
|
||||
this.loanProductToGLAccountMappingHelper.updateChargesToIncomeAccountMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateChargeOffReasonToExpenseAccountMappings(command, element, loanProductId,
|
||||
changes);
|
||||
|
||||
this.loanProductToGLAccountMappingHelper.updateWriteOffReasonToExpenseAccountMappings(command, element, loanProductId, changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateBuyDownFeeClassificationToIncomeAccountMappings(command, element, loanProductId,
|
||||
changes);
|
||||
this.loanProductToGLAccountMappingHelper.updateCapitalizedIncomeClassificationToIncomeAccountMappings(command, element,
|
||||
|
||||
+9
-2
@@ -53,6 +53,7 @@ import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReas
|
||||
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.data.WriteOffReasonsToExpenseAccountMapper;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.service.ProductToGLAccountMappingReadPlatformService;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.service.CommandWrapperBuilder;
|
||||
@@ -355,6 +356,7 @@ public class LoanProductsApiResource {
|
||||
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
|
||||
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
|
||||
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
|
||||
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappings;
|
||||
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
|
||||
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
|
||||
if (loanProduct.hasAccountingEnabled()) {
|
||||
@@ -367,6 +369,8 @@ public class LoanProductsApiResource {
|
||||
.fetchPenaltyToIncomeAccountMappingsForLoanProduct(productId);
|
||||
chargeOffReasonToGLAccountMappings = this.accountMappingReadPlatformService
|
||||
.fetchChargeOffReasonMappingsForLoanProduct(productId);
|
||||
writeOffReasonsToExpenseAccountMappings = this.accountMappingReadPlatformService
|
||||
.fetchWriteOffReasonMappingsForLoanProduct(productId);
|
||||
capitalizedIncomeClassificationToGLAccountMappings = accountMappingReadPlatformService
|
||||
.fetchClassificationMappingsForLoanProduct(productId,
|
||||
LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
@@ -374,7 +378,8 @@ public class LoanProductsApiResource {
|
||||
productId, LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
loanProduct = LoanProductData.withAccountingDetails(loanProduct, accountingMappings, paymentChannelToFundSourceMappings,
|
||||
feeToGLAccountMappings, penaltyToGLAccountMappings, chargeOffReasonToGLAccountMappings,
|
||||
capitalizedIncomeClassificationToGLAccountMappings, buydowFeeClassificationToGLAccountMappings);
|
||||
writeOffReasonsToExpenseAccountMappings, capitalizedIncomeClassificationToGLAccountMappings,
|
||||
buydowFeeClassificationToGLAccountMappings);
|
||||
}
|
||||
|
||||
if (settings.isTemplate()) {
|
||||
@@ -475,6 +480,8 @@ public class LoanProductsApiResource {
|
||||
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeStrategy.class);
|
||||
final List<StringEnumOptionData> buyDownFeeIncomeTypeOptions = ApiFacingEnum
|
||||
.getValuesAsStringEnumOptionDataList(LoanBuyDownFeeIncomeType.class);
|
||||
final List<CodeValueData> writeOffReasonOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanApiConstants.WRITEOFFREASONS);
|
||||
final List<CodeValueData> capitalizedIncomeClassificationOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanTransactionApiConstants.CAPITALIZED_INCOME_CLASSIFICATION_CODE);
|
||||
final List<CodeValueData> buydownFeeClassificationOptions = codeValueReadPlatformService
|
||||
@@ -493,7 +500,7 @@ public class LoanProductsApiResource {
|
||||
creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions,
|
||||
daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions,
|
||||
capitalizedIncomeTypeOptions, buyDownFeeCalculationTypeOptions, buyDownFeeStrategyOptions, buyDownFeeIncomeTypeOptions,
|
||||
capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
|
||||
writeOffReasonOptions, capitalizedIncomeClassificationOptions, buydownFeeClassificationOptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+50
-21
@@ -33,6 +33,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
|
||||
@@ -152,7 +153,9 @@ public final class LoanProductDataValidator {
|
||||
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(),
|
||||
LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(),
|
||||
LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
|
||||
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(),
|
||||
LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue(),
|
||||
LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID.getValue(),
|
||||
LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(),
|
||||
LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID.getValue(),
|
||||
LoanProductAccountingParams.INCOME_FROM_CAPITALIZATION.getValue(),
|
||||
LoanProductAccountingParams.DEFERRED_INCOME_LIABILITY.getValue(), LoanProductAccountingParams.BUY_DOWN_EXPENSE.getValue(),
|
||||
@@ -748,6 +751,7 @@ public final class LoanProductDataValidator {
|
||||
validatePaymentChannelFundSourceMappings(baseDataValidator, element);
|
||||
validateChargeToIncomeAccountMappings(baseDataValidator, element);
|
||||
validateChargeOffToExpenseMappings(baseDataValidator, element);
|
||||
validateWriteOffToExpenseMappings(baseDataValidator, element);
|
||||
validateClassificationToIncomeMappings(baseDataValidator, element,
|
||||
LoanProductAccountingParams.BUYDOWN_FEE_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS);
|
||||
validateClassificationToIncomeMappings(baseDataValidator, element,
|
||||
@@ -2068,54 +2072,79 @@ public final class LoanProductDataValidator {
|
||||
|
||||
private void validateChargeOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||
String parameterName = LoanProductAccountingParams.CHARGE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue();
|
||||
LoanProductAccountingParams reasonCodeValueId = LoanProductAccountingParams.CHARGE_OFF_REASON_CODE_VALUE_ID;
|
||||
String failCode = "chargeOffReason";
|
||||
validateAdditionalAccountMappings(baseDataValidator, element, parameterName, reasonCodeValueId, failCode,
|
||||
productToGLAccountMappingHelper::validateChargeOffMappingsInDatabase);
|
||||
}
|
||||
|
||||
private void validateWriteOffToExpenseMappings(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
|
||||
String parameterName = LoanProductAccountingParams.WRITE_OFF_REASON_TO_EXPENSE_ACCOUNT_MAPPINGS.getValue();
|
||||
LoanProductAccountingParams reasonCodeValueId = LoanProductAccountingParams.WRITE_OFF_REASON_CODE_VALUE_ID;
|
||||
String failCode = "writeOffReason";
|
||||
validateAdditionalAccountMappings(baseDataValidator, element, parameterName, reasonCodeValueId, failCode,
|
||||
productToGLAccountMappingHelper::validateWriteOffMappingsInDatabase);
|
||||
}
|
||||
|
||||
private void validateAdditionalAccountMappings(DataValidatorBuilder baseDataValidator, JsonElement element, String parameterName,
|
||||
LoanProductAccountingParams reasonCodeValueIdParam, String failCode,
|
||||
BiConsumer<List<ApiParameterError>, List<JsonObject>> additionalMappingValidator) {
|
||||
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<>();
|
||||
final JsonArray reasonToExpenseMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(parameterName, element);
|
||||
if (reasonToExpenseMappingArray != null && !reasonToExpenseMappingArray.isEmpty()) {
|
||||
Map<Long, Set<Long>> reasonToAccounts = 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);
|
||||
final JsonObject jsonObject = reasonToExpenseMappingArray.get(i).getAsJsonObject();
|
||||
|
||||
final String expenseGlAccountIdString = this.fromApiJsonHelper
|
||||
.extractStringNamed(LoanProductAccountingParams.EXPENSE_GL_ACCOUNT_ID.getValue(), jsonObject);
|
||||
final String reasonCodeValueIdString = this.fromApiJsonHelper.extractStringNamed(reasonCodeValueIdParam.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();
|
||||
.value(expenseGlAccountIdString).notNull().longGreaterThanZero();
|
||||
baseDataValidator.reset().parameter(
|
||||
parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET + DOT + reasonCodeValueIdParam.getValue())
|
||||
.value(reasonCodeValueIdString).notNull().longGreaterThanZero();
|
||||
|
||||
// Handle duplicate charge-off reason and GL Account validation
|
||||
chargeOffReasonToAccounts.putIfAbsent(chargeOffReasonCodeValueId, new HashSet<>());
|
||||
Set<Long> associatedAccounts = chargeOffReasonToAccounts.get(chargeOffReasonCodeValueId);
|
||||
final Long reasonCodeValueId = Long.valueOf(reasonCodeValueIdString);
|
||||
final Long expenseGlAccountId = Long.valueOf(expenseGlAccountIdString);
|
||||
// Handle duplicate reason and GL Account validation
|
||||
reasonToAccounts.putIfAbsent(reasonCodeValueId, new HashSet<>());
|
||||
Set<Long> associatedAccounts = reasonToAccounts.get(reasonCodeValueId);
|
||||
|
||||
if (associatedAccounts.contains(expenseGlAccountId)) {
|
||||
baseDataValidator.reset().parameter(parameterName + OPENING_SQUARE_BRACKET + i + CLOSING_SQUARE_BRACKET)
|
||||
.failWithCode("duplicate.chargeOffReason.and.glAccount");
|
||||
.failWithCode("duplicate." + failCode + ".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");
|
||||
.failWithCode("multiple.glAccounts.for." + failCode);
|
||||
}
|
||||
|
||||
// Collect mapping for additional validations
|
||||
processedMappings.add(jsonObject);
|
||||
|
||||
i++;
|
||||
} while (i < chargeOffToExpenseMappingArray.size());
|
||||
} while (i < reasonToExpenseMappingArray.size());
|
||||
|
||||
// Call the new validation method for additional checks
|
||||
productToGLAccountMappingHelper.validateChargeOffMappingsInDatabase(processedMappings);
|
||||
final List<ApiParameterError> validationErrors = new ArrayList<>();
|
||||
productToGLAccountMappingHelper.validateGLAccountInDatabase(validationErrors, processedMappings);
|
||||
if (additionalMappingValidator != null) {
|
||||
additionalMappingValidator.accept(validationErrors, processedMappings);
|
||||
}
|
||||
if (!validationErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException(validationErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -606,7 +606,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
|
||||
loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate,
|
||||
daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy,
|
||||
capitalizedIncome, enableBuyDownFee, buyDownFeeCalculationType, buyDownFeeStrategy, buyDownFeeIncomeType,
|
||||
merchantBuyDownFee);
|
||||
merchantBuyDownFee, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -217,4 +217,5 @@
|
||||
<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" />
|
||||
<include file="parts/0199_write_off_reason_mapping_loan.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
|
||||
<changeSet id="17588202536-1" author="fineract">
|
||||
<addColumn tableName="acc_product_mapping">
|
||||
<column name="write_off_reason_id" type="BIGINT">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user