diff options
author | Magnus Hagander | 2019-03-27 14:40:33 +0000 |
---|---|---|
committer | Magnus Hagander | 2019-03-27 14:46:19 +0000 |
commit | d59eb7566d27cbf7b19095f1d1afcec154304412 (patch) | |
tree | bd3d69494aae2eb28a5fdf2edbdbfa232521bcff /postgresqleu/transferwise/api.py | |
parent | 034c89afde2cb7625fa0ae71fac6c7c60524ed48 (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.py | 150 |
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 |