Commits

Anonymous committed 745fbd3

Add first hack at a Stripe payment module. Still need to implement handling of Stripe Webhook callbacks and provide default templates. All of my templates are specific to my use of this module, so I need to write templates for one of the example apps.

Comments (0)

Files changed (8)

satchmo/apps/payment/modules/stripe/__init__.py

Empty file added.

satchmo/apps/payment/modules/stripe/config.py

+from livesettings import *
+from django.utils.translation import ugettext_lazy as _
+
+# this is so that the translation utility will pick up the string
+gettext = lambda s: s
+
+PAYMENT_GROUP = ConfigurationGroup('PAYMENT_STRIPE',
+    _('Stripe Module Settings'),
+    ordering = 101)
+
+config_register_list(
+
+    BooleanValue(PAYMENT_GROUP,
+                 'LIVE',
+                 description=_("Accept real payments"),
+                 help_text=_("False if you want to be in test mode"),
+                 default=False),
+
+    StringValue(PAYMENT_GROUP,
+        'API_KEY',
+        description=_("Stripe Private API Key"),
+        help_text=_("Stripe private API key."),
+        default = ""),
+
+    StringValue(PAYMENT_GROUP,
+        'PUBLISHABLE_KEY',
+        description=_("Stripe Publishable Key"),
+        help_text=_("Stripe publishable key."),
+        default = ""),
+
+    StringValue(PAYMENT_GROUP,
+        'TEST_API_KEY',
+        description=_("Stripe Private Test API Key"),
+        help_text=_("Stripe private test API key."),
+        default = ""),
+
+    StringValue(PAYMENT_GROUP,
+        'PUBLISHABLE_TEST_KEY',
+        description=_("Stripe Publishable Test Key"),
+        help_text=_("Stripe publishable test key."),
+        default = ""),
+
+    ModuleValue(PAYMENT_GROUP,
+        'MODULE',
+        description=_('Implementation module'),
+        hidden=True,
+        default = 'payment.modules.stripe'),
+
+    StringValue(PAYMENT_GROUP,
+        'KEY',
+        description=_("Module key"),
+        hidden=True,
+        default = 'STRIPE'),
+
+    StringValue(PAYMENT_GROUP,
+        'LABEL',
+        description=_('English name for this group on the checkout screens'),
+        default = 'Stripe',
+        help_text = _('This will be passed to the translation utility')),
+
+    StringValue(PAYMENT_GROUP,
+        'URL_BASE',
+        description=_('The url base used for constructing urlpatterns which will use this module'),
+        default = '^stripe/'),
+
+    BooleanValue(PAYMENT_GROUP,
+        'EXTRA_LOGGING',
+        description=_("Verbose logs"),
+        help_text=_("Add extensive logs during post."),
+        default=False)
+
+)
+
+PAYMENT_GROUP['TEMPLATE_OVERRIDES'] = {
+    'shop/checkout/confirm.html' : 'shop/checkout/stripe/confirm.html',
+}

satchmo/apps/payment/modules/stripe/forms.py

+import logging
+from django.utils.translation import ugettext as _
+from django import forms
+from payment.forms import SimplePayShipForm
+from models import StripeToken
+
+from signals_ahoy.signals import form_presave, form_postsave
+
+log = logging.getLogger('payment.stripe.forms')
+
+class StripePayShipForm(SimplePayShipForm):
+    payment_token = forms.CharField(max_length=50)
+    display_cc = forms.CharField(max_length=4)
+
+    def __init__(self, request, paymentmodule, *args, **kwargs):
+        super(StripePayShipForm, self).__init__(request, paymentmodule, *args, **kwargs)
+        self.stripe_token = None
+
+    def clean_payment_token(self):
+        if len(self.cleaned_data['payment_token']) == 0:
+            raise forms.ValidationError(_('Invalid Stripe Token'))
+        # TODO(rjryan): check token w/ stripe API
+        return self.cleaned_data['payment_token']
+
+    def save(self, request, cart, contact, payment_module, data=None):
+        """Save the order and the credit card information for this orderpayment"""
+        form_presave.send(StripePayShipForm, form=self)
+        if data is None:
+            data = self.cleaned_data
+        assert(data)
+        super(StripePayShipForm, self).save(request, cart, contact, payment_module, data=data)
+
+        if self.orderpayment:
+            op = self.orderpayment.capture
+            token = StripeToken(
+                orderpayment=op,
+                payment_token = data['payment_token'],
+                display_cc = data['display_cc'])
+            token.save()
+            self.stripe_token = token
+        form_postsave.send(StripePayShipForm, form=self)

