summaryrefslogtreecommitdiff
path: root/postgresqleu
diff options
context:
space:
mode:
authorMagnus Hagander2016-01-18 21:35:42 +0000
committerMagnus Hagander2016-01-18 21:36:15 +0000
commit470186d25f22b64fdff26aa97686b450438a0d8a (patch)
tree17e13d762de1af244120a70a6f51887efa66d942 /postgresqleu
parenta1cbcf89512d19d8225a23c73b564b1d9fd41afb (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.py155
-rw-r--r--postgresqleu/paypal/util.py48
-rw-r--r--postgresqleu/settings.py4
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/'