Step 13 - All Aboard the Transaction Train

This entry is part of Vue.js Application Tutorial - Creating a Simple Budgeting App with Vue

This is part 13 of an 18 part series titled Vue.js Application Tutorial - Creating a Simple Budgeting App with Vue. It is highly recommended that you read the series in order, as each section builds on the last.

  1. Step 0: Intro
  2. Step 1: Planning Your Application
  3. Step 2: Data Architecture
  4. Step 3: Setup & Project Structure
  5. Step 4: Create & View Accounts
  6. Step 5: Edit & Delete Accounts
  7. Step 6: Adding LocalStorage to our Vue.js Application
  8. Step 7: Interlude & Refactor
  9. Step 8: Budgeting
  10. Step 9: Racing Through Budgets
  11. Step 10: Styling & Navigation
  12. Step 11: Finishing Budgets with Vue.js Dynamic Components
  13. Step 12: Planning for Transactions
  14. Step 13 - All Aboard the Transaction Train
  15. Step 14 - User Testing

The next entry is expected to be published on 03 August 2017. Sign up using the form at the bottom of this post to receive updates when posts are published.


We are finally ready to start coding the last module of our application, Transactions.

The good news is that most of the code is nearly identical to code we have already written in Budgets and Accounts, so the first part of this tutorial should go fast. Repetition is good for learning.

The bad news is that most of the code is nearly identical to code we have already written in Budgets and Accounts, so there are not a lot of opportunities for teaching new concepts in the first part of this tutorial.

I strongly encourage you to tackle the first section of this tutorial on your own instead of wholesale copy-pasting of code. Build out the backend data management for both Transactions and Businesses, then move into the list of transactions, then editing and creating a transaction. If you get stuck refer back to the corresponding code we already wrote for Budgets and Accounts, but try not to rely on that crutch too frequently. When you're done, come back here and compare it to what I came up with. You might even find a better way of doing things!

Author's note: this one kind of got away from me! There's a lot of code coming at you all at once. I believe it should all be easy to understand at this point, but if not, please open a GitHub issue or email me mwhager87 at gmail.

Data Backend

We know that we need both Businesses and Transactions. Each one will need to be created, updated, deleted, and loaded. Because we're building a minimal product, I'm going to leave out updating businesses for now. (This will be a great homework project for later!) As usual, we start with mutations, then move on to actions. Finally, we will move on to the API actions. Everything we are coding to this point is extremely similar to previous modules.

To this point we have not done a great job dealing with input as currency. There are a lot of complications when dealing with currency in real life, but for now our application will take the simple route of forcing all of our dollar inputs into floats. Yes, this is bad practice and should not be used in production! There will be some strange looking rounding errors. That's okay, for now, since our goal is not to teach about dealing with currency in JavaScript.

We know from building the Budget module that we will need a Business getter for populating our select drop down, and a way to get the business by its ID. Experience and planning has its perks.

import Vue from 'Vue';

const forceFloats = (o) => {
 o.amount = parseFloat(o.amount);
};

export default {
 CREATE_TRANSACTION (state, payload) {
 forceFloats(payload.transaction);
 Vue.set(state.transactions, payload.transaction.id, payload.transaction);
 },

 UPDATE_TRANSACTION (state, payload) {
 forceFloats(payload.transaction);
 state.transactions[payload.transaction.id] = payload.transaction;
 },

 DELETE_TRANSACTION (state, payload) {
 Vue.delete(state.transactions, payload.transaction.id);
 },

 LOAD_TRANSACTIONS (state, payload) {
 state.transactions = payload;

 Object.values(state.transactions).forEach((o) => { forceFloats(o); });
 },

 LOAD_BUSINESSES (state, payload) {
 state.businesses = payload;
 },

 CREATE_BUSINESS (state, payload) {
 state.businesses[payload.business.id] = payload.business;
 },

 DELETE_BUSINESS (state, payload) {
 Vue.delete(state.businesses, payload.business.id);
 }
};
import { guid } from '../../../utils';
import { deleteTransaction as deleteTransactionFromAPI, deleteBusiness as deleteBusinessFromAPI, fetchBusinesses, fetchTransactions, saveBusiness, saveTransaction } from '../api';