satchmo/apps/payment/modules/stripe/models.py

+from django.db import models
+from django.utils.translation import ugettext, ugettext_lazy as _
+from satchmo_store.shop.models import OrderPayment
+import config
+PAYMENT_PROCESSOR=True
+
+class StripeToken(models.Model):
+    orderpayment = models.ForeignKey(OrderPayment, unique=True,
+        related_name="stripe_tokens")
+    payment_token = models.CharField(_("Payment Token"),
+        max_length=128, blank=True, null=True, editable=False)
+    display_cc = models.CharField(_("CC Number (Last 4 digits)"),
+        max_length=4,)

satchmo/apps/payment/modules/stripe/notifications.py

Empty file added.

satchmo/apps/payment/modules/stripe/processor.py

+import stripe
+import decimal
+from django.utils.translation import ugettext as _
+
+from payment.modules.base import BasePaymentProcessor, ProcessorResult
+from forms import StripePayShipForm
+from models import StripeToken
+
+FORM = StripePayShipForm
+
+class PaymentProcessor(BasePaymentProcessor):
+    def __init__(self, settings):
+        super(PaymentProcessor, self).__init__('stripe', settings)
+
+    def _load_api_key(self):
+        live = self.is_live()
+        self.log_extra("Loading Stripe %s API Key", "Live" if live else "Test")
+        if live:
+            stripe.api_key = self.settings.API_KEY.value
+        else:
+            stripe.api_key = self.settings.TEST_API_KEY.value
+
+    def _get_payment_token(self, order):
+        for payment in order.payments.order_by('-time_stamp'):
+            for token in payment.stripe_tokens.all():
+                return token
+        return None
+
+    def _validate_token(self, token):
+        self._load_api_key()
+        try:
+            self.log_extra("Validating token %s with Stripe API",
+                           token.payment_token)
+            stripe_token = stripe.Token.retrieve(token.payment_token)
+            self.log_extra("Stripe API returned token: %s", stripe_token)
+            return not stripe_token.used
+        except Exception, e:
+            self.log_extra("Exception while lookup up token %s: %s",
+                           token.payment_token, e)
+        return False
+
+    def capture_payment(self, testing=False, order=None, amount=None):
+        if not order:
+            order = self.order
+
+        if order.paid_in_full:
+            self.log_extra("%s is paid in full, no capture attempted.", order)
+            self.record_payment()
+            return ProcessorResult(self.key, True, _("No charge needed, paid in full."))
+
+        self.log_extra("Capturing payment for %s", order)
+
+        if amount is None:
+            amount = order.balance
+
+        token = self._get_payment_token(order)
+
+        if not token:
+            return ProcessorResult(
+                self.key, False,
+                _("No valid payment found. Please re-enter your payment information."))
+
+        if not self._validate_token(token):
+            payment = self.record_failure(amount=amount, transaction_id=token.payment_token,
+                reason_code='0', details=_("Failed to validate Stripe token."))
+            return ProcessorResult(
+                self.key, False,
+                _("Could not validate payment authorization. Please re-enter your payment information."),
+                payment=payment)
+
+        # Stripe token is valid. Now create a charge.
+        payment = None
+        try:
+            amount_cents = int(amount * decimal.Decimal('100'))
+            charge = stripe.Charge.create(
+                amount = amount_cents,
+                currency = 'usd', # Stripe only supports USD.
+                card = token.payment_token,
+                description = u'Order %d for %s' % (order.id, order.contact.email))
+
+            payment = self.record_payment(
+                order=order, amount=amount,
+                transaction_id=charge.id, reason_code='0')
+            return ProcessorResult(self.key, True, _('Success'), payment)
+        except stripe.InvalidRequestError, e:
+            self.log_extra("Stripe charge creation failed for %s: %s",
+                           token.payment_token, e)
+            error_code = e.json_body.get('error', {}).get('type', '')
+            payment = self.record_failure(amount=amount, transaction_id=token.payment_token,
+                reason_code=error_code, details=e.message)
+        except Exception, e:
+            self.log_extra("Stripe charge creation failed for %s: %s",
+                           token.payment_token, e)
+        if not payment:
+            payment = self.record_failure(amount=amount, transaction_id=token.payment_token,
+                                          reason_code='0', details=_("Failed to create Stripe charge."))
+        return ProcessorResult(self.key, False,
+                               _('Could not charge your credit card. Please re-enter your payment information.'),
+                               payment=payment)

