diff --git a/api-docs/apiLive.htm b/api-docs/apiLive.htm index 8cb5421f4..53fe805f3 100644 --- a/api-docs/apiLive.htm +++ b/api-docs/apiLive.htm @@ -11035,7 +11035,8 @@ Request Body: 2 = Specified Due Date : Adhoc charge can be applied for loans and savings on a specified due date.
3 = Savings Activation : This charge is applicable for savings account and charged at the time of account activation. This charge will be autodeducted once the account is activated with sufficient opening balance.
5 = Withdrawal fee : Charge applied to every withdrawal of savings account.
- 6 = Annual Fee : Recurring charge applied annually for savings on a specified Month and day. + 6 = Annual Fee : Recurring charge applied annually for savings on a specified Month and day.
+ 7 = Monthly Fee : Recurring charge applied on regular interval of months for savings. @@ -11052,6 +11053,22 @@ Request Body: Rules for calculating the amount for a particular Charge. Refer Charges Template for additional details + + feeOnMonthDay + + + Used along with monthDayFormat to indicate the recurring charge due date starting on a given day of the month and follow specified feeInterval. e.g. fee due date starting on 10 May with feeInterval of two (2) months then a recurring fee on 10th of every two months is applied to savings account. monthDayFormat indicates day and month formatting used e.g. "dd MMM" for 10 May
+ This field is mandatory if chargeTimeType is of type Monthly Fee + + + + feeInterval + + + Fee to be applied on a specified interval (number of months).
+ This field is mandatory if chargeTimeType is of type Monthly Fee + + @@ -11144,6 +11161,11 @@ GET https://DomainName/api/v1/charges/template "id": 6, "code": "chargeTimeType.annualFee", "value": "Annual Fee" + }, + { + "id": 7, + "code": "chargeTimeType.monthlyFee", + "value": "Monthly Fee" } ], "chargePaymetModeOptions": @@ -11201,6 +11223,10 @@ GET https://DomainName/api/v1/charges/template { "code" : "chargeTimeType.annualFee", "id" : 6, "value" : "Annual Fee" + }, + { "code" : "chargeTimeType.monthlyFee", + "id" : 7, + "value" : "Monthly Fee" } ] } @@ -11283,7 +11309,31 @@ Request Body: { "resourceId": 5 } - + + +POST charges +Content-Type: application/json +Request Body: +{ + "locale": "en", + "name": "Quarterly Fee", + "amount": "5", + "currencyCode": "USD", + "chargeAppliesTo": "2", + "chargeTimeType": "7", + "feeOnMonthDay": "10 May", + "monthDayFormat": "dd MMM", + "feeInterval": "4", + "chargeCalculationType": "1", + "active": "true", + "penalty": "false" +} + + +{ + "resourceId": 6 +} + @@ -13242,6 +13292,46 @@ Content-Type: application/json "code": "chargepaymentmode.regular", "value": "Regular" } + }, + { + "id": 7, + "name": "Quarterly fee", + "active": true, + "penalty": false, + "currency": { + "code": "USD", + "name": "US Dollar", + "decimalPlaces": 2, + "displaySymbol": "$", + "nameCode": "currency.USD", + "displayLabel": "US Dollar ($)" + }, + "amount": 5, + "chargeTimeType": { + "id": 7, + "code": "chargeTimeType.monthlyFee", + "value": "Monthly Fee" + }, + "chargeAppliesTo": { + "id": 2, + "code": "chargeAppliesTo.savings", + "value": "Savings" + }, + "chargeCalculationType": { + "id": 1, + "code": "chargeCalculationType.flat", + "value": "Flat" + }, + "chargePaymentMode": { + "id": 0, + "code": "chargepaymentmode.regular", + "value": "Regular" + }, + "feeOnMonthDay": [ + 10, + 5 + ], + "feeInterval": 4 } ] } diff --git a/mifosng-db/migrations/core_db/V122__recurring_fee_support_for_savings.sql b/mifosng-db/migrations/core_db/V122__recurring_fee_support_for_savings.sql new file mode 100644 index 000000000..85f4c80ea --- /dev/null +++ b/mifosng-db/migrations/core_db/V122__recurring_fee_support_for_savings.sql @@ -0,0 +1,8 @@ +ALTER TABLE `m_charge` + ADD COLUMN `fee_on_day` SMALLINT(5) NULL AFTER `amount`, + ADD COLUMN `fee_interval` SMALLINT(5) NULL AFTER `fee_on_day`, + ADD COLUMN `fee_on_month` SMALLINT(5) NULL AFTER `fee_interval`; + + +ALTER TABLE `m_savings_account_charge` + ADD COLUMN `fee_interval` SMALLINT(5) NULL DEFAULT NULL AFTER `fee_on_day`; \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/JsonParserHelper.java b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/JsonParserHelper.java index a7f6dfc40..cb8bf3369 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/JsonParserHelper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/infrastructure/core/serialization/JsonParserHelper.java @@ -299,7 +299,7 @@ public class JsonParserHelper { } catch (final IllegalArgumentException e) { final List dataValidationErrors = new ArrayList(); final ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.month.day", - "The parameter " + parameterName + " is invalid based on the dateFormat: '" + dateFormat + "The parameter " + parameterName + " is invalid based on the monthDayFormat: '" + dateFormat + "' and locale: '" + clientApplicationLocale + "' provided:", parameterName, valueAsString, dateFormat); dataValidationErrors.add(error); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/data/ChargeData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/data/ChargeData.java index 903438c3d..74eeb4fc1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/data/ChargeData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/data/ChargeData.java @@ -32,6 +32,8 @@ public class ChargeData implements Comparable, Serializable { private final EnumOptionData chargeAppliesTo; private final EnumOptionData chargeCalculationType; private final EnumOptionData chargePaymentMode; + private final MonthDay feeOnMonthDay; + private final Integer feeInterval; private final Collection currencyOptions; private final List chargeCalculationTypeOptions;// @@ -52,7 +54,7 @@ public class ChargeData implements Comparable, Serializable { return new ChargeData(null, null, null, null, null, null, null, null, false, false, currencyOptions, chargeCalculationTypeOptions, chargeAppliesToOptions, chargeTimeTypeOptions, chargePaymentModeOptions, loansChargeCalculationTypeOptions, - loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions); + loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, null, null); } public static ChargeData withTemplate(final ChargeData charge, final ChargeData template) { @@ -60,12 +62,12 @@ public class ChargeData implements Comparable, Serializable { charge.chargeCalculationType, charge.chargePaymentMode, charge.penalty, charge.active, template.currencyOptions, template.chargeCalculationTypeOptions, template.chargeAppliesToOptions, template.chargeTimeTypeOptions, template.chargePaymetModeOptions, template.loanChargeCalculationTypeOptions, template.loanChargeTimeTypeOptions, - template.savingsChargeCalculationTypeOptions, template.savingsChargeTimeTypeOptions); + template.savingsChargeCalculationTypeOptions, template.savingsChargeTimeTypeOptions, charge.feeOnMonthDay, charge.feeInterval); } public static ChargeData instance(final Long id, final String name, final BigDecimal amount, final CurrencyData currency, final EnumOptionData chargeTimeType, final EnumOptionData chargeAppliesTo, final EnumOptionData chargeCalculationType, - final EnumOptionData chargePaymentMode, final boolean penalty, final boolean active) { + final EnumOptionData chargePaymentMode, final MonthDay feeOnMonthDay, final Integer feeInterval, final boolean penalty, final boolean active) { final Collection currencyOptions = null; final List chargeCalculationTypeOptions = null; @@ -80,7 +82,7 @@ public class ChargeData implements Comparable, Serializable { return new ChargeData(id, name, amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, chargePaymentMode, penalty, active, currencyOptions, chargeCalculationTypeOptions, chargeAppliesToOptions, chargeTimeTypeOptions, chargePaymentModeOptions, loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, - savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions); + savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, feeOnMonthDay, feeInterval); } public static ChargeData lookup(final Long id, final String name, final boolean isPenalty) { @@ -90,6 +92,8 @@ public class ChargeData implements Comparable, Serializable { final EnumOptionData chargeAppliesTo = null; final EnumOptionData chargeCalculationType = null; final EnumOptionData chargePaymentMode = null; + final MonthDay feeOnMonthDay = null; + final Integer feeInterval = null; final Boolean penalty = isPenalty; final Boolean active = false; final Collection currencyOptions = null; @@ -105,7 +109,7 @@ public class ChargeData implements Comparable, Serializable { return new ChargeData(id, name, amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, chargePaymentMode, penalty, active, currencyOptions, chargeCalculationTypeOptions, chargeAppliesToOptions, chargeTimeTypeOptions, chargePaymentModeOptions, loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, - savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions); + savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions, feeOnMonthDay, feeInterval); } private ChargeData(final Long id, final String name, final BigDecimal amount, final CurrencyData currency, @@ -115,7 +119,7 @@ public class ChargeData implements Comparable, Serializable { final List chargeAppliesToOptions, final List chargeTimeTypeOptions, final List chargePaymentModeOptions, final List loansChargeCalculationTypeOptions, final List loansChargeTimeTypeOptions, final List savingsChargeCalculationTypeOptions, - final List savingsChargeTimeTypeOptions) { + final List savingsChargeTimeTypeOptions, final MonthDay feeOnMonthDay, final Integer feeInterval) { this.id = id; this.name = name; this.amount = amount; @@ -124,6 +128,8 @@ public class ChargeData implements Comparable, Serializable { this.chargeAppliesTo = chargeAppliesTo; this.chargeCalculationType = chargeCalculationType; this.chargePaymentMode = chargePaymentMode; + this.feeInterval = feeInterval; + this.feeOnMonthDay = feeOnMonthDay; this.penalty = penalty; this.active = active; this.currencyOptions = currencyOptions; @@ -178,10 +184,9 @@ public class ChargeData implements Comparable, Serializable { final BigDecimal amountPercentageAppliedTo = BigDecimal.ZERO; final Collection chargeOptions = null; final LocalDate dueAsOfDate = null; - final MonthDay feeOnMonthDay = null; return SavingsAccountChargeData.instance(savingsChargeId, this.id, savingsAccountId, this.name, this.currency, this.amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, this.chargeTimeType, dueAsOfDate, this.chargeCalculationType, percentage, - amountPercentageAppliedTo, chargeOptions, this.penalty, feeOnMonthDay); + amountPercentageAppliedTo, chargeOptions, this.penalty, this.feeOnMonthDay, this.feeInterval); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/Charge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/Charge.java index 3ce2887fe..d702c6f2e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/Charge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/Charge.java @@ -18,6 +18,7 @@ import javax.persistence.UniqueConstraint; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; +import org.joda.time.MonthDay; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; @@ -56,6 +57,15 @@ public class Charge extends AbstractPersistable { @Column(name = "charge_payment_mode_enum") private Integer chargePaymentMode; + @Column(name = "fee_on_day", nullable = true) + private Integer feeOnDay; + + @Column(name = "fee_interval", nullable = true) + private Integer feeInterval; + + @Column(name = "fee_on_month", nullable = true) + private Integer feeOnMonth; + @Column(name = "is_penalty", nullable = false) private boolean penalty; @@ -79,8 +89,11 @@ public class Charge extends AbstractPersistable { final boolean penalty = command.booleanPrimitiveValueOfParameterNamed("penalty"); final boolean active = command.booleanPrimitiveValueOfParameterNamed("active"); + final MonthDay feeOnMonthDay = command.extractMonthDayNamed("feeOnMonthDay"); + final Integer feeInterval = command.integerValueOfParameterNamed("feeInterval"); - return new Charge(name, amount, currencyCode, chargeAppliesTo, chargeTimeType, chargeCalculationType, penalty, active, paymentMode); + return new Charge(name, amount, currencyCode, chargeAppliesTo, chargeTimeType, chargeCalculationType, penalty, active, paymentMode, + feeOnMonthDay, feeInterval); } protected Charge() { @@ -89,7 +102,7 @@ public class Charge extends AbstractPersistable { private Charge(final String name, final BigDecimal amount, final String currencyCode, final ChargeAppliesTo chargeAppliesTo, final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculationType, final boolean penalty, - final boolean active, final ChargePaymentMode paymentMode) { + final boolean active, final ChargePaymentMode paymentMode, final MonthDay feeOnMonthDay, final Integer feeInterval) { this.name = name; this.amount = amount; this.currencyCode = currencyCode; @@ -103,6 +116,12 @@ public class Charge extends AbstractPersistable { final List dataValidationErrors = new ArrayList(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("charges"); + if (isMonthlyFee()) { + this.feeOnMonth = feeOnMonthDay.getMonthOfYear(); + this.feeOnDay = feeOnMonthDay.getDayOfMonth(); + this.feeInterval = feeInterval; + } + if (isSavingsCharge()) { if (!isAllowedSavingsChargeTime()) { @@ -129,7 +148,7 @@ public class Charge extends AbstractPersistable { .failWithCodeNoParameterAddedToErrorCode("not.allowed.charge.time.for.loan"); } } - + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } @@ -246,13 +265,20 @@ public class Charge extends AbstractPersistable { baseDataValidator.reset().parameter("chargeTimeType").value(this.chargeTime) .failWithCodeNoParameterAddedToErrorCode("not.allowed.charge.time.for.savings"); } + //if charge time is changed to monthly then validate for feeOnMonthDay and feeInterval + if(isMonthlyFee()){ + final MonthDay monthDay = command.extractMonthDayNamed("feeOnMonthDay"); + baseDataValidator.reset().parameter("feeOnMonthDay").value(monthDay).notNull(); + + final Integer feeInterval = command.integerValueOfParameterNamed("feeInterval"); + baseDataValidator.reset().parameter("feeInterval").value(feeInterval).notNull().inMinMaxRange(1, 12); + } } else if (isLoanCharge()) { if (!isAllowedLoanChargeTime()) { baseDataValidator.reset().parameter("chargeTimeType").value(this.chargeTime) .failWithCodeNoParameterAddedToErrorCode("not.allowed.charge.time.for.loan"); } } - } final String chargeAppliesToParamName = "chargeAppliesTo"; @@ -297,7 +323,33 @@ public class Charge extends AbstractPersistable { actualChanges.put("locale", localeAsInput); this.chargePaymentMode = ChargePaymentMode.fromInt(newValue).getValue(); } + + if (command.hasParameter("feeOnMonthDay")) { + final MonthDay monthDay = command.extractMonthDayNamed("feeOnMonthDay"); + final String actualValueEntered = command.stringValueOfParameterNamed("feeOnMonthDay"); + final Integer dayOfMonthValue = monthDay.getDayOfMonth(); + if (this.feeOnDay != dayOfMonthValue) { + actualChanges.put("feeOnMonthDay", actualValueEntered); + actualChanges.put("locale", localeAsInput); + this.feeOnDay = dayOfMonthValue; + } + final Integer monthOfYear = monthDay.getMonthOfYear(); + if (this.feeOnMonth != monthOfYear) { + actualChanges.put("feeOnMonthDay", actualValueEntered); + actualChanges.put("locale", localeAsInput); + this.feeOnMonth = monthOfYear; + } + } + + final String feeInterval = "feeInterval"; + if (command.isChangeInIntegerParameterNamed(feeInterval, this.feeInterval)) { + final Integer newValue = command.integerValueOfParameterNamed(feeInterval); + actualChanges.put(feeInterval, newValue); + actualChanges.put("locale", localeAsInput); + this.feeInterval = newValue; + } + final String penaltyParamName = "penalty"; if (command.isChangeInBooleanParameterNamed(penaltyParamName, this.penalty)) { final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(penaltyParamName); @@ -339,10 +391,26 @@ public class Charge extends AbstractPersistable { final EnumOptionData chargePaymentmode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode); final CurrencyData currency = new CurrencyData(this.currencyCode, null, 0, 0, null, null); return ChargeData.instance(getId(), this.name, this.amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType, - chargePaymentmode, this.penalty, this.active); + chargePaymentmode, this.getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active); } public Integer getChargePaymentMode() { return this.chargePaymentMode; } + + public boolean isMonthlyFee() { + return ChargeTimeType.fromInt(this.chargeTime).isMonthlyFee(); + } + + public MonthDay getFeeOnMonthDay(){ + MonthDay feeOnMonthDay = null; + if (feeOnDay != null && feeOnMonth != null) { + feeOnMonthDay = new MonthDay(feeOnMonth, feeOnDay); + } + return feeOnMonthDay; + } + + public Integer feeInterval(){ + return this.feeInterval; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/ChargeTimeType.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/ChargeTimeType.java index 15f64fbd0..9f86d2f12 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/ChargeTimeType.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/domain/ChargeTimeType.java @@ -13,7 +13,8 @@ public enum ChargeTimeType { SAVINGS_ACTIVATION(3, "chargeTimeType.savingsActivation"), // only for savings SAVINGS_CLOSURE(4, "chargeTimeType.savingsClosure"), // only for savings WITHDRAWAL_FEE(5, "chargeTimeType.withdrawalFee"), // only for savings - ANNUAL_FEE(6, "chargeTimeType.annualFee"); // only for savings + ANNUAL_FEE(6, "chargeTimeType.annualFee"), // only for savings + MONTHLY_FEE(7, "chargeTimeType.monthlyFee"); // only for savings private final Integer value; private final String code; @@ -64,6 +65,9 @@ public enum ChargeTimeType { case 6: chargeTimeType = ANNUAL_FEE; break; + case 7: + chargeTimeType = MONTHLY_FEE; + break; default: chargeTimeType = INVALID; break; @@ -95,11 +99,16 @@ public enum ChargeTimeType { return this.value.equals(ChargeTimeType.ANNUAL_FEE.getValue()); } + public boolean isMonthlyFee() { + return this.value.equals(ChargeTimeType.MONTHLY_FEE.getValue()); + } + public boolean isAllowedLoanChargeTime() { return isTimeOfDisbursement() || isOnSpecifiedDueDate(); } public boolean isAllowedSavingsChargeTime() { - return isOnSpecifiedDueDate() || isSavingsActivation() || isSavingsClosure() || isWithdrawalFee() || isAnnualFee(); + return isOnSpecifiedDueDate() || isSavingsActivation() || isSavingsClosure() || isWithdrawalFee() || isAnnualFee() + || isMonthlyFee(); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java index fb2ce138b..087dfc450 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/serialization/ChargeDefinitionCommandFromApiJsonDeserializer.java @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; +import org.joda.time.MonthDay; import org.mifosplatform.infrastructure.core.data.ApiParameterError; import org.mifosplatform.infrastructure.core.data.DataValidatorBuilder; import org.mifosplatform.infrastructure.core.exception.InvalidJsonException; @@ -37,7 +38,7 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { */ private final Set supportedParameters = new HashSet(Arrays.asList("name", "amount", "locale", "currencyCode", "currencyOptions", "chargeAppliesTo", "chargeTimeType", "chargeCalculationType", "chargeCalculationTypeOptions", "penalty", - "active", "chargePaymentMode")); + "active", "chargePaymentMode", "feeOnMonthDay", "feeInterval", "monthDayFormat")); private final FromJsonHelper fromApiJsonHelper; @@ -90,6 +91,15 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { final Boolean active = this.fromApiJsonHelper.extractBooleanNamed("active", element); baseDataValidator.reset().parameter("active").value(active).notNull(); } + + boolean isMonthlyFee = ChargeTimeType.fromInt(chargeTimeType).isMonthlyFee(); + if(isMonthlyFee){ + final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed("feeOnMonthDay", element); + baseDataValidator.reset().parameter("feeOnMonthDay").value(monthDay).notNull(); + + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed("feeInterval", element, Locale.getDefault()); + baseDataValidator.reset().parameter("feeInterval").value(feeInterval).notNull().inMinMaxRange(1, 12); + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -130,6 +140,16 @@ public final class ChargeDefinitionCommandFromApiJsonDeserializer { baseDataValidator.reset().parameter("chargeTimeType").value(chargeTimeType).notNull() .inMinMaxRange(ChargeTimeType.minValue(), ChargeTimeType.maxValue()); } + + if (this.fromApiJsonHelper.parameterExists("feeOnMonthDay", element)) { + final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed("feeOnMonthDay", element); + baseDataValidator.reset().parameter("feeOnMonthDay").value(monthDay).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists("feeInterval", element)) { + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed("feeInterval", element, Locale.getDefault()); + baseDataValidator.reset().parameter("feeInterval").value(feeInterval).notNull().inMinMaxRange(1, 12); + } if (this.fromApiJsonHelper.parameterExists("chargeCalculationType", element)) { final Integer chargeCalculationType = this.fromApiJsonHelper.extractIntegerNamed("chargeCalculationType", element, diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java index 23bdbd925..bbad5faa1 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeDropdownReadPlatformServiceImpl.java @@ -83,6 +83,6 @@ public class ChargeDropdownReadPlatformServiceImpl implements ChargeDropdownRead return Arrays.asList(chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE), chargeTimeType(ChargeTimeType.SAVINGS_ACTIVATION), //chargeTimeType(ChargeTimeType.SAVINGS_CLOSURE), chargeTimeType(ChargeTimeType.WITHDRAWAL_FEE), - chargeTimeType(ChargeTimeType.ANNUAL_FEE)); + chargeTimeType(ChargeTimeType.ANNUAL_FEE), chargeTimeType(ChargeTimeType.MONTHLY_FEE)); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java index 61f71acd4..113e709bd 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeEnumerations.java @@ -40,6 +40,9 @@ public class ChargeEnumerations { case ANNUAL_FEE: optionData = new EnumOptionData(ChargeTimeType.ANNUAL_FEE.getValue().longValue(), ChargeTimeType.ANNUAL_FEE.getCode(), "Annual Fee"); break; + case MONTHLY_FEE: + optionData = new EnumOptionData(ChargeTimeType.MONTHLY_FEE.getValue().longValue(), ChargeTimeType.MONTHLY_FEE.getCode(), "Monthly Fee"); + break; default: optionData = new EnumOptionData(ChargeTimeType.INVALID.getValue().longValue(), ChargeTimeType.INVALID.getCode(), "Invalid"); break; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java index 15926d63d..f5b2ee581 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/service/ChargeReadPlatformServiceImpl.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.util.Collection; import java.util.List; +import org.joda.time.MonthDay; import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.infrastructure.core.domain.JdbcSupport; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; @@ -140,9 +141,11 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService return "c.id as id, c.name as name, c.amount as amount, c.currency_code as currencyCode, " + "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, 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 from m_charge c " + + "c.charge_calculation_enum as chargeCalculation, c.is_penalty as penalty, " + + "c.is_active as active, 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, c.fee_on_day as feeOnDay, c.fee_on_month as feeOnMonth, " + + "c.fee_interval as feeInterval from m_charge c " + "join m_organisation_currency oc on c.currency_code = oc.code"; } @@ -185,8 +188,16 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService final boolean penalty = rs.getBoolean("penalty"); final boolean active = rs.getBoolean("active"); + final Integer feeInterval = JdbcSupport.getInteger(rs, "feeInterval"); + MonthDay feeOnMonthDay = null; + final Integer feeOnMonth = JdbcSupport.getInteger(rs, "feeOnMonth"); + final Integer feeOnDay = JdbcSupport.getInteger(rs, "feeOnDay"); + if (feeOnDay != null) { + feeOnMonthDay = new MonthDay(feeOnMonth, feeOnDay); + } + return ChargeData.instance(id, name, amount, currency, chargeTimeType, chargeAppliesToType, chargeCalculationType, - chargePaymentMode, penalty, active); + chargePaymentMode, feeOnMonthDay, feeInterval, penalty, active); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java index 44b7bb681..89ba2d3fa 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/SavingsApiConstants.java @@ -83,6 +83,7 @@ public class SavingsApiConstants { public static final String withdrawalFeeForTransfersParamName = "withdrawalFeeForTransfers"; public static final String feeAmountParamName = "feeAmount";//to be deleted public static final String feeOnMonthDayParamName = "feeOnMonthDay"; + public static final String feeIntervalParamName = "feeInterval"; public static final String accountingRuleParamName = "accountingRule"; public static final String paymentTypeIdParamName = "paymentTypeId"; public static final String transactionAccountNumberParamName = "accountNumber"; @@ -203,7 +204,7 @@ public class SavingsApiConstants { amountPaidParamName, chargeOptionsParamName)); public static final Set SAVINGS_ACCOUNT_CHARGES_ADD_REQUEST_DATA_PARAMETERS = new HashSet(Arrays.asList( - chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, feeOnMonthDayParamName, monthDayFormatParamName)); + chargeIdParamName, amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName, feeOnMonthDayParamName, monthDayFormatParamName, feeIntervalParamName)); public static final Set SAVINGS_ACCOUNT_CHARGES_PAY_CHARGE_REQUEST_DATA_PARAMETERS = new HashSet(Arrays.asList( amountParamName, dueAsOfDateParamName, dateFormatParamName, localeParamName)); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeData.java index 6622f9241..8dc6e2c67 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeData.java @@ -40,6 +40,9 @@ public class SavingsAccountChargeData { @SuppressWarnings("unused") private final MonthDay feeOnMonthDay; + @SuppressWarnings("unused") + private final Integer feeInterval; + private final EnumOptionData chargeCalculationType; private final BigDecimal percentage; @@ -91,29 +94,29 @@ public class SavingsAccountChargeData { final boolean penalty = false; final LocalDate dueAsOfDate = null; final MonthDay feeOnMonthDay = null; - + final Integer feeInterval = null; return new SavingsAccountChargeData(id, chargeId, accountId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage, amountPercentageAppliedTo, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeOptions, - penalty, feeOnMonthDay); + penalty, feeOnMonthDay, feeInterval); } public static SavingsAccountChargeData instance(final Long id, final Long chargeId, final Long accountId, final String name, final CurrencyData currency, final BigDecimal amount, final BigDecimal amountPaid, final BigDecimal amountWaived, final BigDecimal amountWrittenOff, final BigDecimal amountOutstanding, final EnumOptionData chargeTimeType, final LocalDate dueAsOfDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, - final Collection chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay) { + final Collection chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay, final Integer feeInterval) { return new SavingsAccountChargeData(id, chargeId, accountId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage, amountPercentageAppliedTo, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeOptions, - penalty, feeOnMonthDay); + penalty, feeOnMonthDay, feeInterval); } private SavingsAccountChargeData(final Long id, final Long chargeId, final Long accountId, final String name, final EnumOptionData chargeTimeType, final LocalDate dueAsOfDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, final CurrencyData currency, final BigDecimal amount, final BigDecimal amountPaid, final BigDecimal amountWaived, final BigDecimal amountWrittenOff, final BigDecimal amountOutstanding, - final Collection chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay) { + final Collection chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay, final Integer feeInterval) { this.id = id; this.chargeId = chargeId; this.accountId = accountId; @@ -133,6 +136,7 @@ public class SavingsAccountChargeData { this.chargeOptions = chargeOptions; this.penalty = penalty; this.feeOnMonthDay = feeOnMonthDay; + this.feeInterval = feeInterval; } private BigDecimal getAmountOrPercentage() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeDataValidator.java index 9cfc4da44..4931e08ad 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountChargeDataValidator.java @@ -12,11 +12,13 @@ import static org.mifosplatform.portfolio.savings.SavingsApiConstants.amountPara import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargeIdParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeOnMonthDayParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeIntervalParamName; import java.lang.reflect.Type; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import org.apache.commons.lang.StringUtils; @@ -92,6 +94,18 @@ public class SavingsAccountChargeDataValidator { final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(dueAsOfDateParamName, element); baseDataValidator.reset().parameter(dueAsOfDateParamName).value(transactionDate).notNull(); } + + if (this.fromApiJsonHelper.parameterExists(feeOnMonthDayParamName, element)) { + final String monthDayFormat = this.fromApiJsonHelper.extractMonthDayFormatParameter(element.getAsJsonObject()); + final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(element.getAsJsonObject()); + final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed(feeOnMonthDayParamName, element.getAsJsonObject(), monthDayFormat, locale); + baseDataValidator.reset().parameter(feeOnMonthDayParamName).value(monthDay).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(feeIntervalParamName, element)) { + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed(feeIntervalParamName, element, Locale.getDefault()); + baseDataValidator.reset().parameter(feeIntervalParamName).value(feeInterval).notNull().inMinMaxRange(1, 12); + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java index 39bb81e7c..561be8757 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/data/SavingsAccountDataValidator.java @@ -11,7 +11,7 @@ import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargeIdPa import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.clientIdParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.externalIdParamName; -import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeAmountParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeIntervalParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeOnMonthDayParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.fieldOfficerIdParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.groupIdParamName; @@ -196,6 +196,7 @@ public class SavingsAccountDataValidator { if (element.isJsonObject()) { final JsonObject topLevelJsonElement = element.getAsJsonObject(); final Locale locale = this.fromApiJsonHelper.extractLocaleParameter(topLevelJsonElement); + final String monthDayFormat = this.fromApiJsonHelper.extractMonthDayFormatParameter(topLevelJsonElement); if (topLevelJsonElement.has(chargesParamName) && topLevelJsonElement.get(chargesParamName).isJsonArray()) { final JsonArray array = topLevelJsonElement.get(chargesParamName).getAsJsonArray(); for (int i = 0; i < array.size(); i++) { @@ -209,6 +210,16 @@ public class SavingsAccountDataValidator { final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalNamed(amountParamName, savingsChargeElement, locale); baseDataValidator.reset().parameter(amountParamName).value(amount).notNull().positiveAmount(); + + if (this.fromApiJsonHelper.parameterExists(feeOnMonthDayParamName, savingsChargeElement)) { + final MonthDay monthDay = this.fromApiJsonHelper.extractMonthDayNamed(feeOnMonthDayParamName, savingsChargeElement, monthDayFormat, locale); + baseDataValidator.reset().parameter(feeOnMonthDayParamName).value(monthDay).notNull(); + } + + if (this.fromApiJsonHelper.parameterExists(feeIntervalParamName, savingsChargeElement)) { + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed(feeIntervalParamName, savingsChargeElement, Locale.getDefault()); + baseDataValidator.reset().parameter(feeIntervalParamName).value(feeInterval).notNull().inMinMaxRange(1, 12); + } } } } @@ -348,18 +359,7 @@ public class SavingsAccountDataValidator { .ignoreIfNull().validateForBooleanValue(); } - if (this.fromApiJsonHelper.parameterExists(feeAmountParamName, element)) { - final BigDecimal annualFeeAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(feeAmountParamName, element); - baseDataValidator.reset().parameter(feeAmountParamName).value(annualFeeAmount).ignoreIfNull().zeroOrPositiveAmount(); - } - - if (this.fromApiJsonHelper.parameterExists(feeOnMonthDayParamName, element)) { - final MonthDay monthDayOfAnnualFee = this.fromApiJsonHelper.extractMonthDayNamed(feeOnMonthDayParamName, element); - baseDataValidator.reset().parameter(feeOnMonthDayParamName).value(monthDayOfAnnualFee).ignoreIfNull(); - } - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java index b72eb7800..e7e593173 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccount.java @@ -1207,12 +1207,8 @@ public class SavingsAccount extends AbstractPersistable { final SavingsAccountCharge chargeToUndo = savingsAccountChargePaidBy.getSavingsAccountCharge(); if (transactionToUndo.isChargeTransaction()) { chargeToUndo.undoPayment(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); - if(chargeToUndo.isAnnualFee()){ - chargeToUndo.updateToPreviousAnnualFeeDueDate(); - } } else if (transactionToUndo.isWaiveCharge()) { chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency())); - chargeToUndo.updateToPreviousAnnualFeeDueDate(); } } } @@ -1459,7 +1455,7 @@ public class SavingsAccount extends AbstractPersistable { //update annual fee due date for (SavingsAccountCharge charge : this.charges()) { - charge.updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); + charge.updateToNextDueDateFrom(getActivationLocalDate()); } //auto pay the activation time charges @@ -1677,11 +1673,8 @@ public class SavingsAccount extends AbstractPersistable { // waive charge final Money amountWaived = savingsAccountCharge.waive(getCurrency()); - handleWaiverChargeTransactions(savingsAccountCharge, amountWaived); - - //update next annual fee due date - savingsAccountCharge.updateToNextAnnualFeeDueDate(); + } public void addCharge(final DateTimeFormatter formatter, final SavingsAccountCharge savingsAccountCharge, final Charge chargeDefinition) { @@ -1738,13 +1731,15 @@ public class SavingsAccount extends AbstractPersistable { throw new PlatformApiDataValidationException(dataValidationErrors); } + } + + if(savingsAccountCharge.isAnnualFee() || savingsAccountCharge.isMonthlyFee()){ + //update due date if(isActive()){ - savingsAccountCharge.updateToNextAnnualFeeDueDateFrom(getActivationLocalDate()); + savingsAccountCharge.updateToNextDueDateFrom(getActivationLocalDate()); }else if(isApproved()){ - savingsAccountCharge.updateToNextAnnualFeeDueDateFrom(getApprovedOnLocalDate()); - } - //if account is in submitted and pending approval state, - //upon account activation annual fee charges due date get updated. + savingsAccountCharge.updateToNextDueDateFrom(getApprovedOnLocalDate()); + } } //activation charge and withdrawal charges not required this validation @@ -1843,9 +1838,6 @@ public class SavingsAccount extends AbstractPersistable { } this.payCharge(savingsAccountCharge, chargePaid, transactionDate); - - //update next annual fee due date - savingsAccountCharge.updateToNextAnnualFeeDueDate(); } public void payCharge(final SavingsAccountCharge savingsAccountCharge, final Money amountPaid, final LocalDate transactionDate) { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountCharge.java index ca25f95d0..2de297d44 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountCharge.java @@ -8,8 +8,9 @@ package org.mifosplatform.portfolio.savings.domain; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.amountParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.dateFormatParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; -import static org.mifosplatform.portfolio.savings.SavingsApiConstants.localeParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeIntervalParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeOnMonthDayParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.localeParamName; import java.math.BigDecimal; import java.math.MathContext; @@ -64,6 +65,9 @@ public class SavingsAccountCharge extends AbstractPersistable { @Column(name = "fee_on_day", nullable = true) private Integer feeOnDay; + @Column(name = "fee_interval", nullable = true) + private Integer feeInterval; + @Column(name = "charge_calculation_enum") private Integer chargeCalculation; @@ -107,20 +111,20 @@ public class SavingsAccountCharge extends AbstractPersistable { final BigDecimal amount = command.bigDecimalValueOfParameterNamed(amountParamName); final LocalDate dueDate = command.localDateValueOfParameterNamed(dueAsOfDateParamName); final MonthDay feeOnMonthDay = command.extractMonthDayNamed(feeOnMonthDayParamName); + final Integer feeInterval = command.integerValueOfParameterNamed(feeIntervalParamName); final ChargeTimeType chargeTime = null; final ChargeCalculationType chargeCalculation = null; final boolean status = true; return new SavingsAccountCharge(savingsAccount, chargeDefinition, amount, chargeTime, chargeCalculation, dueDate, status, - feeOnMonthDay); + feeOnMonthDay, feeInterval); } - // TODO AA: refactor method signature public static SavingsAccountCharge createNewWithoutSavingsAccount(final Charge chargeDefinition, final BigDecimal amountPayable, final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, final boolean status, - final MonthDay feeOnMonthDay) { + final MonthDay feeOnMonthDay, final Integer feeInterval) { return new SavingsAccountCharge(null, chargeDefinition, amountPayable, chargeTime, chargeCalculation, dueDate, status, - feeOnMonthDay); + feeOnMonthDay, feeInterval); } protected SavingsAccountCharge() { @@ -129,7 +133,7 @@ public class SavingsAccountCharge extends AbstractPersistable { private SavingsAccountCharge(final SavingsAccount savingsAccount, final Charge chargeDefinition, final BigDecimal amount, final ChargeTimeType chargeTime, final ChargeCalculationType chargeCalculation, final LocalDate dueDate, final boolean status, - final MonthDay feeOnMonthDay) { + MonthDay feeOnMonthDay, final Integer feeInterval) { this.savingsAccount = savingsAccount; this.charge = chargeDefinition; @@ -145,14 +149,25 @@ public class SavingsAccountCharge extends AbstractPersistable { } - if (isAnnualFee()) { + if (isAnnualFee() || isMonthlyFee()) { + feeOnMonthDay = (feeOnMonthDay == null) ? chargeDefinition.getFeeOnMonthDay() : feeOnMonthDay; if (feeOnMonthDay == null) { final String defaultUserMessage = "Savings Account charge is missing due date."; throw new SavingsAccountChargeWithoutMandatoryFieldException("savingsaccount.charge", dueAsOfDateParamName, defaultUserMessage, chargeDefinition.getId(), chargeDefinition.getName()); } + this.feeOnMonth = feeOnMonthDay.getMonthOfYear(); this.feeOnDay = feeOnMonthDay.getDayOfMonth(); + + if (isMonthlyFee()) { + this.feeInterval = (feeInterval == null) ? chargeDefinition.feeInterval() : feeInterval; + } + + } else { + this.feeOnDay = null; + this.feeOnMonth = null; + this.feeInterval = null; } this.dueDate = (dueDate == null) ? null : dueDate.toDate(); @@ -170,14 +185,23 @@ public class SavingsAccountCharge extends AbstractPersistable { final BigDecimal transactionAmount = new BigDecimal(0); populateDerivedFields(transactionAmount, chargeAmount); - resetOutstandingAmount();// reset only if it's withdrawal or annual fee + + if (this.isWithdrawalFee()) { + this.amountOutstanding = BigDecimal.ZERO; + } + this.paid = determineIfFullyPaid(); this.status = status; } - private void resetOutstandingAmount() { - if (this.isWithdrawalFee()) { - this.amountOutstanding = BigDecimal.ZERO; + public void recalculateAmountOutstanding() { + if (isMonthlyFee() || isAnnualFee()) { + // FIXME: AA: If charge is percentage of x amount then need to + // update amount outstanding accordingly. + // Right now annual and monthly charges supports charge calculation type flat. + this.amountOutstanding = this.amount; + this.paid = false;//reset to false for recurring fee. + this.waived = false; } } @@ -252,9 +276,13 @@ public class SavingsAccountCharge extends AbstractPersistable { amountPaid = amountPaid.minus(transactionAmount); this.amountPaid = amountPaid.getAmount(); this.amountOutstanding = calculateAmountOutstanding(currency); + if (this.isWithdrawalFee()) { this.amountOutstanding = BigDecimal.ZERO; } + //to reset amount outstanding for annual and monthly fee + recalculateAmountOutstanding(); + updateToPreviousDueDate();//reset annual and monthly due date. this.paid = false; } @@ -262,6 +290,10 @@ public class SavingsAccountCharge extends AbstractPersistable { this.amountWaived = this.amountOutstanding; this.amountOutstanding = BigDecimal.ZERO; this.waived = true; + + recalculateAmountOutstanding(); + updateToNextDueDate(); + return getAmountWaived(currency); } @@ -271,10 +303,12 @@ public class SavingsAccountCharge extends AbstractPersistable { this.amountWaived = amountWaived.getAmount(); this.amountOutstanding = calculateAmountOutstanding(currency); this.waived = false; + + recalculateAmountOutstanding(); + updateToPreviousDueDate(); } public Money pay(final MonetaryCurrency currency, final Money amountPaid) { - Money amountPaidToDate = Money.of(currency, this.amountPaid); Money amountOutstanding = Money.of(currency, this.amountOutstanding); amountPaidToDate = amountPaidToDate.plus(amountPaid); @@ -282,7 +316,14 @@ public class SavingsAccountCharge extends AbstractPersistable { this.amountPaid = amountPaidToDate.getAmount(); this.amountOutstanding = amountOutstanding.getAmount(); this.paid = determineIfFullyPaid(); - return amountOutstanding; + + if (BigDecimal.ZERO.compareTo(this.amountOutstanding) == 0) { + //full outstanding is paid, update to next due date + updateToNextDueDate(); + recalculateAmountOutstanding(); + } + + return Money.of(currency, this.amountOutstanding); } private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) { @@ -293,7 +334,7 @@ public class SavingsAccountCharge extends AbstractPersistable { this.savingsAccount = savingsAccount; } - public void update(final BigDecimal amount, final LocalDate dueDate, final MonthDay feeOnMonthDay) { + public void update(final BigDecimal amount, final LocalDate dueDate, final MonthDay feeOnMonthDay, final Integer feeInterval) { final BigDecimal transactionAmount = BigDecimal.ZERO; if (dueDate != null) { this.dueDate = dueDate.toDate(); @@ -304,6 +345,10 @@ public class SavingsAccountCharge extends AbstractPersistable { this.feeOnDay = feeOnMonthDay.getDayOfMonth(); } + if (feeInterval != null) { + this.feeInterval = feeInterval; + } + if (amount != null) { switch (ChargeCalculationType.fromInt(this.chargeCalculation)) { case INVALID: @@ -574,6 +619,10 @@ public class SavingsAccountCharge extends AbstractPersistable { return ChargeTimeType.fromInt(this.chargeTime).isAnnualFee(); } + public boolean isMonthlyFee() { + return ChargeTimeType.fromInt(this.chargeTime).isMonthlyFee(); + } + public boolean hasCurrencyCodeOf(final String matchingCurrencyCode) { if (this.currencyCode() == null || matchingCurrencyCode == null) { return false; } return this.currencyCode().equalsIgnoreCase(matchingCurrencyCode); @@ -613,30 +662,60 @@ public class SavingsAccountCharge extends AbstractPersistable { return amountPaybale; } - public void updateToNextAnnualFeeDueDateFrom(final LocalDate startingDate) { - if (isAnnualFee()) { - LocalDate nextDueLocalDate = startingDate.withMonthOfYear(this.feeOnMonth).withDayOfMonth(this.feeOnDay); - if (startingDate.isAfter(nextDueLocalDate)) { - nextDueLocalDate = nextDueLocalDate.plusYears(1); + public void updateToNextDueDateFrom(final LocalDate startingDate) { + if (isAnnualFee() || isMonthlyFee()) { + LocalDate nextDueLocalDate = startingDate.withMonthOfYear(this.feeOnMonth); + while (startingDate.isAfter(nextDueLocalDate)) { + if (isAnnualFee()) { + nextDueLocalDate = nextDueLocalDate.plusYears(1); + } else if (isMonthlyFee()) { + nextDueLocalDate = nextDueLocalDate.plusMonths(this.feeInterval); + } } + //set day of month + int maxDayOfMonth = nextDueLocalDate.dayOfMonth().withMaximumValue().getDayOfMonth(); + int newDayOfMonth = (this.feeOnDay.intValue() <= maxDayOfMonth) ? this.feeOnDay.intValue() : maxDayOfMonth; + nextDueLocalDate = nextDueLocalDate.withDayOfMonth(newDayOfMonth); this.dueDate = nextDueLocalDate.toDate(); } } - - public void updateToNextAnnualFeeDueDate() { - if (isAnnualFee()) { + + public void updateToNextDueDate() { + if(isAnnualFee() || isMonthlyFee()){ LocalDate nextDueLocalDate = new LocalDate(dueDate); - this.dueDate = nextDueLocalDate.withMonthOfYear(this.feeOnMonth).withDayOfMonth(this.feeOnDay).plusYears(1).toDate(); + if(isAnnualFee()){ + nextDueLocalDate = nextDueLocalDate.withMonthOfYear(this.feeOnMonth).plusYears(1); + }else if(isMonthlyFee()){ + nextDueLocalDate = nextDueLocalDate.plusMonths(this.feeInterval); + } + + //set day of month + int maxDayOfMonth = nextDueLocalDate.dayOfMonth().withMaximumValue().getDayOfMonth(); + int newDayOfMonth = (this.feeOnDay.intValue() < maxDayOfMonth) ? this.feeOnDay.intValue() : maxDayOfMonth; + nextDueLocalDate = nextDueLocalDate.withDayOfMonth(newDayOfMonth); + + this.dueDate = nextDueLocalDate.toDate(); } } - public void updateToPreviousAnnualFeeDueDate() { - if (isAnnualFee()) { - LocalDate previousDueLocalDate = new LocalDate(dueDate); - this.dueDate = previousDueLocalDate.withMonthOfYear(this.feeOnMonth).withDayOfMonth(this.feeOnDay).minusYears(1).toDate(); + public void updateToPreviousDueDate() { + if(isAnnualFee() || isMonthlyFee()){ + LocalDate nextDueLocalDate = new LocalDate(dueDate); + if(isAnnualFee()){ + nextDueLocalDate = nextDueLocalDate.withMonthOfYear(this.feeOnMonth).minusYears(1); + }else if(isMonthlyFee()){ + nextDueLocalDate = nextDueLocalDate.minusMonths(this.feeInterval); + } + + //set day of month + int maxDayOfMonth = nextDueLocalDate.dayOfMonth().withMaximumValue().getDayOfMonth(); + int newDayOfMonth = (this.feeOnDay.intValue() < maxDayOfMonth) ? this.feeOnDay.intValue() : maxDayOfMonth; + nextDueLocalDate = nextDueLocalDate.withDayOfMonth(newDayOfMonth); + + this.dueDate = nextDueLocalDate.toDate(); } } - + public boolean feeSettingsNotSet() { return !feeSettingsSet(); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountChargeAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountChargeAssembler.java index 199520f89..86e70f75f 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountChargeAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsAccountChargeAssembler.java @@ -13,6 +13,7 @@ import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargeTime import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeOnMonthDayParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeIntervalParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.idParamName; import java.math.BigDecimal; @@ -82,6 +83,7 @@ public class SavingsAccountChargeAssembler { dateFormat, locale); final MonthDay feeOnMonthDay = this.fromApiJsonHelper.extractMonthDayNamed(feeOnMonthDayParamName, savingsChargeElement, monthDayFormat, locale); + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed(feeIntervalParamName, savingsChargeElement, locale); if (id == null) { final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeId); @@ -92,18 +94,19 @@ public class SavingsAccountChargeAssembler { throw new ChargeCannotBeAppliedToException("savings.product", errorMessage, chargeDefinition.getId()); } - final ChargeTimeType chargeTime = null; + ChargeTimeType chargeTime = null; if (chargeTimeType != null) { - ChargeTimeType.fromInt(chargeTimeType); + chargeTime = ChargeTimeType.fromInt(chargeTimeType); } - final ChargeCalculationType chargeCalculation = null; + + ChargeCalculationType chargeCalculation = null; if (chargeCalculationType != null) { - ChargeCalculationType.fromInt(chargeCalculationType); + chargeCalculation = ChargeCalculationType.fromInt(chargeCalculationType); } final boolean status = true; final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount( - chargeDefinition, amount, chargeTime, chargeCalculation, dueDate, status, feeOnMonthDay); + chargeDefinition, amount, chargeTime, chargeCalculation, dueDate, status, feeOnMonthDay, feeInterval); savingsAccountCharges.add(savingsAccountCharge); } else { final Long savingsAccountChargeId = id; @@ -111,7 +114,7 @@ public class SavingsAccountChargeAssembler { .findOne(savingsAccountChargeId); if (savingsAccountCharge == null) { throw new SavingsAccountChargeNotFoundException(savingsAccountChargeId); } - savingsAccountCharge.update(amount, dueDate, feeOnMonthDay); + savingsAccountCharge.update(amount, dueDate, feeOnMonthDay, feeInterval); savingsAccountCharges.add(savingsAccountCharge); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProductChargeAssembler.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProductChargeAssembler.java index a29640cc5..3665c3cdf 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProductChargeAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/domain/SavingsProductChargeAssembler.java @@ -12,6 +12,7 @@ import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargeTime import static org.mifosplatform.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.dueAsOfDateParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeOnMonthDayParamName; +import static org.mifosplatform.portfolio.savings.SavingsApiConstants.feeIntervalParamName; import static org.mifosplatform.portfolio.savings.SavingsApiConstants.idParamName; import java.math.BigDecimal; @@ -73,6 +74,7 @@ public class SavingsProductChargeAssembler { final LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed(dueAsOfDateParamName, savingsChargeElement, dateFormat, locale); final MonthDay feeOnMonthDay = this.fromApiJsonHelper.extractMonthDayNamed(feeOnMonthDayParamName, savingsChargeElement); + final Integer feeInterval = this.fromApiJsonHelper.extractIntegerNamed(feeIntervalParamName, savingsChargeElement, locale); if (id == null) { final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeId); @@ -85,7 +87,7 @@ public class SavingsProductChargeAssembler { ChargeCalculationType.fromInt(chargeCalculationType); } final SavingsAccountCharge savingsAccountCharge = SavingsAccountCharge.createNewWithoutSavingsAccount( - chargeDefinition, amount, chargeTime, chargeCalculation, dueDate, true, feeOnMonthDay); + chargeDefinition, amount, chargeTime, chargeCalculation, dueDate, true, feeOnMonthDay, feeInterval); savingsAccountCharges.add(savingsAccountCharge); } else { final Long savingsAccountChargeId = id; @@ -93,7 +95,7 @@ public class SavingsProductChargeAssembler { .findOne(savingsAccountChargeId); if (savingsAccountCharge == null) { throw new SavingsAccountChargeNotFoundException(savingsAccountChargeId); } - savingsAccountCharge.update(amount, dueDate, null); + savingsAccountCharge.update(amount, dueDate, null, null); savingsAccountCharges.add(savingsAccountCharge); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java index 2516c605f..7d9663d19 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountChargeReadPlatformServiceImpl.java @@ -65,7 +65,7 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou + "sc.is_penalty as penalty, " + "sc.charge_due_date as dueAsOfDate, " + "sc.fee_on_month as feeOnMonth, " - + "sc.fee_on_day as feeOnDay, " + + "sc.fee_on_day as feeOnDay, sc.fee_interval as feeInterval, " + "sc.charge_calculation_enum as chargeCalculation, " + "c.currency_code as currencyCode, oc.name as currencyName, " + "oc.decimal_places as currencyDecimalPlaces, oc.currency_multiplesof as inMultiplesOf, oc.display_symbol as currencyDisplaySymbol, " @@ -104,6 +104,7 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(chargeTime); final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate"); + final Integer feeInterval = JdbcSupport.getInteger(rs, "feeInterval"); MonthDay feeOnMonthDay = null; final Integer feeOnMonth = JdbcSupport.getInteger(rs, "feeOnMonth"); final Integer feeOnDay = JdbcSupport.getInteger(rs, "feeOnDay"); @@ -119,7 +120,7 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou return SavingsAccountChargeData.instance(id, chargeId, accountId, name, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, - amountPercentageAppliedTo, chargeOptions, penalty, feeOnMonthDay); + amountPercentageAppliedTo, chargeOptions, penalty, feeOnMonthDay, feeInterval); } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index 097798150..d78aec3f7 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -849,7 +849,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi account.calculateInterestUsing(mc, today); } - account.validateAccountBalanceDoesNotBecomeNegative(SavingsAccountTransactionType.PAY_CHARGE.name()); + account.validateAccountBalanceDoesNotBecomeNegative("." + SavingsAccountTransactionType.PAY_CHARGE.getCode()); this.savingAccountRepository.save(account);