Big payments update

master
Alice 5 years ago
parent 4b056c8ad0
commit acf34a2ac9

@ -218,26 +218,36 @@ VPN_AUTH_STORAGE = 'inst'
# Payment backends. See payments/backends.py for more infos. # Payment backends. See payments/backends.py for more infos.
PAYMENTS_BACKENDS = { PAYMENTS_BACKENDS = {
'bitcoin': {
'enabled': False,
'url': 'http://test:test@127.0.0.1:18332/',
# 'chain': '', # mainnet, testnet, regnet
},
'paypal': { 'paypal': {
'TEST': True, # Sandbox 'enabled': False,
'ADDRESS': 'paypal@ccrypto.org', # Your PayPal primary address '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': {
'_stripe': { 'enabled': False,
'API_KEY': '', 'secret_key': '',
'PUBLIC_KEY': '', 'public_key': '',
'currency': 'EUR',
# 'title': '',
# 'header_image': '',
}, },
'_bitcoin': { 'coingate': {
'URL': 'http://test:test@127.0.0.1:18332/', 'enabled': False,
'BITCOIN_VALUE': 36000, # Value of one bitcoin in currency*100 # 'sandbox': True,
# 'api_base': '',
'api_token': '',
'currency': 'EUR',
# 'title': '',
}, },
'_coingate': {
# 'SANDBOX': True,
# 'API_BASE': '',
'API_TOKEN': '',
'CURRENCY': '',
'TITLE': '',
}
} }
PAYMENTS_CURRENCY = ('eur', '') PAYMENTS_CURRENCY = ('eur', '')

@ -1,5 +1,7 @@
import json
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .models import Payment, Subscription 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)") 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('<a href="{}">{}</a>', url, text)
def json_format(code):
j = json.dumps(code, indent=2)
return format_html("<pre>{}</pre>", j)
class PaymentAdmin(admin.ModelAdmin): class PaymentAdmin(admin.ModelAdmin):
model = Payment model = Payment
list_display = ('user', 'backend', 'status', 'amount', 'paid_amount', 'created') list_display = ('user', 'backend', 'status', 'amount', 'paid_amount', 'created')
@ -21,24 +35,24 @@ class PaymentAdmin(admin.ModelAdmin):
}), }),
(_("Payment Data"), { (_("Payment Data"), {
'fields': ('amount_fmt', 'paid_amount_fmt', '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', readonly_fields = ('backend', 'user_link', 'time', 'status', 'status_message',
'amount_fmt', 'paid_amount_fmt', 'subscription_link', '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') search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data')
def backend(self, object): def backend(self, object):
return object.backend.backend_verbose_name return object.backend.backend_verbose_name
def backend_data_fmt(self, object):
return json_format(object.backend_data)
def backend_extid_link(self, object): def backend_extid_link(self, object):
ext_url = object.backend.get_ext_url(object) ext_url = object.backend.get_ext_url(object)
if ext_url: return link(object.backend_extid, ext_url)
return '<a href="%s">%s</a>' % (ext_url, object.backend_extid)
return object.backend_extid
backend_extid_link.allow_tags = True
def amount_fmt(self, object): def amount_fmt(self, object):
return '%.2f %s' % (object.amount / 100, object.currency_name) return '%.2f %s' % (object.amount / 100, object.currency_name)
@ -50,24 +64,23 @@ class PaymentAdmin(admin.ModelAdmin):
def user_link(self, object): def user_link(self, object):
change_url = resolve_url('admin:auth_user_change', object.user.id) change_url = resolve_url('admin:auth_user_change', object.user.id)
return '<a href="%s">%s</a>' % (change_url, object.user.username) return link(object.user.username, change_url)
user_link.allow_tags = True
user_link.short_description = 'User' user_link.short_description = 'User'
def subscription_link(self, object): def subscription_link(self, object):
change_url = resolve_url('admin:payments_subscription_change', change_url = resolve_url('admin:payments_subscription_change',
object.subscription.id) object.subscription.id)
return '<a href="%s">%s</a>' % (change_url, object.subscription.id) return link(object.subscription.id, change_url)
subscription_link.allow_tags = True
subscription_link.short_description = 'Subscription' subscription_link.short_description = 'Subscription'
class SubscriptionAdmin(admin.ModelAdmin): class SubscriptionAdmin(admin.ModelAdmin):
model = Subscription model = Subscription
list_display = ('user', 'created', 'status', 'backend', 'backend_extid') list_display = ('user', 'created', 'status', 'backend', 'backend_extid')
list_filter = ('backend_id', 'status')
readonly_fields = ('user_link', 'backend', 'period', 'created', 'status', readonly_fields = ('user_link', 'backend', 'period', 'created', 'status',
'last_confirmed_payment', 'payments_links', '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') search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data')
actions = (subscr_mark_as_cancelled,) actions = (subscr_mark_as_cancelled,)
fieldsets = ( fieldsets = (
@ -76,32 +89,31 @@ class SubscriptionAdmin(admin.ModelAdmin):
'last_confirmed_payment'), 'last_confirmed_payment'),
}), }),
(_("Payment Data"), { (_("Payment Data"), {
'fields': ('backend_extid_link', 'backend_data'), 'fields': ('backend_extid_link', 'backend_data_fmt'),
}), }),
) )
def backend(self, object): def backend(self, object):
return object.backend.backend_verbose_name return object.backend.backend_verbose_name
def backend_data_fmt(self, object):
return json_format(object.backend_data)
def user_link(self, object): def user_link(self, object):
change_url = resolve_url('admin:auth_user_change', object.user.id) change_url = resolve_url('admin:auth_user_change', object.user.id)
return '<a href="%s">%s</a>' % (change_url, object.user.username) return link(object.user.id, change_url)
user_link.allow_tags = True
user_link.short_description = 'User' user_link.short_description = 'User'
def payments_links(self, object): def payments_links(self, object):
fmt = '<a href="%s?subscription__id__exact=%d">%d payments</a>'
payments_url = resolve_url('admin:payments_payment_changelist')
count = Payment.objects.filter(subscription=object).count() count = Payment.objects.filter(subscription=object).count()
return fmt % (payments_url, object.id, count) payments_url = resolve_url('admin:payments_payment_changelist')
payments_links.allow_tags = True url = "%s?subscription__id__exact=%s" % (payments_url, object.id)
return link("%d payment(s)" % count, url)
payments_links.short_description = 'Payments' payments_links.short_description = 'Payments'
def backend_extid_link(self, object): def backend_extid_link(self, object):
ext_url = object.backend.get_subscr_ext_url(object) ext_url = object.backend.get_subscr_ext_url(object)
if ext_url: return link(object.backend_extid, ext_url)
return '<a href="%s">%s</a>' % (ext_url, object.backend_extid)
return object.backend_extid
backend_extid_link.allow_tags = True backend_extid_link.allow_tags = True
admin.site.register(Payment, PaymentAdmin) admin.site.register(Payment, PaymentAdmin)

