Build a Simple Currency Exchange Service with Django.

Build a Simple Currency Exchange Service with Django


Along with the technology development, the need to migrate, or at least incorporate e-commerce to the business, plays a big role for companies of all sizes nowadays. This new modality aims for the clients’ comfort because they don’t have to go out of their homes to get what they want. Not to mention the impact that COVID had daily life worldwide.

Rates source

Basic Currency Exchange Service

./manage.py startapp currency
GET https://api.exchangeratesapi.io/latest?base=EUR HTTP/1.1{
"rates": {
"CAD": 1.5243,
"HKD": 8.4286,
"ISK": 158.3,
"PHP": 54.545,
"DKK": 7.4556,
"HUF": 353.58,
"CZK": 27.408,
"AUD": 1.6687,
"RON": 4.8353,
"SEK": 10.5843,
"IDR": 16092.26,
"INR": 81.8825,
"BRL": 6.3606,
"RUB": 79.5893,
"HRK": 7.5705,
"JPY": 116.28,
"THB": 34.844,
"CHF": 1.0528,
"SGD": 1.538,
"PLN": 4.5636,
"BGN": 1.9558,
"TRY": 7.5861,
"CNY": 7.7102,
"NOK": 10.938,
"NZD": 1.7983,
"ZAR": 19.919,
"USD": 1.0875,
"MXN": 26.2304,
"ILS": 3.819,
"GBP": 0.88245,
"KRW": 1331.08,
"MYR": 4.704
},
"base": "EUR",
"date": "2020-05-13"
}
  1. The base currency, which is the default currency of our ecommerce.
  2. The converted currency, which is the client’s currency.
  3. The amount to be converted (from base currency to converted currency).

import requests
from django.conf import settings

class CurrencyExchangeService:
def get_rates_from_api(self, base_currency):
url = f'{settings.CURRENCY_RATES_URL}?base={base_currency}'
return requests.get(url).json()

def get_rate(self, base_currency, currency):
return self.get_rates_from_api(base_currency)['rates'][currency]

def convert(self, amount, currency, base_currency):
if base.upper() == convert_currency.upper():
return amount
return round(float(amount) * float(self.get_rate(base.upper(), convert_currency.upper())), 3)

Using django-money

import requests
from django.conf import settings
from djmoney.money import Money


class CurrencyExchangeService:
def get_rates_from_api(self, base_currency):
url = f'{settings.CURRENCY_RATES_URL}?base={base_currency}'
return requests.get(url).json()
def get_rate(self, base_currency, currency):
return self.get_rates_from_api(base_currency)['rates'][currency]

def get_converted_amount(amount, base_currency, converted_currency):
return round(float(amount) * float(self.get_rate(base_currency.upper(), convert_currency.upper())), 3)

def validate_money(money):
if not isinstance(money, Money):
raise Exception('A Money instance must be provided')

def convert(self, money, currency):
self.validate_money(money)
amount = money.amount
base_currency = str(money.currency)
converted_currency = str(currency)
if base_currency.upper() == convert_currency.upper():
return money
return Money(self.get_converted_amount(amount, base_currency, converted_currency), converted_currency)
  1. Instead of receiving amount, base currency and converted currency, we’ll now receive just a money object and the currency. From the money object, we can extract the amount and the base currency.
  2. In order to do what we explained in the previous item, we must make sure that the money parameter is actually an instance of the Money class. That’s why we validate it at the beginning and raise an exception if it doesn’t satisfy our requirements.
  3. The currency parameter can be either a string or a currency object (from django-money), but to use it to access the rates response, we have to make sure that all currencies are casted to strings.
  4. Instead of returning an amount, we return a money object now, which is more cohesive with the new implementation.

Caching results

version: "3.1"
services:
redis:
image: redis:6.0-rc1-alpine
restart: always
ports:
- "6379:6379"
CACHES = {
"default": {
"BACKEND": "redis_cache.cache.RedisCache",
"LOCATION": "redis://redis:6379/1",
'TIMEOUT': 86400,
"OPTIONS": {
"CLIENT_CLASS": "redis_cache.client.DefaultClient",
}
}
}
import requests
from django.conf import settings
from django.core.cache import cache
from djmoney.money import Money


class CurrencyExchangeService:

def get_rates_from_api(self, base_currency):
url = f'{settings.CURRENCY_RATES_URL}?base={base_currency}'
return requests.get(url).json()

def get_key(self, base_currency):
return f'currencies-{base_currency}'

def get_all_rates(self, base_currency):
key = self.get_key(base_currency)
return cache.get_or_set(key, lambda: self.get_rates_from_api(base_currency))

def get_rate(self, base_currency, currency):
return self.get_all_rates(base_currency)['rates'][currency]

def get_converted_amount(amount, base_currency, converted_currency):
return round(float(amount) * float(self.get_rate(base_currency.upper(), convert_currency.upper())), 3)

def validate_money(money):
if not isinstance(money, Money):
raise Exception('A Money instance must be provided')

def convert(self, money, currency):
self.validate_money(money)
amount = money.amount
base_currency = str(money.currency)
converted_currency = str(currency)

if base_currency.upper() == convert_currency.upper():
return money
return Money(self.get_converted_amount(amount, base_currency, converted_currency), converted_currency)

Last but not least: testing

import responses
from django.conf import settings
from django.test import override_settings
from django.core.cache import cache
from djmoney.money import Money
from currency.currency_exchange_service import CurrencyExchangeService

EUR_BASE_RESPONSE = {
"rates": {
"CHF": 1.0715,
"USD": 1.1003,
"GBP": 0.84835
},
"base": "EUR",
"date": "2020-02-06"
}
@override_settings(CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
)


class CurrencyExchangeServiceTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.exchanger = CurrencyExchangeService()

def tearDown(self):
cache.clear()

@responses.activate
def test_convert_from_eur_to_gbp(self):
responses.add(responses.GET, f'{settings.CURRENCY_RATES_URL}?base=EUR',
json=EUR_BASE_RESPONSE, status=200)
self.assertEquals(self.exchanger.convert(Money(15, 'EUR'), 'GBP').amount, 12.725)

@responses.activate
def test_convert_use_cache(self):
responses.add(responses.GET, f'{settings.CURRENCY_RATES_URL}?base=EUR',
json=EUR_BASE_RESPONSE, status=200)
self.assertEquals(self.exchanger.convert(Money(15, 'EUR'), 'GBP').amount, 12.725)
self.assertEquals(len(responses.calls), 1)
self.assertEquals(self.exchanger.convert(Money(20, 'EUR'), 'GBP').amount, 16.967)
self.assertEquals(len(responses.calls), 1)
def test_same_currency(self):
self.assertEquals(self.exchanger.convert(Money(15, 'EUR'), 'EUR').amount, 15)

Caching in tests


@override_settings(CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
})

Mocking the API calls

@responses.activate

Summing up

Previous Post Next Post

Contact Form