diff options
| author | Magnus Hagander | 2016-01-18 21:35:42 +0000 |
|---|---|---|
| committer | Magnus Hagander | 2016-01-18 21:36:15 +0000 |
| commit | 470186d25f22b64fdff26aa97686b450438a0d8a (patch) | |
| tree | 17e13d762de1af244120a70a6f51887efa66d942 /postgresqleu | |
| parent | a1cbcf89512d19d8225a23c73b564b1d9fd41afb (diff) | |
Move paypal fetching script to be an admin command
Centralizes Paypal API calls, which we will need for some other things
Diffstat (limited to 'postgresqleu')
| -rw-r--r-- | postgresqleu/paypal/management/commands/paypal_fetch.py | 155 | ||||
| -rw-r--r-- | postgresqleu/paypal/util.py | 48 | ||||
| -rw-r--r-- | postgresqleu/settings.py | 4 |
3 files changed, 207 insertions, 0 deletions
diff --git a/postgresqleu/paypal/management/commands/paypal_fetch.py b/postgresqleu/paypal/management/commands/paypal_fetch.py new file mode 100644 index 00000000..d0012cad --- /dev/null +++ b/postgresqleu/paypal/management/commands/paypal_fetch.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# +# This script downloads all paypal transaction data from one or more accounts, +# and stores them in the database for further processing. No attempt is made +# to match the payment to something elsewhere in the system - that is handled +# by separate scripts. +# +# Copyright (C) 2010-2016, PostgreSQL Europe +# + +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction, connection +from django.conf import settings + +from datetime import datetime, timedelta +from decimal import Decimal + +from postgresqleu.paypal.models import TransactionInfo +from postgresqleu.paypal.util import PaypalAPI + +class PaypalBaseTransaction(object): + def __init__(self, apistruct): + self.message = None + + self.transinfo = TransactionInfo( + paypaltransid=apistruct['TRANSACTIONID'], + sourceaccount_id=settings.PAYPAL_DEFAULT_SOURCEACCOUNT, + ) + try: + self.transinfo.timestamp = datetime.strptime(apistruct['TIMESTAMP'], '%Y-%m-%dT%H:%M:%SZ') + self.transinfo.amount = Decimal(apistruct['AMT']) + self.transinfo.fee = -Decimal(apistruct['FEEAMT']) + self.transinfo.sendername = apistruct['NAME'] + except Exception, e: + self.message = "Unable to parse: %s" % e + + def __str__(self): + if self.message: + return self.message + return str(self.transinfo) + + def already_processed(self): + return TransactionInfo.objects.filter(paypaltransid=self.transinfo.paypaltransid).exists() + + def fetch_details(self, api): + r = api.get_transaction_details(self.transinfo.paypaltransid) + if r['TRANSACTIONTYPE'][0] == 'cart': + # Always retrieve the first item in the cart + # XXX: does this always come back in the same order as sent? + # So far, all testing indicates it does + self.transinfo.transtext = r['L_NAME0'][0] + elif r['TRANSACTIONTYPE'][0] == 'sendmoney': + # This is sending of money, and not receiving. The transaction + # text (naturally) goes in a completely different field. + if r.has_key('NOTE'): + self.transinfo.transtext = 'Paypal payment: %s' % r['NOTE'][0] + else: + self.transinfo.transtext = 'Paypal payment with empty note' + else: + if r.has_key('SUBJECT'): + self.transinfo.transtext = r['SUBJECT'][0] + elif r.has_key('L_NAME0'): + self.transinfo.transtext = r['L_NAME0'][0] + else: + self.transinfo.transtext = "" + + if r['L_CURRENCYCODE0'][0] != settings.CURRENCY_ISO: + self.message = "Invalid currency %s" % r['L_CURRENCYCODE0'][0] + self.transinfo.transtext += ' (currency %s, manually adjust amount!)' % r['L_CURRENCYCODE0'][0] + self.transinfo.amount = -1 # just to be on the safe side + + def store(self): + self.transinfo.matched = False + self.transinfo.matachinfo = self.message + self.transinfo.save() + + +class PaypalTransaction(PaypalBaseTransaction): + def __init__(self, apistruct): + super(PaypalTransaction, self).__init__(apistruct) + try: + self.transinfo.sender = apistruct['EMAIL'] + except Exception, e: + self.message = "Unable to parse: %s" % e + +class PaypalRefund(PaypalTransaction): + def fetch_details(self, api): + super(PaypalRefund, self).fetch_details(api) + if self.transinfo.transtext: + self.transinfo.transtext = "Refund of %s" % self.text + else: + self.transinfo.transtext = "Refund of unknown transaction" + +class PaypalTransfer(PaypalBaseTransaction): + def __init__(self, apistruct): + super(PaypalTransfer, self).__init__(apistruct) + self.transinfo.transtext = "Transfer from Paypal to bank" + self.transinfo.fee = 0 + self.transinfo.sender = 'treasurer@postgresql.eu' + if apistruct['CURRENCYCODE'] != settings.CURRENCY_ISO: + self.message = "Invalid currency %s" % apistruct['CURRENCYCODE'] + self.transinfo.transtext += ' (currency %s, manually adjust amount!)' % r['CURRENCYCODE'] + self.transinfo.amount = -1 # To be on the safe side + + def fetch_details(self, api): + # We cannot fetch more details, but we also don't need more details.. + pass + +class Command(BaseCommand): + help = 'Fetch updated list of transactions from paypal' + + @transaction.atomic + def handle(self, *args, **options): + synctime = datetime.now() + api = PaypalAPI() + + cursor = connection.cursor() + cursor.execute("SELECT lastsync FROM paypal_sourceaccount WHERE id=%(id)s", { + 'id': settings.PAYPAL_DEFAULT_SOURCEACCOUNT, + }) + + # Fetch all transactions from last sync, with a 3 day overlap + for r in api.get_transaction_list(cursor.fetchall()[0][0]-timedelta(days=3)): + if r['TYPE'] in ('Payment', 'Donation', 'Purchase'): + t = PaypalTransaction(r) + elif r['TYPE'] in ('Transfer'): + t = PaypalTransfer(r) + elif r['TYPE'] in ('Refund'): + t = PaypalRefund(r) + elif r['TYPE'] in ('Fee Reversal'): + # These can be ignored since they also show up on the + # actual refund notice. + continue + elif r['TYPE'] in ('Currency Conversion (credit)', 'Currency Conversion (debit)'): + # Cross-currency payments generates multiple entries, but + # we're only interested in the main one. + continue + elif r['TYPE'] in ('Temporary Hold', 'Authorization'): + # Temporary holds and authorizations are ignored, they will + # get re-reported once the actual payment clears. + continue + else: + self.stderr.write("Don't know what to do with paypal transaction of type {0}".format(r['TYPE'])) + continue + + if t.already_processed(): + continue + t.fetch_details(api) + t.store() + + # Update the sync timestamp + cursor.execute("UPDATE paypal_sourceaccount SET lastsync=%(st)s WHERE id=%(id)s", { + 'st': synctime, + 'id': settings.PAYPAL_DEFAULT_SOURCEACCOUNT, + }) diff --git a/postgresqleu/paypal/util.py b/postgresqleu/paypal/util.py new file mode 100644 index 00000000..fd1685aa --- /dev/null +++ b/postgresqleu/paypal/util.py @@ -0,0 +1,48 @@ +from django.conf import settings + +import urllib2 +from urllib import urlencode +from urlparse import parse_qs +import itertools + +class PaypalAPI(object): + def __init__(self): + self.accessparam = { + 'USER': settings.PAYPAL_API_USER, + 'PWD': settings.PAYPAL_API_PASSWORD, + 'SIGNATURE': settings.PAYPAL_API_SIGNATURE, + 'VERSION': 95, + } + if settings.PAYPAL_SANDBOX: + self.API_ENDPOINT = 'https://api-3t.sandbox.paypal.com/nvp' + else: + self.API_ENDPOINT = 'https://api-3t.paypal.com/nvp' + + + def _api_call(self, command, params): + params.update(self.accessparam) + params['METHOD'] = command + resp = urllib2.urlopen(self.API_ENDPOINT, urlencode(params)).read() + return parse_qs(resp) + + def _dateformat(self, d): + return d.strftime("%Y-%m-%dT%H:%M:%S") + + def get_transaction_list(self, firstdate): + r = self._api_call('TransactionSearch', { + 'STARTDATE': self._dateformat(firstdate), + 'STATUS': 'Success', + }) + for i in itertools.count(1): + if not r.has_key('L_TRANSACTIONID{0}'.format(i)): + break + + yield dict([(k,r.get('L_{0}{1}'.format(k, i),[''])[0]) + for k in + ('TRANSACTIONID', 'TIMESTAMP', 'EMAIL', 'TYPE', 'AMT', 'FEEAMT', 'NAME')]) + + + def get_transaction_details(self, transactionid): + return self._api_call('GetTransactionDetails', { + 'TRANSACTIONID': transactionid, + }) diff --git a/postgresqleu/settings.py b/postgresqleu/settings.py index a7bfdbc6..5da9c294 100644 --- a/postgresqleu/settings.py +++ b/postgresqleu/settings.py @@ -133,6 +133,10 @@ PAYPAL_BASEURL='https://www.paypal.com/cgi-bin/webscr' PAYPAL_EMAIL='paypal@postgresql.eu' PAYPAL_PDT_TOKEN='abc123' PAYPAL_DEFAULT_SOURCEACCOUNT=1 +PAYPAL_API_USER='someuser' +PAYPAL_API_PASSWORD='secret' +PAYPAL_API_SIGNATURE='secret' +PAYPAL_SANDBOX=True # Change whether using sandbox or not ADYEN_BASEURL='https://test.adyen.com/' |
