summaryrefslogtreecommitdiff
path: root/postgresqleu/paypal/views.py
blob: 85151b2ed11afcd25447da7228f5375997cfa851 (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
from django.http import HttpResponseForbidden, HttpResponse
from django.db import transaction
from django.shortcuts import render, get_object_or_404
from django.conf import settings
from django.utils import timezone

from decimal import Decimal
from urllib.parse import unquote_plus
import requests

from postgresqleu.invoices.util import InvoiceManager
from postgresqleu.invoices.models import InvoicePaymentMethod
from postgresqleu.accounting.util import create_accounting_entry

from .models import TransactionInfo, ErrorLog


@transaction.atomic
def paypal_return_handler(request, methodid):
    tx = 'UNKNOWN'

    method = get_object_or_404(InvoicePaymentMethod, pk=int(methodid), active=True)
    pm = method.get_implementation()

    # Custom error return that can get to the request context
    def paypal_error(reason):
        return render(request, 'paypal/error.html', {
            'reason': reason,
        })

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

    # Now for the main handler

    # Handle a paypal PDT return
    if 'tx' not in request.GET:
        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': pm.config('pdt_token'),
            }
        resp = requests.post(pm.get_baseurl(), data=params)
        if resp.status_code != 200:
            raise Exception("status code {0}".format(resp.status_code))
        r = resp.text
    except Exception as 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(line).split('=') for line in lines if line != ''])

    # 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'] != pm.config('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 as k:
        return paypal_error('Mandatory field %s is missing from paypal data!' % (k, ))

    # Now let's find the state of the payment
    if 'payment_status' not in d:
        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.

        # Double-check if it is already added. We did check this furter
        # up, but it seems it can sometimes be called more than once
        # asynchronously, due to the check with paypal taking too
        # long.
        if TransactionInfo.objects.filter(paypaltransid=tx).exists():
            return HttpResponse("Transaction already processed", content_type='text/plain')

        # Paypal seems to randomly change which field actually contains
        # the transaction title.
        if d.get('transaction_subject', ''):
            transtext = d['transaction_subject']
        else:
            transtext = d['item_name']
        ti = TransactionInfo(paypaltransid=tx,
                             timestamp=timezone.now(),
                             paymentmethod=method,
                             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 = ["%s?cmd=_view-a-trans&id=%s" % (pm.get_baseurl(), ti.paypaltransid, ), ]

        # Separate out donations made through our website
        if ti.transtext == pm.config('donation_text'):
            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 = [
                (pm.config('accounting_income'), accstr, ti.amount - ti.fee, None),
                (pm.config('accounting_fee'), accstr, ti.fee, None),
                (settings.ACCOUNTING_DONATIONS_ACCOUNT, accstr, -ti.amount, None),
                ]
            create_accounting_entry(accrows, True, urls)

            return render(request, 'paypal/noinvoice.html', {
            })

        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,
                                                            pm.config('accounting_income'),
                                                            pm.config('accounting_fee'),
                                                            urls,
                                                            payment_logger,
                                                            method,
        )
        if r == invoicemanager.RESULT_OK:
            # Matched it!
            ti.matched = True
            ti.matchinfo = 'Matched standard invoice (auto)'
            ti.save()

            return render(request, 'paypal/complete.html', {
                'invoice': i,
                'url': invoicemanager.get_invoice_return_url(i),
            })
        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(request, 'paypal/noinvoice.html', {
            })

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