satchmo/apps/payment/modules/stripe/urls.py

+from django.conf.urls.defaults import patterns
+from satchmo_store.shop.satchmo_settings import get_satchmo_setting
+
+ssl = get_satchmo_setting('SSL', default_value=False)
+
+urlpatterns = patterns('',
+     (r'^$', 'payment.modules.stripe.views.pay_ship_info', {'SSL': ssl}, 'STRIPE_satchmo_checkout-step2'),
+     (r'^confirm/$', 'payment.modules.stripe.views.confirm_info', {'SSL': ssl}, 'STRIPE_satchmo_checkout-step3'),
+     (r'^success/$', 'payment.views.checkout.success', {'SSL': ssl}, 'STRIPE_satchmo_checkout-success'),
+     (r'^notification/$', 'payment.modules.stripe.views.notification', {'SSL': ssl},
+        'STRIPE_satchmo_checkout-notification'),
+     (r'^confirmorder/$', 'payment.views.confirm.confirm_free_order',
+        {'SSL' : ssl, 'key' : 'STRIPE'}, 'STRIPE_satchmo_checkout_free-confirm')
+)

satchmo/apps/payment/modules/stripe/views.py

+from django import http
+from django.shortcuts import render_to_response
+from django.template import Context, RequestContext
+from django.template.loader import get_template
+from django.utils.translation import ugettext as _
+from django.views.decorators.cache import never_cache
+from livesettings import config_get_group, config_value
+from payment.config import gateway_live
+from payment.views import confirm, payship
+from satchmo_store.shop.models import Order
+from satchmo_store.shop.satchmo_settings import get_satchmo_setting
+from satchmo_utils.dynamic import lookup_url, lookup_template
+from satchmo_utils.views import bad_or_missing
+import base64
+import hmac
+import logging
+import notifications
+import sha
+
+log = logging.getLogger("payment.modules.stripe.processor")
+
+@never_cache
+def pay_ship_info(request):
+    template = 'shop/checkout/stripe/pay_ship.html'
+    payment_module = config_get_group('PAYMENT_STRIPE')
+    form_handler = payship.credit_pay_ship_process_form
+    result = payship.pay_ship_info_verify(request, payment_module)
+    if not result[0]:
+        return result[1]
+
+    contact = result[1]
+    working_cart = result[2]
+
+    success, form = form_handler(request, contact, working_cart, payment_module)
+    if success:
+        return form
+
+    template = lookup_template(payment_module, template)
+    live = gateway_live(payment_module)
+    if live:
+        stripe_publishable_key = payment_module.PUBLISHABLE_KEY.value
+    else:
+        stripe_publishable_key = payment_module.PUBLISHABLE_TEST_KEY.value
+    log.debug("Stripe live: %s publishable key: %s", live, stripe_publishable_key)
+
+    ctx = RequestContext(request, {
+        'form': form,
+        'STRIPE_PUBLISHABLE_KEY': stripe_publishable_key,
+        'PAYMENT_LIVE': live,
+        })
+    return render_to_response(template, context_instance=ctx)
+
+@never_cache
+def confirm_info(request):
+    payment_module = config_get_group('PAYMENT_STRIPE')
+
+    controller = confirm.ConfirmController(request, payment_module)
+    if not controller.sanity_check():
+        return controller.response
+
+    live = gateway_live(payment_module)
+
+    default_view_tax = config_value('TAX', 'DEFAULT_VIEW_TAX')
+
+    ctx = {
+        'PAYMENT_LIVE' : live
+    }
+
+    controller.extra_context = ctx
+    controller.confirm()
+    return controller.response
+
+@never_cache
+def notification(request):
+    """
+    View to receive notifications from Stripe
+    """
+    data = request.POST
+    log.debug("Stripe Notification: %s", data)
+
+    # Stripe treats any 200 response as a confirmation.
+    response = http.HttpResponse('')
+    return response