const prepareTransaction = (getters, data) => {
 // code shared by createTransaction and updateTransaction

 // find the budget based on the date
 // we'll go ahead and do this for existing transactions in case the user changes the date
 // in the future we may want to only do it on a date change
 let budget = getters.getBudgetByDate(data.date);
 if (!budget) throw new Error('Could not find a budget for the date ' + data.date);
 data.budget = budget.id;

 // tell the budget category that the transaction is occurring so it can update its amount
 let budgetCategory = getters.getBudgetCategoryByBudgetAndCategory(budget.id, data.category);
 if (!budgetCategory) throw new Error('Could not find a budget category for ' + data.category);
 // don't dispatch yet, we are just preparing data here

 return { preparedData: data, budgetCategory: budgetCategory, budget: budget };
};

export const createTransaction = ({ commit, dispatch, getters }, data) => {
 let { preparedData, budgetCategory, budget } = prepareTransaction(getters, data);

 let id = guid();
 let transaction = Object.assign({ id: id }, preparedData);

 // update the budget category, which updates the budget spend total
 dispatch('updateBudgetCategorySpent', {
 budgetCategory: budgetCategory,
 budget: budget,
 amount: transaction.amount
 });

 // update the account balance
 dispatch('updateAccountBalance', {
 account: getters.getAccountById(data.account),
 amount: transaction.amount
 });

 commit('CREATE_TRANSACTION', { transaction: transaction });
 saveTransaction(transaction);
};

export const updateTransaction = ({ commit, getters }, data) => {
 // TODO: handle any change the user could make here! Including
 // updating budgets or account balances

 let { preparedData } = prepareTransaction(getters, data);

 commit('UPDATE_TRANSACTION', { transaction: preparedData });
 saveTransaction(preparedData);
};

export const deleteTransaction = ({ commit }, data) => {
 commit('DELETE_TRANSACTION', { transaction: data });
 deleteTransactionFromAPI(data);
};

export const loadTransactions = ({ state, commit }) => {
 // loads transactions if they're not already loaded
 if (!state.transactions || Object.keys(state.transactions).length === 0) {
 return fetchTransactions().then((res) => {
 commit('LOAD_TRANSACTIONS', res);
 });
 }
};

export const createBusiness = ({ commit, state }, data) => {
 let id = guid();
 let business = Object.assign({ id: id }, data);
 commit('CREATE_BUSINESS', { business: business });
 saveBusiness(business);

 return business;
};

export const loadBusinesses = ({ state, commit }) => {
 // loads businesses if they're not already loaded
 if (!state.businesses || Object.keys(state.businesses).length === 0) {
 return fetchBusinesses().then((res) => {
 commit('LOAD_BUSINESSES', res);
 });
 }
};

export const deleteBusiness = ({ commit }, data) => {
 commit('DELETE_BUSINESS', { business: data });
 deleteBusinessFromAPI(data);
};```

```javascript export default { getBusinessSelectList: (state, getters) => { return state.businesses && Object.keys(state.businesses).length > 0 ? Object.values(state.businesses) : []; }, getBusinessById: (state, getters) => (businessId) => { return state.businesses && businessId in state.businesses ? state.businesses[businessId] : false; } };

import localforage from 'localforage';
import { processAPIData } from '../../utils';

const TRANSACTION_NAMESPACE = 'TRANSACTION-';
const BUSINESS_NAMESPACE = 'BUSINESS-';

export const fetchTransactions = () => {
 return localforage.startsWith(TRANSACTION_NAMESPACE).then((res) => {
 return processAPIData(res);
 });
};

export const saveTransaction = (transaction) => {
 return localforage.setItem(
 TRANSACTION_NAMESPACE + transaction.id,
 transaction
 ).then((value) => {
 return value;
 }).catch((err) => {
 console.log('he\'s dead, jim, the transaction is dead', err);
 });
};

export const deleteTransaction = (transaction) => {
 return localforage.removeItem(
 TRANSACTION_NAMESPACE + transaction.id
 ).then(() => {
 return true;
 }).catch((err) => {
 console.log(err);
 return false;
 });
};

export const fetchBusinesses = () => {
 return localforage.startsWith(BUSINESS_NAMESPACE).then((res) => {
 return processAPIData(res);
 });
};

export const saveBusiness = (business) => {
 return localforage.setItem(
 BUSINESS_NAMESPACE + business.id,
 business
 ).then((value) => {
 return value;
 });
};

