Skip to content

Commit

Permalink
FINERACT-2042 Configurable CreditAllocations for Loan Product
Browse files Browse the repository at this point in the history
  • Loading branch information
reluxa committed Jan 30, 2024
1 parent f7a0562 commit 9fe34f7
Show file tree
Hide file tree
Showing 19 changed files with 1,206 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,24 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;

import java.util.Arrays;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;

@RequiredArgsConstructor
@Getter
public enum AllocationType {
PENALTY, //
FEE, //
PRINCIPAL, //
INTEREST //

PENALTY("Penalty"), //
FEE("Fee"), //
PRINCIPAL("Principal"), //
INTEREST("Interest"); //

private final String humanReadableName;

public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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.portfolio.loanproduct.domain;

import com.google.common.base.Enums;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
public class CreditAllocationsJsonParser {

public final CreditAllocationsValidator creditAllocationsValidator;

public List<LoanProductCreditAllocationRule> assembleLoanProductCreditAllocationRules(final JsonCommand command,
String loanTransactionProcessingStrategyCode) {
JsonArray creditAllocation = command.arrayOfParameterNamed("creditAllocation");
List<LoanProductCreditAllocationRule> productCreditAllocationRules = null;
if (creditAllocation != null) {
productCreditAllocationRules = creditAllocation.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
LoanProductCreditAllocationRule creditAllocationRule = new LoanProductCreditAllocationRule();
populateCreditAllocationRules(map, creditAllocationRule);
populateTransactionType(map, creditAllocationRule);
return creditAllocationRule;
}).toList();
}
creditAllocationsValidator.validate(productCreditAllocationRules, loanTransactionProcessingStrategyCode);
return productCreditAllocationRules;
}

private void populateCreditAllocationRules(Map<String, JsonElement> map, LoanProductCreditAllocationRule creditAllocationRule) {
JsonArray creditAllocationOrder = asJsonArrayOrNull(map.get("creditAllocationOrder"));
if (creditAllocationOrder != null) {
creditAllocationRule.setAllocationTypes(getAllocationTypes(creditAllocationOrder));
}
}

private void populateTransactionType(Map<String, JsonElement> map, LoanProductCreditAllocationRule creditAllocationRule) {
String transactionType = asStringOrNull(map.get("transactionType"));
if (transactionType != null) {
creditAllocationRule.setTransactionType(Enums.getIfPresent(CreditAllocationTransactionType.class, transactionType).orNull());
}
}

@NotNull
private List<AllocationType> getAllocationTypes(JsonArray allocationOrder) {
if (allocationOrder != null) {
List<Pair<Integer, AllocationType>> parsedListWithOrder = allocationOrder.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
AllocationType allocationType = null;
String creditAllocationRule = asStringOrNull(map.get("creditAllocationRule"));
if (creditAllocationRule != null) {
allocationType = Enums.getIfPresent(AllocationType.class, creditAllocationRule).orNull();
}
return Pair.of(asIntegerOrNull(map.get("order")), allocationType);
}).sorted(Comparator.comparing(Pair::getLeft)).toList();
creditAllocationsValidator.validatePairOfOrderAndCreditAllocationType(parsedListWithOrder);
return parsedListWithOrder.stream().map(Pair::getRight).toList();
} else {
return List.of();
}
}

private Integer asIntegerOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsInt();
}
return null;
}

private String asStringOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsString();
}
return null;
}