@ -22,13 +22,13 @@ class BitcoinBackend(BackendBase):
from bitcoin import SelectParams from bitcoin import SelectParams
from bitcoin.rpc import Proxy 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: if chain:
SelectParams(chain) SelectParams(chain)
self.url = settings.get('URL') self.url = settings.get('url')
if not self.url: if not self.url:
return return

@ -26,18 +26,20 @@ class CoinGateBackend(BackendBase):
backend_has_recurring = False backend_has_recurring = False
def __init__(self, settings): def __init__(self, settings):
self.api_token = settings.get('API_TOKEN') self.api_token = settings.get('api_token')
if not self.api_token: if not self.api_token:
return return
self.currency = settings.get('CURRENCY', 'EUR') self.currency = settings.get('currency', 'EUR')
self.title = settings.get('TITLE', 'VPN Payment') 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" self.api_base = "https://api-sandbox.coingate.com"
else: else:
self.sandbox = False
default_base = "https://api.coingate.com" 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 self.backend_enabled = True
@ -71,6 +73,7 @@ class CoinGateBackend(BackendBase):
url = order['payment_url'] url = order['payment_url']
payment.backend_extid = order['id']
payment.backend_data['coingate_id'] = order['id'] payment.backend_data['coingate_id'] = order['id']
payment.backend_data['coingate_url'] = url payment.backend_data['coingate_url'] = url
payment.backend_data['coingate_token'] = token payment.backend_data['coingate_token'] = token
@ -126,6 +129,9 @@ class CoinGateBackend(BackendBase):
def get_ext_url(self, payment): def get_ext_url(self, payment):
if not payment.backend_extid: if not payment.backend_extid:
return None 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

