summaryrefslogtreecommitdiff
path: root/postgresqleu/transferwise/api.py
diff options
context:
space:
mode:
authorMagnus Hagander2019-03-27 14:40:33 +0000
committerMagnus Hagander2019-03-27 14:46:19 +0000
commitd59eb7566d27cbf7b19095f1d1afcec154304412 (patch)
treebd3d69494aae2eb28a5fdf2edbdbfa232521bcff /postgresqleu/transferwise/api.py
parent034c89afde2cb7625fa0ae71fac6c7c60524ed48 (diff)
Add payment provider for TransferWise
This uses the TransferWise REST API to get access to an IBAN account, allowing "traditional" bank paid invoices to be reasonably automated. The provider integrates with the "managed bank transfer" system, thereby handling automated payments using the payment reference. Since this reference is created by us it can be printed on the invoice, making it easier to deal with in traditional corporate environments. Payments that are incorrect in either amount or payment reference will now also show up in the regular "pending bank transactions" view and can be processed manually as necessary. For most SEPA transfers, TransferWise will be able to provide the IBAN number to the sending account. When this is the case, the provider also supports refunds, that will be issued as general IBAN transfers to tihs account. Note that refunds requires the API token to have "full access" as it's permissions in the TW system, meaning it can make arbitrary transfers of any funds. There is no way to specifically tie it to just refunds, as these are just transfers and not payments.
Diffstat (limited to 'postgresqleu/transferwise/api.py')
-rw-r--r--postgresqleu/transferwise/api.py150
1 files changed, 150 insertions, 0 deletions
diff --git a/postgresqleu/transferwise/api.py b/postgresqleu/transferwise/api.py
new file mode 100644
index 00000000..1f26486e
--- /dev/null
+++ b/postgresqleu/transferwise/api.py
@@ -0,0 +1,150 @@
+from django.conf import settings
+
+import requests
+from datetime import datetime, timedelta
+from decimal import Decimal
+import uuid
+
+from .models import TransferwiseRefund
+
+
+class TransferwiseApi(object):
+ def __init__(self, pm):
+ self.pm = pm
+ self.session = requests.session()
+ self.session.headers.update({
+ 'Authorization': 'Bearer {}'.format(self.pm.config('apikey')),
+ })
+
+ self.profile = self.account = None
+
+ def format_date(self, dt):
+ return dt.strftime('%Y-%m-%dT00:00:00.000Z')
+
+ def parse_datetime(self, s):
+ return datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ')
+
+ def get(self, suburl, params=None):
+ r = self.session.get(
+ 'https://api.transferwise.com/v1/{}'.format(suburl),
+ params=params,
+ )
+ if r.status_code != 200:
+ r.raise_for_status()
+ return r.json()
+
+ def post(self, suburl, params):
+ r = self.session.post(
+ 'https://api.transferwise.com/v1/{}'.format(suburl),
+ json=params,
+ )
+ r.raise_for_status()
+ return r.json()
+
+ def get_profile(self):
+ if not self.profile:
+ try:
+ self.profile = next((p['id'] for p in self.get('profiles') if p['type'] == 'business'))
+ except Exception as e:
+ raise Exception("Failed to get profile: {}".format(e))
+ pass
+ return self.profile
+
+ def get_account(self):
+ if not self.account:
+ try:
+ self.account = next((a for a in self.get('borderless-accounts', {'profileId': self.get_profile()}) if a['balances'][0]['currency'] == settings.CURRENCY_ABBREV))
+ except Exception as e:
+ raise Exception("Failed to get account: {}".format(e))
+ return self.account
+
+ def get_balance(self):
+ for b in self.get_account()['balances']:
+ if b['currency'] == settings.CURRENCY_ABBREV:
+ return b['amount']['value']
+ return None
+
+ def get_transactions(self, startdate=None, enddate=None):
+ if not enddate:
+ enddate = (datetime.today() + timedelta(days=1)).date()
+
+ if not startdate:
+ startdate = enddate - timedelta(days=60)
+
+ return self.get(
+ 'borderless-accounts/{0}/statement.json'.format(self.get_account()['id']),
+ {
+ 'currency': settings.CURRENCY_ABBREV,
+ 'intervalStart': self.format_date(startdate),
+ 'intervalEnd': self.format_date(enddate),
+ },
+ )['transactions']
+
+ def validate_iban(self, iban):
+ return self.get('validators/iban?iban={}'.format(iban))['validation'] == 'success'
+
+ def get_structured_amount(self, amount):
+ if amount['currency'] != settings.CURRENCY_ABBREV:
+ raise Exception("Invalid currency {} found, exepcted {}".format(amount['currency'], settings.CURRENCY_ABBREV))
+ return Decimal(amount['value']).quantize(Decimal('0.01'))
+
+ def refund_transaction(self, origtrans, refundid, refundamount, refundstr):
+ if not origtrans.counterpart_valid_iban:
+ raise Exception("Cannot refund transaction without valid counterpart IBAN!")
+
+ # This is a many-step process, unfortunately complicated.
+ twr = TransferwiseRefund(origtransaction=origtrans, uuid=uuid.uuid4(), refundid=refundid)
+
+ # Create a recipient account
+ acc = self.post(
+ 'accounts',
+ {
+ 'profile': self.get_profile(),
+ 'currency': settings.CURRENCY_ABBREV,
+ 'accountHolderName': origtrans.counterpart_name,
+ 'type': 'iban',
+ 'details': {
+ 'IBAN': origtrans.counterpart_account,
+ },
+ }
+ )
+ twr.accid = acc['id']
+
+ # Create a quote (even though we're not doing currency exchange)
+ quote = self.post(
+ 'quotes',
+ {
+ 'profile': self.get_profile(),
+ 'source': settings.CURRENCY_ABBREV,
+ 'target': settings.CURRENCY_ABBREV,
+ 'rateType': 'FIXED',
+ 'targetAmount': refundamount,
+ 'type': 'BALANCE_PAYOUT',
+ },
+ )
+ twr.quoteid = quote['id']
+
+ # Create the actual transfer
+ transfer = self.post(
+ 'transfers',
+ {
+ 'targetAccount': twr.accid,
+ 'quote': twr.quoteid,
+ 'customerTransactionId': str(twr.uuid),
+ 'details': {
+ 'reference': refundstr,
+ },
+ },
+ )
+ twr.transferid = transfer['id']
+ twr.save()
+
+ # Fund the transfer from our account
+ fund = self.post(
+ 'transfers/{0}/payments'.format(twr.transferid),
+ {
+ 'type': 'BALANCE',
+ },
+ )
+
+ return twr.id