Commits

Allan Davis committed 8bf4384

Added Django-paypal to app

Comments (0)

Files changed (52)

includes/js/pixels.js

+var total = 0;  
+        var totalPounds = 0;     
+        var droppedCounter = 0;
+        $(document).ready(function() {
+            $(".productItemStyle").draggable({ helper: "clone", opacity: "0.5" });  
+            $(".bagDropZone").droppable( {
+                accept: ".productItemStyle",
+                hoverClass: "dropHover",
+                drop: function(ev, ui) {  
+                    var droppedItem = ui.draggable.clone().addClass("droppedItemStyle");                                                              
+                    var productCode = droppedItem[0].attributes["code"].nodeValue; 
+                    var productPounds = droppedItem[0].attributes["pounds"].nodeValue;  
+                    var productPrice = getFormattedPrice(droppedItem[0].attributes["price"].nodeValue);                                                                          
+                         
+                    // build and insert styled and javascript capable remove links                   
+                    var itemId = droppedItem[0].id + "_" + droppedCounter;                             
+                    var originalHtml = $('#stuff').html();
+                    var divOpen = "<div class=\"foodInBag\" id=\"removeLink" + itemId + "\">";
+                    var divClose = "</div>";
+                    var removeX = "<a href=\"#\" class=\"removeLink\" onClick=\"javascript:remove('" + itemId + "', '" + productPrice + "', '" + productPounds + "')\"><span>X</span></a>";      
+                    var complete =  originalHtml + divOpen + removeX + '<h5>' + productCode + '</h5>' + divClose;                  
+                    $('#stuff').html(complete);
+            
+                    droppedCounter++;                                                                                                                    
+                    updateTotal(productPrice);
+                    updatePounds(productPounds);                    
+                }    
+            });   
+        }); 
+        
+        // remove item from bag, decrease total ans pounds
+        function remove(id, price, pounds) {
+          $(".bagDropZone").children().remove("#" + id);      
+          $('#removeLink' + id).remove();
+             
+          droppedCounter++;         
+
+          updateTotal(price * (-1));     
+          updatePounds(pounds * (-1));             
+        }        
+    
+        // format the code and remove junky stuff! 
+        function getFormattedPrice(unformattedPrice) {    
+            return unformattedPrice.replace(/[\n\r\t$/\s/g]/g, '');           
+        }    
+    
+        // update the total
+        function updateTotal(price) {  
+            total += parseFloat(price);
+            $("#total").html(total.toFixed(2));   
+        }
+    
+        // update the pounds
+        function updatePounds(pounds) {  
+            totalPounds += parseFloat(pounds);          
+            $("#poundTotal").html(totalPounds.toString());
+        }                
+
+
+        // bounce arrow on drag and drop
+        function bounceArrow() {
+           $("#arrow").effect("bounce", { times:3 }, 300);
+        }
+        
+        // scroll to anchors
+        function goToByScroll(anchor){
+     	   $('html,body').animate({scrollTop: $("#" + anchor).offset().top}, 5000);
+	}
+	
+	function checkout(){
+	   $("#lbs").val( $("#poundTotal").html());
+	   $("#amount").val($("#total").html());
+	   $("#checkout_form").submit();
+	}
+        

paypal/.gitignore

