Sales Tax & V.A.T.

May 3rd 2020

This is an excerpt from the Sales Tax and V.A.T chapter in my book Playbook Thirty-nine -- A guide to shipping interactive web apps with minimal tooling.

In this chapter I talk about the complicated nature of taxes, and how to simplify implementation within Ruby on Rails applications. This section has been shortened from the actual chapter.

____________________

For the rest of this chapter we’ll continue with the context of a customer with an online cart who is checking out to purchase a product.

Geolocation

For calculating taxes on internet purchases, it all starts with determining where the user is located. For US Sales Tax you’ll be passing the Country, State, City, and Zipcode. For VAT we need to have the Country, City, and Zipcode. We get this information by passing in the user’s IP address to a geolocation service. I use IP Stack, but there are many others to choose from. 

What’s IP Stack?

IP Stack is a geolocation API service. You give it an IP address, and it’ll return a payload of valuable data that includes the users country, city, and county if in the US. But be forewarned, this isn’t always accurate. I’ve found the state to at least be accurate most of the time, but internet users are using hot spots and other devices that mask the true origin.

Here is an example using IP Stack. I use HTTParty for this to add some syntactical sugar to the GET calls to the API. 

module AppServices

 class GeoFinderService

  include HTTParty

  def initialize(params)
   @ip  			= params[:ip]
  end

  def call
   gfs_get_data if @ip.present?
  end

  private

   attr_reader :ip

   def gfs_get_data
    result = HTTParty.get("http://api.ipstack.com/#{ip}?access_key=#{gfs_access_key}&format=1")
   rescue HTTParty::Error => e
    OpenStruct.new({success?: false, error: e})
   else
    OpenStruct.new({success?: true, payload: result})
   end

   def gfs_access_key
    ENV['IPSTACK_ACCESS_KEY']
   end

 end

end

This service object is called at the controller level due to the requirement of an IP address. There are a plethora of these services as of writing, but I’ve been using IP Stack and continue to be happy with them. Call the above service with the following: 

AppServices ::GeoFinderService.new({ip:’SOMEIP’}).call 

And here’s what we’ll get back:

<OpenStruct success ?=true, payload=<HTTParty ::Response:0x7fa15f3e16d0> 

The payload is the HTTParty response from IP Stack. Using the data in this payload such as Country, State, and Zipcode, we’ll call yet another service object which calls out to TaxJar to get the tax for the current amount and location.

Tax Calculations

There are quite a few tax calculation APIs available. We use TaxJar. These API’s include features like automatically remitting taxes, but their API’s for calculating US Sales Tax are what we’ll be focusing on today.

For calculating VAT, you can either use vatlayer or just store the countries and the rates yourself (this is the approach I opted for, and I just update them once a month in case any of them have changed). This reduces the number of API calls being made. 

class VatRate < ApplicationRecord

 validates_presence_of :country_code, :rate

 def self.eu_countries
  ["AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GB", "GR", "HU", "HR", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI", "SK"]
 end
end

Another benefit to this approach is being able to determine if the user is coming from somewhere in the EU, based on the country code being returned by IP Stack.

The logic for tax calculations lives in a service object. Its sole purpose is to call the GeoFinderService, and with that payload, call TaxJar to calculate and return tax amounts.

We’re passing into the tax service, the items in the users cart. This is a requirement by TaxJar’s API, to accept a line_items param which includes the array of products being purchased. The reason for this, is that some items have different tax codes. Since we just deal with digital products, we give it the designated tax code of 31000, as directed by TaxJar. 

module AppServices
 class CalculateTaxService

  def initialize(params)
   @taxjar_key    = params[:taxjar_key]
   @items       = params[:items] #object
   @ip_address    = Rails.env.development? ? '5.181.155.255' : params[:ip_address] #nc ip address # test with '65.190.141.7' to trigger sales tax
   @location_data  ||= AppServices::GeoFinderService.new(ip:@ip_address).call
  end

  def call
   if location_data_valid?
    call_taxjar
   else
    OpenStruct.new({success?:false, error: "Location data invalid. Maybe the zipcode is missing?" })
   end
  end


  private

   attr_reader :items, :location_data, :taxjar_key

   def location_data_valid?
    location_data && location_data.success? && location_data.payload.dig('zip').present?
   end

   def call_taxjar
    result ||= TaxjarServices::TaxForOrder.new({order_params:order_params, taxjar_key: taxjar_key}).call
    if result && result.success?
     OpenStruct.new({success?:true, payload: result.payload})
    else
     OpenStruct.new({success?:false, error: result.error})
    end
   end

   def order_params
    {
     shipping:0,
     to_country: location_data.payload['country_code'],
     to_state: location_data.payload['region_code'],
     to_zip: location_data.payload['zip'],
     line_items: line_items
    }
   end

   # todo account for sales and variants
   def line_items
    items.map{ |item| { product_tax_code:'31000', unit_price: (item.product.price_cents / 100.00) } }
   end

 end
end

The TaxJarServices ::TaxForOrder calls TaxJar’s API directly. If you recall the Service Objects chapter, this highlights the importance of service objects performing a single responsibility, and why the actual API calls are also in their own service objects. 

____________________

Purchase your copy of Playbook Thirty-nine today, and continue reading the rest of this chapter!

Get the latest

Sign up with your email address and get the latest, straight to your inbox.