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!