Merge pull request #621 from ashok-conflux/MIFOSX-697-patch

Fixed annual fee batch job and savings charges related issues
This commit is contained in:
Vishwas Babu A J
2013-10-05 08:53:23 -07:00
17 changed files with 453 additions and 84 deletions
+15
View File
@@ -11123,6 +11123,21 @@ GET https://DomainName/api/v1/charges/template
"id": 2,
"code": "chargeTimeType.specifiedDueDate",
"value": "Specified due date"
},
{
"id": 3,
"code": "chargeTimeType.savingsActivation",
"value": "Savings Activation"
},
{
"id": 5,
"code": "chargeTimeType.withdrawalFee",
"value": "Withdrawal Fee"
},
{
"id": 6,
"code": "chargeTimeType.annualFee",
"value": "Annual Fee"
}
],
"chargePaymetModeOptions":
@@ -1,25 +1,253 @@
ALTER TABLE `m_savings_account_charge`
CHANGE COLUMN `due_for_collection_as_of_date` `charge_due_date` DATE NULL DEFAULT NULL AFTER `charge_time_enum`;
ALTER TABLE `m_savings_account_charge`
ADD COLUMN `fee_on_month` SMALLINT(5) NULL DEFAULT NULL AFTER `charge_due_date`,
ADD COLUMN `fee_on_day` SMALLINT(5) NULL DEFAULT NULL AFTER `fee_on_month`,
ADD COLUMN `is_active` TINYINT(1) NOT NULL DEFAULT '1' AFTER `waived`;
ALTER TABLE `m_savings_account`
DROP COLUMN `withdrawal_fee_amount`,
DROP COLUMN `withdrawal_fee_type_enum`;
ALTER TABLE `m_savings_account_charge`
ADD COLUMN `fee_on_month` SMALLINT(5) NULL DEFAULT NULL AFTER `due_for_collection_as_of_date`,
ADD COLUMN `fee_on_day` SMALLINT(5) NULL DEFAULT NULL AFTER `fee_on_month`;
delimiter //
CREATE PROCEDURE migrate_withdrwal_fees()
begin
declare no_more_rows boolean default false;
declare v_currency_code VARCHAR(3);
declare v_withdrawal_fee_type_enum SMALLINT(5);
declare v_withdrawal_fee_amount_charge_def DECIMAL(19,6);
declare v_account_id BIGINT(20);
declare v_withdrawal_fee_amount DECIMAL(19,6);
declare t_calculation_percentage DECIMAL(19,6);
declare t_calculation_on_amount DECIMAL(19,6);
declare t_withdrawal_fee_name VARCHAR(100);
declare t_charge_id BIGINT(20);
declare t_savings_charge_id BIGINT(20);
ALTER TABLE `m_savings_account_charge`
CHANGE COLUMN `due_for_collection_as_of_date` `charge_due_date` DATE NULL DEFAULT NULL AFTER `charge_time_enum`;
-- savings transaction variables
declare v_savings_transaction_id BIGINT(20);
declare v_transaction_amount DECIMAL(19,6);
declare cursor1 cursor for
select sa.currency_code, sa.withdrawal_fee_type_enum, sa.withdrawal_fee_amount
from m_savings_account sa where sa.withdrawal_fee_amount is not null and sa.withdrawal_fee_amount > 0 group by sa.currency_code, sa.withdrawal_fee_type_enum;
declare cursor2 cursor for
select sa.id, sa.withdrawal_fee_amount from m_savings_account sa where sa.currency_code=v_currency_code and sa.withdrawal_fee_type_enum=v_withdrawal_fee_type_enum and sa.withdrawal_fee_amount is not null and sa.withdrawal_fee_amount > 0;
declare cursor3 cursor for
select sat.id, sat.amount from m_savings_account_transaction sat
where sat.savings_account_id=v_account_id and sat.transaction_type_enum=4;
declare continue handler for not found
set no_more_rows := true;
open cursor1;
LOOP1: loop
fetch cursor1 into v_currency_code, v_withdrawal_fee_type_enum, v_withdrawal_fee_amount_charge_def;
if no_more_rows then
close cursor1;
leave LOOP1;
end if;
-- set withdrawal fee name
if(v_withdrawal_fee_type_enum = 1) then
set t_withdrawal_fee_name = CONCAT('Withdrawal fee-Flat-',v_currency_code);
else
set t_withdrawal_fee_name = CONCAT('Withdrawal fee-Percentage-',v_currency_code);
end if;
-- get charge id if already exists
set t_charge_id = (select id from m_charge where name = t_withdrawal_fee_name);
if t_charge_id is null then
-- add withdrawal fee to charges
INSERT INTO `m_charge` (`name`, `currency_code`, `charge_applies_to_enum`, `charge_time_enum`, `charge_calculation_enum`, `charge_payment_mode_enum`, `amount`, `is_penalty`, `is_active`, `is_deleted`) VALUES (t_withdrawal_fee_name , v_currency_code, 2, 5, v_withdrawal_fee_type_enum, 0, v_withdrawal_fee_amount_charge_def, 0, 1, 0);
-- get inserted charge id
set t_charge_id = last_insert_id();
end if;
open cursor2;
LOOP2: loop
fetch cursor2 into v_account_id, v_withdrawal_fee_amount;
if no_more_rows then
set no_more_rows := false;
close cursor2;
leave LOOP2;
end if;
if (v_withdrawal_fee_type_enum=1) then
set t_calculation_percentage = NULL;
set t_calculation_on_amount = NULL;
else
set t_calculation_percentage = v_withdrawal_fee_amount;
set v_withdrawal_fee_amount = 0;
set t_calculation_on_amount = 0;
end if;
if not exists (select id from m_savings_account_charge sac where
sac.savings_account_id=v_account_id and sac.charge_id=t_charge_id and sac.charge_time_enum=5) then
-- attach withdrawal charge to savings
INSERT INTO `m_savings_account_charge` (`savings_account_id`, `charge_id`, `is_penalty`, `charge_time_enum`, `charge_due_date`, `fee_on_month`, `fee_on_day`, `charge_calculation_enum`, `calculation_percentage`, `calculation_on_amount`, `amount`, `amount_paid_derived`, `amount_waived_derived`, `amount_writtenoff_derived`, `amount_outstanding_derived`, `is_paid_derived`, `waived`, `is_active`) VALUES (v_account_id, t_charge_id, 0, 5, NULL, NULL, NULL, v_withdrawal_fee_type_enum, t_calculation_percentage, t_calculation_on_amount, v_withdrawal_fee_amount, NULL, NULL, NULL, 0.000000, 0, 0, 1);
-- set savings account charge id
set t_savings_charge_id = last_insert_id();
else
set t_savings_charge_id = (select id from m_savings_account_charge sac where sac.savings_account_id=v_account_id and sac.charge_id=t_charge_id and sac.charge_time_enum=5);
end if;
open cursor3;
LOOP3: loop
fetch cursor3 into v_savings_transaction_id, v_transaction_amount;
if no_more_rows then
set no_more_rows := false;
close cursor3;
leave LOOP3;
end if;
if not exists (select id from m_savings_account_charge_paid_by sacp where
sacp.savings_account_transaction_id=v_savings_transaction_id and sacp.savings_account_charge_id=t_savings_charge_id) then
-- insert a record into savings account charge paid by
INSERT INTO `m_savings_account_charge_paid_by` (`savings_account_transaction_id`, `savings_account_charge_id`, `amount`) VALUES(v_savings_transaction_id, t_savings_charge_id, v_transaction_amount);
end if;
end loop LOOP3;
end loop LOOP2;
end loop LOOP1;
end //
CREATE PROCEDURE migrate_annual_fees()
begin
declare no_more_rows boolean default false;
declare v_currency_code VARCHAR(3);
declare v_annual_fee_amount_charge_def DECIMAL(19,6);
declare v_account_id BIGINT(20);
declare v_annual_fee_amount DECIMAL(19,6);
declare v_annual_fee_on_month SMALLINT(5);
declare v_annual_fee_on_day SMALLINT(5);
declare v_annual_fee_next_due_date DATE;
declare t_annual_fee_name VARCHAR(100);
declare t_charge_id BIGINT(20);
declare t_savings_charge_id BIGINT(20);
-- savings transaction variables
declare v_savings_transaction_id BIGINT(20);
declare v_transaction_amount DECIMAL(19,6);
declare cursor1 cursor for
select sa.currency_code, sa.annual_fee_amount
from m_savings_account sa where sa.annual_fee_amount is not null and sa.annual_fee_on_month is not null and sa.annual_fee_on_day is not null group by sa.currency_code;
declare cursor2 cursor for
select sa.id, sa.annual_fee_amount, sa.annual_fee_on_month, sa.annual_fee_on_day, sa.annual_fee_next_due_date from m_savings_account sa where sa.currency_code=v_currency_code and sa.annual_fee_amount is not null and sa.annual_fee_on_month is not null and sa.annual_fee_on_day is not null;
declare cursor3 cursor for
select sat.id, sat.amount from m_savings_account_transaction sat
where sat.savings_account_id=v_account_id and sat.transaction_type_enum=5;
declare continue handler for not found
set no_more_rows := true;
open cursor1;
LOOP1: loop
fetch cursor1 into v_currency_code, v_annual_fee_amount_charge_def;
if no_more_rows then
close cursor1;
leave LOOP1;
end if;
-- set annual fee name
set t_annual_fee_name = CONCAT('Annual fee - ',v_currency_code);
-- get charge id if already exists
set t_charge_id = (select id from m_charge where name = t_annual_fee_name);
if t_charge_id is null then
-- add annual fee to charges
INSERT INTO `m_charge` (`name`, `currency_code`, `charge_applies_to_enum`, `charge_time_enum`, `charge_calculation_enum`, `charge_payment_mode_enum`, `amount`, `is_penalty`, `is_active`, `is_deleted`) VALUES (t_annual_fee_name, v_currency_code, 2, 6, 1, 0, v_annual_fee_amount_charge_def, 0, 1, 0);
-- get inserted charge id
set t_charge_id = last_insert_id();
end if;
open cursor2;
LOOP2: loop
fetch cursor2 into v_account_id, v_annual_fee_amount, v_annual_fee_on_month, v_annual_fee_on_day, v_annual_fee_next_due_date;
if no_more_rows then
set no_more_rows := false;
close cursor2;
leave LOOP2;
end if;
if not exists (select id from m_savings_account_charge sac where
sac.savings_account_id=v_account_id and sac.charge_id=t_charge_id and sac.charge_time_enum=6) then
-- attach annual charge to savings
INSERT INTO `m_savings_account_charge` (`savings_account_id`, `charge_id`, `is_penalty`, `charge_time_enum`, `charge_due_date`, `fee_on_month`, `fee_on_day`, `charge_calculation_enum`, `calculation_percentage`, `calculation_on_amount`, `amount`, `amount_paid_derived`, `amount_waived_derived`, `amount_writtenoff_derived`, `amount_outstanding_derived`, `is_paid_derived`, `waived`, `is_active`) VALUES (v_account_id, t_charge_id, 0, 6, v_annual_fee_next_due_date, v_annual_fee_on_month, v_annual_fee_on_day, 1, NULL, NULL, v_annual_fee_amount, NULL, NULL, NULL, v_annual_fee_amount, 0, 0, 1);
-- set savings account charge id
set t_savings_charge_id = last_insert_id();
else
set t_savings_charge_id = (select id from m_savings_account_charge sac where sac.savings_account_id=v_account_id and sac.charge_id=t_charge_id and sac.charge_time_enum=6);
end if;
open cursor3;
LOOP3: loop
fetch cursor3 into v_savings_transaction_id, v_transaction_amount;
if no_more_rows then
set no_more_rows := false;
close cursor3;
leave LOOP3;
end if;
if not exists (select id from m_savings_account_charge_paid_by sacp where
sacp.savings_account_transaction_id=v_savings_transaction_id and sacp.savings_account_charge_id=t_savings_charge_id) then
-- insert a record into savings account charge paid by
INSERT INTO `m_savings_account_charge_paid_by` (`savings_account_transaction_id`, `savings_account_charge_id`, `amount`) VALUES(v_savings_transaction_id, t_savings_charge_id, v_transaction_amount);
end if;
end loop LOOP3;
end loop LOOP2;
end loop LOOP1;
end //
delimiter ;
call migrate_withdrwal_fees();
call migrate_annual_fees();
drop procedure if exists migrate_annual_fees;
drop procedure if exists migrate_withdrwal_fees;
ALTER TABLE `m_savings_account`
DROP COLUMN `annual_fee_amount`,
DROP COLUMN `annual_fee_on_month`,
DROP COLUMN `annual_fee_on_day`,
DROP COLUMN `annual_fee_next_due_date`;
DROP COLUMN `annual_fee_next_due_date`,
DROP COLUMN `withdrawal_fee_amount`,
DROP COLUMN `withdrawal_fee_type_enum`;
ALTER TABLE `m_savings_product`
DROP COLUMN `annual_fee_amount`,
DROP COLUMN `annual_fee_on_month`,
DROP COLUMN `annual_fee_on_day`;
DROP COLUMN `annual_fee_on_day`;
@@ -293,8 +293,20 @@ public class JsonParserHelper {
final JsonPrimitive primitive = object.get(parameterName).getAsJsonPrimitive();
final String valueAsString = primitive.getAsString();
if (StringUtils.isNotBlank(valueAsString)) {
final DateTimeFormatter formatter = DateTimeFormat.forPattern(dateFormat).withLocale(clientApplicationLocale);
value = MonthDay.parse(valueAsString.toLowerCase(clientApplicationLocale), formatter);
try {
final DateTimeFormatter formatter = DateTimeFormat.forPattern(dateFormat).withLocale(clientApplicationLocale);
value = MonthDay.parse(valueAsString.toLowerCase(clientApplicationLocale), formatter);
} catch (final IllegalArgumentException e) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final ApiParameterError error = ApiParameterError.parameterError("validation.msg.invalid.month.day",
"The parameter " + parameterName + " is invalid based on the dateFormat: '" + dateFormat
+ "' and locale: '" + clientApplicationLocale + "' provided:", parameterName, valueAsString,
dateFormat);
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
}
@@ -169,6 +169,7 @@ public class ChargeData implements Comparable<ChargeData>, Serializable {
public SavingsAccountChargeData toSavingsAccountChargeData() {
final Long savingsChargeId = null;
final Long savingsAccountId = null;
final BigDecimal amountPaid = BigDecimal.ZERO;
final BigDecimal amountWaived = BigDecimal.ZERO;
final BigDecimal amountWrittenOff = BigDecimal.ZERO;
@@ -179,7 +180,7 @@ public class ChargeData implements Comparable<ChargeData>, Serializable {
final LocalDate dueAsOfDate = null;
final MonthDay feeOnMonthDay = null;
return SavingsAccountChargeData.instance(savingsChargeId, this.id, this.name, this.currency, this.amount, amountPaid, amountWaived,
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);
}
@@ -117,7 +117,7 @@ public class Charge extends AbstractPersistable<Long> {
if (!ChargeTimeType.fromInt(getChargeTime()).isWithdrawalFee() && ChargeCalculationType.fromInt(getChargeCalculation()).isPercentageOfAmount()) {
baseDataValidator.reset().parameter("chargeCalculationType").value(this.chargeCalculation)
.failWithCodeNoParameterAddedToErrorCode("charge.calculation.type.percentage.allowed.only.for.withdrawal");
.failWithCodeNoParameterAddedToErrorCode("savings.charge.calculation.type.percentage.allowed.only.for.withdrawal");
}
} else if (isLoanCharge()) {
@@ -49,7 +49,7 @@ public class ChargeDropdownReadPlatformServiceImpl implements ChargeDropdownRead
public List<EnumOptionData> retrieveCollectionTimeTypes() {
final List<EnumOptionData> chargeTimeTypes = new ArrayList<EnumOptionData>();
for (final ChargeTimeType chargeTimeType : ChargeTimeType.values()) {
if (ChargeTimeType.INVALID.equals(chargeTimeType)) {
if (ChargeTimeType.INVALID.equals(chargeTimeType) || ChargeTimeType.SAVINGS_CLOSURE.equals(chargeTimeType)) {
continue;
}
chargeTimeTypes.add(ChargeEnumerations.chargeTimeType(chargeTimeType));
@@ -81,7 +81,8 @@ public class ChargeDropdownReadPlatformServiceImpl implements ChargeDropdownRead
@Override
public List<EnumOptionData> retrieveSavingsCollectionTimeTypes() {
return Arrays.asList(chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE), chargeTimeType(ChargeTimeType.SAVINGS_ACTIVATION),
chargeTimeType(ChargeTimeType.SAVINGS_CLOSURE), chargeTimeType(ChargeTimeType.WITHDRAWAL_FEE),
//chargeTimeType(ChargeTimeType.SAVINGS_CLOSURE),
chargeTimeType(ChargeTimeType.WITHDRAWAL_FEE),
chargeTimeType(ChargeTimeType.ANNUAL_FEE));
}
}
@@ -164,7 +164,7 @@ public class SavingsApiConstants {
interestCalculationDaysInYearTypeParamName, minRequiredOpeningBalanceParamName, lockinPeriodFrequencyParamName,
lockinPeriodFrequencyTypeParamName,
//withdrawalFeeAmountParamName, withdrawalFeeTypeParamName,
//withdrawalFeeForTransfersParamName,
withdrawalFeeForTransfersParamName,
feeAmountParamName, feeOnMonthDayParamName, chargesParamName));
/**
@@ -10,15 +10,17 @@ import org.joda.time.LocalDate;
public class SavingsAccountAnnualFeeData {
private final Long id;
private final Long accountId;
private final String accountNo;
private final LocalDate nextAnnualFeeDueDate;
public static SavingsAccountAnnualFeeData instance(final Long id, final String accountNo, final LocalDate nextAnnualFeeDueDate) {
return new SavingsAccountAnnualFeeData(id, accountNo, nextAnnualFeeDueDate);
public static SavingsAccountAnnualFeeData instance(final Long id, final Long accountId, final String accountNo, final LocalDate nextAnnualFeeDueDate) {
return new SavingsAccountAnnualFeeData(id, accountId, accountNo, nextAnnualFeeDueDate);
}
private SavingsAccountAnnualFeeData(final Long id, final String accountNo, final LocalDate nextAnnualFeeDueDate) {
private SavingsAccountAnnualFeeData(final Long id, final Long accountId, final String accountNo, final LocalDate nextAnnualFeeDueDate) {
this.id = id;
this.accountId = accountId;
this.accountNo = accountNo;
this.nextAnnualFeeDueDate = nextAnnualFeeDueDate;
}
@@ -27,11 +29,15 @@ public class SavingsAccountAnnualFeeData {
return this.id;
}
public String getAccountNo() {
return this.accountNo;
public Long getAccountId() {
return this.accountId;
}
public LocalDate getNextAnnualFeeDueDate() {
return this.nextAnnualFeeDueDate;
}
public String getAccountNo() {
return this.accountNo;
}
}
@@ -25,6 +25,9 @@ public class SavingsAccountChargeData {
@SuppressWarnings("unused")
private final Long chargeId;
@SuppressWarnings("unused")
private final Long accountId;
@SuppressWarnings("unused")
private final String name;
@@ -73,6 +76,7 @@ public class SavingsAccountChargeData {
public static SavingsAccountChargeData template(final Collection<ChargeData> chargeOptions) {
final Long id = null;
final Long chargeId = null;
final Long accountId = null;
final String name = null;
final CurrencyData currency = null;
final BigDecimal amount = BigDecimal.ZERO;
@@ -89,29 +93,30 @@ public class SavingsAccountChargeData {
final MonthDay feeOnMonthDay = null;
return new SavingsAccountChargeData(id, chargeId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage,
return new SavingsAccountChargeData(id, chargeId, accountId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage,
amountPercentageAppliedTo, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeOptions,
penalty, feeOnMonthDay);
}
public static SavingsAccountChargeData instance(final Long id, final Long chargeId, final String name, final CurrencyData currency,
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<ChargeData> chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay) {
return new SavingsAccountChargeData(id, chargeId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage,
return new SavingsAccountChargeData(id, chargeId, accountId, name, chargeTimeType, dueAsOfDate, chargeCalculationType, percentage,
amountPercentageAppliedTo, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeOptions,
penalty, feeOnMonthDay);
}
private SavingsAccountChargeData(final Long id, final Long chargeId, final String name, final EnumOptionData chargeTimeType,
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<ChargeData> chargeOptions, final boolean penalty, final MonthDay feeOnMonthDay) {
this.id = id;
this.chargeId = chargeId;
this.accountId = accountId;
this.name = name;
this.chargeTimeType = chargeTimeType;
this.dueDate = dueAsOfDate;
@@ -1212,6 +1212,7 @@ public class SavingsAccount extends AbstractPersistable<Long> {
}
} else if (transactionToUndo.isWaiveCharge()) {
chargeToUndo.undoWaiver(this.getCurrency(), transactionToUndo.getAmount(this.getCurrency()));
chargeToUndo.updateToPreviousAnnualFeeDueDate();
}
}
}
@@ -1678,6 +1679,9 @@ public class SavingsAccount extends AbstractPersistable<Long> {
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) {
@@ -1733,13 +1737,21 @@ public class SavingsAccount extends AbstractPersistable<Long> {
"multiple.annual.fee.per.account.not.supported");
throw new PlatformApiDataValidationException(dataValidationErrors);
}
if(isActive()){
savingsAccountCharge.updateToNextAnnualFeeDueDateFrom(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.
}
//activation charge and withdrawal charges not required this validation
if(savingsAccountCharge.isOnSpecifiedDueDate()){
validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_APPLY_CHARGE, chargeDueDate);
}
// add new charge to savings account
charges().add(savingsAccountCharge);
@@ -211,7 +211,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
}
public boolean isPostInterestCalculationRequired(){
return this.isDeposit() || this.isWithdrawal() || this.isPayCharge();
return this.isDeposit() || this.isChargeTransaction();
}
public boolean isInterestPostingAndNotReversed() {
@@ -278,7 +278,14 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
}
public void updateCumulativeBalanceAndDates(final MonetaryCurrency currency, final LocalDate endOfBalanceDate) {
this.balanceEndDate = endOfBalanceDate.toDate();
//balance end date should not be before transaction date
if (endOfBalanceDate != null && endOfBalanceDate.isBefore(this.transactionLocalDate())){
this.balanceEndDate = this.transactionLocalDate().toDate();
}else if(endOfBalanceDate != null){
this.balanceEndDate = endOfBalanceDate.toDate();
}else{
this.balanceEndDate = null;
}
this.balanceNumberOfDays = LocalDateInterval.create(getTransactionLocalDate(), endOfBalanceDate).daysInPeriodInclusiveOfEndDate();
this.cumulativeBalance = Money.of(currency, this.runningBalance).multipliedBy(this.balanceNumberOfDays).getAmount();
}
@@ -374,7 +381,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
Money endOfDayBalance = openingBalance.copy();
if (isDeposit()) {
endOfDayBalance = openingBalance.plus(getAmount(currency));
} else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
} else if (isWithdrawal() || isChargeTransactionAndNotReversed()) {
endOfDayBalance = openingBalance.minus(getAmount(currency));
}
@@ -390,7 +397,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
Money endOfDayBalance = openingBalance.copy();
if (isDeposit()) {
endOfDayBalance = openingBalance.plus(getAmount(currency));
} else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
} else if (isWithdrawal() || isChargeTransactionAndNotReversed()) {
if (openingBalance.isGreaterThanZero()) {
endOfDayBalance = openingBalance.minus(getAmount(currency));
@@ -420,7 +427,7 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
// if (endOfDayBalance.isLessThanZero()) {
endOfDayBalance = Money.of(currency, this.runningBalance);
// }
} else if (isWithdrawal() || isWithdrawalFeeAndNotReversed()) {
} else if (isWithdrawal() || isChargeTransactionAndNotReversed()) {
// endOfDayBalance = openingBalance.minus(getAmount(currency));
// if (endOfDayBalance.isLessThanZero()) {
endOfDayBalance = Money.of(currency, this.runningBalance);
@@ -471,6 +478,10 @@ public final class SavingsAccountTransaction extends AbstractPersistable<Long> {
return SavingsAccountTransactionType.fromInt(this.typeOf).isChargeTransaction();
}
public boolean isChargeTransactionAndNotReversed(){
return SavingsAccountTransactionType.fromInt(this.typeOf).isChargeTransaction() && isNotReversed();
}
public boolean isWaiveCharge(){
return SavingsAccountTransactionType.fromInt(this.typeOf).isWaiveCharge();
}
@@ -8,6 +8,7 @@ package org.mifosplatform.portfolio.savings.service;
import java.util.Collection;
import org.mifosplatform.portfolio.charge.data.ChargeData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountChargeData;
public interface SavingsAccountChargeReadPlatformService {
@@ -17,4 +18,8 @@ public interface SavingsAccountChargeReadPlatformService {
Collection<SavingsAccountChargeData> retrieveSavingsAccountCharges(Long savingsAccountId);
SavingsAccountChargeData retrieveSavingsAccountChargeDetails(Long savingsAccountChargeId, Long savingsAccountId);
Collection<SavingsAccountAnnualFeeData> retrieveChargesWithAnnualFeeDue();
Collection<SavingsAccountAnnualFeeData> retrieveChargesWithDue();
}
@@ -19,10 +19,13 @@ import org.mifosplatform.infrastructure.core.service.RoutingDataSource;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
import org.mifosplatform.portfolio.charge.data.ChargeData;
import org.mifosplatform.portfolio.charge.domain.ChargeTimeType;
import org.mifosplatform.portfolio.charge.exception.SavingsAccountChargeNotFoundException;
import org.mifosplatform.portfolio.charge.service.ChargeDropdownReadPlatformService;
import org.mifosplatform.portfolio.charge.service.ChargeEnumerations;
import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData;
import org.mifosplatform.portfolio.savings.data.SavingsAccountChargeData;
import org.mifosplatform.portfolio.savings.domain.SavingsAccountStatusType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -35,6 +38,9 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou
private final JdbcTemplate jdbcTemplate;
private final PlatformSecurityContext context;
private final ChargeDropdownReadPlatformService chargeDropdownReadPlatformService;
//mappers
private final SavingsAccountChargeDueMapper chargeDueMapper;
@Autowired
public SavingsAccountChargeReadPlatformServiceImpl(final PlatformSecurityContext context,
@@ -42,12 +48,13 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou
this.context = context;
this.chargeDropdownReadPlatformService = chargeDropdownReadPlatformService;
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.chargeDueMapper = new SavingsAccountChargeDueMapper();
}
private static final class SavingsAccountChargeMapper implements RowMapper<SavingsAccountChargeData> {
public String schema() {
return "sc.id as id, c.id as chargeId, c.name as name, "
return "sc.id as id, c.id as chargeId, sc.savings_account_id as accountId, c.name as name, "
+ "sc.amount as amountDue, "
+ "sc.amount_paid_derived as amountPaid, "
+ "sc.amount_waived_derived as amountWaived, "
@@ -72,6 +79,7 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou
final Long id = rs.getLong("id");
final Long chargeId = rs.getLong("chargeId");
final Long accountId = rs.getLong("accountId");
final String name = rs.getString("name");
final BigDecimal amount = rs.getBigDecimal("amountDue");
final BigDecimal amountPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "amountPaid");
@@ -102,16 +110,16 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou
if (feeOnDay != null) {
feeOnMonthDay = new MonthDay(feeOnMonth, feeOnDay);
}
final int chargeCalculation = rs.getInt("chargeCalculation");
final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(chargeCalculation);
final boolean penalty = rs.getBoolean("penalty");
final Collection<ChargeData> chargeOptions = null;
return SavingsAccountChargeData.instance(id, chargeId, name, currency, amount, amountPaid, amountWaived, amountWrittenOff,
amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo,
chargeOptions, penalty, feeOnMonthDay);
return SavingsAccountChargeData.instance(id, chargeId, accountId, name, currency, amount, amountPaid, amountWaived,
amountWrittenOff, amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf,
amountPercentageAppliedTo, chargeOptions, penalty, feeOnMonthDay);
}
}
@@ -161,4 +169,56 @@ public class SavingsAccountChargeReadPlatformServiceImpl implements SavingsAccou
return this.jdbcTemplate.query(sql, rm, new Object[] { loanId });
}
private static final class SavingsAccountChargeDueMapper implements RowMapper<SavingsAccountAnnualFeeData> {
private final String schemaSql;
public SavingsAccountChargeDueMapper() {
final StringBuilder sqlBuilder = new StringBuilder(200);
sqlBuilder.append("sac.id as id, ");
sqlBuilder.append("sa.id as accountId, ");
sqlBuilder.append("sa.account_no as accountNo, ");
sqlBuilder.append("sac.charge_due_date as dueDate ");
sqlBuilder.append("from m_savings_account_charge sac join m_savings_account sa on sac.savings_account_id = sa.id ");
this.schemaSql = sqlBuilder.toString();
}
public String schema() {
return this.schemaSql;
}
@Override
public SavingsAccountAnnualFeeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final Long accountId = rs.getLong("accountId");
final String accountNo = rs.getString("accountNo");
final LocalDate annualFeeNextDueDate = JdbcSupport.getLocalDate(rs, "dueDate");
return SavingsAccountAnnualFeeData.instance(id, accountId, accountNo, annualFeeNextDueDate);
}
}
@Override
public Collection<SavingsAccountAnnualFeeData> retrieveChargesWithAnnualFeeDue() {
final String sql = "select " + this.chargeDueMapper.schema() + " where sac.charge_due_date is not null and sac.charge_time_enum = "
+ ChargeTimeType.ANNUAL_FEE.getValue() + " and sac.charge_due_date <= NOW() and sa.status_enum = "
+ SavingsAccountStatusType.ACTIVE.getValue();
return this.jdbcTemplate.query(sql, this.chargeDueMapper, new Object[] {});
}
@Override
public Collection<SavingsAccountAnnualFeeData> retrieveChargesWithDue() {
final String sql = "select "
+ this.chargeDueMapper.schema()
+ " where sac.charge_due_date is not null and sac.charge_due_date <= NOW() and sac.waived = 0 and sac.is_paid_derived=0 and sac.is_active=1 and sa.status_enum = "
+ SavingsAccountStatusType.ACTIVE.getValue() + " order by sac.charge_due_date ";
return this.jdbcTemplate.query(sql, this.chargeDueMapper, new Object[] {});
}
}
@@ -20,7 +20,7 @@ public interface SavingsAccountWritePlatformService {
CommandProcessingResult withdrawal(Long savingsId, JsonCommand command);
/*CommandProcessingResult applyAnnualFee(Long savingsId, LocalDate annualFeeTransactionDate);*/
CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId);
CommandProcessingResult calculateInterest(Long savingsId);
@@ -52,5 +52,5 @@ public interface SavingsAccountWritePlatformService {
CommandProcessingResult payCharge(Long savingsAccountId, Long savingsAccountChargeId, JsonCommand command);
void processDueCharges();
void applyChargeDue(final Long savingsAccountChargeId, final Long accountId);
}
@@ -248,36 +248,25 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
.build();
}
/*@Transactional
@Transactional
@Override
public CommandProcessingResult applyAnnualFee(final Long savingsId, final LocalDate annualFeeTransactionDate) {
public CommandProcessingResult applyAnnualFee(final Long savingsAccountChargeId, final Long accountId) {
final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId);
checkClientOrGroupActive(account);
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(
savingsAccountChargeId, accountId);
final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy");
final Set<Long> existingTransactionIds = new HashSet<Long>();
final Set<Long> existingReversedTransactionIds = new HashSet<Long>();
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getLocalDateOfTenant();
final SavingsAccountTransaction annualFee = account.addAnnualFee(mc, fmt, annualFeeTransactionDate, today);
final Long transactionId = saveTransactionToGenerateTransactionId(annualFee);
this.savingAccountRepository.save(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
this.payCharge(savingsAccountCharge, savingsAccountCharge.getDueLocalDate(), savingsAccountCharge.amount(), fmt);
return new CommandProcessingResultBuilder() //
.withEntityId(transactionId) //
.withOfficeId(account.officeId()) //
.withClientId(account.clientId()) //
.withGroupId(account.groupId()) //
.withSavingsId(savingsId) //
.withEntityId(savingsAccountCharge.getId()) //
.withOfficeId(savingsAccountCharge.savingsAccount().officeId()) //
.withClientId(savingsAccountCharge.savingsAccount().clientId()) //
.withGroupId(savingsAccountCharge.savingsAccount().groupId()) //
.withSavingsId(savingsAccountCharge.savingsAccount().getId()) //
.build();
}*/
}
@Transactional
@Override
@@ -828,21 +817,16 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
}
@Transactional
@Override
@CronTarget(jobName = JobName.PAY_DUE_SAVINGS_CHARGES)
public void processDueCharges() {
public void applyChargeDue(final Long savingsAccountChargeId, final Long accountId) {
// always use current date as transaction date for batch job
final LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
final List<SavingsAccountCharge> pendingCharges = this.savingsAccountChargeRepository.findPendingCharges(transactionDate.toDate());
final SavingsAccountCharge savingsAccountCharge = this.savingsAccountChargeRepository.findOneWithNotFoundDetection(
savingsAccountChargeId, accountId);
final DateTimeFormatter fmt = DateTimeFormat.forPattern("dd MM yyyy");
for (SavingsAccountCharge savingsAccountCharge : pendingCharges) {
try {
payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt);
} catch (Exception exception) {
// TODO: AA - provide a meaningful error with failed
// transactions list
}
}
payCharge(savingsAccountCharge, transactionDate, savingsAccountCharge.amoutOutstanding(), fmt);
}
@Transactional
@@ -857,7 +841,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
account.payCharge(savingsAccountCharge, amountPaid, transactionDate, formatter);
final MathContext mc = MathContext.DECIMAL64;
if (account.isBeforeLastPostingPeriod(savingsAccountCharge.getDueLocalDate())) {
if (account.isBeforeLastPostingPeriod(transactionDate)) {
final LocalDate today = DateUtils.getLocalDateOfTenant();
account.postInterest(mc, today);
} else {
@@ -14,4 +14,6 @@ public interface ScheduledJobRunnerService {
void updateLoanPaidInAdvance();
void applyAnnualFeeForSavings();
void applyDueChargesForSavings();
}
@@ -5,11 +5,17 @@
*/
package org.mifosplatform.scheduledjobs.service;
import java.util.Collection;
import java.util.List;
import org.mifosplatform.infrastructure.core.data.ApiParameterError;
import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidationException;
import org.mifosplatform.infrastructure.core.service.RoutingDataSourceServiceFactory;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.jobs.annotation.CronTarget;
import org.mifosplatform.infrastructure.jobs.service.JobName;
import org.mifosplatform.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.mifosplatform.portfolio.savings.data.SavingsAccountAnnualFeeData;
import org.mifosplatform.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.mifosplatform.portfolio.savings.service.SavingsAccountWritePlatformService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -24,18 +30,16 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
private final static Logger logger = LoggerFactory.getLogger(ScheduledJobRunnerServiceImpl.class);
private final RoutingDataSourceServiceFactory dataSourceServiceFactory;
@SuppressWarnings("unused")
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
@SuppressWarnings("unused")
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService;
@Autowired
public ScheduledJobRunnerServiceImpl(final RoutingDataSourceServiceFactory dataSourceServiceFactory,
final SavingsAccountWritePlatformService savingsAccountWritePlatformService,
final SavingsAccountReadPlatformService savingsAccountReadPlatformService) {
final SavingsAccountChargeReadPlatformService savingsAccountChargeReadPlatformService) {
this.dataSourceServiceFactory = dataSourceServiceFactory;
this.savingsAccountWritePlatformService = savingsAccountWritePlatformService;
this.savingsAccountReadPlatformService = savingsAccountReadPlatformService;
this.savingsAccountChargeReadPlatformService = savingsAccountChargeReadPlatformService;
}
@Transactional
@@ -194,27 +198,50 @@ public class ScheduledJobRunnerServiceImpl implements ScheduledJobRunnerService
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Results affected by update: " + result);
}
@Transactional
@Override
@CronTarget(jobName = JobName.APPLY_ANNUAL_FEE_FOR_SAVINGS)
public void applyAnnualFeeForSavings() {
/*final Collection<SavingsAccountAnnualFeeData> annualFeeData = this.savingsAccountReadPlatformService
.retrieveAccountsWithAnnualFeeDue();
final Collection<SavingsAccountAnnualFeeData> annualFeeData = this.savingsAccountChargeReadPlatformService
.retrieveChargesWithAnnualFeeDue();
for (final SavingsAccountAnnualFeeData savingsAccountReference : annualFeeData) {
try {
this.savingsAccountWritePlatformService.applyAnnualFee(savingsAccountReference.getId(),
savingsAccountReference.getNextAnnualFeeDueDate());
savingsAccountReference.getAccountId());
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
logger.error("Apply annual fee failed for account:" + savingsAccountReference.getAccountNo() + " with message "
+ error.getDeveloperMessage());
}
} catch (final Exception ex){
//need to handle this scenario
}
}
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + annualFeeData.size());*/
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + annualFeeData.size());
}
@Override
@CronTarget(jobName = JobName.PAY_DUE_SAVINGS_CHARGES)
public void applyDueChargesForSavings() {
final Collection<SavingsAccountAnnualFeeData> chargesDueData = this.savingsAccountChargeReadPlatformService
.retrieveChargesWithDue();
for (final SavingsAccountAnnualFeeData savingsAccountReference : chargesDueData) {
try {
this.savingsAccountWritePlatformService.applyChargeDue(savingsAccountReference.getId(), savingsAccountReference.getAccountId());
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
for (final ApiParameterError error : errors) {
logger.error("Apply Charges due for savings failed for account:" + savingsAccountReference.getAccountNo() + " with message "
+ error.getDeveloperMessage());
}
}
}
logger.info(ThreadLocalContextUtil.getTenant().getName() + ": Savings accounts affected by update: " + chargesDueData.size());
}
}