diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResource.java index 0b18874c332..2778ccffb80 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/api/ChargesApiResource.java @@ -68,7 +68,7 @@ public class ChargesApiResource { "chargeAppliesTo", "chargeTimeType", "chargeCalculationType", "chargeCalculationTypeOptions", "chargeAppliesToOptions", "chargeTimeTypeOptions", "currencyOptions", "loanChargeCalculationTypeOptions", "loanChargeTimeTypeOptions", "savingsChargeCalculationTypeOptions", "savingsChargeTimeTypeOptions", "incomeAccount", "clientChargeCalculationTypeOptions", - "clientChargeTimeTypeOptions")); + "clientChargeTimeTypeOptions", "adminFeeRanges", "chargeDisbursementType", "chargeDisbursementTypeOptions")); private final String resourceNameForPermissions = "CHARGE"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/data/ChargeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/data/ChargeData.java index 1b4c47a333c..5706d017476 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/data/ChargeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/data/ChargeData.java @@ -22,12 +22,14 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.MonthDay; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.charge.domain.ChargeRange; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; @@ -63,6 +65,7 @@ public final class ChargeData implements Comparable, Serializable { private final EnumOptionData feeFrequency; private final GLAccountData incomeOrLiabilityAccount; private final TaxGroupData taxGroup; + private final EnumOptionData chargeDisbursementType; private final Collection currencyOptions; private final List chargeCalculationTypeOptions;// @@ -87,6 +90,8 @@ public final class ChargeData implements Comparable, Serializable { private final String accountMappingForChargeConfig; private final List expenseAccountOptions; private final List assetAccountOptions; + private List chargeRanges; + private final List chargeDisbursementTypeOptions; public static ChargeData template(final Collection currencyOptions, final List chargeCalculationTypeOptions, final List chargeAppliesToOptions, @@ -97,7 +102,8 @@ public static ChargeData template(final Collection currencyOptions final List feeFrequencyOptions, final Map> incomeOrLiabilityAccountOptions, final Collection taxGroupOptions, final List shareChargeCalculationTypeOptions, final List shareChargeTimeTypeOptions, String accountMappingForChargeConfig, - List expenseAccountOptions, List assetAccountOptions) { + List expenseAccountOptions, List assetAccountOptions, + final List chargeDisbursementTypeOptions) { final GLAccountData account = null; final TaxGroupData taxGroupData = null; @@ -107,7 +113,7 @@ public static ChargeData template(final Collection currencyOptions savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, null, null, null, null, null, feeFrequencyOptions, account, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, accountMappingForChargeConfig, - expenseAccountOptions, assetAccountOptions); + expenseAccountOptions, assetAccountOptions, chargeDisbursementTypeOptions, null, null); } public static ChargeData withTemplate(final ChargeData charge, final ChargeData template) { @@ -121,7 +127,8 @@ public static ChargeData withTemplate(final ChargeData charge, final ChargeData charge.feeOnMonthDay, charge.feeInterval, charge.minCap, charge.maxCap, charge.feeFrequency, template.feeFrequencyOptions, charge.incomeOrLiabilityAccount, template.incomeOrLiabilityAccountOptions, template.taxGroupOptions, template.shareChargeCalculationTypeOptions, template.shareChargeTimeTypeOptions, template.accountMappingForChargeConfig, - template.expenseAccountOptions, template.assetAccountOptions); + template.expenseAccountOptions, template.assetAccountOptions, template.chargeDisbursementTypeOptions, + charge.chargeDisbursementType, charge.chargeRanges); } public static ChargeData instance(final Long id, final String name, final BigDecimal amount, final CurrencyData currency, @@ -130,7 +137,7 @@ public static ChargeData instance(final Long id, final String name, final BigDec final boolean active, final boolean freeWithdrawal, final Integer freeWithdrawalChargeFrequency, final Integer restartFrequency, final Integer restartFrequencyEnum, final boolean isPaymentType, final PaymentTypeData paymentTypeOptions, final BigDecimal minCap, final BigDecimal maxCap, final EnumOptionData feeFrequency, final GLAccountData accountData, - TaxGroupData taxGroupData) { + TaxGroupData taxGroupData, final EnumOptionData chargeDisbursementType) { final Collection currencyOptions = null; final List chargeCalculationTypeOptions = null; @@ -151,6 +158,8 @@ public static ChargeData instance(final Long id, final String name, final BigDec final String accountMappingForChargeConfig = null; final List expenseAccountOptions = null; final List assetAccountOptions = null; + final List chargeDisbursementTypeOptions = null; + return new ChargeData(id, name, amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, chargePaymentMode, penalty, active, freeWithdrawal, freeWithdrawalChargeFrequency, restartFrequency, restartFrequencyEnum, isPaymentType, paymentTypeOptions, taxGroupData, currencyOptions, chargeCalculationTypeOptions, chargeAppliesToOptions, @@ -158,7 +167,8 @@ public static ChargeData instance(final Long id, final String name, final BigDec savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, feeOnMonthDay, feeInterval, minCap, maxCap, feeFrequency, feeFrequencyOptions, accountData, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, - accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions); + accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions, chargeDisbursementTypeOptions, + chargeDisbursementType, null); } public static ChargeData lookup(final Long id, final String name, final boolean isPenalty) { @@ -168,6 +178,7 @@ public static ChargeData lookup(final Long id, final String name, final boolean final EnumOptionData chargeAppliesTo = null; final EnumOptionData chargeCalculationType = null; final EnumOptionData chargePaymentMode = null; + final EnumOptionData chargeDisbursementType = null; final MonthDay feeOnMonthDay = null; final Integer feeInterval = null; final Boolean penalty = isPenalty; @@ -202,6 +213,8 @@ public static ChargeData lookup(final Long id, final String name, final boolean final String accountMappingForChargeConfig = null; final List expenseAccountOptions = null; final List assetAccountOptions = null; + final List chargeDisbursementTypeOptions = null; + final List chargeRanges = null; return new ChargeData(id, name, amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, chargePaymentMode, penalty, active, freeWithdrawal, freeWithdrawalChargeFrequency, restartFrequency, restartFrequencyEnum, isPaymentType, @@ -210,7 +223,8 @@ public static ChargeData lookup(final Long id, final String name, final boolean savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, feeOnMonthDay, feeInterval, minCap, maxCap, feeFrequency, feeFrequencyOptions, account, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, - accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions); + accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions, chargeDisbursementTypeOptions, + chargeDisbursementType, chargeRanges); } private ChargeData(final Long id, final String name, final BigDecimal amount, final CurrencyData currency, @@ -229,7 +243,8 @@ private ChargeData(final Long id, final String name, final BigDecimal amount, fi final Map> incomeOrLiabilityAccountOptions, final Collection taxGroupOptions, final List shareChargeCalculationTypeOptions, final List shareChargeTimeTypeOptions, final String accountMappingForChargeConfig, final List expenseAccountOptions, - final List assetAccountOptions) { + final List assetAccountOptions, final List chargeDisbursementTypeOptions, + final EnumOptionData chargeDisbursementType, final List chargeRanges) { this.id = id; this.name = name; this.amount = amount; @@ -250,6 +265,7 @@ private ChargeData(final Long id, final String name, final BigDecimal amount, fi this.paymentTypeOptions = paymentTypeOptions; this.minCap = minCap; this.maxCap = maxCap; + this.chargeDisbursementType = chargeDisbursementType; this.currencyOptions = currencyOptions; this.chargeCalculationTypeOptions = chargeCalculationTypeOptions; this.chargeAppliesToOptions = chargeAppliesToOptions; @@ -272,6 +288,8 @@ private ChargeData(final Long id, final String name, final BigDecimal amount, fi this.accountMappingForChargeConfig = accountMappingForChargeConfig; this.assetAccountOptions = assetAccountOptions; this.expenseAccountOptions = expenseAccountOptions; + this.chargeDisbursementTypeOptions = chargeDisbursementTypeOptions; + this.chargeRanges = chargeRanges; } @Override @@ -388,4 +406,24 @@ public BigDecimal getAmount() { public CurrencyData getCurrency() { return currency; } + + public void addRange(ChargeRange chargeRange) { + if (this.chargeRanges == null) { + this.chargeRanges = new ArrayList(); + } + this.chargeRanges.add(new ChargeRangeData(chargeRange.getMinDay(), chargeRange.getMaxDay(), chargeRange.getFeeRate())); + } + + public static class ChargeRangeData { + + public final Integer adminFeeMin; + public final Integer adminFeeMax; + public final BigDecimal adminFeeRate; + + public ChargeRangeData(final Integer adminFeeMin, final Integer adminFeeMax, BigDecimal adminFeeRate) { + this.adminFeeMin = adminFeeMin; + this.adminFeeMax = adminFeeMax; + this.adminFeeRate = adminFeeRate; + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java index e52b3528820..bfc355f8dd3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/Charge.java @@ -18,20 +18,30 @@ */ package org.apache.fineract.portfolio.charge.domain; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import java.math.BigDecimal; +import java.time.LocalDate; import java.time.MonthDay; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.accounting.glaccount.data.GLAccountData; import org.apache.fineract.accounting.glaccount.domain.GLAccount; import org.apache.fineract.infrastructure.core.api.JsonCommand; @@ -40,6 +50,7 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.charge.api.ChargesApiConstants; @@ -49,6 +60,7 @@ import org.apache.fineract.portfolio.charge.exception.ChargeParameterUpdateNotSupportedException; import org.apache.fineract.portfolio.charge.service.ChargeEnumerations; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.domain.PaymentType; import org.apache.fineract.portfolio.tax.data.TaxGroupData; @@ -133,6 +145,12 @@ public class Charge extends AbstractPersistableCustom { @JoinColumn(name = "tax_group_id") private TaxGroup taxGroup; + @Column(name = "charge_disbursement_type_enum", nullable = false) + private Integer chargeDisbursementType; + + @OneToMany(orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "charge") + private List chargeRanges; + public static Charge fromJson(final JsonCommand command, final GLAccount account, final TaxGroup taxGroup, final PaymentType paymentType) { @@ -173,9 +191,38 @@ public static Charge fromJson(final JsonCommand command, final GLAccount account countFrequencyType = PeriodFrequencyType.fromInt(command.integerValueOfParameterNamed("countFrequencyType")); } - return new Charge(name, amount, currencyCode, chargeAppliesTo, chargeTimeType, chargeCalculationType, penalty, active, paymentMode, - feeOnMonthDay, feeInterval, minCap, maxCap, feeFrequency, enableFreeWithdrawalCharge, freeWithdrawalFrequency, - restartCountFrequency, countFrequencyType, account, taxGroup, enablePaymentType, paymentType); + final ChargeDisbursementType chargeDisbursementType = ChargeDisbursementType + .fromInt(command.integerValueOfParameterNamed("chargeDisbursementType")); + + Charge charge = new Charge(name, amount, currencyCode, chargeAppliesTo, chargeTimeType, chargeCalculationType, penalty, active, + paymentMode, feeOnMonthDay, feeInterval, minCap, maxCap, feeFrequency, enableFreeWithdrawalCharge, freeWithdrawalFrequency, + restartCountFrequency, countFrequencyType, account, taxGroup, enablePaymentType, paymentType, chargeDisbursementType); + + setChargeRanges(charge, command); + + return charge; + } + + private static void setChargeRanges(final Charge charge, final JsonCommand command) { + charge.setChargeRanges(new ArrayList()); + + if (command.hasParameter("adminFeeRanges")) { + JsonParserHelper helper = new JsonParserHelper(); + + JsonArray limits = command.arrayOfParameterNamed("adminFeeRanges"); + Iterator it = limits.iterator(); + while (it.hasNext()) { + JsonElement el = it.next(); + + ChargeRange chargeRange = new ChargeRange(); + chargeRange.setCharge(charge); + chargeRange.setMinDay(helper.extractIntegerWithLocaleNamed("adminFeeMin", el, new HashSet<>())); + chargeRange.setMaxDay(helper.extractIntegerWithLocaleNamed("adminFeeMax", el, new HashSet<>())); + chargeRange.setFeeRate(helper.extractBigDecimalWithLocaleNamed("adminFeeRate", el, new HashSet<>())); + + charge.getChargeRanges().add(chargeRange); + } + } } protected Charge() {} @@ -185,7 +232,8 @@ private Charge(final String name, final BigDecimal amount, final String currency final ChargePaymentMode paymentMode, final MonthDay feeOnMonthDay, final Integer feeInterval, final BigDecimal minCap, final BigDecimal maxCap, final Integer feeFrequency, final boolean enableFreeWithdrawalCharge, final Integer freeWithdrawalFrequency, final Integer restartFrequency, final PeriodFrequencyType restartFrequencyEnum, - final GLAccount account, final TaxGroup taxGroup, final boolean enablePaymentType, final PaymentType paymentType) { + final GLAccount account, final TaxGroup taxGroup, final boolean enablePaymentType, final PaymentType paymentType, + final ChargeDisbursementType chargeDisbursementType) { this.name = name; this.amount = amount; this.currencyCode = currencyCode; @@ -197,6 +245,7 @@ private Charge(final String name, final BigDecimal amount, final String currency this.account = account; this.taxGroup = taxGroup; this.chargePaymentMode = paymentMode == null ? null : paymentMode.getValue(); + this.chargeDisbursementType = chargeDisbursementType == null ? null : chargeDisbursementType.getValue(); final List dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("charges"); @@ -543,6 +592,14 @@ public Map update(final JsonCommand command) { actualChanges.put("locale", localeAsInput); this.chargePaymentMode = ChargePaymentMode.fromInt(newValue).getValue(); } + + final String chargeDisbursementTypeParamName = "chargeDisbursementType"; + if (command.isChangeInIntegerParameterNamed(chargeDisbursementTypeParamName, this.chargeDisbursementType)) { + final Integer newValue = command.integerValueOfParameterNamed(chargeDisbursementTypeParamName); + actualChanges.put(chargeDisbursementTypeParamName, newValue); + actualChanges.put("locale", localeAsInput); + this.chargeDisbursementType = ChargeDisbursementType.fromInt(newValue).getValue(); + } } if (command.hasParameter("feeOnMonthDay")) { @@ -635,6 +692,8 @@ public Map update(final JsonCommand command) { } } + setChargeRanges(this, command); + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } @@ -658,6 +717,7 @@ public ChargeData toData() { final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(this.chargeCalculation); final EnumOptionData chargePaymentmode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode); final EnumOptionData feeFrequencyType = ChargeEnumerations.chargePaymentMode(this.feeFrequency); + final EnumOptionData chargeDisbursementType = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode); GLAccountData accountData = null; if (account != null) { accountData = new GLAccountData(account.getId(), account.getName(), account.getGlCode()); @@ -676,7 +736,7 @@ public ChargeData toData() { return ChargeData.instance(getId(), this.name, this.amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, chargePaymentmode, getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active, this.enableFreeWithdrawal, this.freeWithdrawalFrequency, this.restartFrequency, this.restartFrequencyEnum, this.enablePaymentType, paymentTypeData, - this.minCap, this.maxCap, feeFrequencyType, accountData, taxGroupData); + this.minCap, this.maxCap, feeFrequencyType, accountData, taxGroupData, chargeDisbursementType); } public Integer getChargePaymentMode() { @@ -744,6 +804,10 @@ public boolean isDisbursementCharge() { || ChargeTimeType.fromInt(this.chargeTimeType).equals(ChargeTimeType.TRANCHE_DISBURSEMENT); } + public boolean isAddOnDisbursementType() { + return ChargeDisbursementType.fromInt(this.chargeDisbursementType).equals(ChargeDisbursementType.ADD_ON); + } + public TaxGroup getTaxGroup() { return this.taxGroup; } @@ -752,6 +816,45 @@ public void setTaxGroup(TaxGroup taxGroup) { this.taxGroup = taxGroup; } + public Integer getChargeDisbursementType() { + return chargeDisbursementType; + } + + public List getChargeRanges() { + return chargeRanges; + } + + public void setChargeRanges(List chargeRanges) { + this.chargeRanges = chargeRanges; + } + + public Pair getAddOnDisbursementChargeRate(LocalDate disbursementDate, LocalDate firstRepaymentDate) { + BigDecimal addOnDisbursementChargeRate = BigDecimal.ZERO; + int defaultDays = LoanProductConstants.DEFAULT_LIMIT_OF_DAYS_FOR_ADDON; + int daysAddOnApplicable = 0; + + if (isDisbursementCharge() && isAddOnDisbursementType() && this.chargeRanges != null && !this.chargeRanges.isEmpty()) { + // calculate days since disbursement date + int numberOfDays = Math.toIntExact(daysBetween(disbursementDate, firstRepaymentDate)); + if (numberOfDays > defaultDays) { + daysAddOnApplicable = numberOfDays - defaultDays; + } + for (ChargeRange chargeRange : this.chargeRanges) { + if ((chargeRange.getMinDay() != null && daysAddOnApplicable >= chargeRange.getMinDay()) + && (chargeRange.getMaxDay() != null && daysAddOnApplicable <= chargeRange.getMaxDay())) { + addOnDisbursementChargeRate = chargeRange.getFeeRate(); + break; + } + } + } + + return Pair.of(daysAddOnApplicable, addOnDisbursementChargeRate); + } + + private static long daysBetween(LocalDate d1, LocalDate d2) { + return ChronoUnit.DAYS.between(d1, d2); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -776,4 +879,5 @@ public int hashCode() { return Objects.hash(name, amount, currencyCode, chargeAppliesTo, chargeTimeType, chargeCalculation, chargePaymentMode, feeOnDay, feeInterval, feeOnMonth, penalty, active, deleted, minCap, maxCap, feeFrequency, account, taxGroup); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeDisbursementType.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeDisbursementType.java new file mode 100644 index 00000000000..331cdcd3f55 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeDisbursementType.java @@ -0,0 +1,67 @@ +/** + * 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.charge.domain; + +public enum ChargeDisbursementType { + + INVALID(0, "chargeDisbursementType.invalid"), // + REGULAR(1, "chargeDisbursementType.regular"), // + ADD_ON(2, "chargeDisbursementType.addOn"); + + private final Integer value; + private final String code; + + ChargeDisbursementType(final Integer value, final String code) { + this.value = value; + this.code = code; + } + + public Integer getValue() { + return this.value; + } + + public String getCode() { + return this.code; + } + + public static ChargeDisbursementType fromInt(final Integer chargeCalculation) { + ChargeDisbursementType chargeCalculationType = ChargeDisbursementType.INVALID; + if (chargeCalculation != null) { + switch (chargeCalculation) { + case 1: + chargeCalculationType = REGULAR; + break; + case 2: + chargeCalculationType = ADD_ON; + break; + default: + chargeCalculationType = REGULAR; + } + } + return chargeCalculationType; + } + + public boolean isRegular() { + return this.value.equals(ChargeDisbursementType.REGULAR.getValue()); + } + + public boolean isAddOn() { + return this.value.equals(ChargeDisbursementType.ADD_ON.getValue()); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRange.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRange.java new file mode 100644 index 00000000000..9676c830912 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRange.java @@ -0,0 +1,77 @@ +/** + * 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.charge.domain; + +import java.math.BigDecimal; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "m_charge_range") +public class ChargeRange extends AbstractPersistableCustom { + + @ManyToOne + @JoinColumn(name = "charge_id") + private Charge charge; + + @Column(name = "fee_rate", nullable = false) + private BigDecimal feeRate; + + @Column(name = "min_day", nullable = false) + private Integer minDay; + + @Column(name = "max_day", nullable = false) + private Integer maxDay; + + public Charge getCharge() { + return charge; + } + + public void setCharge(Charge charge) { + this.charge = charge; + } + + public BigDecimal getFeeRate() { + return feeRate; + } + + public void setFeeRate(BigDecimal feeRate) { + this.feeRate = feeRate; + } + + public Integer getMinDay() { + return minDay; + } + + public void setMinDay(Integer minDay) { + this.minDay = minDay; + } + + public Integer getMaxDay() { + return maxDay; + } + + public void setMaxDay(Integer maxDay) { + this.maxDay = maxDay; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRangeRepository.java new file mode 100644 index 00000000000..a8f2ea0125f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/domain/ChargeRangeRepository.java @@ -0,0 +1,31 @@ +/** + * 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.charge.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ChargeRangeRepository extends JpaRepository, JpaSpecificationExecutor { + + @Query("SELECT cr FROM ChargeRange cr WHERE cr.charge.id = :chargeId ORDER BY cr.minDay") + List findByChargeId(@Param("chargeId") Long chargeId); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java index eee9ff988a1..9595d8e3629 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.charge.serialization; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; @@ -27,10 +28,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TreeSet; + import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; @@ -38,8 +42,10 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.portfolio.charge.api.ChargesApiConstants; +import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargeDisbursementType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.springframework.beans.factory.annotation.Autowired; @@ -55,7 +61,8 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { "currencyOptions", "chargeAppliesTo", "chargeTimeType", "chargeCalculationType", "chargeCalculationTypeOptions", "penalty", "active", "chargePaymentMode", "feeOnMonthDay", "feeInterval", "monthDayFormat", "minCap", "maxCap", "feeFrequency", "enableFreeWithdrawalCharge", "freeWithdrawalFrequency", "restartCountFrequency", "countFrequencyType", "paymentTypeId", - "enablePaymentType", ChargesApiConstants.glAccountIdParamName, ChargesApiConstants.taxGroupIdParamName)); + "enablePaymentType", ChargesApiConstants.glAccountIdParamName, ChargesApiConstants.taxGroupIdParamName, "adminFeeRanges", + "chargeDisbursementType", "chargeDisbursementTypeOptions")); private final FromJsonHelper fromApiJsonHelper; @@ -130,6 +137,10 @@ public void validateForCreate(final String json) { } final ChargeAppliesTo appliesTo = ChargeAppliesTo.fromInt(chargeAppliesTo); + + final ChargeDisbursementType chargeDisbursementType = ChargeDisbursementType + .fromInt(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("chargeDisbursementType", element)); + if (appliesTo.isLoanCharge()) { // loan applicable validation final Integer chargeTimeType = this.fromApiJsonHelper.extractIntegerSansLocaleNamed("chargeTimeType", element); @@ -264,7 +275,11 @@ public void validateForCreate(final String json) { final Long taxGroupId = this.fromApiJsonHelper.extractLongNamed(ChargesApiConstants.taxGroupIdParamName, element); baseDataValidator.reset().parameter(ChargesApiConstants.taxGroupIdParamName).value(taxGroupId).notNull().longGreaterThanZero(); } - + + if (appliesTo.isLoanCharge() && chargeDisbursementType.isAddOn()) { + this.validateChargeLimits(false, dataValidationErrors, baseDataValidator, element); + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -350,8 +365,9 @@ public void validateForUpdate(final String json) { .isOneOfTheseValues(ChargeAppliesTo.validValues()); } + Integer chargeAppliesTo = 0; if (this.fromApiJsonHelper.parameterExists("chargeAppliesTo", element)) { - final Integer chargeAppliesTo = this.fromApiJsonHelper.extractIntegerSansLocaleNamed("chargeAppliesTo", element); + chargeAppliesTo = this.fromApiJsonHelper.extractIntegerSansLocaleNamed("chargeAppliesTo", element); baseDataValidator.reset().parameter("chargeAppliesTo").value(chargeAppliesTo).notNull() .isOneOfTheseValues(ChargeAppliesTo.validValues()); } @@ -426,6 +442,15 @@ public void validateForUpdate(final String json) { baseDataValidator.reset().parameter(ChargesApiConstants.taxGroupIdParamName).value(taxGroupId).notNull().longGreaterThanZero(); } + ChargeAppliesTo appliesTo = null; + if (chargeAppliesTo != null) { + appliesTo = ChargeAppliesTo.fromInt(chargeAppliesTo); + } + final ChargeDisbursementType chargeDisbursementType = ChargeDisbursementType + .fromInt(this.fromApiJsonHelper.extractIntegerSansLocaleNamed("chargeDisbursementType", element)); + if (appliesTo != null && appliesTo.isLoanCharge() && chargeDisbursementType.isAddOn()) { + this.validateChargeLimits(false, dataValidationErrors, baseDataValidator, element); + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -457,4 +482,171 @@ private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors, final DataValidatorBuilder baseDataValidator, final JsonElement element) { + boolean hasRanges = false; + + if (this.fromApiJsonHelper.parameterExists("adminFeeRanges", element)) { + JsonArray ranges = this.fromApiJsonHelper.extractJsonArrayNamed("adminFeeRanges", element); + + hasRanges = ranges.size() > 0; + + Iterator it = ranges.iterator(); + Set chargeLimits = new TreeSet<>((a, b) -> { + int result = this.compareIntegers(a.adminFeeMin, b.adminFeeMin); + if (result == 0) { + result = this.compareIntegers(a.adminFeeMax, b.adminFeeMax); + if (result == 0) { + result = this.compareBigDecimals(a.adminFeeRate, b.adminFeeRate); + } + } + return result; + }); + while (it.hasNext()) { + JsonElement el = it.next(); + + BigDecimal a = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("adminFeeRate", el); + baseDataValidator.reset().parameter("adminFeeRate").value(a).notNull().zeroOrPositiveAmount(); + + Integer min = null; + Integer max = null; + if (this.fromApiJsonHelper.parameterExists("adminFeeMin", el)) { + min = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("adminFeeMin", el); + baseDataValidator.reset().parameter("adminFeeMin").value(min).ignoreIfNull().zeroOrPositiveAmount(); + } + if (this.fromApiJsonHelper.parameterExists("adminFeeMax", el)) { + max = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("adminFeeMax", el); + baseDataValidator.reset().parameter("adminFeeMax").value(max).ignoreIfNull().zeroOrPositiveAmount(); + } + if (min != null && max != null) { + if ((max - min) <= 0) { + dataValidationErrors.add(ApiParameterError.parameterError( + "validation.msg.charge.ranges.min.greaterorequal.to", + "The parameter adminFeeMin must be less than " + max, + "adminFeeMin", + min, max + )); + } + } + + chargeLimits.add(new ChargeData.ChargeRangeData(min, max, a)); + } + + Iterator itt = chargeLimits.iterator(); + if (itt.hasNext()) { + ChargeData.ChargeRangeData prev = itt.next(); + while (itt.hasNext()) { + ChargeData.ChargeRangeData curr = itt.next(); + + if (prev.adminFeeMin == null && curr.adminFeeMin == null) { + dataValidationErrors.add(ApiParameterError.parameterError( + "validation.msg.ranges.overlap", + "Ranges cannot overlap", + "adminFeeMin", prev.adminFeeMin, curr.adminFeeMin + )); + } else if ((prev.adminFeeMax == null || prev.adminFeeMax.doubleValue() == Double.POSITIVE_INFINITY)) { + dataValidationErrors.add(ApiParameterError.parameterError( + "validation.msg.ranges.overlap", + "Ranges cannot overlap", + "adminFeeMax", prev.adminFeeMax, curr.adminFeeMin + )); + } else if (prev.adminFeeMax >= curr.adminFeeMin) { + dataValidationErrors.add(ApiParameterError.parameterError( + "validation.msg.ranges.overlap", + "Ranges cannot overlap", + "adminFeeMin", prev.adminFeeMax, curr.adminFeeMin + )); + } + + prev = curr; + } + } + } + + if (!hasRanges) { + if (this.fromApiJsonHelper.parameterExists("adminFeeRate", element) || !isAmountNullable) { + final BigDecimal feeRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("adminFeeRate", element.getAsJsonObject()); + DataValidatorBuilder bdv = baseDataValidator.reset(); + bdv.parameter("adminFeeRate").value(feeRate); + if (isAmountNullable) { + bdv.ignoreIfNull(); + } else { + bdv.notNull(); + } + bdv.positiveAmount(); + } + } + } + + private int compareIntegers(final Integer a, final Integer b) { + int result = 0; + if (a == null || b == null) { + if (a == null) { + if (b == null) { + result = 0; + } else { + result = -1; + } + } else { + result = 1; + } + } else { + if ((a.intValue() == Double.POSITIVE_INFINITY && b.intValue() == Double.POSITIVE_INFINITY)) { + result = 0; + } else { + if ((a.intValue() == Double.NEGATIVE_INFINITY && b.intValue() == Double.NEGATIVE_INFINITY)) { + result = 0; + } else { + if (a.intValue() == Double.POSITIVE_INFINITY) { + result = 1; + } else if (b.intValue() == Double.POSITIVE_INFINITY) { + result = -1; + } else if (a.intValue() == Double.NEGATIVE_INFINITY) { + result = -1; + } else if (b.intValue() == Double.NEGATIVE_INFINITY) { + result = 1; + } else { + result = a.compareTo(b); + } + } + } + } + return result; + } + + private int compareBigDecimals(final BigDecimal a, final BigDecimal b) { + int result = 0; + if (a == null || b == null) { + if (a == null) { + if (b == null) { + result = 0; + } else { + result = -1; + } + } else { + result = 1; + } + } else { + if ((a.doubleValue() == Double.POSITIVE_INFINITY && b.doubleValue() == Double.POSITIVE_INFINITY)) { + result = 0; + } else { + if ((a.doubleValue() == Double.NEGATIVE_INFINITY && b.doubleValue() == Double.NEGATIVE_INFINITY)) { + result = 0; + } else { + if (a.doubleValue() == Double.POSITIVE_INFINITY) { + result = 1; + } else if (b.doubleValue() == Double.POSITIVE_INFINITY) { + result = -1; + } else if (a.doubleValue() == Double.NEGATIVE_INFINITY) { + result = -1; + } else if (b.doubleValue() == Double.NEGATIVE_INFINITY) { + result = 1; + } else { + result = Double.valueOf(a.doubleValue()).compareTo(b.doubleValue()); + } + } + } + } + return result; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformService.java index abd3b7400ae..31628ba136d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformService.java @@ -47,4 +47,6 @@ public interface ChargeDropdownReadPlatformService { List retrieveSharesCollectionTimeTypes(); + List retrieveDisbursementTypeOptions(); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java index d694fb68b17..4b3a09468b7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.charge.service; import static org.apache.fineract.portfolio.charge.service.ChargeEnumerations.chargeCalculationType; +import static org.apache.fineract.portfolio.charge.service.ChargeEnumerations.chargeDisbursementType; import static org.apache.fineract.portfolio.charge.service.ChargeEnumerations.chargePaymentMode; import static org.apache.fineract.portfolio.charge.service.ChargeEnumerations.chargeTimeType; @@ -28,6 +29,7 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargeDisbursementType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.springframework.stereotype.Service; @@ -126,4 +128,10 @@ public List retrieveSharesCollectionTimeTypes() { return Arrays.asList(chargeTimeType(ChargeTimeType.SHAREACCOUNT_ACTIVATION), chargeTimeType(ChargeTimeType.SHARE_PURCHASE), chargeTimeType(ChargeTimeType.SHARE_REDEEM)); } + + @Override + public List retrieveDisbursementTypeOptions() { + return Arrays.asList(chargeDisbursementType(ChargeDisbursementType.REGULAR), chargeDisbursementType(ChargeDisbursementType.ADD_ON)); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java index a8af2ad6d5d..8b1a40f3f94 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeEnumerations.java @@ -21,6 +21,7 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargeDisbursementType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -191,4 +192,27 @@ public static EnumOptionData chargePaymentMode(final ChargePaymentMode type) { return optionData; } + public static EnumOptionData chargeDisbursementType(final int id) { + return chargeDisbursementType(ChargeDisbursementType.fromInt(id)); + } + + public static EnumOptionData chargeDisbursementType(final ChargeDisbursementType type) { + EnumOptionData optionData = null; + switch (type) { + case REGULAR: + optionData = new EnumOptionData(ChargeDisbursementType.REGULAR.getValue().longValue(), + ChargeDisbursementType.REGULAR.getCode(), "Regular"); + break; + case ADD_ON: + optionData = new EnumOptionData(ChargeDisbursementType.ADD_ON.getValue().longValue(), + ChargeDisbursementType.ADD_ON.getCode(), "Add-on"); + break; + default: + optionData = new EnumOptionData(ChargeDisbursementType.INVALID.getValue().longValue(), + ChargeDisbursementType.INVALID.getCode(), "Invalid"); + break; + } + return optionData; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 568d60bacca..1818f1ee27e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -38,6 +38,8 @@ import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo; +import org.apache.fineract.portfolio.charge.domain.ChargeRange; +import org.apache.fineract.portfolio.charge.domain.ChargeRangeRepository; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.exception.ChargeNotFoundException; import org.apache.fineract.portfolio.common.service.CommonEnumerations; @@ -69,6 +71,7 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final TaxReadPlatformService taxReadPlatformService; private final ConfigurationDomainServiceJpa configurationDomainServiceJpa; + private final ChargeRangeRepository chargeRangeRepository; @Autowired public ChargeReadPlatformServiceImpl(final CurrencyReadPlatformService currencyReadPlatformService, @@ -76,7 +79,7 @@ public ChargeReadPlatformServiceImpl(final CurrencyReadPlatformService currencyR final DropdownReadPlatformService dropdownReadPlatformService, final FineractEntityAccessUtil fineractEntityAccessUtil, final AccountingDropdownReadPlatformService accountingDropdownReadPlatformService, final TaxReadPlatformService taxReadPlatformService, final ConfigurationDomainServiceJpa configurationDomainServiceJpa, - final NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + final NamedParameterJdbcTemplate namedParameterJdbcTemplate, final ChargeRangeRepository chargeRangeRepository) { this.chargeDropdownReadPlatformService = chargeDropdownReadPlatformService; this.jdbcTemplate = jdbcTemplate; this.currencyReadPlatformService = currencyReadPlatformService; @@ -86,6 +89,7 @@ public ChargeReadPlatformServiceImpl(final CurrencyReadPlatformService currencyR this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; this.taxReadPlatformService = taxReadPlatformService; this.configurationDomainServiceJpa = configurationDomainServiceJpa; + this.chargeRangeRepository = chargeRangeRepository; } @Override @@ -124,12 +128,19 @@ public ChargeData retrieveCharge(final Long chargeId) { sql += addInClauseToSQL_toLimitChargesMappedToOffice_ifOfficeSpecificProductsEnabled(); sql = sql + " ;"; - return this.jdbcTemplate.queryForObject(sql, rm, new Object[] { chargeId }); // NOSONAR + ChargeData charge = this.jdbcTemplate.queryForObject(sql, rm, new Object[] { chargeId }); // NOSONAR + appendRangeToCharge(charge); + return charge; } catch (final EmptyResultDataAccessException e) { throw new ChargeNotFoundException(chargeId, e); } } + private void appendRangeToCharge(ChargeData charge) { + List chargeRanges = this.chargeRangeRepository.findByChargeId(charge.getId()); + chargeRanges.forEach(cl -> charge.addRange(cl)); + } + @Override public ChargeData retrieveNewChargeDetails() { @@ -158,13 +169,14 @@ public ChargeData retrieveNewChargeDetails() { final String accountMappingForChargeConfig = this.configurationDomainServiceJpa.getAccountMappingForCharge(); final List expenseAccountOptions = this.accountingDropdownReadPlatformService.retrieveExpenseAccountOptions(); final List assetAccountOptions = this.accountingDropdownReadPlatformService.retrieveAssetAccountOptions(); + final List chargeDisbursementTypeOptions = this.chargeDropdownReadPlatformService.retrieveDisbursementTypeOptions(); return ChargeData.template(currencyOptions, allowedChargeCalculationTypeOptions, allowedChargeAppliesToOptions, allowedChargeTimeOptions, chargePaymentOptions, loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, feeFrequencyOptions, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, accountMappingForChargeConfig, expenseAccountOptions, - assetAccountOptions); + assetAccountOptions, chargeDisbursementTypeOptions); } @Override @@ -220,7 +232,6 @@ public Collection retrieveLoanAccountApplicableCharges(final Long lo /** * @param excludeChargeTimes * @param excludeClause - * @param params * @return */ private void processChargeExclusionsForLoans(ChargeTimeType[] excludeChargeTimes, StringBuilder excludeClause) { @@ -287,7 +298,9 @@ public String chargeSchema() { + "c.charge_applies_to_enum as chargeAppliesTo, c.charge_time_enum as chargeTime, " + "c.charge_payment_mode_enum as chargePaymentMode, " + "c.charge_calculation_enum as chargeCalculation, c.is_penalty as penalty, " - + "c.is_active as active, c.is_free_withdrawal as isFreeWithdrawal, c.free_withdrawal_charge_frequency as freeWithdrawalChargeFrequency, c.restart_frequency as restartFrequency, c.restart_frequency_enum as restartFrequencyEnum," + + "c.is_active as active, c.is_free_withdrawal as isFreeWithdrawal, c.free_withdrawal_charge_frequency as freeWithdrawalChargeFrequency, " + + "c.restart_frequency as restartFrequency, c.restart_frequency_enum as restartFrequencyEnum, " + + "c.charge_disbursement_type_enum as chargeDisbursementType, " + "oc.name as currencyName, oc.decimal_places as currencyDecimalPlaces, " + "oc.currency_multiplesof as inMultiplesOf, oc.display_symbol as currencyDisplaySymbol, " + "oc.internationalized_name_code as currencyNameCode, oc.int_code as intCode, c.fee_on_day as feeOnDay, c.fee_on_month as feeOnMonth, " @@ -334,6 +347,9 @@ public ChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final i final int chargeTime = rs.getInt("chargeTime"); final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(chargeTime); + final int chargeDisbursementTypeId = rs.getInt("chargeDisbursementType"); + final EnumOptionData chargeDisbursementType = ChargeEnumerations.chargeDisbursementType(chargeDisbursementTypeId); + final int chargeCalculation = rs.getInt("chargeCalculation"); final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(chargeCalculation); @@ -391,7 +407,7 @@ public ChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final i return ChargeData.instance(id, name, amount, currency, chargeTimeType, chargeAppliesToType, chargeCalculationType, chargePaymentMode, feeOnMonthDay, feeInterval, penalty, active, isFreeWithdrawal, freeWithdrawalChargeFrequency, restartFrequency, restartFrequencyEnum, isPaymentType, paymentTypeData, minCap, maxCap, feeFrequencyType, glAccountData, - taxGroupData); + taxGroupData, chargeDisbursementType); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 22e5410ef5d..1c2d05ff1c3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -39,6 +39,8 @@ import javax.persistence.OneToOne; import javax.persistence.Table; import javax.persistence.UniqueConstraint; + +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -46,6 +48,7 @@ import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; +import org.apache.fineract.portfolio.charge.domain.ChargeDisbursementType; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException; @@ -124,6 +127,9 @@ public class LoanCharge extends AbstractPersistableCustom { @Column(name = "external_id") private String externalId; + @Column(name = "charge_disbursement_type_enum", nullable = false) + private Integer chargeDisbursementType; + @OneToOne(mappedBy = "loancharge", cascade = CascadeType.ALL, optional = true, orphanRemoval = true, fetch = FetchType.EAGER) private LoanOverdueInstallmentCharge overdueInstallmentCharge; @@ -205,6 +211,7 @@ public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeD dueDate, chargePaymentMode, null, loanCharge); final String externalId = command.stringValueOfParameterNamedAllowingNull("externalId"); newLoanCharge.setExternalId(externalId); + newLoanCharge.setChargeDisbursementType(chargeDefinition.getChargeDisbursementType()); return newLoanCharge; } @@ -230,6 +237,7 @@ public LoanCharge(final Loan loan, final Charge chargeDefinition, final BigDecim this.penaltyCharge = chargeDefinition.isPenalty(); this.minCap = chargeDefinition.getMinCap(); this.maxCap = chargeDefinition.getMaxCap(); + this.chargeDisbursementType = chargeDefinition.getChargeDisbursementType(); this.chargeTime = chargeDefinition.getChargeTimeType(); if (chargeTime != null) { @@ -396,7 +404,8 @@ public void update(final BigDecimal amount, final LocalDate dueDate, final BigDe this.dueDate = dueDate; } - if (amount != null) { + BigDecimal updatedAmount = amount; + if (updatedAmount != null) { switch (ChargeCalculationType.fromInt(this.chargeCalculation)) { case INVALID: break; @@ -405,24 +414,35 @@ public void update(final BigDecimal amount, final LocalDate dueDate, final BigDe if (numberOfRepayments == null) { numberOfRepayments = this.loan.fetchNumberOfInstallmensAfterExceptions(); } - this.amount = amount.multiply(BigDecimal.valueOf(numberOfRepayments)); + this.amount = updatedAmount.multiply(BigDecimal.valueOf(numberOfRepayments)); } else { - this.amount = amount; + this.amount = updatedAmount; } break; case PERCENT_OF_AMOUNT: case PERCENT_OF_AMOUNT_AND_INTEREST: case PERCENT_OF_INTEREST: case PERCENT_OF_DISBURSEMENT_AMOUNT: - this.percentage = amount; - this.amountPercentageAppliedTo = loanPrincipal; - if (loanCharge.compareTo(BigDecimal.ZERO) == 0) { - loanCharge = percentageOf(this.amountPercentageAppliedTo); + if (this.loan != null && isDisbursementCharge() && this.isAddOnDisbursementType()) { + LocalDate disbursementDate = this.loan.getDisbursementDate(); + LocalDate firstRepaymentDate = this.loan.fetchRepaymentScheduleInstallment(1).getDueDate(); + Pair addOnDaysAndRate = this.charge.getAddOnDisbursementChargeRate(disbursementDate, firstRepaymentDate); + Integer daysToApplyAddOn = addOnDaysAndRate.getLeft(); + BigDecimal feeRate = addOnDaysAndRate.getRight(); + this.percentage = feeRate; + updatedAmount = feeRate; + loanCharge = percentageOf(this.amountPercentageAppliedTo).multiply(BigDecimal.valueOf(daysToApplyAddOn), MoneyHelper.getMathContext()); + } else { + this.percentage = amount; + if (loanCharge.compareTo(BigDecimal.ZERO) == 0) { + loanCharge = percentageOf(this.amountPercentageAppliedTo); + } } + this.amountPercentageAppliedTo = loanPrincipal; this.amount = minimumAndMaximumCap(loanCharge); break; } - this.amountOrPercentage = amount; + this.amountOrPercentage = updatedAmount; this.amountOutstanding = calculateOutstanding(); if (this.loan != null && isInstalmentFee()) { updateInstallmentCharges(); @@ -1075,4 +1095,16 @@ public void setExternalId(String externalId) { this.externalId = externalId; } + public Integer getChargeDisbursementType() { + return chargeDisbursementType; + } + + public void setChargeDisbursementType(Integer chargeDisbursementType) { + this.chargeDisbursementType = chargeDisbursementType; + } + + public boolean isAddOnDisbursementType() { + return ChargeDisbursementType.fromInt(this.chargeDisbursementType).equals(ChargeDisbursementType.ADD_ON); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java index 924a2f6a953..952f96f4015 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java @@ -142,6 +142,7 @@ public ChargeData retrieveLoanChargeTemplate() { .retrieveSavingsCollectionTimeTypes(); final List clientChargeCalculationTypeOptions = null; final List clientChargeTimeTypeOptions = null; + final List chargeDisbursementTypeOptions = null; final List feeFrequencyOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); // this field is applicable only for client charges @@ -158,7 +159,7 @@ public ChargeData retrieveLoanChargeTemplate() { loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, feeFrequencyOptions, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, - accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions); + accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions, chargeDisbursementTypeOptions); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 6e06f16ce94..1cc6b6665aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -53,6 +53,9 @@ public interface LoanProductConstants { String INTEREST_RATE_VALUE_USAGE_CONDITION_PARAM_NAME = "interestRateValueUsageCondition"; String INTEREST_RATE_CYCLE_NUMBER_PARAM_NAME = "interestRateCycleNumber"; + String LIMIT_OF_DAYS_FOR_ADDON = "daysLimitAddOn"; + Integer DEFAULT_LIMIT_OF_DAYS_FOR_ADDON = Integer.valueOf(35); + String PRINCIPAL = "principal"; String MIN_PRINCIPAL = "minPrincipal"; String MAX_PRINCIPAL = "maxPrincipalValue"; 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 b8524b61bf5..817631b431c 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 @@ -42,6 +42,7 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; +import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -202,6 +203,7 @@ public class LoanProductData implements Serializable { private final Integer ageLimitBlock; private final EnumOptionData ownerTypeOption; private final boolean addNewCyclesEnabled; + private final Integer daysLimitAddOn; /** * Used when returning lookup information about loan product for dropdowns. @@ -291,6 +293,7 @@ public static LoanProductData lookup(final Long id, final String name, final Boo final Integer ageLimitBlock = null; final EnumOptionData ownerTypeOption = null; final boolean addNewCyclesEnabled = true; + final Integer daysLimitAddOn = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, annualInterestRate, repaymentFrequencyType, interestRateFrequencyType, @@ -307,7 +310,8 @@ public static LoanProductData lookup(final Long id, final String name, final Boo floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled); + fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled, + daysLimitAddOn); } @@ -397,6 +401,7 @@ public static LoanProductData lookupWithCurrency(final Long id, final String nam final Integer ageLimitBlock = null; final EnumOptionData ownerTypeOption = null; final boolean addNewCyclesEnabled = true; + final Integer daysLimitAddOn = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -414,7 +419,8 @@ public static LoanProductData lookupWithCurrency(final Long id, final String nam floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled); + fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled, + daysLimitAddOn); } @@ -511,6 +517,7 @@ public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { final Integer ageLimitBlock = null; final EnumOptionData ownerTypeOption = null; final boolean addNewCyclesEnabled = true; + final Integer daysLimitAddOn = LoanProductConstants.DEFAULT_LIMIT_OF_DAYS_FOR_ADDON; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -528,7 +535,8 @@ public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled); + fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled, + daysLimitAddOn); } @@ -619,6 +627,7 @@ public static LoanProductData loanProductWithFloatingRates(final Long id, final final Integer ageLimitBlock = null; final EnumOptionData ownerTypeOption = null; final boolean addNewCyclesEnabled = true; + final Integer daysLimitAddOn = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -636,7 +645,8 @@ public static LoanProductData loanProductWithFloatingRates(final Long id, final floatingRateName, interestRateDifferential, minDifferentialLendingRate, defaultDifferentialLendingRate, maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, rates, isRatesEnabled, - fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled); + fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, addNewCyclesEnabled, + daysLimitAddOn); } @@ -682,7 +692,7 @@ public LoanProductData(final Long id, final String name, final String shortName, final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization, Collection rateOptions, Collection rates, final boolean isRatesEnabled, final BigDecimal fixedPrincipalPercentagePerInstallmen, final Integer ageLimitWarning, final Integer ageLimitBlock, - final EnumOptionData ownerTypeOption, final boolean addNewCyclesEnabled) { + final EnumOptionData ownerTypeOption, final boolean addNewCyclesEnabled, final Integer daysLimitAddOn) { this.id = id; this.name = name; this.shortName = shortName; @@ -801,6 +811,7 @@ public LoanProductData(final Long id, final String name, final String shortName, this.isEqualAmortization = isEqualAmortization; this.ageLimitBlock = ageLimitBlock; this.ageLimitWarning = ageLimitWarning; + this.daysLimitAddOn = daysLimitAddOn; } @@ -954,6 +965,7 @@ public LoanProductData(final LoanProductData productData, final Collection nullIfEmpty(final Collection charges) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index df6d2a9b605..014cd690519 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -207,6 +207,10 @@ public class LoanProduct extends AbstractPersistableCustom { @Column(name = "add_new_cycles_enabled", nullable = false) private boolean addNewCycledEnabled; + @Column(name = "limit_of_days_for_addon", nullable = false) + private Integer daysLimitAddOn; + + public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy, final List productCharges, final JsonCommand command, final AprCalculator aprCalculator, FloatingRate floatingRate, final List productRates) { @@ -224,6 +228,7 @@ public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactio final BigDecimal maxPrincipal = command.bigDecimalValueOfParameterNamed("maxPrincipal"); final Integer ageLimitWarning = command.integerValueOfParameterNamed("ageLimitWarning"); final Integer ageLimitBlock = command.integerValueOfParameterNamed("ageLimitBlock"); + final Integer daysLimitAddOn = command.integerValueOfParameterNamed("daysLimitAddOn"); final InterestMethod interestMethod = InterestMethod.fromInt(command.integerValueOfParameterNamed("interestType")); final InterestCalculationPeriodMethod interestCalculationPeriodMethod = InterestCalculationPeriodMethod @@ -408,7 +413,7 @@ public static LoanProduct assembleFromJson(final Fund fund, final LoanTransactio minimumGapBetweenInstallments, maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment, disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, ageLimitWarning, ageLimitBlock, - addNewCyclesEnabled, loanProductOwnerType); + addNewCyclesEnabled, loanProductOwnerType, daysLimitAddOn); } @@ -646,7 +651,7 @@ public LoanProduct(final Fund fund, final LoanTransactionProcessingStrategy tran final List rates, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean disallowExpectedDisbursements, final boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType, final Integer overAppliedNumber, final Integer ageLimitWarning, final Integer ageLimitBlock, final boolean addNewCyclesEnabled, - final LoanProductOwnerType loanProductOwnerType) { + final LoanProductOwnerType loanProductOwnerType, final Integer daysLimitAddOn) { this.fund = fund; this.transactionProcessingStrategy = transactionProcessingStrategy; this.name = name.trim(); @@ -727,6 +732,7 @@ public LoanProduct(final Fund fund, final LoanTransactionProcessingStrategy tran this.ageLimitBlock = ageLimitBlock; this.ageLimitWarning = ageLimitWarning; this.addNewCycledEnabled = addNewCyclesEnabled; + this.daysLimitAddOn = daysLimitAddOn; if (loanProductOwnerType != null) { this.ownerType = loanProductOwnerType.getValue(); @@ -1245,6 +1251,13 @@ public Map update(final JsonCommand command, final AprCalculator this.ageLimitBlock = newValue; } + final String daysLimitAddOn = "daysLimitAddOn"; + if (command.isChangeInIntegerParameterNamed(daysLimitAddOn, this.daysLimitAddOn)) { + final Integer newValue = command.integerValueOfParameterNamed(daysLimitAddOn); + actualChanges.put(daysLimitAddOn, newValue); + this.daysLimitAddOn = newValue; + } + if (command.isChangeInIntegerParameterNamed(LoanProductConstants.LOAN_PRODUCT_OWNER_TYPE, this.ownerType)) { final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.LOAN_PRODUCT_OWNER_TYPE); actualChanges.put(LoanProductConstants.LOAN_PRODUCT_OWNER_TYPE, newValue); 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 444d4f0d003..5e262a8dd30 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 @@ -112,7 +112,7 @@ public final class LoanProductDataValidator { LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME, LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, - LoanProductConstants.OVER_APPLIED_NUMBER)); + LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.LIMIT_OF_DAYS_FOR_ADDON)); private static final String[] supportedloanConfigurableAttributes = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyIdParamName, 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 e2cdd3df6d2..a8cd6b8018b 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 @@ -238,8 +238,9 @@ public String loanProductSchema() { + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " + "lvi.maximum_gap as maximumGap, " + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization, " - + " lp.owner_type_enum as productOwnerType, lp.add_new_cycles_enabled as addNewCyclesEnabled " - + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + + "lp.owner_type_enum as productOwnerType, lp.add_new_cycles_enabled as addNewCyclesEnabled, " + + " lp.limit_of_days_for_addon as daysLimitAddOn " + " from m_product_loan lp " + + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " + " left join m_product_loan_guarantee_details lpg on lpg.loan_product_id=lp.id " + " left join ref_loan_transaction_processing_strategy ltps on ltps.id = lp.loan_transaction_strategy_id" @@ -473,6 +474,7 @@ public LoanProductData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi final boolean isRatesEnabled = false; final Integer ageLimitWarning = JdbcSupport.getInteger(rs, "ageLimitWarning"); final Integer ageLimitBlock = JdbcSupport.getInteger(rs, "ageLimitBlock"); + final Integer daysLimitAddOn = JdbcSupport.getInteger(rs, "daysLimitAddOn"); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -492,7 +494,7 @@ public LoanProductData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi maxDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableIntallmentsAllowed, minimumGap, maximumGap, syncExpectedWithDisbursementDate, canUseForTopup, isEqualAmortization, rateOptions, this.rates, isRatesEnabled, fixedPrincipalPercentagePerInstallment, ageLimitWarning, ageLimitBlock, ownerTypeOption, - addNewCyclesEnabled); + addNewCyclesEnabled, daysLimitAddOn); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java index 1e7308697ff..0404b8a54e6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java @@ -166,6 +166,7 @@ public ChargeData retrieveSavingsAccountChargeTemplate() { .retrieveSavingsCollectionTimeTypes(); final List clientChargeCalculationTypeOptions = null; final List clientChargeTimeTypeOptions = null; + final List chargeDisbursementTypeOptions = null; final List feeFrequencyOptions = this.dropdownReadPlatformService.retrievePeriodFrequencyTypeOptions(); // this field is applicable only for client charges @@ -183,7 +184,7 @@ public ChargeData retrieveSavingsAccountChargeTemplate() { loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, clientChargeCalculationTypeOptions, clientChargeTimeTypeOptions, feeFrequencyOptions, incomeOrLiabilityAccountOptions, taxGroupOptions, shareChargeCalculationTypeOptions, shareChargeTimeTypeOptions, - accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions); + accountMappingForChargeConfig, expenseAccountOptions, assetAccountOptions, chargeDisbursementTypeOptions); } @Override diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 293e182dce6..d2c6fdd21b6 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -105,4 +105,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0089_FBR_104_create_charge_admin_fee_configurations.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0089_FBR_104_create_charge_admin_fee_configurations.xml new file mode 100644 index 00000000000..9a4d894c40e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0089_FBR_104_create_charge_admin_fee_configurations.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +