├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.rst ├── django_braintree ├── __init__.py ├── admin.py ├── forms.py ├── models.py ├── templates │ └── django_braintree │ │ ├── fragments │ │ ├── cc_form.html │ │ ├── current_cc_info.html │ │ ├── pay.html │ │ └── payments_billing.html │ │ └── payments_billing.html ├── test_settings.py ├── tests.py ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | env: 6 | - DJANGO=1.3.1 DJANGO_SETTINGS_MODULE="django_braintree.test_settings" 7 | - DJANGO=1.4 DJANGO_SETTINGS_MODULE="django_braintree.test_settings" 8 | install: 9 | - pip install -q Django==$DJANGO --use-mirrors 10 | script: 11 | - python setup.py test -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | http://github.com/Tivix/django-braintree/contributors 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Tivix, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | django-braintree 3 | ================ 4 | 5 | 6 | Installation 7 | ------------ 8 | 9 | - Install django_braintree (ideally in your virtualenv!) using pip or simply getting a copy of the code and putting it in a directory in your codebase. 10 | 11 | ``pip install tivix-django-braintree`` 12 | 13 | 14 | - Add ``django_braintree`` to your Django settings ``INSTALLED_APPS``:: 15 | 16 | INSTALLED_APPS = [ 17 | # ... 18 | "django_braintree", 19 | ] 20 | 21 | - Add these lines in settings.py file:: 22 | 23 | BRAINTREE_MERCHANT = 'your_merchant_key' 24 | BRAINTREE_PUBLIC_KEY = 'your_public_key' 25 | BRAINTREE_PRIVATE_KEY = 'your_private_key' 26 | 27 | from braintree import Configuration, Environment 28 | 29 | Configuration.configure( 30 | Environment.Sandbox, 31 | BRAINTREE_MERCHANT, 32 | BRAINTREE_PUBLIC_KEY, 33 | BRAINTREE_PRIVATE_KEY 34 | ) 35 | 36 | - Add url to urls.py:: 37 | 38 | url(r'', include('django_braintree.urls')), 39 | 40 | - If you're using South for schema migrations run ``python manage.py migrate django_braintree`` or simply do a ``syncdb``. 41 | 42 | 43 | Additional Information 44 | ---------------------- 45 | 46 | - Braintree uses default templates:: 47 | 48 | django_braintree/payments_billing.html 49 | django_braintree/fragments/cc_form.html 50 | django_braintree/fragments/current_cc_info.html 51 | django_braintree/fragments/pay.html 52 | django_braintree/fragments/payments_billing.html 53 | 54 | - Braintree requires including the js from ``django_common`` that enables ajax forms etc. ``django_common`` is available at https://github.com/Tivix/django-common 55 | - If a template variable ``cc_form_post_url`` is passed to the template then this form posts to it, otherwise it posts to the url ``payments_billing``. 56 | - If a template variable ``cc_form_success_redirect_url`` is passed it takes user to that url then after form post has succeeded. 57 | - Braintree is set up to sandbox mode at default. To change this you must switch ``Environment.Sandbox`` to ``Environment.Production`` in settings file. 58 | 59 | 60 | Revision History 61 | ---------------- 62 | 63 | - v0.1.2 Changed urls.py to be compatible with Django 1.4+ 64 | 65 | 66 | This opensource app is brought to you by Tivix, Inc. ( http://tivix.com/ ) 67 | -------------------------------------------------------------------------------- /django_braintree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tivix/django-braintree/7beb2c8392c2a454c36b353818f3e1db20511ef9/django_braintree/__init__.py -------------------------------------------------------------------------------- /django_braintree/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from django_braintree.models import UserVault, PaymentLog 4 | 5 | 6 | admin.site.register(UserVault) 7 | admin.site.register(PaymentLog) 8 | -------------------------------------------------------------------------------- /django_braintree/forms.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | from django import forms 5 | 6 | from django_common.helper import md5_hash 7 | from braintree import Customer, CreditCard 8 | from django_braintree.models import UserVault 9 | 10 | 11 | class UserCCDetailsForm(forms.Form): 12 | __MONTH_CHOICES = ( 13 | (1, 'January'), 14 | (2, 'February'), 15 | (3, 'March'), 16 | (4, 'April'), 17 | (5, 'May'), 18 | (6, 'June'), 19 | (7, 'July'), 20 | (8, 'August'), 21 | (9, 'September'), 22 | (10, 'October'), 23 | (11, 'November'), 24 | (12, 'December'), 25 | ) 26 | 27 | __YEAR_CHOICES = ( 28 | (2010, '2010'), 29 | (2011, '2011'), 30 | (2012, '2012'), 31 | (2013, '2013'), 32 | (2014, '2014'), 33 | (2015, '2015'), 34 | (2016, '2016'), 35 | (2017, '2017'), 36 | (2018, '2018'), 37 | (2019, '2019'), 38 | (2020, '2020'), 39 | ) 40 | 41 | name = forms.CharField(max_length=64, label='Name as on card') 42 | 43 | cc_number = forms.CharField(max_length=16, label='Credit Card Number') 44 | expiration_month = forms.ChoiceField(choices=__MONTH_CHOICES) 45 | expiration_year = forms.ChoiceField(choices=__YEAR_CHOICES) 46 | 47 | zip_code = forms.CharField(max_length=8, label='Zip Code') 48 | cvv = forms.CharField(max_length=4, label='CVV') 49 | 50 | def __init__(self, user, post_to_update=False, *args, **kwargs): 51 | """ 52 | Takes in a user to figure out whether a vault id exists or not etc. 53 | 54 | @post_to_update: if set to True, then form contents are meant to be posted to Braintree, otherwise its implied 55 | this form is meant for rendering to the user, hence initialize with braintree data (if any). 56 | """ 57 | self.__user = user 58 | self.__user_vault = UserVault.objects.get_user_vault_instance_or_none(user) 59 | 60 | if not post_to_update and self.__user_vault and not args: 61 | logging.debug('Looking up payment info for vault_id: %s' % self.__user_vault.vault_id) 62 | 63 | try: 64 | response = Customer.find(self.__user_vault.vault_id) 65 | info = response.credit_cards[0] 66 | 67 | initial = { 68 | 'name': info.cardholder_name, 69 | 'cc_number': info.masked_number, 70 | 'expiration_month': int(info.expiration_month), 71 | 'expiration_year': info.expiration_year, 72 | 'zip_code': info.billing_address.postal_code, 73 | } 74 | super(UserCCDetailsForm, self).__init__(initial=initial, *args, **kwargs) 75 | except Exception, e: 76 | logging.error('Was not able to get customer from vault. %s' % e) 77 | super(UserCCDetailsForm, self).__init__(initial = {'name': '%s %s' % (user.first_name, user.last_name)}, 78 | *args, **kwargs) 79 | else: 80 | super(UserCCDetailsForm, self).__init__(initial = {'name': '%s %s' % (user.first_name, user.last_name)}, 81 | *args, **kwargs) 82 | 83 | def clean(self): 84 | today = datetime.today() 85 | exp_month = int(self.cleaned_data['expiration_month']) 86 | exp_year = int(int(self.cleaned_data['expiration_year'])) 87 | 88 | if exp_year < today.year or (exp_month <= today.month and exp_year <= today.year): 89 | raise forms.ValidationError('Please make sure your Credit Card expires in the future.') 90 | 91 | return self.cleaned_data 92 | 93 | def save(self, prepend_vault_id=''): 94 | """ 95 | Adds or updates a users CC to the vault. 96 | 97 | @prepend_vault_id: any string to prepend all vault id's with in case the same braintree account is used by 98 | multiple projects/apps. 99 | """ 100 | assert self.is_valid() 101 | 102 | cc_details_map = { # cc details 103 | 'number': self.cleaned_data['cc_number'], 104 | 'cardholder_name': self.cleaned_data['name'], 105 | 'expiration_date': '%s/%s' %\ 106 | (self.cleaned_data['expiration_month'], self.cleaned_data['expiration_year']), 107 | 'cvv': self.cleaned_data['cvv'], 108 | 'billing_address': { 109 | 'postal_code': self.cleaned_data['zip_code'], 110 | } 111 | } 112 | 113 | if self.__user_vault: 114 | try: 115 | # get customer info, its credit card and then update that credit card 116 | response = Customer.find(self.__user_vault.vault_id) 117 | cc_info = response.credit_cards[0] 118 | return CreditCard.update(cc_info.token, params=cc_details_map) 119 | except Exception, e: 120 | logging.error('Was not able to get customer from vault. %s' % e) 121 | self.__user_vault.delete() # delete the stale instance from our db 122 | 123 | # in case the above updating fails or user was never in the vault 124 | new_customer_vault_id = '%s%s' % (prepend_vault_id, md5_hash()[:24]) 125 | respone = Customer.create({ # creating a customer, but we really just want to store their CC details 126 | 'id': new_customer_vault_id, # vault id, uniquely identifies customer. We're not caring about tokens (used for storing multiple CC's per user) 127 | 'credit_card': cc_details_map 128 | }) 129 | 130 | if respone.is_success: # save a new UserVault instance 131 | UserVault.objects.create(user=self.__user, vault_id=new_customer_vault_id) 132 | 133 | return respone 134 | -------------------------------------------------------------------------------- /django_braintree/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from decimal import Decimal 3 | 4 | from django.db import models 5 | from django.contrib.auth.models import User 6 | 7 | from braintree import Transaction 8 | 9 | 10 | class UserVaultManager(models.Manager): 11 | def get_user_vault_instance_or_none(self, user): 12 | """Returns a vault_id string or None""" 13 | qset = self.filter(user=user) 14 | if not qset: 15 | return None 16 | 17 | if qset.count() > 1: 18 | raise Exception('This app does not currently support multiple vault ids') 19 | 20 | return qset.get() 21 | 22 | def is_in_vault(self, user): 23 | return True if self.filter(user=user) else False 24 | 25 | def charge(self, user, vault_id=None): 26 | """If vault_id is not passed this will assume that there is only one instane of user and vault_id in the db.""" 27 | assert self.is_in_vault(user) 28 | if vault_id: 29 | user_vault = self.get(user=user, vault_id=vault_id) 30 | else: 31 | user_vault = self.get(user=user) 32 | 33 | class UserVault(models.Model): 34 | """Keeping it open that one user can have multiple vault credentials, hence the FK to User and not a OneToOne.""" 35 | user = models.ForeignKey(User, unique=True) 36 | vault_id = models.CharField(max_length=64, unique=True) 37 | 38 | objects = UserVaultManager() 39 | 40 | def __unicode__(self): 41 | return self.user.username 42 | 43 | def charge(self, amount): 44 | """ 45 | Charges the users credit card, with he passed $amount, if they are in the vault. Returns the payment_log instance 46 | or None (if charge fails etc.) 47 | """ 48 | try: 49 | result = Transaction.sale( 50 | { 51 | 'amount': amount.quantize(Decimal('.01')), 52 | 'customer_id': self.vault_id, 53 | "options": { 54 | "submit_for_settlement": True 55 | } 56 | } 57 | ) 58 | 59 | if result.is_success: 60 | # create a payment log 61 | payment_log = PaymentLog.objects.create(user=self.user, amount=amount, transaction_id=result.transaction.id) 62 | return payment_log 63 | else: 64 | raise Exception('Logical error in CC transaction') 65 | except Exception: 66 | logging.error('Failed to charge $%s to user: %s with vault_id: %s' % (amount, self.user, self.vault_id)) 67 | return None 68 | 69 | class PaymentLog(models.Model): 70 | """ 71 | Captures raw charges made to a users credit card. Extra info related to this payment should be a OneToOneField 72 | referencing this model. 73 | """ 74 | user = models.ForeignKey(User) 75 | amount = models.DecimalField(max_digits=7, decimal_places=2) 76 | timestamp = models.DateTimeField(auto_now=True) 77 | transaction_id = models.CharField(max_length=128) 78 | 79 | def __unicode__(self): 80 | return '%s charged $%s - %s' % (self.user, self.amount, self.transaction_id) 81 | -------------------------------------------------------------------------------- /django_braintree/templates/django_braintree/fragments/cc_form.html: -------------------------------------------------------------------------------- 1 | {% load custom_tags %} 2 | 3 | {% render_form_field cc_form.name %} 4 | {% render_form_field cc_form.cc_number %} 5 | {% render_form_field cc_form.expiration_month %} 6 | {% render_form_field cc_form.expiration_year %} 7 | {% render_form_field cc_form.zip_code %} 8 | {% render_form_field cc_form.cvv 'The 3 digit code at the back of your card. For AMEX, its the 4 digit code on the front of the card.' %} 9 | -------------------------------------------------------------------------------- /django_braintree/templates/django_braintree/fragments/current_cc_info.html: -------------------------------------------------------------------------------- 1 |
  • Billing Name:

    {{ current_cc_info.cardholder_name }}
  • 2 |
  • Credit Card Number:

    {{ current_cc_info.masked_number }}
  • 3 |
  • Expiration Date:

    {{ current_cc_info.expiration_month }} / {{ current_cc_info.expiration_year }}
  • 4 | -------------------------------------------------------------------------------- /django_braintree/templates/django_braintree/fragments/pay.html: -------------------------------------------------------------------------------- 1 | {# template that lets the user just pay $'s either via a stored CC in vault or through form. Required template variables are cc_form_post_url (the form/post request posts to it) and cc_form_success_redirect_url (takes user to that url then after form/post request has succeeded). #} 2 | 3 |
    4 | {% if current_cc_info %} 5 | 9 | {% else %} 10 |
    {% csrf_token %} 11 | {% include 'django_braintree/fragments/cc_form.html' %} 12 | 13 | 14 |
    15 | {% endif %} 16 |
    17 | 18 | 31 | -------------------------------------------------------------------------------- /django_braintree/templates/django_braintree/fragments/payments_billing.html: -------------------------------------------------------------------------------- 1 | {# Requires including the js from django_common that enables ajax forms etc. If a template variable cc_form_post_url is passed to the template then this form posts to it, otherwise it posts to the url 'payments_billing'. If a template variable cc_form_success_redirect_url is passed it takes user to that url then after form post has succeeded. #} 2 | 3 | 4 |
    5 |

    Your billing information

    6 | {% if current_cc_info %} 7 | 11 | {% endif %} 12 | 13 |
    {% csrf_token %} 14 | {% include 'django_braintree/fragments/cc_form.html' %} 15 | 16 | 17 | 18 | {% if current_cc_info %} 19 | Cancel 20 | {% endif %} 21 |
    22 |
    23 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /django_braintree/templates/django_braintree/payments_billing.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block body %} 4 | {% include 'django_braintree/fragments/payments_billing.html' %} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /django_braintree/test_settings.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | if django.VERSION[:2] >= (1, 3): 4 | DATABASES = { 5 | 'default': { 6 | 'ENGINE': 'django.db.backends.sqlite3', 7 | 'NAME': ':memory:', 8 | } 9 | } 10 | else: 11 | DATABASE_ENGINE = 'sqlite3' 12 | 13 | INSTALLED_APPS = [ 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.humanize', 17 | 'django.contrib.contenttypes', 18 | 'django.contrib.sessions', 19 | 'django.contrib.sites', 20 | 'django.contrib.staticfiles', 21 | 'django_braintree', 22 | ] 23 | -------------------------------------------------------------------------------- /django_braintree/tests.py: -------------------------------------------------------------------------------- 1 | import fudge 2 | from django.test import TestCase 3 | from django.contrib.auth.models import User 4 | from models import UserVault, PaymentLog 5 | from decimal import Decimal 6 | from django.core.management import call_command 7 | from django.db.models import loading 8 | 9 | loading.cache.loaded = False 10 | call_command('syncdb', interactive=False) 11 | 12 | 13 | class FakeTransaction(object): 14 | def __init__(self): 15 | self.id = 1 16 | 17 | 18 | class FakeResponse(object): 19 | def __init__(self): 20 | self.is_success = True 21 | self.transaction = FakeTransaction() 22 | 23 | 24 | @fudge.patch('braintree.Transaction.sale') 25 | def fake_charge(vault, amount, FakeTransactionSale): 26 | (FakeTransactionSale.expects_call() 27 | .with_args() 28 | .returns(FakeResponse()) 29 | ) 30 | vault.charge(Decimal(amount)) 31 | 32 | 33 | class PayTest(TestCase): 34 | 35 | def test_charge(self): 36 | 37 | # Create user vault data 38 | user = User.objects.create_user('test', 'test@tivix.com', 'test') 39 | vault = UserVault.objects.create(user=user, vault_id="1cf373103e6657b96421348a") 40 | 41 | # Try charge account using FUDGE 42 | fake_charge(vault, 10) 43 | 44 | # Check if PaymentLog is Saved 45 | self.failUnlessEqual(PaymentLog.objects.all().count(), 1) 46 | -------------------------------------------------------------------------------- /django_braintree/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import * 2 | 3 | 4 | urlpatterns = patterns('django_braintree.views', 5 | url(r'^payments-billing/$', 'payments_billing', name='payments_billing'), 6 | ) 7 | -------------------------------------------------------------------------------- /django_braintree/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.shortcuts import render 5 | from django.contrib import messages 6 | 7 | from braintree import Customer 8 | from django_common.http import JsonResponse 9 | from django_common.helper import form_errors_serialize 10 | from django_common.decorators import ssl_required 11 | 12 | from django_braintree.forms import UserCCDetailsForm 13 | from django_braintree.models import UserVault 14 | 15 | 16 | BAD_CC_ERROR_MSG = 'Oops! Doesn\'t seem like your Credit Card details are correct. Please re-check and try again.' 17 | 18 | @ssl_required() 19 | @login_required 20 | def payments_billing(request, template='django_braintree/payments_billing.html'): 21 | """ 22 | Renders both the past payments that have occurred on the users credit card, but also their CC information on file 23 | (if any) 24 | """ 25 | d = {} 26 | 27 | if request.method == 'POST': 28 | # Credit Card is being changed/updated by the user 29 | form = UserCCDetailsForm(request.user, True, request.POST) 30 | if form.is_valid(): 31 | response = form.save() 32 | if response.is_success: 33 | messages.add_message(request, messages.SUCCESS, 'Your credit card information has been securely saved.') 34 | return JsonResponse() 35 | else: 36 | return JsonResponse(success=False, errors=[BAD_CC_ERROR_MSG]) 37 | 38 | return JsonResponse(success=False, data={'form': form_errors_serialize(form)}) 39 | else: 40 | if UserVault.objects.is_in_vault(request.user): 41 | try: 42 | response = Customer.find(UserVault.objects.get_user_vault_instance_or_none(request.user).vault_id) 43 | d['current_cc_info'] = response.credit_cards[0] 44 | except Exception, e: 45 | logging.error('Unable to get vault information for user from braintree. %s' % e) 46 | d['cc_form'] = UserCCDetailsForm(request.user) 47 | 48 | return render(request, template, d) 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup, find_packages 5 | from setuptools.command.test import test 6 | except ImportError: 7 | from ez_setup import use_setuptools 8 | use_setuptools() 9 | from setuptools import setup, find_packages 10 | from setuptools.command.test import test 11 | 12 | 13 | import os 14 | 15 | here = os.path.dirname(os.path.abspath(__file__)) 16 | f = open(os.path.join(here, 'README.rst')) 17 | long_description = f.read().strip() 18 | f.close() 19 | 20 | 21 | setup( 22 | name='tivix-django-braintree', 23 | version='0.1.2', 24 | author='Sumit Chachra', 25 | author_email='chachra@tivix.com', 26 | url='http://github.com/tivix/django-braintree', 27 | description = 'An easy way to integrate with Braintree Payment Solutions from Django.', 28 | long_description=long_description, 29 | keywords = 'django braintree payment', 30 | packages=find_packages(), 31 | zip_safe=False, 32 | install_requires=[ 33 | 'Django>=1.4.0', 34 | 'South>=0.7.2', 35 | 'braintree>=2.10.0', 36 | 'django-common>=0.1', 37 | 'fudge==1.0.3' 38 | ], 39 | #dependency_links=["git://github.com/Tivix/django-common.git@91e23cd5e0e8b420e8d4#egg=django_common-0.1"], 40 | test_suite = 'django_braintree.tests', 41 | include_package_data=True, 42 | # cmdclass={}, 43 | classifiers=[ 44 | 'Framework :: Django', 45 | 'Intended Audience :: Developers', 46 | 'Intended Audience :: System Administrators', 47 | 'Operating System :: OS Independent', 48 | 'Topic :: Software Development' 49 | ], 50 | ) 51 | --------------------------------------------------------------------------------