@ -15,18 +15,18 @@ class PaypalBackend(BackendBase):
backend_has_recurring = True backend_has_recurring = True
def __init__(self, settings): def __init__(self, settings):
self.test = settings.get('TEST', False) self.test = settings.get('test', False)
self.header_image = settings.get('HEADER_IMAGE', None) self.header_image = settings.get('header_image', None)
self.title = settings.get('TITLE', 'VPN Payment') self.title = settings.get('title', 'VPN Payment')
self.currency = settings.get('CURRENCY', 'EUR') self.currency = settings.get('currency', 'EUR')
self.account_address = settings.get('ADDRESS') self.account_address = settings.get('address')
self.receiver_address = settings.get('RECEIVER', self.account_address) self.receiver_address = settings.get('receiver', self.account_address)
if self.test: if self.test:
default_api = 'https://www.sandbox.paypal.com/' default_api = 'https://www.sandbox.paypal.com/'
else: else:
default_api = 'https://www.paypal.com/' 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: if self.account_address:
self.backend_enabled = True self.backend_enabled = True

@ -1,7 +1,6 @@
import json
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse from django.urls import reverse
from django.conf import settings as project_settings
from .base import BackendBase from .base import BackendBase
@ -16,133 +15,154 @@ class StripeBackend(BackendBase):
return 'ccvpn_' + period return 'ccvpn_' + period
def __init__(self, settings): def __init__(self, settings):
if 'API_KEY' not in settings or 'PUBLIC_KEY' not in settings: self.public_key = settings.get('public_key')
return 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 import stripe
self.stripe = stripe self.stripe = stripe
stripe.api_key = self.secret_key
stripe.api_key = settings['API_KEY'] self.header_image = settings.get('header_image', '')
self.pubkey = settings['PUBLIC_KEY'] self.currency = settings.get('currency', 'EUR')
self.header_image = settings.get('HEADER_IMAGE', '') self.title = settings.get('title', 'VPN Payment')
self.currency = settings.get('CURRENCY', 'EUR')
self.name = settings.get('NAME', 'VPN Payment')
self.backend_enabled = True self.backend_enabled = True
def make_redirect(self, session):
return '''
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
document.write("<p>Redirecting to the payment page...</p>");
var stripe = Stripe("{pk}");
stripe.redirectToCheckout({{
sessionId: "{sess}"
}});
</script>
<noscript><p>Please enable JavaScript to use the payment form.</p></noscript>
'''.format(pk=self.public_key, sess=session['id'])
def new_payment(self, payment): def new_payment(self, payment):
desc = str(payment.time) + ' for ' + payment.user.username root_url = project_settings.ROOT_URL
form = ''' assert root_url
<form action="{post}" method="POST">
<script months = payment.time.days // 30
src="https://checkout.stripe.com/checkout.js" class="stripe-button" if months > 1:
data-key="{pubkey}" desc = '{} months for {}'.format(months, payment.user.username)
data-image="{img}" else:
data-name="{name}" desc = 'One month for {}'.format(payment.user.username)
data-currency="{curr}"
data-description="{desc}" session = self.stripe.checkout.Session.create(
data-amount="{amount}" success_url=root_url + reverse('payments:view', args=(payment.id,)),
data-email="{email}" cancel_url=root_url + reverse('payments:cancel', args=(payment.id,)),
data-locale="auto" payment_method_types=['card'],
data-zip-code="true" line_items=[
data-alipay="true"> {
</script> 'amount': payment.amount,
</form> 'currency': self.currency.lower(),
''' 'name': self.title,
return form.format( 'description': desc,
post=reverse('payments:cb_stripe', args=(payment.id,)), 'quantity': 1,
pubkey=self.pubkey, }
img=self.header_image, ],
email=payment.user.email or '',
name=self.name,
desc=desc,
amount=payment.amount,
curr=self.currency,
) )
payment.backend_extid = session['id']
payment.backend_data = {'session_id': session['id']}
payment.save()
return self.make_redirect(session)
def new_subscription(self, subscr): def new_subscription(self, subscr):
desc = 'Subscription (' + str(subscr.period) + ') for ' + subscr.user.username root_url = project_settings.ROOT_URL
form = ''' assert root_url
<form action="{post}" method="POST">
<script session = self.stripe.checkout.Session.create(
src="https://checkout.stripe.com/checkout.js" class="stripe-button" success_url=root_url + reverse('payments:return_subscr', args=(subscr.id,)),
data-key="{pubkey}" cancel_url=root_url + reverse('payments:cancel_subscr', args=(subscr.id,)),
data-image="{img}" client_reference_id='sub_%d'%subscr.id,
data-name="{name}" payment_method_types=['card'],
data-currency="{curr}" subscription_data={
data-description="{desc}" 'items': [{
data-amount="{amount}" 'plan': self.get_plan_id(subscr.period),
data-email="{email}" 'quantity': 1,
data-locale="auto" }],
data-zip-code="true" },
data-alipay="true">
</script>
</form>
<noscript><p>Please enable JavaScript to use the payment form.</p></noscript>
'''
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,
) )
subscr.backend_data = {'session_id': session['id']}
subscr.save()
return self.make_redirect(session)
def cancel_subscription(self, subscr): def cancel_subscription(self, subscr):
if subscr.status not in ('new', 'unconfirmed', 'active'): if subscr.status not in ('new', 'unconfirmed', 'active'):
return return
try: if subscr.backend_extid.startswith('pi'):
cust = self.stripe.Customer.retrieve(subscr.backend_extid) # a session that didn't create a subscription yet (intent)
except self.stripe.error.InvalidRequestError:
return
try:
# Delete customer and cancel any active subscription
cust.delete()
except self.stripe.error.InvalidRequestError:
pass 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.status = 'cancelled'
subscr.save() subscr.save()
def callback(self, payment, request): def webhook_session_completed(self, event):
post_data = request.POST session = event['data']['object']
token = post_data.get('stripeToken') if session['subscription']:
if not token: # Subscription creation
payment.status = 'cancelled' from payments.models import Payment, Subscription
payment.status_message = _("No payment information was received.")
return
months = int(payment.time.days / 30) sub_id = session['subscription']
username = payment.user.username assert sub_id
try: parts = session['client_reference_id'].split('_')
charge = self.stripe.Charge.create( if len(parts) != 2 or parts[0] != 'sub':
amount=payment.amount, raise Exception("invalid reference id")
currency=self.currency, sub_internal_id = int(parts[1])
card=token,
description="%d months for %s" % (months, username),
)
payment.backend_extid = charge['id']
if charge['refunded'] or not charge['paid']: # Fetch sub by ID and confirm it
payment.status = 'rejected' subscr = Subscription.objects.get(id=sub_internal_id)
payment.status_message = _("The payment has been refunded or rejected.") subscr.status = 'active'
payment.save() subscr.backend_extid = sub_id
return 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.user.vpnuser.add_paid_time(payment.time)
payment.status = 'error' payment.user.vpnuser.on_payment_confirmed(payment)
payment.status_message = _("The paid amount is under the required amount.") payment.user.vpnuser.save()
payment.save() payment.save()
return
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 = 'confirmed'
payment.status_message = None payment.status_message = None
@ -151,54 +171,47 @@ class StripeBackend(BackendBase):
payment.user.vpnuser.on_payment_confirmed(payment) payment.user.vpnuser.on_payment_confirmed(payment)
payment.user.vpnuser.save() payment.user.vpnuser.save()
except self.stripe.error.CardError as e: def get_subscription_from_invoice(self, invoice):
payment.status = 'rejected' from payments.models import Subscription
payment.status_message = e.json_body['error']['message']
payment.save()
def callback_subscr(self, subscr, request): subscription_id = invoice['subscription']
post_data = request.POST customer_id = invoice['customer']
token = post_data.get('stripeToken')
if not token:
subscr.status = 'cancelled'
subscr.save()
return
try: # once it's confirmed, the id to the subscription is stored as extid
cust = self.stripe.Customer.create( subscr = Subscription.objects.filter(backend_extid=subscription_id).first()
source=token, if subscr:
plan=self.get_plan_id(subscr.period), return subscr
)
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
# We don't know much about the new Payment, but we know it # older subscriptions will have a customer id instead
# succeeded. Wekhooks aren't very reliable, so let's mark it as active subscr = Subscription.objects.filter(backend_extid=customer_id).first()
# anyway. if subscr:
subscr.status = 'active' return subscr
subscr.backend_extid = cust['id']
subscr.save() return None
def webhook_payment_succeeded(self, event): 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'] 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 # Prevent making duplicate Payments if event is received twice
pc = Payment.objects.filter(backend_extid=invoice['id']).count() pc = Payment.objects.filter(backend_extid=invoice['id']).count()
if pc > 0: if pc > 0:
return return
subscr = Subscription.objects.get(backend_extid=customer_id)
payment = subscr.create_payment() payment = subscr.create_payment()
payment.status = 'confirmed' payment.status = 'confirmed'
payment.paid_amount = invoice['total'] payment.paid_amount = payment.amount
payment.backend_extid = invoice['id'] payment.backend_extid = invoice['id']
if invoice['subscription']:
payment.backend_sub_id = invoice['subscription']['id']
payment.backend_data = {'event_id': event['id']} payment.backend_data = {'event_id': event['id']}
payment.save() payment.save()
@ -207,26 +220,51 @@ class StripeBackend(BackendBase):
payment.user.vpnuser.save() payment.user.vpnuser.save()
payment.save() payment.save()
subscr.status = 'active'
subscr.save()
def webhook(self, request): def webhook(self, request):
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
try: try:
event_json = json.loads(request.body.decode('utf-8')) event = self.stripe.Webhook.construct_event(
event = self.stripe.Event.retrieve(event_json["id"]) payload, sig_header, self.wh_key,
except (ValueError, self.stripe.error.InvalidRequestError): )
except (ValueError, self.stripe.error.InvalidRequestError, self.stripe.error.SignatureVerificationError):
return False return False
if event['type'] == 'invoice.payment_succeeded': if event['type'] == 'invoice.payment_succeeded':
self.webhook_payment_succeeded(event) self.webhook_payment_succeeded(event)
if event['type'] == 'checkout.session.completed':
self.webhook_session_completed(event)
return True return True
def get_ext_url(self, payment): def get_ext_url(self, payment):
if not payment.backend_extid: extid = payment.backend_extid
if not extid:
return None 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): def get_subscr_ext_url(self, subscr):
if not subscr.backend_extid: extid = subscr.backend_extid
if not extid:
return None 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

