There's a simple action to manage PayPal IPN notifications I developed for an Italian ISP (E4A) to handle VoIP recharges.
Why IPN is useful?
IPN is a very useful PayPal feature to allow automatic payments with immediate delivery of the chosen service/product. In my case a VoIP recharge, the user choses the amount, pays via PayPal and when PayPal notifies back to our servers the succesful payment, we automatically send the invoice and apply recharge to the VoIP account.
More informations about PayPal IPN: https://www.paypal.com/ipn
How IPN works
In order to make benefit of the IPN feature, you have to request payments from the users with a form like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <table> <tr> <th>Accredito Ricarica</th> <th>Costo Ricarica</th> <th>Acquista</th> </tr> % for ricarica in h.ricariche_voip: <td>${ricarica.accredito} Euro</td> <td>${ricarica.costo} Euro</td> <td> <form action="https://${h.paypal_url}/cgi-bin/webscr" method="post"> <input type="hidden" name="cmd" value="_xclick"/> <input type="hidden" name="business" value="${h.paypal_business}"/> <input type="hidden" name="item_name" value="Ricarica VoIP E4A ${ricarica.accredito} Euro"/> <input type="hidden" name="item_number" value="${ricarica}"/> <input type="hidden" name="amount" value="${ricarica.costo}"/> <input type="hidden" name="shipping" value="0.00"/> <input type="hidden" name="no_shipping" value="1"/> <input type="hidden" name="no_note" value="1"/> <input type="hidden" name="custom" value="${c.voipacct.whssid}"/> <input type="hidden" name="currency_code" value="EUR"/> <input type="hidden" name="tax" value="0.00"/> <input type="hidden" name="lc" value="IT"/> <input type="hidden" name="bn" value="PP-BuyNowBF"/> <input type="hidden" name="charset" value="utf-8"/> <input type="hidden" name="form-charset" value="utf-8"/> <input type="hidden" name="return" value="${h.url_for(action='ricarica', qualified=True)}"/> <input type="hidden" name="cancel_return" value="${h.url_for(action='ricarica', qualified=True)}"/> <input type="hidden" name="notify_url" value="${h.url_for(action='ipn', qualified=True)}"/> <input class="image" type="image" src="/pictures/paypal.png" border="0" name="submit" alt="PayPal" title="PayPal"/> </form> </td> </tr> %endfor </table> |
Most important field in this form for IPN is "notify_url", that's where PayPal servers notify the payment after successful transfer.
When this occurs, pylons receives a POST request directed toward this url with transaction data (variables reference: https://www.paypal.com/IntegrationCenter/ic_ipn-pdt-variable-reference.html). In order to verify that this is a valid notification from PayPal, before continuing you should post data back to https://www.paypal.com/cgi-bin/webscr and if response is "VERIFIED", you can safely continue with post-payment operations.
Code
e4acpanel/controller/voip.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | from e4acpanel.lib.base import * class VoipController(BaseController): ... def ipn(self): "PayPal Instant Payment Notification" newpost = request.POST newpost['cmd']=u'_notify-validate' params = unicode_urlencode(newpost) headers = { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain', 'User-Agent': 'E4A Control Panel (http://ganimede.e4a.it)' } conn = httplib.HTTPSConnection(h.paypal_url) conn.request('POST', '/cgi-bin/webscr', params, headers) response = conn.getresponse() if not response.status == 200: raise str(response.status) + ' ' + response.reason + ' while connecting to PayPal' paymentStatus = response.read() logFile = open('payments.log', 'a') logFile.write('%s [%s] %s %s\n' % ( time.ctime(time.time()), params, paymentStatus, repr(newpost))) logFile.close() if paymentStatus != 'VERIFIED': response.close() raise 'IPN VoIP invalid' if request.POST.get('business') != h.paypal_business: response.close() raise 'IPN VoIP invalid for ' + request.POST.get('business') txn = request.POST.get('txn_id') payer = request.POST.get('payer_email') item_number = request.POST.get('item_number') ammount = Decimal(request.POST.get('mc_gross')) custom = request.POST.get('custom') # send invoice and process recharge # ... response.close() return 'thanks' |
e4acpanel/lib/utils.py
Due to unicode.urlencode not managing unicode strings, I used a custom urlencode. I stole some code from Trac and adapted to work with Pylons:
1 2 3 4 5 6 7 8 9 | from urllib import urlencode from paste.util.multidict import UnicodeMultiDict, MultiDict def unicode_urlencode(params): """A unicode aware version of urllib.urlencode""" if isinstance(params, (dict, UnicodeMultiDict, MultiDict)): params = params.items() return urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) for k, v in params]) |
License
This is part of e4acpanel, the control panel of E4A ISP. e4acpanel is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2 of the License. e4acpanel is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with e4acpanel. If not, see <http://www.gnu.org/licenses/>.