diff --git a/ccvpn/settings.py b/ccvpn/settings.py index 6c86532..21cf7d6 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -218,26 +218,36 @@ VPN_AUTH_STORAGE = 'inst' # Payment backends. See payments/backends.py for more infos. PAYMENTS_BACKENDS = { + 'bitcoin': { + 'enabled': False, + 'url': 'http://test:test@127.0.0.1:18332/', + # 'chain': '', # mainnet, testnet, regnet + }, 'paypal': { - 'TEST': True, # Sandbox - 'ADDRESS': 'paypal@ccrypto.org', # Your PayPal primary address + 'enabled': False, + 'test': True, # Sandbox + # 'api_base': '', + 'address': '', # PayPal address to send to + # 'receiver': '', # PayPal primary address if different + # 'currency': 'EUR', + # 'header_image': '', }, - # Remove the leading '_' to enable these backends. - '_stripe': { - 'API_KEY': '', - 'PUBLIC_KEY': '', + 'stripe': { + 'enabled': False, + 'secret_key': '', + 'public_key': '', + 'currency': 'EUR', + # 'title': '', + # 'header_image': '', }, - '_bitcoin': { - 'URL': 'http://test:test@127.0.0.1:18332/', - 'BITCOIN_VALUE': 36000, # Value of one bitcoin in currency*100 + 'coingate': { + 'enabled': False, + # 'sandbox': True, + # 'api_base': '', + 'api_token': '', + 'currency': 'EUR', + # 'title': '', }, - '_coingate': { - # 'SANDBOX': True, - # 'API_BASE': '', - 'API_TOKEN': '', - 'CURRENCY': '', - 'TITLE': '', - } } PAYMENTS_CURRENCY = ('eur', '€') diff --git a/payments/admin.py b/payments/admin.py index f78951a..29cdecf 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -1,5 +1,7 @@ +import json from django.shortcuts import resolve_url from django.contrib import admin +from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from .models import Payment, Subscription @@ -9,6 +11,18 @@ def subscr_mark_as_cancelled(modeladmin, request, queryset): subscr_mark_as_cancelled.short_description = _("Mark as cancelled (do not actually cancel)") +def link(text, url): + if not url: + return text + if not text: + text = url + return format_html('{}', url, text) + +def json_format(code): + j = json.dumps(code, indent=2) + return format_html("
{}
", j) + + class PaymentAdmin(admin.ModelAdmin): model = Payment list_display = ('user', 'backend', 'status', 'amount', 'paid_amount', 'created') @@ -21,24 +35,24 @@ class PaymentAdmin(admin.ModelAdmin): }), (_("Payment Data"), { 'fields': ('amount_fmt', 'paid_amount_fmt', - 'backend_extid_link', 'backend_data'), + 'backend_extid_link', 'backend_data_fmt'), }), ) readonly_fields = ('backend', 'user_link', 'time', 'status', 'status_message', 'amount_fmt', 'paid_amount_fmt', 'subscription_link', - 'backend_extid_link', 'backend_data') + 'backend_extid_link', 'backend_data_fmt') search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data') def backend(self, object): return object.backend.backend_verbose_name + def backend_data_fmt(self, object): + return json_format(object.backend_data) + def backend_extid_link(self, object): ext_url = object.backend.get_ext_url(object) - if ext_url: - return '%s' % (ext_url, object.backend_extid) - return object.backend_extid - backend_extid_link.allow_tags = True + return link(object.backend_extid, ext_url) def amount_fmt(self, object): return '%.2f %s' % (object.amount / 100, object.currency_name) @@ -50,24 +64,23 @@ class PaymentAdmin(admin.ModelAdmin): def user_link(self, object): change_url = resolve_url('admin:auth_user_change', object.user.id) - return '%s' % (change_url, object.user.username) - user_link.allow_tags = True + return link(object.user.username, change_url) user_link.short_description = 'User' def subscription_link(self, object): change_url = resolve_url('admin:payments_subscription_change', object.subscription.id) - return '%s' % (change_url, object.subscription.id) - subscription_link.allow_tags = True + return link(object.subscription.id, change_url) subscription_link.short_description = 'Subscription' class SubscriptionAdmin(admin.ModelAdmin): model = Subscription list_display = ('user', 'created', 'status', 'backend', 'backend_extid') + list_filter = ('backend_id', 'status') readonly_fields = ('user_link', 'backend', 'period', 'created', 'status', 'last_confirmed_payment', 'payments_links', - 'backend_extid_link', 'backend_data') + 'backend_extid_link', 'backend_data_fmt') search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data') actions = (subscr_mark_as_cancelled,) fieldsets = ( @@ -76,32 +89,31 @@ class SubscriptionAdmin(admin.ModelAdmin): 'last_confirmed_payment'), }), (_("Payment Data"), { - 'fields': ('backend_extid_link', 'backend_data'), + 'fields': ('backend_extid_link', 'backend_data_fmt'), }), ) def backend(self, object): return object.backend.backend_verbose_name + def backend_data_fmt(self, object): + return json_format(object.backend_data) + def user_link(self, object): change_url = resolve_url('admin:auth_user_change', object.user.id) - return '%s' % (change_url, object.user.username) - user_link.allow_tags = True + return link(object.user.id, change_url) user_link.short_description = 'User' def payments_links(self, object): - fmt = '%d payments' - payments_url = resolve_url('admin:payments_payment_changelist') count = Payment.objects.filter(subscription=object).count() - return fmt % (payments_url, object.id, count) - payments_links.allow_tags = True + payments_url = resolve_url('admin:payments_payment_changelist') + url = "%s?subscription__id__exact=%s" % (payments_url, object.id) + return link("%d payment(s)" % count, url) payments_links.short_description = 'Payments' def backend_extid_link(self, object): ext_url = object.backend.get_subscr_ext_url(object) - if ext_url: - return '%s' % (ext_url, object.backend_extid) - return object.backend_extid + return link(object.backend_extid, ext_url) backend_extid_link.allow_tags = True admin.site.register(Payment, PaymentAdmin) diff --git a/payments/backends/bitcoin.py b/payments/backends/bitcoin.py index 8e200ec..26a075b 100644 --- a/payments/backends/bitcoin.py +++ b/payments/backends/bitcoin.py @@ -22,13 +22,13 @@ class BitcoinBackend(BackendBase): from bitcoin import SelectParams from bitcoin.rpc import Proxy - self.account = settings.get('ACCOUNT', 'ccvpn3') + self.account = settings.get('account', 'ccvpn3') - chain = settings.get('CHAIN') + chain = settings.get('chain') if chain: SelectParams(chain) - self.url = settings.get('URL') + self.url = settings.get('url') if not self.url: return diff --git a/payments/backends/coingate.py b/payments/backends/coingate.py index ae17e18..f4cab72 100644 --- a/payments/backends/coingate.py +++ b/payments/backends/coingate.py @@ -26,18 +26,20 @@ class CoinGateBackend(BackendBase): backend_has_recurring = False def __init__(self, settings): - self.api_token = settings.get('API_TOKEN') + self.api_token = settings.get('api_token') if not self.api_token: return - self.currency = settings.get('CURRENCY', 'EUR') - self.title = settings.get('TITLE', 'VPN Payment') + self.currency = settings.get('currency', 'EUR') + self.title = settings.get('title', 'VPN Payment') - if settings.get('SANDBOX'): + if settings.get('sandbox'): + self.sandbox = True self.api_base = "https://api-sandbox.coingate.com" else: + self.sandbox = False default_base = "https://api.coingate.com" - self.api_base = settings.get('API_BASE', default_base) + self.api_base = settings.get('api_base', default_base) self.backend_enabled = True @@ -71,6 +73,7 @@ class CoinGateBackend(BackendBase): url = order['payment_url'] + payment.backend_extid = order['id'] payment.backend_data['coingate_id'] = order['id'] payment.backend_data['coingate_url'] = url payment.backend_data['coingate_token'] = token @@ -126,6 +129,9 @@ class CoinGateBackend(BackendBase): def get_ext_url(self, payment): if not payment.backend_extid: return None - return 'https://dashboard.stripe.com/payments/%s' % payment.backend_extid + if self.sandbox: + return 'https://sandbox.coingate.com/account/orders/%s' % payment.backend_extid + else: + return 'https://coingate.com/account/orders/%s' % payment.backend_extid diff --git a/payments/backends/paypal.py b/payments/backends/paypal.py index 5a335ff..db5dafc 100644 --- a/payments/backends/paypal.py +++ b/payments/backends/paypal.py @@ -15,18 +15,18 @@ class PaypalBackend(BackendBase): backend_has_recurring = True def __init__(self, settings): - self.test = settings.get('TEST', False) - self.header_image = settings.get('HEADER_IMAGE', None) - self.title = settings.get('TITLE', 'VPN Payment') - self.currency = settings.get('CURRENCY', 'EUR') - self.account_address = settings.get('ADDRESS') - self.receiver_address = settings.get('RECEIVER', self.account_address) + self.test = settings.get('test', False) + self.header_image = settings.get('header_image', None) + self.title = settings.get('title', 'VPN Payment') + self.currency = settings.get('currency', 'EUR') + self.account_address = settings.get('address') + self.receiver_address = settings.get('receiver', self.account_address) if self.test: default_api = 'https://www.sandbox.paypal.com/' else: default_api = 'https://www.paypal.com/' - self.api_base = settings.get('API_BASE', default_api) + self.api_base = settings.get('api_base', default_api) if self.account_address: self.backend_enabled = True diff --git a/payments/backends/stripe.py b/payments/backends/stripe.py index 232bd69..3d74877 100644 --- a/payments/backends/stripe.py +++ b/payments/backends/stripe.py @@ -1,7 +1,6 @@ -import json - from django.utils.translation import ugettext_lazy as _ from django.urls import reverse +from django.conf import settings as project_settings from .base import BackendBase @@ -16,133 +15,154 @@ class StripeBackend(BackendBase): return 'ccvpn_' + period def __init__(self, settings): - if 'API_KEY' not in settings or 'PUBLIC_KEY' not in settings: - return + self.public_key = settings.get('public_key') + self.secret_key = settings.get('secret_key') + self.wh_key = settings.get('wh_key') + + if not self.public_key or not self.secret_key or not self.wh_key: + raise Exception("Missing keys for stripe backend") import stripe self.stripe = stripe + stripe.api_key = self.secret_key - stripe.api_key = settings['API_KEY'] - self.pubkey = settings['PUBLIC_KEY'] - self.header_image = settings.get('HEADER_IMAGE', '') - self.currency = settings.get('CURRENCY', 'EUR') - self.name = settings.get('NAME', 'VPN Payment') - + self.header_image = settings.get('header_image', '') + self.currency = settings.get('currency', 'EUR') + self.title = settings.get('title', 'VPN Payment') self.backend_enabled = True + def make_redirect(self, session): + return ''' + + + + '''.format(pk=self.public_key, sess=session['id']) + def new_payment(self, payment): - desc = str(payment.time) + ' for ' + payment.user.username - form = ''' -
- -
- ''' - return form.format( - post=reverse('payments:cb_stripe', args=(payment.id,)), - pubkey=self.pubkey, - img=self.header_image, - email=payment.user.email or '', - name=self.name, - desc=desc, - amount=payment.amount, - curr=self.currency, + root_url = project_settings.ROOT_URL + assert root_url + + months = payment.time.days // 30 + if months > 1: + desc = '{} months for {}'.format(months, payment.user.username) + else: + desc = 'One month for {}'.format(payment.user.username) + + session = self.stripe.checkout.Session.create( + success_url=root_url + reverse('payments:view', args=(payment.id,)), + cancel_url=root_url + reverse('payments:cancel', args=(payment.id,)), + payment_method_types=['card'], + line_items=[ + { + 'amount': payment.amount, + 'currency': self.currency.lower(), + 'name': self.title, + 'description': desc, + 'quantity': 1, + } + ], ) + payment.backend_extid = session['id'] + payment.backend_data = {'session_id': session['id']} + payment.save() + return self.make_redirect(session) def new_subscription(self, subscr): - desc = 'Subscription (' + str(subscr.period) + ') for ' + subscr.user.username - form = ''' -
- -
- - ''' - return form.format( - post=reverse('payments:cb_stripe_subscr', args=(subscr.id,)), - pubkey=self.pubkey, - img=self.header_image, - email=subscr.user.email or '', - name=self.name, - desc=desc, - amount=subscr.period_amount, - curr=self.currency, + root_url = project_settings.ROOT_URL + assert root_url + + session = self.stripe.checkout.Session.create( + success_url=root_url + reverse('payments:return_subscr', args=(subscr.id,)), + cancel_url=root_url + reverse('payments:cancel_subscr', args=(subscr.id,)), + client_reference_id='sub_%d'%subscr.id, + payment_method_types=['card'], + subscription_data={ + 'items': [{ + 'plan': self.get_plan_id(subscr.period), + 'quantity': 1, + }], + }, ) + subscr.backend_data = {'session_id': session['id']} + subscr.save() + return self.make_redirect(session) def cancel_subscription(self, subscr): if subscr.status not in ('new', 'unconfirmed', 'active'): return - try: - cust = self.stripe.Customer.retrieve(subscr.backend_extid) - except self.stripe.error.InvalidRequestError: - return - - try: - # Delete customer and cancel any active subscription - cust.delete() - except self.stripe.error.InvalidRequestError: + if subscr.backend_extid.startswith('pi'): + # a session that didn't create a subscription yet (intent) pass + elif subscr.backend_extid.startswith('sub_'): + # Subscription object + try: + self.stripe.Subscription.delete(subscr.backend_extid) + except self.stripe.error.InvalidRequestError: + pass + elif subscr.backend_extid.startswith('cus_'): + # Legacy Customer object + try: + cust = self.stripe.Customer.retrieve(subscr.backend_extid) + except self.stripe.error.InvalidRequestError: + return + + try: + # Delete customer and cancel any active subscription + cust.delete() + except self.stripe.error.InvalidRequestError: + pass + else: + raise Exception("Failed to cancel subscription %r" % subscr.backend_extid) subscr.status = 'cancelled' subscr.save() - def callback(self, payment, request): - post_data = request.POST + def webhook_session_completed(self, event): + session = event['data']['object'] - token = post_data.get('stripeToken') - if not token: - payment.status = 'cancelled' - payment.status_message = _("No payment information was received.") - return + if session['subscription']: + # Subscription creation + from payments.models import Payment, Subscription - months = int(payment.time.days / 30) - username = payment.user.username + sub_id = session['subscription'] + assert sub_id - try: - charge = self.stripe.Charge.create( - amount=payment.amount, - currency=self.currency, - card=token, - description="%d months for %s" % (months, username), - ) - payment.backend_extid = charge['id'] + parts = session['client_reference_id'].split('_') + if len(parts) != 2 or parts[0] != 'sub': + raise Exception("invalid reference id") + sub_internal_id = int(parts[1]) - if charge['refunded'] or not charge['paid']: - payment.status = 'rejected' - payment.status_message = _("The payment has been refunded or rejected.") - payment.save() - return + # Fetch sub by ID and confirm it + subscr = Subscription.objects.get(id=sub_internal_id) + subscr.status = 'active' + subscr.backend_extid = sub_id + subscr.backend_data['subscription_id'] = sub_id + subscr.save() - payment.paid_amount = int(charge['amount']) + payment = subscr.create_payment() + payment.status = 'confirmed' + payment.paid_amount = payment.amount + payment.backend_extid = None + payment.save() - if payment.paid_amount < payment.amount: - payment.status = 'error' - payment.status_message = _("The paid amount is under the required amount.") - payment.save() - return + payment.user.vpnuser.add_paid_time(payment.time) + payment.user.vpnuser.on_payment_confirmed(payment) + payment.user.vpnuser.save() + payment.save() + + else: + from payments.models import Payment + payment = Payment.objects.filter(backend_extid=session['id']).get() + + # the amount is provided server-side, we do not have to check + payment.paid_amount = payment.amount payment.status = 'confirmed' payment.status_message = None @@ -151,54 +171,47 @@ class StripeBackend(BackendBase): payment.user.vpnuser.on_payment_confirmed(payment) payment.user.vpnuser.save() - except self.stripe.error.CardError as e: - payment.status = 'rejected' - payment.status_message = e.json_body['error']['message'] - payment.save() + def get_subscription_from_invoice(self, invoice): + from payments.models import Subscription - def callback_subscr(self, subscr, request): - post_data = request.POST - token = post_data.get('stripeToken') - if not token: - subscr.status = 'cancelled' - subscr.save() - return + subscription_id = invoice['subscription'] + customer_id = invoice['customer'] - try: - cust = self.stripe.Customer.create( - source=token, - plan=self.get_plan_id(subscr.period), - ) - except self.stripe.error.InvalidRequestError: - return - except self.stripe.CardError as e: - subscr.status = 'error' - subscr.backend_data['stripe_error'] = e.json_body['error']['message'] - return + # once it's confirmed, the id to the subscription is stored as extid + subscr = Subscription.objects.filter(backend_extid=subscription_id).first() + if subscr: + return subscr - # We don't know much about the new Payment, but we know it - # succeeded. Wekhooks aren't very reliable, so let's mark it as active - # anyway. - subscr.status = 'active' - subscr.backend_extid = cust['id'] - subscr.save() + # older subscriptions will have a customer id instead + subscr = Subscription.objects.filter(backend_extid=customer_id).first() + if subscr: + return subscr + + return None def webhook_payment_succeeded(self, event): - from payments.models import Subscription, Payment + """ webhook event for a subscription's succeeded payment """ + from payments.models import Payment invoice = event['data']['object'] - customer_id = invoice['customer'] + subscr = self.get_subscription_from_invoice(invoice) + if not subscr: + # the subscription does not exist + # checkout.confirmed event will create it and handle the initial payment + return True + # raise Exception("Unknown subscription for invoice %r" % invoice['id']) # Prevent making duplicate Payments if event is received twice pc = Payment.objects.filter(backend_extid=invoice['id']).count() if pc > 0: return - subscr = Subscription.objects.get(backend_extid=customer_id) payment = subscr.create_payment() payment.status = 'confirmed' - payment.paid_amount = invoice['total'] + payment.paid_amount = payment.amount payment.backend_extid = invoice['id'] + if invoice['subscription']: + payment.backend_sub_id = invoice['subscription']['id'] payment.backend_data = {'event_id': event['id']} payment.save() @@ -207,26 +220,51 @@ class StripeBackend(BackendBase): payment.user.vpnuser.save() payment.save() - subscr.status = 'active' - subscr.save() - def webhook(self, request): + payload = request.body + sig_header = request.META['HTTP_STRIPE_SIGNATURE'] + try: - event_json = json.loads(request.body.decode('utf-8')) - event = self.stripe.Event.retrieve(event_json["id"]) - except (ValueError, self.stripe.error.InvalidRequestError): + event = self.stripe.Webhook.construct_event( + payload, sig_header, self.wh_key, + ) + except (ValueError, self.stripe.error.InvalidRequestError, self.stripe.error.SignatureVerificationError): return False if event['type'] == 'invoice.payment_succeeded': self.webhook_payment_succeeded(event) + if event['type'] == 'checkout.session.completed': + self.webhook_session_completed(event) return True def get_ext_url(self, payment): - if not payment.backend_extid: + extid = payment.backend_extid + + if not extid: return None - return 'https://dashboard.stripe.com/payments/%s' % payment.backend_extid + + if extid.startswith('in_'): + return 'https://dashboard.stripe.com/invoices/%s' % extid + if extid.startswith('ch_'): + return 'https://dashboard.stripe.com/payments/%s' % extid def get_subscr_ext_url(self, subscr): - if not subscr.backend_extid: + extid = subscr.backend_extid + + if not extid: return None - return 'https://dashboard.stripe.com/customers/%s' % subscr.backend_extid + + if extid.startswith('sub_') and self.stripe: + livemode = False + try: + sub = self.stripe.Subscription.retrieve(extid) + livemode = sub['livemode'] + except Exception: + pass + if livemode: + return 'https://dashboard.stripe.com/subscriptions/' + extid + else: + return 'https://dashboard.stripe.com/test/subscriptions/' + extid + + if extid.startswith('cus_'): + return 'https://dashboard.stripe.com/customers/%s' % subscr.backend_extid diff --git a/payments/models.py b/payments/models.py index 3791ed7..ea92a0e 100644 --- a/payments/models.py +++ b/payments/models.py @@ -1,3 +1,4 @@ +import logging from django.db import models from django.conf import settings from django.utils.translation import ugettext_lazy as _ @@ -7,6 +8,8 @@ from datetime import timedelta from ccvpn.common import get_price from .backends import BackendBase +logger = logging.getLogger(__name__) + backends_settings = settings.PAYMENTS_BACKENDS assert isinstance(backends_settings, dict) @@ -39,6 +42,8 @@ SUBSCR_PERIOD_CHOICES = ( ('12m', _("Every year")), ) +BACKEND_CLASSES = BackendBase.__subclasses__() + # All known backends (classes) BACKENDS = {} BACKEND_CHOICES = [] @@ -47,11 +52,14 @@ BACKEND_CHOICES = [] ACTIVE_BACKENDS = {} ACTIVE_BACKEND_CHOICES = [] -for cls in BackendBase.__subclasses__(): +logger.info("loading payment backends...") +for cls in BACKEND_CLASSES: name = cls.backend_id + prefix = "Backend {:<8}:".format(name) assert isinstance(name, str) if name not in backends_settings: + logger.info("%s ☒ disabled (no settings)", prefix) continue backend_settings = backends_settings.get(name, {}) @@ -59,10 +67,11 @@ for cls in BackendBase.__subclasses__(): if hasattr(v, '__call__'): backend_settings[k] = v() + if not backend_settings.get('enabled'): + logger.info("%s ☒ disabled (by settings)", prefix) + continue + obj = cls(backend_settings) - if not obj.backend_enabled: - if name in backends_settings: - raise Exception("Invalid settings for payment backend %r" % name) BACKENDS[name] = obj BACKEND_CHOICES.append((name, cls.backend_verbose_name)) @@ -70,10 +79,15 @@ for cls in BackendBase.__subclasses__(): if obj.backend_enabled: ACTIVE_BACKENDS[name] = obj ACTIVE_BACKEND_CHOICES.append((name, cls.backend_verbose_name)) + logger.info("%s ☑ initialized", prefix) + else: + logger.info("%s ☒ disabled (initialization failed)", prefix) BACKEND_CHOICES = sorted(BACKEND_CHOICES, key=lambda x: x[0]) ACTIVE_BACKEND_CHOICES = sorted(ACTIVE_BACKEND_CHOICES, key=lambda x: x[0]) +logger.info("finished. %d/%d backends active", len(ACTIVE_BACKENDS), len(BACKEND_CLASSES)) + def period_months(p): return { diff --git a/payments/tests/coingate.py b/payments/tests/coingate.py index 6b4c6dd..3c9150d 100644 --- a/payments/tests/coingate.py +++ b/payments/tests/coingate.py @@ -13,9 +13,9 @@ class CoinGateBackendTest(TestCase): self.user = User.objects.create_user('test', 'test_user@example.com', None) self.backend_settings = dict( - API_TOKEN='test', - TITLE='Test Title', - CURRENCY='EUR', + api_token='test', + title='Test Title', + currency='EUR', ) def test_payment(self): diff --git a/payments/tests/paypal.py b/payments/tests/paypal.py index d88807e..96dcda4 100644 --- a/payments/tests/paypal.py +++ b/payments/tests/paypal.py @@ -143,10 +143,10 @@ class PaypalBackendTest(TestCase): ) settings = dict( - TEST=True, - TITLE='Test Title', - CURRENCY='EUR', - ADDRESS='test_business@example.com', + test=True, + title='Test Title', + currency='EUR', + address='test_business@example.com', ) with self.settings(ROOT_URL='root'): @@ -196,10 +196,10 @@ class PaypalBackendTest(TestCase): ) settings = dict( - TEST=True, - TITLE='Test Title', - CURRENCY='EUR', - ADDRESS='test_business@example.com', + test=True, + title='Test Title', + currency='EUR', + address='test_business@example.com', ) with self.settings(ROOT_URL='root'): @@ -235,10 +235,10 @@ class PaypalBackendTest(TestCase): ) settings = dict( - TEST=True, - TITLE='Test Title', - CURRENCY='EUR', - ADDRESS='test_business@example.com', + test=True, + title='Test Title', + currency='EUR', + address='test_business@example.com', ) with self.settings(ROOT_URL='root'): diff --git a/payments/tests/stripe.py b/payments/tests/stripe.py index 2518add..91e7603 100644 --- a/payments/tests/stripe.py +++ b/payments/tests/stripe.py @@ -20,10 +20,11 @@ class StripeBackendTest(TestCase): ) settings = dict( - API_KEY='test_secret_key', - PUBLIC_KEY='test_public_key', - CURRENCY='EUR', - NAME='Test Name', + secret_key='test_secret_key', + public_key='test_public_key', + wh_key='test_wh_key', + currency='EUR', + name='Test Name', ) with self.settings(ROOT_URL='root'): diff --git a/payments/views.py b/payments/views.py index b95f73f..f26e110 100644 --- a/payments/views.py +++ b/payments/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render, redirect from django.urls import reverse from django.conf import settings from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, Http404 from django.views.decorators.csrf import csrf_exempt from django.utils import timezone from django.contrib import messages @@ -13,6 +13,15 @@ from .forms import NewPaymentForm from .models import Payment, Subscription, BACKENDS, ACTIVE_BACKENDS +def require_backend(name): + backend = BACKENDS.get(name) + if not backend: + raise Http404() + if not backend.backend_enabled: + raise Http404() + return backend + + @login_required def new(request): if request.method != 'POST': @@ -68,11 +77,10 @@ def new(request): @csrf_exempt def callback_paypal(request, id): """ PayPal IPN """ - if not BACKENDS['paypal'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('paypal') p = Payment.objects.get(id=id) - if BACKENDS['paypal'].callback(p, request): + if backend.callback(p, request): return HttpResponse() else: return HttpResponseBadRequest() @@ -82,22 +90,20 @@ def callback_paypal(request, id): @login_required def callback_stripe(request, id): """ Stripe button POST """ - if not BACKENDS['stripe'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('stripe') p = Payment.objects.get(id=id) - BACKENDS['stripe'].callback(p, request) + backend.callback(p, request) return redirect(reverse('payments:view', args=(id,))) @csrf_exempt def callback_coingate(request, id): """ CoinGate payment callback """ - if not BACKENDS['coingate'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('coingate') p = Payment.objects.get(id=id) - if BACKENDS['coingate'].callback(p, request): + if backend.callback(p, request): return HttpResponse() else: return HttpResponseBadRequest() @@ -105,10 +111,9 @@ def callback_coingate(request, id): @csrf_exempt def callback_coinbase(request): - if not BACKENDS['coinbase'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('coinbase') - if BACKENDS['coinbase'].callback(Payment, request): + if backend.callback(Payment, request): return HttpResponse() else: return HttpResponseBadRequest() @@ -117,11 +122,10 @@ def callback_coinbase(request): @csrf_exempt def callback_paypal_subscr(request, id): """ PayPal Subscription IPN """ - if not BACKENDS['paypal'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('paypal') p = Subscription.objects.get(id=id) - if BACKENDS['paypal'].callback_subscr(p, request): + if paypal.callback_subscr(p, request): return HttpResponse() else: return HttpResponseBadRequest() @@ -131,11 +135,10 @@ def callback_paypal_subscr(request, id): @login_required def callback_stripe_subscr(request, id): """ Stripe subscription form target """ - if not BACKENDS['stripe'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('stripe') p = Subscription.objects.get(id=id) - BACKENDS['stripe'].callback_subscr(p, request) + backend.callback_subscr(p, request) if p.status == 'error' or p.status == 'cancelled': messages.add_message(request, messages.ERROR, _("Error subscribing. It usually means you don't" @@ -147,10 +150,9 @@ def callback_stripe_subscr(request, id): @csrf_exempt def stripe_hook(request): - if not BACKENDS['stripe'].backend_enabled: - return HttpResponseNotFound() + backend = require_backend('stripe') - if BACKENDS['stripe'].webhook(request): + if backend.webhook(request): return HttpResponse() else: return HttpResponseBadRequest()