+*.pyc
+.svn
+.project
+.pydevproject
+Django PayPal
+=============
+
+
+About
+-----
+
+Django PayPal is a pluggable application that implements with PayPal Payments 
+Standard and Payments Pro.
+
+Before diving in, a quick review of PayPal's payment methods is in order! [PayPal Payments Standard](https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_WebsitePaymentsStandard_IntegrationGuide.pdf) is the "Buy it Now" buttons you may have
+seen floating around the internets. Buyers click on the button and are taken to PayPal's website where they can pay for the product. After completing the purchase PayPal makes an HTTP POST to your  `notify_url`. PayPal calls this process [Instant Payment Notification](https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_OrderMgmt_IntegrationGuide.pdf) (IPN) but you may know it as [webhooks](http://blog.webhooks.org). This method kinda sucks because it drops your customers off at PayPal's website but it's easy to implement and doesn't require SSL.
+
+PayPal Payments Pro allows you to accept payments on your website. It contains two distinct payment flows - Direct Payment allows the user to enter credit card information on your website and pay on your website. Express Checkout sends the user over to PayPal to confirm their payment method before redirecting back to your website for confirmation. PayPal rules state that both methods must be implemented.
+
+There is currently an active discussion over the handling of some of the finer points of the PayPal API and the evolution of this code base - check it out over at [Django PayPal on Google Groups](http://groups.google.com/group/django-paypal).
+
+
+Using PayPal Payments Standard IPN:
+-------------------------------
+
+1. Download the code from GitHub:
+
+        git clone git://github.com/johnboxall/django-paypal.git paypal
+
+1. Edit `settings.py` and add  `paypal.standard.ipn` to your `INSTALLED_APPS` 
+   and `PAYPAL_RECEIVER_EMAIL`:
+
+        # settings.py
+        ...
+        INSTALLED_APPS = (... 'paypal.standard.ipn', ...)
+        ...
+        PAYPAL_RECEIVER_EMAIL = "yourpaypalemail@example.com"
+
+1.  Create an instance of the `PayPalPaymentsForm` in the view where you would 
+    like to collect money. Call `render` on the instance in your template to 
+    write out the HTML.
+
+        # views.py
+        ...
+        from paypal.standard.forms import PayPalPaymentsForm
+        
+        def view_that_asks_for_money(request):
+        
+            # What you want the button to do.
+            paypal_dict = {
+                "business": "yourpaypalemail@example.com",
+                "amount": "10000000.00",
+                "item_name": "name of the item",
+                "invoice": "unique-invoice-id",
+                "notify_url": "http://www.example.com/your-ipn-location/",
+                "return_url": "http://www.example.com/your-return-location/",
+                "cancel_return": "http://www.example.com/your-cancel-location/",
+            
+            }
+            
+            # Create the instance.
+            form = PayPalPaymentsForm(initial=paypal_dict)
+            context = {"form": form}
+            return render_to_response("payment.html", context)
+            
+            
+        <!-- payment.html -->
+        ...
+        <h1>Show me the money!</h1>
+        <!-- writes out the form tag automatically -->
+        {{ form.render }}
+
+1.  When someone uses this button to buy something PayPal makes a HTTP POST to 
+    your "notify_url". PayPal calls this Instant Payment Notification (IPN). 
+    The view `paypal.standard.ipn.views.ipn` handles IPN processing. To set the 
+    correct `notify_url` add the following to your `urls.py`:
+
+        # urls.py
+        ...
+        urlpatterns = patterns('',
+            (r'^something/hard/to/guess/', include('paypal.standard.ipn.urls')),
+        )
+
+1.  Whenever an IPN is processed a signal will be sent with the result of the 
+    transaction. Connect the signals to actions to perform the needed operations
+    when a successful payment is recieved.
+    
+    There are two signals for basic transactions:
+    - `payment_was_succesful` 
+    - `payment_was_flagged`
+    
+    And four signals for subscriptions:
+    - `subscription_cancel` - Sent when a subscription is cancelled.
+    - `subscription_eot` - Sent when a subscription expires.
+    - `subscription_modify` - Sent when a subscription is modified.
+    - `subscription_signup` - Sent when a subscription is created.
+
+	Connect to these signals and update your data accordingly. [Django Signals Documentation](http://docs.djangoproject.com/en/dev/topics/signals/).
+
+        # models.py
+        ...
+        from paypal.standard.ipn.signals import payment_was_successful
+        
+        def show_me_the_money(sender, **kwargs):
+            ipn_obj = sender
+            # Undertake some action depending upon `ipn_obj`.
+            if ipn_obj.custom == "Upgrade all users!":
+                Users.objects.update(paid=True)        
+        payment_was_successful.connect(show_me_the_money)
+        
+        
+Using PayPal Payments Standard PDT:
+-------------------------------
+
+Paypal Payment Data Transfer (PDT) allows you to display transaction details to a customer immediately on return to your site unlike PayPal IPN which may take some seconds. [You will need to enable PDT in your PayPal account to use it.your PayPal account to use it](https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_html_paymentdatatransfer).
+
+1. Download the code from GitHub:
+
+        git clone git://github.com/johnboxall/django-paypal.git paypal
+
+1. Edit `settings.py` and add  `paypal.standard.pdt` to your `INSTALLED_APPS`. Also set `PAYPAL_IDENTITY_TOKEN` - you can find the correct value of this setting from the PayPal website:
+
+        # settings.py
+        ...
+        INSTALLED_APPS = (... 'paypal.standard.pdt', ...)
+        
+        PAYPAL_IDENTITY_TOKEN = "xxx"
+
+1.  Create a view that uses `PayPalPaymentsForm` just like in PayPal IPN.
+
+1.  After someone uses this button to buy something PayPal will return the user to your site at 
+    your "return_url" with some extra GET parameters. PayPal calls this Payment Data Transfer (PDT). 
+    The view `paypal.standard.pdt.views.pdt` handles PDT processing. to specify the correct
+     `return_url` add the following to your `urls.py`:
+
+        # urls.py
+        ...
+        urlpatterns = patterns('',
+            (r'^paypal/pdt/', include('paypal.standard.pdt.urls')),
+            ...
+        )
+
+Using PayPal Payments Standard with Subscriptions:
+-------------------------------
+
+1.  For subscription actions, you'll need to add a parameter to tell it to use the subscription buttons and the command, plus any 
+    subscription-specific settings:
+
+        # views.py
+        ...
+        paypal_dict = {
+            "cmd": "_xclick-subscriptions",
+            "business": "your_account@paypal",
+            "a3": "9.99",                      # monthly price 
+            "p3": 1,                           # duration of each unit (depends on unit)
+            "t3": "M",                         # duration unit ("M for Month")
+            "src": "1",                        # make payments recur
+            "sra": "1",                        # reattempt payment on payment error
+            "no_note": "1",                    # remove extra notes (optional)
+            "item_name": "my cool subscription",
+            "notify_url": "http://www.example.com/your-ipn-location/",
+            "return_url": "http://www.example.com/your-return-location/",
+            "cancel_return": "http://www.example.com/your-cancel-location/",
+        }
+
+        # Create the instance.
+        form = PayPalPaymentsForm(initial=paypal_dict, button_type="subscribe")
+        
+        # Output the button.
+        form.render()
+
+
+Using PayPal Payments Standard with Encrypted Buttons:
+------------------------------------------------------
+
+Use this method to encrypt your button so sneaky gits don't try to hack it. Thanks to [Jon Atkinson](http://jonatkinson.co.uk/) for the [tutorial](http://jonatkinson.co.uk/paypal-encrypted-buttons-django/).
+
+1. Encrypted buttons require the `M2Crypto` library:
+
+        easy_install M2Crypto
+    
+
+1. Encrypted buttons require certificates. Create a private key:
+
+        openssl genrsa -out paypal.pem 1024
+
+1. Create a public key:
+
+        openssl req -new -key paypal.pem -x509 -days 365 -out pubpaypal.pem
+
+1. Upload your public key to the paypal website (sandbox or live).
+        
+    [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert](https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert)
+
+    [https://www.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert](https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-website-cert)
+
+1.  Copy your `cert id` - you'll need it in two steps. It's on the screen where
+    you uploaded your public key.
+
+1. Download PayPal's public certificate - it's also on that screen.
+
+1. Edit your `settings.py` to include cert information:
+
+        # settings.py
+        PAYPAL_PRIVATE_CERT = '/path/to/paypal.pem'
+        PAYPAL_PUBLIC_CERT = '/path/to/pubpaypal.pem'
+        PAYPAL_CERT = '/path/to/paypal_cert.pem'
+        PAYPAL_CERT_ID = 'get-from-paypal-website'
+
+1. Swap out your unencrypted button for a `PayPalEncryptedPaymentsForm`:
+
+        # views.py
+        from paypal.standard.forms import PayPalEncryptedPaymentsForm
+        
+        def view_that_asks_for_money(request):
+            ...
+            # Create the instance.
+            form = PayPalPaymentsForm(initial=paypal_dict)
+            # Works just like before!
+            form.render()
+
+
+Using PayPal Payments Standard with Encrypted Buttons and Shared Secrets:
+-------------------------------------------------------------------------
+
+This method uses Shared secrets instead of IPN postback to verify that transactions
+are legit. PayPal recommends you should use Shared Secrets if:
+
+    * You are not using a shared website hosting service. 
+    * You have enabled SSL on your web server. 
+    * You are using Encrypted Website Payments. 
+    * You use the notify_url variable on each individual payment transaction.
+    
+Use postbacks for validation if: 
+    * You rely on a shared website hosting service 
+    * You do not have SSL enabled on your web server 
+
+1. Swap out your button for a `PayPalSharedSecretEncryptedPaymentsForm`:
+
+        # views.py
+        from paypal.standard.forms import PayPalSharedSecretEncryptedPaymentsForm
+        
+        def view_that_asks_for_money(request):
+            ...
+            # Create the instance.
+            form = PayPalSharedSecretEncryptedPaymentsForm(initial=paypal_dict)
+            # Works just like before!
+            form.render()
+            
+1. Verify that your IPN endpoint is running on SSL - `request.is_secure()` should return `True`!
+
+
+Using PayPal Payments Pro (WPP)
+-------------------------------
+
+WPP is the more awesome version of PayPal that lets you accept payments on your 
+site. WPP reuses code from `paypal.standard` so you'll need to include both 
+apps. [There is an explanation of WPP in the PayPal Forums](http://www.pdncommunity.com/pdn/board/message?board.id=wppro&thread.id=192).
+
+
+1. Edit `settings.py` and add  `paypal.standard` and `paypal.pro` to your 
+   `INSTALLED_APPS`, also set your PayPal settings:
+
+        # settings.py
+        ...
+        INSTALLED_APPS = (... 'paypal.standard', 'paypal.pro', ...)
+        PAYPAL_TEST = True           # Testing mode on
+        PAYPAL_WPP_USER = "???"      # Get from PayPal
+        PAYPAL_WPP_PASSWORD = "???"
+        PAYPAL_WPP_SIGNATURE = "???"
+
+1. Run `python manage.py syncdb` to add the required tables.
+
+1. Write a wrapper view for `paypal.pro.views.PayPalPro`:
+
+        # views.py
+        from paypal.pro.views import PayPalPro
+
+        def buy_my_item(request):
+          item = {"amt": "10.00",             # amount to charge for item
+                  "inv": "inventory",         # unique tracking variable paypal
+                  "custom": "tracking",       # custom tracking variable for you
+                  "cancelurl": "http://...",  # Express checkout cancel url
+                  "returnurl": "http://..."}  # Express checkout return url
+        
+          kw = {"item": item,                            # what you're selling
+                "payment_template": "payment.html",      # template name for payment
+                "confirm_template": "confirmation.html", # template name for confirmation
+                "success_url": "/success/"}              # redirect location after success
+                
+          ppp = PayPalPro(**kw)
+          return ppp(request)
+
+
+1. Create templates for payment and confirmation. By default both templates are 
+   populated with the context variable `form` which contains either a 
+   `PaymentForm` or a `Confirmation` form.
+
+    <!-- payment.html -->
+    <h1>Show me the money</h1>
+    <form method="post" action="">
+      {{ form }}
+      <input type="submit" value="Pay Up">
+    </form>
+    
+    <!-- confirmation.html -->
+    <h1>Are you sure you want to buy this thing?</h1>
+    <form method="post" action="">
+      {{ form }}
+      <input type="submit" value="Yes I Yams">
+    </form>
+
+1. Add your view to `urls.py`, and add the IPN endpoint to receive callbacks 
+   from PayPal:
+
+        # urls.py
+        ...
+        urlpatterns = ('',
+            ...
+            (r'^payment-url/$', 'myproject.views.buy_my_item')
+            (r'^some/obscure/name/', include('paypal.standard.ipn.urls')),
+        )
+
+1. Profit.
+
+
+Links:
+------
+
+1. [Set your IPN Endpoint on the PayPal Sandbox](https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-ipn-notify)
+
+2. [Django PayPal on Google Groups](http://groups.google.com/group/django-paypal)
+
+License (MIT)
+=============
+
+Copyright (c) 2009 Handi Mobility Inc.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.

paypal/__init__.py

Empty file added.

paypal/pro/__init__.py

Empty file added.

paypal/pro/admin.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from string import split as L
+from django.contrib import admin
+from paypal.pro.models import PayPalNVP
+
+
+class PayPalNVPAdmin(admin.ModelAdmin):
+    list_display = L("user method flag flag_code created_at")
+admin.site.register(PayPalNVP, PayPalNVPAdmin)

paypal/pro/creditcard.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Adapted from:
+    - http://www.djangosnippets.org/snippets/764/
+    - http://www.satchmoproject.com/trac/browser/satchmo/trunk/satchmo/apps/satchmo_utils/views.py
+    - http://tinyurl.com/shoppify-credit-cards
+"""
+import re
+
+
+# Well known card regular expressions.
+CARDS = {
+    'Visa': re.compile(r"^4\d{12}(\d{3})?$"),
+    'Mastercard': re.compile(r"(5[1-5]\d{4}|677189)\d{10}$"),
+    'Dinersclub': re.compile(r"^3(0[0-5]|[68]\d)\d{11}"),
+    'Amex': re.compile("^3[47]\d{13}$"),
+    'Discover': re.compile("^(6011|65\d{2})\d{12}$"),
+}
+
+# Well known test numbers
+TEST_NUMBERS = [
+    "378282246310005", "371449635398431", "378734493671000", "30569309025904",
+    "38520000023237", "6011111111111117", "6011000990139424", "555555555554444",
+    "5105105105105100", "4111111111111111", "4012888888881881", "4222222222222"
+]
+
+def verify_credit_card(number):
+    """Returns the card type for given card number or None if invalid."""
+    return CreditCard(number).verify()
+
+class CreditCard(object):
+    def __init__(self, number):
+        self.number = number
+	
+    def is_number(self):
+        """True if there is at least one digit in number."""
+        self.number = re.sub(r'[^\d]', '', self.number)
+        return self.number.isdigit()
+
+    def is_mod10(self):
+        """Returns True if number is valid according to mod10."""
+        double = 0
+        total = 0
+        for i in range(len(self.number) - 1, -1, -1):
+            for c in str((double + 1) * int(self.number[i])):
+                total = total + int(c)
+            double = (double + 1) % 2
+        return (total % 10) == 0
+
+    def is_test(self):
+        """Returns True if number is a test card number."""
+        return self.number in TEST_NUMBERS
+
+    def get_type(self):
+        """Return the type if it matches one of the cards."""
+        for card, pattern in CARDS.iteritems():
+            if pattern.match(self.number):
+                return card
+        return None
+
+    def verify(self):
+        """Returns the card type if valid else None."""
+        if self.is_number() and not self.is_test() and self.is_mod10():
+            return self.get_type()
+        return None

paypal/pro/fields.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from calendar import monthrange
+from datetime import date
+
+from django.db import models
+from django import forms
+from django.utils.translation import ugettext as _
+
+from paypal.pro.creditcard import verify_credit_card
+
+
+class CreditCardField(forms.CharField):
+    """Form field for checking out a credit card."""
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('max_length', 20)
+        super(CreditCardField, self).__init__(*args, **kwargs)
+        
+    def clean(self, value):
+        """Raises a ValidationError if the card is not valid and stashes card type."""
+        self.card_type = verify_credit_card(value)
+        if self.card_type is None:
+            raise forms.ValidationError("Invalid credit card number.")
+        return value
+
+
+# Credit Card Expiry Fields from:
+# http://www.djangosnippets.org/snippets/907/
+class CreditCardExpiryWidget(forms.MultiWidget):
+    """MultiWidget for representing credit card expiry date."""
+    def decompress(self, value):
+        if value:
+            return [value.month, value.year]
+        else:
+            return [None, None]
+
+    def format_output(self, rendered_widgets):
+        html = u' / '.join(rendered_widgets)
+        return u'<span style="white-space: nowrap">%s</span>' % html
+
+class CreditCardExpiryField(forms.MultiValueField):
+    EXP_MONTH = [(x, x) for x in xrange(1, 13)]
+    EXP_YEAR = [(x, x) for x in xrange(date.today().year, date.today().year + 15)]
+
+    default_error_messages = {
+        'invalid_month': u'Enter a valid month.',
+        'invalid_year': u'Enter a valid year.',
+    }
+
+    def __init__(self, *args, **kwargs):
+        errors = self.default_error_messages.copy()
+        if 'error_messages' in kwargs:
+            errors.update(kwargs['error_messages'])
+        
+        fields = (
+            forms.ChoiceField(choices=self.EXP_MONTH, error_messages={'invalid': errors['invalid_month']}),
+            forms.ChoiceField(choices=self.EXP_YEAR, error_messages={'invalid': errors['invalid_year']}),
+        )
+        
+        super(CreditCardExpiryField, self).__init__(fields, *args, **kwargs)
+        self.widget = CreditCardExpiryWidget(widgets=[fields[0].widget, fields[1].widget])
+
+    def clean(self, value):
+        exp = super(CreditCardExpiryField, self).clean(value)
+        if date.today() > exp:
+            raise forms.ValidationError("The expiration date you entered is in the past.")
+        return exp
+
+    def compress(self, data_list):
+        if data_list:
+            if data_list[1] in forms.fields.EMPTY_VALUES:
+                error = self.error_messages['invalid_year']
+                raise forms.ValidationError(error)
+            if data_list[0] in forms.fields.EMPTY_VALUES:
+                error = self.error_messages['invalid_month']
+                raise forms.ValidationError(error)
+            year = int(data_list[1])
+            month = int(data_list[0])
+            # find last day of the month
+            day = monthrange(year, month)[1]
+            return date(year, month, day)
+        return None
+
+
+class CreditCardCVV2Field(forms.CharField):
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('max_length', 4)
+        super(CreditCardCVV2Field, self).__init__(*args, **kwargs)
+        
+
+# Country Field from:
+# http://www.djangosnippets.org/snippets/494/
+# http://xml.coverpages.org/country3166.html
+COUNTRIES = (
+    ('US', _('United States of America')),
+    ('CA', _('Canada')),
+    ('AD', _('Andorra')),
+    ('AE', _('United Arab Emirates')),
+    ('AF', _('Afghanistan')),
+    ('AG', _('Antigua & Barbuda')),
+    ('AI', _('Anguilla')),
+    ('AL', _('Albania')),
+    ('AM', _('Armenia')),
+    ('AN', _('Netherlands Antilles')),
+    ('AO', _('Angola')),
+    ('AQ', _('Antarctica')),
+    ('AR', _('Argentina')),
+    ('AS', _('American Samoa')),
+    ('AT', _('Austria')),
+    ('AU', _('Australia')),
+    ('AW', _('Aruba')),
+    ('AZ', _('Azerbaijan')),
+    ('BA', _('Bosnia and Herzegovina')),
+    ('BB', _('Barbados')),
+    ('BD', _('Bangladesh')),
+    ('BE', _('Belgium')),
+    ('BF', _('Burkina Faso')),
+    ('BG', _('Bulgaria')),
+    ('BH', _('Bahrain')),
+    ('BI', _('Burundi')),
+    ('BJ', _('Benin')),
+    ('BM', _('Bermuda')),
+    ('BN', _('Brunei Darussalam')),
+    ('BO', _('Bolivia')),
+    ('BR', _('Brazil')),
+    ('BS', _('Bahama')),
+    ('BT', _('Bhutan')),
+    ('BV', _('Bouvet Island')),
+    ('BW', _('Botswana')),
+    ('BY', _('Belarus')),
+    ('BZ', _('Belize')),
+    ('CC', _('Cocos (Keeling) Islands')),
+    ('CF', _('Central African Republic')),
+    ('CG', _('Congo')),
+    ('CH', _('Switzerland')),
+    ('CI', _('Ivory Coast')),
+    ('CK', _('Cook Iislands')),
+    ('CL', _('Chile')),
+    ('CM', _('Cameroon')),
+    ('CN', _('China')),
+    ('CO', _('Colombia')),
+    ('CR', _('Costa Rica')),
+    ('CU', _('Cuba')),
+    ('CV', _('Cape Verde')),
+    ('CX', _('Christmas Island')),
+    ('CY', _('Cyprus')),
+    ('CZ', _('Czech Republic')),
+    ('DE', _('Germany')),
+    ('DJ', _('Djibouti')),
+    ('DK', _('Denmark')),
+    ('DM', _('Dominica')),
+    ('DO', _('Dominican Republic')),
+    ('DZ', _('Algeria')),
+    ('EC', _('Ecuador')),
+    ('EE', _('Estonia')),
+    ('EG', _('Egypt')),
+    ('EH', _('Western Sahara')),
+    ('ER', _('Eritrea')),
+    ('ES', _('Spain')),
+    ('ET', _('Ethiopia')),
+    ('FI', _('Finland')),
+    ('FJ', _('Fiji')),
+    ('FK', _('Falkland Islands (Malvinas)')),
+    ('FM', _('Micronesia')),
+    ('FO', _('Faroe Islands')),
+    ('FR', _('France')),
+    ('FX', _('France, Metropolitan')),
+    ('GA', _('Gabon')),
+    ('GB', _('United Kingdom (Great Britain)')),
+    ('GD', _('Grenada')),
+    ('GE', _('Georgia')),
+    ('GF', _('French Guiana')),
+    ('GH', _('Ghana')),
+    ('GI', _('Gibraltar')),
+    ('GL', _('Greenland')),
+    ('GM', _('Gambia')),
+    ('GN', _('Guinea')),
+    ('GP', _('Guadeloupe')),
+    ('GQ', _('Equatorial Guinea')),
+    ('GR', _('Greece')),
+    ('GS', _('South Georgia and the South Sandwich Islands')),
+    ('GT', _('Guatemala')),
+    ('GU', _('Guam')),
+    ('GW', _('Guinea-Bissau')),
+    ('GY', _('Guyana')),
+    ('HK', _('Hong Kong')),
+    ('HM', _('Heard & McDonald Islands')),
+    ('HN', _('Honduras')),
+    ('HR', _('Croatia')),
+    ('HT', _('Haiti')),
+    ('HU', _('Hungary')),
+    ('ID', _('Indonesia')),
+    ('IE', _('Ireland')),
+    ('IL', _('Israel')),
+    ('IN', _('India')),
+    ('IO', _('British Indian Ocean Territory')),
+    ('IQ', _('Iraq')),
+    ('IR', _('Islamic Republic of Iran')),
+    ('IS', _('Iceland')),
+    ('IT', _('Italy')),
+    ('JM', _('Jamaica')),
+    ('JO', _('Jordan')),
+    ('JP', _('Japan')),
+    ('KE', _('Kenya')),
+    ('KG', _('Kyrgyzstan')),
+    ('KH', _('Cambodia')),
+    ('KI', _('Kiribati')),
+    ('KM', _('Comoros')),
+    ('KN', _('St. Kitts and Nevis')),
+    ('KP', _('Korea, Democratic People\'s Republic of')),
+    ('KR', _('Korea, Republic of')),
+    ('KW', _('Kuwait')),
+    ('KY', _('Cayman Islands')),
+    ('KZ', _('Kazakhstan')),
+    ('LA', _('Lao People\'s Democratic Republic')),
+    ('LB', _('Lebanon')),
+    ('LC', _('Saint Lucia')),
+    ('LI', _('Liechtenstein')),
+    ('LK', _('Sri Lanka')),
+    ('LR', _('Liberia')),
+    ('LS', _('Lesotho')),
+    ('LT', _('Lithuania')),
+    ('LU', _('Luxembourg')),
+    ('LV', _('Latvia')),
+    ('LY', _('Libyan Arab Jamahiriya')),
+    ('MA', _('Morocco')),
+    ('MC', _('Monaco')),
+    ('MD', _('Moldova, Republic of')),
+    ('MG', _('Madagascar')),
+    ('MH', _('Marshall Islands')),
+    ('ML', _('Mali')),
+    ('MN', _('Mongolia')),
+    ('MM', _('Myanmar')),
+    ('MO', _('Macau')),
+    ('MP', _('Northern Mariana Islands')),
+    ('MQ', _('Martinique')),
+    ('MR', _('Mauritania')),
+    ('MS', _('Monserrat')),
+    ('MT', _('Malta')),
+    ('MU', _('Mauritius')),
+    ('MV', _('Maldives')),
+    ('MW', _('Malawi')),
+    ('MX', _('Mexico')),
+    ('MY', _('Malaysia')),
+    ('MZ', _('Mozambique')),
+    ('NA', _('Namibia')),
+    ('NC', _('New Caledonia')),
+    ('NE', _('Niger')),
+    ('NF', _('Norfolk Island')),
+    ('NG', _('Nigeria')),
+    ('NI', _('Nicaragua')),
+    ('NL', _('Netherlands')),
+    ('NO', _('Norway')),
+    ('NP', _('Nepal')),
+    ('NR', _('Nauru')),
+    ('NU', _('Niue')),
+    ('NZ', _('New Zealand')),
+    ('OM', _('Oman')),
+    ('PA', _('Panama')),
+    ('PE', _('Peru')),
+    ('PF', _('French Polynesia')),
+    ('PG', _('Papua New Guinea')),
+    ('PH', _('Philippines')),
+    ('PK', _('Pakistan')),
+    ('PL', _('Poland')),
+    ('PM', _('St. Pierre & Miquelon')),
+    ('PN', _('Pitcairn')),
+    ('PR', _('Puerto Rico')),
+    ('PT', _('Portugal')),
+    ('PW', _('Palau')),
+    ('PY', _('Paraguay')),
+    ('QA', _('Qatar')),
+    ('RE', _('Reunion')),
+    ('RO', _('Romania')),
+    ('RU', _('Russian Federation')),
+    ('RW', _('Rwanda')),
+    ('SA', _('Saudi Arabia')),
+    ('SB', _('Solomon Islands')),
+    ('SC', _('Seychelles')),
+    ('SD', _('Sudan')),
+    ('SE', _('Sweden')),
+    ('SG', _('Singapore')),
+    ('SH', _('St. Helena')),
+    ('SI', _('Slovenia')),
+    ('SJ', _('Svalbard & Jan Mayen Islands')),
+    ('SK', _('Slovakia')),
+    ('SL', _('Sierra Leone')),
+    ('SM', _('San Marino')),
+    ('SN', _('Senegal')),
+    ('SO', _('Somalia')),
+    ('SR', _('Suriname')),
+    ('ST', _('Sao Tome & Principe')),
+    ('SV', _('El Salvador')),
+    ('SY', _('Syrian Arab Republic')),
+    ('SZ', _('Swaziland')),
+    ('TC', _('Turks & Caicos Islands')),
+    ('TD', _('Chad')),
+    ('TF', _('French Southern Territories')),
+    ('TG', _('Togo')),
+    ('TH', _('Thailand')),
+    ('TJ', _('Tajikistan')),
+    ('TK', _('Tokelau')),
+    ('TM', _('Turkmenistan')),
+    ('TN', _('Tunisia')),
+    ('TO', _('Tonga')),
+    ('TP', _('East Timor')),
+    ('TR', _('Turkey')),
+    ('TT', _('Trinidad & Tobago')),
+    ('TV', _('Tuvalu')),
+    ('TW', _('Taiwan, Province of China')),
+    ('TZ', _('Tanzania, United Republic of')),
+    ('UA', _('Ukraine')),
+    ('UG', _('Uganda')),
+    ('UM', _('United States Minor Outlying Islands')),
+    ('UY', _('Uruguay')),
+    ('UZ', _('Uzbekistan')),
+    ('VA', _('Vatican City State (Holy See)')),
+    ('VC', _('St. Vincent & the Grenadines')),
+    ('VE', _('Venezuela')),
+    ('VG', _('British Virgin Islands')),
+    ('VI', _('United States Virgin Islands')),
+    ('VN', _('Viet Nam')),
+    ('VU', _('Vanuatu')),
+    ('WF', _('Wallis & Futuna Islands')),
+    ('WS', _('Samoa')),
+    ('YE', _('Yemen')),
+    ('YT', _('Mayotte')),
+    ('YU', _('Yugoslavia')),
+    ('ZA', _('South Africa')),
+    ('ZM', _('Zambia')),
+    ('ZR', _('Zaire')),
+    ('ZW', _('Zimbabwe')),
+    ('ZZ', _('Unknown or unspecified country')),
+)
+
+class CountryField(forms.ChoiceField):
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault('choices', COUNTRIES)
+        super(CountryField, self).__init__(*args, **kwargs)

paypal/pro/forms.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from django import forms
+
+from paypal.pro.fields import CreditCardField, CreditCardExpiryField, CreditCardCVV2Field, CountryField
+
+
+class PaymentForm(forms.Form):
+    """Form used to process direct payments."""
+    firstname = forms.CharField(255, label="First Name")
+    lastname = forms.CharField(255, label="Last Name")
+    street = forms.CharField(255, label="Street Address")
+    city = forms.CharField(255, label="City")
+    state = forms.CharField(255, label="State")
+    countrycode = CountryField(label="Country", initial="US")
+    zip = forms.CharField(32, label="Postal / Zip Code")
+    acct = CreditCardField(label="Credit Card Number")
+    expdate = CreditCardExpiryField(label="Expiration Date")
+    cvv2 = CreditCardCVV2Field(label="Card Security Code")
+
+    def process(self, request, item):
+        """Process a PayPal direct payment."""
+        from paypal.pro.helpers import PayPalWPP
+        wpp = PayPalWPP(request) 
+        params = self.cleaned_data
+        params['creditcardtype'] = self.fields['acct'].card_type
+        params['expdate'] = self.cleaned_data['expdate'].strftime("%m%Y")
+        params['ipaddress'] = request.META.get("REMOTE_ADDR", "")
+        params.update(item)
+ 
+        # Create single payment:
+        if 'billingperiod' not in params:
+            response = wpp.doDirectPayment(params)
+
+        # Create recurring payment:
+        else:
+            response = wpp.createRecurringPaymentsProfile(params, direct=True)
+ 
+        return response
+
+
+class ConfirmForm(forms.Form):
+    """Hidden form used by ExpressPay flow to keep track of payer information."""
+    token = forms.CharField(max_length=255, widget=forms.HiddenInput())
+    PayerID = forms.CharField(max_length=255, widget=forms.HiddenInput())

paypal/pro/helpers.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import datetime
+import pprint
+import time
+import urllib
+import urllib2
+
+from django.conf import settings
+from django.forms.models import fields_for_model
+from django.utils.datastructures import MergeDict
+from django.utils.http import urlencode
+
+from paypal.pro.models import PayPalNVP, L
+
+
+TEST = settings.PAYPAL_TEST
+USER = settings.PAYPAL_WPP_USER 
+PASSWORD = settings.PAYPAL_WPP_PASSWORD
+SIGNATURE = settings.PAYPAL_WPP_SIGNATURE
+VERSION = 54.0
+BASE_PARAMS = dict(USER=USER , PWD=PASSWORD, SIGNATURE=SIGNATURE, VERSION=VERSION)
+ENDPOINT = "https://api-3t.paypal.com/nvp"
+SANDBOX_ENDPOINT = "https://api-3t.sandbox.paypal.com/nvp"
+NVP_FIELDS = fields_for_model(PayPalNVP).keys()
+
+
+def paypal_time(time_obj=None):
+    """Returns a time suitable for PayPal time fields."""
+    if time_obj is None:
+        time_obj = time.gmtime()
+    return time.strftime(PayPalNVP.TIMESTAMP_FORMAT, time_obj)
+    
+def paypaltime2datetime(s):
+    """Convert a PayPal time string to a DateTime."""
+    return datetime.datetime(*(time.strptime(s, PayPalNVP.TIMESTAMP_FORMAT)[:6]))
+
+
+class PayPalError(TypeError):
+    """Error thrown when something be wrong."""
+    
+
+class PayPalWPP(object):
+    """
+    Wrapper class for the PayPal Website Payments Pro.
+    
+    Website Payments Pro Integration Guide:
+    https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_WPP_IntegrationGuide.pdf
+
+    Name-Value Pair API Developer Guide and Reference:
+    https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_NVPAPI_DeveloperGuide.pdf
+    """
+    def __init__(self, request, params=BASE_PARAMS):
+        """Required - USER / PWD / SIGNATURE / VERSION"""
+        self.request = request
+        if TEST:
+            self.endpoint = SANDBOX_ENDPOINT
+        else:
+            self.endpoint = ENDPOINT
+        self.signature_values = params
+        self.signature = urlencode(self.signature_values) + "&"
+
+    def doDirectPayment(self, params):
+        """Call PayPal DoDirectPayment method."""
+        defaults = {"method": "DoDirectPayment", "paymentaction": "Sale"}
+        required = L("creditcardtype acct expdate cvv2 ipaddress firstname lastname street city state countrycode zip amt")
+        nvp_obj = self._fetch(params, required, defaults)
+        # @@@ Could check cvv2match / avscode are both 'X' or '0'
+        # qd = django.http.QueryDict(nvp_obj.response)
+        # if qd.get('cvv2match') not in ['X', '0']:
+        #   nvp_obj.set_flag("Invalid cvv2match: %s" % qd.get('cvv2match')
+        # if qd.get('avscode') not in ['X', '0']:
+        #   nvp_obj.set_flag("Invalid avscode: %s" % qd.get('avscode')
+        return not nvp_obj.flag
+
+    def setExpressCheckout(self, params):
+        """
+        Initiates an Express Checkout transaction.
+        Optionally, the SetExpressCheckout API operation can set up billing agreements for
+        reference transactions and recurring payments.
+        Returns a NVP instance - check for token and payerid to continue!
+        """
+        if self._is_recurring(params):
+            params = self._recurring_setExpressCheckout_adapter(params)
+
+        defaults = {"method": "SetExpressCheckout", "noshipping": 1}
+        required = L("returnurl cancelurl amt")
+        return self._fetch(params, required, defaults)
+
+    def doExpressCheckoutPayment(self, params):
+        """
+        Check the dude out:
+        """
+        defaults = {"method": "DoExpressCheckoutPayment", "paymentaction": "Sale"}
+        required =L("returnurl cancelurl amt token payerid")
+        nvp_obj = self._fetch(params, required, defaults)
+        return not nvp_obj.flag
+        
+    def createRecurringPaymentsProfile(self, params, direct=False):
+        """
+        Set direct to True to indicate that this is being called as a directPayment.
+        Returns True PayPal successfully creates the profile otherwise False.
+        """
+        defaults = {"method": "CreateRecurringPaymentsProfile"}
+        required = L("profilestartdate billingperiod billingfrequency amt")
+
+        # Direct payments require CC data
+        if direct:
+            required + L("creditcardtype acct expdate firstname lastname")
+        else:
+            required + L("token payerid")
+
+        nvp_obj = self._fetch(params, required, defaults)
+        
+        # Flag if profile_type != ActiveProfile
+        return not nvp_obj.flag
+
+    def getExpressCheckoutDetails(self, params):
+        raise NotImplementedError
+
+    def setCustomerBillingAgreement(self, params):
+        raise DeprecationWarning
+
+    def getTransactionDetails(self, params):
+        raise NotImplementedError
+
+    def massPay(self, params):
+        raise NotImplementedError
+
+    def getRecurringPaymentsProfileDetails(self, params):
+        raise NotImplementedError
+
+    def updateRecurringPaymentsProfile(self, params):
+        raise NotImplementedError
+    
+    def billOutstandingAmount(self, params):
+        raise NotImplementedError
+        
+    def manangeRecurringPaymentsProfileStatus(self, params):
+        raise NotImplementedError
+        
+    def refundTransaction(self, params):
+        raise NotImplementedError
+
+    def _is_recurring(self, params):
+        """Returns True if the item passed is a recurring transaction."""
+        return 'billingfrequency' in params
+
+    def _recurring_setExpressCheckout_adapter(self, params):
+        """
+        The recurring payment interface to SEC is different than the recurring payment
+        interface to ECP. This adapts a normal call to look like a SEC call.
+        """
+        params['l_billingtype0'] = "RecurringPayments"
+        params['l_billingagreementdescription0'] = params['desc']
+
+        REMOVE = L("billingfrequency billingperiod profilestartdate desc")
+        for k in params.keys():
+            if k in REMOVE:
+                del params[k]
+                
+        return params
+
+    def _fetch(self, params, required, defaults):
+        """Make the NVP request and store the response."""
+        defaults.update(params)
+        pp_params = self._check_and_update_params(required, defaults)        
+        pp_string = self.signature + urlencode(pp_params)
+        response = self._request(pp_string)
+        response_params = self._parse_response(response)
+        
+        if settings.DEBUG:
+            print 'PayPal Request:'
+            pprint.pprint(defaults)
+            print '\nPayPal Response:'
+            pprint.pprint(response_params)
+
+        # Gather all NVP parameters to pass to a new instance.
+        nvp_params = {}
+        for k, v in MergeDict(defaults, response_params).items():
+            if k in NVP_FIELDS:
+                nvp_params[k] = v    
+
+        # PayPal timestamp has to be formatted.
+        if 'timestamp' in nvp_params:
+            nvp_params['timestamp'] = paypaltime2datetime(nvp_params['timestamp'])
+
+        nvp_obj = PayPalNVP(**nvp_params)
+        nvp_obj.init(self.request, params, response_params)
+        nvp_obj.save()
+        return nvp_obj
+        
+    def _request(self, data):
+        """Moved out to make testing easier."""
+        return urllib2.urlopen(self.endpoint, data).read()
+
+    def _check_and_update_params(self, required, params):
+        """
+        Ensure all required parameters were passed to the API call and format
+        them correctly.
+        """
+        for r in required:
+            if r not in params:
+                raise PayPalError("Missing required param: %s" % r)    
+
+        # Upper case all the parameters for PayPal.
+        return (dict((k.upper(), v) for k, v in params.iteritems()))
+
+    def _parse_response(self, response):
+        """Turn the PayPal response into a dict"""
+        response_tokens = {}
+        for kv in response.split('&'):
+            key, value = kv.split("=")
+            response_tokens[key.lower()] = urllib.unquote(value)
+        return response_tokens

paypal/pro/models.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from string import split as L
+from django.db import models
+from django.utils.http import urlencode
+from django.forms.models import model_to_dict
+from django.contrib.auth.models import User
+
+
+class PayPalNVP(models.Model):
+    """Record of a NVP interaction with PayPal."""
+    TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # 2009-02-03T17:47:41Z
+    RESTRICTED_FIELDS = L("expdate cvv2 acct")
+    ADMIN_FIELDS = L("id user flag flag_code flag_info query response created_at updated_at ")
+    ITEM_FIELDS = L("amt custom invnum")
+    DIRECT_FIELDS = L("firstname lastname street city state countrycode zip")
+
+    # Response fields
+    method = models.CharField(max_length=64, blank=True)
+    ack = models.CharField(max_length=32, blank=True)    
+    profilestatus = models.CharField(max_length=32, blank=True)
+    timestamp = models.DateTimeField(blank=True, null=True)
+    profileid = models.CharField(max_length=32, blank=True)  # I-E596DFUSD882
+    profilereference = models.CharField(max_length=128, blank=True)  # PROFILEREFERENCE
+    correlationid = models.CharField(max_length=32, blank=True) # 25b380cda7a21
+    token = models.CharField(max_length=64, blank=True)
+    payerid = models.CharField(max_length=64, blank=True)
+    
+    # Transaction Fields
+    firstname = models.CharField("First Name", max_length=255, blank=True)
+    lastname = models.CharField("Last Name", max_length=255, blank=True)
+    street = models.CharField("Street Address", max_length=255, blank=True)
+    city = models.CharField("City", max_length=255, blank=True)
+    state = models.CharField("State", max_length=255, blank=True)
+    countrycode = models.CharField("Country", max_length=2,blank=True)
+    zip = models.CharField("Postal / Zip Code", max_length=32, blank=True)
+    
+    # Custom fields
+    invnum = models.CharField(max_length=255, blank=True)
+    custom = models.CharField(max_length=255, blank=True) 
+    
+    # Admin fields
+    user = models.ForeignKey(User, blank=True, null=True)
+    flag = models.BooleanField(default=False, blank=True)
+    flag_code = models.CharField(max_length=32, blank=True)
+    flag_info = models.TextField(blank=True)    
+    ipaddress = models.IPAddressField(blank=True)
+    query = models.TextField(blank=True)
+    response = models.TextField(blank=True)
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
+        
+    class Meta:
+        db_table = "paypal_nvp"
+        verbose_name = "PayPal NVP"
+    
+    def init(self, request, paypal_request, paypal_response):
+        """Initialize a PayPalNVP instance from a HttpRequest."""
+        self.ipaddress = request.META.get('REMOTE_ADDR', '')
+        if hasattr(request, "user") and request.user.is_authenticated():
+            self.user = request.user
+
+        # No storing credit card info.
+        query_data = dict((k,v) for k, v in paypal_request.iteritems() if k not in self.RESTRICTED_FIELDS)
+        self.query = urlencode(query_data)
+        self.response = urlencode(paypal_response)
+
+        # Was there a flag on the play?        
+        ack = paypal_response.get('ack', False)
+        if ack != "Success":
+            if ack == "SuccessWithWarning":
+                self.flag_info = paypal_response.get('l_longmessage0', '')
+            else:
+                self.set_flag(paypal_response.get('l_longmessage0', ''), paypal_response.get('l_errorcode', ''))
+
+    def set_flag(self, info, code=None):
+        """Flag this instance for investigation."""
+        self.flag = True
+        self.flag_info += info
+        if code is not None:
+            self.flag_code = code
+
+    def process(self, request, item):
+        """Do a direct payment."""
+        from paypal.pro.helpers import PayPalWPP
+        wpp = PayPalWPP(request)
+
+        # Change the model information into a dict that PayPal can understand.        
+        params = model_to_dict(self, exclude=self.ADMIN_FIELDS)
+        params['acct'] = self.acct
+        params['creditcardtype'] = self.creditcardtype
+        params['expdate'] = self.expdate
+        params['cvv2'] = self.cvv2
+        params.update(item)      
+
+        # Create recurring payment:
+        if 'billingperiod' in params:
+            return wpp.createRecurringPaymentsProfile(params, direct=True)
+        # Create single payment:
+        else:
+            return wpp.doDirectPayment(params)

paypal/pro/signals.py

+from django.dispatch import Signal
+
+"""
+These signals are different from IPN signals in that they are sent the second
+the payment is failed or succeeds and come with the `item` object passed to
+PayPalPro rather than an IPN object.
+
+### SENDER is the item? is that right???
+
+"""
+
+# Sent when a payment is successfully processed.
+payment_was_successful = Signal() #providing_args=["item"])
+
+# Sent when a payment is flagged.
+payment_was_flagged = Signal() #providing_args=["item"])

paypal/pro/templates/pro/confirm.html

+<html>
+
+<head>
+<title></title>
+</head>
+<body>
+
+<form action="" method="post">
+    <table>
+        <tr>
+            {{ form.as_table }}
+            <td colspan="2" align="right"><input type="submit" value="confirm" /></td>
+        </tr>
+    </table>
+</form>
+
+</body>
+
+
+</html>
+
+

paypal/pro/templates/pro/payment.html

+<html>
+
+<head>
+<title></title>
+</head>
+<body>
+
+<form action="" method="post">
+    <table>
+        <tbody>
+            {% if errors %}<tr><td colspan="2" align="center">{{ errors }}</td></tr>{% endif %}
+            <tr><td colspan="2" align="center"><a href="?express">Pay by PayPal</a></td></tr>
+            {{ form.as_table }}
+            <tr><td colspan="2" align="right"><input type="submit" /></td></tr>
+        </tbody>
+    </table>
+</form>
+
+</body>
+
+
+</html>
+
+

paypal/pro/tests.py

+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+from django.conf import settings
+from django.core.handlers.wsgi import WSGIRequest
+from django.forms import ValidationError
+from django.http import QueryDict
+from django.test import TestCase
+from django.test.client import Client
+
+from paypal.pro.fields import CreditCardField
+from paypal.pro.helpers import PayPalWPP, PayPalError
+
+
+class RequestFactory(Client):
+    # Used to generate request objects.
+    def request(self, **request):
+        environ = {
+            'HTTP_COOKIE': self.cookies,
+            'PATH_INFO': '/',
+            'QUERY_STRING': '',
+            'REQUEST_METHOD': 'GET',
+            'SCRIPT_NAME': '',
+            'SERVER_NAME': 'testserver',
+            'SERVER_PORT': 80,
+            'SERVER_PROTOCOL': 'HTTP/1.1',
+        }
+        environ.update(self.defaults)
+        environ.update(request)
+        return WSGIRequest(environ)
+
+RF = RequestFactory()
+REQUEST = RF.get("/pay/", REMOTE_ADDR="127.0.0.1:8000")
+
+
+class DummyPayPalWPP(PayPalWPP):
+    pass
+#     """Dummy class for testing PayPalWPP."""
+#     responses = {
+#         # @@@ Need some reals data here.
+#         "DoDirectPayment": """ack=Success&timestamp=2009-03-12T23%3A52%3A33Z&l_severitycode0=Error&l_shortmessage0=Security+error&l_longmessage0=Security+header+is+not+valid&version=54.0&build=854529&l_errorcode0=&correlationid=""",
+#     }
+# 
+#     def _request(self, data):
+#         return self.responses["DoDirectPayment"]
+
+
+class CreditCardFieldTest(TestCase):
+    def testCreditCardField(self):
+        field = CreditCardField()
+        field.clean('4797503429879309')
+        self.assertEquals(field.card_type, "Visa")
+        self.assertRaises(ValidationError, CreditCardField().clean, '1234567890123455')
+
+        
+class PayPalWPPTest(TestCase):
+    def setUp(self):
+    
+        # Avoding blasting real requests at PayPal.
+        self.old_debug = settings.DEBUG
+        settings.DEBUG = True
+            
+        self.item = {
+            'amt': '9.95',
+            'inv': 'inv',
+            'custom': 'custom',
+            'next': 'http://www.example.com/next/',
+            'returnurl': 'http://www.example.com/pay/',
+            'cancelurl': 'http://www.example.com/cancel/'
+        }                    
+        self.wpp = DummyPayPalWPP(REQUEST)
+        
+    def tearDown(self):
+        settings.DEBUG = self.old_debug
+
+    def test_doDirectPayment_missing_params(self):
+        data = {'firstname': 'Chewbacca'}
+        self.assertRaises(PayPalError, self.wpp.doDirectPayment, data)
+
+    def test_doDirectPayment_valid(self):
+        data = {
+            'firstname': 'Brave',
+            'lastname': 'Star',
+            'street': '1 Main St',
+            'city': u'San Jos\xe9',
+            'state': 'CA',
+            'countrycode': 'US',
+            'zip': '95131',
+            'expdate': '012019',
+            'cvv2': '037',
+            'acct': '4797503429879309',
+            'creditcardtype': 'visa',
+            'ipaddress': '10.0.1.199',}
+        data.update(self.item)
+        self.assertTrue(self.wpp.doDirectPayment(data))
+        
+    def test_doDirectPayment_invalid(self):
+        data = {
+            'firstname': 'Epic',
+            'lastname': 'Fail',
+            'street': '100 Georgia St',
+            'city': 'Vancouver',
+            'state': 'BC',
+            'countrycode': 'CA',
+            'zip': 'V6V 1V1',
+            'expdate': '012019',
+            'cvv2': '999',
+            'acct': '1234567890',
+            'creditcardtype': 'visa',
+            'ipaddress': '10.0.1.199',}
+        data.update(self.item)
+        self.assertFalse(self.wpp.doDirectPayment(data))
+
+    def test_setExpressCheckout(self):
+        # We'll have to stub out tests for doExpressCheckoutPayment and friends
+        # because they're behind paypal's doors.
+        nvp_obj = self.wpp.setExpressCheckout(self.item)
+        self.assertTrue(nvp_obj.ack == "Success")
+
+
+### DoExpressCheckoutPayment
+# PayPal Request:
+# {'amt': '10.00',
+#  'cancelurl': u'http://xxx.xxx.xxx.xxx/deploy/480/upgrade/?upgrade=cname',
+#  'custom': u'website_id=480&cname=1',
+#  'inv': u'website-480-cname',
+#  'method': 'DoExpressCheckoutPayment',
+#  'next': u'http://xxx.xxx.xxx.xxx/deploy/480/upgrade/?upgrade=cname',
+#  'payerid': u'BN5JZ2V7MLEV4',
+#  'paymentaction': 'Sale',
+#  'returnurl': u'http://xxx.xxx.xxx.xxx/deploy/480/upgrade/?upgrade=cname',
+#  'token': u'EC-6HW17184NE0084127'}
+# 
+# PayPal Response:
+# {'ack': 'Success',
+#  'amt': '10.00',
+#  'build': '848077',
+#  'correlationid': '375f4773c3d34',
+#  'currencycode': 'USD',
+#  'feeamt': '0.59',
+#  'ordertime': '2009-03-04T20:56:08Z',
+#  'paymentstatus': 'Completed',
+#  'paymenttype': 'instant',
+#  'pendingreason': 'None',
+#  'reasoncode': 'None',
+#  'taxamt': '0.00',
+#  'timestamp': '2009-03-04T20:56:09Z',
+#  'token': 'EC-6HW17184NE0084127',
+#  'transactionid': '3TG42202A7335864V',
+#  'transactiontype': 'expresscheckout',
+#  'version': '54.0'}

paypal/pro/views.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from django.template import RequestContext
+from django.shortcuts import render_to_response
+from django.http import HttpResponseRedirect
+from django.utils.http import urlencode
+
+from paypal.pro.forms import PaymentForm, ConfirmForm
+from paypal.pro.models import PayPalNVP
+from paypal.pro.helpers import PayPalWPP, TEST
+from paypal.pro.signals import payment_was_successful, payment_was_flagged
+
+
+# PayPal Edit IPN URL:
+# https://www.sandbox.paypal.com/us/cgi-bin/webscr?cmd=_profile-ipn-notify
+EXPRESS_ENDPOINT = "https://www.paypal.com/webscr?cmd=_express-checkout&%s"
+SANDBOX_EXPRESS_ENDPOINT = "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&%s"
+
+
+class PayPalPro(object):
+    """
+    This class-based view takes care of PayPal WebsitePaymentsPro (WPP).
+    PayPalPro has two separate flows - DirectPayment and ExpressPayFlow. In 
+    DirectPayment the user buys on your site. In ExpressPayFlow the user is
+    direct to PayPal to confirm their purchase. PayPalPro implements both 
+    flows. To it create an instance using the these parameters:
+
+    item: a dictionary that holds information about the item being purchased.
+    
+    For single item purchase (pay once):
+    
+        Required Keys:
+            * amt: Float amount of the item.
+        
+        Optional Keys:
+            * custom: You can set this to help you identify a transaction.
+            * invnum: Unique ID that identifies this transaction.
+    
+    For recurring billing:
+    
+        Required Keys:
+          * amt: Float amount for each billing cycle.
+          * billingperiod: String unit of measure for the billing cycle (Day|Week|SemiMonth|Month|Year)
+          * billingfrequency: Integer number of periods that make up a cycle.
+          * profilestartdate: The date to begin billing. "2008-08-05T17:00:00Z" UTC/GMT
+          * desc: Description of what you're billing for.
+          
+        Optional Keys:
+          * trialbillingperiod: String unit of measure for trial cycle (Day|Week|SemiMonth|Month|Year)
+          * trialbillingfrequency: Integer # of periods in a cycle.
+          * trialamt: Float amount to bill for the trial period.
+          * trialtotalbillingcycles: Integer # of cycles for the trial payment period.
+          * failedinitamtaction: set to continue on failure (ContinueOnFailure / CancelOnFailure)
+          * maxfailedpayments: number of payments before profile is suspended.
+          * autobilloutamt: automatically bill outstanding amount.
+          * subscribername: Full name of the person who paid.
+          * profilereference: Unique reference or invoice number.
+          * taxamt: How much tax.
+          * initamt: Initial non-recurring payment due upon creation.
+          * currencycode: defaults to USD
+          * + a bunch of shipping fields
+        
+    payment_form_cls: form class that will be used to display the payment form.
+    It should inherit from `paypal.pro.forms.PaymentForm` if you're adding more.
+    
+    payment_template: template used to ask the dude for monies. To comply with
+    PayPal standards it must include a link to PayPal Express Checkout.
+    
+    confirm_form_cls: form class that will be used to display the confirmation form.
+    It should inherit from `paypal.pro.forms.ConfirmForm`. It is only used in the Express flow.
+    
+    success_url / fail_url: URLs to be redirected to when the payment successful or fails.
+    """
+    errors = {
+        "processing": "There was an error processing your payment. Check your information and try again.",
+        "form": "Please correct the errors below and try again.",
+        "paypal": "There was a problem contacting PayPal. Please try again later."
+    }
+    
+    def __init__(self, item=None, payment_form_cls=PaymentForm,
+                 payment_template="pro/payment.html", confirm_form_cls=ConfirmForm, 
+                 confirm_template="pro/confirm.html", success_url="?success", 
+                 fail_url=None, context=None, form_context_name="form"):
+        self.item = item
+        self.payment_form_cls = payment_form_cls
+        self.payment_template = payment_template
+        self.confirm_form_cls = confirm_form_cls
+        self.confirm_template = confirm_template
+        self.success_url = success_url
+        self.fail_url = fail_url
+        self.context = context or {}
+        self.form_context_name = form_context_name
+
+    def __call__(self, request):
+        """Return the appropriate response for the state of the transaction."""
+        self.request = request
+        if request.method == "GET":
+            if self.should_redirect_to_express():
+                return self.redirect_to_express()
+            elif self.should_render_confirm_form():
+                return self.render_confirm_form()
+            elif self.should_render_payment_form():
+                return self.render_payment_form() 
+        else:
+            if self.should_validate_confirm_form():
+                return self.validate_confirm_form()
+            elif self.should_validate_payment_form():
+                return self.validate_payment_form()
+        
+        # Default to the rendering the payment form.
+        return self.render_payment_form()
+
+    def is_recurring(self):
+        return self.item is not None and 'billingperiod' in self.item
+
+    def should_redirect_to_express(self):
+        return 'express' in self.request.GET
+        
+    def should_render_confirm_form(self):
+        return 'token' in self.request.GET and 'PayerID' in self.request.GET
+        
+    def should_render_payment_form(self):
+        return True
+
+    def should_validate_confirm_form(self):
+        return 'token' in self.request.POST and 'PayerID' in self.request.POST  
+        
+    def should_validate_payment_form(self):
+        return True
+
+    def render_payment_form(self):
+        """Display the DirectPayment for entering payment information."""
+        self.context[self.form_context_name] = self.payment_form_cls()
+        return render_to_response(self.payment_template, self.context, RequestContext(self.request))
+
+    def validate_payment_form(self):
+        """Try to validate and then process the DirectPayment form."""
+        form = self.payment_form_cls(self.request.POST)        
+        if form.is_valid():
+            success = form.process(self.request, self.item)
+            if success:
+                payment_was_successful.send(sender=self.item)
+                return HttpResponseRedirect(self.success_url)
+            else:
+                self.context['errors'] = self.errors['processing']
+
+        self.context[self.form_context_name] = form
+        self.context.setdefault("errors", self.errors['form'])
+        return render_to_response(self.payment_template, self.context, RequestContext(self.request))
+
+    def get_endpoint(self):
+        if TEST:
+            return SANDBOX_EXPRESS_ENDPOINT
+        else:
+            return EXPRESS_ENDPOINT
+
+    def redirect_to_express(self):
+        """
+        First step of ExpressCheckout. Redirect the request to PayPal using the 
+        data returned from setExpressCheckout.
+        """
+        wpp = PayPalWPP(self.request)
+        nvp_obj = wpp.setExpressCheckout(self.item)
+        if not nvp_obj.flag:
+            pp_params = dict(token=nvp_obj.token, AMT=self.item['amt'], 
+                             RETURNURL=self.item['returnurl'], 
+                             CANCELURL=self.item['cancelurl'])
+            pp_url = self.get_endpoint() % urlencode(pp_params)
+            return HttpResponseRedirect(pp_url)
+        else:
+            self.context['errors'] = self.errors['paypal']
+            return self.render_payment_form()
+
+    def render_confirm_form(self):
+        """
+        Second step of ExpressCheckout. Display an order confirmation form which
+        contains hidden fields with the token / PayerID from PayPal.
+        """
+        initial = dict(token=self.request.GET['token'], PayerID=self.request.GET['PayerID'])
+        self.context[self.form_context_name] = self.confirm_form_cls(initial=initial)
+        return render_to_response(self.confirm_template, self.context, RequestContext(self.request))
+
+    def validate_confirm_form(self):
+        """
+        Third and final step of ExpressCheckout. Request has pressed the confirmation but
+        and we can send the final confirmation to PayPal using the data from the POST'ed form.
+        """
+        wpp = PayPalWPP(self.request)
+        pp_data = dict(token=self.request.POST['token'], payerid=self.request.POST['PayerID'])
+        self.item.update(pp_data)
+        
+        # @@@ This check and call could be moved into PayPalWPP.
+        if self.is_recurring():
+            success = wpp.createRecurringPaymentsProfile(self.item)
+        else:
+            success = wpp.doExpressCheckoutPayment(self.item)
+
+        if success:
+            payment_was_successful.send(sender=self.item)
+            return HttpResponseRedirect(self.success_url)
+        else:
+            self.context['errors'] = self.errors['processing']
+            return self.render_payment_form()

paypal/standard/__init__.py

Empty file added.

paypal/standard/conf.py

+from django.conf import settings
+
+class PayPalSettingsError(Exception):
+    """Raised when settings be bad."""
+    
+
+TEST = getattr(settings, "PAYPAL_TEST", True)
+
+
+RECEIVER_EMAIL = settings.PAYPAL_RECEIVER_EMAIL
+
+
+# API Endpoints.
+POSTBACK_ENDPOINT = "https://www.paypal.com/cgi-bin/webscr"
+SANDBOX_POSTBACK_ENDPOINT = "https://www.sandbox.paypal.com/cgi-bin/webscr"
+
+# Images
+IMAGE = getattr(settings, "PAYPAL_IMAGE", "http://images.paypal.com/images/x-click-but01.gif")
+SUBSCRIPTION_IMAGE = "https://www.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif"
+SANDBOX_IMAGE = getattr(settings, "PAYPAL_SANDBOX_IMAGE", "https://www.sandbox.paypal.com/en_US/i/btn/btn_buynowCC_LG.gif")
+SUBSCRIPTION_SANDBOX_IMAGE = "https://www.sandbox.paypal.com/en_US/i/btn/btn_subscribeCC_LG.gif"

paypal/standard/forms.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from django import forms
+from django.conf import settings
+from django.utils.safestring import mark_safe
+from paypal.standard.conf import *
+from paypal.standard.widgets import ValueHiddenInput, ReservedValueHiddenInput
+from paypal.standard.conf import (POSTBACK_ENDPOINT, SANDBOX_POSTBACK_ENDPOINT, 
+    RECEIVER_EMAIL)
+
+
+# 20:18:05 Jan 30, 2009 PST - PST timezone support is not included out of the box.
+# PAYPAL_DATE_FORMAT = ("%H:%M:%S %b. %d, %Y PST", "%H:%M:%S %b %d, %Y PST",)
+# PayPal dates have been spotted in the wild with these formats, beware!
+PAYPAL_DATE_FORMAT = ("%H:%M:%S %b. %d, %Y PST",
+                      "%H:%M:%S %b. %d, %Y PDT",
+                      "%H:%M:%S %b %d, %Y PST",
+                      "%H:%M:%S %b %d, %Y PDT",)
+
+class PayPalPaymentsForm(forms.Form):
+    """
+    Creates a PayPal Payments Standard "Buy It Now" button, configured for a
+    selling a single item with no shipping.
+    
+    For a full overview of all the fields you can set (there is a lot!) see:
+    http://tinyurl.com/pps-integration
+    
+    Usage:
+    >>> f = PayPalPaymentsForm(initial={'item_name':'Widget 001', ...})
+    >>> f.render()
+    u'<form action="https://www.paypal.com/cgi-bin/webscr" method="post"> ...'
+    
+    """    
+    CMD_CHOICES = (
+        ("_xclick", "Buy now or Donations"), 
+        ("_cart", "Shopping cart"), 
+        ("_xclick-subscriptions", "Subscribe")
+    )
+    SHIPPING_CHOICES = ((1, "No shipping"), (0, "Shipping"))
+    NO_NOTE_CHOICES = ((1, "No Note"), (0, "Include Note"))
+    RECURRING_PAYMENT_CHOICES = (
+        (1, "Subscription Payments Recur"), 
+        (0, "Subscription payments do not recur")
+    )
+    REATTEMPT_ON_FAIL_CHOICES = (
+        (1, "reattempt billing on Failure"), 
+        (0, "Do Not reattempt on failure")
+    )
+        
+    # Where the money goes.
+    business = forms.CharField(widget=ValueHiddenInput(), initial=RECEIVER_EMAIL)
+    
+    # Item information.
+    amount = forms.IntegerField(widget=ValueHiddenInput())
+    item_name = forms.CharField(widget=ValueHiddenInput())
+    item_number = forms.CharField(widget=ValueHiddenInput())
+    quantity = forms.CharField(widget=ValueHiddenInput())
+    
+    # Subscription Related.
+    a1 = forms.CharField(widget=ValueHiddenInput())  # Trial 1 Price
+    p1 = forms.CharField(widget=ValueHiddenInput())  # Trial 1 Duration
+    t1 = forms.CharField(widget=ValueHiddenInput())  # Trial 1 unit of Duration, default to Month
+    a2 = forms.CharField(widget=ValueHiddenInput())  # Trial 2 Price
+    p2 = forms.CharField(widget=ValueHiddenInput())  # Trial 2 Duration
+    t2 = forms.CharField(widget=ValueHiddenInput())  # Trial 2 unit of Duration, default to Month    
+    a3 = forms.CharField(widget=ValueHiddenInput())  # Subscription Price
+    p3 = forms.CharField(widget=ValueHiddenInput())  # Subscription Duration
+    t3 = forms.CharField(widget=ValueHiddenInput())  # Subscription unit of Duration, default to Month
+    src = forms.CharField(widget=ValueHiddenInput()) # Is billing recurring? default to yes
+    sra = forms.CharField(widget=ValueHiddenInput()) # Reattempt billing on failed cc transaction
+    no_note = forms.CharField(widget=ValueHiddenInput())    
+    # Can be either 1 or 2. 1 = modify or allow new subscription creation, 2 = modify only
+    modify = forms.IntegerField(widget=ValueHiddenInput()) # Are we modifying an existing subscription?
+    
+    # Localization / PayPal Setup
+    lc = forms.CharField(widget=ValueHiddenInput())
+    page_style = forms.CharField(widget=ValueHiddenInput())
+    cbt = forms.CharField(widget=ValueHiddenInput())
+    
+    # IPN control.
+    notify_url = forms.CharField(widget=ValueHiddenInput())
+    cancel_return = forms.CharField(widget=ValueHiddenInput())
+    return_url = forms.CharField(widget=ReservedValueHiddenInput(attrs={"name":"return"}))
+    custom = forms.CharField(widget=ValueHiddenInput())
+    invoice = forms.CharField(widget=ValueHiddenInput())
+    
+    # Default fields.
+    cmd = forms.ChoiceField(widget=forms.HiddenInput(), initial=CMD_CHOICES[0][0])
+    charset = forms.CharField(widget=forms.HiddenInput(), initial="utf-8")
+    currency_code = forms.CharField(widget=forms.HiddenInput(), initial="USD")
+    no_shipping = forms.ChoiceField(widget=forms.HiddenInput(), choices=SHIPPING_CHOICES, 
+        initial=SHIPPING_CHOICES[0][0])
+
+    def __init__(self, button_type="buy", *args, **kwargs):
+        super(PayPalPaymentsForm, self).__init__(*args, **kwargs)
+        self.button_type = button_type
+
+    def render(self):
+        return mark_safe(u"""<form action="%s" method="post">
+    %s
+    <input type="image" src="%s" border="0" name="submit" alt="Buy it Now" />
+</form>""" % (POSTBACK_ENDPOINT, self.as_p(), self.get_image()))
+        
+        
+    def sandbox(self):
+        return mark_safe(u"""<form action="%s" method="post">
+    %s
+    <input type="image" src="%s" border="0" name="submit" alt="Buy it Now" />
+</form>""" % (SANDBOX_POSTBACK_ENDPOINT, self.as_p(), self.get_image()))
+        
+    def get_image(self):
+        return {
+            (True, True): SUBSCRIPTION_SANDBOX_IMAGE,
+            (True, False): SANDBOX_IMAGE,
+            (False, True): SUBSCRIPTION_IMAGE,
+            (False, False): IMAGE
+        }[TEST, self.is_subscription()]
+
+    def is_transaction(self):
+        return self.button_type == "buy"
+
+    def is_subscription(self):
+        return self.button_type == "subscribe"
+
+
+class PayPalEncryptedPaymentsForm(PayPalPaymentsForm):
+    """
+    Creates a PayPal Encrypted Payments "Buy It Now" button.
+    Requires the M2Crypto package.
+
+    Based on example at:
+    http://blog.mauveweb.co.uk/2007/10/10/paypal-with-django/
+    
+    """
+    def _encrypt(self):
+        """Use your key thing to encrypt things."""
+        from M2Crypto import BIO, SMIME, X509
+        # @@@ Could we move this to conf.py?
+        CERT = settings.PAYPAL_PRIVATE_CERT
+        PUB_CERT = settings.PAYPAL_PUBLIC_CERT
+        PAYPAL_CERT = settings.PAYPAL_CERT
+        CERT_ID = settings.PAYPAL_CERT_ID
+
+        # Iterate through the fields and pull out the ones that have a value.
+        plaintext = 'cert_id=%s\n' % CERT_ID
+        for name, field in self.fields.iteritems():
+            value = None
+            if name in self.initial:
+                value = self.initial[name]
+            elif field.initial is not None:
+                value = field.initial
+            if value is not None:
+                # @@@ Make this less hackish and put it in the widget.
+                if name == "return_url":
+                    name = "return"
+                plaintext += u'%s=%s\n' % (name, value)
+        plaintext = plaintext.encode('utf-8')
+        
+    	# Begin crypto weirdness.
+    	s = SMIME.SMIME()	
+    	s.load_key_bio(BIO.openfile(CERT), BIO.openfile(PUB_CERT))
+    	p7 = s.sign(BIO.MemoryBuffer(plaintext), flags=SMIME.PKCS7_BINARY)
+    	x509 = X509.load_cert_bio(BIO.openfile(settings.PAYPAL_CERT))
+    	sk = X509.X509_Stack()
+    	sk.push(x509)
+    	s.set_x509_stack(sk)
+    	s.set_cipher(SMIME.Cipher('des_ede3_cbc'))
+    	tmp = BIO.MemoryBuffer()
+    	p7.write_der(tmp)
+    	p7 = s.encrypt(tmp, flags=SMIME.PKCS7_BINARY)
+    	out = BIO.MemoryBuffer()
+    	p7.write(out)	
+    	return out.read()
+    	
+    def as_p(self):
+        return mark_safe(u"""
+<input type="hidden" name="cmd" value="_s-xclick" />
+<input type="hidden" name="encrypted" value="%s" />            
+        """ % self._encrypt())
+
+
+class PayPalSharedSecretEncryptedPaymentsForm(PayPalEncryptedPaymentsForm):
+    """
+    Creates a PayPal Encrypted Payments "Buy It Now" button with a Shared Secret.
+    Shared secrets should only be used when your IPN endpoint is on HTTPS.
+    
+    Adds a secret to the notify_url based on the contents of the form.
+
+    """
+    def __init__(self, *args, **kwargs):
+        "Make the secret from the form initial data and slip it into the form."
+        from paypal.standard.helpers import make_secret
+        super(PayPalSharedSecretEncryptedPaymentsForm, self).__init__(self, *args, **kwargs)
+        # @@@ Attach the secret parameter in a way that is safe for other query params.
+        secret_param = "?secret=%s" % make_secret(self)
+        # Initial data used in form construction overrides defaults
+        if 'notify_url' in self.initial:
+            self.initial['notify_url'] += secret_param
+        else:
+            self.fields['notify_url'].initial += secret_param
+
+
+class PayPalStandardBaseForm(forms.ModelForm):
+    """Form used to receive and record PayPal IPN/PDT."""
+    # PayPal dates have non-standard formats.
+    time_created = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT)
+    payment_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT)
+    next_payment_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT)
+    subscr_date = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT)
+    subscr_effective = forms.DateTimeField(required=False, input_formats=PAYPAL_DATE_FORMAT)

paypal/standard/helpers.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from django.conf import settings
+
+
+def duplicate_txn_id(ipn_obj):
+    """Returns True if a record with this transaction id exists."""
+    return ipn_obj._default_manager.filter(txn_id=ipn_obj.txn_id).count() > 0
+    
+def make_secret(form_instance, secret_fields=None):
+    """
+    Returns a secret for use in a EWP form or an IPN verification based on a
+    selection of variables in params. Should only be used with SSL.
+    
+    """
+    # @@@ Moved here as temporary fix to avoid dependancy on auth.models.
+    from django.contrib.auth.models import get_hexdigest
+    # @@@ amount is mc_gross on the IPN - where should mapping logic go?
+    # @@@ amount / mc_gross is not nessecarily returned as it was sent - how to use it? 10.00 vs. 10.0
+    # @@@ the secret should be based on the invoice or custom fields as well - otherwise its always the same.
+    
+    # Build the secret with fields availible in both PaymentForm and the IPN. Order matters.
+    if secret_fields is None:
+        secret_fields = ['business', 'item_name']
+
+    data = ""
+    for name in secret_fields:
+        if hasattr(form_instance, 'cleaned_data'):
+            if name in form_instance.cleaned_data:
+                data += unicode(form_instance.cleaned_data[name])
+        else:
+            # Initial data passed into the constructor overrides defaults.
+            if name in form_instance.initial:
+                data += unicode(form_instance.initial[name])
+            elif name in form_instance.fields and form_instance.fields[name].initial is not None:
+                data += unicode(form_instance.fields[name].initial)
+
+    secret = get_hexdigest('sha1', settings.SECRET_KEY, data)
+    return secret
+
+def check_secret(form_instance, secret):
+    """
+    Returns true if received `secret` matches expected secret for form_instance.
+    Used to verify IPN.
+    
+    """
+    # @@@ add invoice & custom
+    # secret_fields = ['business', 'item_name']
+    return make_secret(form_instance) == secret

paypal/standard/ipn/__init__.py

Empty file added.

paypal/standard/ipn/admin.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from django.contrib import admin
+from paypal.standard.ipn.models import PayPalIPN
+
+
+class PayPalIPNAdmin(admin.ModelAdmin):
+    date_hierarchy = 'payment_date'
+    fieldsets = (
+        (None, {
+            "fields": [
+                "flag", "txn_id", "txn_type", "payment_status", "payment_date",
+                "transaction_entity", "reason_code", "pending_reason", 
+                "mc_gross", "mc_fee", "auth_status", "auth_amount", "auth_exp", 
+                "auth_id"
+            ]
+        }),
+        ("Address", {
+            "description": "The address of the Buyer.",
+            'classes': ('collapse',),
+            "fields": [
+                "address_city", "address_country", "address_country_code",
+                "address_name", "address_state", "address_status", 
+                "address_street", "address_zip"
+            ]
+        }),
+        ("Buyer", {
+            "description": "The information about the Buyer.",
+            'classes': ('collapse',),
+            "fields": [
+                "first_name", "last_name", "payer_business_name", "payer_email",
+                "payer_id", "payer_status", "contact_phone", "residence_country"
+            ]
+        }),
+        ("Seller", {
+            "description": "The information about the Seller.",
+            'classes': ('collapse',),
+            "fields": [
+                "business", "item_name", "item_number", "quantity", 
+                "receiver_email", "receiver_id", "custom", "invoice", "memo"
+            ]
+        }),
+        ("Recurring", {
+            "description": "Information about recurring Payments.",
+            "classes": ("collapse",),
+            "fields": [
+                "profile_status", "initial_payment_amount", "amount_per_cycle", 
+                "outstanding_balance", "period_type", "product_name", 
+                "product_type", "recurring_payment_id", "receipt_id", 
+                "next_payment_date"
+            ]
+        }),
+        ("Admin", {
+            "description": "Additional Info.",
+            "classes": ('collapse',),
+            "fields": [
+                "test_ipn", "ipaddress", "query", "response", "flag_code", 
+                "flag_info"
+            ]
+        }),
+    )
+    list_display = [
+        "__unicode__", "flag", "flag_info", "invoice", "custom", 
+        "payment_status", "created_at"
+    ]
+    search_fields = ["txn_id", "recurring_payment_id"]
+
+
+admin.site.register(PayPalIPN, PayPalIPNAdmin)

paypal/standard/ipn/forms.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from paypal.standard.forms import PayPalStandardBaseForm 
+from paypal.standard.ipn.models import PayPalIPN
+
+
+class PayPalIPNForm(PayPalStandardBaseForm):
+    """
+    Form used to receive and record PayPal IPN notifications.
+    
+    PayPal IPN test tool:
+    https://developer.paypal.com/us/cgi-bin/devscr?cmd=_tools-session
+    """
+    class Meta:
+        model = PayPalIPN
+

paypal/standard/ipn/migrations/0001_first_migration.py

+# -*- coding: utf-8 -*-
+from django.db import models
+from south.db import db
+from paypal.standard.ipn.models import *
+
+
+class Migration:    
+    def forwards(self, orm):
+        # Adding model 'PayPalIPN'
+        db.create_table('paypal_ipn', (
+            ('id', models.AutoField(primary_key=True)),
+            ('business', models.CharField(max_length=127, blank=True)),
+            ('charset', models.CharField(max_length=32, blank=True)),
+            ('custom', models.CharField(max_length=255, blank=True)),
+            ('notify_version', models.DecimalField(default=0, null=True, max_digits=64, decimal_places=2, blank=True)),
+            ('parent_txn_id', models.CharField("Parent Transaction ID", max_length=19, blank=True)),
+            ('receiver_email', models.EmailField(max_length=127, blank=True)),
+            ('receiver_id', models.CharField(max_length=127, blank=True)),
+            ('residence_country', models.CharField(max_length=2, blank=True)),
+            ('test_ipn', models.BooleanField(default=False, blank=True)),
+            ('txn_id', models.CharField("Transaction ID", max_length=19, blank=True)),
+            ('txn_type', models.CharField("Transaction Type", max_length=128, blank=True)),
+            ('verify_sign', models.CharField(max_length=255, blank=True)),
+            ('address_country', models.CharField(max_length=64, blank=True)),
+            ('address_city', models.CharField(max_length=40, blank=True)),
+            ('address_country_code', models.CharField(max_length=64, blank=True)),
+            ('address_name', models.CharField(max_length=128, blank=True)),
+            ('address_state', models.CharField(max_length=40, blank=True)),
+            ('address_status', models.CharField(max_length=11, blank=True)),
+            ('address_street', models.CharField(max_length=200, blank=True)),
+            ('address_zip', models.CharField(max_length=20, blank=True)),
+            ('contact_phone', models.CharField(max_length=20, blank=True)),
+            ('first_name', models.CharField(max_length=64, blank=True)),
+            ('last_name', models.CharField(max_length=64, blank=True)),
+            ('payer_business_name', models.CharField(max_length=127, blank=True)),
+            ('payer_email', models.CharField(max_length=127, blank=True)),
+            ('payer_id', models.CharField(max_length=13, blank=True)),
+            ('auth_amount', models.DecimalField(default=0, null=True, max_digits=64, decimal_places=2, blank=True