@ -1,3 +1,4 @@
import logging
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -7,6 +8,8 @@ from datetime import timedelta
from ccvpn.common import get_price from ccvpn.common import get_price
from .backends import BackendBase from .backends import BackendBase
logger = logging.getLogger(__name__)
backends_settings = settings.PAYMENTS_BACKENDS backends_settings = settings.PAYMENTS_BACKENDS
assert isinstance(backends_settings, dict) assert isinstance(backends_settings, dict)
@ -39,6 +42,8 @@ SUBSCR_PERIOD_CHOICES = (
('12m', _("Every year")), ('12m', _("Every year")),
) )
BACKEND_CLASSES = BackendBase.__subclasses__()
# All known backends (classes) # All known backends (classes)
BACKENDS = {} BACKENDS = {}
BACKEND_CHOICES = [] BACKEND_CHOICES = []
@ -47,11 +52,14 @@ BACKEND_CHOICES = []
ACTIVE_BACKENDS = {} ACTIVE_BACKENDS = {}
ACTIVE_BACKEND_CHOICES = [] ACTIVE_BACKEND_CHOICES = []
for cls in BackendBase.__subclasses__(): logger.info("loading payment backends...")
for cls in BACKEND_CLASSES:
name = cls.backend_id name = cls.backend_id
prefix = "Backend {:<8}:".format(name)
assert isinstance(name, str) assert isinstance(name, str)
if name not in backends_settings: if name not in backends_settings:
logger.info("%s ☒ disabled (no settings)", prefix)
continue continue
backend_settings = backends_settings.get(name, {}) backend_settings = backends_settings.get(name, {})
@ -59,10 +67,11 @@ for cls in BackendBase.__subclasses__():
if hasattr(v, '__call__'): if hasattr(v, '__call__'):
backend_settings[k] = v() backend_settings[k] = v()
if not backend_settings.get('enabled'):
logger.info("%s ☒ disabled (by settings)", prefix)
continue
obj = cls(backend_settings) 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 BACKENDS[name] = obj
BACKEND_CHOICES.append((name, cls.backend_verbose_name)) BACKEND_CHOICES.append((name, cls.backend_verbose_name))
@ -70,10 +79,15 @@ for cls in BackendBase.__subclasses__():
if obj.backend_enabled: if obj.backend_enabled:
ACTIVE_BACKENDS[name] = obj ACTIVE_BACKENDS[name] = obj
ACTIVE_BACKEND_CHOICES.append((name, cls.backend_verbose_name)) 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]) BACKEND_CHOICES = sorted(BACKEND_CHOICES, key=lambda x: x[0])
ACTIVE_BACKEND_CHOICES = sorted(ACTIVE_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): def period_months(p):
return { return {

@ -13,9 +13,9 @@ class CoinGateBackendTest(TestCase):
self.user = User.objects.create_user('test', 'test_user@example.com', None) self.user = User.objects.create_user('test', 'test_user@example.com', None)
self.backend_settings = dict( self.backend_settings = dict(
API_TOKEN='test', api_token='test',
TITLE='Test Title', title='Test Title',
CURRENCY='EUR', currency='EUR',
) )
def test_payment(self): def test_payment(self):

@ -143,10 +143,10 @@ class PaypalBackendTest(TestCase):
) )
settings = dict( settings = dict(
TEST=True, test=True,
TITLE='Test Title', title='Test Title',
CURRENCY='EUR', currency='EUR',
ADDRESS='test_business@example.com', address='test_business@example.com',
) )
with self.settings(ROOT_URL='root'): with self.settings(ROOT_URL='root'):
@ -196,10 +196,10 @@ class PaypalBackendTest(TestCase):
) )
settings = dict( settings = dict(
TEST=True, test=True,
TITLE='Test Title', title='Test Title',
CURRENCY='EUR', currency='EUR',
ADDRESS='test_business@example.com', address='test_business@example.com',
) )
with self.settings(ROOT_URL='root'): with self.settings(ROOT_URL='root'):
@ -235,10 +235,10 @@ class PaypalBackendTest(TestCase):
) )
settings = dict( settings = dict(
TEST=True, test=True,
TITLE='Test Title', title='Test Title',
CURRENCY='EUR', currency='EUR',
ADDRESS='test_business@example.com', address='test_business@example.com',
) )
with self.settings(ROOT_URL='root'): with self.settings(ROOT_URL='root'):

