from django.conf import settings from django import forms from django.core.exceptions import ValidationError from django.utils import timezone import re from postgresqleu.util.db import exec_to_list, exec_to_scalar from postgresqleu.util.widgets import StaticTextWidget from postgresqleu.invoices.models import Invoice, InvoicePaymentMethod from postgresqleu.invoices.util import diff_workdays from postgresqleu.invoices.backendforms import BackendInvoicePaymentMethodForm from postgresqleu.accounting.util import get_account_choices from postgresqleu.adyen.models import TransactionStatus from postgresqleu.adyen.util import AdyenAPI from . import BasePayment class BackendAdyenCreditCardForm(BackendInvoicePaymentMethodForm): merchantaccount = forms.CharField(required=True, label="Merchant account") test = forms.BooleanField(required=False, label="Testing system") apibaseurl = forms.CharField(required=True, label="API Base URL", help_text="For test, use https://pal-test.adyen.com/. For prod, find in Adyen CA -> Account -> API Urls") checkoutbaseurl = forms.CharField(required=False, label="Checkout API Base Url", help_text="For test, use https://checkout-test.adyen.com/. For prod, find in Adyen CA -> Developers -> API URLs") ws_user = forms.CharField(required=True, label="Web Service user", help_text="Web Service user with Merchant PAL Webservice role") ws_password = forms.CharField(required=True, label="Web Service user password", widget=forms.widgets.PasswordInput(render_value=True)) ws_apikey = forms.CharField(required=True, label="Web Service API key", widget=forms.widgets.PasswordInput(render_value=True)) report_user = forms.CharField(required=True, label="Report user", help_text="Report user with Merchant Report Download role") report_password = forms.CharField(required=True, label="Report user password", widget=forms.widgets.PasswordInput(render_value=True)) notify_user = forms.CharField(required=True, label="Notify User", help_text="User Adyen will use to post notifications with") notify_password = forms.CharField(required=True, label="Notify user password", widget=forms.widgets.PasswordInput(render_value=True)) notification_receiver = forms.EmailField(required=True) merchantref_prefix = forms.CharField(required=True, label="Merchant Reference prefix", help_text="Prefixed to invoice number for all invoices") merchantref_refund_prefix = forms.CharField(required=True, label="Merchant Refund prefix", help_text="Prefixed to refund number for all refunds") accounting_authorized = forms.ChoiceField(required=True, choices=get_account_choices, label="Authorized payments account") accounting_payable = forms.ChoiceField(required=True, choices=get_account_choices, label="Payable balance account") accounting_merchant = forms.ChoiceField(required=True, choices=get_account_choices, label="Merchant account") accounting_fee = forms.ChoiceField(required=True, choices=get_account_choices, label="Fees account") accounting_refunds = forms.ChoiceField(required=True, choices=get_account_choices, label="Pending refunds account") accounting_payout = forms.ChoiceField(required=True, choices=get_account_choices, label="Payout account") notifications = forms.CharField(widget=StaticTextWidget) returnurl = forms.CharField(label="Return URL", widget=StaticTextWidget) config_fields = ['merchantaccount', 'test', 'apibaseurl', 'checkoutbaseurl', 'ws_user', 'ws_password', 'ws_apikey', 'report_user', 'report_password', 'notify_user', 'notify_password', 'notification_receiver', 'merchantref_prefix', 'merchantref_refund_prefix', 'accounting_authorized', 'accounting_payable', 'accounting_merchant', 'accounting_fee', 'accounting_refunds', 'accounting_payout', 'notifications', 'returnurl', ] config_readonly = ['notifications', 'returnurl', ] config_fieldsets = [ { 'id': 'adyen', 'legend': 'Adyen', 'fields': ['merchantaccount', 'test', ], }, { 'id': 'api', 'legend': 'API and users', 'fields': ['apibaseurl', 'checkoutbaseurl', 'ws_user', 'ws_password', 'ws_apikey', 'report_user', 'report_password', 'notify_user', 'notify_password', ], }, { 'id': 'integration', 'legend': 'Integration', 'fields': ['notification_receiver', 'merchantref_prefix', 'merchantref_refund_prefix', ], }, { 'id': 'accounting', 'legend': 'Accounting', 'fields': ['accounting_authorized', 'accounting_payable', 'accounting_merchant', 'accounting_fee', 'accounting_refunds', 'accounting_payout'], }, { 'id': 'adyenconf', 'legend': 'Adyen configuration', 'fields': ['notifications', 'returnurl', ], } ] def fix_fields(self): super(BackendAdyenCreditCardForm, self).fix_fields() if self.instance.id: self.initial.update({ 'notifications': """ In Adyen setup, select the merchant account (not the master account), then click Notifications in the account menu. In the field for URL, enter {0}/p/adyen_notify/{1}/, and pick format HTTP POST.""".format( settings.SITEBASE, self.instance.id, ), 'returnurl': """ In Adyen Test setup, edit the skin, and in the field for Result URL (production or test) enter {0}/p/adyen_return/{1}/. If this is a production setup, you also have to publish a new version of the skin. """.format( settings.SITEBASE, self.instance.id, ), }) def _get_merchantaccount_choices(): # Get all possible merchant accounts off creditcard settings return [('', '---')] + exec_to_list("SELECT DISTINCT config->>'merchantaccount',config->>'merchantaccount' FROM invoices_invoicepaymentmethod where classname='postgresqleu.util.payment.adyen.AdyenCreditcard'") class BackendAdyenBanktransferForm(BackendInvoicePaymentMethodForm): merchantaccount = forms.ChoiceField(required=True, choices=_get_merchantaccount_choices, label="Merchant account") config_fields = ['merchantaccount', ] config_fieldsets = [ { 'id': 'adyen', 'legend': 'Adyen', 'fields': ['merchantaccount', ], }, ] def clean_merchantaccount(self): n = exec_to_scalar("SELECT count(1) FROM invoices_invoicepaymentmethod WHERE classname='postgresqleu.util.payment.adyen.AdyenBanktransfer' AND config->>'merchantaccount' = %(account)s AND (id != %(self)s OR %(self)s IS NULL)", { 'account': self.cleaned_data['merchantaccount'], 'self': self.instance.id, }) if n > 0: raise ValidationError("Sorry, there is already a bank transfer entry for this merchant account") return self.cleaned_data['merchantaccount'] class _AdyenBase(BasePayment): def build_payment_url(self, invoicestr, invoiceamount, invoiceid, returnurl=None): i = Invoice.objects.get(pk=invoiceid) if i.recipient_secret: return "/invoices/adyenpayment/{0}/{1}/{2}/".format(self.id, invoiceid, i.recipient_secret) else: return "/invoices/adyenpayment/{0}/{1}/".format(self.id, invoiceid) _re_adyen = re.compile('^Adyen id ([A-Z0-9]+)$') def _find_invoice_transaction(self, invoice): m = self._re_adyen.match(invoice.paymentdetails) if m: try: # For the IBAN method, the transaction is actually booked on our "Parent account" # (but the invoice is listed s paid by the banktransfer properly) # We still allow it to be booked on our own account directly as well, as this was # done in the old system. if isinstance(self, AdyenBanktransfer): methods = (self.id, InvoicePaymentMethod.objects.filter(classname="postgresqleu.util.payment.adyen.AdyenCreditcard").extra( where=["config->>'merchantaccount' = %s"], params=[self.config('merchantaccount')], )[0].id, ) else: methods = (self.id, ) return (TransactionStatus.objects.get(pspReference=m.groups(1)[0], paymentmethod__in=methods), None) except TransactionStatus.DoesNotExist: return (None, "not found") except InvoicePaymentMethod.DoesNotExist: return (None, "parent not found") else: return (None, "unknown format") def payment_fees(self, invoice): (trans, reason) = self._find_invoice_transaction(invoice) if not trans: return reason if trans.settledamount: return trans.amount - trans.settledamount else: return "not settled yet" def autorefund(self, refund): (trans, reason) = self._find_invoice_transaction(refund.invoice) if not trans: raise Exception(reason) api = AdyenAPI(self) refund.payment_reference = api.refund_transaction( refund.id, trans.pspReference, refund.fullamount, ) # At this point, we succeeded. Anything that failed will bubble # up as an exception. return True class AdyenCreditcard(_AdyenBase): backend_form_class = BackendAdyenCreditCardForm description = """ Pay using your credit card, including Mastercard, VISA and American Express. """ def used_method_details(self, invoice): # For credit card payments we try to figure out which type of # card it is as well. (trans, reason) = self._find_invoice_transaction(invoice) if not trans: raise Exception(reason) return "Credit Card ({0})".format(trans.method) class AdyenBanktransfer(_AdyenBase): backend_form_class = BackendAdyenBanktransferForm description = """ Pay using a direct IBAN bank transfer. Due to the unreliable and slow processing of these payments, this method is not recommended unless it is the only option possible. In particular, 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. """ def __init__(self, id, method=None): super(AdyenBanktransfer, self).__init__(id, method) self.parentmethod = InvoicePaymentMethod.objects.filter( classname='postgresqleu.util.payment.adyen.AdyenCreditcard' ).extra( where=["config->>'merchantaccount' = %s"], params=[super(AdyenBanktransfer, self).config('merchantaccount')] )[0] self.parent = self.parentmethod.get_implementation() def config(self, param, default=None): # Override the config parameter, because we want to get everything *except* for the # payment processor id from the "parent" instead. if param == 'merchantaccount': return super(AdyenBanktransfer, self).config(param, default) return self.parent.config(param, default) def build_payment_url(self, invoicestr, invoiceamount, invoiceid, returnurl=None): i = Invoice.objects.get(pk=invoiceid) if i.recipient_secret: return "/invoices/adyen_bank/{0}/{1}/{2}/".format(self.id, invoiceid, i.recipient_secret) else: return "/invoices/adyen_bank/{0}/{1}/".format(self.id, invoiceid) def build_adyen_payment_url(self, invoicestr, invoiceamount, invoiceid): return super(AdyenBanktransfer, self).build_payment_url(invoicestr, invoiceamount, invoiceid) + 'iban/' # Override availability for direct bank transfers. We hide it if the invoice will be # automatically canceled in less than 4 working days. def available(self, invoice): if invoice.canceltime: if diff_workdays(timezone.now(), invoice.canceltime) < 5: return False return True def unavailable_reason(self, invoice): if invoice.canceltime: if diff_workdays(timezone.now(), invoice.canceltime) < 5: return "Since this invoice will be automatically canceled in less than 5 working days, it requires the use of a faster payment method." def used_method_details(self, invoice): # Bank transfers don't need any extra information return "IBAN bank transfers"