summaryrefslogtreecommitdiff
path: root/postgresqleu/paypal/util.py
blob: 097d812cf7d4105fadd59c7780b87071018a5fba (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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
from django.conf import settings
from django.utils import timezone

import requests
from requests.auth import HTTPBasicAuth
from decimal import Decimal
from datetime import datetime, timedelta


class PaypalAPI(object):
    BASE_HEADERS = {
        'Accept': 'application/json',
        'Accept-Language': 'en_US',
    }

    def __init__(self, pm):
        self.token = None
        self.pm = pm
        if pm.config('sandbox'):
            self.REST_ENDPOINT = 'https://api.sandbox.paypal.com/'
        else:
            self.REST_ENDPOINT = 'https://api.paypal.com/'

    def ensure_access_token(self):
        if not self.token:
            r = requests.post(
                '{0}v1/oauth2/token'.format(self.REST_ENDPOINT),
                headers=self.BASE_HEADERS,
                data={
                    'grant_type': 'client_credentials',
                },
                auth=HTTPBasicAuth(self.pm.config('clientid'), self.pm.config('clientsecret')),
            )
            if r.status_code != 200:
                r.raise_for_status()
            j = r.json()
            self.token = j['access_token']
            self.tokenscope = j['scope']

    def _authorized_headers(self):
        self.ensure_access_token()
        h = self.BASE_HEADERS.copy()
        h['Authorization'] = 'Bearer ' + self.token
        return h

    def _rest_api_call(self, suburl, params):
        return requests.get('{0}{1}'.format(self.REST_ENDPOINT, suburl),
                            params=params,
                            headers=self._authorized_headers(),
        )

    def _rest_api_post(self, suburl, json):
        self.ensure_access_token()
        h = self.BASE_HEADERS.copy()
        h['Authorization'] = 'Bearer ' + self.token
        return requests.post('{0}{1}'.format(self.REST_ENDPOINT, suburl),
                             json=json,
                             headers=self._authorized_headers(),
        )

    def _dateformat(self, d):
        return d.strftime("%Y-%m-%dT%H:%M:%S+0000")

    def get_transaction_list(self, startdate):
        r = self._rest_api_call('v1/reporting/transactions/', {
            'start_date': self._dateformat(startdate),
            'end_date': self._dateformat(startdate + timedelta(days=30)),
            'fields': 'transaction_info,payer_info,shipping_info,cart_info',
            'page_size': 500,
        })
        if r.status_code != 200:
            raise Exception("Failed to get transactions: %s" % r.json()['message'])

        for t in r.json()['transaction_details']:
            if t['transaction_info']['transaction_status'] != 'S':
                continue

            if t['transaction_info']['transaction_amount']['currency_code'] != settings.CURRENCY_ISO:
                raise Exception("Transaction {0} is wrong currency: {1}".format(
                    t['transaction_info']['transaction_id'],
                    t['transaction_info']['transaction_amount']['currency_code'],
                ))

            code = t['transaction_info']['transaction_event_code']
            r = {
                'TRANSACTIONID': t['transaction_info']['transaction_id'],
                'TIMESTAMP': t['transaction_info']['transaction_updated_date'],
                'AMT': t['transaction_info']['transaction_amount']['value'],
                'EMAIL': None,
                'NAME': None,
                'SUBJECT': None,
            }

            if code in ('T1105', ):
                # Some things are better left ignored
                continue

            if code in ('T2101', 'T2102'):
                # General hold and release of general hold. Ignore those.
                continue

            if code in ('T0400', 'T0403'):
                # General withdrawal, doesn't have normal details
                r['EMAIL'] = self.pm.config('email')
                r['NAME'] = self.pm.config('email')
                r['SUBJECT'] = 'Transfer from Paypal to bank'
                yield r
                continue

            if code == 'T0300':
                # General funding of paypal account, such as a direct debit
                r['EMAIL'] = self.pm.config('email')
                r['NAME'] = self.pm.config('email')
                r['SUBJECT'] = 'Funding of Paypal account'
                yield r
                continue

            if code == 'T0303':
                # Bank deposit, doesn't have normal details
                r['EMAIL'] = self.pm.config('email')
                r['NAME'] = self.pm.config('email')
                r['SUBJECT'] = 'Bank deposit to paypal'
                yield r
                continue

            if code == 'T1201':
                # Chargeback, also doesn't have normal details
                r['EMAIL'] = self.pm.config('email')
                r['NAME'] = self.pm.config('email')
                r['SUBJECT'] = 'Paypal chargeback'
                yield r
                continue

            if code in ('T1106', 'T1108', 'T0106'):
                # "Payment reversal, initiated by PayPal", *sometimes* has email and
                # sometimes not. Undocumented when they differ.
                # T0106 is chargeback fee, also from no real sender
                r['EMAIL'] = t['payer_info'].get('email_address', self.pm.config('email'))

            if not r['EMAIL']:
                # Seems with some type of transfers things can show up with non-existent
                # email addresses which shouldn't happen. We'll just have to fall back to our
                # own which we know is wrong, but we also know it exists-
                r['EMAIL'] = t['payer_info'].get('email_address', self.pm.config('email'))

            # Figure out the name, since it can be in completely different places
            # depending on the transaction (even for the same type of transactions)
            if 'name' in t['shipping_info']:
                r['NAME'] = t['shipping_info']['name']
            elif 'payer_name' in t['payer_info']:
                if 'given_name' in t['payer_info']['payer_name']:
                    r['NAME'] = "{0} {1}".format(t['payer_info']['payer_name']['given_name'], t['payer_info']['payer_name']['surname'])
                elif 'alternate_full_name' in t['payer_info']['payer_name']:
                    r['NAME'] = t['payer_info']['payer_name']['alternate_full_name']

            # If we haven't found a name on the transaction *anywhere*, set the name field to
            # the email address.
            if not r['NAME']:
                r['NAME'] = r['EMAIL']

            if 'fee_amount' in t['transaction_info']:
                r['FEEAMT'] = t['transaction_info']['fee_amount']['value']

            if code in ('T0000', 'T0001', 'T0003', 'T0006', 'T0007', 'T0011', 'T0013', 'T0700'):
                if 'item_details' in t['cart_info']:
                    r['SUBJECT'] = t['cart_info']['item_details'][0]['item_name']
                elif 'transaction_note' in t['transaction_info']:
                    r['SUBJECT'] = t['transaction_info']['transaction_note']
                else:
                    r['SUBJECT'] = 'Paypal payment with empty note'
            elif code == 'T0002':
                r['SUBJECT'] = 'Recurring paypal payment without note'
            elif code == 'T1107':
                if t['transaction_info'].get('transaction_subject', ''):
                    r['SUBJECT'] = 'Refund of Paypal payment: %s' % t['transaction_info']['transaction_subject']
                else:
                    r['SUBJECT'] = 'Refund of unknown transaction'
            elif code == 'T1106':
                # Payment reversal initiated by paypal
                r['SUBJECT'] = 'Reversal of {0}'.format(t['transaction_info']['paypal_reference_id'])
            elif code == 'T1108':
                r['SUBJECT'] = 'Reversal of fee for {0}'.format(t['transaction_info']['paypal_reference_id'])
            elif code == 'T0106':
                r['SUBJECT'] = 'Paypal chargeback fee for {0}'.format(t['transaction_info']['paypal_reference_id'])
            else:
                raise Exception("Unknown paypal transaction event code %s" % code)

            yield r

    def get_primary_balance(self):
        r = self._rest_api_call('v1/reporting/balances', {
            'currency_code': settings.CURRENCY_ISO,
        })
        if r.status_code != 200:
            raise Exception("Failed to get paypal balance: %s" % r.json()['message'])
        j = r.json()

        for b in j['balances']:
            if b['primary']:
                if b['currency'] != settings.CURRENCY_ISO:
                    raise Exception("Mismatched currency on primary account: %s" % j['balances'])
                if b['total_balance']['currency_code'] != settings.CURRENCY_ISO:
                    raise Exception("Mismatched currency on total balance: %s" % b)
                return Decimal(b['total_balance']['value'])

        raise Exception("No primary balance found in %s" % j['balances'])

    def refund_transaction(self, paypaltransid, amount, isfull, refundnote):
        r = self._rest_api_post(
            'v1/payments/sale/{0}/refund'.format(paypaltransid),
            {
                'amount': {
                    'total': '{0:.2f}'.format(amount),
                    'currency': settings.CURRENCY_ISO,
                },
                'description': refundnote,
            }
        )
        if r.status_code != 201:
            raise Exception("Failed to issue refund: %s" % r.json()['message'])
        return r.json()['id']