Mwalker has uploaded a new change for review.
https://gerrit.wikimedia.org/r/77463
Change subject: Doing something here with amazon auditing
......................................................................
Doing something here with amazon auditing
Change-Id: Ic418fe8869800314679a6dd886c513a25d9a7123
---
A AmazonAudit/amazon-audit-chargebacks.py
A AmazonAudit/amazon-csv-audit.py
2 files changed, 391 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/wikimedia/fundraising/tools
refs/changes/63/77463/1
diff --git a/AmazonAudit/amazon-audit-chargebacks.py
b/AmazonAudit/amazon-audit-chargebacks.py
new file mode 100644
index 0000000..5f9f571
--- /dev/null
+++ b/AmazonAudit/amazon-audit-chargebacks.py
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+
+from amazon import Amazon
+import stomp
+from ConfigParser import SafeConfigParser
+from optparse import OptionParser
+from datetime import datetime
+import dateutil.parser
+import time
+import pytz
+import MySQLdb as MySQL
+import json
+import sys
+import os
+
+AWS_HISTORY_FILE_VERSTR = "AWSChargebackHistory.1"
+
+def main():
+ # === Extract options ===
+ parser = OptionParser(usage="usage: %prog [options] <# of seconds to
audit>")
+ parser.add_option("-c", "--config", dest='configFile', default=None,
help='Path to configuration file')
+ parser.add_option("-g", "--gracePeriod", dest='gracePeriod', default=0,
help='Number of seconds from now backwards to ignore')
+ parser.add_option("-i", "--historyFile", dest='historyFile', default=None,
help='Stores any pending transactions and the last run time')
+ parser.add_option('-l', "--logFile", dest='logFile', default=None,
help='Saves a log of all Amazon transactions')
+ (options, args) = parser.parse_args()
+
+ if len(args) != 1:
+ parser.print_usage()
+ exit()
+
+ startTime = datetime.fromtimestamp(int(time.time()) - int(args[0]),
pytz.utc)
+ endTime = datetime.fromtimestamp(int(time.time()) -
int(options.gracePeriod), pytz.utc)
+ print("AWS refund audit requested from %s to %s" % (startTime.isoformat(),
endTime.isoformat()))
+
+ # === Get the configuration options ===
+ config = SafeConfigParser()
+ fileList = ['./amazon-config.cfg']
+ if options.configFile is not None:
+ fileList.append(options.configFile)
+ config.read(fileList)
+
+ # === Open up ze STOMP ===
+ host_and_ports = (config.get('Stomp', 'server'), config.getint('Stomp',
'port'))
+ sc = stomp.Connection(host_and_ports=[host_and_ports])
+ sc.start()
+ sc.connect()
+
+ # === Connection to Amazon ===
+ aws = Amazon(
+ awsEndpoint = config.get('AwsConfig', 'endpoint'),
+ awsAccessKey = config.get('AwsConfig', 'accessKey'),
+ awsSecret = config.get('AwsConfig', 'secretKey')
+ )
+
+ # === Connection to MySQL ===
+ dbcon = MySQL.connect(
+ config.get('MySQL', 'host'),
+ config.get('MySQL', 'user'),
+ config.get('MySQL', 'password'),
+ config.get('MySQL', 'schema')
+ )
+
+ # === Open up the history and log files ===
+ # If the history file exists, it will modify the start time of this script
to be the end time of the
+ # history file.
+ hfile = None
+ historyStart = startTime
+ historyEnd = endTime
+ if options.historyFile and os.path.exists(options.historyFile):
+ hfile = open(options.historyFile, 'r')
+ if hfile.readline().strip() == AWS_HISTORY_FILE_VERSTR:
+ historyStart = dateutil.parser.parse(hfile.readline().strip())
+ historyEnd = dateutil.parser.parse(hfile.readline().strip())
+ startTime = historyEnd
+ print("History file modified search period, now %s to %s" %
(startTime.isoformat(), endTime.isoformat()))
+ else:
+ print('Not starting with a valid history file.')
+
+ sfile = None
+ if options.logFile:
+ sfile = open(options.logFile, 'a')
+ sfile.write("!!! Starting run for dates %s -> %s\n" %
(startTime.isoformat(), endTime.isoformat()))
+
+ # === Sanity checks ===
+ if endTime < startTime:
+ startTime = endTime
+
+ # === Main Application ===
+ # --- Process all previously pending transactions from the history file.
If the transaction is still in some form
+ # of pending, add it back to the history list.
+ historyCount = 0
+ historyList = []
+ historyStats = {
+ 'Success': 0,
+ 'Pending': 0,
+ 'Failed': 0,
+ 'Ignored': 0
+ }
+ if hfile:
+ print("Processing history file")
+ for txn in hfile:
+ historyCount += 1
+ txn = json.loads(txn)
+ result = processTransaction(txn, dbcon, aws, sc, sfile, config)
+ historyStats[result] += 1
+ if result == 'Pending':
+ historyList.append(txn)
+ hfile.close()
+
+ # --- Obtain AWS history ---
+ print("Obtaining AWS transactions for the period %s -> %s" %
(startTime.isoformat(), endTime.isoformat()))
+ awsTransactions = aws.getAccountActivity(startTime, endDate=endTime,
fpsOperation='Pay')
+ print("Obtained %d transactions" % len(awsTransactions))
+
+ # --- Main loop: checks each aws transaction against the Civi database;
adding it if it doesn't exist ---
+ txncount = 0
+ for txn in awsTransactions:
+ txncount += 1
+ result = processTransaction(txn, dbcon, aws, sc, sfile, config)
+ historyStats[result] += 1
+ if result == 'Pending':
+ historyList.append(txn)
+
+ print("\n--- Finished processing of messages. ---\n")
+
+ # --- Prepare the history file for write ---
+ if options.historyFile:
+ print("Rewriting history file with %d transactions" % len(historyList))
+ hfile = open(options.historyFile, 'w')
+ hfile.write("%s\n%s\n%s\n" % (AWS_HISTORY_FILE_VERSTR,
historyStart.isoformat(), endTime.isoformat()))
+ for txn in historyList:
+ hfile.write("%s\n" % json.dumps(txn))
+ print("Flushing history file in preparation for main loop")
+ hfile.flush()
+
+ # --- Final statistics ---
+ print("%d new AWS messages" % txncount)
+ print(" Additionally %d messages were processed from history" %
historyCount)
+ print("This resulted in the following:")
+ for entry in historyStats.items():
+ print(" %s Messages: %d" % entry)
+
+ # === Final Application Cleanup ===
+ print("\nCleaning up.")
+ dbcon.close()
+ sc.disconnect()
+
+ if hfile:
+ hfile.close()
+ if sfile:
+ sfile.close()
+
+ time.sleep(1) # Let the STOMP library catch up
+
+def processTransaction(txn, dbcon, aws, sc, sfile, config):
+ """Main message processing logic. Will determine if a message needs to be
injected or not
+
+ txn -- The transaction from getAccountActivity()
+ aws -- The AWS connection object
+ sc -- The Stomp connection object
+ sfile -- The log file
+ config -- The configuration object
+
+ returns "Success on injection, Pending on AWS pending, Failed on AWS
failure, Ignored on already present in Civi
+ """
+ retval = ''
+ smallString = '.'
+ ctid = '?'
+
+ if (txn['TransactionStatus'] != 'Failure') and (not
isTxnInCivi(txn['TransactionId'], dbcon)):
+ # Get additional information about the transaction because
getAccountActivity does not provide all
+ # the required information. We also have to check what the status of
this transaction is.
+ txnInfo = aws.getTransaction( txn['TransactionId'] )
+
+ # Do that aforementioned status check
+ if txnInfo['TransactionStatus'] == 'Success':
+ ctid = remediateTransaction(txn, txnInfo, sc, config)
+ retval = 'Success'
+ smallString = '+'
+ elif txnInfo['TransactionStatus'] == 'Pending' or
txnInfo['TransactionStatus'] == 'Reserved':
+ retval = 'Pending'
+ smallString = '-'
+ else:
+ retval = 'Failed'
+ else:
+ retval = 'Ignored'
+
+ bigString = "%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
+ retval,
+ txn['TransactionStatus'],
+ txn['TransactionId'],
+ ctid if ctid is not None else '?',
+
dateutil.parser.parse(txn['DateReceived']).astimezone(dateutil.tz.tzutc()).isoformat(),
+
dateutil.parser.parse(txn['DateCompleted']).astimezone(dateutil.tz.tzutc()).isoformat(),
+ datetime.now(dateutil.tz.tzutc()).isoformat()
+ )
+
+ if sfile:
+ sfile.write("%s\n" % bigString)
+ sys.stdout.write(smallString)
+ sys.stdout.flush()
+ else:
+ print(bigString)
+
+ return retval
+
+def isTxnInCivi(txnid, dbcon):
+ """ Query the Civi database to determine if the txnid is present.
+
+ txnid -- The Amazon transaction ID
+ dbcon -- Database connection object
+
+ returns True if transaction is present in Civi
+ """
+
+ cur = dbcon.cursor()
+ cur.execute("SELECT trxn_id FROM civicrm_contribution WHERE trxn_id LIKE
'AMAZON %s%%';" % txnid)
+ rc = cur.rowcount
+ cur.close()
+ if rc >= 1:
+ return True
+ else:
+ return False
+
+def remediateTransaction(txn, txnInfo, sc, config):
+ """Injects a new message into the queue for consumption by Civi
+
+ txn -- The transaction data given from the getAccountActivity call
+ txnInfo -- Transaction data given from the getTransaction call
+ sc -- Stomp queue object
+ config -- Configuration object
+
+ returns -- The contribution tracking ID
+ """
+
+ # --- Get our contribution tracking ID (but it's stupid because AWS is
stupid... ugh!)
+ ctid = None
+ if isinstance(txn['TransactionPart'], list):
+ for part in txn['TransactionPart']:
+ if part['Role'] == 'Recipient' and 'Reference' in part:
+ ctid = part['Reference']
+ elif txn['TransactionPart']['Role'] == 'Recipient' and 'Reference' in
txn['TransactionPart']:
+ ctid = txn['TransactionPart']['Reference']
+
+ if ctid is not None:
+ try:
+ ctid = int(ctid)
+ except ValueError:
+ if not '-' in ctid:
+ # It's not a number or a UUID... very strange... not using is
+ ctid = None
+
+ # Construct the STOMP message
+ headers = {
+ 'correlation-id': 'amazon-%s' % txn['TransactionId'],
+ 'destination': config.get('Stomp', 'verified-queue'),
+ 'persistent': 'true'
+ }
+ msg = {
+ "contribution_tracking_id": ctid if ctid is not None else '',
+ "gateway_txn_id": txn['TransactionId'],
+
+ "email": txnInfo['SenderEmail'],
+ "first_name": txnInfo['SenderName'].split(' ')[0],
+ "middle_name":"",
+ "last_name": " ".join(txnInfo['SenderName'].split(' ')[1:]),
+ "last_name_2":"",
+
+ "currency": txnInfo['TransactionAmount']['CurrencyCode'],
+ "original_currency": txnInfo['TransactionAmount']['CurrencyCode'],
+ "original_gross": txnInfo['TransactionAmount']['Value'],
+ "fee":"0",
+ "gross": txnInfo['TransactionAmount']['Value'],
+ "net": txnInfo['TransactionAmount']['Value'],
+
+ "date":
dateutil.parser.parse(txnInfo['DateReceived']).astimezone(dateutil.tz.tzutc()).strftime('%s'),
+
+ "gateway":"amazon",
+ "gateway_account": config.get('AwsConfig', 'accountName'),
+ "payment_method":"amazon",
+ "payment_submethod": txn['PaymentMethod'],
+ "referrer":"",
+ "comment":"", "size":"",
+ "premium_language":"en",
+ "language":"en",
+ "utm_source":"..amazon",
+ "utm_medium":"",
+ "utm_campaign":"",
+ "street_address":"",
+ "supplemental_address_1":"",
+ "city":"",
+ "state_province":"",
+ "country":"",
+ "street_address_2":"",
+ "supplemental_address_2":"",
+ "city_2":"",
+ "state_province_2":"",
+ "country_2":"",
+ "postal_code_2":"",
+ "user_ip":"",
+ "response":False,
+ "recurring":""
+ }
+
+ # Inject the message
+ sc.send(
+ json.dumps(msg),
+ headers
+ )
+
+ return ctid
+
+if __name__ == "__main__":
+ main()
diff --git a/AmazonAudit/amazon-csv-audit.py b/AmazonAudit/amazon-csv-audit.py
new file mode 100644
index 0000000..b44bfb5
--- /dev/null
+++ b/AmazonAudit/amazon-csv-audit.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+# Using a CSV file downloaded from the Amazon Payments portal, check that all
expected transactions
+# are present in Civi.
+
+import csv
+import sys
+from dateutil.parser import parse as dparse
+import MySQLdb as MySQL
+
+if len(sys.argv) != 5:
+ print("Expected path to CSV file, dbhost, dbuser, dbpass")
+ exit(-1)
+
+r = csv.reader(file(sys.argv[1],'r'))
+
+def isTxnInCivi(txnid, dbcon):
+ """ Query the Civi database to determine if the txnid is present.
+
+ txnid -- The Amazon transaction ID
+ dbcon -- Database connection object
+
+ returns True if transaction is present in Civi
+ """
+
+ cur = dbcon.cursor()
+ cur.execute("SELECT trxn_id FROM civicrm_contribution WHERE trxn_id LIKE
'AMAZON %s%%';" % txnid)
+ rc = cur.rowcount
+ cur.close()
+ if rc == 1:
+ return True
+ elif rc > 1:
+ print "%s duplicated!" % txnid
+ return True
+ else:
+ return False
+
+dbcon = MySQL.connect(
+ sys.argv[2],
+ sys.argv[3],
+ sys.argv[4],
+ 'civicrm'
+)
+
+# Header line
+r.next()
+
+found = 0
+notfound = 0
+pending = 0
+other = 0
+
+for record in r:
+ date = record[0]
+ type = record[1]
+ tf = record[2]
+ donorName = record[3]
+ status = record[4]
+ amount = record[5]
+ fees = record[6]
+ txnid = record[7]
+
+ if type=='Payment' and tf=='From':
+ if status=="Completed":
+ if not isTxnInCivi(txnid, dbcon):
+ print "%s - %s not found" % (date, txnid)
+ notfound += 1
+ else:
+ found += 1
+ elif status=="Pending":
+ print "%s pending!" % txnid
+ pending += 1
+ else:
+ other+=1
+
+dbcon.close()
+
+print("Found: %d\nNot Found: %d\nPending: %d\nOther: %d" % (found, notfound,
pending, other))
--
To view, visit https://gerrit.wikimedia.org/r/77463
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic418fe8869800314679a6dd886c513a25d9a7123
Gerrit-PatchSet: 1
Gerrit-Project: wikimedia/fundraising/tools
Gerrit-Branch: master
Gerrit-Owner: Mwalker <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits