FINERACT-1968

- API - Advanced payment allocation strategy configuration on Loan product
This commit is contained in:
Peter Bagrij
2023-08-18 17:34:19 +02:00
committed by Adam Saghy
parent 03c1379205
commit 0802860835
28 changed files with 1575 additions and 50 deletions
@@ -461,6 +461,7 @@ public class FineractProperties {
private FineractTransactionProcessorItemProperties rbiIndia;
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
private FineractTransactionProcessorItemProperties duePenaltyInterestPrincipalFeeInAdvancePenaltyInterestPrincipalFee;
private FineractTransactionProcessorItemProperties advancedPaymentStrategy;
private boolean errorNotFoundFail;
}
@@ -30,7 +30,7 @@ public abstract class GenericEnumListConverter<E extends Enum<E>> implements Att
private final Class<E> clazz;
protected boolean isUnique() {
public boolean isUnique() {
return false;
}
@@ -0,0 +1,122 @@
/**
* 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 AdvancedPaymentAllocationsJsonParser {
public final AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
public List<LoanProductPaymentAllocationRule> assembleLoanProductPaymentAllocationRules(final JsonCommand command,
String loanTransactionProcessingStrategyCode) {
JsonArray paymentAllocations = command.arrayOfParameterNamed("paymentAllocation");
if (paymentAllocations != null) {
List<LoanProductPaymentAllocationRule> productPaymentAllocationRules = paymentAllocations.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule = new LoanProductPaymentAllocationRule();
populatePaymentAllocationRules(map, loanProductPaymentAllocationRule);
populateFutureInstallment(map, loanProductPaymentAllocationRule);
populateTransactionType(map, loanProductPaymentAllocationRule);
return loanProductPaymentAllocationRule;
}).toList();
advancedPaymentAllocationsValidator.validate(productPaymentAllocationRules, loanTransactionProcessingStrategyCode);
return productPaymentAllocationRules;
}
return null;
}
private void populatePaymentAllocationRules(Map<String, JsonElement> map,
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
JsonArray paymentAllocationOrder = asJsonArrayOrNull(map.get("paymentAllocationOrder"));
if (paymentAllocationOrder != null) {
loanProductPaymentAllocationRule.setAllocationTypes(getPaymentAllocationTypes(paymentAllocationOrder));
}
}
private void populateFutureInstallment(Map<String, JsonElement> map,
LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
String futureInstallmentAllocationRule = asStringOrNull(map.get("futureInstallmentAllocationRule"));
if (futureInstallmentAllocationRule != null) {
loanProductPaymentAllocationRule.setFutureInstallmentAllocationRule(
Enums.getIfPresent(FutureInstallmentAllocationRule.class, futureInstallmentAllocationRule).orNull());
}
}
private void populateTransactionType(Map<String, JsonElement> map, LoanProductPaymentAllocationRule loanProductPaymentAllocationRule) {
String transactionType = asStringOrNull(map.get("transactionType"));
if (transactionType != null) {
loanProductPaymentAllocationRule
.setTransactionType(Enums.getIfPresent(PaymentAllocationTransactionType.class, transactionType).orNull());
}
}
@NotNull
private List<PaymentAllocationType> getPaymentAllocationTypes(JsonArray paymentAllocationOrder) {
if (paymentAllocationOrder != null) {
List<Pair<Integer, PaymentAllocationType>> parsedListWithOrder = paymentAllocationOrder.asList().stream().map(json -> {
Map<String, JsonElement> map = json.getAsJsonObject().asMap();
PaymentAllocationType paymentAllocationType = null;
String paymentAllocationRule = asStringOrNull(map.get("paymentAllocationRule"));
if (paymentAllocationRule != null) {
paymentAllocationType = Enums.getIfPresent(PaymentAllocationType.class, paymentAllocationRule).orNull();
}
return Pair.of(asIntegerOrNull(map.get("order")), paymentAllocationType);
}).sorted(Comparator.comparing(Pair::getLeft)).toList();
advancedPaymentAllocationsValidator.validatePairOfOrderAndPaymentAllocationType(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;
}
}
@@ -0,0 +1,109 @@
/**
* 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 AdvancedPaymentAllocationsValidator {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public void validate(List<LoanProductPaymentAllocationRule> rules, String code) {
if (isAdvancedPaymentStrategy(code)) {
if (!hasLoanProductPaymentAllocationRule(rules) || !hasAtLeastOneDefaultPaymentAllocation(rules)) {
raiseValidationError("advanced-payment-strategy-without-default-payment-allocation",
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided");
}
if (hasDuplicateTransactionTypes(rules)) {
raiseValidationError("advanced-payment-strategy-with-duplicate-payment-allocation",
"The same transaction type must be provided only once");
}
for (LoanProductPaymentAllocationRule rule : rules) {
validateAllocationRule(rule);
}
} else {
if (hasLoanProductPaymentAllocationRule(rules)) {
raiseValidationError("payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + code + "' payment strategy, payment_allocation must not be provided");
}
}
}
public void validatePairOfOrderAndPaymentAllocationType(List<Pair<Integer, PaymentAllocationType>> rules) {
if (rules.size() != 12) {
raiseValidationError("advanced-payment-strategy.each_payment_allocation_order.must.contain.12.entries",
"Each provided payment allocation must contain exactly 12 allocation rules, but " + rules.size() + " were provided");
}
List<PaymentAllocationType> deduped = rules.stream().map(Pair::getRight).distinct().toList();
if (deduped.size() != 12) {
raiseValidationError("advanced-payment-strategy.must.not.have.duplicate.payment.allocation.rule",
"The list of provided payment allocation rules must not contain any duplicates");
}
if (!Arrays.equals(IntStream.rangeClosed(1, 12).boxed().toArray(), rules.stream().map(Pair::getLeft).toArray())) {
raiseValidationError("advanced-payment-strategy.invalid.order", "The provided orders must be between 1 and 12");
}
}
private boolean hasDuplicateTransactionTypes(List<LoanProductPaymentAllocationRule> rules) {
return rules.stream().map(LoanProductPaymentAllocationRule::getTransactionType).distinct().toList().size() != rules.size();
}
private void validateAllocationRule(LoanProductPaymentAllocationRule rule) {
if (rule.getTransactionType() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.transaction.type",
"Payment allocation was provided with a not valid transaction type");
}
if (rule.getFutureInstallmentAllocationRule() == null) {
raiseValidationError("advanced-payment-strategy.with.not.valid.future.installment.allocation.rule",
"Payment allocation was provided without a valid future installment allocation rule");
}
}
private boolean isAdvancedPaymentStrategy(String code) {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(code);
}
private boolean hasAtLeastOneDefaultPaymentAllocation(List<LoanProductPaymentAllocationRule> rules) {
return rules.stream() //
.filter(r -> PaymentAllocationTransactionType.DEFAULT.equals(r.getTransactionType())) //
.toList() //
.size() > 0;
}
private boolean hasLoanProductPaymentAllocationRule(List<LoanProductPaymentAllocationRule> rules) {
return rules.size() > 0;
}
private void raiseValidationError(String globalisationMessageCode, String msg) {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg)));
}
}
@@ -18,18 +18,21 @@
*/
package org.apache.fineract.portfolio.loanproduct.domain;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.List;
import org.apache.fineract.infrastructure.core.data.GenericEnumListConverter;
@Converter(autoApply = true)
public class AllocationTypeListConverter extends GenericEnumListConverter<PaymentAllocationType> {
public class AllocationTypeListConverter extends GenericEnumListConverter<PaymentAllocationType>
implements AttributeConverter<List<PaymentAllocationType>, String> {
@Override
protected boolean isUnique() {
public boolean isUnique() {
return true;
}
protected AllocationTypeListConverter() {
public AllocationTypeListConverter() {
super(PaymentAllocationType.class);
}
@@ -90,6 +90,9 @@ public class LoanProduct extends AbstractPersistableCustom {
@Column(name = "loan_transaction_strategy_name")
private String transactionProcessingStrategyName;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER)
private List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = new ArrayList<>();
@Column(name = "name", nullable = false, unique = true)
private String name;
@@ -212,7 +215,7 @@ public class LoanProduct extends AbstractPersistableCustom {
public static LoanProduct assembleFromJson(final Fund fund, final String loanTransactionProcessingStrategy,
final List<Charge> productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate,
final List<Rate> productRates) {
final List<Rate> productRates, List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules) {
final String name = command.stringValueOfParameterNamed("name");
final String shortName = command.stringValueOfParameterNamed(LoanProductConstants.SHORT_NAME);
@@ -391,23 +394,24 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean enableAutoRepaymentForDownPayment = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT);
return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal,
maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType,
annualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, repaymentEvery,
repaymentFrequencyType, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, graceOnPrincipalPayment,
recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod,
inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle, startDate, closeDate, externalId,
useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount, outstandingLoanBalance,
graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType, isInterestRecalculationEnabled,
interestRecalculationSettings, minimumDaysBetweenDisbursalAndFirstRepayment, holdGuarantorFunds,
loanProductGuaranteeDetails, principalThresholdForLastInstallment, accountMovesOutOfNPAOnlyOnArrearsCompletion,
canDefineEmiAmount, installmentAmountInMultiplesOf, loanConfigurableAttributes, isLinkedToFloatingInterestRates,
floatingRate, interestRateDifferential, minDifferentialLendingRate, maxDifferentialLendingRate,
defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed,
minimumGapBetweenInstallments, maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment, disallowExpectedDisbursements,
allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment);
return new LoanProduct(fund, loanTransactionProcessingStrategy, loanProductPaymentAllocationRules, name, shortName, description,
currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod,
interestFrequencyType, annualInterestRate, interestMethod, interestCalculationPeriodMethod,
allowPartialPeriodInterestCalcualtion, repaymentEvery, repaymentFrequencyType, numberOfRepayments, minNumberOfRepayments,
maxNumberOfRepayments, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment,
graceOnInterestCharged, amortizationMethod, inArrearsTolerance, productCharges, accountingRuleType, includeInBorrowerCycle,
startDate, closeDate, externalId, useBorrowerCycle, loanProductBorrowerCycleVariations, multiDisburseLoan, maxTrancheCount,
outstandingLoanBalance, graceOnArrearsAgeing, overdueDaysForNPA, daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, interestRecalculationSettings, minimumDaysBetweenDisbursalAndFirstRepayment,
holdGuarantorFunds, loanProductGuaranteeDetails, principalThresholdForLastInstallment,
accountMovesOutOfNPAOnlyOnArrearsCompletion, canDefineEmiAmount, installmentAmountInMultiplesOf, loanConfigurableAttributes,
isLinkedToFloatingInterestRates, floatingRate, interestRateDifferential, minDifferentialLendingRate,
maxDifferentialLendingRate, defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed,
isVariableInstallmentsAllowed, minimumGapBetweenInstallments, maximumGapBetweenInstallments,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment,
disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment,
enableAutoRepaymentForDownPayment);
}
@@ -589,7 +593,8 @@ public class LoanProduct extends AbstractPersistableCustom {
this.loanProductMinMaxConstraints = null;
}
public LoanProduct(final Fund fund, final String transactionProcessingStrategyCode, final String name, final String shortName,
public LoanProduct(final Fund fund, final String transactionProcessingStrategyCode,
final List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules, final String name, final String shortName,
final String description, final MonetaryCurrency currency, final BigDecimal defaultPrincipal,
final BigDecimal defaultMinPrincipal, final BigDecimal defaultMaxPrincipal,
final BigDecimal defaultNominalInterestRatePerPeriod, final BigDecimal defaultMinNominalInterestRatePerPeriod,
@@ -623,6 +628,14 @@ public class LoanProduct extends AbstractPersistableCustom {
final boolean enableAutoRepaymentForDownPayment) {
this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
this.loanProductPaymentAllocationRules = loanProductPaymentAllocationRules;
if (this.loanProductPaymentAllocationRules != null) {
for (LoanProductPaymentAllocationRule loanProductPaymentAllocationRule : this.loanProductPaymentAllocationRules) {
loanProductPaymentAllocationRule.setLoanProduct(this);
}
}
this.name = name.trim();
this.shortName = shortName.trim();
if (StringUtils.isNotBlank(description)) {
@@ -711,6 +724,12 @@ public class LoanProduct extends AbstractPersistableCustom {
}
public void validateLoanProductPreSave() {
if (this.loanProductPaymentAllocationRules != null && loanProductPaymentAllocationRules.size() > 0
&& !transactionProcessingStrategyCode.equals("advanced-payment-allocation-strategy")) {
throw new LoanProductGeneralRuleException(
"payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
"In case '" + transactionProcessingStrategyCode + "' payment strategy, payment_allocation must not be provided");
}
if (this.disallowExpectedDisbursements) {
if (!this.isMultiDisburseLoan()) {
@@ -775,6 +794,10 @@ public class LoanProduct extends AbstractPersistableCustom {
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
}
public String getTransactionProcessingStrategyCode() {
return this.transactionProcessingStrategyCode;
}
public void setTransactionProcessingStrategyName(final String transactionProcessingStrategyName) {
this.transactionProcessingStrategyName = transactionProcessingStrategyName;
}
@@ -837,6 +860,10 @@ public class LoanProduct extends AbstractPersistableCustom {
return this.charges;
}
public List<LoanProductPaymentAllocationRule> getLoanProductPaymentAllocationRules() {
return this.loanProductPaymentAllocationRules;
}
public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) {
this.loanConfigurableAttributes = loanConfigurableAttributes;
}
@@ -922,6 +949,14 @@ public class LoanProduct extends AbstractPersistableCustom {
actualChanges.put(transactionProcessingStrategyCodeParamName, newValue);
}
final String paymentAllocationParamName = "paymentAllocation";
if (command.hasParameter(paymentAllocationParamName)) {
final JsonArray jsonArray = command.arrayOfParameterNamed(paymentAllocationParamName);
if (jsonArray != null) {
actualChanges.put(paymentAllocationParamName, command.jsonFragment(paymentAllocationParamName));
}
}
final String chargesParamName = "charges";
if (command.hasParameter(chargesParamName)) {
final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName);
@@ -19,7 +19,7 @@
package org.apache.fineract.portfolio.loanproduct.domain;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
@@ -39,7 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
@Getter
@Setter
@Entity
@Table(name = "m_loan_product_payment_allocation", uniqueConstraints = {
@Table(name = "m_loan_product_payment_allocation_rule", uniqueConstraints = {
@UniqueConstraint(columnNames = { "loan_product_id", "transaction_type" }, name = "uq_m_loan_product_payment_allocation_rule") })
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -53,7 +53,7 @@ public class LoanProductPaymentAllocationRule extends AbstractAuditableWithUTCDa
@Enumerated(EnumType.STRING)
private PaymentAllocationTransactionType transactionType;
@ElementCollection(fetch = FetchType.EAGER)
@Convert(converter = AllocationTypeListConverter.class)
@Column(name = "allocation_types", nullable = false)
private List<PaymentAllocationType> allocationTypes;
@@ -0,0 +1,223 @@
/**
* 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 static org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule.NEXT_INSTALLMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.IN_ADVANCE_PENALTY;
import static org.mockito.Mockito.times;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AdvancedPaymentAllocationsJsonParserTest {
@Mock
private AdvancedPaymentAllocationsValidator advancedPaymentAllocationsValidator;
@InjectMocks
private AdvancedPaymentAllocationsJsonParser advancedPaymentAllocationsJsonParser;
private FromJsonHelper fromJsonHelper = new FromJsonHelper();
@Test
public void testEmptyJson() throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, "other-strategy");
// then
Assertions.assertNull(loanProductPaymentAllocationRules);
Mockito.verifyNoInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testParseSinglePaymentAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry("DEFAULT", "NEXT_INSTALLMENT", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertEquals(NEXT_INSTALLMENT, loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertEquals(DEFAULT, loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testInvalidTransactionTypeAndFutureAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry("INVALID", "INVALID", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testInvalidAndNullAllocationRules() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = Arrays.asList(new String[] { "invalid", null, "IN_ADVANCE_PENALTY" });
paymentAllocations.add(createPaymentAllocationEntry("DEFAULT", "NEXT_INSTALLMENT", allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertEquals(NEXT_INSTALLMENT, loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertEquals(DEFAULT, loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(3, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(0));
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(1));
Assertions.assertEquals(IN_ADVANCE_PENALTY, loanProductPaymentAllocationRules.get(0).getAllocationTypes().get(2));
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(List.of(Pair.of(1, null), Pair.of(2, null), Pair.of(3, IN_ADVANCE_PENALTY)));
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
@Test
public void testNullTransactionTypeAndFutureAllocation() throws JsonProcessingException {
// given
Map<String, Object> map = new HashMap<>();
List<Map<String, Object>> paymentAllocations = new ArrayList<>();
map.put("paymentAllocation", paymentAllocations);
List<String> allocationRule = EnumSet.allOf(PaymentAllocationType.class).stream().map(Enum::name).toList();
paymentAllocations.add(createPaymentAllocationEntry(null, null, allocationRule));
JsonCommand command = createJsonCommand(map);
// when
List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentAllocationsJsonParser
.assembleLoanProductPaymentAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
// then
Assertions.assertEquals(1, loanProductPaymentAllocationRules.size());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getFutureInstallmentAllocationRule());
Assertions.assertNull(loanProductPaymentAllocationRules.get(0).getTransactionType());
Assertions.assertEquals(12, loanProductPaymentAllocationRules.get(0).getAllocationTypes().size());
Assertions.assertEquals(EnumSet.allOf(PaymentAllocationType.class).stream().toList(),
loanProductPaymentAllocationRules.get(0).getAllocationTypes());
Mockito.verify(advancedPaymentAllocationsValidator, times(1)).validate(loanProductPaymentAllocationRules,
ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
Mockito.verify(advancedPaymentAllocationsValidator, times(1))
.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
Mockito.verifyNoMoreInteractions(advancedPaymentAllocationsValidator);
}
public Map<String, Object> createPaymentAllocationEntry(String transactionType, String futureInstallmentAllocation,
List<String> orderedRules) {
Map<String, Object> map = new HashMap<>();
map.put("transactionType", transactionType);
map.put("futureInstallmentAllocationRule", futureInstallmentAllocation);
List<Map<String, Object>> paymentAllocationOrder = new ArrayList<>();
map.put("paymentAllocationOrder", paymentAllocationOrder);
for (int i = 0; i < orderedRules.size(); i++) {
HashMap<String, Object> entry = new HashMap<>();
entry.put("paymentAllocationRule", orderedRules.get(i));
entry.put("order", i + 1);
paymentAllocationOrder.add(entry);
}
return map;
}
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream()
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
return list;
}
@NotNull
private JsonCommand createJsonCommand(Map<String, Object> jsonMap) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
JsonCommand command = JsonCommand.from(json, JsonParser.parseString(json), fromJsonHelper, null, 1L, 2L, 3L, 4L, null, null, null,
null, null, null, null, null);
return command;
}
}
@@ -0,0 +1,169 @@
/**
* 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 static org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule.LAST_INSTALLMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.REPAYMENT;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
class AdvancedPaymentAllocationsValidatorTest {
private AdvancedPaymentAllocationsValidator underTest = new AdvancedPaymentAllocationsValidator();
@Test
public void testPaymentAllocationsHasNoError() {
underTest.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList());
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenLessElement() {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(createPaymentAllocationTypeList().subList(0, 11)));
assertPlatformException("Each provided payment allocation must contain exactly 12 allocation rules, but 11 were provided",
"advanced-payment-strategy.each_payment_allocation_order.must.contain.12.entries", validationException);
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenWithDuplicate() {
ArrayList<Pair<Integer, PaymentAllocationType>> pairs = new ArrayList<>(createPaymentAllocationTypeList().subList(0, 11));
pairs.add(pairs.get(10));
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(pairs));
assertPlatformException("The list of provided payment allocation rules must not contain any duplicates",
"advanced-payment-strategy.must.not.have.duplicate.payment.allocation.rule", validationException);
}
@Test
public void testPaymentAllocationsValidationThrowsErrorWhenOrderIsNotInRange() {
List<Pair<Integer, PaymentAllocationType>> pairs = createPaymentAllocationTypeList().stream()
.map(p -> Pair.of(p.getLeft() + 1, p.getRight())).toList();
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validatePairOfOrderAndPaymentAllocationType(pairs));
assertPlatformException("The provided orders must be between 1 and 12", "advanced-payment-strategy.invalid.order",
validationException);
}
@Test
public void testValidateNoError() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
}
@Test
public void testValidatePaymentAllocationThrowsErrorWhenNoDefault() {
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
assertPlatformValidationException(
"Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided",
"advanced-payment-strategy-without-default-payment-allocation",
() -> underTest.validate(List.of(lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenDuplicate() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
LoanProductPaymentAllocationRule lppr3 = createLoanProductAllocationRule2();
assertPlatformValidationException("The same transaction type must be provided only once",
"advanced-payment-strategy-with-duplicate-payment-allocation",
() -> underTest.validate(List.of(lppr1, lppr2, lppr3), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenPaymentAllocationProvidedWithOtherStrategy() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule2();
assertPlatformValidationException("In case 'some-other-strategy' payment strategy, payment_allocation must not be provided",
"payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy",
() -> underTest.validate(List.of(lppr1, lppr2), "some-other-strategy"));
}
@Test
public void testValidateThrowsErrorWhenFutureInstallmentIsEmpty() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
lppr1.setFutureInstallmentAllocationRule(null);
assertPlatformValidationException("Payment allocation was provided without a valid future installment allocation rule",
"advanced-payment-strategy.with.not.valid.future.installment.allocation.rule",
() -> underTest.validate(List.of(lppr1), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
@Test
public void testValidateThrowsErrorWhenTransactionTypeEmpty() {
LoanProductPaymentAllocationRule lppr1 = createLoanProductAllocationRule1();
LoanProductPaymentAllocationRule lppr2 = createLoanProductAllocationRule1();
lppr2.setTransactionType(null);
assertPlatformValidationException("Payment allocation was provided with a not valid transaction type",
"advanced-payment-strategy.with.not.valid.transaction.type",
() -> underTest.validate(List.of(lppr1, lppr2), ADVANCED_PAYMENT_ALLOCATION_STRATEGY));
}
private void assertPlatformValidationException(String message, String code, Executable executable) {
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
assertPlatformException(message, code, validationException);
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule2() {
LoanProductPaymentAllocationRule lppr2 = new LoanProductPaymentAllocationRule();
lppr2.setTransactionType(REPAYMENT);
lppr2.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
ArrayList<PaymentAllocationType> allocationTypes = new ArrayList<>(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
Collections.shuffle(allocationTypes);
lppr2.setAllocationTypes(allocationTypes);
return lppr2;
}
@NotNull
private static LoanProductPaymentAllocationRule createLoanProductAllocationRule1() {
LoanProductPaymentAllocationRule lppr1 = new LoanProductPaymentAllocationRule();
lppr1.setTransactionType(DEFAULT);
lppr1.setFutureInstallmentAllocationRule(LAST_INSTALLMENT);
lppr1.setAllocationTypes(EnumSet.allOf(PaymentAllocationType.class).stream().toList());
return lppr1;
}
@NotNull
private static List<Pair<Integer, PaymentAllocationType>> createPaymentAllocationTypeList() {
AtomicInteger i = new AtomicInteger(1);
List<Pair<Integer, PaymentAllocationType>> list = EnumSet.allOf(PaymentAllocationType.class).stream()
.map(p -> Pair.of(i.getAndIncrement(), p)).toList();
return list;
}
private void assertPlatformException(String expectedMessage, String expectedCode,
PlatformApiDataValidationException platformApiDataValidationException) {
Assertions.assertEquals(expectedMessage, platformApiDataValidationException.getErrors().get(0).getDefaultUserMessage());
Assertions.assertEquals(expectedCode, platformApiDataValidationException.getErrors().get(0).getUserMessageGlobalisationCode());
}
}
@@ -0,0 +1,72 @@
/**
* 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.loanaccount.domain.transactionprocessor.impl;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
@Override
public String getCode() {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
}
@Override
public String getName() {
return "Advanced payment allocation strategy";
}
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
throw new NotImplementedException();
}
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
List<LoanRepaymentScheduleInstallment> installments, LoanTransaction loanTransaction, Money paymentInAdvance,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
throw new NotImplementedException();
}
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
throw new NotImplementedException();
}
@Override
protected Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
throw new NotImplementedException();
}
}
@@ -0,0 +1,30 @@
/**
* 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.loanaccount.starter;
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
public class AdvancedPaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
@Override
protected boolean matches(FineractProperties properties) {
return properties.getLoan().getTransactionProcessor().getAdvancedPaymentStrategy().isEnabled();
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor;
@@ -99,4 +100,11 @@ public class LoanAccountAutoStarter {
List<LoanRepaymentScheduleTransactionProcessor> processors) {
return new LoanRepaymentScheduleTransactionProcessorFactory(defaultLoanRepaymentScheduleTransactionProcessor, processors);
}
@Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() {
return new AdvancedPaymentScheduleTransactionProcessor();
}
}
@@ -113,6 +113,7 @@ final class LoanProductsApiResourceSwagger {
public Integer interestCalculationPeriodType;
@Schema(example = "mifos-standard-strategy")
public String transactionProcessingStrategyCode;
public List<AdvancedPaymentData> paymentAllocation;
@Schema(example = "false")
public Boolean isLinkedToFloatingInterestRates;
@Schema(example = "false")
@@ -1180,6 +1181,8 @@ final class LoanProductsApiResourceSwagger {
@Schema(example = "Mifos style")
public String transactionProcessingStrategyName;
@Schema(example = "[]")
public List<AdvancedPaymentData> paymentAllocation;
@Schema(example = "[]")
public List<Integer> charges;
public Set<GetLoanProductsPrincipalVariationsForBorrowerCycle> productsPrincipalVariationsForBorrowerCycle;
@Schema(example = "[]")
@@ -1294,6 +1297,7 @@ final class LoanProductsApiResourceSwagger {
public Integer interestCalculationPeriodType;
@Schema(example = "mifos-standard-strategy")
public String transactionProcessingStrategyCode;
public List<AdvancedPaymentData> paymentAllocation;
@Schema(example = "false")
public Boolean isLinkedToFloatingInterestRates;
@Schema(example = "false")
@@ -1485,6 +1489,26 @@ final class LoanProductsApiResourceSwagger {
}
public static final class AdvancedPaymentData {
@Schema(example = "DEFAULT")
public String transactionType;
@Schema(example = "[]")
public List<PaymentAllocationOrder> paymentAllocationOrder;
@Schema(example = "NEXT_INSTALLMENT")
public String futureInstallmentAllocationRule;
}
public static class PaymentAllocationOrder {
@Schema(example = "DUE_PAST_PENALTY")
public String paymentAllocationRule;
@Schema(example = "1")
public Integer order;
}
@Schema(description = "PutLoanProductsProductIdResponse")
public static final class PutLoanProductsProductIdResponse {
@@ -0,0 +1,41 @@
/**
* 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.data;
import java.io.Serializable;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class AdvancedPaymentData implements Serializable {
private final String transactionType;
private final String futureInstallmentAllocationRule;
private final List<PaymentAllocationOrder> paymentAllocationOrder;
@Getter
@AllArgsConstructor
public static class PaymentAllocationOrder implements Serializable {
private final String paymentAllocationRule;
private final Integer order;
}
}
@@ -109,6 +109,7 @@ public class LoanProductData implements Serializable {
private final BigDecimal inArrearsTolerance;
private final String transactionProcessingStrategyCode;
private final String transactionProcessingStrategyName;
private final Collection<AdvancedPaymentData> paymentAllocation;
private final Integer graceOnPrincipalPayment;
private final Integer recurringMoratoriumOnPrincipalPeriods;
private final Integer graceOnInterestPayment;
@@ -287,6 +288,7 @@ public class LoanProductData implements Serializable {
final Integer overDueDaysForRepaymentEvent = null;
final boolean enableDownPayment = false;
final BigDecimal disbursedAmountPercentageDownPayment = null;
final Collection<AdvancedPaymentData> paymentAllocation = null;
final boolean enableAutoRepaymentForDownPayment = false;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
@@ -306,7 +308,8 @@ public class LoanProductData implements Serializable {
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment);
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
}
@@ -399,6 +402,7 @@ public class LoanProductData implements Serializable {
final boolean enableDownPayment = false;
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -417,7 +421,8 @@ public class LoanProductData implements Serializable {
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment);
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
}
@@ -517,6 +522,7 @@ public class LoanProductData implements Serializable {
final boolean enableDownPayment = false;
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -535,7 +541,8 @@ public class LoanProductData implements Serializable {
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment);
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
}
@@ -629,6 +636,7 @@ public class LoanProductData implements Serializable {
final boolean enableDownPayment = false;
final BigDecimal disbursedAmountPercentageDownPayment = null;
final boolean enableAutoRepaymentForDownPayment = false;
final Collection<AdvancedPaymentData> paymentAllocation = null;
return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance,
numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod,
@@ -647,8 +655,8 @@ public class LoanProductData implements Serializable {
maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap,
syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled,
fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent,
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment);
overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment,
paymentAllocation);
}
public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings,
@@ -695,7 +703,8 @@ public class LoanProductData implements Serializable {
final BigDecimal fixedPrincipalPercentagePerInstallment, final Collection<DelinquencyBucketData> delinquencyBucketOptions,
final DelinquencyBucketData delinquencyBucket, final Integer dueDaysForRepaymentEvent,
final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment,
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment) {
final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment,
final Collection<AdvancedPaymentData> paymentAllocation) {
this.id = id;
this.name = name;
this.shortName = shortName;
@@ -815,6 +824,7 @@ public class LoanProductData implements Serializable {
this.overDueDaysForRepaymentEvent = overDueDaysForRepaymentEvent;
this.enableDownPayment = enableDownPayment;
this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment;
this.paymentAllocation = paymentAllocation;
this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment;
}
@@ -970,6 +980,7 @@ public class LoanProductData implements Serializable {
this.enableDownPayment = productData.enableDownPayment;
this.disbursedAmountPercentageForDownPayment = productData.disbursedAmountPercentageForDownPayment;
this.enableAutoRepaymentForDownPayment = productData.enableAutoRepaymentForDownPayment;
this.paymentAllocation = productData.paymentAllocation;
}
private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
@@ -83,6 +83,7 @@ public final class LoanProductDataValidator {
public static final String INTEREST_CALCULATION_PERIOD_TYPE = "interestCalculationPeriodType";
public static final String IN_ARREARS_TOLERANCE = "inArrearsTolerance";
public static final String TRANSACTION_PROCESSING_STRATEGY_CODE = "transactionProcessingStrategyCode";
public static final String ADVANCED_PAYMENT_ALLOCATIONS = "paymentAllocation";
public static final String GRACE_ON_PRINCIPAL_PAYMENT = "graceOnPrincipalPayment";
public static final String GRACE_ON_INTEREST_PAYMENT = "graceOnInterestPayment";
public static final String GRACE_ON_INTEREST_CHARGED = "graceOnInterestCharged";
@@ -106,10 +107,10 @@ public final class LoanProductDataValidator {
NUMBER_OF_REPAYMENTS, MIN_NUMBER_OF_REPAYMENTS, MAX_NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY_TYPE, INTEREST_RATE_PER_PERIOD,
MIN_INTEREST_RATE_PER_PERIOD, MAX_INTEREST_RATE_PER_PERIOD, INTEREST_RATE_FREQUENCY_TYPE, AMORTIZATION_TYPE, INTEREST_TYPE,
INTEREST_CALCULATION_PERIOD_TYPE, LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME,
IN_ARREARS_TOLERANCE, TRANSACTION_PROCESSING_STRATEGY_CODE, GRACE_ON_PRINCIPAL_PAYMENT, "recurringMoratoriumOnPrincipalPeriods",
GRACE_ON_INTEREST_PAYMENT, GRACE_ON_INTEREST_CHARGED, "charges", ACCOUNTING_RULE, INCLUDE_IN_BORROWER_CYCLE, "startDate",
"closeDate", "externalId", IS_LINKED_TO_FLOATING_INTEREST_RATES, FLOATING_RATES_ID, INTEREST_RATE_DIFFERENTIAL,
MIN_DIFFERENTIAL_LENDING_RATE, DEFAULT_DIFFERENTIAL_LENDING_RATE, MAX_DIFFERENTIAL_LENDING_RATE,
IN_ARREARS_TOLERANCE, TRANSACTION_PROCESSING_STRATEGY_CODE, ADVANCED_PAYMENT_ALLOCATIONS, GRACE_ON_PRINCIPAL_PAYMENT,
"recurringMoratoriumOnPrincipalPeriods", GRACE_ON_INTEREST_PAYMENT, GRACE_ON_INTEREST_CHARGED, "charges", ACCOUNTING_RULE,
INCLUDE_IN_BORROWER_CYCLE, "startDate", "closeDate", "externalId", IS_LINKED_TO_FLOATING_INTEREST_RATES, FLOATING_RATES_ID,
INTEREST_RATE_DIFFERENTIAL, MIN_DIFFERENTIAL_LENDING_RATE, DEFAULT_DIFFERENTIAL_LENDING_RATE, MAX_DIFFERENTIAL_LENDING_RATE,
IS_FLOATING_INTEREST_RATE_CALCULATION_ALLOWED, "syncExpectedWithDisbursementDate",
LoanProductAccountingParams.FEES_RECEIVABLE.getValue(), LoanProductAccountingParams.FUND_SOURCE.getValue(),
LoanProductAccountingParams.INCOME_FROM_FEES.getValue(), LoanProductAccountingParams.INCOME_FROM_PENALTIES.getValue(),
@@ -0,0 +1,99 @@
/**
* 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.service;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
public class LoanProductPaymentAllocationRuleMerger {
public boolean updateProductPaymentAllocationRules(LoanProduct loanProduct,
final List<LoanProductPaymentAllocationRule> newLoanProductPaymentAllocationRules) {
if (newLoanProductPaymentAllocationRules == null) {
return false;
}
boolean updated = false;
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> originalItems = loanProduct
.getLoanProductPaymentAllocationRules().stream()
.collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> newItems = newLoanProductPaymentAllocationRules.stream()
.collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
// elements to be deleted
Set<PaymentAllocationTransactionType> existing = new HashSet<>(originalItems.keySet());
Set<PaymentAllocationTransactionType> newSet = new HashSet<>(newItems.keySet());
existing.removeAll(newSet);
if (existing.size() > 0) {
updated = true;
existing.forEach(type -> {
loanProduct.getLoanProductPaymentAllocationRules().remove(originalItems.get(type));
});
}
// elements to be added
existing = new HashSet<>(originalItems.keySet());
newSet = new HashSet<>(newItems.keySet());
newSet.removeAll(existing);
if (newSet.size() > 0) {
updated = true;
newSet.forEach(type -> {
loanProduct.getLoanProductPaymentAllocationRules().add(newItems.get(type));
});
}
// elements to be merged
existing = new HashSet<>(originalItems.keySet());
newSet = new HashSet<>(newItems.keySet());
existing.retainAll(newSet);
for (PaymentAllocationTransactionType type : existing) {
boolean result = mergeLoanProductPaymentAllocationRule(originalItems.get(type), newItems.get(type));
if (result) {
updated = true;
}
}
return updated;
}
private boolean mergeLoanProductPaymentAllocationRule(LoanProductPaymentAllocationRule into,
LoanProductPaymentAllocationRule newElement) {
boolean changed = false;
if (!Objects.equals(into.getFutureInstallmentAllocationRule(), newElement.getFutureInstallmentAllocationRule())) {
into.setFutureInstallmentAllocationRule(newElement.getFutureInstallmentAllocationRule());
changed = true;
}
if (!Objects.equals(into.getAllocationTypes(), newElement.getAllocationTypes())) {
into.setAllocationTypes(newElement.getAllocationTypes());
changed = true;
}
return changed;
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanproduct.service;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
@@ -50,5 +51,7 @@ public interface LoanProductReadPlatformService {
Collection<LoanProductBorrowerCycleVariationData> retrieveLoanProductBorrowerCycleVariations(Long loanProductId);
Collection<AdvancedPaymentData> retrieveAdvancedPaymentData(Long loanProductId);
LoanProductData retrieveLoanProductFloatingDetails(Long loanProductId);
}
@@ -23,7 +23,10 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingEnumerations;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@@ -40,6 +43,8 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
import org.apache.fineract.portfolio.common.service.CommonEnumerations;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData;
import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData.PaymentAllocationOrder;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductGuaranteeData;
@@ -78,9 +83,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final Collection<RateData> rates = this.rateReadService.retrieveProductLoanRates(loanProductId);
final Collection<LoanProductBorrowerCycleVariationData> borrowerCycleVariationDatas = retrieveLoanProductBorrowerCycleVariations(
loanProductId);
final Collection<AdvancedPaymentData> advancedPaymentData = retrieveAdvancedPaymentData(loanProductId);
final Collection<DelinquencyBucketData> delinquencyBucketOptions = this.delinquencyReadPlatformService
.retrieveAllDelinquencyBuckets();
final LoanProductMapper rm = new LoanProductMapper(charges, borrowerCycleVariationDatas, rates, delinquencyBucketOptions);
final LoanProductMapper rm = new LoanProductMapper(charges, borrowerCycleVariationDatas, rates, delinquencyBucketOptions,
advancedPaymentData);
final String sql = "select " + rm.loanProductSchema() + " where lp.id = ?";
return this.jdbcTemplate.queryForObject(sql, rm, loanProductId); // NOSONAR
@@ -102,12 +109,19 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
return this.jdbcTemplate.query(sql, rm, loanProductId); // NOSONAR
}
@Override
public List<AdvancedPaymentData> retrieveAdvancedPaymentData(final Long loanProductId) {
final AdvancedPaymentDataMapper apdm = new AdvancedPaymentDataMapper();
final String sql = "select " + apdm.schema() + " where loan_product_id = ?";
return this.jdbcTemplate.query(sql, apdm, loanProductId); // NOSONAR
}
@Override
public Collection<LoanProductData> retrieveAllLoanProducts() {
this.context.authenticatedUser();
final LoanProductMapper rm = new LoanProductMapper(null, null, null, null);
final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null);
String sql = "select " + rm.loanProductSchema();
@@ -184,17 +198,20 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
private final Collection<LoanProductBorrowerCycleVariationData> borrowerCycleVariationDatas;
private final Collection<AdvancedPaymentData> advancedPaymentData;
private final Collection<RateData> rates;
private final Collection<DelinquencyBucketData> delinquencyBucketOptions;
LoanProductMapper(final Collection<ChargeData> charges,
final Collection<LoanProductBorrowerCycleVariationData> borrowerCycleVariationDatas, final Collection<RateData> rates,
final Collection<DelinquencyBucketData> delinquencyBucketOptions) {
final Collection<DelinquencyBucketData> delinquencyBucketOptions, Collection<AdvancedPaymentData> advancedPaymentData) {
this.charges = charges;
this.borrowerCycleVariationDatas = borrowerCycleVariationDatas;
this.rates = rates;
this.delinquencyBucketOptions = delinquencyBucketOptions;
this.advancedPaymentData = advancedPaymentData;
}
public String loanProductSchema() {
@@ -504,7 +521,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates,
isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket,
dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment,
enableAutoRepaymentForDownPayment);
enableAutoRepaymentForDownPayment, advancedPaymentData);
}
}
@@ -548,6 +565,30 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
}
}
private static final class AdvancedPaymentDataMapper implements RowMapper<AdvancedPaymentData> {
public String schema() {
return "transaction_type, allocation_types, future_installment_allocation_rule from m_loan_product_payment_allocation_rule";
}
@Override
public AdvancedPaymentData mapRow(ResultSet rs, int rowNum) throws SQLException {
final String transactionType = rs.getString("transaction_type");
final String allocationTypes = rs.getString("allocation_types");
final String futureInstallmentAllocationRule = rs.getString("future_installment_allocation_rule");
return new AdvancedPaymentData(transactionType, futureInstallmentAllocationRule, convert(allocationTypes));
}
private List<PaymentAllocationOrder> convert(String futureInstallmentAllocationRule) {
String[] allocationRule = futureInstallmentAllocationRule.split(",");
AtomicInteger order = new AtomicInteger(1);
return Arrays.stream(allocationRule) //
.map(s -> new PaymentAllocationOrder(s, order.getAndIncrement())) //
.toList();
}
}
private static final class LoanProductBorrowerCycleMapper implements RowMapper<LoanProductBorrowerCycleVariationData> {
public String schema() {
@@ -579,7 +620,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
public Collection<LoanProductData> retrieveAllLoanProductsForCurrency(String currencyCode) {
this.context.authenticatedUser();
final LoanProductMapper rm = new LoanProductMapper(null, null, null, null);
final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null);
String sql = "select " + rm.loanProductSchema() + " where lp.currency_code= ? ";
@@ -50,7 +50,9 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTra
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.AdvancedPaymentAllocationsJsonParser;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductCannotBeModifiedDueToNonClosedLoansException;
@@ -85,6 +87,8 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
private final BusinessEventNotifierService businessEventNotifierService;
private final DelinquencyBucketRepository delinquencyBucketRepository;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser;
private final LoanProductPaymentAllocationRuleMerger loanProductPaymentAllocationRuleMerger = new LoanProductPaymentAllocationRuleMerger();
@Transactional
@Override
@@ -104,14 +108,15 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
final String currencyCode = command.stringValueOfParameterNamed("currencyCode");
final List<Charge> charges = assembleListOfProductCharges(command, currencyCode);
final List<Rate> rates = assembleListOfProductRates(command);
final List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentJsonParser
.assembleLoanProductPaymentAllocationRules(command, loanTransactionProcessingStrategyCode);
FloatingRate floatingRate = null;
if (command.parameterExists("floatingRatesId")) {
floatingRate = this.floatingRateRepository
.findOneWithNotFoundDetection(command.longValueOfParameterNamed("floatingRatesId"));
}
final LoanProduct loanProduct = LoanProduct.assembleFromJson(fund, loanTransactionProcessingStrategyCode, charges, command,
this.aprCalculator, floatingRate, rates);
this.aprCalculator, floatingRate, rates, loanProductPaymentAllocationRules);
loanProduct.updateLoanProductInRelatedClasses();
loanProduct.setTransactionProcessingStrategyName(
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(loanTransactionProcessingStrategyCode).getName());
@@ -212,6 +217,17 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
}
}
if (changes.containsKey("paymentAllocation")) {
final List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = advancedPaymentJsonParser
.assembleLoanProductPaymentAllocationRules(command, product.getTransactionProcessingStrategyCode());
loanProductPaymentAllocationRules.forEach(lppar -> lppar.setLoanProduct(product));
final boolean updated = loanProductPaymentAllocationRuleMerger.updateProductPaymentAllocationRules(product,
loanProductPaymentAllocationRules);
if (!updated) {
changes.remove("paymentAllocation");
}
}
// accounting related changes
final boolean accountingTypeChanged = changes.containsKey("accountingRule");
final Map<String, Object> accountingMappingChanges = this.accountMappingWritePlatformService
@@ -138,6 +138,7 @@ fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=${F
fineract.loan.transactionprocessor.rbi-india.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_RBI_INDIA_ENABLED:true}
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_ENABLED:true}
fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_ENABLED:true}
fineract.loan.transactionprocessor.advanced-payment-strategy.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_ADVANCED_PAYMENT_STRATEGY_ENABLED:true}
fineract.loan.transactionprocessor.error-not-found-fail=${FINERACT_LOAN_TRANSACTIONPROCESSOR_ERROR_NOT_FOUND_FAIL:true}
fineract.content.regex-whitelist-enabled=${FINERACT_CONTENT_REGEX_WHITELIST_ENABLED:true}
@@ -0,0 +1,136 @@
/**
* 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.service;
import static org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule.LAST_INSTALLMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.DEFAULT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType.REPAYMENT;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.DUE_INTEREST;
import static org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType.PAST_DUE_FEE;
import java.util.List;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class LoanProductPaymentAllocationRuleMergerTest {
@Test
public void testMergerOneNewAdded() {
// given
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
// when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule1));
// then
Assertions.assertTrue(result);
Assertions.assertEquals(1, loanProduct.getLoanProductPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0));
}
@Test
public void testMergeExistingUpdated() {
// given
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
LoanProductPaymentAllocationRule rule2 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE));
loanProduct.getLoanProductPaymentAllocationRules().add(rule1);
// when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2));
// then
Assertions.assertTrue(result);
Assertions.assertEquals(1, loanProduct.getLoanProductPaymentAllocationRules().size());
Assertions.assertEquals(PAST_DUE_FEE, loanProduct.getLoanProductPaymentAllocationRules().get(0).getAllocationTypes().get(0));
}
@Test
public void testNothingChanged() {
// given
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE));
loanProduct.getLoanProductPaymentAllocationRules().addAll(List.of(rule1, rule2));
// when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2, rule1));
// then
Assertions.assertFalse(result);
Assertions.assertEquals(2, loanProduct.getLoanProductPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0));
Assertions.assertEquals(rule2, loanProduct.getLoanProductPaymentAllocationRules().get(1));
}
@Test
public void testMergerExistingDeleted() {
// given
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
loanProduct.getLoanProductPaymentAllocationRules().add(rule1);
// when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of());
// then
Assertions.assertTrue(result);
Assertions.assertEquals(0, loanProduct.getLoanProductPaymentAllocationRules().size());
}
@Test
public void testMergeOneOriginalOneAdded() {
// given
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
loanProduct.getLoanProductPaymentAllocationRules().add(rule1);
LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
// when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule1, rule2));
// then
Assertions.assertTrue(result);
Assertions.assertEquals(2, loanProduct.getLoanProductPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0));
Assertions.assertEquals(rule2, loanProduct.getLoanProductPaymentAllocationRules().get(1));
}
public LoanProductPaymentAllocationRule createRule(PaymentAllocationTransactionType transactionType,
FutureInstallmentAllocationRule futureInstallmentAllocationRule, List<PaymentAllocationType> allocationTypeList) {
return new LoanProductPaymentAllocationRule(null, transactionType, allocationTypeList, futureInstallmentAllocationRule);
}
}
@@ -70,6 +70,7 @@ fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=tru
fineract.loan.transactionprocessor.rbi-india.enabled=true
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=true
fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee.enabled=true
fineract.loan.transactionprocessor.advanced-payment-strategy.enabled=true
fineract.loan.transactionprocessor.error-not-found-fail=true
fineract.content.regex-whitelist-enabled=true
@@ -0,0 +1,286 @@
/**
* 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.integrationtests;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.GetFinancialActivityAccountsResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostFinancialActivityAccountsRequest;
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
import org.apache.fineract.client.util.CallFailedRuntimeException;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@Slf4j
@ExtendWith(LoanTestLifecycleExtension.class)
public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
private static ResponseSpecification RESPONSE_SPEC;
private static RequestSpecification REQUEST_SPEC;
private static Account ASSET_ACCOUNT;
private static Account FEE_PENALTY_ACCOUNT;
private static Account TRANSFER_ACCOUNT;
private static Account EXPENSE_ACCOUNT;
private static Account INCOME_ACCOUNT;
private static Account OVERPAYMENT_ACCOUNT;
private static FinancialActivityAccountHelper FINANCIAL_ACTIVITY_ACCOUNT_HELPER;
private static LoanTransactionHelper LOAN_TRANSACTION_HELPER;
@BeforeAll
public static void setupTests() {
Utils.initializeRESTAssured();
REQUEST_SPEC = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build();
AccountHelper accountHelper = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC);
FINANCIAL_ACTIVITY_ACCOUNT_HELPER = new FinancialActivityAccountHelper(REQUEST_SPEC);
LOAN_TRANSACTION_HELPER = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC);
ASSET_ACCOUNT = accountHelper.createAssetAccount();
FEE_PENALTY_ACCOUNT = accountHelper.createAssetAccount();
TRANSFER_ACCOUNT = accountHelper.createAssetAccount();
EXPENSE_ACCOUNT = accountHelper.createExpenseAccount();
INCOME_ACCOUNT = accountHelper.createIncomeAccount();
OVERPAYMENT_ACCOUNT = accountHelper.createLiabilityAccount();
setProperFinancialActivity(TRANSFER_ACCOUNT);
}
@Test
public void testCreateAndReadLoanProductWithAdvancedPayment() {
// given
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
// when
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
// then
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(first.isPresent());
Assertions.assertEquals(defaultAllocation, first.get());
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "REPAYMENT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(second.isPresent());
Assertions.assertEquals(repaymentPaymentAllocation, second.get());
}
@Test
public void testUpdateLoanProductOneAllocationIsRemoved() {
// given a loan with two allocations
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
// when an allocation is removed
LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), updateLoanProductRequest(defaultAllocation));
// then it shall be removed.
loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(1, loanProduct.getPaymentAllocation().size());
Assertions.assertEquals(defaultAllocation, loanProduct.getPaymentAllocation().get(0));
}
@Test
public void testUpdateLoanProductOneAllocationIsAdded() {
// given a loan with one allocations
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(1, loanProduct.getPaymentAllocation().size());
// when a new allocation is added
LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
updateLoanProductRequest(defaultAllocation, repaymentPaymentAllocation));
// then it shall be added.
loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(first.isPresent());
Assertions.assertEquals(defaultAllocation, first.get());
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
.filter(advancedPaymentData -> "REPAYMENT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(second.isPresent());
Assertions.assertEquals(repaymentPaymentAllocation, second.get());
}
@Test
public void testUpdateShouldFailWhenNoDefaultAllocationIsProvided() {
// given a loan with two allocations
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
// when an allocation is removed
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
updateLoanProductRequest(repaymentPaymentAllocation)));
Assertions.assertTrue(callFailedRuntimeException.getMessage()
.contains("Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided"));
}
@Test
public void testUpdateShouldFailWhenStrategyIsChangedBackButPaymentAllocationsAreNotRemoved() {
// given a loan with two allocations
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation, repaymentPaymentAllocation));
Assertions.assertNotNull(loanProductId);
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
// when an allocation is removed
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
updateLoanProductRequest("mifos-standard-strategy")));
Assertions.assertTrue(callFailedRuntimeException.getMessage()
.contains("In case 'mifos-standard-strategy' payment strategy, payment_allocation must not be provided"));
}
@Test
public void testCreateShouldFailWhenNoDefaultAllocationIsProvided() {
// given
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(400).build();
LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse);
// when
List<Map<String, String>> loanProductError = validationErrorHelper.getLoanProductError(createLoanJSON(repaymentPaymentAllocation),
"errors");
Assertions.assertEquals("Advanced-payment-allocation-strategy was selected but no DEFAULT payment allocation was provided",
loanProductError.get(0).get("defaultUserMessage"));
}
private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) {
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4")
.withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
.withAccountingRulePeriodicAccrual(new Account[] { ASSET_ACCOUNT, EXPENSE_ACCOUNT, INCOME_ACCOUNT, OVERPAYMENT_ACCOUNT })
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
.withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).addAdvancedPaymentAllocation(advancedPaymentData).build();
return loanProductJSON;
}
private PutLoanProductsProductIdRequest updateLoanProductRequest(AdvancedPaymentData... advancedPaymentData) {
PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = new PutLoanProductsProductIdRequest();
putLoanProductsProductIdRequest.paymentAllocation(Arrays.stream(advancedPaymentData).toList());
return putLoanProductsProductIdRequest;
}
private PutLoanProductsProductIdRequest updateLoanProductRequest(String transactionProcessingStrategyCode) {
PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = new PutLoanProductsProductIdRequest();
putLoanProductsProductIdRequest.setTransactionProcessingStrategyCode(transactionProcessingStrategyCode);
return putLoanProductsProductIdRequest;
}
private AdvancedPaymentData createRepaymentPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("REPAYMENT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_INTEREST, PaymentAllocationType.PAST_DUE_PRINCIPAL,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private AdvancedPaymentData createDefaultPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder;
}).toList();
}
private static void setProperFinancialActivity(Account transferAccount) {
List<GetFinancialActivityAccountsResponse> financialMappings = FINANCIAL_ACTIVITY_ACCOUNT_HELPER.getAllFinancialActivityAccounts();
financialMappings.forEach(mapping -> FINANCIAL_ACTIVITY_ACCOUNT_HELPER.deleteFinancialActivityAccount(mapping.getId()));
FINANCIAL_ACTIVITY_ACCOUNT_HELPER.createFinancialActivityAccount(new PostFinancialActivityAccountsRequest()
.financialActivityId((long) AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue())
.glAccountId((long) transferAccount.getAccountID()));
}
}
@@ -140,7 +140,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "0", false)
.build(null, delinquencyBucketId);
ArrayList<HashMap> loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper
ArrayList<HashMap<String, Object>> loanProductErrorData = validationErrorHelper
.getLoanProductError(Utils.convertToJson(loanProductMap), CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.is.less.than.min",
@@ -150,7 +150,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap_1 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, "101", false)
.build(null, delinquencyBucketId);
loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_1),
loanProductErrorData = validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_1),
CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.is.greater.than.max",
@@ -160,7 +160,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap_2 = new LoanProductTestBuilder()
.withEnableDownPayment(enableDownPayment, "12.55555555", false).build(null, delinquencyBucketId);
loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_2),
loanProductErrorData = validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_2),
CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.scale.is.greater.than.6",
@@ -170,7 +170,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap_3 = new LoanProductTestBuilder().withEnableDownPayment(false, "12.5", false)
.build(null, delinquencyBucketId);
loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_3),
loanProductErrorData = validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_3),
CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.supported.only.for.enable.down.payment.true",
@@ -180,7 +180,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap_4 = new LoanProductTestBuilder().withEnableDownPayment(enableDownPayment, null, false)
.build(null, delinquencyBucketId);
loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_4),
loanProductErrorData = validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_4),
CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.disbursedAmountPercentageForDownPayment.required.for.enable.down.payment.true",
@@ -190,7 +190,7 @@ public class LoanProductWithDownPaymentConfigurationTest {
final HashMap<String, Object> loanProductMap_5 = new LoanProductTestBuilder().withEnableDownPayment(false, null, true).build(null,
delinquencyBucketId);
loanProductErrorData = (ArrayList<HashMap>) validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_5),
loanProductErrorData = validationErrorHelper.getLoanProductError(Utils.convertToJson(loanProductMap_5),
CommonConstants.RESPONSE_ERROR);
assertNotNull(loanProductErrorData);
assertEquals("validation.msg.loanproduct.enableAutoRepaymentForDownPayment.supported.only.for.enable.down.payment.true",
@@ -0,0 +1,71 @@
/**
* 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.integrationtests.common.loans;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
public class AdvancedPaymentAllocation extends HashMap<String, Object> {
public static class AdvancedPaymentAllocationBuilder {
private PaymentAllocationTransactionType transactionType;
private FutureInstallmentAllocationRule futureInstallmentAllocationRule;
private List<PaymentAllocationType> paymentAllocationOrder = new ArrayList<>();
public AdvancedPaymentAllocationBuilder withTransactionType(PaymentAllocationTransactionType transactionType) {
this.transactionType = transactionType;
return this;
}
public AdvancedPaymentAllocationBuilder withFutureInstallmentAllocationRule(
FutureInstallmentAllocationRule futureInstallmentAllocationRule) {
this.futureInstallmentAllocationRule = futureInstallmentAllocationRule;
return this;
}
public AdvancedPaymentAllocationBuilder withPaymentAllocationType(PaymentAllocationType... allocationTypes) {
paymentAllocationOrder.addAll(Arrays.asList(allocationTypes));
return this;
}
public AdvancedPaymentAllocation build() {
AdvancedPaymentAllocation advancedPaymentAllocation = new AdvancedPaymentAllocation();
advancedPaymentAllocation.put("transactionType", transactionType.name());
advancedPaymentAllocation.put("futureInstallmentAllocationRule", futureInstallmentAllocationRule.name());
advancedPaymentAllocation.put("paymentAllocationOrder", getPaymentAllocationOrders());
return advancedPaymentAllocation;
}
public List<Map<String, Object>> getPaymentAllocationOrders() {
List<Map<String, Object>> result = new ArrayList<>();
for (int i = 0; i < paymentAllocationOrder.size(); i++) {
PaymentAllocationType paymentAllocationType = paymentAllocationOrder.get(i);
result.add(Map.of("paymentAllocationRule", paymentAllocationType.name(), "order", i + 1));
}
return result;
}
}
}
@@ -21,9 +21,11 @@ package org.apache.fineract.integrationtests.common.loans;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.accounting.Account;
@@ -88,6 +90,7 @@ public class LoanProductTestBuilder {
private String interestCalculationPeriodType = CALCULATION_PERIOD_SAME_AS_REPAYMENT_PERIOD;
private String inArrearsTolerance = "0";
private String transactionProcessingStrategyCode = DEFAULT_STRATEGY;
private List<AdvancedPaymentData> advancedPaymentAllocations = null;
private String accountingRule = NONE;
private final String currencyCode = USD;
private String amortizationType = EQUAL_INSTALLMENTS;
@@ -147,6 +150,11 @@ public class LoanProductTestBuilder {
private String disbursedAmountPercentageForDownPayment = null;
private boolean enableAutoRepaymentForDownPayment = false;
public String build() {
final HashMap<String, Object> map = build(null, null);
return new Gson().toJson(map);
}
public String build(final String chargeId) {
final HashMap<String, Object> map = build(chargeId, null);
return new Gson().toJson(map);
@@ -182,6 +190,7 @@ public class LoanProductTestBuilder {
map.put("interestCalculationPeriodType", this.interestCalculationPeriodType);
map.put("inArrearsTolerance", this.inArrearsTolerance);
map.put("transactionProcessingStrategyCode", this.transactionProcessingStrategyCode);
map.put("paymentAllocation", this.advancedPaymentAllocations);
map.put("accountingRule", this.accountingRule);
map.put("minPrincipal", this.minPrincipal);
map.put("maxPrincipal", this.maxPrincipal);
@@ -708,4 +717,10 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder addAdvancedPaymentAllocation(AdvancedPaymentData... advancedPaymentData) {
this.transactionProcessingStrategyCode = "advanced-payment-allocation-strategy";
this.advancedPaymentAllocations = new ArrayList<>(Arrays.stream(advancedPaymentData).toList());
return this;
}
}
@@ -46,6 +46,7 @@ import org.apache.fineract.client.models.DeleteLoansLoanIdChargesChargeIdRespons
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoanProductsResponse;
import org.apache.fineract.client.models.GetLoansApprovalTemplateResponse;
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdChargesTemplateResponse;
@@ -128,11 +129,17 @@ public class LoanTransactionHelper extends IntegrationTest {
return GSON.fromJson(response, GetLoanProductsProductIdResponse.class);
}
public GetLoanProductsResponse[] listAllLoanProducts() {
final String GET_LOANPRODUCT_URL = "/fineract-provider/api/v1/loanproducts?" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerGet(this.requestSpec, this.responseSpec, GET_LOANPRODUCT_URL);
return GSON.fromJson(response, GetLoanProductsResponse[].class);
}
public Integer getLoanProductId(final String loanProductJSON) {
return Utils.performServerPost(this.requestSpec, this.responseSpec, CREATE_LOAN_PRODUCT_URL, loanProductJSON, "resourceId");
}
public Object getLoanProductError(final String loanProductJSON, final String jsonAttributeToGetBack) {
public <T> T getLoanProductError(final String loanProductJSON, final String jsonAttributeToGetBack) {
return Utils.performServerPost(this.requestSpec, this.responseSpec, CREATE_LOAN_PRODUCT_URL, loanProductJSON,
jsonAttributeToGetBack);
}