Coding Journal

Jordan Vidrine

Daily Coding Journal

A Self-taught coder from the Cajun Prairie in rural Louisiana, currently working as a designer for Discourse.

I am always trying to better myself, and do so through learning new skills, trying out new routines, traveling, and pouring energy into hobbies. Since 2014, I have been recording, editing, and producing my own music under the name Skies Speak.

July 30th & 31st 2019

Since I was so interested in making my idea work, I spent this week diving into finishing my simple Budgeting/Text Messaging app instead of my regular developer course work I normally do. For today’s post I will give you an overview of the project and what I did to accomplish the goals I had when I set out.

Project Goals

Even with the YNAB app being well designed and easy to use, entering small and basic transactions can be done in an easier way in my opinion. I set out to save time by not having to log into the YNAB app, navigate to the appropriate budget account, and input the transaction. The two main things I wanted to be able to do were:

1. Get the amount left in a budget category by sms text

2. Enter transactions by sms text

Getting Started

After researching a bit I decided to use the Twilio service to serve as the SMS interaction between the express routes I would create to interact with the YNAB data. I went through the basic walkthrough on their site to get familiar with the service, but decided I would build the app and test it via Postman before incorporating the Twilio service.

One big reason for this is that Twilio gives you a free trial credit of $15.00 that slowly gets used up when sending and receiving messages. I knew I’d be sending ALOT of back and forth during the testing so I didnt want to dwindle this balance.

Setting up the Express App

This for me is getting to the point where it is very straightforward and the syntax is finally starting to look normal and make sense.

The gist of what’s happening here is that I am telling express to use the bodyParser (needed for the Twilio Integration), express.json() method, and lastly (at the bottom of the code, but before the app.listen() call) to use my custom error handler. I also tell express to use my smsParse middleware when recieving POST requests. The last line tells the express app to be listening on port 1337 of my computer, aka ‘localhost’.

const express = require('express');
const MessagingResponse = require('twilio').twiml.MessagingResponse;
const bodyParser = require('body-parser');
const request = require('request')
const smsParse = require('./middleware/smsParse')
let handleError = require('./middleware/handleError')

const app = express();
app.use(bodyParser.urlencoded({extended:false}));
app.use(express.json())

app.post('/', smsParse, (req,res) => {
  // Boiler plate formatting to send a text message.
  // res.message has the message saved to it from the smsParse middleware
  // we will be sending that to the user who sent an SMS to the number
  // Twilio has provided us
  let twiml = new MessagingResponse();
  twiml.message(res.message);
  res.writeHead(200,{'Content-Type': 'text/xml'});
  res.end(twiml.toString())
})

app.use(handleError)

app.listen(1337, () => {
  console.log('Express server listening on port 1337')
})

Middleware

The following are the syntax examples of how a user can interact with the app. The first part is what is sent by the user to the number Twilio provides and the second part is the response that would be received.

To get budget data
Groceries // You have $50.41 left in your Dining Out budget.
Enter a Purchase
Groceries -12.34 Wal-Mart // Your transaction with Wal Mart in the amount of $-12.34 was successfully entered.
Enter a Refund
// Enter Credit: Groceries 10.00 Wal-Mart » Your transaction with Wal Mart in the amount of $10.00 was successfully entered.

I setup this middleware to be able to parse the body of the request and determine if the user wants to inset a transaction, or get their budget amount.

const getCategory = require('../helpers/getCategory')
const ynabActions = require('../helpers/ynabActions')

smsParse = async function(req,res,next) {
  let numOfArgs = req.body.Body.split(' ').length;

  // checks if arguments exist
  if (numOfArgs) {
    let category = req.body.Body.split(' ')[0].replace('-',' ');

    // Only passing in one argument retrieves the balance of that argument/category
    if (numOfArgs === 1) {
      try {
        let data = await ynabActions.getBalance(category);
        res.message = `You have ${data.balance} left in your ${data.budget} budget.`
        next();
      } catch(e) {
        next(e);
      }

    // To Cred or Deb an acct, 3 arguments must be passed
    } else if (numOfArgs === 3) {
      let args = [...req.body.Body.split(' ')]
      let cat = args[0].replace('-',' ');

      // converting amt to miliunits (number passed MUST in have two decimal places >> 2.33, -23.45)
      args[1] = args[1].replace('.','') + '0'
      let amt = Number(args[1]);
      let payee = args[2].replace('-',' ');

      // checks the argument types, must be correct to continue on
      if (typeof cat === 'string' && typeof amt === 'number' && typeof payee === 'string') {
        try {
          let transactionData = await ynabActions.addTransaction(category,amt,payee)
          // saves the response to res.message to be passed back to the user
          res.message = transactionData.successMsg;

          next()
        } catch(e) {
          next(e);
        }
      }
    } else {
      // Syntax Error Message
      res.status(500).send({Error:'Syntax Error!\nFor Balance Inquiry >> Category \n For Purchase >> Category, -Purchase Amt, Payee\n For Refund >> Category, Refund Amt, Payee' });
    }
  }
}

module.exports = smsParse

YNAB Helper Functions

