Skip to content

Commit cb70555

Browse files
Jose Alberto Hernandezadamsaghy
authored andcommitted
FINERACT-2374: Advance Accounting rule for classification type
1 parent 6d3e5fa commit cb70555

File tree

31 files changed

+1023
-66
lines changed

31 files changed

+1023
-66
lines changed

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMapping.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,20 @@ public class ProductToGLAccountMapping extends AbstractPersistableCustom<Long> {
6868
@JoinColumn(name = "charge_off_reason_id", nullable = true)
6969
private CodeValue chargeOffReason;
7070

71+
@ManyToOne
72+
@JoinColumn(name = "capitalized_income_classification_id", nullable = true)
73+
private CodeValue capitalizedIncomeClassification;
74+
75+
@ManyToOne
76+
@JoinColumn(name = "buydown_fee_classification_id", nullable = true)
77+
private CodeValue buydownFeeClassification;
78+
7179
public static ProductToGLAccountMapping createNew(final GLAccount glAccount, final Long productId, final int productType,
72-
final int financialAccountType, final CodeValue chargeOffReason) {
80+
final int financialAccountType, final CodeValue chargeOffReason, final CodeValue capitalizedIncomeClassification,
81+
final CodeValue buydownFeeClassification) {
7382

7483
return new ProductToGLAccountMapping().setGlAccount(glAccount).setProductId(productId).setProductType(productType)
75-
.setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason);
84+
.setFinancialAccountType(financialAccountType).setChargeOffReason(chargeOffReason)
85+
.setCapitalizedIncomeClassification(capitalizedIncomeClassification).setBuydownFeeClassification(buydownFeeClassification);
7686
}
7787
}

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ProductToGLAccountMapping findProductIdAndProductTypeAndFinancialAccountTypeAndC
3535
@Param("productType") int productType, @Param("financialAccountType") int financialAccountType,
3636
@Param("chargeId") Long ChargeId);
3737

38-
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL")
38+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.financialAccountType=:financialAccountType and mapping.paymentType is NULL and mapping.charge is NULL and mapping.chargeOffReason is NULL and mapping.capitalizedIncomeClassification is NULL and mapping.buydownFeeClassification is NULL")
3939
ProductToGLAccountMapping findCoreProductToFinAccountMapping(@Param("productId") Long productId, @Param("productType") int productType,
4040
@Param("financialAccountType") int financialAccountType);
4141

@@ -70,7 +70,7 @@ List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productI
7070
ProductToGLAccountMapping findChargeOffReasonMapping(@Param("productId") Long productId, @Param("productType") Integer productType,
7171
@Param("chargeOffReasonId") Long chargeOffReasonId);
7272

73-
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL")
73+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL AND mapping.capitalizedIncomeClassification is NULL AND mapping.buydownFeeClassification is NULL")
7474
List<ProductToGLAccountMapping> findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType);
7575

7676
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.paymentType is not NULL")
@@ -82,4 +82,21 @@ List<ProductToGLAccountMapping> findAllPaymentTypeMappings(@Param("productId") L
8282

8383
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge.penalty = FALSE")
8484
List<ProductToGLAccountMapping> findAllFeeMappings(@Param("productId") Long productId, @Param("productType") Integer productType);
85+
86+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.capitalizedIncomeClassification is not NULL")
87+
List<ProductToGLAccountMapping> findAllCapitalizedIncomeClassificationsMappings(@Param("productId") Long productId,
88+
@Param("productType") int productType);
89+
90+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.buydownFeeClassification is not NULL")
91+
List<ProductToGLAccountMapping> findAllBuyDownFeeClassificationsMappings(@Param("productId") Long productId,
92+
@Param("productType") int productType);
93+
94+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.buydownFeeClassification.id = :classificationId AND mapping.productId = :productId AND mapping.productType = :productType")
95+
ProductToGLAccountMapping findBuydownFeeClassificationMapping(@Param("productId") Long productId,
96+
@Param("productType") Integer productType, @Param("classificationId") Long classificationId);
97+
98+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.capitalizedIncomeClassification.id = :classificationId AND mapping.productId = :productId AND mapping.productType = :productType")
99+
ProductToGLAccountMapping findCapitalizedIncomeClassificationMapping(@Param("productId") Long productId,
100+
@Param("productType") Integer productType, @Param("classificationId") Long classificationId);
101+
85102
}

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,30 @@ public void saveChargeOffReasonToGLAccountMappings(final JsonCommand command, fi
227227
}
228228
}
229229

230+
public void saveClassificationToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
231+
final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
232+
final LoanProductAccountingParams classificationParameter) {
233+
234+
final String arrayName = classificationParameter.getValue();
235+
final JsonArray classificationToIncomeAccountMappingArray = this.fromApiJsonHelper.extractJsonArrayNamed(arrayName, element);
236+
237+
if (classificationToIncomeAccountMappingArray != null) {
238+
if (changes != null) {
239+
changes.put(arrayName, command.jsonFragment(arrayName));
240+
}
241+
242+
for (int i = 0; i < classificationToIncomeAccountMappingArray.size(); i++) {
243+
final JsonObject jsonObject = classificationToIncomeAccountMappingArray.get(i).getAsJsonObject();
244+
final Long classificationId = jsonObject.get(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue())
245+
.getAsLong();
246+
final Long incomeAccountId = jsonObject.get(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()).getAsLong();
247+
248+
saveClassificationToIncomeMapping(productId, classificationId, incomeAccountId, portfolioProductType,
249+
classificationParameter);
250+
}
251+
}
252+
}
253+
230254
/**
231255
* @param command
232256
* @param element
@@ -448,6 +472,75 @@ public void updateChargeOffReasonToGLAccountMappings(final JsonCommand command,
448472
}
449473
}
450474

475+
public void updateClassificationToGLAccountMappings(final JsonCommand command, final JsonElement element, final Long productId,
476+
final Map<String, Object> changes, final PortfolioProductType portfolioProductType,
477+
final LoanProductAccountingParams classificationParameter) {
478+
479+
final List<ProductToGLAccountMapping> existingClassificationToGLAccountMappings = classificationParameter
480+
.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)
481+
? this.accountMappingRepository.findAllCapitalizedIncomeClassificationsMappings(productId,
482+
portfolioProductType.getValue())
483+
: this.accountMappingRepository.findAllBuyDownFeeClassificationsMappings(productId,
484+
portfolioProductType.getValue());
485+
486+
final JsonArray classificationToGLAccountMappingArray = this.fromApiJsonHelper
487+
.extractJsonArrayNamed(classificationParameter.getValue(), element);
488+
489+
final Map<Long, Long> inputClassificationToGLAccountMap = new HashMap<>();
490+
491+
final Set<Long> existingClassifications = new HashSet<>();
492+
if (classificationToGLAccountMappingArray != null) {
493+
if (changes != null) {
494+
changes.put(classificationParameter.getValue(), command.jsonFragment(classificationParameter.getValue()));
495+
}
496+
497+
for (int i = 0; i < classificationToGLAccountMappingArray.size(); i++) {
498+
final JsonObject jsonObject = classificationToGLAccountMappingArray.get(i).getAsJsonObject();
499+
final Long incomeGlAccountId = jsonObject.get(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()).getAsLong();
500+
final Long classificationCodeValueId = jsonObject.get(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue())
501+
.getAsLong();
502+
inputClassificationToGLAccountMap.put(classificationCodeValueId, incomeGlAccountId);
503+
}
504+
505+
// If input map is empty, delete all existing mappings
506+
if (inputClassificationToGLAccountMap.isEmpty()) {
507+
this.accountMappingRepository.deleteAllInBatch(existingClassificationToGLAccountMappings);
508+
} else {
509+
for (final ProductToGLAccountMapping existingClassificationToGLAccountMapping : existingClassificationToGLAccountMappings) {
510+
final Long currentClassificationId = classificationParameter
511+
.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)
512+
? existingClassificationToGLAccountMapping.getCapitalizedIncomeClassification().getId()
513+
: existingClassificationToGLAccountMapping.getBuydownFeeClassification().getId();
514+
515+
if (currentClassificationId != null) {
516+
existingClassifications.add(currentClassificationId);
517+
// update existing mappings (if required)
518+
if (inputClassificationToGLAccountMap.containsKey(currentClassificationId)) {
519+
final Long newGLAccountId = inputClassificationToGLAccountMap.get(currentClassificationId);
520+
if (!newGLAccountId.equals(existingClassificationToGLAccountMapping.getGlAccount().getId())) {
521+
final Optional<GLAccount> glAccount = accountRepository.findById(newGLAccountId);
522+
if (glAccount.isPresent()) {
523+
existingClassificationToGLAccountMapping.setGlAccount(glAccount.get());
524+
this.accountMappingRepository.saveAndFlush(existingClassificationToGLAccountMapping);
525+
}
526+
}
527+
} // deleted previous record
528+
else {
529+
this.accountMappingRepository.delete(existingClassificationToGLAccountMapping);
530+
}
531+
}
532+
}
533+
534+
// only the newly added
535+
for (Map.Entry<Long, Long> entry : inputClassificationToGLAccountMap.entrySet().stream()
536+
.filter(e -> !existingClassifications.contains(e.getKey())).toList()) {
537+
saveClassificationToIncomeMapping(productId, entry.getKey(), entry.getValue(), portfolioProductType,
538+
classificationParameter);
539+
}
540+
}
541+
}
542+
}
543+
451544
/**
452545
* @param productId
453546
*
@@ -514,6 +607,39 @@ private void saveChargeOffReasonToExpenseMapping(final Long productId, final Lon
514607
}
515608
}
516609

610+
private void saveClassificationToIncomeMapping(final Long productId, final Long classificationId, final Long incomeAccountId,
611+
final PortfolioProductType portfolioProductType, final LoanProductAccountingParams classificationParameter) {
612+
613+
final Optional<GLAccount> glAccount = accountRepository.findById(incomeAccountId);
614+
615+
boolean classificationMappingExists = false;
616+
if (classificationParameter.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)) {
617+
classificationMappingExists = this.accountMappingRepository
618+
.findAllCapitalizedIncomeClassificationsMappings(productId, portfolioProductType.getValue()).stream()
619+
.anyMatch(mapping -> mapping.getCapitalizedIncomeClassification().getId().equals(classificationId));
620+
} else {
621+
classificationMappingExists = this.accountMappingRepository
622+
.findAllBuyDownFeeClassificationsMappings(productId, portfolioProductType.getValue()).stream()
623+
.anyMatch(mapping -> mapping.getBuydownFeeClassification().getId().equals(classificationId));
624+
}
625+
626+
final Optional<CodeValue> codeValueOptional = codeValueRepository.findById(classificationId);
627+
628+
if (glAccount.isPresent() && !classificationMappingExists && codeValueOptional.isPresent()) {
629+
final ProductToGLAccountMapping accountMapping = new ProductToGLAccountMapping().setGlAccount(glAccount.get())
630+
.setProductId(productId).setProductType(portfolioProductType.getValue())
631+
.setFinancialAccountType(CashAccountsForLoan.CLASSIFICATION_INCOME.getValue());
632+
633+
if (classificationParameter.equals(LoanProductAccountingParams.CAPITALIZED_INCOME_CLASSIFICATION_TO_INCOME_ACCOUNT_MAPPINGS)) {
634+
accountMapping.setCapitalizedIncomeClassification(codeValueOptional.get());
635+
} else {
636+
accountMapping.setBuydownFeeClassification(codeValueOptional.get());
637+
}
638+
639+
this.accountMappingRepository.saveAndFlush(accountMapping);
640+
}
641+
}
642+
517643
private List<GLAccountType> getAllowedAccountTypesForFeeMapping() {
518644
List<GLAccountType> allowedAccountTypes = new ArrayList<>();
519645
allowedAccountTypes.add(GLAccountType.INCOME);
@@ -610,4 +736,38 @@ public void validateChargeOffMappingsInDatabase(final List<JsonObject> mappings)
610736
throw new PlatformApiDataValidationException(validationErrors);
611737
}
612738
}
739+
740+
public void validateClassificationMappingsInDatabase(final List<JsonObject> mappings, final String dataCodeName) {
741+
final List<ApiParameterError> validationErrors = new ArrayList<>();
742+
743+
for (JsonObject jsonObject : mappings) {
744+
final Long incomeGlAccountId = this.fromApiJsonHelper.extractLongNamed(LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue(),
745+
jsonObject);
746+
final Long classificationCodeValueId = this.fromApiJsonHelper
747+
.extractLongNamed(LoanProductAccountingParams.CLASSIFICATION_CODE_VALUE_ID.getValue(), jsonObject);
748+
749+
// Validation: classificationCodeValueId must exist in the database
750+
final CodeValue codeValue = this.codeValueRepository.findByCodeNameAndId(dataCodeName, classificationCodeValueId);
751+
if (codeValue == null) {
752+
validationErrors.add(ApiParameterError.parameterError("validation.msg.classification.invalid",
753+
"Classification with ID " + classificationCodeValueId + " does not exist", dataCodeName));
754+
}
755+
756+
// Validation: expenseGLAccountId must exist as a valid Expense GL account
757+
final Optional<GLAccount> glAccount = accountRepository.findById(incomeGlAccountId);
758+
759+
if (glAccount.isEmpty() || !GLAccountType.fromInt(glAccount.get().getType()).isIncomeType()) {
760+
validationErrors.add(ApiParameterError.parameterError("validation.msg.glaccount.not.found",
761+
"GL Account with ID " + incomeGlAccountId + " does not exist or is not an Income GL account",
762+
LoanProductAccountingParams.INCOME_ACCOUNT_ID.getValue()));
763+
764+
}
765+
}
766+
767+
// Throw all collected validation errors, if any
768+
if (!validationErrors.isEmpty()) {
769+
throw new PlatformApiDataValidationException(validationErrors);
770+
}
771+
}
772+
613773
}

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020

2121
import java.util.List;
2222
import java.util.Map;
23+
import org.apache.fineract.accounting.common.AccountingConstants.LoanProductAccountingParams;
2324
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeOffReasonToGLAccountMapper;
2425
import org.apache.fineract.accounting.producttoaccountmapping.data.ChargeToGLAccountMapper;
26+
import org.apache.fineract.accounting.producttoaccountmapping.data.ClassificationToGLAccountData;
2527
import org.apache.fineract.accounting.producttoaccountmapping.data.PaymentTypeToGLAccountMapper;
2628

2729
public interface ProductToGLAccountMappingReadPlatformService {
@@ -49,4 +51,7 @@ public interface ProductToGLAccountMappingReadPlatformService {
4951
List<ChargeToGLAccountMapper> fetchFeeToIncomeAccountMappingsForShareProduct(Long productId);
5052

5153
List<ChargeOffReasonToGLAccountMapper> fetchChargeOffReasonMappingsForLoanProduct(Long loanProductId);
54+
55+
List<ClassificationToGLAccountData> fetchClassificationMappingsForLoanProduct(Long loanProductId,
56+
LoanProductAccountingParams classificationParameter);
5257
}

0 commit comments

Comments
 (0)