summaryrefslogtreecommitdiff
path: root/postgresqleu/paypal/views.py
blob: df92baf15fa0dafaaaa8ab84431757fff44563a9 (plain)
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
from django.http import HttpResponseForbidden
from django.db import transaction
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.conf import settings

from datetime import datetime, date
from decimal import Decimal
import urllib2
from urllib import urlencode, unquote_plus

from postgresqleu.invoices.util import InvoiceManager
from postgresqleu.util.decorators import ssl_required
from postgresqleu.accounting.util import create_accounting_entry

from models import TransactionInfo, ErrorLog, SourceAccount

@ssl_required
@transaction.commit_on_success
def paypal_return_handler(request):
	tx = 'UNKNOWN'

	# Custom error return that can get to the request context
	def paypal_error(reason):
		# Explicitly roll back the transaction, since our
		# error page will look like a success to django...
		transaction.rollback()
		return render_to_response('paypal/error.html', {
				'reason': reason,
				}, context_instance=RequestContext(request))

	# Logger for the invoice processing - we store it in the genereal
	# paypal logs
	def payment_logger(msg):
		ErrorLog(timestamp=datetime.now(),
				 sent=False,
				 message='Paypal automatch for %s: %s' % (tx, msg)
				 ).save()

	# Now for the main handler

	# Handle a paypal PDT return
	if not request.GET.has_key('tx'):
		return paypal_error('Transaction id not received from paypal')

	tx = request.GET['tx']
	# We have a transaction id. First we check if we already have it
	# in the database.
	# We only store transactions with status paid, so if it's in there,
	# then it's already paid, and what's happening here is a replay
	# (either by mistake or intentional). So we don't redirect the user
	# at this point, we just give an error message.
	try:
		ti = TransactionInfo.objects.get(paypaltransid=tx)
		return HttpResponseForbidden('This transaction has already been processed')
	except TransactionInfo.DoesNotExist:
		pass

	# We haven't stored the status of this transaction. It either means
	# this is the first load, or that we have only seen pending state on
	# it before. Thus, we need to post back to paypal to figure out the
	# current status.
	try:
		params = {
			'cmd': '_notify-synch',
			'tx': tx,
			'at': settings.PAYPAL_PDT_TOKEN,
			}
		u = urllib2.urlopen(settings.PAYPAL_BASEURL, urlencode(params))
		r = u.read()
		u.close()
	except Exception, ex:
		# Failed to talk to paypal somehow. It should be ok to retry.
		return paypal_error('Failed to verify status with paypal: %s' % ex)

	# First line of paypal response contains SUCCESS if we got a valid
	# response (which might *not* mean it's actually a payment!)
	lines = r.split("\n")
	if lines[0] != 'SUCCESS':
		return paypal_error('Received an error from paypal.')

	# Drop the SUCCESS line
	lines = lines[1:]

	# The rest of the response is urlencoded key/value pairs
	d = dict([unquote_plus(l).decode('latin1').split('=') for l in lines if l != ''])

	# Validate things that should never be wrong
	try:
		if d['txn_id'] != tx:
			return paypal_error('Received invalid transaction id from paypal')
		if d['txn_type'] != 'web_accept':
			return paypal_error('Received transaction type %s which is unknown by this system!' % d['txn_type'])
		if d['business'] != settings.PAYPAL_EMAIL:
			return paypal_error('Received payment for %s which is not the correct recipient!' % d['business'])
		if d['mc_currency'] != settings.CURRENCY_ABBREV:
			return paypal_error('Received payment in %s, not %s. We cannot currently process this automatically.' % (d['mc_currency'], settings.CURRENCY_ABBREV))
	except KeyError, k:
		return paypal_error('Mandatory field %s is missing from paypal data!', k)

	# Now let's find the state of the payment
	if not d.has_key('payment_status'):
		return paypal_error('Payment status not received from paypal!')

	if d['payment_status'] == 'Completed':
		# Payment is completed. Create a paypal transaction info
		# object for it, and then try to match it to an invoice.

		# Paypal seems to randomly change which field actually contains
		# the transaction title.
		if d.has_key('transaction_subject') and d['transaction_subject'] != '':
			transtext = d['transaction_subject']
		else:
			transtext = d['item_name']
		ti = TransactionInfo(paypaltransid = tx,
							 timestamp = datetime.now(),
							 sourceaccount = SourceAccount.objects.get(pk=settings.PAYPAL_DEFAULT_SOURCEACCOUNT),
							 sender = d['payer_email'],
							 sendername = d['first_name'] + ' ' + d['last_name'],
							 amount = Decimal(d['mc_gross']),
							 fee = Decimal(d['mc_fee']),
							 transtext = transtext,
							 matched = False)
		ti.save()

		# Generate URLs that link back to paypal in a way that we can use
		# from the accounting system. Note that this is an undocumented
		# URL format for paypal, so it may stop working at some point in
		# the future.
		urls = ["https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s" % ti.paypaltransid,]

		# Separate out donations made through our website
		if ti.transtext == "PostgreSQL Europe donation":
			ti.matched = True
			ti.matchinfo = 'Donation, automatically matched'
			ti.save()

			# Generate a simple accounting record, that will have to be
			# manually completed.
			accstr = "Paypal donation %s" % ti.paypaltransid
			accrows = [
				(settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT, accstr, ti.amount-ti.fee, None),
				(settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT, accstr, ti.fee, None),
				(settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -ti.amount, None),
				]
			create_accounting_entry(date.today(), accrows, True, urls)

			return render_to_response('paypal/noinvoice.html', {
					}, context_instance=RequestContext(request))

		invoicemanager = InvoiceManager()
		(r,i,p) = invoicemanager.process_incoming_payment(ti.transtext,
														  ti.amount,
														  "Paypal id %s, from %s <%s>, auto" % (ti.paypaltransid, ti.sendername, ti.sender),
														  ti.fee,
														  settings.ACCOUNTING_PAYPAL_INCOME_ACCOUNT,
														  settings.ACCOUNTING_PAYPAL_FEE_ACCOUNT,
														  urls,
														  payment_logger)
		if r == invoicemanager.RESULT_OK:
			# Matched it!
			ti.matched = True
			ti.matchinfo = 'Matched standard invoice (auto)'
			ti.save()

			# Now figure out where to return the user. This comes from the
			# invoice processor, assuming we have one
			if p:
				url = p.get_return_url(i)
			else:
				# No processor, so redirect the user back to the basic
				# invoice page.
				if i.recipient_user:
					# Registered to a specific user, so request that users
					# login on redirect
					url = "%s/invoices/%s/" % (settings.SITEBASE_SSL, i.pk)
				else:
					# No user account registered, so send back to the secret
					# url version
					url = "%s/invoices/%s/%s/" % (settings.SITEBASE_SSL, i.pk, i.recipient_secret)

			return render_to_response('paypal/complete.html', {
					'invoice': i,
					'url': url,
					}, context_instance=RequestContext(request))
		else:
			# Did not match an invoice anywhere!
			# We'll leave the transaction in the paypal transaction
			# list, where it will generate an alert in the nightly mail.
			return render_to_response('paypal/noinvoice.html', {
					}, context_instance=RequestContext(request))

	# For a pending payment, we set ourselves up with a redirect loop
	if d['payment_status'] == 'Pending':
		try:
			pending_reason = d['pending_reason']
		except:
			pending_reason = 'no reason given'
		return render_to_response('paypal/pending.html', {
				'reason': pending_reason,
				}, context_instance=RequestContext(request))
	return paypal_error('Unknown payment status %s.' % d['payment_status'])