├── .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 |