2019-01-08 20:51:36 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2019-01-09 12:19:00 +00:00
|
|
|
|
2017-12-16 01:09:29 +00:00
|
|
|
"""Payment forms."""
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
from django import forms
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2019-01-08 19:53:00 +00:00
|
|
|
from stripe import Charge, Customer
|
|
|
|
from stripe.error import InvalidRequestError
|
2015-10-05 01:05:13 +00:00
|
|
|
|
2015-10-22 17:45:23 +00:00
|
|
|
from .utils import stripe
|
|
|
|
|
2019-01-08 19:53:00 +00:00
|
|
|
|
2015-10-05 01:05:13 +00:00
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-01-03 15:56:33 +00:00
|
|
|
class StripeResourceMixin:
|
2015-10-05 01:05:13 +00:00
|
|
|
|
2017-12-16 01:09:29 +00:00
|
|
|
"""Stripe actions for resources, available as a Form mixin class."""
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def ensure_stripe_resource(self, resource, attrs):
|
|
|
|
try:
|
2015-10-16 21:17:49 +00:00
|
|
|
instance = resource.retrieve(attrs['id'])
|
2015-10-05 01:05:13 +00:00
|
|
|
except (KeyError, InvalidRequestError):
|
|
|
|
try:
|
|
|
|
del attrs['id']
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2015-10-16 23:14:45 +00:00
|
|
|
return resource.create(**attrs)
|
|
|
|
else:
|
2017-05-30 22:09:59 +00:00
|
|
|
for (key, val) in list(attrs.items()):
|
2015-10-16 23:14:45 +00:00
|
|
|
setattr(instance, key, val)
|
|
|
|
instance.save()
|
|
|
|
return instance
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def get_customer_kwargs(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get_customer(self):
|
2019-01-08 20:51:36 +00:00
|
|
|
return self.ensure_stripe_resource(
|
|
|
|
resource=Customer,
|
|
|
|
attrs=self.get_customer_kwargs(),
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def get_subscription_kwargs(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get_subscription(self):
|
|
|
|
customer = self.get_customer()
|
2019-01-08 20:51:36 +00:00
|
|
|
return self.ensure_stripe_resource(
|
|
|
|
resource=customer.subscriptions,
|
|
|
|
attrs=self.get_subscription_kwargs(),
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def get_charge_kwargs(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def get_charge(self):
|
2019-01-08 20:51:36 +00:00
|
|
|
return self.ensure_stripe_resource(
|
|
|
|
resource=Charge,
|
|
|
|
attrs=self.get_charge_kwargs(),
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
|
2015-10-15 06:58:23 +00:00
|
|
|
class StripeModelForm(forms.ModelForm):
|
2015-10-05 01:05:13 +00:00
|
|
|
|
2017-12-16 01:09:29 +00:00
|
|
|
"""
|
|
|
|
Payment form base for Stripe interaction.
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
Use this as a base class for payment forms. It includes the necessary fields
|
|
|
|
for card input and manipulates the Knockout field data bindings correctly.
|
|
|
|
|
|
|
|
:cvar stripe_token: Stripe token passed from Stripe.js
|
|
|
|
:cvar cc_number: Credit card number field, used only by Stripe.js
|
|
|
|
:cvar cc_expiry: Credit card expiry field, used only by Stripe.js
|
|
|
|
:cvar cc_cvv: Credit card security code field, used only by Stripe.js
|
|
|
|
"""
|
|
|
|
|
2018-10-17 11:22:28 +00:00
|
|
|
business_vat_id = forms.CharField(
|
|
|
|
label=_('VAT ID number'),
|
|
|
|
required=False,
|
|
|
|
)
|
|
|
|
|
2015-10-05 01:05:13 +00:00
|
|
|
# Stripe token input from Stripe.js
|
|
|
|
stripe_token = forms.CharField(
|
|
|
|
required=False,
|
2019-01-08 20:51:36 +00:00
|
|
|
widget=forms.HiddenInput(
|
|
|
|
attrs={
|
|
|
|
'data-bind': 'valueInit: stripe_token',
|
2019-01-21 17:49:11 +00:00
|
|
|
},
|
2019-01-08 20:51:36 +00:00
|
|
|
),
|
2015-10-05 01:05:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# Fields used for fetching token with javascript, listed as form fields so
|
|
|
|
# that data can survive validation errors
|
|
|
|
cc_number = forms.CharField(
|
|
|
|
label=_('Card number'),
|
2019-01-08 20:51:36 +00:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={
|
|
|
|
'data-bind': (
|
|
|
|
'valueInit: cc_number, '
|
|
|
|
'textInput: cc_number, '
|
|
|
|
'''css: {'field-error': error_cc_number() != null}'''
|
|
|
|
),
|
2019-01-21 17:49:11 +00:00
|
|
|
},
|
2019-01-08 20:51:36 +00:00
|
|
|
),
|
2015-10-05 01:05:13 +00:00
|
|
|
max_length=25,
|
2019-01-08 20:51:36 +00:00
|
|
|
required=False,
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
cc_expiry = forms.CharField(
|
|
|
|
label=_('Card expiration'),
|
2019-01-08 20:51:36 +00:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={
|
|
|
|
'data-bind': (
|
|
|
|
'valueInit: cc_expiry, '
|
|
|
|
'textInput: cc_expiry, '
|
|
|
|
'''css: {'field-error': error_cc_expiry() != null}'''
|
|
|
|
),
|
2019-01-21 17:49:11 +00:00
|
|
|
},
|
2019-01-08 20:51:36 +00:00
|
|
|
),
|
2015-10-05 01:05:13 +00:00
|
|
|
max_length=10,
|
2019-01-08 20:51:36 +00:00
|
|
|
required=False,
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
cc_cvv = forms.CharField(
|
|
|
|
label=_('Card CVV'),
|
2019-01-08 20:51:36 +00:00
|
|
|
widget=forms.TextInput(
|
|
|
|
attrs={
|
|
|
|
'data-bind': (
|
|
|
|
'valueInit: cc_cvv, '
|
|
|
|
'textInput: cc_cvv, '
|
|
|
|
'''css: {'field-error': error_cc_cvv() != null}'''
|
|
|
|
),
|
|
|
|
'autocomplete': 'off',
|
2019-01-21 17:49:11 +00:00
|
|
|
},
|
2019-01-08 20:51:36 +00:00
|
|
|
),
|
2015-10-05 01:05:13 +00:00
|
|
|
max_length=8,
|
2019-01-08 20:51:36 +00:00
|
|
|
required=False,
|
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.customer = kwargs.pop('customer', None)
|
2019-01-03 15:56:33 +00:00
|
|
|
super().__init__(*args, **kwargs)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def validate_stripe(self):
|
2017-12-16 01:09:29 +00:00
|
|
|
"""
|
|
|
|
Run validation against Stripe.
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
This is what will create several objects using the Stripe API. We need
|
|
|
|
to actually create the objects, as that is what will provide us with
|
|
|
|
validation errors to throw back at the form.
|
|
|
|
|
|
|
|
Form fields can be accessed here via ``self.cleaned_data`` as this
|
|
|
|
method is triggered from the :py:meth:`clean` method. Cleaned form data
|
|
|
|
should already exist on the form at this point.
|
|
|
|
"""
|
2015-10-15 06:55:47 +00:00
|
|
|
raise NotImplementedError
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def clean_stripe_token(self):
|
|
|
|
data = self.cleaned_data['stripe_token']
|
|
|
|
if not data:
|
|
|
|
data = None
|
|
|
|
return data
|
|
|
|
|
|
|
|
def clean(self):
|
2017-12-16 01:09:29 +00:00
|
|
|
"""
|
|
|
|
Clean form to add Stripe objects via API during validation phase.
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
This will handle ensuring a customer and subscription exist and will
|
2017-12-16 01:09:29 +00:00
|
|
|
raise any issues as validation errors. This is required because part of
|
|
|
|
Stripe's validation happens on the API call to establish a subscription.
|
2015-10-05 01:05:13 +00:00
|
|
|
"""
|
2019-01-03 15:56:33 +00:00
|
|
|
cleaned_data = super().clean()
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
# Form isn't valid, no need to try to associate a card now
|
|
|
|
if not self.is_valid():
|
|
|
|
self.clear_card_data()
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.validate_stripe()
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
self.clear_card_data()
|
|
|
|
field_lookup = {
|
|
|
|
'cvc': 'cc_cvv',
|
|
|
|
'number': 'cc_number',
|
|
|
|
'expiry': 'cc_expiry',
|
|
|
|
'exp_month': 'cc_expiry',
|
|
|
|
'exp_year': 'cc_expiry',
|
|
|
|
}
|
|
|
|
error_field = field_lookup.get(e.param, None)
|
|
|
|
self.add_error(
|
|
|
|
error_field,
|
2017-05-31 20:51:04 +00:00
|
|
|
forms.ValidationError(str(e)),
|
2015-10-05 01:05:13 +00:00
|
|
|
)
|
|
|
|
except stripe.error.StripeError as e:
|
2018-04-05 22:57:30 +00:00
|
|
|
log.exception('There was a problem communicating with Stripe')
|
2015-10-05 01:05:13 +00:00
|
|
|
raise forms.ValidationError(
|
2019-01-08 20:51:36 +00:00
|
|
|
_('There was a problem communicating with Stripe'),
|
|
|
|
)
|
2015-10-15 06:55:47 +00:00
|
|
|
return cleaned_data
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def clear_card_data(self):
|
2017-12-16 01:09:29 +00:00
|
|
|
"""
|
|
|
|
Clear card data on validation errors.
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
This requires the form was created by passing in a mutable QueryDict
|
2016-07-15 16:23:01 +00:00
|
|
|
instance, see :py:class:`readthedocs.payments.mixin.StripeMixin`
|
2015-10-05 01:05:13 +00:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self.data['stripe_token'] = None
|
|
|
|
except AttributeError:
|
2019-01-08 20:51:36 +00:00
|
|
|
raise AttributeError(
|
2019-01-21 17:49:11 +00:00
|
|
|
'Form was passed immutable QueryDict POST data',
|
2019-01-08 20:51:36 +00:00
|
|
|
)
|
2015-10-05 01:05:13 +00:00
|
|
|
|
|
|
|
def fields_with_cc_group(self):
|
|
|
|
group = {
|
|
|
|
'is_cc_group': True,
|
2019-01-08 20:51:36 +00:00
|
|
|
'fields': [],
|
2015-10-05 01:05:13 +00:00
|
|
|
}
|
|
|
|
for field in self:
|
|
|
|
if field.name in ['cc_number', 'cc_expiry', 'cc_cvv']:
|
|
|
|
group['fields'].append(field)
|
|
|
|
else:
|
|
|
|
yield field
|
|
|
|
yield group
|