#!/usr/bin/env python # -*- coding: utf-8 -*- from django.conf import settings 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.helpers import PayPalWPP from paypal.pro.exceptions import PayPalFailure # 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: 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 getattr(settings, 'PAYPAL_TEST', True): 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) try: nvp_obj = wpp.setExpressCheckout(self.item) except PayPalFailure: self.context['errors'] = self.errors['paypal'] return self.render_payment_form() else: pp_params = dict(token=nvp_obj.token) pp_url = self.get_endpoint() % urlencode(pp_params) return HttpResponseRedirect(pp_url) 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. try: if self.is_recurring(): wpp.createRecurringPaymentsProfile(self.item) else: wpp.doExpressCheckoutPayment(self.item) except PayPalFailure: self.context['errors'] = self.errors['processing'] return self.render_payment_form() else: return HttpResponseRedirect(self.success_url)