FINERACT-1968: Advanced payment allocation strategy for Loan

This commit is contained in:
Adam Saghy
2023-08-30 13:47:30 +02:00
parent 349103015f
commit e2e10d2008
11 changed files with 341 additions and 40 deletions
@@ -237,6 +237,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
@Column(name = "loan_transaction_strategy_name") @Column(name = "loan_transaction_strategy_name")
private String transactionProcessingStrategyName; private String transactionProcessingStrategyName;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanPaymentAllocationRule> paymentAllocationRules = new ArrayList<>();
@Embedded @Embedded
private LoanProductRelatedDetail loanRepaymentScheduleDetail; private LoanProductRelatedDetail loanRepaymentScheduleDetail;
@@ -7187,4 +7190,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public LoanInterestRecalculationDetails getLoanInterestRecalculationDetails() { public LoanInterestRecalculationDetails getLoanInterestRecalculationDetails() {
return loanInterestRecalculationDetails; return loanInterestRecalculationDetails;
} }
public List<LoanPaymentAllocationRule> getPaymentAllocationRules() {
return paymentAllocationRules;
}
public void setPaymentAllocationRules(List<LoanPaymentAllocationRule> loanPaymentAllocationRules) {
this.paymentAllocationRules = loanPaymentAllocationRules;
}
public String getTransactionProcessingStrategyCode() {
return transactionProcessingStrategyCode;
}
public String getTransactionProcessingStrategyName() {
return transactionProcessingStrategyName;
}
} }
@@ -19,11 +19,10 @@
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection; import jakarta.persistence.Convert;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@@ -35,6 +34,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; 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.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@@ -42,13 +42,13 @@ import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@Getter @Getter
@Setter @Setter
@Entity @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") }) @UniqueConstraint(columnNames = { "loan_id", "transaction_type" }, name = "uq_m_loan_payment_allocation_rule") })
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LoanPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom { public class LoanPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne
@JoinColumn(name = "loan_id", nullable = false) @JoinColumn(name = "loan_id", nullable = false)
private Loan loan; private Loan loan;
@@ -56,7 +56,7 @@ public class LoanPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeC
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private PaymentAllocationTransactionType transactionType; private PaymentAllocationTransactionType transactionType;
@ElementCollection(fetch = FetchType.EAGER) @Convert(converter = AllocationTypeListConverter.class)
@Column(name = "allocation_types", nullable = false) @Column(name = "allocation_types", nullable = false)
private List<PaymentAllocationType> allocationTypes; private List<PaymentAllocationType> allocationTypes;
@@ -91,7 +91,7 @@ public class LoanProduct extends AbstractPersistableCustom {
private String transactionProcessingStrategyName; private String transactionProcessingStrategyName;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanProduct", orphanRemoval = true, fetch = FetchType.EAGER)
private List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules = new ArrayList<>(); private List<LoanProductPaymentAllocationRule> paymentAllocationRules = new ArrayList<>();
@Column(name = "name", nullable = false, unique = true) @Column(name = "name", nullable = false, unique = true)
private String name; private String name;
@@ -600,7 +600,7 @@ public class LoanProduct extends AbstractPersistableCustom {
} }
public LoanProduct(final Fund fund, final String transactionProcessingStrategyCode, public LoanProduct(final Fund fund, final String transactionProcessingStrategyCode,
final List<LoanProductPaymentAllocationRule> loanProductPaymentAllocationRules, final String name, final String shortName, final List<LoanProductPaymentAllocationRule> paymentAllocationRules, final String name, final String shortName,
final String description, final MonetaryCurrency currency, final BigDecimal defaultPrincipal, final String description, final MonetaryCurrency currency, final BigDecimal defaultPrincipal,
final BigDecimal defaultMinPrincipal, final BigDecimal defaultMaxPrincipal, final BigDecimal defaultMinPrincipal, final BigDecimal defaultMaxPrincipal,
final BigDecimal defaultNominalInterestRatePerPeriod, final BigDecimal defaultMinNominalInterestRatePerPeriod, final BigDecimal defaultNominalInterestRatePerPeriod, final BigDecimal defaultMinNominalInterestRatePerPeriod,
@@ -635,9 +635,9 @@ public class LoanProduct extends AbstractPersistableCustom {
this.fund = fund; this.fund = fund;
this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode;
this.loanProductPaymentAllocationRules = loanProductPaymentAllocationRules; this.paymentAllocationRules = paymentAllocationRules;
if (this.loanProductPaymentAllocationRules != null) { if (this.paymentAllocationRules != null) {
for (LoanProductPaymentAllocationRule loanProductPaymentAllocationRule : this.loanProductPaymentAllocationRules) { for (LoanProductPaymentAllocationRule loanProductPaymentAllocationRule : this.paymentAllocationRules) {
loanProductPaymentAllocationRule.setLoanProduct(this); loanProductPaymentAllocationRule.setLoanProduct(this);
} }
} }
@@ -731,7 +731,7 @@ public class LoanProduct extends AbstractPersistableCustom {
} }
public void validateLoanProductPreSave() { public void validateLoanProductPreSave() {
if (this.loanProductPaymentAllocationRules != null && loanProductPaymentAllocationRules.size() > 0 if (this.paymentAllocationRules != null && paymentAllocationRules.size() > 0
&& !transactionProcessingStrategyCode.equals("advanced-payment-allocation-strategy")) { && !transactionProcessingStrategyCode.equals("advanced-payment-allocation-strategy")) {
throw new LoanProductGeneralRuleException( throw new LoanProductGeneralRuleException(
"payment_allocation.must.not.be.provided.when.allocation.strategy.is.not.advanced-payment-strategy", "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; return this.charges;
} }
public List<LoanProductPaymentAllocationRule> getLoanProductPaymentAllocationRules() { public List<LoanProductPaymentAllocationRule> getPaymentAllocationRules() {
return this.loanProductPaymentAllocationRules; return this.paymentAllocationRules;
} }
public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) { public void update(final LoanProductConfigurableAttributes loanConfigurableAttributes) {
@@ -23,7 +23,6 @@ import jakarta.persistence.Convert;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@@ -45,7 +44,7 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LoanProductPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom { public class LoanProductPaymentAllocationRule extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne
@JoinColumn(name = "loan_product_id", nullable = false) @JoinColumn(name = "loan_product_id", nullable = false)
private LoanProduct loanProduct; private LoanProduct loanProduct;
@@ -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.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; 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.beans.factory.InitializingBean;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -59,6 +61,7 @@ public class InternalLoanInformationApiResource implements InitializingBean {
private final ToApiJsonSerializer<Map> toApiJsonSerializerForMap; private final ToApiJsonSerializer<Map> toApiJsonSerializerForMap;
private final ToApiJsonSerializer<List> toApiJsonSerializerForList; private final ToApiJsonSerializer<List> toApiJsonSerializerForList;
private final ApiRequestParameterHelper apiRequestParameterHelper; private final ApiRequestParameterHelper apiRequestParameterHelper;
private final AdvancedPaymentDataMapper advancedPaymentDataMapper;
@Override @Override
public void afterPropertiesSet() { public void afterPropertiesSet() {
@@ -126,4 +129,20 @@ public class InternalLoanInformationApiResource implements InitializingBean {
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.toApiJsonSerializerForList.serialize(settings, loanIds); return this.toApiJsonSerializerForList.serialize(settings, loanIds);
} }
@GET
@Path("{loanId}/advanced-payment-allocation-rules")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public List<AdvancedPaymentData> 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());
}
} }
@@ -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.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; 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.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; 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.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals; import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals;
import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException;
@@ -234,7 +236,7 @@ public class LoanAssembler {
BigDecimal fixedPrincipalPercentagePerInstallment = fromApiJsonHelper BigDecimal fixedPrincipalPercentagePerInstallment = fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element); .extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, element);
Loan loanApplication = null; Loan loanApplication;
Client client = null; Client client = null;
Group group = null; Group group = null;
@@ -273,21 +275,19 @@ public class LoanAssembler {
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment); fixedPrincipalPercentagePerInstallment);
loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName);
} else if (group != null) { } else if (group != null) {
loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer, loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanType.getId().intValue(), loanProduct, fund, loanOfficer,
loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, null, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, null,
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment); fixedPrincipalPercentagePerInstallment);
loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName);
} else if (client != null) { } else if (client != null) {
loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund, loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanType.getId().intValue(), loanProduct, fund,
loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, collateral, loanOfficer, loanPurpose, transactionProcessingStrategyCode, loanProductRelatedDetail, loanCharges, collateral,
fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement,
isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment); isFloatingInterestRate, interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment);
loanApplication.updateTransactionProcessingStrategy(transactionProcessingStrategyCode, transactionProcessingStrategyName); } else {
loanApplication = null;
} }
final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element); final String externalIdStr = this.fromApiJsonHelper.extractStringNamed("externalId", element);
@@ -297,6 +297,8 @@ public class LoanAssembler {
if (loanApplication == null) { if (loanApplication == null) {
throw new IllegalStateException("No loan application exists for either a client or group (or both)."); 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, loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
this.loanRepaymentScheduleTransactionProcessorFactory); this.loanRepaymentScheduleTransactionProcessorFactory);
@@ -359,4 +361,15 @@ public class LoanAssembler {
loanApplication.validateExpectedDisbursementForHolidayAndNonWorkingDay(workingDays, allowTransactionsOnHoliday, holidays, loanApplication.validateExpectedDisbursementForHolidayAndNonWorkingDay(workingDays, allowTransactionsOnHoliday, holidays,
allowTransactionsOnNonWorkingDay); allowTransactionsOnNonWorkingDay);
} }
private void copyAdvancedPaymentRulesIfApplicable(String transactionProcessingStrategyCode, LoanProduct loanProduct,
Loan loanApplication) {
if (transactionProcessingStrategyCode.equals(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) {
List<LoanPaymentAllocationRule> loanPaymentAllocationRules = loanProduct.getPaymentAllocationRules().stream()
.map(r -> new LoanPaymentAllocationRule(loanApplication, r.getTransactionType(), r.getAllocationTypes(),
r.getFutureInstallmentAllocationRule()))
.toList();
loanApplication.setPaymentAllocationRules(loanPaymentAllocationRules);
}
}
} }
@@ -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<AdvancedPaymentData> mapLoanProductPaymentAllocationRule(List<LoanProductPaymentAllocationRule> paymentAllocationRule);
List<AdvancedPaymentData> mapLoanPaymentAllocationRule(List<LoanPaymentAllocationRule> paymentAllocationRule);
@Mapping(target = "paymentAllocationOrder", source = "allocationTypes")
AdvancedPaymentData mapLoanProductPaymentAllocationRule(LoanProductPaymentAllocationRule paymentAllocationRule);
@Mapping(target = "paymentAllocationOrder", source = "allocationTypes")
AdvancedPaymentData mapLoanPaymentAllocationRule(LoanPaymentAllocationRule paymentAllocationRule);
default List<AdvancedPaymentData.PaymentAllocationOrder> mapAllocationTypes(List<PaymentAllocationType> allocationTypes) {
AtomicInteger counter = new AtomicInteger(1);
return allocationTypes.stream().map(a -> new AdvancedPaymentData.PaymentAllocationOrder(a.name(), counter.getAndIncrement()))
.toList();
}
}
@@ -37,9 +37,8 @@ public class LoanProductPaymentAllocationRuleMerger {
return false; return false;
} }
boolean updated = false; boolean updated = false;
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> originalItems = loanProduct Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> originalItems = loanProduct.getPaymentAllocationRules()
.getLoanProductPaymentAllocationRules().stream() .stream().collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
.collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> newItems = newLoanProductPaymentAllocationRules.stream() Map<PaymentAllocationTransactionType, LoanProductPaymentAllocationRule> newItems = newLoanProductPaymentAllocationRules.stream()
.collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity())); .collect(Collectors.toMap(LoanProductPaymentAllocationRule::getTransactionType, Function.identity()));
@@ -50,7 +49,7 @@ public class LoanProductPaymentAllocationRuleMerger {
if (existing.size() > 0) { if (existing.size() > 0) {
updated = true; updated = true;
existing.forEach(type -> { 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) { if (newSet.size() > 0) {
updated = true; updated = true;
newSet.forEach(type -> { newSet.forEach(type -> {
loanProduct.getLoanProductPaymentAllocationRules().add(newItems.get(type)); loanProduct.getPaymentAllocationRules().add(newItems.get(type));
}); });
} }
@@ -48,8 +48,8 @@ class LoanProductPaymentAllocationRuleMergerTest {
// then // then
Assertions.assertTrue(result); Assertions.assertTrue(result);
Assertions.assertEquals(1, loanProduct.getLoanProductPaymentAllocationRules().size()); Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0)); Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
} }
@Test @Test
@@ -61,15 +61,15 @@ class LoanProductPaymentAllocationRuleMergerTest {
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
LoanProductPaymentAllocationRule rule2 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE)); LoanProductPaymentAllocationRule rule2 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE));
loanProduct.getLoanProductPaymentAllocationRules().add(rule1); loanProduct.getPaymentAllocationRules().add(rule1);
// when // when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2)); boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2));
// then // then
Assertions.assertTrue(result); Assertions.assertTrue(result);
Assertions.assertEquals(1, loanProduct.getLoanProductPaymentAllocationRules().size()); Assertions.assertEquals(1, loanProduct.getPaymentAllocationRules().size());
Assertions.assertEquals(PAST_DUE_FEE, loanProduct.getLoanProductPaymentAllocationRules().get(0).getAllocationTypes().get(0)); Assertions.assertEquals(PAST_DUE_FEE, loanProduct.getPaymentAllocationRules().get(0).getAllocationTypes().get(0));
} }
@Test @Test
@@ -81,16 +81,16 @@ class LoanProductPaymentAllocationRuleMergerTest {
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(PAST_DUE_FEE)); 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 // when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2, rule1)); boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of(rule2, rule1));
// then // then
Assertions.assertFalse(result); Assertions.assertFalse(result);
Assertions.assertEquals(2, loanProduct.getLoanProductPaymentAllocationRules().size()); Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0)); Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
Assertions.assertEquals(rule2, loanProduct.getLoanProductPaymentAllocationRules().get(1)); Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1));
} }
@Test @Test
@@ -99,14 +99,14 @@ class LoanProductPaymentAllocationRuleMergerTest {
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger(); LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct(); LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
loanProduct.getLoanProductPaymentAllocationRules().add(rule1); loanProduct.getPaymentAllocationRules().add(rule1);
// when // when
boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of()); boolean result = underTest.updateProductPaymentAllocationRules(loanProduct, List.of());
// then // then
Assertions.assertTrue(result); Assertions.assertTrue(result);
Assertions.assertEquals(0, loanProduct.getLoanProductPaymentAllocationRules().size()); Assertions.assertEquals(0, loanProduct.getPaymentAllocationRules().size());
} }
@Test @Test
@@ -115,7 +115,7 @@ class LoanProductPaymentAllocationRuleMergerTest {
LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger(); LoanProductPaymentAllocationRuleMerger underTest = new LoanProductPaymentAllocationRuleMerger();
LoanProduct loanProduct = new LoanProduct(); LoanProduct loanProduct = new LoanProduct();
LoanProductPaymentAllocationRule rule1 = createRule(DEFAULT, LAST_INSTALLMENT, List.of(DUE_INTEREST)); 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)); LoanProductPaymentAllocationRule rule2 = createRule(REPAYMENT, LAST_INSTALLMENT, List.of(DUE_INTEREST));
// when // when
@@ -123,9 +123,9 @@ class LoanProductPaymentAllocationRuleMergerTest {
// then // then
Assertions.assertTrue(result); Assertions.assertTrue(result);
Assertions.assertEquals(2, loanProduct.getLoanProductPaymentAllocationRules().size()); Assertions.assertEquals(2, loanProduct.getPaymentAllocationRules().size());
Assertions.assertEquals(rule1, loanProduct.getLoanProductPaymentAllocationRules().get(0)); Assertions.assertEquals(rule1, loanProduct.getPaymentAllocationRules().get(0));
Assertions.assertEquals(rule2, loanProduct.getLoanProductPaymentAllocationRules().get(1)); Assertions.assertEquals(rule2, loanProduct.getPaymentAllocationRules().get(1));
} }
public LoanProductPaymentAllocationRule createRule(PaymentAllocationTransactionType transactionType, public LoanProductPaymentAllocationRule createRule(PaymentAllocationTransactionType transactionType,
@@ -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<AdvancedPaymentData> allocationRules = LOAN_TRANSACTION_HELPER.getAdvancedPaymentAllocationRules(loanId);
Assertions.assertNotNull(allocationRules);
Optional<AdvancedPaymentData> first = allocationRules.stream()
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
Assertions.assertTrue(first.isPresent());
Assertions.assertEquals(defaultAllocation, first.get());
Optional<AdvancedPaymentData> 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<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_INTEREST, PaymentAllocationType.PAST_DUE_PRINCIPAL,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private AdvancedPaymentData createDefaultPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder;
}).toList();
}
}
@@ -42,6 +42,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; 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.DeleteLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse; import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse; import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
@@ -1912,4 +1913,8 @@ public class LoanTransactionHelper extends IntegrationTest {
public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(Long loanId, PostLoansLoanIdTransactionsRequest request) { public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(Long loanId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "downPayment")); return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "downPayment"));
} }
public List<AdvancedPaymentData> getAdvancedPaymentAllocationRules(final Integer loanId) {
return ok(fineract().legacy.getAdvancedPaymentAllocationRulesOfLoan(loanId.longValue()));
}
} }