Fundraising tech/Recurring donations

Recurring donations are supported via PayPal (both legacy and Express Checkout integrations) and via Ingenico (both WebCollect and Connect integrations). In CiviCRM, recurring donations are stored in the civicrm_contribution_recur table and each installment has an entry in civicrm_contribution with a value in the contribution_recur_id pointing to the civicrm_contribution_recur row. We store them slightly idiosyncratically - while core CiviCRM's payment processors interpret the 'Completed' status to mean that there are no more installments expected, we use 'Completed' to mean that the subscription is in a good state.

At Payments-wiki, we interpret any truthy value sent for the query string parameter 'recurring' to mean we should set up a recurring payment for the donor.

PayPal
PayPal recurring donations are all driven by PayPal - that is, we indicate that we want to set up a monthly donation for a specific amount, and PayPal takes care of the scheduling, charging, and retry attempts on failed charges. For PayPal Express Checkout, we use the MAXFAILEDPAYMENTS parameter to instruct PayPal to stop trying to charge the donor after N failed attempts. Express Checkout subscriptions set up prior to October 15th, 2019 had this value set to 3, and after that date have the value set to 0, meaning to retry indefinitely. On failure, PayPal retries every 5 days. PayPal's documentation

Ingenico
Ingenico recurring donations are driven by us - after setting up a recurring donation, Ingenico gives us something to store and refer to in future donation attempts. We schedule the donations and make API calls to charge them once each month. Our hourly scheduled jobs check the civicrm_contribution_recur table for rows where the next_sched_contribution_date is now or before, and charge a batch of them.

Connect
Ingenico Connect recurring donations (coded as gateway=connect) are tokenized. That is, we receive a payment token at the time of the setup and store it in the civicrm_payment_token table. The code to retry them is in the CiviCRM extension org.wikimedia.smashpig, which uses the SmashPig library to interface with the Connect API. The scheduled job is configured in recurring_smashpig_charge.yaml, which runs drush cvapi job.process_smashpig_recurring.

WebCollect
WebCollect recurring donations (coded as gateway=globalcollect) use the original transaction ID to make repeated payment. The recurring_globalcollect drupal module instantiates a globalcollect gateway adapter (using DonationInterface as a library under drupal) and makes a DO_PAYMENT call with an incremented EFFORT_ID. The scheduled job is configured in ingenico_recurring_charge.yaml, which runs drush rg.

Failures and retry logic
If we get a failure charging a recurring contribution, we set the contribution_recur status to 'Failed', increment the failure_count column, and set a failure_retry_date. The maximum number of failures and the retry interval is configurable. For Ingenico Connect, settings are in the Civi top menu under Administer->System Settings->SmashPig Settings. For WebCollect, see the 'Recurring GlobalCollect Processor' link in the left-hand menu. Both are currently set to retry failed payments 1 day later, and to stop retrying after 3 failures.