package com.code44.finance.data.model; import android.content.ContentValues; import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import com.code44.finance.common.model.TransactionState; import com.code44.finance.common.model.TransactionType; import com.code44.finance.common.utils.Preconditions; import com.code44.finance.common.utils.StringUtils; import com.code44.finance.data.db.Column; import com.code44.finance.data.db.Tables; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class Transaction extends Model { public static final Parcelable.Creator<Transaction> CREATOR = new Parcelable.Creator<Transaction>() { public Transaction createFromParcel(Parcel in) { return new Transaction(in); } public Transaction[] newArray(int size) { return new Transaction[size]; } }; private Account accountFrom; private Account accountTo; private Category category; private List<Tag> tags; private long date; private long amount; private double exchangeRate; private String note; private TransactionState transactionState; private TransactionType transactionType; private boolean includeInReports; public Transaction() { super(); setAccountFrom(null); setAccountTo(null); setCategory(null); setTags(null); setDate(System.currentTimeMillis()); setAmount(0); setExchangeRate(1.0); setNote(null); setTransactionState(TransactionState.Confirmed); setTransactionType(TransactionType.Expense); setIncludeInReports(true); } public Transaction(Parcel parcel) { super(parcel); setAccountFrom((Account) parcel.readParcelable(Account.class.getClassLoader())); setAccountTo((Account) parcel.readParcelable(Account.class.getClassLoader())); setCategory((Category) parcel.readParcelable(Category.class.getClassLoader())); tags = new ArrayList<>(); parcel.readTypedList(tags, Tag.CREATOR); setDate(parcel.readLong()); setAmount(parcel.readLong()); setExchangeRate(parcel.readDouble()); setNote(parcel.readString()); setTransactionState(TransactionState.fromInt(parcel.readInt())); setTransactionType(TransactionType.fromInt(parcel.readInt())); setIncludeInReports(parcel.readInt() != 0); } public static Transaction from(Cursor cursor) { final Transaction transaction = new Transaction(); if (cursor.getCount() > 0) { transaction.updateFrom(cursor, null); } return transaction; } @Override protected Column getLocalIdColumn() { return Tables.Transactions.LOCAL_ID; } @Override protected Column getIdColumn() { return Tables.Transactions.ID; } @Override protected Column getModelStateColumn() { return Tables.Transactions.MODEL_STATE; } @Override protected Column getSyncStateColumn() { return Tables.Transactions.SYNC_STATE; } @Override public void prepareForDb() { super.prepareForDb(); if (tags == null) { tags = Collections.emptyList(); } if (amount < 0) { amount = 0; } if (Double.compare(exchangeRate, 0) < 0) { exchangeRate = 1.0; } if (note == null) { note = ""; } if (transactionState == null) { transactionState = TransactionState.Pending; } if (transactionType == null) { transactionType = TransactionType.Expense; } switch (transactionType) { case Expense: accountTo = null; exchangeRate = 1.0; break; case Income: accountFrom = null; exchangeRate = 1.0; break; case Transfer: category = null; break; } } @Override public void validate() throws IllegalStateException { super.validate(); Preconditions.notNull(transactionState, "Transaction state cannot be null."); Preconditions.notNull(transactionType, "Transaction type cannot be null."); Preconditions.moreOrEquals(amount, 0, "Amount must be >= 0."); Preconditions.notNull(note, "Note cannot be null."); switch (transactionType) { case Expense: if (transactionState == TransactionState.Confirmed) { Preconditions.notNull(accountFrom, "AccountFrom cannot be null."); Preconditions.isTrue(accountFrom.hasId(), "AccountFrom must have an Id."); //noinspection ResultOfMethodCallIgnored Preconditions.equals(exchangeRate, 1.0, "Exchange rate must be 1.0."); } Preconditions.isNull(accountTo, "AccountTo must be null."); break; case Income: if (transactionState == TransactionState.Confirmed) { Preconditions.notNull(accountTo, "AccountTo cannot be null."); Preconditions.isTrue(accountTo.hasId(), "AccountTo must have an Id."); //noinspection ResultOfMethodCallIgnored Preconditions.equals(exchangeRate, 1.0, "Exchange rate must be 1.0."); } Preconditions.isNull(accountFrom, "AccountFrom must be null."); break; case Transfer: if (transactionState == TransactionState.Confirmed) { Preconditions.notNull(accountFrom, "AccountFrom cannot be null."); Preconditions.isTrue(accountFrom.hasId(), "AccountFrom must have an Id."); Preconditions.notNull(accountTo, "AccountTo cannot be null."); Preconditions.isTrue(accountTo.hasId(), "AccountTo must have an Id."); Preconditions.moreOrEquals(exchangeRate, 0, "Exchange rate must be > 0."); if (accountFrom.equals(accountTo)) { throw new IllegalStateException("AccountFrom cannot be equal to AccountTo."); } } Preconditions.isNull(category, "Transfer cannot have a category."); break; default: throw new IllegalArgumentException("Transaction type " + transactionType + " is not supported."); } if (Double.compare(exchangeRate, 0) < 0) { throw new IllegalStateException("Exchange rate must be > 0."); } } @Override public ContentValues asValues() { final ContentValues values = super.asValues(); values.put(Tables.Transactions.ACCOUNT_FROM_ID.getName(), accountFrom == null ? null : accountFrom.getId()); values.put(Tables.Transactions.ACCOUNT_TO_ID.getName(), accountTo == null ? null : accountTo.getId()); values.put(Tables.Transactions.CATEGORY_ID.getName(), category == null ? null : category.getId()); values.put(Tables.Transactions.DATE.getName(), date); values.put(Tables.Transactions.AMOUNT.getName(), amount); values.put(Tables.Transactions.EXCHANGE_RATE.getName(), exchangeRate); values.put(Tables.Transactions.NOTE.getName(), note); values.put(Tables.Transactions.STATE.getName(), transactionState.asInt()); values.put(Tables.Transactions.TYPE.getName(), transactionType.asInt()); values.put(Tables.Transactions.INCLUDE_IN_REPORTS.getName(), includeInReports); final StringBuilder sb = new StringBuilder(); for (Tag tag : tags) { if (sb.length() > 0) { sb.append(Tables.CONCAT_SEPARATOR); } sb.append(tag.getId()); } values.put(Tables.Tags.ID.getName(), sb.toString()); return values; } @Override public void writeToParcel(Parcel parcel, int flags) { super.writeToParcel(parcel, flags); parcel.writeParcelable(accountFrom, 0); parcel.writeParcelable(accountTo, 0); parcel.writeParcelable(category, 0); parcel.writeTypedList(tags); parcel.writeLong(date); parcel.writeLong(amount); parcel.writeDouble(exchangeRate); parcel.writeString(note); parcel.writeInt(transactionState.asInt()); parcel.writeInt(transactionType.asInt()); parcel.writeInt(includeInReports ? 1 : 0); } @Override public void updateFrom(Cursor cursor, String columnPrefixTable) { super.updateFrom(cursor, columnPrefixTable); int index; // Transaction type index = cursor.getColumnIndex(Tables.Transactions.TYPE.getName(columnPrefixTable)); if (index >= 0) { setTransactionType(TransactionType.fromInt(cursor.getInt(index))); } // Account from index = cursor.getColumnIndex(Tables.Transactions.ACCOUNT_FROM_ID.getName(columnPrefixTable)); if (index >= 0 && !StringUtils.isEmpty(cursor.getString(index)) && !cursor.getString(index).equals("null")) { final Account accountFrom = Account.fromAccountFrom(cursor); accountFrom.setId(cursor.getString(index)); setAccountFrom(accountFrom); } else { setAccountFrom(null); } // Account to index = cursor.getColumnIndex(Tables.Transactions.ACCOUNT_TO_ID.getName(columnPrefixTable)); if (index >= 0 && !StringUtils.isEmpty(cursor.getString(index)) && !cursor.getString(index).equals("null")) { final Account accountTo = Account.fromAccountTo(cursor); accountTo.setId(cursor.getString(index)); setAccountTo(accountTo); } else { setAccountTo(null); } // Category index = cursor.getColumnIndex(Tables.Transactions.CATEGORY_ID.getName(columnPrefixTable)); if (index >= 0 && !StringUtils.isEmpty(cursor.getString(index)) && !cursor.getString(index).equals("null")) { final Category category = Category.from(cursor); category.setId(cursor.getString(index)); setCategory(category); } else { setCategory(null); } // Tags final String[] tagIds; final String[] tagTitles; index = cursor.getColumnIndex(Tables.Tags.ID.getName(columnPrefixTable)); if (index >= 0) { final String str = cursor.getString(index); if (!StringUtils.isEmpty(str)) { tagIds = TextUtils.split(str, Tables.CONCAT_SEPARATOR); } else { tagIds = null; } } else { tagIds = null; } index = cursor.getColumnIndex(Tables.Tags.TITLE.getName(columnPrefixTable)); if (index >= 0) { final String str = cursor.getString(index); if (!StringUtils.isEmpty(str)) { tagTitles = TextUtils.split(str, Tables.CONCAT_SEPARATOR); } else { tagTitles = null; } } else { tagTitles = null; } if (tagIds != null || tagTitles != null) { final List<Tag> tags = new ArrayList<>(); final int count = tagIds != null ? tagIds.length : tagTitles.length; for (int i = 0; i < count; i++) { final Tag tag = new Tag(); if (tagIds != null) { tag.setId(tagIds[i]); } if (tagTitles != null) { tag.setTitle(tagTitles[i]); } tags.add(tag); } setTags(tags); } else { setTags(null); } // Date index = cursor.getColumnIndex(Tables.Transactions.DATE.getName(columnPrefixTable)); if (index >= 0) { setDate(cursor.getLong(index)); } // Amount index = cursor.getColumnIndex(Tables.Transactions.AMOUNT.getName(columnPrefixTable)); if (index >= 0) { setAmount(cursor.getLong(index)); } // Exchange rate index = cursor.getColumnIndex(Tables.Transactions.EXCHANGE_RATE.getName(columnPrefixTable)); if (index >= 0) { setExchangeRate(cursor.getDouble(index)); } // Note index = cursor.getColumnIndex(Tables.Transactions.NOTE.getName(columnPrefixTable)); if (index >= 0) { setNote(cursor.getString(index)); } // Transaction state index = cursor.getColumnIndex(Tables.Transactions.STATE.getName(columnPrefixTable)); if (index >= 0) { setTransactionState(TransactionState.fromInt(cursor.getInt(index))); } // Include in reports index = cursor.getColumnIndex(Tables.Transactions.INCLUDE_IN_REPORTS.getName(columnPrefixTable)); if (index >= 0) { setIncludeInReports(cursor.getInt(index) != 0); } } public Account getAccountFrom() { return accountFrom; } public void setAccountFrom(Account accountFrom) { this.accountFrom = accountFrom; } public Account getAccountTo() { return accountTo; } public void setAccountTo(Account accountTo) { this.accountTo = accountTo; } public Category getCategory() { return category; } public void setCategory(Category category) { this.category = category; } public List<Tag> getTags() { return tags; } public void setTags(List<Tag> tags) { this.tags = tags == null ? Collections.<Tag>emptyList() : tags; } public long getDate() { return date; } public void setDate(long date) { this.date = date; } public long getAmount() { return amount; } public void setAmount(long amount) { this.amount = amount; } public double getExchangeRate() { return exchangeRate; } public void setExchangeRate(double exchangeRate) { this.exchangeRate = exchangeRate; } public String getNote() { return note; } public void setNote(String note) { this.note = note; } public TransactionState getTransactionState() { return transactionState; } public void setTransactionState(TransactionState transactionState) { this.transactionState = transactionState; } public TransactionType getTransactionType() { return transactionType; } public void setTransactionType(TransactionType transactionType) { this.transactionType = transactionType; } public boolean includeInReports() { return includeInReports; } public void setIncludeInReports(boolean includeInReports) { this.includeInReports = includeInReports; } }