From e2e10d2008df29902e4bd70c034d9c9cec7fdfc7 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 29 Aug 2023 15:41:56 +0200 Subject: [PATCH] FINERACT-1968: Advanced payment allocation strategy for Loan --- .../portfolio/loanaccount/domain/Loan.java | 19 ++ .../domain/LoanPaymentAllocationRule.java | 10 +- .../loanproduct/domain/LoanProduct.java | 16 +- .../LoanProductPaymentAllocationRule.java | 3 +- .../InternalLoanInformationApiResource.java | 19 ++ .../loanaccount/service/LoanAssembler.java | 23 +- .../mapper/AdvancedPaymentDataMapper.java | 49 +++++ ...oanProductPaymentAllocationRuleMerger.java | 9 +- ...roductPaymentAllocationRuleMergerTest.java | 30 +-- ...ncedPaymentAllocationIntegrationTests.java | 198 ++++++++++++++++++ .../common/loans/LoanTransactionHelper.java | 5 + 11 files changed, 341 insertions(+), 40 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/AdvancedPaymentDataMapper.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWithAdvancedPaymentAllocationIntegrationTests.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 25a479a84..72eb5fe8d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -237,6 +237,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "loan_transaction_strategy_name") private String transactionProcessingStrategyName; + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) + private List paymentAllocationRules = new ArrayList<>(); + @Embedded private LoanProductRelatedDetail loanRepaymentScheduleDetail; @@ -7187,4 +7190,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public LoanInterestRecalculationDetails getLoanInterestRecalculationDetails() { return loanInterestRecalculationDetails; } + + public List getPaymentAllocationRules() { + return paymentAllocationRules; + } + + public void setPaymentAllocationRules(List loanPaymentAllocationRules) { + this.paymentAllocationRules = loanPaymentAllocationRules; + } + + public String getTransactionProcessingStrategyCode() { + return transactionProcessingStrategyCode; + } + + public String getTransactionProcessingStrategyName() { + return transactionProcessingStrategyName; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanPaymentAllocationRule.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanPaymentAllocationRule.java index a16b80f50..ce1bc7486 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanPaymentAllocationRule.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanPaymentAllocationRule.java @@ -19,11 +19,10 @@ package org.apache.fineract.portfolio.loanaccount.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; -import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -35,6 +34,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; +import org.apache.fineract.portfolio.loanproduct.domain.AllocationTypeListConverter; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; @@ -42,13 +42,13 @@ import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; @Getter @Setter @Entity -@Table(name = "m_loan_payment_allocation", uniqueConstraints = { +@Table(name = "m_loan_payment_allocation_rule", uniqueConstraints = { @UniqueConstraint(columnNames = { "loan_id", "transaction_type" }, name = "uq_m_loan_payment_allocation_rule") }) @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class LoanPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom { - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne @JoinColumn(name = "loan_id", nullable = false) private Loan loan; @@ -56,7 +56,7 @@ public class LoanPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeC @Enumerated(EnumType.STRING) private PaymentAllocationTransactionType transactionType; - @ElementCollection(fetch = FetchType.EAGER) + @Convert(converter = AllocationTypeListConverter.class) @Column(name = "allocation_types", nullable = false) private List allocationTypes; 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 89a3fe47a..4910a7548 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 @@ -91,7 +91,7 @@ public class LoanProduct extends AbstractPersistableCustom { private String transactionProcessingStrategyName; @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER) - private List loanProductPaymentAllocationRules = new ArrayList<>(); + private List paymentAllocationRules = new ArrayList<>(); @Column(name = "name", nullable = false, unique = true) private String name; @@ -600,7 +600,7 @@ public class LoanProduct extends AbstractPersistableCustom { } public LoanProduct(final Fund fund, final String transactionProcessingStrategyCode, - final List loanProductPaymentAllocationRules, final String name, final String shortName, + final List paymentAllocationRules, 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, @@ -635,9 +635,9 @@ public class LoanProduct extends AbstractPersistableCustom { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; - this.loanProductPaymentAllocationRules = loanProductPaymentAllocationRules; - if (this.loanProductPaymentAllocationRules != null) { - for (LoanProductPaymentAllocationRule loanProductPaymentAllocationRule : this.loanProductPaymentAllocationRules) { + this.paymentAllocationRules = paymentAllocationRules; + if (this.paymentAllocationRules != null) { + for (LoanProductPaymentAllocationRule loanProductPaymentAllocationRule : this.paymentAllocationRules) { loanProductPaymentAllocationRule.setLoanProduct(this); } } @@ -731,7 +731,7 @@ public class LoanProduct extends AbstractPersistableCustom { } public void validateLoanProductPreSave() { - if (this.loanProductPaymentAllocationRules != null && loanProductPaymentAllocationRules.size() > 0 + if (this.paymentAllocationRules != null && paymentAllocationRules.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", @@ -867,8 +867,8 @@ public class LoanProduct extends AbstractPersistableCustom { return this.charges; } - public List getLoanProductPaymentAllocationRules() { - return this.loanProductPaymentAllocationRules; + public List getPaymentAllocationRules() { + return this.paymentAllocationRules; } public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductPaymentAllocationRule.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductPaymentAllocationRule.java index c59309bd0..a1953c060 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductPaymentAllocationRule.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductPaymentAllocationRule.java @@ -23,7 +23,6 @@ import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -45,7 +44,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa @NoArgsConstructor(access = AccessLevel.PROTECTED) public class LoanProductPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom { - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne @JoinColumn(name = "loan_product_id", nullable = false) private LoanProduct loanProduct; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java index b6f4e1497..a3662c791 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java @@ -43,6 +43,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData; +import org.apache.fineract.portfolio.loanproduct.mapper.AdvancedPaymentDataMapper; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -59,6 +61,7 @@ public class InternalLoanInformationApiResource implements InitializingBean { private final ToApiJsonSerializer toApiJsonSerializerForMap; private final ToApiJsonSerializer toApiJsonSerializerForList; private final ApiRequestParameterHelper apiRequestParameterHelper; + private final AdvancedPaymentDataMapper advancedPaymentDataMapper; @Override public void afterPropertiesSet() { @@ -126,4 +129,20 @@ public class InternalLoanInformationApiResource implements InitializingBean { final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); return this.toApiJsonSerializerForList.serialize(settings, loanIds); } + + @GET + @Path("{loanId}/advanced-payment-allocation-rules") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public List getAdvancedPaymentAllocationRulesOfLoan(@Context final UriInfo uriInfo, + @PathParam("loanId") Long loanId) { + log.warn("------------------------------------------------------------"); + log.warn(" "); + log.warn("Fetching advanced payment allocation rules by loanId {}", loanId); + log.warn(" "); + log.warn("------------------------------------------------------------"); + + final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + return advancedPaymentDataMapper.mapLoanPaymentAllocationRule(loan.getPaymentAllocationRules()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index e73883e0e..78a8eead9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -66,9 +66,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; @@ -234,7 +236,7 @@ public class LoanAssembler { BigDecimal fixedPrincipalPercentagePerInstallment = fromApiJsonHelper .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); - Loan loanApplication = null; + Loan loanApplication; Client client = null; Group group = null; @@ -273,21 +275,19 @@ public class LoanAssembler { syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); - loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); } else if (group != null) { loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); - loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); } else if (client != null) { - loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, collateral, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); - loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); + } else { + loanApplication = null; } final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); @@ -297,6 +297,8 @@ public class LoanAssembler { if (loanApplication == null) { throw new IllegalStateException("No loan application exists for either a client or group (or both)."); } + loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); + copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication); loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); @@ -359,4 +361,15 @@ public class LoanAssembler { loanApplication.validateExpectedDisbursementForHolidayAndNonWorkingDay(workingDays, allowTransactionsOnHoliday, holidays, allowTransactionsOnNonWorkingDay); } + + private void copyAdvancedPaymentRulesIfApplicable(String transactionProcessingStrategyCode, LoanProduct loanProduct, + Loan loanApplication) { + if (transactionProcessingStrategyCode.equals(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) { + List loanPaymentAllocationRules = loanProduct.getPaymentAllocationRules().stream() + .map(r -> new LoanPaymentAllocationRule(loanApplication, r.getTransactionType(), r.getAllocationTypes(), + r.getFutureInstallmentAllocationRule())) + .toList(); + loanApplication.setPaymentAllocationRules(loanPaymentAllocationRules); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/AdvancedPaymentDataMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/AdvancedPaymentDataMapper.java new file mode 100644 index 000000000..09523a681 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/AdvancedPaymentDataMapper.java @@ -0,0 +1,49 @@ +/** + * 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.mapper; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapstructMapperConfig.class) +public interface AdvancedPaymentDataMapper { + + List mapLoanProductPaymentAllocationRule(List paymentAllocationRule); + + List mapLoanPaymentAllocationRule(List paymentAllocationRule); + + @Mapping(target = "paymentAllocationOrder", source = "allocationTypes") + AdvancedPaymentData mapLoanProductPaymentAllocationRule(LoanProductPaymentAllocationRule paymentAllocationRule); + + @Mapping(target = "paymentAllocationOrder", source = "allocationTypes") + AdvancedPaymentData mapLoanPaymentAllocationRule(LoanPaymentAllocationRule paymentAllocationRule); + + default List mapAllocationTypes(List allocationTypes) { + AtomicInteger counter = new AtomicInteger(1); + return allocationTypes.stream().map(a -> new AdvancedPaymentData.PaymentAllocationOrder(a.name(), counter.getAndIncrement())) + .toList(); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMerger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMerger.java index a43776d92..56b7f4520 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMerger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMerger.java @@ -37,9 +37,8 @@ public class LoanProductPaymentAllocationRuleMerger { return false; } boolean updated = false; - Map originalItems = loanProduct - .getLoanProductPaymentAllocationRules().stream() - .collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity())); + Map originalItems = loanProduct.getPaymentAllocationRules() + .stream().collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity())); Map newItems = newLoanProductPaymentAllocationRules.stream() .collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity())); @@ -50,7 +49,7 @@ public class LoanProductPaymentAllocationRuleMerger { if (existing.size() > 0) { updated = true; existing.forEach(type -> { - loanProduct.getLoanProductPaymentAllocationRules().remove(originalItems.get(type)); + loanProduct.getPaymentAllocationRules().remove(originalItems.get(type)); }); } @@ -61,7 +60,7 @@ public class LoanProductPaymentAllocationRuleMerger { if (newSet.size() > 0) { updated = true; newSet.forEach(type -> { - loanProduct.getLoanProductPaymentAllocationRules().add(newItems.get(type)); + loanProduct.getPaymentAllocationRules().add(newItems.get(type)); }); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMergerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMergerTest.java index d97ffa949..325654cfe 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMergerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductPaymentAllocationRuleMergerTest.java @@ -48,8 +48,8 @@ class LoanProductPaymentAllocationRuleMergerTest { // then Assertions.assertTrue(result); - Assertions.assertEquals(1, loanProduct.getLoanProductPaymentAllocationRules().size()); - Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0)); + Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size()); + Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0)); } @Test @@ -61,15 +61,15 @@ class LoanProductPaymentAllocationRuleMergerTest { 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); + loanProduct.getPaymentAllocationRules().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)); + Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size()); + Assertions.assertEquals(PAST_DUE_FEE, loanProduct.getPaymentAllocationRules().get(0).getAllocationTypes().get(0)); } @Test @@ -81,16 +81,16 @@ class LoanProductPaymentAllocationRuleMergerTest { 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)); + loanProduct.getPaymentAllocationRules().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)); + Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size()); + Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0)); + Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1)); } @Test @@ -99,14 +99,14 @@ class LoanProductPaymentAllocationRuleMergerTest { LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger(); LoanProduct loanProduct = new LoanProduct(); LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); - loanProduct.getLoanProductPaymentAllocationRules().add(rule1); + loanProduct.getPaymentAllocationRules().add(rule1); // when boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of()); // then Assertions.assertTrue(result); - Assertions.assertEquals(0, loanProduct.getLoanProductPaymentAllocationRules().size()); + Assertions.assertEquals(0, loanProduct.getPaymentAllocationRules().size()); } @Test @@ -115,7 +115,7 @@ class LoanProductPaymentAllocationRuleMergerTest { LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger(); LoanProduct loanProduct = new LoanProduct(); LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); - loanProduct.getLoanProductPaymentAllocationRules().add(rule1); + loanProduct.getPaymentAllocationRules().add(rule1); LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); // when @@ -123,9 +123,9 @@ class LoanProductPaymentAllocationRuleMergerTest { // then Assertions.assertTrue(result); - Assertions.assertEquals(2, loanProduct.getLoanProductPaymentAllocationRules().size()); - Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0)); - Assertions.assertEquals(rule2, loanProduct.getLoanProductPaymentAllocationRules().get(1)); + Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size()); + Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0)); + Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1)); } public LoanProductPaymentAllocationRule createRule(PaymentAllocationTransactionType transactionType, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWithAdvancedPaymentAllocationIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWithAdvancedPaymentAllocationIntegrationTests.java new file mode 100644 index 000000000..fc0d6281e --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWithAdvancedPaymentAllocationIntegrationTests.java @@ -0,0 +1,198 @@ +/** + * 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 static org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY; + +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.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.PaymentAllocationOrder; +import org.apache.fineract.client.models.PostClientsRequest; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; +import org.apache.fineract.integrationtests.common.ClientHelper; +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.loans.LoanApplicationTestBuilder; +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 LoanWithAdvancedPaymentAllocationIntegrationTests { + + private static ClientHelper CLIENT_HELPER; + private static ResponseSpecification RESPONSE_SPEC; + private static RequestSpecification REQUEST_SPEC; + private static Account ASSET_ACCOUNT; + private static Account FEE_PENALTY_ACCOUNT; + private static Account EXPENSE_ACCOUNT; + private static Account INCOME_ACCOUNT; + private static Account OVERPAYMENT_ACCOUNT; + 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); + LOAN_TRANSACTION_HELPER = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC); + CLIENT_HELPER = new ClientHelper(REQUEST_SPEC, RESPONSE_SPEC); + + ASSET_ACCOUNT = accountHelper.createAssetAccount(); + FEE_PENALTY_ACCOUNT = accountHelper.createAssetAccount(); + EXPENSE_ACCOUNT = accountHelper.createExpenseAccount(); + INCOME_ACCOUNT = accountHelper.createIncomeAccount(); + OVERPAYMENT_ACCOUNT = accountHelper.createLiabilityAccount(); + } + + @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); + final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(new PostClientsRequest().activationDate("01 January 2022") + .active(true).dateFormat("dd MMMM yyyy").fullname("fullName").locale("en").legalFormId(1).officeId(1)); + Integer loanId = createLoanAccount(LOAN_TRANSACTION_HELPER, clientResponse.getClientId().toString(), loanProductId.toString(), + "02 January 2022"); + // then + List allocationRules = LOAN_TRANSACTION_HELPER.getAdvancedPaymentAllocationRules(loanId); + Assertions.assertNotNull(allocationRules); + + Optional first = allocationRules.stream() + .filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst(); + Assertions.assertTrue(first.isPresent()); + Assertions.assertEquals(defaultAllocation, first.get()); + + Optional second = allocationRules.stream() + .filter(advancedPaymentData -> "REPAYMENT".equals(advancedPaymentData.getTransactionType())).findFirst(); + Assertions.assertTrue(second.isPresent()); + Assertions.assertEquals(repaymentPaymentAllocation, second.get()); + + // when + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), updateLoanProductRequest(defaultAllocation)); + // then + allocationRules = LOAN_TRANSACTION_HELPER.getAdvancedPaymentAllocationRules(loanId); + Assertions.assertNotNull(allocationRules); + + first = allocationRules.stream().filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())) + .findFirst(); + Assertions.assertTrue(first.isPresent()); + Assertions.assertEquals(defaultAllocation, first.get()); + + second = allocationRules.stream().filter(advancedPaymentData -> "REPAYMENT".equals(advancedPaymentData.getTransactionType())) + .findFirst(); + Assertions.assertTrue(second.isPresent()); + Assertions.assertEquals(repaymentPaymentAllocation, second.get()); + } + + private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) { + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") + .withRepaymentStrategy(ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .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 Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId, + final String operationDate) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("15,000.00").withLoanTermFrequency("4") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("0") // + .withExpectedDisbursementDate(operationDate) // + .withInterestTypeAsDecliningBalance() // + .withSubmittedOnDate(operationDate) // + .withRepaymentStrategy(ADVANCED_PAYMENT_ALLOCATION_STRATEGY) // + .build(clientId, loanProductId, null); + return loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + 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(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 86324375b..ffbf4a39d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -42,6 +42,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.DeleteLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.DeleteLoansLoanIdResponse; import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse; @@ -1912,4 +1913,8 @@ public class LoanTransactionHelper extends IntegrationTest { public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(Long loanId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "downPayment")); } + + public List getAdvancedPaymentAllocationRules(final Integer loanId) { + return ok(fineract().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue())); + } }