from django import forms
from django.core.validators import MinValueValidator
from django.contrib import messages
from django.shortcuts import render
from django.conf import settings
import re
import uuid
from io import StringIO
from postgresqleu.util.payment.banktransfer import BaseManagedBankPayment
from postgresqleu.util.payment.banktransfer import BaseManagedBankPaymentForm
from postgresqleu.util.forms import SubmitButtonField
from postgresqleu.util.widgets import MonospaceTextarea, StaticTextWidget
from postgresqleu.util.crypto import validate_pem_public_key, validate_pem_private_key
from postgresqleu.util.crypto import generate_rsa_keypair
from postgresqleu.accounting.util import get_account_choices
from postgresqleu.transferwise.api import TransferwiseApi
from postgresqleu.transferwise.models import TransferwiseTransaction, TransferwisePayout
class BackendTransferwiseForm(BaseManagedBankPaymentForm):
apikey = forms.CharField(required=True, widget=forms.widgets.PasswordInput(render_value=True))
public_key = forms.CharField(required=False, widget=MonospaceTextarea, validators=[validate_pem_public_key, ])
private_key = forms.CharField(required=False, widget=MonospaceTextarea, validators=[validate_pem_private_key, ])
generatekey = SubmitButtonField(label="Generate new keypair", required=False)
canrefund = forms.BooleanField(required=False, label='Can refund',
help_text='Process automatic refunds. This requires an API key with full access to make transfers to any accounts')
autopayout = forms.BooleanField(required=False, label='Automatic payouts',
help_text='Issue automatic payouts when account balances goes above a specified level.')
autopayouttrigger = forms.IntegerField(required=False, label='Payout trigger',
validators=[MinValueValidator(1), ],
help_text='Trigger automatic payouts when balance goes above this')
autopayoutlimit = forms.IntegerField(required=False, label='Payout limit',
validators=[MinValueValidator(0), ],
help_text='When issuing automatic payouts, keep this amount in the account after the payout is done')
autopayoutname = forms.CharField(required=False, max_length=64, label='Recipent name',
help_text='Name of recipient to make IBAN payouts to')
autopayoutiban = forms.CharField(required=False, max_length=64, label='Recipient IBAN',
help_text='IBAN number of account to make payouts to')
notification_receiver = forms.EmailField(required=True)
send_statements = forms.BooleanField(required=False, label="Send statements",
help_text="Send monthly PDF statements by email")
accounting_payout = forms.ChoiceField(required=False, choices=[(None, '---')] + get_account_choices(),
label="Payout account")
accounting_cashback = forms.ChoiceField(required=False, choices=[(None, '---')] + get_account_choices(),
label="Cashback account",
help_text="Accounting account that any cashback payments are booked against")
webhookurl = forms.CharField(label="Webhook URL", widget=StaticTextWidget)
exclude_fields_from_validation = ('generatekey', )
config_readonly = ['webhookurl', ]
managed_fields = ['apikey', 'canrefund', 'notification_receiver', 'autopayout', 'autopayouttrigger',
'autopayoutlimit', 'autopayoutname', 'autopayoutiban', 'accounting_payout', 'accounting_cashback',
'send_statements', 'public_key', 'private_key', 'generatekey', ]
managed_fieldsets = [
{
'id': 'tw',
'legend': 'TransferWise',
'fields': ['notification_receiver', 'send_statements', 'canrefund', 'apikey', 'generatekey', 'public_key', 'private_key'],
},
{
'id': 'twautopayout',
'legend': 'Automatic Payouts',
'fields': ['autopayout', 'autopayouttrigger', 'autopayoutlimit',
'autopayoutname', 'autopayoutiban', 'accounting_payout'],
},
{
'id': 'twconf',
'legend': 'TransferWise configuration',
'fields': ['webhookurl', ],
},
]
@property
def config_fieldsets(self):
fss = super().config_fieldsets
for fs in fss:
if fs['id'] == 'accounting':
fs['fields'].append('accounting_cashback')
return fss
def fix_fields(self):
super().fix_fields()
self.fields['generatekey'].callback = self.generate_keypair
self.initial['webhookurl'] = """
On the TransferWise account, go into Settings and click
Create new webhook. Give it a reasonable name, set it to
receive Balance deposit events, and specify the URL
{}/wh/tw/{}/balance/
.""".format(
settings.SITEBASE,
self.instance.id,
)
def generate_keypair(self, request):
(private, public) = generate_rsa_keypair()
self.instance.config['public_key'] = public
self.instance.config['private_key'] = private
self.instance.save(update_fields=['config'])
messages.info(request, "New RSA keypair generated")
return True
def clean(self):
cleaned_data = super(BackendTransferwiseForm, self).clean()
if cleaned_data['autopayout']:
if not cleaned_data.get('canrefund', None):
self.add_error('autopayout', 'Automatic payouts can only be enabled if refunds are enabled')
# If auto payouts are enabled, a number of fields become mandateory
for fn in ('autopayouttrigger', 'autopayoutlimit', 'autopayoutname', 'autopayoutiban', 'accounting_payout'):
if not cleaned_data.get(fn, None):
self.add_error(fn, 'This field is required when automatic payouts are enabled')
if cleaned_data['autopayoutlimit'] >= cleaned_data['autopayouttrigger']:
self.add_error('autopayoutlimit', 'This value must be lower than the trigger value')
# Actually make an API call to validate the IBAN
if 'autopayoutiban' in cleaned_data and cleaned_data['autopayoutiban']:
pm = self.instance.get_implementation()
api = pm.get_api()
try:
if not api.validate_iban(cleaned_data['autopayoutiban']):
self.add_error('autopayoutiban', 'IBAN number could not be validated')
except Exception as e:
self.add_error('autopayoutiban', 'IBAN number could not be validated: {}'.format(e))
return cleaned_data
@classmethod
def validate_data_for(self, instance):
pm = instance.get_implementation()
api = pm.get_api()
try:
address = api.get_account_details()
return """Successfully retreived information:
{0}""".format(address) except Exception as e: return "Verification failed: {}".format(e) class Transferwise(BaseManagedBankPayment): backend_form_class = BackendTransferwiseForm description = """ Pay using a direct IBAN bank transfer in {}. We strongly advise not using this method if making a payment from an account in a different currency, as amounts must be exact and all fees covered by sender. """.format(settings.CURRENCY_ABBREV) def render_page(self, request, invoice): return render(request, 'invoices/genericbankpayment.html', { 'invoice': invoice, 'bankinfo': self.config('bankinfo'), }) def get_api(self): return TransferwiseApi(self) def _find_invoice_transaction(self, invoice): r = re.compile(r'Bank transfer from method {} with id (\d+)'.format(self.method.id)) m = r.match(invoice.paymentdetails) if m: try: return (TransferwiseTransaction.objects.get(pk=m.groups(1)[0], paymentmethod=self.method), None) except TransferwiseTransaction.DoesNotExist: return (None, "not found") else: return (None, "unknown format") def can_autorefund(self, invoice): if not self.config('canrefund'): return False (trans, reason) = self._find_invoice_transaction(invoice) if trans: if not trans.counterpart_valid_iban: # If there is no valid IBAN on the counterpart, we won't be able to # refund this invoice. return False return True return False def autorefund(self, refund): if not self.config('canrefund'): raise Exception("Cannot process automatic refunds. Configuration has changed?") (trans, reason) = self._find_invoice_transaction(refund.invoice) if not trans: raise Exception(reason) api = self.get_api() refund.payment_reference = api.refund_transaction( trans, refund.id, refund.fullamount, '{0} refund {1}'.format(settings.ORG_SHORTNAME, refund.id), ) # At this point, we succeeded. Anything that failed will bubble # up as an exception. return True def return_payment(self, trans): # Return a payment that is *not* attached to an invoice if not self.config('canrefund'): raise Exception("Cannot process automatic refunds. Configuration has changed?") twtrans = TransferwiseTransaction.objects.get( paymentmethod=trans.method, id=trans.methodidentifier, ) if not twtrans.counterpart_valid_iban: raise Exception("Cannot return payment without a valid IBAN") payout = TransferwisePayout( paymentmethod=trans.method, amount=twtrans.amount, reference='{0} returned payment {1}'.format(settings.ORG_SHORTNAME, twtrans.id), counterpart_name=twtrans.counterpart_name, counterpart_account=twtrans.counterpart_account, uuid=uuid.uuid4(), ) payout.save()