|
|
|
@ -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 '''
|
|
|
|
|
<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):
|
|
|
|
|
desc = str(payment.time) + ' for ' + payment.user.username
|
|
|
|
|
form = '''
|
|
|
|
|
<form action="{post}" method="POST">
|
|
|
|
|
<script
|
|
|
|
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
|
|
|
|
data-key="{pubkey}"
|
|
|
|
|
data-image="{img}"
|
|
|
|
|
data-name="{name}"
|
|
|
|
|
data-currency="{curr}"
|
|
|
|
|
data-description="{desc}"
|
|
|
|
|
data-amount="{amount}"
|
|
|
|
|
data-email="{email}"
|
|
|
|
|
data-locale="auto"
|
|
|
|
|
data-zip-code="true"
|
|
|
|
|
data-alipay="true">
|
|
|
|
|
</script>
|
|
|
|
|
</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 = '''
|
|
|
|
|
<form action="{post}" method="POST">
|
|
|
|
|
<script
|
|
|
|
|
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
|
|
|
|
|
data-key="{pubkey}"
|
|
|
|
|
data-image="{img}"
|
|
|
|
|
data-name="{name}"
|
|
|
|
|
data-currency="{curr}"
|
|
|
|
|
data-description="{desc}"
|
|
|
|
|
data-amount="{amount}"
|
|
|
|
|
data-email="{email}"
|
|
|
|
|
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,
|
|
|
|
|
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
|
|
|
|
|