taxamo

Taxamo python bindings


License
Apache-2.0
Install
pip install taxamo==0.0.18

Documentation

Taxamo Python bindings

Documentation

Connecting to Taxamo from Python

This project provides Python bindings to Taxamo's RESTful API, generated by Swagger-codegen.

The project is also used to report Taxamo Python API related issues: https://github.com/taxamo/taxamo-python/issues.

Also consult the regression tests for example usage of the API.

Installation

Taxamo's Python package is available on PyPi, so it can be installed via pip:

pip install taxamo

It is also possible to clone https://github.com/taxamo/taxamo-python GitHub repository:

git clone https://github.com/taxamo/taxamo-python

And then install the bindings using setup.py, for example by running the following command from taxamo-python directory:

sudo python setup.py install

Example - Stripe & Flask

This guide enhances the Stripe’s Flask guide to support VAT calculation and transaction confirmation with Taxamo’s RESTful API and to serve as an example for integration with Python apps - not necessarily using Stripe or Python.

The complete source codes for this example are located in the flask-stripe-example subdirectory of the taxamo-python repository.

This example assumes that some details for a customer are already known/set – the billing country and a customer’s name.

As Stripe and Flask are already installed in the original example, we just need to install the Taxamo API client Python bindings as described in previous section.

Setting up Taxamo API

To access the Taxamo RESTful API from Python, we need to import the appropriate packages in the app.pyfile:

import taxamo.api
import taxamo.swagger
import taxamo.error

Initialize the API client:

taxamo_api = taxamo.api.ApiApi(
    taxamo.swagger.ApiClient(apiKey=os.environ['TAXAMO_PRIVATE_TOKEN'],
                             apiServer='https://api.taxamo.com'))

We’re using the TAXAMO_PRIVATE_TOKEN environment variable. It is recommended to use the test token for this tutorial.

We will also be using sessions, so in this example we use the cookie-based implementation (please remember to update the secret_key to a different value):

app.secret_key = 'xa7Caxc5|wxf6x9ax9axaexa6x87xcexf8xa8x82xd7xeax96Kzx9axf4xae'

Pre-calculating tax

Before we proceed with the transaction, we should present the actual price to the customer. We can use the customer’s IP address to resolve their country, but we need to provide them with ability to alter that information.

To do so, we need to enhance our index function in app.py to support that:

@app.route('/')
def index():
    #simplify billing country information
    if 'billing_country_code' not in session:
        ip_data = taxamo_api.locateGivenIP(request.remote_addr)
        session['billing_country_code'] = ip_data.country_code
        
    tax_resp = taxamo_api.calculateTax({'transaction': {
        'currency_code': 'USD',
        'buyer_ip': request.remote_addr,
        # force country code makes sense only if the customer 
        # will provide additional details later on
        'force_country_code': session['billing_country_code'],
        'billing_country_code': session['billing_country_code'],
        'transaction_lines': [{'amount': 5, 'custom_id': 'line1'}]
    }})
    return render_template('index.html',
                           key=stripe_keys['publishable_key'],
                           total_amount=tax_resp.transaction.total_amount,
                           tax_rate=tax_resp.transaction.transaction_lines[0].tax_rate,
                           billing_country_code=session['billing_country_code'],
                           ip_country_code=tax_resp.transaction.countries.by_ip.code,
                           tax_country_code=tax_resp.transaction.tax_country_code,
                           countries=taxamo_api.getCountriesDict().dictionary)

First, if the billing country is not set, we base it on the IP address using taxamo_api.locateGivenIP. Next we calculate tax, forcing the customer’s billing country for a VAT rate as we don’t know the credit card number BIN/country of issue.

We can also present the customer with the tax rate, the detected country, and the ability to update the billing country to something else in templates/index.html:

{% extends "layout.html" %}
{% block content %}
<form action="/set_country" method="POST">
    <article>
        <label>
            <span>Amount is <b>${{ format_price(total_amount) }}</b></span>,

            {% if tax_rate %}
               <span>VAT rate is <b>{{ format_price(tax_rate) }}%</b></span>,
            {% endif %}
            <span>Detected country is: {{ ip_country_code }}</span>,
            {% if tax_rate %}
                <span>TAX country is: {{ tax_country_code }}</span>,
            {% endif %}
            <span>Billing country is:
                <select name="billing_country_code">
                    {% for country in countries %}
                        <option value="{{ country.code }}"
                                {% if country.code == billing_country_code %}
                                    selected="1"
                                {% endif %}
                                >{{ country.code }} - {{ country.name }}</option>
                    {% endfor %}
                </select>
                <button type="submit">Update</button>
            </span>

            {% if billing_country_code != ip_country_code %}
                <h5>Warning</h5>
                <ul>
                    <li>Billing country code set to: <b>{{ billing_country_code }}</b></li>
                    <li>Computer's country code detected as from: <b>{{ ip_country_code }}</b></li>
                </ul>
                <p>Tax calculated for:  <b>{{ billing_country_code }}</b>, but credit card issued in <b></b> will need to be used.</p>
            {% endif %}
        </label>

    </article>