I consolidated the YNAB interaction functions to an object with methods inside of it. All of the YNAB interaction happens inside the following script. They interact with that API through the YNAB npm package, rather than making http requests on my own. It is also at this time that I learned about the dotenv NPM package. This is used to store secret keys, passwords, and auth tokens for use in the node.js code. Thanks to Dan at the Acadiana Software Group’s slack page for that!

require('dotenv').config()
const ynab = require('ynab');
const ynabAPI = new ynab.API(process.env.YNAB_ACCESS_TOKEN)
const getCategory = require('./getCategory')

module.exports = {
  async getBalance(category) {
    try {
      let categoryData = await this.getCategoryData(category)
      let balance = `$` + ynab.utils.convertMilliUnitsToCurrencyAmount(categoryData.balance,2);
      // add trailing 0 to any number with one decimal place
      if (balance.includes('.') && balance.split('.')[1].length < 2) { balance = balance+'0'; }
      let budget = categoryData.name;
      return { balance , budget }
    } catch(e) {
      throw(e)
    }
  },

  async addTransaction(category,amt,payee) {
    try {
      let categoryData = await this.getCategoryData(category);
      if (!categoryData) {
        throw new Error('Category not found!')
      }
      // Search to see if payee already exists in the payee database
      let payeesData = await this.getPayee(payee)
      let payee_name;
      // If payee does exist, set payee_name to match name in database so a new payee is not created
      if (payeesData !== undefined) {
        payee_name = payeesData.name;
      // if payee does not exist in database, set the payee name to the one entered
      } else if (payeesData === undefined) {
        payee_name = payee;
      }

      // define transaction object to pass to the ynab api
      let transaction = {
        account_id: process.env.FAKE_BANK_ID,
        amount: amt,
        date: ynab.utils.getCurrentDateInISOFormat(),
        payee_id: null,
        payee_name: payee_name,
        category_id: categoryData.id,
        cleared: null,
        approved: true,
        memo: null,
      };

      let transactionData = await ynabAPI.transactions.createTransaction(process.env.TEST_BUDGET_ID, {transaction});

      return {transactionData, successMsg: `Your transaction with ${transactionData.data.transaction.payee_name} in the amount of ${`$` + ynab.utils.convertMilliUnitsToCurrencyAmount(transactionData.data.transaction.amount,2)} was successfully entered in the ${categoryData.name} category.`};

    } catch(e) {
      // create custom error based on the returned error from the YNAB api
      if (e.error) {
        throw new Error(`${e.error.name}: ${e.error.detail}`)
      }
      throw(e)
    }
  },

  async getCategoryData(category) {
    try {
      let categoriesData = await ynabAPI.categories.getCategories(process.env.TEST_BUDGET_ID);
      // I perform this filter because the API returns an internal master list that doesnt seem necessary to my app
      let filteredData = categoriesData.data.category_groups.filter(cat => cat["name"] !== "Internal Master Category");
      let categoryData = getCategory(filteredData, category)
      return categoryData;
    } catch(e) {
      throw(e)
    }
  },

  async getPayee(payee) {
    try {
      let payees = await ynabAPI.payees.getPayees(process.env.TEST_BUDGET_ID);
      let foundPayee = payees.data.payees.find(item => {return item.name.toLowerCase() === payee.toLowerCase()})
      return foundPayee;
    } catch(e) {
      throw(e)
    }
  }
}

Recursing through the data

One issue I had to tackle was the ability to find the correct Category’s ID, based on what the user inputs into the text. YNAB returns an entire list of categories when you request a budget using the budget ID, but in order to get the specific amount left in a category, you need the category ID and have to make a separate request.

To find the ID, I created a recursive function that would search through category data, and their sub categories until it found the matching category that the user needed. The function returns the category object, which includes data like the amount left in the budget, and other important data points one may be interested in.

let getCategory = function (data, categoryToMatch) {
    let category = undefined;

    function recurse (data,categoryToMatch) {
        for (let i = 0; i < data.length; i++ ) {
                if (data[i].name.toLowerCase() === categoryToMatch.toLowerCase()) {
                  category = data[i];
                  break;
                } else if (data[i].categories !== undefined) {
                  if (data[i].categories.length > 0) {
                     recurse(data[i].categories, categoryToMatch)
                     if (category) {
                         break;
                     }
                  }
                }
            }
    }

    recurse(data,categoryToMatch);
    return category;
}
module.exports = getCategory

Custom Error Handling

Express handles errors automatically pretty well. After encountering small issues with returning the correct error at certain points in the code, I decided to make my own simple error handler.

To do this, you just need to call app.use(YourErrorHandler) at the bottom of the express script. In your error handler you need to pass in err as the first argument, which will be the errors that express has encountered. Here is that code:

function handleError(err,req,res,next) {
  res.send({error:err.message, type: err.name})
}
module.exports = handleError;

Working Example

example-app

Final Thoughts

I really REALLY enjoyed creating and working on this app. The challenge of not working with a browser AT ALL was unique and exciting for me. It helped me to realize how much of our interactions with data and the web are for the most part behind the scenes. It sort of solidified my interest in back-end development over front-end as well. I am interested in hashing this idea out even further to add a bit more complexity to the user interactions, while still keeping it simple enough to not have to go into the YNAB app for every interaction with your budget.

Back to blog...