private JsonArray asJsonArrayOrNull(JsonElement element) {
if (!element.isJsonNull()) {
return element.getAsJsonArray();
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* 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.portfolio.loanproduct.domain;

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.springframework.stereotype.Service;

@Service
public class CreditAllocationsValidator {

public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";

public void validate(List<LoanProductCreditAllocationRule> rules, String code) {
if (isAdvancedPaymentStrategy(code)) {
if (hasDuplicateTransactionTypes(rules)) {
raiseValidationError("advanced-payment-strategy-with-duplicate-credit-allocation",
"The same transaction type must be provided only once");
}

if (rules != null) {
for (LoanProductCreditAllocationRule rule : rules) {
validateAllocationRule(rule);
}
}

} else {
if (hasLoanProductCreditAllocationRule(rules)) {
raiseValidationError("credit_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + code + "' payment strategy, creditAllocation must not be provided");
}
}
}

public void validatePairOfOrderAndCreditAllocationType(List<Pair<Integer, AllocationType>> rules) {
if (rules.size() != 4) {
raiseValidationError("advanced-payment-strategy.each_credit_allocation_order.must.contain.4.entries",
"Each provided credit allocation must contain exactly 4 allocation rules, but " + rules.size() + " were provided");
}

List<AllocationType> deduped = rules.stream().map(Pair::getRight).distinct().toList();
if (deduped.size() != 4) {
raiseValidationError("advanced-payment-strategy.must.not.have.duplicate.credit.allocation.rule",
"The list of provided credit allocation rules must not contain any duplicates");
}

if (!Arrays.equals(IntStream.rangeClosed(1, 4).boxed().toArray(), rules.stream().map(Pair::getLeft).toArray())) {
raiseValidationError("advanced-payment-strategy.invalid.order", "The provided orders must be between 1 and 4");
}
}

private boolean hasDuplicateTransactionTypes(List<LoanProductCreditAllocationRule> rules) {
return rules != null
&& rules.stream().map(LoanProductCreditAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
}

private void validateAllocationRule(LoanProductCreditAllocationRule rule) {
if (rule.getTransactionType() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.transaction.type",
"Credit allocation was provided with a not valid transaction type");
}
}

private boolean isAdvancedPaymentStrategy(String code) {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(code);
}

private boolean hasLoanProductCreditAllocationRule(List<LoanProductCreditAllocationRule> rules) {
return rules != null && rules.size() > 0;
}

private void raiseValidationError(String globalisationMessageCode, String msg) {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,13 @@ public LoanProduct(final Fund fund, final String transactionProcessingStrategyCo
}
}

this.creditAllocationRules = creditAllocationRules;
if (this.creditAllocationRules != null) {
for (LoanProductCreditAllocationRule loanProductCreditAllocationRule : this.creditAllocationRules) {
loanProductCreditAllocationRule.setLoanProduct(this);
}
}

this.name = name.trim();
this.shortName = shortName.trim();
if (StringUtils.isNotBlank(description)) {
Expand Down Expand Up @@ -773,6 +780,13 @@ public void validateLoanProductPreSave() {
"In case '" + transactionProcessingStrategyCode + "' payment strategy, payment_allocation must not be provided");
}

if (this.creditAllocationRules != null && creditAllocationRules.size() > 0
&& !transactionProcessingStrategyCode.equals("advanced-payment-allocation-strategy")) {
throw new LoanProductGeneralRuleException(
"creditAllocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + transactionProcessingStrategyCode + "' payment strategy, creditAllocation must not be provided");
}

if (this.disallowExpectedDisbursements) {
if (!this.isMultiDisburseLoan()) {
throw new LoanProductGeneralRuleException("allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set",
Expand Down Expand Up @@ -906,6 +920,10 @@ public List<LoanProductPaymentAllocationRule> getPaymentAllocationRules() {
return this.paymentAllocationRules;
}

public List<LoanProductCreditAllocationRule> getCreditAllocationRules() {
return this.creditAllocationRules;
}

public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) {
this.loanConfigurableAttributes = loanConfigurableAttributes;
}
Expand Down Expand Up @@ -999,6 +1017,14 @@ public Map<String, Object> update(final JsonCommand command, final AprCalculator
}
}

final String creditAllocationParamName = "creditAllocation";
if (command.hasParameter(creditAllocationParamName)) {
final JsonArray jsonArray = command.arrayOfParameterNamed(creditAllocationParamName);
if (jsonArray != null) {
actualChanges.put(creditAllocationParamName, command.jsonFragment(creditAllocationParamName));
}
}

final String chargesParamName = "charges";
if (command.hasParameter(chargesParamName)) {
final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName);
Expand Down
Loading

0 comments on commit 9fe34f7

Please sign in to comment.