</form>


<form action="/charge" method="post">
    <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
            data-key="{{ key }}"
            data-description="A Flask Charge"
            data-amount="{{ format_int(total_amount * 100)}}"></script>
</form>
{% endblock %}

As we have allowed the customer to update their billing country on the payment form, we need to add an appropriate controller to app.py:

@app.route('/set_country', methods=['POST'])
def set_country():
    session['billing_country_code'] = request.form['billing_country_code']
    return redirect('/')

We also need a processor for the proper formatting of numbers in our templates. Let’s just add it to app.py:

@app.context_processor
def utility_processor():
    def format_price(amount):
        if amount:
            return u'{0:.2f}'.format(amount)
        else:
            return None
    def format_int(amount):
        if amount:
            return u'{0:.0f}'.format(amount)
        else:
            return None
    return dict(format_price=format_price, format_int=format_int)

Payment and Taxamo transaction storage

Once the payment has been initiated by the user and Stripe.js has collected credit card details.

To do so, we need to enhance the charge() controller to contact Taxamo to validate the tax evidence by (1) storing a transaction and (2) once the payment is accepted by Stripe confirm the transaction in Taxamo to signal that the payment succeeded so that it will appear on the settlement.

@app.route('/charge', methods=['POST'])
def charge():
    # Amount in cents
    amount = 500

    token = stripe.Token.retrieve(request.form['stripeToken'])

    #simplify billing country information
    if 'billing_country_code' not in session:
        ip_data = taxamo_api.locateGivenIP(request.remote_addr)
        session['billing_country_code'] = ip_data.country_code

    try:
        resp = taxamo_api.createTransaction({'transaction': {
            'currency_code': 'USD',
            'buyer_ip': request.remote_addr,
            'buyer_name': 'John Doe', #that should be collected from a user
            'evidence': {'by_payment_method': {'evidence_value': token.card.country}},
            'billing_country_code': session['billing_country_code'],
            'transaction_lines': [{'amount': 5, 'custom_id': 'line1'}]
        }})
        amount = resp.transaction.total_amount
        if resp.transaction.tax_country_code != session['billing_country_code']:
            return redirect('/wrong_card?code=' + token.card.country)

    except taxamo.error.ValidationError: #we might need to dive into the error details
        return redirect('/wrong_card?code=' + token.card.country)

    customer = stripe.Customer.create(
        email='customer@example.com',
        card=request.form['stripeToken']
    )

    charge = stripe.Charge.create(
        customer=customer.id,
        amount=int(amount*100),
        currency='usd',
        description='Flask Charge'
    )

    taxamo_api.confirmTransaction(resp.transaction.key, {'transaction': {'invoice_place': 'Someplace'}})

    return render_template('charge.html', amount=int(amount*100))

As we don’t have access to the credit card country earlier in this approach (Stripe.js popup form takes care of credit card number), we need to verify that the new piece of evidence hasn’t changed the VAT country. If the form is embedded in our HTML file, we might choose to read the credit card’s BIN (leading digits) and propagate it to Taxamo usingtaxamo.js.

@app.route('/wrong_card')
def wrong_card():
    return render_template('wrong_card.html', code=request.args.get('code'))

And we need the template wrong_card.html:

{% extends "layout.html" %}
{% block content %}
<h2>Your card was issued in {{ code }}, but other evidence points to different countries.</h2>
<a href="/">Try again</a>
{% endblock %}

Finally, we should display the dynamic amount after the transaction was successful by updating the templates/charge.html file:

{% extends "layout.html" %}
{% block content %}
<h2>Thanks, you paid <strong>$ {{ format_price(amount/100) }}</strong>!</h2>
{% endblock %}

Running the example

With all of the changes applied, we can run the code:

PUBLISHABLE_KEY=pk_test_wlmUwh5iKQSAaesWKFrQX7oj SECRET_KEY=sk_test_F1lO6aFaLncuLd8AAkUEgjBw TAXAMO_PRIVATE_TOKEN='SamplePrivateTestKey1' python app.py

Testing

We commit to being compatible with Python 2.7+, Python 3.1+ and PyPy. We need to test against all of these environments to ensure compatibility.

docker build -t taxamo-python-tox .
docker run --rm -it --name my-tox-app -v "$PWD:/code" -w /code taxamo-python-tox tox