export const deleteBusiness = (business) => {
 return localforage.removeItem(
 BUSINESS_NAMESPACE + business.id
 ).then(() => {
 return true;
 }).catch((err) => {
 console.log(err);
 return false;
 });
};
// tie together all the pieces we just coded
import * as actions from './actions';
import mutations from './mutations';
import getters from './getters';

const state = {
 transactions: [],
 businesses: []
};

export default {
 state,
 actions,
 mutations,
 getters
};

That's a big update with a lot of things happening, especially in the transaction actions file. Read through all the different pieces to ensure you understand what's going on.

The prepareTransaction function is shared by both createTransaction and updateTransaction. It is preparing the data sent by the frontend to be saved into vuex storage. First, we locate the apprioriate budget based on the date the user selected. Then we look for the selected budgetCategory for that month. This is then thrown back to the calling method. You'll notice that we dispatch some additional actions in saveTransaction, but we haven't yet accounted for all of the edits a user can make in updateTransaction. At some point we'll have to flesh out that code to ensure everything stays up to date.

List View

Our list view will be very simple - for now. Remember this is the key part of the application, the section that the user will return to every day. At some point it will grow unwieldy and we will have to let the user filter and sort transactions. Moreso, once we reach a certain number of transactions we might run into performance problems. Loading and displaying possibly thousands of transactions is something Vue should be able to handle, but why would we make the user wait on transactions from 3 years prior to load? Studies have shown that people are very sensitive to load times even in the milliseconds range, so it is an important consideration. But we're getting ahead of ourselves - keep it minimal!

Right now we simply need to show a list of transactions. Since the user will edit these inline, we are going to use a component for each transaction in the list and dynamically swap it out for a CreateUpdate component when the user clicks to edit.

// src/app/transactions/components/TransactionsList.vue

<template>
 <div id="transactions-list">
 <nav class="level">
 <div class="level-left">
 <h1 class="title is-2">Transactions</h1>
 </div>
 </nav>

 <table class="table is-bordered">
 <thead>
 <tr>
 <th>Date</th>
 <th>Business</th>
 <th>Category</th>
 <th>Account</th>
 <th>Note</th>
 <th>Debit</th>
 <th>Credit</th>
 <th></th>
 </tr>
 </thead>
 <tbody>
 <template
 v-for="transaction, key in sortedTransactions"
 :class="{ 'is-delinquent': false }"
 >
 <component
 :is="transactionComponent(transaction)"
 v-model="transaction"
 v-on:updated-transaction="activeTransaction = null"
 v-on:edit-transaction="activeTransaction = transaction"
 ></component>
 </template>
 <CreateUpdateTransaction></CreateUpdateTransaction>
 </tbody>
 </table>
 </div>
</template>

<script>
import { mapActions, mapState } from 'vuex';

import CreateUpdateTransaction from './CreateUpdateTransaction';
import Transaction from './Transaction';
import { sortObjects } from '../../../utils';

export default {
 name: 'transactions-list',

 components: {
 Transaction,
 CreateUpdateTransaction
 },

 data () {
 return {
 activeTransaction: null
 };
 },

 mounted () {
 this.loadTransactions();
 },

 methods: {
 ...mapActions([
 'createTransaction',
 'updateTransaction',
 'loadTransactions'
 ]),

 transactionComponent (transaction) {
 if (this.activeTransaction && this.activeTransaction === transaction) {
 return 'CreateUpdateTransaction';
 }
 return 'Transaction';
 }
 },

 computed: {
 ...mapState({
 'transactions': state => state.transactions.transactions
 }),

 sortedTransactions () {
 return sortObjects(this.transactions, 'date', true); // sort in date order, oldest to newest
 }
 }
};
</script>

<style scoped lang='scss'>
#transactions-list {
}
</style>

Did you sort the transactions in reverse date order, so the new ones are at the top? I did at first too. People are used to adding items to the bottom of a list or a spreadsheet, so it makes more sense to do in ascending date order. This means we'll eventually want to scroll to the bottom of this transactions page automatically, so the newest transactions and the add transaction row are in view by default. Writing that one down in my programmer's notebook!

