Fundraising/Data and flow/Audits
Reconciliation (Audit framework)
We have a framework to reconcile the donations/refunds/chargebacks etc in CiviCRM with the reports we receive from the payment processors.
We do this to achieve 2 goals
1) ensure that all donor gifts are shown in CiviCRM (sometimes other notifications are not received)
2) ensure that we can accurately send finance batches of the amouts settled by each of our settlement providers (currently adyen, paypal, braintree, dlocal and trustly).
The priorities are slightly different for the 2 goals. For the first goal our priority is to get the donations in as soon as we can - so we process 'whatever we get, when we get it'. In practice this means we process both payment and settlement reports from the payment processors and payment reports from Gravy. (In most cases the first report we receive is the processor payment report and the other 2 are for redundancy). We only move the report from 'incoming' to 'completed' when all transactions in it are in CiviCRM.
For the second goal our priority is to verify the exact total that settled in a batch and ensure that all the transactions that contributed to that total are recorded in CiviCRM against the batch with final amount information in the settled currency. Determining the exact total varies by payment processor so the table below shows how we reach this amount based on the information we receive from each payment processor.
The information from the settlement providers is our primary source of information. Gravy also receives these reports in some cases and returns them to us with minor formatting changes (we don't do anything with these but I have added a column to indicate status). Gravy and CiviCRM can be considered secondary sources. In the case of CiviCRM all records are ingressed into CiviCRM where they can be validated, queried and matched with GL-related information.
We retain the information we get from the primary sources on disk and can, if necessary, access it from there. We currently never delete them and have primary data from PayPal and Adyen going back over 12 years!
| Report type | Name | Batch total calculation | Gravy report | |
|---|---|---|---|---|
| Adyen | csv Settlement report | *settlement_batch* | Adyen has a row `payout` and each transaction has a batch number. The records with the batch number add up to the batch payout row. | Identical to what we get from Adyen directly (column names differ) |
| PayPal | csv Settlement report | STL-*** | PayPal has a footer section that gives the details for amounts by currency. We calculate the file total as Credits - Debits + Fee Credits - Fee Debits . To get the batch total we then deduct any Debits that relate to accounts transfers or expense re-imbursements | Data gaps:
No information to allow us to determine batch total. Some transactions missing - eg. https://phabricator.wikimedia.org/T418191 affects 4 settlement batches but is only present in 2 of the gravy files (skipped) Otherwise mostly the same as the primary source (some formatting changes) |
| Trustly | csv Settlement rport | P11KFUN- | Trustly provides the batch total in the footer and in payout rows. We use the payout rows (in case there is ever more than one per file) | n/a |
| Dlocal | csv Settlement report | cross_border | Dlocal provides the batch total in the header. The calculations are all done before rounding (5 decimal places) so we calculate a rounding transaction which we code as a fee | n/a |
| Braintree | api - returned in json | We retrieve the transactions by disbursement date. We have to add these up to get the batch total. I went through a period of manually verifying these against the Disbursement reports in the UI and these calculated totals accurately reflect the Donations + Refunds that show up in the UI.
Chargebacks sit outside those 2 - they are not counted in the main disbursement reports in the UI or retrievable by disbursement dates. These are low volume and we are currently treating them as separate batches. |
n/a |
This documents the workflow to process audit files from payment processors and import missing messages into CiviCRM.
The WMFAudit.parse api call processes the reconciliation files. The code in the civicrm extension handles reading the list of files from the directory, searching for existing transactions in the database, and finding missing information in the payments-wiki logs (mounted at /srv/archive/frlog/logs) for each transaction that isn't in the database. The code to parse the individual files to an array of normalized transactions lives under the SmashPig codebase, in classes that implement the AuditParser interface.
Audit files are located on civi1001 in /var/spool/audit/[payment-processor] and divided into two directories: incoming and completed.
How to run locally
Run the audit parser for one gateway:
wmf-cv api4 -vv WMFAudit.parse gateway=adyen logSearchPastDays=12
Run with just one specific file:
wmf-cv api4 -vv WMFAudit.parse gateway=adyen logSearchPastDays=12 file=payments_accounting_report_2024_04_06.csv
Additional optional parameters
- settleMode ("queue" or "now", blank is implicitly false) - should the settle queue be populated from these
- file (specify file name) - if set then only one file will be processed
- isStopOnFirstMissing (bool) - primarily for debug usage
- rowLimit (int) - primarily for debug usage
- offset (int) -primarily for debug usage
- logInterval (int) - how often should progress be output
- isMoveCompletedFile (bool) should the file be moved afterwards - mostly for test & debug
- isCompleted (bool) look in the completed folder - test & debug usage
Refunds
If a refund is successful it will put a message on the refund queue:
wmf-cv api4 -vv WMFQueue.Consume timeLimit=280 queueConsumer=Refund queueName=refund
params
gateway=adyen
logSearchPastDays=1
fileLimit=1
file=settlement_detail_report_batch_4.csv
settleMode=queue
isMoveCompletedFile=0
Schedule and Files
Adyen
Runs 3 times a day
payments_account_report - nightly file, has the previous days transactions
settlement_detail_report_batch - weekly file every Friday, has the previous weeks transactions
settlement_detail_xxxx_batch - same weekly file with a different name - we get both because Gravy wanted a different name. Currently parsing both due to perceived risk that Gravy requirements will change and one will go away & it will be the one we are parsing.
Amazon (DISCONTINUED)
No longer runs
settlement_data - weekly file on Tuesday
refund_data - only if there are refunds, not always present, weekly file on Tuesday
Braintree
Runs every night at 00:00 UTC
transactions, refunds, chargebacks stored in 1 json file but disbursement date for chargebacks may not be consistent with the other 2.
Dlocal
Runs every night at 00:20 UTC
files - nightly
Gravy
Runs every night at 02:10 UTC
files - nightly at 01:00 UTC. We only parse the payments file and this appears to run after other files so is a bit of a null op.
Ingenico (DISCONTINUED)
No longer runs
wikimedia_report - nightly file, has previous days transactions
Paypal
Runs nightly at 17:35 UTC
TRR - nightly Transaction Detail Report - this is used by the audit and has all the single transactions
STL - Settlement Report - this is used by the audit and has all the settled transactions
SAR - nightly Subscription Agreement Report - this 'disappeared' - possibly Gravy related.
WIkimedia - not used
Fundraiseup (DISCONTINUED)
Runs nightly at 01:00 UTC
Including new donations, new recurring, recurring cancellations, and refunds.
Payment Processor Specific Information
Adyen
Fundraising/Data and flow/PSP integrations/Adyen Checkout#Audits
Amazon
Instead of an SFTP download, we have to call methods on the Amazon Pay SDK to get our reports. This is kicked off with the DownloadReports php script in SmashPig.
Dlocal (formerly Astropay)
Fundraising/Data and flow/PSP integrations/dLocal#Audits
Braintree
Using graphQL run and consume the report nightly
Fundraiseup
Process control jobs
fundraise-up_audit.yaml - Runs at 1AM UTC daily. Calls fundraise-up_audit_download and then fundraise-up_audit_parse.
fundraise-up_audit_download - Downloads fundraiseup export files from the Fundraiseup SFTP server. This files contains the new donations, new recurrings, cancelled recurring, and refunded transactions from Fundraiseup.
fundraise-up_audit_parse - Imports the transactions from the exported files. Calls cv api4 --user=admin -vv WMFAudit.parse gateway=fundraiseup logSearchPastDays=12
PayPal
PayPal uses a python script deployed in the fundraising tools repository: https://phabricator.wikimedia.org/diffusion/WFTO/browse/master/audit/paypal/
We currently use PayPal's Express Checkout integration, and classify those donations using gateway value 'paypal_ec'. However, we still have some legacy recurring donations from the old integration, and those are classified with gateway value 'paypal'.
Log
check audit parse log from frlog1002, located at /srv/archive/civi/process-control/<yyyymmdd>/<paymentMethod>_audit_parse-<yyyymmdd>-xxxxxx.log.civi1001.bz2
Result print example as:
Done! Final stats:
Total number of donations in audit file: xxx
Number missing from database: xxx
Missing transactions found in logs: xxx
Missing transactions not found in logs: xxx
Missing transaction summary:
Regular donations: 2
Returned from hook drush_wmf_audit_parse_audit [1.13 sec, 36.01 MB] [debug]
xxxxxxxx: 1 xxxxxxxx: 1
Refunds and chargebacks: 0
Recurring donations: 0
Command dispatch complete [1.13 sec, 35.92 MB] [notice]
Transaction IDs:
xxx xxxxxxxxxx xxx xxxxxxxxxx
Initial stats on recon files: Array
(
[/var/spool/audit/xxx/incoming/xxx] => 0 [/var/spool/audit/xxx/incoming/xxx] => 0 [/var/spool/audit/xxx/incoming/xxx] => 2
)
File Wrangling
Sometimes the audit processor can't resolve all the transactions in a file, even after trying for several days. This can lead to a build-up of files in the incoming directory and to subsequent processor runs getting longer and longer till finally they start timing out. The solution is to 'manually' move the older files from the incoming to the completed directory. Since our personal accounts don't have permissions to move the files, we do this with a one-off process-control job such as ingenico_move_audit_files. Since process control runs each command as a separate process under python, we need to wrap any file globs that we want expanded with 'sh -c', for example:
sh -c "mv /srv/archive/civi1001/audit/globalcollect/incoming//wx1*202010[01][0-5]*xml* /var/spool/audit/globalcollect/"
Adding a New Payment Processor
Once the new processor code has been added
1. Enable the Module
2. Add the folders
This is done via puppet by adding the processor name to the $audit_processors array in modules/civicrm/manifests/audit.pp. Additionally, some files (YAML usually) may be needed for the audit configuration. Those vary by processor but are stored in the same audit.pp manifest.