|
|
# Overview
|
|
|
|
|
|
This page provides a proposal for how CiviCRM core would be modified to enable refunds in CiviCRM via payment processor extensions.
|
|
|
|
|
|
# Background
|
|
|
|
|
|
Currently (Nov 2018) there is support for recording in CiviCRM a refund that is made offline, eg by giving person cash, mailing them a cheque, reversing a payment through a PoS system, or even logging into the account for a payment processor used by CiviCRM and reversing in that system a payment received by CiviCRM. However, there is no way as of 5.7 to refund from within CiviCRM a payment that CiviCRM has received. Many APIs for payment processors integrated with CiviCRM have this functionality.
|
|
|
|
|
|
https://stripe.com/docs/api/refunds?lang=php
|
|
|
https://github.com/AuthorizeNet/sample-code-php/blob/master/PaymentTransactions/refund-transaction.php#L9
|
|
|
https://github.com/Moneris/eCommerce-Unified-API-PHP/blob/master/Examples/CA/TestRefund.php#L30
|
|
|
https://github.com/iATSPayments/PHP/blob/master/Tests/ProcessLinkTest.php#L544
|
|
|
|
|
|
# Exposing Payment Processor Support for Refunds
|
|
|
|
|
|
Even if a payment processor API supports refunds, and its integration with CiviCRM supports refunds, the account setup for a particular organization or a particular account for CiviCRM may not have enabled that functionality.
|
|
|
|
|
|
In order to support processor integrations that might want to dynamically query the payment processor to determine if support for refunds is currently turned on or off, core will call the following method on payment processors to determine if this functionality is available.
|
|
|
|
|
|
- [ ] In CRM/Core/Payment.php which defines the abstract CRM_Core_Payment class that is the parent class of payment processors, define the following method:
|
|
|
```php
|
|
|
public function supportsRefund() {return FALSE;}
|
|
|
```
|
|
|
Payment processors will selectively override this function (to return TRUE) if they (are currently configured to) support refunds.
|
|
|
|
|
|
# Enhance Core to Link Refunds to Payments
|
|
|
|
|
|
A contribution may have:
|
|
|
- been edited multiple times to increase or decrease the total amount owed.
|
|
|
- had one or more payments via different payment methods or payment processors.
|
|
|
- had one or more refunds recorded or paid.
|
|
|
|
|
|
Currently recording a refund made outside of CiviCRM is not linked to a specific payment or payment(s) received by CiviCRM. We propose to enhance what is stored in the existing schema to enable tracking of refunds against payments. This will enable refunds to be paid when multiple payments have been made against a contribution.
|
|
|
|
|
|
No support will be provided in the initial implementation for an interface to allow users to specify what payments to refund. It will be assumed that oldest payments will be refunded first, both for recording refunds made outside of CiviCRM and paying refunds via CiviCRM.
|
|
|
|
|
|
This means certain use cases combining online and offline payments and refunds will not be supported well. For example, suppose an online payment for $100 is followed by an offline payment of $100 then an offline refund of $100 is recorded. At this point the online payment is marked as refunded, and an online payment of a refund against the original payment will not be supported. A workaround would be for the organization to do an additional refund outside of CiviCRM (eg through the payment processor website) and then record it in CiviCRM.
|
|
|
|
|
|
## Implementation of Link from Refunds to Payments
|
|
|
|
|
|
- [ ] Once the current spec is approved we will change the CiviAccounts Data Flow spec (https://wiki.civicrm.org/confluence/display/CRM/CiviAccounts+Data+Flow#CiviAccountsDataFlow-Extension:ProcessPendingRefundPayment(RecordpaymentforAccountsPayableoutstanding)) to insert one or more additional entity_financial_trxn records linking the refund's financial_trxn record to financial_trxn records representing completed payments received for the contribution.
|
|
|
|
|
|
- [ ] Implement the following algorithm for determining which payments to link to when *recording* a refund (as opposed to paying for one) is: refund oldest payment first; if amount to be refunded is greater than a payment, then refund next most recent payment until amount to be refunded has been fully allocated across payment(s).
|
|
|
|
|
|
- [ ] Use this algorithm on upgrade to insert appropriate records to deal with existing refunds.
|
|
|
|
|
|
- [ ] Implement a similar algorithm for determining which payments to *pay* refunds to, filtering to payments made via payment processors that support refunds.
|
|
|
|
|
|
# Making a Refund: BAO interface
|
|
|
|
|
|
Reviewing the APIs for refunds listed above shows that multiple refunds can usually be made against the same payment up to the original amount of the payment. Core CiviCRM will be responsible for determining the amount of a refund.
|
|
|
|
|
|
Payment information is stored in civicrm_financial_trxn. The payment processor for a payment (https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L211) provides a trxn_id (https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L173) which generally needs to be passed back in order to do a refund.
|
|
|
|
|
|
We will have a core BAO function that is not overridable by payment processors, payRefund(). It will be responsible for:
|
|
|
1. [ ] Determining if the relevant payment processor supports refunds and raising an error if it doesn't.
|
|
|
1. [ ] Invoking a pre hook to allow amount to be altered or for refund action to be cancelled.
|
|
|
1. [ ] Confirming that contribution status is Refund pending, and that the $amount to be refunded is less than the currently paid amount on the payment minus any refunds that have been processed to completion. (We'll allow repeated attempts to refund an amount if the status of these attempts is pending, failed, etc.)
|
|
|
1. [ ] Calling payment processors to do the refund payment via a CRM_Core_Payment:payRefundPayment() function defined below after marshalling the appropriate arguments.
|
|
|
1. [ ] If the refund payment succeeds then
|
|
|
1.1 [ ] Recording appropriate financial transaction similar to https://wiki.civicrm.org/confluence/display/CRM/CiviAccounts+Data+Flow#CiviAccountsDataFlow-Extension:ProcessPendingRefundPayment(RecordpaymentforAccountsPayableoutstanding).
|
|
|
1.1 [ ] The determination of an appropriate status for the contribution is getting very difficult. We will stipulate that if the status will be as follows:
|
|
|
case:
|
|
|
refunded amount plus paid amount < contribution total: Partially paid;
|
|
|
case:
|
|
|
refunded amount plus paid amount == contribution total: Completed;
|
|
|
case:
|
|
|
refunded amount plus paid amount > contribution total: Refund owing (no change);
|
|
|
1. [ ] Returning null or a reference to the civicrm_financial_trxn of the refund successfully created.
|
|
|
|
|
|
The calling code is responsible for higher level business logic, such as dealing with status of related objects like event registrations, memberships, access to purchased downloadable items, etc.
|
|
|
|
|
|
```php
|
|
|
/**
|
|
|
*
|
|
|
* payRefund
|
|
|
* ....
|
|
|
* @return \CRM_Financial_DAO_FinancialTrxn
|
|
|
* @throws \CRM_Core_Exception
|
|
|
*/
|
|
|
function payRefund($financial_trxn_id, // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L10
|
|
|
$amount, // decimal to number of significant digits for currency (default 2)
|
|
|
$currency // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L134
|
|
|
) {
|
|
|
|
|
|
....
|
|
|
}
|
|
|
```
|
|
|
|
|
|
# Making a Refund: Payment Processor interface
|
|
|
|
|
|
- [ ] The following function will be defined by core CiviCRM in the parent class of payment processors, which will be required to override the class with a concrete implementation if they support making refunds.
|
|
|
|
|
|
```php
|
|
|
|
|
|
/**
|
|
|
*
|
|
|
* payRefundPayment
|
|
|
* ....
|
|
|
* @return \CRM_Financial_DAO_FinancialTrxn
|
|
|
* @throws \CRM_Core_Exception
|
|
|
*/
|
|
|
public function payRefundPayment(
|
|
|
$financial_trxn_id, // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L10
|
|
|
$amount, // decimal to number of significant digits for currency (default 2)
|
|
|
$currency, // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L134
|
|
|
$card_type_id, // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L244
|
|
|
$contact_id, // to be used as customer_id where needed
|
|
|
$trxn_id, // https://github.com/civicrm/civicrm-core/blob/master/xml/schema/Financial/FinancialTrxn.xml#L173 This is the trxn id of the original payment, that is needed by the refund APIs, from civicrm_financial_trxn.trxn_id, though specific payment processors can choose to override this with something else if they prefer
|
|
|
$contribution_id, // not really needed but provides convenient access
|
|
|
$contribution_recur_id // not really needed but provides convenient access in case payment being refunded is part of a recurring series and there is information like a unique id associated with the series or its first payment
|
|
|
) {
|
|
|
return NULL;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
# Making a Refund - User Interface
|
|
|
|
|
|
This section specifies how CiviCRM will initially support *paying* refunds for Contributions, Memberships and Event registrations in addition to the current support for *recording* refunds processed outside of Civi. Future efforts may enhance this initial implementation.
|
|
|
|
|
|
## Refund Receipt
|
|
|
|
|
|
A new core Message Template will be added to tarball and on upgrade that will contain information on a refund. It will be sent to the same email as a contribution receipt, and will be based on the template for Contributions - Receipt (online).
|
|
|
|
|
|
## Backoffice
|
|
|
|
|
|
Wherever 'Record Refund' button is shown on a backoffice form, if there is an unrefunded amount paid through a payment processor which supports refunds (see above for details) then an additional 'Pay Refund' button will be displayed. If Pay Refund is clicked and the unrefunded amount is greater than the unrefunded amounts of payments made through payment processor that support refunds, then present a dialog box 'Would you like to pay a refund of $x online? $y is greater than the balance of online payments through processors that support refunds.' with 'Pay Refund' and 'Cancel' options, where $x is the unrefunded amount of payments for contribution through processors that support refunds, and $y is the amount to be refunded for the contribution.
|
|
|
|
|
|
Upon submission, a refund request will be posted to the payment processor via the refund API and the payment processor integrations.
|
|
|
|
|
|
The 'Record Refund' link button becomes available in various places when the amount paid exceeds the amount owed:
|
|
|
|
|
|
$ grep -R 'Record Refund' ./*
|
|
|
./CRM/Contribute/Selector/Search.php: $buttonName = ts('Record Refund');
|
|
|
./CRM/Contribute/Page/ContributionRecurPayments.php: $buttonName = ts('Record Refund');
|
|
|
./CRM/Contribute/Form/AdditionalPayment.php: $buttonName = $this->_refund ? 'Record Refund' : 'Record Payment';
|
|
|
./CRM/Contribute/Form/ContributionView.php: $this->assign('paymentButtonName', ts('Record Refund'));
|
|
|
./CRM/Contribute/BAO/Contribution.php: 'title' => ts('Record Refund'),
|
|
|
./CRM/Event/Selector/Search.php: 'name' => ts('Record Refund'),
|
|
|
./CRM/Event/Selector/Search.php: 'title' => ts('Record Refund'),
|
|
|
./templates/CRM/Contribute/Page/PaymentInfo.tpl: {assign var=paymentButtonName value='Record Refund'}
|
|
|
|
|
|
This can happen either when a payment is recorded that exceeds the amount owing, or when an edit is made that reduces the amount owing below what has been paid. These edits can be made either on Edit Event Registration using Change Fee Selections feature, or on the Edit Contribution page. (Note that using Edit Membership to change it to a different less expensive type does not change the associated Contribution, and instead prompts the backend user to use Edit Contribution to do that.)
|
|
|
|
|
|
## FrontOffice
|
|
|
|
|
|
It is possible for users to cancel an event registration if Allow Self-service Cancellation or Transfer functionality is enabled for an event.
|
|
|
|
|
|
Create a CiviContribute Setting, 'Enable auto-refund of self-service cancellations'. For initial implementation, this setting will apply to all events and there will be no event specific setting exposed when the site wide setting is enabled.
|
|
|
|
|
|
When:
|
|
|
1. this setting is enabled,
|
|
|
1. a self-service cancellation is done, and
|
|
|
1. the contribution paying for the event registration has an unrefunded payment via a payment processor supporting refunds that exceeds the cost of the event registration
|
|
|
Then:
|
|
|
1. a refund will be automatically processed for the amount of the cancelled registration, and
|
|
|
1. a Refund Receipt will be sent.
|
|
|
|
|
|
|
|
|
# Tasks
|
|
|
|
|
|
* [ ] Create permission 'CiviContribute: pay refund'
|
|
|
* [ ] Add fn ```supportRefund()``` to CRM_Core_Payment that will be overriden by Payment Processor class wrapper to return TRUE, if the Payment Processor supports refund
|
|
|
* [ ] Add BAO function ```payRefund()```.If ```supportRefund()``` return TRUE, processor will call its ```payRefundPayment(...)``` fn.
|
|
|
* [ ] Register Offline and Online Refund reciept template. Handle is install and upgrade.
|
|
|
* [ ] Implement ```Pay Refund``` feature as explained in **Paying Refund/Online Form**
|
|
|
* [ ] Modify 'Record Refund' backoffice form to support refund via PP (this involves adding 'Payment Processor' field - select field to show PPs of same type that support refund)
|
|
|
* [ ] Support 'Pending Refund' in Membership backoffice form.
|
|
|
* [ ] Extend Order and Payment APIs to handle refunds.
|
|
|
* [ ] Add Unit tests
|
|
|
|
|
|
----------
|
|
|
|
|
|
For lack of a commenting function (grrr), I've cleaned up the spec above and moved discussion down to here.
|
|
|
|
|
|
# Discussion: Hooks
|
|
|
|
|
|
Should we include no hooks, an alterParams hook, a pre hook to allow cancel, a post hook to allow reactions to refund?
|
|
|
|
|
|
* MJW: {some stuff removed as incorporated above} We should be very clear what parameters are passed, and if possible pass all relevant IDs, eg. contribution_id, contribution_recur_id, membership_id, participant_id, pledge_id. That will make future use-cases far easier to implement*
|
|
|
> Monish: Agree. I have added a $params which will contain additional info.
|
|
|
|
|
|
* MJW: It would also be really nice to see a hook that could be triggered at the beginning of payRefund() (and later extended to all payment processor methods) - see for example in the Smartdebit extension: `CRM_Smartdebit_Hook::alterVariableDDIParams($recurParams, $smartDebitParams, 'cancel');` https://github.com/mattwire/org.civicrm.smartdebit/blob/master/CRM/Core/Payment/Smartdebit.php#L1108
|
|
|
This allows for extensions to do other things such as modify amounts, add/remove parameters when a payment processor action is happening.
|
|
|
> Monish: I have modified the function definition which now calls ```CRM_Utils_Hook::alterPaymentProcessorParams``` at the beginning of the fn.
|
|
|
|
|
|
Joe: FWIW, I'm philosophically opposed to making pre hooks on all payment processor methods. In this case, I'd be willing to implement a hook as specified above. My preference is that the hook NOT allow changes to additional parameters passed as this is a bit dangerous. Making it easier to stop payments going out from the organization rather than easier to change how much and to whom it will go seems very prudent. Loosening this restriction on a well-articulated case by case basis is my recommendation. I tend to think that hooks into higher level business processes would be more appropriate.
|
|
|
|
|
|
Despite MJW's points, Joe prefers to not include a bag of $params as defined below at the end of the argument list for various reasons including:
|
|
|
1. We should not bloat the surface of an interface with arguments that might conceivably be useful in some future use cases, but restrict current implementation to well understood use cases.
|
|
|
2. We _*don't*_ want payment processors to have any responsibility to handle business functions around what happens if a refund succeeds or fails. (FWIW, from contribution_id it is possible for a bad payment processor integration that wants to to hack around this restriction.)
|
|
|
3. I would also be concerned about the performance hit associated with looking up all potentially related objects in order to pass them to the hook as a convenience for those using the hook.
|
|
|
4. If we did want to pass the following information to payment processors when they are asked to perform a refund, then a better form imho would be as explicitly named arguments with types that can be checked and prompted for by IDE. This makes it very clear what is being passed.
|
|
|
5. If we do go ahead with $params arg following the pattern present in so many other places in codebase, then this spec should explicitly name what will and what will not be included in the array, and the required or allowed array keys should be documented in any pre-hook implementation.
|
|
|
|
|
|
Additional potential argument that some like and some don't:
|
|
|
$params // contains additional information membership_id, participant_id, pledge_id etc.
|
|
|
) {
|
|
|
$newParams = $params;
|
|
|
CRM_Utils_Hook::alterPaymentProcessorParams($this,
|
|
|
$params,
|
|
|
$newParams
|
|
|
);
|
|
|
foreach ($newParams as $field => $value) {
|
|
|
$this->_setParam($field, $value);
|
|
|
} |
|
|
\ No newline at end of file |