We're listening to two events on our dynamic transaction component. edit-transaction will be sent from Transaction to indicate that we want to edit something. When the editing is done, CreateUpdateTransaction will fire off the updated-transaction event so we can switch back to view mode.

Hey! In budgets, didn't we send the save/update signals up to the list view and save them from there? Why aren't we doing that here in transactions? Great question! Take a look at the data model for the two modules and see if you can come up with the answer. Each budgetCategory was a child of a budget object. We decided this model made the most sense, as the user would primarily be looking at categories on a monthly basis. This required us to save the entire budget at once. A transaction is an independent object, and not a child, so there is no parent to save here and the object can take care of itself.

Viewing & Editing Transactions

We need two components - one to simply view a transaction as part of the list, and another to create/update the transaction. When editing a transaction, the user also needs to be able to create a new business object. The only information we need about the business is its name. This is simply a placeholder to log where the user is spending their hard earned cash.

This component will do a lot of lookups! It essentially ties together everything we have created so far - budgets, categories, accounts, and businesses. This would be another place to do work on some performance improvements in the future. For now we will load just about every piece of data the application has stored in the database.

Since the transaction is the linchpin, it also causes a ripple effect throughout the application. When a user spends $100 on groceries using their credit card, the application needs to

  • deduct $100 from their credit card account
  • figure out what budget needs to be updated based on the date of the transaction
  • show $100 as spent in the groceries category for this month's budget
  • ensure that budget totals are also updated

That's a lot for one tiny create/update component to think about. If you are thinking that much of that business logic should be happening elsewhere, you're right! When I was writing this, I started by writing the component so that it is semi-functional - we could add and edit transactions, but they didn't affect the rest of the application. I'm presenting it all at once here so there isn't as much back and forth, but check out the commit log if you're getting overwhelmed by so much happening at once.

<template>
 <tr class="transaction">
 <td>
 <span class="subtitle is-5">{{ value.date | moment('YYYY-MM-DD') }}</span>
 </td>
 <td><span class="subtitle is-5">{{ getBusinessById(value.business).name }}</span></td>
 <td><span class="subtitle is-5">{{ getCategoryById(value.category).name }}</span></td>
 <td><span class="subtitle is-5">{{ getAccountById(value.account).name }}</span></td>
 <td><span class="subtitle is-5">{{ value.note }}</span></td>
 <td><span class="subtitle is-5" v-if="value.amount < 0">${{ value.amount }}</span></td>
 <td><span class="subtitle is-5" v-if="value.amount > 0">${{ value.amount }}</span></td>
 <td><a class='button' @click.prevent="$emit('edit-transaction')">Edit</a></td>
 </tr>
</template>

<script>
import { moment } from '../../../filters';
import { mapGetters } from 'vuex';

export default {
 name: 'transaction',

 props: ['value'],

 filters: {
 moment
 },

 computed: {
 ...mapGetters([
 'getCategoryById',
 'getAccountById',
 'getBusinessById'
 ])
 }
};
</script>
<template>
 <tr class="transaction-create-update">
 <td>
 <span class="subtitle is-5">
 <p class="control has-icon has-addons">
 <datepicker name="month" input-class="input" v-model="transaction.date"></datepicker>
 <span class="icon">
 <i class="fa fa-calendar" aria-hidden="true"></i>
 </span>
 </p>
 </span>
 </td>

 <td>
 <multiselect
 :value="transaction.business"
 @input="updateSelection('business', $event)"
 :taggable="true"
 @tag="handleCreateBusiness"
 :options="getBusinessSelectList"
 placeholder="Select a business"
 label="name"
 track-by="id"
 ></multiselect>
 </td>

 <td>
 <multiselect
 :value="transaction.category"
 @input="updateSelection('category', $event)"
 :options="getCategorySelectList"
 placeholder="Select a category"
 label="name"
 track-by="id"
 ></multiselect>
 </td>

 <td>
 <multiselect
 :value="transaction.account"
 @input="updateSelection('account', $event)"
 :options="getAccountSelectList"
 placeholder="Select an account"
 label="name"
 track-by="id"
 ></multiselect>
 </td>

 <td>
 <p class="control">
 <input type="text" class="input" v-model="transaction.note" />
 </p>
 </td>

 <td>
 <p class="control has-icon">
 <input type="number" step="0.01" class="input" v-model="debit" />
 <span class="icon">
 <i class="fa fa-usd" aria-hidden="true"></i>
 </span>
 </p>
 </td>

 <td>
 <p class="control has-icon">
 <input type="number" step="0.01" class="input" v-model="credit" />
 <span class="icon">
 <i class="fa fa-usd" aria-hidden="true"></i>
 </span>
 </p>
 </td>

 <td>
 <a class="button is-primary" @click.prevent="processSave">
 {{ editing ? 'Save' : 'Add' }}
 </a>
 </td>
 </tr>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import Datepicker from 'vuejs-datepicker';
