FINERACT-2358: Advanced accounting rules based on write-off reason

This commit is contained in:
Jose Alberto Hernandez
2025-10-14 20:26:24 +02:00
committed by Arnold Gálovics
parent bef931ca17
commit c518d8622a
9 changed files with 50 additions and 111 deletions
@@ -21,11 +21,10 @@ package org.apache.fineract.accounting.producttoaccountmapping.service;
import java.util.List;
import java.util.Map;
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData;
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;
public interface ProductToGLAccountMappingReadPlatformService {
@@ -51,9 +50,9 @@ public interface ProductToGLAccountMappingReadPlatformService {
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId);
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
List<AdvancedMappingToExpenseAccountData> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId);
List<AdvancedMappingToExpenseAccountData> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId);
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
LoanProductAccountingParams classificationParameter);
@@ -36,11 +36,10 @@ import org.apache.fineract.accounting.common.AccountingConstants.SharesProductAc
import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.accounting.common.AccountingValidations;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData;
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.domain.ProductToGLAccountMapping;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@@ -275,39 +274,33 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
return chargeToGLAccountMappers;
}
private List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType,
private List<AdvancedMappingToExpenseAccountData> fetchChargeOffReasonMappings(final PortfolioProductType portfolioProductType,
final Long loanProductId) {
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId,
portfolioProductType.getValue());
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappers = mappings.isEmpty() ? null : new ArrayList<>();
return fetchAdvancedMappingToExpenseAccountData(
productToGLAccountMappingRepository.findAllChargeOffReasonsMappings(loanProductId, portfolioProductType.getValue()));
}
private List<AdvancedMappingToExpenseAccountData> fetchWriteOffReasonMappings(final PortfolioProductType portfolioProductType,
final Long loanProductId) {
return fetchAdvancedMappingToExpenseAccountData(
productToGLAccountMappingRepository.findAllWriteOffReasonsMappings(loanProductId, portfolioProductType.getValue()));
}
private List<AdvancedMappingToExpenseAccountData> fetchAdvancedMappingToExpenseAccountData(
final List<ProductToGLAccountMapping> mappings) {
List<AdvancedMappingToExpenseAccountData> advancedMappingToExpenseAccountData = mappings.isEmpty() ? null : new ArrayList<>();
for (final ProductToGLAccountMapping mapping : mappings) {
final Long glAccountId = mapping.getGlAccount().getId();
final String glAccountName = mapping.getGlAccount().getName();
final String glCode = mapping.getGlAccount().getGlCode();
final GLAccountData chargeOffExpenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode);
final CodeValueData chargeOffReasonsCodeValue = codeValueMapper.map(mapping.getChargeOffReason());
final GLAccountData expenseAccount = new GLAccountData().setId(glAccountId).setName(glAccountName).setGlCode(glCode);
final CodeValueData codeValue = (mapping.getChargeOffReason() != null) ? codeValueMapper.map(mapping.getChargeOffReason())
: codeValueMapper.map(mapping.getWriteOffReason());
final ChargeOffReasonToGLAccountMapper chargeOffReasonToGLAccountMapper = new ChargeOffReasonToGLAccountMapper()
.setChargeOffReasonCodeValue(chargeOffReasonsCodeValue).setExpenseAccount(chargeOffExpenseAccount);
chargeOffReasonToGLAccountMappers.add(chargeOffReasonToGLAccountMapper);
advancedMappingToExpenseAccountData
.add(new AdvancedMappingToExpenseAccountData().setReasonCodeValue(codeValue).setExpenseAccount(expenseAccount));
}
return chargeOffReasonToGLAccountMappers;
}
private List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappings(final PortfolioProductType portfolioProductType,
final Long loanProductId) {
final List<ProductToGLAccountMapping> mappings = productToGLAccountMappingRepository.findAllWriteOffReasonsMappings(loanProductId,
portfolioProductType.getValue());
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappers = mappings.isEmpty() ? null : new ArrayList<>();
for (final ProductToGLAccountMapping mapping : mappings) {
final String glCode = String.valueOf(mapping.getGlAccount().getId());
final String writeOffReasonId = String.valueOf(mapping.getWriteOffReason().getId());
final WriteOffReasonsToExpenseAccountMapper writeOffReasonToGLAccountMapper = new WriteOffReasonsToExpenseAccountMapper()
.setWriteOffReasonCodeValueId(writeOffReasonId).setExpenseAccountId(glCode);
writeOffReasonsToExpenseAccountMappers.add(writeOffReasonToGLAccountMapper);
}
return writeOffReasonsToExpenseAccountMappers;
return advancedMappingToExpenseAccountData;
}
private List<ClassificationToGLAccountData> fetchClassificationMappings(final PortfolioProductType portfolioProductType,
@@ -380,12 +373,12 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product
}
@Override
public List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId) {
public List<AdvancedMappingToExpenseAccountData> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId) {
return fetchChargeOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
}
@Override
public List<WriteOffReasonsToExpenseAccountMapper> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
public List<AdvancedMappingToExpenseAccountData> fetchWriteOffReasonMappingsForLoanProduct(Long loanProductId) {
return fetchWriteOffReasonMappings(PortfolioProductType.LOAN, loanProductId);
}
@@ -28,9 +28,9 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class ChargeOffReasonToGLAccountMapper implements Serializable {
public class AdvancedMappingToExpenseAccountData implements Serializable {
private static final long serialVersionUID = 1L;
private CodeValueData chargeOffReasonCodeValue;
private CodeValueData reasonCodeValue;
private GLAccountData expenseAccount;
}
@@ -1,36 +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.accounting.producttoaccountmapping.data;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class WriteOffReasonsToExpenseAccountMapper implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String writeOffReasonCodeValueId;
private String expenseAccountId;
}
@@ -1346,25 +1346,9 @@ public final class LoanProductsApiResourceSwagger {
private GetChargeOffReasonToExpenseAccountMappings() {}
public GetCodeValueData chargeOffReasonCodeValue;
public GetCodeValuesDataResponse reasonCodeValue;
public GetGLAccountData expenseAccount;
static final class GetCodeValueData {
private GetCodeValueData() {}
@Schema(example = "1")
public Long id;
@Schema(example = "ChargeOffReasons")
public String name;
@Schema(example = "1")
public Integer position;
public String description;
@Schema(example = "true")
public Boolean active;
@Schema(example = "false")
public Boolean mandatory;
}
}
static final class GetLoanFeeToIncomeAccountMappings {
@@ -1492,7 +1476,7 @@ public final class LoanProductsApiResourceSwagger {
public Set<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings;
public Set<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings;
public List<GetChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings;
public List<PostLoanProductsRequest.PostWriteOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
public List<GetChargeOffReasonToExpenseAccountMappings> writeOffReasonsToExpenseMappings;
@Schema(example = "false")
public Boolean isRatesEnabled;
@Schema(example = "true")
@@ -30,11 +30,10 @@ import lombok.Getter;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.accounting.common.AccountingRuleType;
import org.apache.fineract.accounting.glaccount.data.GLAccountData;
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
import org.apache.fineract.accounting.producttoaccountmapping.data.AdvancedMappingToExpenseAccountData;
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.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.api.ApiFacingEnum;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@@ -164,9 +163,9 @@ public class LoanProductData implements Serializable {
private Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
private Collection<ChargeToGLAccountMapper> feeToIncomeAccountMappings;
private Collection<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings;
private List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToExpenseAccountMappings;
private List<AdvancedMappingToExpenseAccountData> chargeOffReasonToExpenseAccountMappings;
private final boolean enableAccrualActivityPosting;
private List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings;
private List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings;
private final List<CodeValueData> writeOffReasonOptions;
// rates
private final boolean isRatesEnabled;
@@ -381,7 +380,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
@@ -521,7 +520,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
@@ -668,7 +667,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
@@ -809,7 +808,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData buyDownFeeStrategy = null;
final StringEnumOptionData buyDownFeeIncomeType = null;
final boolean merchantBuyDownFee = false;
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings = null;
final List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings = null;
final List<CodeValueData> writeOffReasonOptions = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
@@ -842,8 +841,8 @@ public class LoanProductData implements Serializable {
final Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings,
final Collection<ChargeToGLAccountMapper> feeToGLAccountMappings,
final Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings,
final List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings,
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonToGLAccountMappings,
final List<AdvancedMappingToExpenseAccountData> chargeOffReasonToGLAccountMappings,
final List<AdvancedMappingToExpenseAccountData> writeOffReasonToGLAccountMappings,
final List<ClassificationToGLAccountData> capitalizedIncomeClassificationToIncomeAccountMappings,
final List<ClassificationToGLAccountData> buydownFeeClassificationToIncomeAccountMappings) {
productData.accountingMappings = accountingMappings;
@@ -901,7 +900,7 @@ public class LoanProductData implements Serializable {
final StringEnumOptionData capitalizedIncomeType, final boolean enableBuyDownFee,
final StringEnumOptionData buyDownFeeCalculationType, final StringEnumOptionData buyDownFeeStrategy,
final StringEnumOptionData buyDownFeeIncomeType, final boolean merchantBuyDownFee,
final List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseMappings,
final List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseMappings,
final List<CodeValueData> writeOffReasonOptions) {
this.id = id;
this.name = name;
@@ -49,11 +49,10 @@ 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.AdvancedMappingToExpenseAccountData;
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,8 +354,8 @@ public class LoanProductsApiResource {
Collection<PaymentTypeToGLAccountMapper> paymentChannelToFundSourceMappings;
Collection<ChargeToGLAccountMapper> feeToGLAccountMappings;
Collection<ChargeToGLAccountMapper> penaltyToGLAccountMappings;
List<ChargeOffReasonToGLAccountMapper> chargeOffReasonToGLAccountMappings;
List<WriteOffReasonsToExpenseAccountMapper> writeOffReasonsToExpenseAccountMappings;
List<AdvancedMappingToExpenseAccountData> chargeOffReasonToGLAccountMappings;
List<AdvancedMappingToExpenseAccountData> writeOffReasonsToExpenseAccountMappings;
List<ClassificationToGLAccountData> capitalizedIncomeClassificationToGLAccountMappings;
List<ClassificationToGLAccountData> buydowFeeClassificationToGLAccountMappings;
if (loanProduct.hasAccountingEnabled()) {
@@ -60,7 +60,7 @@ public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationT
Assertions.assertEquals(expenseAccount.getAccountID().longValue(),
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getExpenseAccount().getId());
Assertions.assertEquals(Long.valueOf(chargeOffReasons),
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffReasonCodeValue().getId());
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getReasonCodeValue().getId());
List<PostChargeOffReasonToExpenseAccountMappings> chargeOffReasonToExpenseAccountMappings = createPostChargeOffReasonToExpenseAccountMappings(
Long.valueOf(chargeOffReasons), otherExpenseAccount.getAccountID().longValue());
@@ -72,7 +72,7 @@ public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationT
Assertions.assertEquals(otherExpenseAccount.getAccountID().longValue(),
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getExpenseAccount().getId());
Assertions.assertEquals(Long.valueOf(chargeOffReasons),
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getChargeOffReasonCodeValue().getId());
loanProductDetails.getChargeOffReasonToExpenseAccountMappings().get(0).getReasonCodeValue().getId());
});
}
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetChargeOffReasonToExpenseAccountMappings;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoanProductsTemplateResponse;
import org.apache.fineract.client.models.GetLoanProductsWriteOffReasonOptions;
@@ -546,14 +547,14 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
// Verify that get loan product API has the corresponding fields
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProductId);
List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonToExpenseAccountMappings = getLoanProductsProductIdResponse
List<GetChargeOffReasonToExpenseAccountMappings> writeOffReasonToExpenseAccountMappings = getLoanProductsProductIdResponse
.getWriteOffReasonsToExpenseMappings();
Assertions.assertNotNull(writeOffReasonToExpenseAccountMappings);
Assertions.assertEquals(1, writeOffReasonToExpenseAccountMappings.size());
PostWriteOffReasonToExpenseAccountMappings writeOffMapping = writeOffReasonToExpenseAccountMappings.getFirst();
GetChargeOffReasonToExpenseAccountMappings writeOffMapping = writeOffReasonToExpenseAccountMappings.getFirst();
Assertions.assertNotNull(writeOffMapping);
Assertions.assertEquals(expenseAccountId, writeOffMapping.getExpenseAccountId());
Assertions.assertEquals(reasonCodeId, writeOffMapping.getWriteOffReasonCodeValueId());
Assertions.assertEquals(expenseAccountId, writeOffMapping.getExpenseAccount().getId().toString());
Assertions.assertEquals(reasonCodeId, writeOffMapping.getReasonCodeValue().getId().toString());
List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptionsResultNonTemplate = getLoanProductsProductIdResponse
.getWriteOffReasonOptions();