diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AllocationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AllocationType.java index af9d894d4..ff27ecad7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AllocationType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/AllocationType.java @@ -18,9 +18,24 @@ */ package org.apache.fineract.portfolio.loanproduct.domain; +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; + +@RequiredArgsConstructor +@Getter public enum AllocationType { - PENALTY, // - FEE, // - PRINCIPAL, // - INTEREST // + + PENALTY("Penalty"), // + FEE("Fee"), // + PRINCIPAL("Principal"), // + INTEREST("Interest"); // + + private final String humanReadableName; + + public static List getValuesAsEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList(); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParser.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParser.java new file mode 100644 index 000000000..29c7fc38b --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParser.java @@ -0,0 +1,110 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproduct.domain; + +import com.google.common.base.Enums; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class CreditAllocationsJsonParser { + + public final CreditAllocationsValidator creditAllocationsValidator; + + public List assembleLoanProductCreditAllocationRules(final JsonCommand command, + String loanTransactionProcessingStrategyCode) { + JsonArray creditAllocation = command.arrayOfParameterNamed("creditAllocation"); + List productCreditAllocationRules = null; + if (creditAllocation != null) { + productCreditAllocationRules = creditAllocation.asList().stream().map(json -> { + Map map = json.getAsJsonObject().asMap(); + LoanProductCreditAllocationRule creditAllocationRule = new LoanProductCreditAllocationRule(); + populateCreditAllocationRules(map, creditAllocationRule); + populateTransactionType(map, creditAllocationRule); + return creditAllocationRule; + }).toList(); + } + creditAllocationsValidator.validate(productCreditAllocationRules, loanTransactionProcessingStrategyCode); + return productCreditAllocationRules; + } + + private void populateCreditAllocationRules(Map map, LoanProductCreditAllocationRule creditAllocationRule) { + JsonArray creditAllocationOrder = asJsonArrayOrNull(map.get("creditAllocationOrder")); + if (creditAllocationOrder != null) { + creditAllocationRule.setAllocationTypes(getAllocationTypes(creditAllocationOrder)); + } + } + + private void populateTransactionType(Map map, LoanProductCreditAllocationRule creditAllocationRule) { + String transactionType = asStringOrNull(map.get("transactionType")); + if (transactionType != null) { + creditAllocationRule.setTransactionType(Enums.getIfPresent(CreditAllocationTransactionType.class, transactionType).orNull()); + } + } + + @NotNull + private List getAllocationTypes(JsonArray allocationOrder) { + if (allocationOrder != null) { + List> parsedListWithOrder = allocationOrder.asList().stream().map(json -> { + Map map = json.getAsJsonObject().asMap(); + AllocationType allocationType = null; + String creditAllocationRule = asStringOrNull(map.get("creditAllocationRule")); + if (creditAllocationRule != null) { + allocationType = Enums.getIfPresent(AllocationType.class, creditAllocationRule).orNull(); + } + return Pair.of(asIntegerOrNull(map.get("order")), allocationType); + }).sorted(Comparator.comparing(Pair::getLeft)).toList(); + creditAllocationsValidator.validatePairOfOrderAndCreditAllocationType(parsedListWithOrder); + return parsedListWithOrder.stream().map(Pair::getRight).toList(); + } else { + return List.of(); + } + } + + private Integer asIntegerOrNull(JsonElement element) { + if (!element.isJsonNull()) { + return element.getAsInt(); + } + return null; + } + + private String asStringOrNull(JsonElement element) { + if (!element.isJsonNull()) { + return element.getAsString(); + } + return null; + } + + private JsonArray asJsonArrayOrNull(JsonElement element) { + if (!element.isJsonNull()) { + return element.getAsJsonArray(); + } + return null; + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidator.java new file mode 100644 index 000000000..fb1b1afa3 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidator.java @@ -0,0 +1,96 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanproduct.domain; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.springframework.stereotype.Service; + +@Service +public class CreditAllocationsValidator { + + public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; + + public void validate(List rules, String code) { + if (isAdvancedPaymentStrategy(code)) { + if (hasDuplicateTransactionTypes(rules)) { + raiseValidationError("advanced-payment-strategy-with-duplicate-credit-allocation", + "The same transaction type must be provided only once"); + } + + if (rules != null) { + for (LoanProductCreditAllocationRule rule : rules) { + validateAllocationRule(rule); + } + } + + } else { + if (hasLoanProductCreditAllocationRule(rules)) { + raiseValidationError("credit_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy", + "In case '" + code + "' payment strategy, creditAllocation must not be provided"); + } + } + } + + public void validatePairOfOrderAndCreditAllocationType(List> rules) { + if (rules.size() != 4) { + raiseValidationError("advanced-payment-strategy.each_credit_allocation_order.must.contain.4.entries", + "Each provided credit allocation must contain exactly 4 allocation rules, but " + rules.size() + " were provided"); + } + + List deduped = rules.stream().map(Pair::getRight).distinct().toList(); + if (deduped.size() != 4) { + raiseValidationError("advanced-payment-strategy.must.not.have.duplicate.credit.allocation.rule", + "The list of provided credit allocation rules must not contain any duplicates"); + } + + if (!Arrays.equals(IntStream.rangeClosed(1, 4).boxed().toArray(), rules.stream().map(Pair::getLeft).toArray())) { + raiseValidationError("advanced-payment-strategy.invalid.order", "The provided orders must be between 1 and 4"); + } + } + + private boolean hasDuplicateTransactionTypes(List rules) { + return rules != null + && rules.stream().map(LoanProductCreditAllocationRule::getTransactionType).distinct().toList().size() != rules.size(); + } + + private void validateAllocationRule(LoanProductCreditAllocationRule rule) { + if (rule.getTransactionType() == null) { + raiseValidationError("advanced-payment-strategy.with.not.valid.transaction.type", + "Credit allocation was provided with a not valid transaction type"); + } + } + + private boolean isAdvancedPaymentStrategy(String code) { + return ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(code); + } + + private boolean hasLoanProductCreditAllocationRule(List rules) { + return rules != null && rules.size() > 0; + } + + private void raiseValidationError(String globalisationMessageCode, String msg) { + throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg))); + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 6db51d8e5..83a848d60 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -675,6 +675,13 @@ public class LoanProduct extends AbstractPersistableCustom { } } + this.creditAllocationRules = creditAllocationRules; + if (this.creditAllocationRules != null) { + for (LoanProductCreditAllocationRule loanProductCreditAllocationRule : this.creditAllocationRules) { + loanProductCreditAllocationRule.setLoanProduct(this); + } + } + this.name = name.trim(); this.shortName = shortName.trim(); if (StringUtils.isNotBlank(description)) { @@ -773,6 +780,13 @@ public class LoanProduct extends AbstractPersistableCustom { "In case '" + transactionProcessingStrategyCode + "' payment strategy, payment_allocation must not be provided"); } + if (this.creditAllocationRules != null && creditAllocationRules.size() > 0 + && !transactionProcessingStrategyCode.equals("advanced-payment-allocation-strategy")) { + throw new LoanProductGeneralRuleException( + "creditAllocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy", + "In case '" + transactionProcessingStrategyCode + "' payment strategy, creditAllocation must not be provided"); + } + if (this.disallowExpectedDisbursements) { if (!this.isMultiDisburseLoan()) { throw new LoanProductGeneralRuleException("allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set", @@ -906,6 +920,10 @@ public class LoanProduct extends AbstractPersistableCustom { return this.paymentAllocationRules; } + public List getCreditAllocationRules() { + return this.creditAllocationRules; + } + public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) { this.loanConfigurableAttributes = loanConfigurableAttributes; } @@ -999,6 +1017,14 @@ public class LoanProduct extends AbstractPersistableCustom { } } + final String creditAllocationParamName = "creditAllocation"; + if (command.hasParameter(creditAllocationParamName)) { + final JsonArray jsonArray = command.arrayOfParameterNamed(creditAllocationParamName); + if (jsonArray != null) { + actualChanges.put(creditAllocationParamName, command.jsonFragment(creditAllocationParamName)); + } + } + final String chargesParamName = "charges"; if (command.hasParameter(chargesParamName)) { final JsonArray jsonArray = command.arrayOfParameterNamed(chargesParamName); diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java new file mode 100644 index 000000000..d33aa1fe8 --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsJsonParserTest.java @@ -0,0 +1,125 @@ +/** + * 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.CreditAllocationsValidator.ADVANCED_PAYMENT_ALLOCATION_STRATEGY; +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.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) +public class CreditAllocationsJsonParserTest { + + @InjectMocks + private CreditAllocationsJsonParser creditAllocationsJsonParser; + + @Mock + private CreditAllocationsValidator creditAllocationsValidator; + + private FromJsonHelper fromJsonHelper = new FromJsonHelper(); + + @Test + public void testEmptyJson() throws JsonProcessingException { + Map map = new HashMap<>(); + JsonCommand command = createJsonCommand(map); + + // when + List loanProductCreditAllocationRules = creditAllocationsJsonParser + .assembleLoanProductCreditAllocationRules(command, "other-strategy"); + + // then + Assertions.assertNull(loanProductCreditAllocationRules); + Mockito.verify(creditAllocationsValidator, times(1)).validate(null, "other-strategy"); + } + + @Test + public void testParseSingleChargebackAllocation() throws JsonProcessingException { + // given + Map map = new HashMap<>(); + List> creditAllocations = new ArrayList<>(); + map.put("creditAllocation", creditAllocations); + List allocationRule = EnumSet.allOf(AllocationType.class).stream().map(Enum::name).toList(); + creditAllocations.add(createCreditAllocationEntry("CHARGEBACK", allocationRule)); + JsonCommand command = createJsonCommand(map); + + // when + List creditAllocationRules = creditAllocationsJsonParser + .assembleLoanProductCreditAllocationRules(command, ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + // then + Assertions.assertEquals(1, creditAllocationRules.size()); + Assertions.assertEquals(CreditAllocationTransactionType.CHARGEBACK, creditAllocationRules.get(0).getTransactionType()); + Assertions.assertEquals(4, creditAllocationRules.get(0).getAllocationTypes().size()); + Assertions.assertEquals(EnumSet.allOf(AllocationType.class).stream().toList(), creditAllocationRules.get(0).getAllocationTypes()); + + Mockito.verify(creditAllocationsValidator, times(1)).validate(creditAllocationRules, ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + Mockito.verify(creditAllocationsValidator, times(1)).validatePairOfOrderAndCreditAllocationType(createAllocationTypeList()); + Mockito.verifyNoMoreInteractions(creditAllocationsValidator); + } + + private static List> createAllocationTypeList() { + AtomicInteger i = new AtomicInteger(1); + List> list = EnumSet.allOf(AllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)) + .toList(); + return list; + } + + public Map createCreditAllocationEntry(String transactionType, List orderedRules) { + Map map = new HashMap<>(); + map.put("transactionType", transactionType); + List> creditAllocationOrder = new ArrayList<>(); + map.put("creditAllocationOrder", creditAllocationOrder); + for (int i = 0; i < orderedRules.size(); i++) { + HashMap entry = new HashMap<>(); + entry.put("creditAllocationRule", orderedRules.get(i)); + entry.put("order", i + 1); + creditAllocationOrder.add(entry); + } + return map; + } + + @NotNull + private JsonCommand createJsonCommand(Map 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; + } + +} diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidatorTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidatorTest.java new file mode 100644 index 000000000..d5cca26ac --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/CreditAllocationsValidatorTest.java @@ -0,0 +1,132 @@ +/** + * 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.CreditAllocationTransactionType.CHARGEBACK; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +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; + +public class CreditAllocationsValidatorTest { + + private CreditAllocationsValidator underTest = new CreditAllocationsValidator(); + + @Test + public void testCreditAllocationsHasNoError() { + underTest.validatePairOfOrderAndCreditAllocationType(createCreditAllocationTypeList()); + } + + @Test + public void testCreditAllocationsValidationThrowsErrorWhenLessElement() { + PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, + () -> underTest.validatePairOfOrderAndCreditAllocationType(createCreditAllocationTypeList().subList(0, 3))); + assertPlatformException("Each provided credit allocation must contain exactly 4 allocation rules, but 3 were provided", + "advanced-payment-strategy.each_credit_allocation_order.must.contain.4.entries", validationException); + } + + @Test + public void testCreditAllocationsValidationThrowsErrorWhenWithDuplicate() { + ArrayList> pairs = new ArrayList<>(createCreditAllocationTypeList().subList(0, 3)); + pairs.add(pairs.get(2)); + PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, + () -> underTest.validatePairOfOrderAndCreditAllocationType(pairs)); + assertPlatformException("The list of provided credit allocation rules must not contain any duplicates", + "advanced-payment-strategy.must.not.have.duplicate.credit.allocation.rule", validationException); + } + + @Test + public void testCreditAllocationsValidationThrowsErrorWhenOrderIsNotInRange() { + List> pairs = createCreditAllocationTypeList().stream() + .map(p -> Pair.of(p.getLeft() + 1, p.getRight())).toList(); + PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, + () -> underTest.validatePairOfOrderAndCreditAllocationType(pairs)); + assertPlatformException("The provided orders must be between 1 and 4", "advanced-payment-strategy.invalid.order", + validationException); + } + + @Test + public void testValidateThrowsErrorWhenPaymentAllocationProvidedWithOtherStrategy() { + assertPlatformValidationException("In case 'some-other-strategy' payment strategy, creditAllocation must not be provided", + "credit_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy", + () -> underTest.validate(List.of(createLoanProductCreditAllocationRule1()), "some-other-strategy")); + } + + @Test + public void testValidateThrowsErrorWhenTransactionTypeEmpty() { + LoanProductCreditAllocationRule lpcar = createLoanProductCreditAllocationRule1(); + lpcar.setTransactionType(null); + assertPlatformValidationException("Credit allocation was provided with a not valid transaction type", + "advanced-payment-strategy.with.not.valid.transaction.type", + () -> underTest.validate(List.of(lpcar), ADVANCED_PAYMENT_ALLOCATION_STRATEGY)); + } + + @Test + public void testValidateNoError() { + underTest.validate(List.of(createLoanProductCreditAllocationRule1()), ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + } + + @Test + public void testValidateCreditAllocationIsOptional() { + underTest.validate(List.of(), ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + } + + @Test + public void testValidateThrowsErrorWhenDuplicate() { + assertPlatformValidationException("The same transaction type must be provided only once", + "advanced-payment-strategy-with-duplicate-credit-allocation", + () -> underTest.validate(List.of(createLoanProductCreditAllocationRule1(), createLoanProductCreditAllocationRule1()), + ADVANCED_PAYMENT_ALLOCATION_STRATEGY)); + } + + @NotNull + private static List> createCreditAllocationTypeList() { + AtomicInteger i = new AtomicInteger(1); + return EnumSet.allOf(AllocationType.class).stream().map(p -> Pair.of(i.getAndIncrement(), p)).toList(); + } + + @NotNull + private static LoanProductCreditAllocationRule createLoanProductCreditAllocationRule1() { + LoanProductCreditAllocationRule lpcr1 = new LoanProductCreditAllocationRule(); + lpcr1.setTransactionType(CHARGEBACK); + lpcr1.setAllocationTypes(EnumSet.allOf(AllocationType.class).stream().toList()); + return lpcr1; + } + + private void assertPlatformValidationException(String message, String code, Executable executable) { + PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable); + assertPlatformException(message, code, validationException); + } + + 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()); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index c87528c17..8be92659d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -81,6 +81,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; +import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; @@ -417,6 +419,8 @@ public class LoanProductsApiResource { final List advancedPaymentAllocationFutureInstallmentAllocationRules = FutureInstallmentAllocationRule .getValuesAsEnumOptionDataList(); final List advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList(); + final List creditAllocationTransactionTypes = CreditAllocationTransactionType.getValuesAsEnumOptionDataList(); + final List creditAllocationAllocationTypes = AllocationType.getValuesAsEnumOptionDataList(); return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, @@ -427,7 +431,8 @@ public class LoanProductsApiResource { interestRecalculationDayOfWeekTypeOptions, isRatesEnabled, delinquencyBucketOptions, repaymentStartDateTypeOptions, advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules, advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(), - LoanScheduleProcessingType.getValuesAsEnumOptionDataList()); + LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, + creditAllocationAllocationTypes); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index 7804fc93d..ed031bb64 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -114,6 +114,7 @@ final class LoanProductsApiResourceSwagger { @Schema(example = "mifos-standard-strategy") public String transactionProcessingStrategyCode; public List paymentAllocation; + public List creditAllocation; @Schema(example = "false") public Boolean isLinkedToFloatingInterestRates; @Schema(example = "false") @@ -1042,6 +1043,10 @@ final class LoanProductsApiResourceSwagger { public List advancedPaymentAllocationTypes; public List loanScheduleTypeOptions; public List loanScheduleProcessingTypeOptions; + + public List creditAllocationAllocationTypes; + public List creditAllocationTransactionTypes; + } @Schema(description = "GetLoanProductsProductIdResponse") @@ -1222,6 +1227,8 @@ final class LoanProductsApiResourceSwagger { @Schema(example = "[]") public List paymentAllocation; @Schema(example = "[]") + public List creditAllocation; + @Schema(example = "[]") public List charges; public Set productsPrincipalVariationsForBorrowerCycle; @Schema(example = "[]") @@ -1343,7 +1350,10 @@ final class LoanProductsApiResourceSwagger { public Integer interestCalculationPeriodType; @Schema(example = "mifos-standard-strategy") public String transactionProcessingStrategyCode; + @Schema(example = "[]") public List paymentAllocation; + @Schema(example = "[]") + public List creditAllocation; @Schema(example = "false") public Boolean isLinkedToFloatingInterestRates; @Schema(example = "false") @@ -1564,6 +1574,23 @@ final class LoanProductsApiResourceSwagger { public Integer order; } + public static final class CreditAllocationData { + + @Schema(example = "Chargeback") + public String transactionType; + @Schema(example = "[]") + public List creditAllocationOrder; + } + + public static class CreditAllocationOrder { + + @Schema(example = "PENALTY") + public String creditAllocationRule; + + @Schema(example = "1") + public Integer order; + } + @Schema(description = "PutLoanProductsProductIdResponse") public static final class PutLoanProductsProductIdResponse { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/CreditAllocationData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/CreditAllocationData.java new file mode 100644 index 000000000..917c818e1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/CreditAllocationData.java @@ -0,0 +1,40 @@ +/** + * 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 CreditAllocationData implements Serializable { + + private final String transactionType; + private final List creditAllocationOrder; + + @Getter + @AllArgsConstructor + public static class CreditAllocationOrder implements Serializable { + + private final String creditAllocationRule; + private final Integer order; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index c721add00..ddded6d7c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -46,7 +46,9 @@ import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; +import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -116,6 +118,7 @@ public class LoanProductData implements Serializable { private final String transactionProcessingStrategyCode; private final String transactionProcessingStrategyName; private final Collection paymentAllocation; + private final Collection creditAllocation; private final Integer graceOnPrincipalPayment; private final Integer recurringMoratoriumOnPrincipalPeriods; private final Integer graceOnInterestPayment; @@ -175,6 +178,10 @@ public class LoanProductData implements Serializable { private final List advancedPaymentAllocationTransactionTypes; private final List advancedPaymentAllocationFutureInstallmentAllocationRules; private final List advancedPaymentAllocationTypes; + + private final List creditAllocationTransactionTypes; + private final List creditAllocationAllocationTypes; + private final List loanScheduleTypeOptions; private final List loanScheduleProcessingTypeOptions; @@ -306,6 +313,7 @@ public class LoanProductData implements Serializable { final boolean enableDownPayment = false; final BigDecimal disbursedAmountPercentageDownPayment = null; final Collection paymentAllocation = null; + final Collection creditAllocation = null; final boolean enableAutoRepaymentForDownPayment = false; final EnumOptionData repaymentStartDateType = null; final boolean enableInstallmentLevelDelinquency = false; @@ -332,7 +340,8 @@ public class LoanProductData implements Serializable { syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, - paymentAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType); + paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, + loanScheduleProcessingType); } @@ -426,6 +435,7 @@ public class LoanProductData implements Serializable { final BigDecimal disbursedAmountPercentageDownPayment = null; final boolean enableAutoRepaymentForDownPayment = false; final Collection paymentAllocation = null; + final Collection creditAllocation = null; final EnumOptionData repaymentStartDateType = null; final boolean enableInstallmentLevelDelinquency = false; final EnumOptionData loanScheduleType = null; @@ -449,7 +459,8 @@ public class LoanProductData implements Serializable { syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, - paymentAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType); + paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, + loanScheduleProcessingType); } @@ -550,6 +561,7 @@ public class LoanProductData implements Serializable { final BigDecimal disbursedAmountPercentageDownPayment = null; final boolean enableAutoRepaymentForDownPayment = false; final Collection paymentAllocation = null; + final Collection creditAllocation = null; final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE); final boolean enableInstallmentLevelDelinquency = false; final EnumOptionData loanScheduleType = LoanScheduleType.CUMULATIVE.asEnumOptionData(); @@ -573,7 +585,8 @@ public class LoanProductData implements Serializable { syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, - paymentAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType); + paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, + loanScheduleProcessingType); } @@ -668,6 +681,7 @@ public class LoanProductData implements Serializable { final BigDecimal disbursedAmountPercentageDownPayment = null; final boolean enableAutoRepaymentForDownPayment = false; final Collection paymentAllocation = null; + final Collection creditAllocationData = null; final EnumOptionData repaymentStartDateType = LoanEnumerations.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE); final boolean enableInstallmentLevelDelinquency = false; final EnumOptionData loanScheduleType = null; @@ -691,7 +705,8 @@ public class LoanProductData implements Serializable { syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, - paymentAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType); + paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, + loanScheduleProcessingType); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -739,9 +754,9 @@ public class LoanProductData implements Serializable { final DelinquencyBucketData delinquencyBucket, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, - final Collection paymentAllocation, final EnumOptionData repaymentStartDateType, - final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, - final EnumOptionData loanScheduleProcessingType) { + final Collection paymentAllocation, final Collection creditAllocation, + final EnumOptionData repaymentStartDateType, final boolean enableInstallmentLevelDelinquency, + final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType) { this.id = id; this.name = name; this.shortName = shortName; @@ -862,12 +877,15 @@ public class LoanProductData implements Serializable { this.enableDownPayment = enableDownPayment; this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; this.paymentAllocation = paymentAllocation; + this.creditAllocation = creditAllocation; this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; this.repaymentStartDateType = repaymentStartDateType; this.repaymentStartDateTypeOptions = null; this.advancedPaymentAllocationTransactionTypes = PaymentAllocationTransactionType.getValuesAsEnumOptionDataList(); this.advancedPaymentAllocationFutureInstallmentAllocationRules = FutureInstallmentAllocationRule.getValuesAsEnumOptionDataList(); this.advancedPaymentAllocationTypes = PaymentAllocationType.getValuesAsEnumOptionDataList(); + this.creditAllocationTransactionTypes = CreditAllocationTransactionType.getValuesAsEnumOptionDataList(); + this.creditAllocationAllocationTypes = AllocationType.getValuesAsEnumOptionDataList(); this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency; this.loanScheduleType = loanScheduleType; this.loanScheduleProcessingType = loanScheduleProcessingType; @@ -893,7 +911,9 @@ public class LoanProductData implements Serializable { final List advancedPaymentAllocationTransactionTypes, final List advancedPaymentAllocationFutureInstallmentAllocationRules, final List advancedPaymentAllocationTypes, final List loanScheduleTypeOptions, - final List loanScheduleProcessingTypeOptions) { + final List loanScheduleProcessingTypeOptions, final List creditAllocationTransactionTypes, + final List creditAllocationAllocationTypes) { + this.id = productData.id; this.name = productData.name; this.shortName = productData.shortName; @@ -1032,11 +1052,14 @@ public class LoanProductData implements Serializable { this.disbursedAmountPercentageForDownPayment = productData.disbursedAmountPercentageForDownPayment; this.enableAutoRepaymentForDownPayment = productData.enableAutoRepaymentForDownPayment; this.paymentAllocation = productData.paymentAllocation; + this.creditAllocation = productData.creditAllocation; this.repaymentStartDateType = productData.repaymentStartDateType; this.repaymentStartDateTypeOptions = repaymentStartDateTypeOptions; this.advancedPaymentAllocationTransactionTypes = advancedPaymentAllocationTransactionTypes; this.advancedPaymentAllocationFutureInstallmentAllocationRules = advancedPaymentAllocationFutureInstallmentAllocationRules; this.advancedPaymentAllocationTypes = advancedPaymentAllocationTypes; + this.creditAllocationAllocationTypes = creditAllocationAllocationTypes; + this.creditAllocationTransactionTypes = creditAllocationTransactionTypes; this.enableInstallmentLevelDelinquency = productData.enableInstallmentLevelDelinquency; this.loanScheduleType = productData.getLoanScheduleType(); this.loanScheduleProcessingType = productData.getLoanScheduleProcessingType(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 5870ec09f..6a6abe9ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -91,6 +91,7 @@ public final class LoanProductDataValidator { 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 CREDIT_ALLOCATIONS = "creditAllocation"; 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"; @@ -114,17 +115,18 @@ 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, 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(), - LoanProductAccountingParams.INTEREST_ON_LOANS.getValue(), LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), - LoanProductAccountingParams.LOAN_PORTFOLIO.getValue(), LoanProductAccountingParams.OVERPAYMENT.getValue(), - LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), - LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), LoanProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), + IN_ARREARS_TOLERANCE, TRANSACTION_PROCESSING_STRATEGY_CODE, ADVANCED_PAYMENT_ALLOCATIONS, CREDIT_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(), LoanProductAccountingParams.INTEREST_ON_LOANS.getValue(), + LoanProductAccountingParams.INTEREST_RECEIVABLE.getValue(), LoanProductAccountingParams.LOAN_PORTFOLIO.getValue(), + LoanProductAccountingParams.OVERPAYMENT.getValue(), LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), + LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), + LoanProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), LoanProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), LoanProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMerger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMerger.java new file mode 100644 index 000000000..1704cbc11 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMerger.java @@ -0,0 +1,92 @@ +/** + * 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.CreditAllocationTransactionType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule; + +public class LoanProductCreditAllocationRuleMerger { + + public boolean updateCreditAllocationRules(LoanProduct loanProduct, + final List newLoanProductCreditAllocationRules) { + if (newLoanProductCreditAllocationRules == null) { + return false; + } + boolean updated = false; + Map originalItems = loanProduct.getCreditAllocationRules() + .stream().collect(Collectors.toMap(LoanProductCreditAllocationRule::getTransactionType, Function.identity())); + Map newItems = newLoanProductCreditAllocationRules.stream() + .collect(Collectors.toMap(LoanProductCreditAllocationRule::getTransactionType, Function.identity())); + + // elements to be deleted + Set existing = new HashSet<>(originalItems.keySet()); + Set newSet = new HashSet<>(newItems.keySet()); + existing.removeAll(newSet); + if (existing.size() > 0) { + updated = true; + existing.forEach(type -> { + loanProduct.getCreditAllocationRules().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.getCreditAllocationRules().add(newItems.get(type)); + }); + } + + // elements to be merged + existing = new HashSet<>(originalItems.keySet()); + newSet = new HashSet<>(newItems.keySet()); + existing.retainAll(newSet); + + for (CreditAllocationTransactionType type : existing) { + boolean result = mergeLoanProductCreditAllocationRule(originalItems.get(type), newItems.get(type)); + if (result) { + updated = true; + } + } + + return updated; + } + + private boolean mergeLoanProductCreditAllocationRule(LoanProductCreditAllocationRule into, LoanProductCreditAllocationRule newElement) { + boolean changed = false; + + if (!Objects.equals(into.getAllocationTypes(), newElement.getAllocationTypes())) { + into.setAllocationTypes(newElement.getAllocationTypes()); + changed = true; + } + + return changed; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java index 302334503..d6ddef51a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformService.java @@ -21,6 +21,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.CreditAllocationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -53,5 +54,7 @@ public interface LoanProductReadPlatformService { Collection retrieveAdvancedPaymentData(Long loanProductId); + Collection retrieveCreditAllocationData(Long loanProductId); + LoanProductData retrieveLoanProductFloatingDetails(Long loanProductId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 91b10f463..a09ac1f58 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -47,6 +47,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData; import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData.PaymentAllocationOrder; +import org.apache.fineract.portfolio.loanproduct.data.CreditAllocationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.LoanProductGuaranteeData; @@ -84,10 +85,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final Collection borrowerCycleVariationDatas = retrieveLoanProductBorrowerCycleVariations( loanProductId); final Collection advancedPaymentData = retrieveAdvancedPaymentData(loanProductId); + final Collection creditAllocationData = retrieveCreditAllocationData(loanProductId); final Collection delinquencyBucketOptions = this.delinquencyReadPlatformService .retrieveAllDelinquencyBuckets(); final LoanProductMapper rm = new LoanProductMapper(charges, borrowerCycleVariationDatas, rates, delinquencyBucketOptions, - advancedPaymentData); + advancedPaymentData, creditAllocationData); final String sql = "select " + rm.loanProductSchema() + " where lp.id = ?"; return this.jdbcTemplate.queryForObject(sql, rm, loanProductId); // NOSONAR @@ -116,12 +118,19 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo return this.jdbcTemplate.query(sql, apdm, loanProductId); // NOSONAR } + @Override + public List retrieveCreditAllocationData(final Long loanProductId) { + final CreditAllocationDataMapper cadm = new CreditAllocationDataMapper(); + final String sql = "select " + cadm.schema() + " where loan_product_id = ?"; + return this.jdbcTemplate.query(sql, cadm, loanProductId); // NOSONAR + } + @Override public Collection retrieveAllLoanProducts() { this.context.authenticatedUser(); - final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null); + final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null, null); String sql = "select " + rm.loanProductSchema(); @@ -200,18 +209,22 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo private final Collection advancedPaymentData; + private final Collection creditAllocationData; + private final Collection rates; private final Collection delinquencyBucketOptions; LoanProductMapper(final Collection charges, final Collection borrowerCycleVariationDatas, final Collection rates, - final Collection delinquencyBucketOptions, Collection advancedPaymentData) { + final Collection delinquencyBucketOptions, Collection advancedPaymentData, + Collection creditAllocationData) { this.charges = charges; this.borrowerCycleVariationDatas = borrowerCycleVariationDatas; this.rates = rates; this.delinquencyBucketOptions = delinquencyBucketOptions; this.advancedPaymentData = advancedPaymentData; + this.creditAllocationData = creditAllocationData; } public String loanProductSchema() { @@ -530,8 +543,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment, - enableAutoRepaymentForDownPayment, advancedPaymentData, repaymentStartDateType, enableInstallmentLevelDelinquency, - loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData()); + enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType, + enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData()); } } @@ -589,8 +602,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo return new AdvancedPaymentData(transactionType, futureInstallmentAllocationRule, convert(allocationTypes)); } - private List convert(String futureInstallmentAllocationRule) { - String[] allocationRule = futureInstallmentAllocationRule.split(","); + private List convert(String allocationOrders) { + String[] allocationRule = allocationOrders.split(","); AtomicInteger order = new AtomicInteger(1); return Arrays.stream(allocationRule) // .map(s -> new PaymentAllocationOrder(s, order.getAndIncrement())) // @@ -599,6 +612,29 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo } + private static final class CreditAllocationDataMapper implements RowMapper { + + public String schema() { + return "transaction_type, allocation_types from m_loan_product_credit_allocation_rule"; + } + + @Override + public CreditAllocationData mapRow(ResultSet rs, int rowNum) throws SQLException { + final String transactionType = rs.getString("transaction_type"); + final String allocationTypes = rs.getString("allocation_types"); + return new CreditAllocationData(transactionType, convert(allocationTypes)); + } + + private List convert(String allocationOrders) { + String[] allocationRule = allocationOrders.split(","); + AtomicInteger order = new AtomicInteger(1); + return Arrays.stream(allocationRule) // + .map(s -> new CreditAllocationData.CreditAllocationOrder(s, order.getAndIncrement())) // + .toList(); + } + + } + private static final class LoanProductBorrowerCycleMapper implements RowMapper { public String schema() { @@ -630,7 +666,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo public Collection retrieveAllLoanProductsForCurrency(String currencyCode) { this.context.authenticatedUser(); - final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null); + final LoanProductMapper rm = new LoanProductMapper(null, null, null, null, null, null); String sql = "select " + rm.loanProductSchema() + " where lp.currency_code= ? "; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 848b74d87..0408406b2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -55,6 +55,7 @@ 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.CreditAllocationsJsonParser; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; @@ -88,7 +89,9 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro private final DelinquencyBucketRepository delinquencyBucketRepository; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser; + private final CreditAllocationsJsonParser creditAllocationsJsonParser; private final LoanProductPaymentAllocationRuleMerger loanProductPaymentAllocationRuleMerger = new LoanProductPaymentAllocationRuleMerger(); + private final LoanProductCreditAllocationRuleMerger loanProductCreditAllocationRuleMerger = new LoanProductCreditAllocationRuleMerger(); @Transactional @Override @@ -110,11 +113,8 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro final List rates = assembleListOfProductRates(command); final List loanProductPaymentAllocationRules = advancedPaymentJsonParser .assembleLoanProductPaymentAllocationRules(command, loanTransactionProcessingStrategyCode); - List loanProductCreditAllocationRules = new ArrayList<>(); // TODO: this - // shall be - // parsed from - // the json - // command + final List loanProductCreditAllocationRules = creditAllocationsJsonParser + .assembleLoanProductCreditAllocationRules(command, loanTransactionProcessingStrategyCode); FloatingRate floatingRate = null; if (command.parameterExists("floatingRatesId")) { floatingRate = this.floatingRateRepository @@ -239,6 +239,17 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro } } + if (changes.containsKey("creditAllocation")) { + final List loanProductCreditAllocationRules = creditAllocationsJsonParser + .assembleLoanProductCreditAllocationRules(command, product.getTransactionProcessingStrategyCode()); + loanProductCreditAllocationRules.forEach(lpcar -> lpcar.setLoanProduct(product)); + final boolean updated = loanProductCreditAllocationRuleMerger.updateCreditAllocationRules(product, + loanProductCreditAllocationRules); + if (!updated) { + changes.remove("creditAllocation"); + } + } + // accounting related changes final boolean accountingTypeChanged = changes.containsKey("accountingRule"); final Map accountingMappingChanges = this.accountMappingWritePlatformService diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java index e1e465e16..e5349dc65 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java @@ -33,6 +33,7 @@ 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.domain.AdvancedPaymentAllocationsJsonParser; +import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationsJsonParser; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; @@ -78,10 +79,10 @@ public class LoanProductConfiguration { LoanRepositoryWrapper loanRepositoryWrapper, BusinessEventNotifierService businessEventNotifierService, DelinquencyBucketRepository delinquencyBucketRepository, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, - AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser) { + AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser, CreditAllocationsJsonParser creditAllocationsJsonParser) { return new LoanProductWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, loanProductRepository, aprCalculator, fundRepository, chargeRepository, rateRepository, accountMappingWritePlatformService, fineractEntityAccessUtil, floatingRateRepository, loanRepositoryWrapper, businessEventNotifierService, delinquencyBucketRepository, - loanRepaymentScheduleTransactionProcessorFactory, advancedPaymentJsonParser); + loanRepaymentScheduleTransactionProcessorFactory, advancedPaymentJsonParser, creditAllocationsJsonParser); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMergerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMergerTest.java new file mode 100644 index 000000000..27b569d12 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductCreditAllocationRuleMergerTest.java @@ -0,0 +1,106 @@ +/** + * 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.AllocationType.FEE; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL; + +import java.util.List; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; +import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductCreditAllocationRule; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LoanProductCreditAllocationRuleMergerTest { + + private LoanProductCreditAllocationRuleMerger underTest = new LoanProductCreditAllocationRuleMerger(); + + @Test + public void testMergerOneNewAdded() { + LoanProduct loanProduct = new LoanProduct(); + + LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(PENALTY, FEE, INTEREST, PRINCIPAL)); + + boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule1)); + Assertions.assertTrue(result); + Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size()); + Assertions.assertEquals(rule1, loanProduct.getCreditAllocationRules().get(0)); + } + + @Test + public void testMergerExistingUpdated() { + LoanProduct loanProduct = new LoanProduct(); + + LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(PENALTY, FEE, INTEREST, PRINCIPAL)); + LoanProductCreditAllocationRule rule2 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(FEE, INTEREST, PRINCIPAL, PENALTY)); + + loanProduct.getCreditAllocationRules().add(rule1); + + boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule2)); + Assertions.assertTrue(result); + Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size()); + Assertions.assertEquals(rule2.getTransactionType(), loanProduct.getCreditAllocationRules().get(0).getTransactionType()); + Assertions.assertEquals(rule2.getAllocationTypes(), loanProduct.getCreditAllocationRules().get(0).getAllocationTypes()); + } + + @Test + public void testMergerNothingIsChanged() { + LoanProduct loanProduct = new LoanProduct(); + + LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(PENALTY, FEE, INTEREST, PRINCIPAL)); + LoanProductCreditAllocationRule rule2 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(PENALTY, FEE, INTEREST, PRINCIPAL)); + + loanProduct.getCreditAllocationRules().add(rule1); + + boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of(rule2)); + Assertions.assertFalse(result); + Assertions.assertEquals(1, loanProduct.getCreditAllocationRules().size()); + Assertions.assertEquals(rule1.getTransactionType(), loanProduct.getCreditAllocationRules().get(0).getTransactionType()); + Assertions.assertEquals(rule1.getAllocationTypes(), loanProduct.getCreditAllocationRules().get(0).getAllocationTypes()); + } + + @Test + public void testMergerExistingDeleted() { + LoanProduct loanProduct = new LoanProduct(); + + LoanProductCreditAllocationRule rule1 = createRule(CreditAllocationTransactionType.CHARGEBACK, + List.of(PENALTY, FEE, INTEREST, PRINCIPAL)); + + loanProduct.getCreditAllocationRules().add(rule1); + + boolean result = underTest.updateCreditAllocationRules(loanProduct, List.of()); + Assertions.assertTrue(result); + Assertions.assertEquals(0, loanProduct.getCreditAllocationRules().size()); + } + + public LoanProductCreditAllocationRule createRule(CreditAllocationTransactionType transactionType, + List allocationTypeList) { + return new LoanProductCreditAllocationRule(null, transactionType, allocationTypeList); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithCreditAllocationsIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithCreditAllocationsIntegrationTests.java new file mode 100644 index 000000000..aca226fb7 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithCreditAllocationsIntegrationTests.java @@ -0,0 +1,309 @@ +/** + * 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.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.CreditAllocationData; +import org.apache.fineract.client.models.CreditAllocationOrder; +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.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +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 LoanProductWithCreditAllocationsIntegrationTests { + + 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 testCreateAndReadLoanProductWithAdvancedPaymentAndCreditAllocations() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // when + String loanProductJSON = baseLoanProduct().addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .addCreditAllocations(createChargebackAllocation()).build(); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + + // then + Assertions.assertNotNull(loanProduct.getCreditAllocation()); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(createChargebackAllocation(), loanProduct.getCreditAllocation().get(0)); + } + + @Test + public void testCreateLoanProductAndLaterAddCreditAllocation() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // create empty + String loanProductJSON = baseLoanProduct().addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation).build(); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertEquals(0, loanProduct.getCreditAllocation().size()); + + // add credit allocation + PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = updateLoanProductRequest(createChargebackAllocation()); + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), putLoanProductsProductIdRequest); + loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertNotNull(loanProduct.getCreditAllocation()); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(createChargebackAllocation(), loanProduct.getCreditAllocation().get(0)); + } + + @Test + public void testCreateAndUpdateCreditAllocation() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // when + String loanProductJSON = baseLoanProduct().addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .addCreditAllocations(createChargebackAllocation()).build(); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertNotNull(loanProduct.getCreditAllocation()); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(createChargebackAllocation(), loanProduct.getCreditAllocation().get(0)); + + CreditAllocationData updated = createChargebackAllocation(); + List updatedOrder = createCreditAllocationOrders("FEE", "INTEREST", "PRINCIPAL", "PENALTY"); + updated.setCreditAllocationOrder(updatedOrder); + + PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = updateLoanProductRequest(updated); + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), putLoanProductsProductIdRequest); + loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(updatedOrder, loanProduct.getCreditAllocation().get(0).getCreditAllocationOrder()); + } + + @Test + public void testCreateAndDeleteCreditAllocation() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // when + String loanProductJSON = baseLoanProduct().addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .addCreditAllocations(createChargebackAllocation()).build(); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertNotNull(loanProduct.getCreditAllocation()); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(createChargebackAllocation(), loanProduct.getCreditAllocation().get(0)); + + PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = updateLoanProductRequest(new CreditAllocationData[] {}); + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), putLoanProductsProductIdRequest); + loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertEquals(0, loanProduct.getCreditAllocation().size()); + } + + @Test + public void testCreditAllocationIsNotAllowedWhenPaymentStrategyIsNotAdvancedPaymentStrategy() { + // given + String loanProductJSON = baseLoanProduct().withRepaymentStrategy("mifos-standard-strategy") + .addCreditAllocations(createChargebackAllocation()).build(); + ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(400).build(); + LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse); + + // when + List> loanProductError = validationErrorHelper.getLoanProductError(loanProductJSON, "errors"); + + // then + Assertions.assertEquals("In case 'mifos-standard-strategy' payment strategy, creditAllocation must not be provided", + loanProductError.get(0).get("defaultUserMessage")); + } + + @Test + public void testCreateLoanProductWithCreditAllocationThenUpdatePaymentStrategyShouldFail() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + String loanProductJSON = baseLoanProduct().addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .addCreditAllocations(createChargebackAllocation()).build(); + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductJSON); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + Assertions.assertNotNull(loanProduct.getCreditAllocation()); + Assertions.assertEquals(1, loanProduct.getCreditAllocation().size()); + Assertions.assertEquals(createChargebackAllocation(), loanProduct.getCreditAllocation().get(0)); + PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = updateLoanProductRequest("mifos-standard-strategy"); + putLoanProductsProductIdRequest.setPaymentAllocation(List.of()); + + // when + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, () -> { + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), putLoanProductsProductIdRequest); + }); + + // then + Assertions.assertTrue(callFailedRuntimeException.getMessage() + .contains("In case 'mifos-standard-strategy' payment strategy, creditAllocation must not be provided")); + } + + private CreditAllocationData createChargebackAllocation() { + CreditAllocationData creditAllocationData = new CreditAllocationData(); + creditAllocationData.setTransactionType("CHARGEBACK"); + creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders("PENALTY", "FEE", "INTEREST", "PRINCIPAL")); + return creditAllocationData; + } + + public List createCreditAllocationOrders(String... allocationRule) { + AtomicInteger integer = new AtomicInteger(1); + return Arrays.stream(allocationRule).map(allocation -> { + CreditAllocationOrder creditAllocationOrder = new CreditAllocationOrder(); + creditAllocationOrder.setCreditAllocationRule(allocation); + creditAllocationOrder.setOrder(integer.getAndIncrement()); + return creditAllocationOrder; + }).toList(); + } + + private LoanProductTestBuilder baseLoanProduct() { + return 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).withLoanScheduleType(LoanScheduleType.PROGRESSIVE) + .withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL); + } + + private PutLoanProductsProductIdRequest updateLoanProductRequest(CreditAllocationData... creditAllocationData) { + PutLoanProductsProductIdRequest putLoanProductsProductIdRequest = new PutLoanProductsProductIdRequest(); + putLoanProductsProductIdRequest.creditAllocation(Arrays.stream(creditAllocationData).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 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 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 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 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())); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 5f2f5e2f5..a5973d554 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.CreditAllocationData; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -94,6 +95,7 @@ public class LoanProductTestBuilder { private String inArrearsTolerance = "0"; private String transactionProcessingStrategyCode = DEFAULT_STRATEGY; private List advancedPaymentAllocations = null; + private List creditAllocations = null; private String accountingRule = NONE; private final String currencyCode = USD; private String amortizationType = EQUAL_INSTALLMENTS; @@ -197,6 +199,7 @@ public class LoanProductTestBuilder { map.put("inArrearsTolerance", this.inArrearsTolerance); map.put("transactionProcessingStrategyCode", this.transactionProcessingStrategyCode); map.put("paymentAllocation", this.advancedPaymentAllocations); + map.put("creditAllocation", this.creditAllocations); map.put("accountingRule", this.accountingRule); map.put("minPrincipal", this.minPrincipal); map.put("maxPrincipal", this.maxPrincipal); @@ -736,6 +739,11 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder addCreditAllocations(CreditAllocationData... creditAllocationData) { + this.creditAllocations = new ArrayList<>(Arrays.stream(creditAllocationData).toList()); + return this; + } + public LoanProductTestBuilder withRepaymentStartDateType(final Integer repaymentStartDateType) { this.repaymentStartDateType = repaymentStartDateType; return this;