import Multiselect from 'vue-multiselect';

export default {
 name: 'transaction-create-update',

 props: ['value'],

 components: {
 Datepicker,
 Multiselect
 },

 data () {
 return {
 transaction: {},
 debit: null,
 credit: null,
 editing: false
 };
 },

 mounted () {
 this.loadTransactions();
 this.loadBudgets();
 this.loadCategories();
 this.loadAccounts();
 this.loadBusinesses();

 if (this.value) {
 this.transaction = Object.assign({}, this.value);

 // we need the selected category, account, and business name and ID, but the object only holds their IDs by default
 this.transaction.category = this.getCategoryById(this.transaction.category);
 this.transaction.account = this.getAccountById(this.transaction.account);
 this.transaction.business = this.getBusinessById(this.transaction.business);

 if (this.transaction.amount > 0) this.credit = this.transaction.amount;
 else this.debit = this.transaction.amount;

 this.editing = true;
 }
 },

 methods: {
 ...mapActions([
 'loadTransactions',
 'loadCategories',
 'loadAccounts',
 'loadBudgets',
 'createBusiness',
 'createTransaction',
 'updateTransaction',
 'deleteTransaction',
 'loadBusinesses',
 'deleteBusiness'
 ]),

 processSave () {
 if (this.editing) {
 // TODO: I hate this - have to remember to change it if we change how transaction stores data
 // surely there is a better way?
 this.updateTransaction({
 account: this.transaction.account.id,
 amount: this.transaction.amount,
 business: this.transaction.business.id,
 budget: this.transaction.budget,
 category: this.transaction.category.id,
 date: this.transaction.date,
 note: this.transaction.note,
 id: this.transaction.id
 }).then(() => {
 this.$emit('updated-transaction', this.transaction);
 });
 } else {
 this.createTransaction({
 account: this.transaction.account.id,
 amount: this.transaction.amount,
 business: this.transaction.business.id,
 category: this.transaction.category.id,
 note: this.transaction.note,
 date: this.transaction.date
 }).then(() => {
 this.transaction = {};
 });
 }
 },

 updateSelection (name, obj) {
 // if using v-model and not using Vue.set directly, vue-multiselect seems to struggle to properly
 // keep its internal value up to date with the value in our component. So we're skipping v-model
 // and handling updates manually.
 this.$set(this.transaction, name, obj);
 },

 handleCreateBusiness (business) {
 let newBusiness = { name: business };
 this.createBusiness(newBusiness).then((val) => {
 this.updateSelection('business', val);
 });
 }
 },

 computed: {
 ...mapGetters([
 'getCategoryById',
 'getAccountById',
 'getCategorySelectList',
 'getAccountSelectList',
 'getBusinessSelectList',
 'getBusinessById'
 ])
 },

 watch: {
 credit: function (val) {
 this.transaction.amount = Math.abs(val);
 },

 debit: function (val) {
 this.transaction.amount = -Math.abs(val);
 }
 }
};
</script>
export default {
 getAccountById: (state, getters) => (accountId) => {
 return state.accounts && accountId in state.accounts ? state.accounts[accountId] : false;
 },

 getAccountSelectList: (state, getters) => {
 return state.accounts && Object.keys(state.accounts).length > 0 ? Object.values(state.accounts) : [];
 }
};

Note that we added a getter for loading the accounts into a select list.

There are two places in this code where I'm not really satisfied. You probably already noticed it - the processSave() code is rather verbose and unwieldy. I left in a personal TODO note that I made while coding this. (I also added it to my programmer's notebook.) Even worse than being verbose and repetitive, the code is likely to break in the future if we make any changes to transactions but forget to come edit this section. I will fix this issue in a later release with my own code, but in the meantime, see what you can come up with.

