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 class StripeBackend(BackendBase): backend_id = 'stripe' backend_verbose_name = _("Stripe") backend_display_name = _("Credit Card") backend_has_recurring = True def get_plan_id(self, period): return 'ccvpn_' + period def __init__(self, settings): 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 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): 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): 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 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() return True def refresh_subscription(self, subscr): if subscr.backend_extid.startswith('cus_'): customer = self.stripe.Customer.retrieve(subscr.backend_extid) for s in customer['subscriptions']['data']: if s['status'] == 'active': sub = s break else: return elif subscr.backend_extid.startswith('sub_'): sub = self.stripe.Subscription.retrieve(subscr.backend_extid) else: print("unhandled subscription backend extid: {}".format(subscr.backend_extid)) return if sub['status'] == 'canceled': subscr.status = 'cancelled' if sub['status'] == 'past_due': subscr.status = 'error' def webhook_session_completed(self, event): session = event['data']['object'] if session['subscription']: # Subscription creation from payments.models import Payment, Subscription sub_id = session['subscription'] assert sub_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]) # Fetch sub by ID and confirm it subscr = Subscription.objects.get(id=sub_internal_id) subscr.status = 'active' subscr.backend_extid = sub_id subscr.set_data('subscription_id', sub_id) subscr.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 payment.save() payment.user.vpnuser.add_paid_time(payment.time) payment.user.vpnuser.on_payment_confirmed(payment) payment.user.vpnuser.save() payment.user.vpnuser.lcore_sync() def get_subscription_from_invoice(self, invoice): from payments.models import Subscription subscription_id = invoice['subscription'] customer_id = invoice['customer'] # 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 # 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): """ webhook event for a subscription's succeeded payment """ from payments.models import Payment invoice = event['data']['object'] 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 payment = subscr.create_payment() payment.status = 'confirmed' payment.paid_amount = payment.amount payment.backend_extid = invoice['id'] if invoice['subscription']: if isinstance(invoice['subscription'], str): payment.backend_sub_id = invoice['subscription'] else: payment.backend_sub_id = invoice['subscription']['id'] payment.set_data('event_id', event['id']) payment.set_data('sub_id', payment.backend_sub_id) payment.save() payment.user.vpnuser.add_paid_time(payment.time) payment.user.vpnuser.on_payment_confirmed(payment) payment.user.vpnuser.save() payment.save() payment.user.vpnuser.lcore_sync() def webhook_subscr_update(self, event): from payments.models import Subscription stripe_sub = event['data']['object'] sub = Subscription.objects.get(backend_id='stripe', backend_extid=stripe_sub['id']) if not sub: return if stripe_sub['status'] == 'canceled': sub.status = 'cancelled' if stripe_sub['status'] == 'past_due': sub.status = 'error' sub.save() def webhook(self, request): payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] try: 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) if event['type'] == 'customer.subscription.deleted': self.webhook_subscr_update(event) return True def get_ext_url(self, payment): extid = payment.backend_extid if not extid: return None 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): extid = subscr.backend_extid if not extid: return None 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