@ -20,10 +20,11 @@ class StripeBackendTest(TestCase):
) )
settings = dict( settings = dict(
API_KEY='test_secret_key', secret_key='test_secret_key',
PUBLIC_KEY='test_public_key', public_key='test_public_key',
CURRENCY='EUR', wh_key='test_wh_key',
NAME='Test Name', currency='EUR',
name='Test Name',
) )
with self.settings(ROOT_URL='root'): with self.settings(ROOT_URL='root'):

@ -3,7 +3,7 @@ from django.shortcuts import render, redirect
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required 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.views.decorators.csrf import csrf_exempt
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
@ -13,6 +13,15 @@ from .forms import NewPaymentForm
from .models import Payment, Subscription, BACKENDS, ACTIVE_BACKENDS 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 @login_required
def new(request): def new(request):
if request.method != 'POST': if request.method != 'POST':
@ -68,11 +77,10 @@ def new(request):
@csrf_exempt @csrf_exempt
def callback_paypal(request, id): def callback_paypal(request, id):
""" PayPal IPN """ """ PayPal IPN """
if not BACKENDS['paypal'].backend_enabled: backend = require_backend('paypal')
return HttpResponseNotFound()
p = Payment.objects.get(id=id) p = Payment.objects.get(id=id)
if BACKENDS['paypal'].callback(p, request): if backend.callback(p, request):
return HttpResponse() return HttpResponse()
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -82,22 +90,20 @@ def callback_paypal(request, id):
@login_required @login_required
def callback_stripe(request, id): def callback_stripe(request, id):
""" Stripe button POST """ """ Stripe button POST """
if not BACKENDS['stripe'].backend_enabled: backend = require_backend('stripe')
return HttpResponseNotFound()
p = Payment.objects.get(id=id) p = Payment.objects.get(id=id)
BACKENDS['stripe'].callback(p, request) backend.callback(p, request)
return redirect(reverse('payments:view', args=(id,))) return redirect(reverse('payments:view', args=(id,)))
@csrf_exempt @csrf_exempt
def callback_coingate(request, id): def callback_coingate(request, id):
""" CoinGate payment callback """ """ CoinGate payment callback """
if not BACKENDS['coingate'].backend_enabled: backend = require_backend('coingate')
return HttpResponseNotFound()
p = Payment.objects.get(id=id) p = Payment.objects.get(id=id)
if BACKENDS['coingate'].callback(p, request): if backend.callback(p, request):
return HttpResponse() return HttpResponse()
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -105,10 +111,9 @@ def callback_coingate(request, id):
@csrf_exempt @csrf_exempt
def callback_coinbase(request): def callback_coinbase(request):
if not BACKENDS['coinbase'].backend_enabled: backend = require_backend('coinbase')
return HttpResponseNotFound()
if BACKENDS['coinbase'].callback(Payment, request): if backend.callback(Payment, request):
return HttpResponse() return HttpResponse()
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -117,11 +122,10 @@ def callback_coinbase(request):
@csrf_exempt @csrf_exempt
def callback_paypal_subscr(request, id): def callback_paypal_subscr(request, id):
""" PayPal Subscription IPN """ """ PayPal Subscription IPN """
if not BACKENDS['paypal'].backend_enabled: backend = require_backend('paypal')
return HttpResponseNotFound()
p = Subscription.objects.get(id=id) p = Subscription.objects.get(id=id)
if BACKENDS['paypal'].callback_subscr(p, request): if paypal.callback_subscr(p, request):
return HttpResponse() return HttpResponse()
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -131,11 +135,10 @@ def callback_paypal_subscr(request, id):
@login_required @login_required
def callback_stripe_subscr(request, id): def callback_stripe_subscr(request, id):
""" Stripe subscription form target """ """ Stripe subscription form target """
if not BACKENDS['stripe'].backend_enabled: backend = require_backend('stripe')
return HttpResponseNotFound()
p = Subscription.objects.get(id=id) 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': if p.status == 'error' or p.status == 'cancelled':
messages.add_message(request, messages.ERROR, messages.add_message(request, messages.ERROR,
_("Error subscribing. It usually means you don't" _("Error subscribing. It usually means you don't"
@ -147,10 +150,9 @@ def callback_stripe_subscr(request, id):
@csrf_exempt @csrf_exempt
def stripe_hook(request): def stripe_hook(request):
if not BACKENDS['stripe'].backend_enabled: backend = require_backend('stripe')
return HttpResponseNotFound()
if BACKENDS['stripe'].webhook(request): if backend.webhook(request):
return HttpResponse() return HttpResponse()
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()

Loading…
Cancel
Save