The second spot is the watcher I set for credit and debit. This code ensures that a value entered in the debit field is always negative, and the credit field is always positive. When I mapped out the application, I didn't give enough thought to how this approach would actually work, with a separate field for credit and debit. Is having two fields really better than letting the user add their own negative sign in the field? Remember that the user will be entering many more debits than they will credits, so the chances for a forgotten negative sign are pretty high. If it's better for the user, then we should be able to find a programmatic way to make it work. This is what we've done, but it is not very elegant yet.

If you haven't committed yet, fix that now!

Before we move on, let's add to our routes and navigation so this section can start to become usable.

import * as components from './components';

export default [
 {
 path: '/transactions',
 name: 'transactionsList',
 component: components.TransactionsListView
 }
];
...

 <li><router-link :to="{ name: 'budgetsList' }">Budgets</router-link></li>
 <li><router-link :to="{ name: 'transactionsList' }">Transactions</router-link></li>

...

Updating Data

Remember way back to step 4 in this tutorial when I espoused the benefits of vuex? Me either. But we're about to see it in action. Vuex is the central hub where all of our data manipulation occurs. Nothing is allowed to change the data unless it goes through the proper channels - triggering an action, which commits a mutation, which updates the central store, which is reflected back through the rest of the application.

Earlier we identified the various dominoes that need to fall when we create or update a transaction. Here they are again, as we'll work through them one at a time.

  • figure out what budget needs to be updated based on the date of the transaction
  • update the transaction amount spent in the selected category for this month's budget
  • deduct the amount from the selected account (or add to it)
  • ensure that budget totals are also updated

The first step is to find a budget based on a date. This is clearly a budget problem and not a transaction problem, and it seems to me like a good case for a getter. This simply iterates through the existing budgets to find the one that matches a given date. If you look back into the actions.js file we wrote, you may also notice we called a getBudgetCategoryByBudgetAndCategory getter that still needs to be created. If the name isn't descriptive enough to tell you what it does, I don't know what is.

// /src/app/budgets/vuex/getters.js

