/*
* Copyright (C) 2012-2016 The Android Money Manager Ex Project Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.money.manager.ex.servicelayer;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.widget.Toast;
import com.money.manager.ex.Constants;
import com.money.manager.ex.R;
import com.money.manager.ex.core.UIHelper;
import com.money.manager.ex.database.ISplitTransaction;
import com.money.manager.ex.datalayer.RecurringTransactionRepository;
import com.money.manager.ex.datalayer.SplitRecurringCategoriesRepository;
import com.money.manager.ex.domainmodel.RecurringTransaction;
import com.money.manager.ex.domainmodel.SplitRecurringCategory;
import com.money.manager.ex.recurring.transactions.Recurrence;
import com.money.manager.ex.utils.MmxDate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
/**
* Represent a first Recurring Transaction object and provides related operations.
*/
public class RecurringTransactionService
extends ServiceBase {
public static final String LOGCAT = RecurringTransactionService.class.getSimpleName();
public RecurringTransactionService(Context context){
super(context);
}
public RecurringTransactionService(int recurringTransactionId, Context context){
super(context);
this.recurringTransactionId = recurringTransactionId;
}
public int recurringTransactionId = Constants.NOT_SET;
private RecurringTransactionRepository mRepository;
private RecurringTransaction mRecurringTransaction;
/**
* @param date to start calculate
* @param repeatType type of repeating transactions
* @param numberOfPeriods Number of instances (days, months) parameter. Used for In (x) Days, for
* example to indicate x.
* @return next Date
*/
public Date getNextScheduledDate(Date date, Recurrence repeatType, Integer numberOfPeriods) {
if (numberOfPeriods == null || numberOfPeriods == Constants.NOT_SET) {
numberOfPeriods = 0;
}
if (repeatType.getValue() >= 200) {
repeatType = Recurrence.valueOf(repeatType.getValue() - 200);
} // set auto execute without user acknowledgement
if (repeatType.getValue() >= 100) {
repeatType = Recurrence.valueOf(repeatType.getValue() - 100);
} // set auto execute on the next occurrence
MmxDate result = new MmxDate(date);
switch (repeatType) {
case ONCE: //none
break;
case WEEKLY: //weekly
result = result.plusWeeks(1);
break;
case BIWEEKLY: //bi_weekly
result = result.plusWeeks(2);
break;
case MONTHLY: //monthly
result = result.plusMonths(1);
break;
case BIMONTHLY: //bi_monthly
result = result.plusMonths(2);
break;
case QUARTERLY: //quarterly
result = result.plusMonths(3);
break;
case SEMIANNUALLY: //half_year
result = result.plusMonths(6);
break;
case ANNUALLY: //yearly
result = result.plusYears(1);
break;
case FOUR_MONTHS: //four_months
result = result.plusMonths(4);
break;
case FOUR_WEEKS: //four_weeks
result = result.plusWeeks(4);
break;
case DAILY: //daily
result = result.plusDays(1);
break;
case IN_X_DAYS: //in_x_days
case EVERY_X_DAYS: //every_x_days
result = result.plusDays(numberOfPeriods);
break;
case IN_X_MONTHS: //in_x_months
case EVERY_X_MONTHS: //every_x_months
result = result.plusMonths(numberOfPeriods);
break;
case MONTHLY_LAST_DAY: //month (last day)
// if the date is not the last day of this month, set it to the end of the month.
// else set it to the end of the next month.
MmxDate lastDayOfMonth = result.lastDayOfMonth();
if (!result.equals(lastDayOfMonth)) {
// set to last day of the month
result = lastDayOfMonth;
} else {
result = lastDayOfMonth.plusMonths(1);
}
break;
case MONTHLY_LAST_BUSINESS_DAY: //month (last business day)
// if the date is not the last day of this month, set it to the end of the month.
// else set it to the end of the next month.
MmxDate lastDayOfMonth2 = result.lastDayOfMonth();
if (!result.equals(lastDayOfMonth2)) {
// set to last day of the month
result = lastDayOfMonth2;
} else {
result = lastDayOfMonth2.plusMonths(1);
}
// get the last day of the next month,
// then iterate backwards until we are on a weekday.
while(result.getDayOfWeek() == Calendar.SATURDAY ||
result.getDayOfWeek() == Calendar.SUNDAY) {
result = result.minusDays(1);
}
break;
}
return result.toDate();
}
public RecurringTransactionRepository getRepository(){
if (mRepository == null) {
mRepository = new RecurringTransactionRepository(getContext());
}
return mRepository;
}
public RecurringTransaction load(int id) {
return getRepository().load(id);
}
/**
* This will process the Recurring Transaction so that the dates are moved to the next
* occurrence, if it is to occur again.
* If not, the recurring transaction is deleted.
*/
public void moveNextOccurrence() {
RecurringTransaction tx = getRecurringTransaction();
if (tx == null) {
throw new IllegalArgumentException("Recurring Transaction is not set!");
}
Integer recurrenceType = tx.getRecurrenceInt();
if (recurrenceType == null) {
String message = getContext().getString(R.string.recurrence_type_not_set);
throw new IllegalArgumentException(message);
}
/**
* The action will depend on the transaction preferences.
*/
Recurrence recurrence = Recurrence.valueOf(recurrenceType);
if (recurrence == null) {
String recurrenceTypeString = Integer.toString(recurrenceType);
throw new IllegalArgumentException(getContext().getString(R.string.invalid_recurrence_type)
+ " " + recurrenceTypeString);
}
switch (recurrence) {
// periodical (monthly, weekly)
case ONCE:
delete();
// exit now.
return;
case WEEKLY:
case BIWEEKLY:
case MONTHLY:
case BIMONTHLY:
case QUARTERLY:
case SEMIANNUALLY:
case ANNUALLY:
case FOUR_MONTHS:
case FOUR_WEEKS:
case DAILY:
case MONTHLY_LAST_DAY:
case MONTHLY_LAST_BUSINESS_DAY:
moveDatesForward();
// Delete if occurrence is down to 1. 0 means repeat forever.
deleteIfLastPayment();
decreasePaymentsLeft();
break;
// every n periods
case EVERY_X_DAYS:
case EVERY_X_MONTHS:
moveDatesForward();
break;
// in n periods
case IN_X_DAYS:
case IN_X_MONTHS:
// reset number of periods
mRecurringTransaction.setPaymentsLeft(Constants.NOT_SET);
break;
default:
break;
}
// Save changes
RecurringTransactionRepository repo = getRepository();
boolean updated = repo.update(mRecurringTransaction);
if (!updated) {
new UIHelper(getContext()).showToast(R.string.error_saving_record);
}
}
/**
* Delete current recurring transaction record.
* @return success
*/
public boolean delete() {
boolean result;
// Delete any related split transactions.
result = this.deleteSplitCategories();
// Exit if the deletion of splits failed.
if(!result) return false;
// Delete recurring transactions.
RecurringTransactionRepository repo = new RecurringTransactionRepository(getContext());
int deleteResult = repo.delete(this.recurringTransactionId);
// int deleteResult = getContext().getContentResolver().delete(repo.getUri(),
// RecurringTransaction.BDID + "=" + this.recurringTransactionId, null);
if (deleteResult == 0) {
Toast.makeText(getContext(), R.string.db_delete_failed, Toast.LENGTH_SHORT).show();
Log.w(LOGCAT, "Deleting recurring transaction " +
this.recurringTransactionId + " failed.");
result = false;
}
// result is true if deletion of related splits was successful.
return result;
}
/**
* Delete any split categories for the current recurring transaction.
* @return success
*/
public boolean deleteSplitCategories() {
boolean result = false;
// first check if there are any records.
Cursor cursor = this.getCursorForSplitTransactions();
if (cursor == null) return false;
int existingRecords = cursor.getCount();
cursor.close();
if(existingRecords == 0) {
return true;
}
// delete them
SplitRecurringCategoriesRepository repo = new SplitRecurringCategoriesRepository(getContext());
int deleteResult = getContext().getContentResolver().delete(
repo.getUri(),
SplitRecurringCategory.TRANSID + "=" + this.recurringTransactionId, null);
if (deleteResult != 0) {
result = true;
} else {
Toast.makeText(getContext(), R.string.db_delete_failed, Toast.LENGTH_SHORT).show();
Log.w(LOGCAT, "Deleting split categories for recurring transaction " +
this.recurringTransactionId + " failed.");
}
return result;
}
/**
* Load split transactions.
* @return array list of all related split transactions
*/
public ArrayList<ISplitTransaction> loadSplitTransactions() {
ArrayList<ISplitTransaction> result = new ArrayList<>();
Cursor cursor = this.getCursorForSplitTransactions();
if (cursor == null) return result;
while (cursor.moveToNext()) {
SplitRecurringCategory entity = new SplitRecurringCategory();
entity.loadFromCursor(cursor);
result.add(entity);
}
cursor.close();
return result;
}
/**
* @param repeat frequency repeats
* @return frequency
*/
public String getRecurrenceLocalizedName(int repeat) {
// set auto execute without user acknowledgement
if (repeat >= 200) {
repeat = repeat - 200;
}
// set auto execute on the next occurrence
if (repeat >= 100) {
repeat = repeat - 100;
}
String[] arrays = getContext().getResources().getStringArray(R.array.frequencies_items);
if (arrays != null && repeat >= 0 && repeat <= arrays.length) {
return arrays[repeat];
}
return "";
}
// Private.
private void decreasePaymentsLeft() {
RecurringTransaction tx = getRecurringTransaction();
Integer paymentsLeft = tx.getPaymentsLeft();
if (paymentsLeft == null) {
tx.setPaymentsLeft(0);
return;
}
if (paymentsLeft > 1) {
paymentsLeft = paymentsLeft - 1;
}
tx.setPaymentsLeft(paymentsLeft);
}
private void deleteIfLastPayment() {
RecurringTransaction tx = getRecurringTransaction();
Integer paymentsLeft = tx.getPaymentsLeft();
if (paymentsLeft == null) {
tx.setPaymentsLeft(0);
return;
}
if (paymentsLeft == 1) {
delete();
}
}
/**
* Creates a query for getting all related split transactions.
* @return cursor for all the related split transactions
*/
private Cursor getCursorForSplitTransactions(){
SplitRecurringCategoriesRepository repo = new SplitRecurringCategoriesRepository(getContext());
return getContext().getContentResolver().query(
repo.getUri(),
null,
SplitRecurringCategory.TRANSID + "=" + Integer.toString(this.recurringTransactionId),
null,
SplitRecurringCategory.SPLITTRANSID);
}
private RecurringTransaction getRecurringTransaction() {
if (mRecurringTransaction == null) {
mRecurringTransaction = getRepository().load(recurringTransactionId);
}
return mRecurringTransaction;
}
/**
* Set the recurring transaction's Due date and the Payment date to the next occurrence.
* Saves changes to the database.
*/
private void moveDatesForward() {
// Due date.
moveDueDateForward();
// Payment date.
RecurringTransaction tx = getRecurringTransaction();
Recurrence repeatType = Recurrence.valueOf(tx.getRecurrenceInt());
Date newPaymentDate = tx.getPaymentDate();
Integer paymentsLeft = tx.getPaymentsLeft();
// calculate the next payment date
newPaymentDate = getNextScheduledDate(newPaymentDate, repeatType, paymentsLeft);
if (newPaymentDate != null) {
tx.setPaymentDate(newPaymentDate);
}
}
private void moveDueDateForward() {
RecurringTransaction tx = getRecurringTransaction();
Recurrence repeats = Recurrence.valueOf(tx.getRecurrenceInt());
Date dueDate = tx.getDueDate();
Integer paymentsLeft = tx.getPaymentsLeft();
Date newDueDate = getNextScheduledDate(dueDate, repeats, paymentsLeft);
if (newDueDate != null) {
tx.setDueDate(newDueDate);
}
}
}