...

 },

 getBudgetCategoryByBudgetAndCategory: (state, getters) => (budgetId, categoryId) => {
 let budget = getters.getBudgetById(budgetId);
 if (!budget) return false;

 return budget.budgetCategories ? Object.values(budget.budgetCategories).find((o) => { return o.category === categoryId; }) : false;
 },

 getBudgetByDate: (state, getters) => (date) => {
 if (!state.budgets) return false;

 let month = moment(date);
 return Object.values(state.budgets).find((o) => {
 return month.isSame(o.month, 'month'); // remember this checks month and year are the same https://momentjs.com/docs/#/query/is-same/
 });

Now we need a way to update a budgetCategory's balance, and an account balance.

export const updateBudgetCategorySpent = ({ commit, dispatch, getters }, data) => {
 // expects data.budget, data.budgetCategory, and data.spent
 // spent should always be the amount spent on a transaction, not a total amount
 commit('UPDATE_BUDGET_CATEGORY_BALANCE', { budget: data.budget, value: data.amount, budgetCategory: data.budgetCategory, param: 'spent' });

 dispatch('updateBudgetBalance', {
 budget: data.budget,
 param: 'spent',
 value: data.budget.spent + data.amount
 });

 // save using the budget in our store
 saveBudget(getters.getBudgetById(data.budget.id));
};
...

 UPDATE_BUDGET_CATEGORY_BALANCE (state, payload) {
 if (!(payload['param'] === 'budgeted' || payload['param'] === 'spent')) {
 throw new Error('UPDATE_BUDGET_BALANCE expects either { param: "budgeted" } or { param: "spent" }');
 }

 state.budgets[payload.budget.id].budgetCategories[payload.budgetCategory.id][payload.param] += parseFloat(payload.value);
 }
...

export const updateAccountBalance = ({ commit, getters }, data) => {
 /*
 Accepts a transaction amount and sums that with the current balance
 account: account
 amount: num
 */
 commit('UPDATE_ACCOUNT_BALANCE', data);
 saveAccount(getters.getAccountById(data.account.id)); // save the updated account
};
...

 UPDATE_ACCOUNT_BALANCE (state, payload) {
 state.accounts[payload.account.id].balance += parseFloat(payload.amount);
 },

...

One final thing to hit on here. As you are looking through the mutations code for accounts and budgets, do you notice something wrong? We are never forcing floats for our currency data! We wanted to keep it simple at first, but really, this is something we should have done from the start. Now your test database is probably filled with all sorts of misrepresented numbers. We better fix this right away.

There are a lot of changes throughout the mutations file, so the whole thing will be listed here. Look for all the times we force floats throughout the code here.

import Vue from 'vue';

const forceBudgetFloats = (o) => {
 o.budgeted = parseFloat(o.budgeted);
 o.income = parseFloat(o.income);
 o.spent = parseFloat(o.spent);

 if (o.budgetCategories && Object.keys(o.budgetCategories).length > 0) {
 Object.values(o.budgetCategories).forEach((bc) => { forceBudgetCategoryFloats(bc); });
 }
};

const forceBudgetCategoryFloats = (o) => {
 o.budgeted = parseFloat(o.budgeted);
 o.spent = parseFloat(o.spent);
};

export default {
 CREATE_BUDGET (state, payload) {
 forceBudgetFloats(payload.budget);
 state.budgets[payload.budget.id] = payload.budget;
 },

 UPDATE_BUDGET (state, payload) {
 forceBudgetFloats(payload.budget);
 state.budgets[payload.budget.id] = payload.budget;
 },

 LOAD_BUDGETS (state, payload) {
 state.budgets = payload;

 Object.values(state.budgets).forEach((o) => {
 forceBudgetFloats(o);
 });
 },

 UPDATE_BUDGET_BALANCE (state, payload) {
 if (!(payload['param'] === 'budgeted' || payload['param'] === 'spent') || payload['param'] === 'income') {
 throw new Error('UPDATE_BUDGET_BALANCE expects either { param: "budgeted" } or { param: "spent" } or { param: "income" }');
 }

 state.budgets[payload.budget.id][payload.param] += parseFloat(payload.value);
 },

 CREATE_CATEGORY (state, payload) {
 Vue.set(state.categories, payload.category.id, payload.category);
 },

 UPDATE_CATEGORY (state, payload) {
 state.categories[payload.category.id] = payload.category;
 },

 LOAD_CATEGORIES (state, payload) {
 state.categories = payload;
 },

 CREATE_EMPTY_BUDGET_CATEGORY_OBJECT (state, payload) {
 Vue.set(state.budgets[payload.id], 'budgetCategories', {});
 },

 CREATE_BUDGET_CATEGORY (state, payload) {
 forceBudgetCategoryFloats(payload.budgetCategory);
 Vue.set(state.budgets[payload.budget.id].budgetCategories, payload.budgetCategory.id, payload.budgetCategory);
 },

 UPDATE_BUDGET_CATEGORY (state, payload) {
 forceBudgetCategoryFloats(payload.budgetCategory);
 state.budgets[payload.budget.id].budgetCategories[payload.budgetCategory.id] = payload.budgetCategory;
 },

 UPDATE_BUDGET_CATEGORY_BALANCE (state, payload) {
 if (!(payload['param'] === 'budgeted' || payload['param'] === 'spent')) {
 throw new Error('UPDATE_BUDGET_BALANCE expects either { param: "budgeted" } or { param: "spent" }');
 }

 state.budgets[payload.budget.id].budgetCategories[payload.budgetCategory.id][payload.param] += parseFloat(payload.value);
 }
};
import Vue from 'vue';

export default {
 CREATE_ACCOUNT (state, payload) {
 payload.account.balance = parseFloat(payload.account.balance);
 state.accounts[payload.account.id] = payload.account;
 },

 UPDATE_ACCOUNT (state, payload) {
 payload.account.balance = parseFloat(payload.account.balance);
 state.accounts[payload.account.id] = payload.account;
 },

 UPDATE_ACCOUNT_BALANCE (state, payload) {
 state.accounts[payload.account.id].balance += parseFloat(payload.amount);
 },

 DELETE_ACCOUNT (state, payload) {
 Vue.delete(state.accounts, payload.account.id);
 },

 LOAD_ACCOUNTS (state, payload) {
 state.accounts = payload;

 Object.values(state.accounts).forEach((o) => { o.balance = parseFloat(o.balance); });
 }
};

I don't know about you, but I'm out of breath at this point. Better commit.

572a573

Originally published on

Last updated on