Commit 93064c6f authored by mattwire's avatar mattwire
Browse files

Add support for issuing refunds via the payment UI for payment processors that...

Add support for issuing refunds via the payment UI for payment processors that support refunds (eg. Stripe).
parent 59b346b5
<?php
use CRM_Mjwshared_ExtensionUtil as E;
/**
* Form controller class
*
* @see https://docs.civicrm.org/dev/en/latest/framework/quickform/
*/
class CRM_Mjwshared_Form_PaymentRefund extends CRM_Core_Form {
/**
* @var int $paymentID
*/
private $paymentID;
/**
* @var int $contributionID
*/
private $contributionID;
public function buildQuickForm() {
if (!CRM_Core_Permission::check('edit contributions')) {
CRM_Core_Error::statusBounce(ts('You do not have permission to access this page.'));
}
if ($this->isSubmitted()) {
return;
}
$this->setTitle('Refund payment');
$this->paymentID = CRM_Utils_Request::retrieveValue('id', 'Positive', NULL, FALSE, 'GET');
if (!$this->paymentID) {
CRM_Core_Error::statusBounce('Payment not found!');
}
$this->contributionID = CRM_Utils_Request::retrieveValue('contribution_id', 'Positive', NULL, FALSE, 'GET');
if (!$this->contributionID) {
CRM_Core_Error::statusBounce('Contribution not found!');
}
$this->add('hidden', 'payment_id');
$lineItems = civicrm_api3('LineItem', 'get', [
'contribution_id' => $this->contributionID,
])['values'];
foreach ($lineItems as $lineItemID => $lineItemDetails) {
switch ($lineItemDetails['entity_table']) {
case 'civicrm_participant':
$participantIDs[] = $lineItemDetails['entity_id'];
break;
}
}
if (!empty($participantIDs)) {
$this->set('participant_ids', $participantIDs);
$participants = civicrm_api3('Participant', 'get', [
'id' => ['IN' => $participantIDs],
])['values'];
$this->assign('participants', $participants);
$this->addYesNo('cancel_participants', E::ts('Cancel Participants'), NULL, TRUE);
}
$this->addButtons([
[
'type' => 'submit',
'name' => ts('Refund'),
'isDefault' => TRUE,
],
[
'type' => 'cancel',
'name' => ts('Cancel'),
],
]);
}
public function setDefaultValues() {
if ($this->paymentID) {
$this->_defaults['payment_id'] = $this->paymentID;
$this->set('payment_id', $this->paymentID);
}
return $this->_defaults;
}
public function postProcess() {
$formValues = $this->getSubmitValues();
$paymentID = $this->get('payment_id');
$participantIDs = $this->get('participant_ids');
// For some reason cancel_participants is a required field but is not being validated as "required" by the form.
// so do a crude validation here.
if (!empty($participantIDs) && !isset($formValues['cancel_participants'])) {
CRM_Core_Error::statusBounce('Cancel Participants is a required field');
}
try {
$payment = reset(civicrm_api3('Mjwpayment', 'get_payment', ['id' => $paymentID])['values']);
$refundParams = [
'payment_processor_id' => $payment['payment_processor_id'],
'amount' => $payment['total_amount'],
'currency' => $payment['currency'],
'trxn_id' => $payment['trxn_id'],
];
$refund = reset(civicrm_api3('PaymentProcessor', 'Refund', $refundParams)['values']);
if ($refund['refund_status_name'] === 'Completed') {
$refundPaymentParams = [
'contribution_id' => $payment['contribution_id'],
'trxn_id' => $refund['refund_trxn_id'],
'order_reference' => $payment['order_reference'],
'total_amount' => (-$payment['total_amount']),
'payment_processor_id' => $payment['payment_processor_id'],
];
// Record the refund in CiviCRM
civicrm_api3('Mjwpayment', 'create_payment', $refundPaymentParams);
$message = E::ts('Refund was processed successfully.');
if ($formValues['cancel_participants'] && !empty($participantIDs)) {
foreach ($participantIDs as $participantID) {
civicrm_api3('Participant', 'create', [
'id' => $participantID,
'status_id' => 'Cancelled',
]);
}
$message .= ' ' . E::ts('Cancelled %1 participant registration(s).', [1 => count($participantIDs)]);
}
CRM_Core_Session::setStatus($message, 'Refund processed', 'success');
}
else {
CRM_Core_Error::statusBounce("Refund status '{$refund['refund_status_name']}'is not supported at this time and was not recorded in CiviCRM.");
}
} catch (Exception $e) {
CRM_Core_Error::statusBounce($e->getMessage(), NULL, 'Refund failed');
}
}
}
......@@ -14,3 +14,10 @@ The extension is licensed under [AGPL-3.0](LICENSE.txt).
## Installation
See: https://docs.civicrm.org/sysadmin/en/latest/customize/extensions/#installing-a-new-extension
## Support and Maintenance
This extension is supported and maintained by:
[![MJW Consulting](docs/images/mjwconsulting.jpg)](https://www.mjwconsult.co.uk)
We offer paid [support and development](https://mjw.pt/support) as well as a [troubleshooting/investigation service](https://mjw.pt/investigation).
# Payment Shared library
This library is used by all payment processors by MJW Consulting and other extensions.
It provides multiple functions such as APIs, refund UI, shared code and a compatibility layer to support multiple versions of CiviCRM without requiring explicit support in the payment processor.
## Support and Maintenance
This extension is supported and maintained by:
[![MJW Consulting](images/mjwconsulting.jpg)](https://www.mjwconsult.co.uk)
We offer paid [support and development](https://mjw.pt/support) as well as a [troubleshooting/investigation service](https://mjw.pt/investigation).
# Refunds UI (experimental)
There is a refunds UI available for payments:
![Refund UI](images/refundui.png)
To access the refund click the "undo" icon by the payment:
![Refund icon](images/refundpaymenticon.png)
Currently it can be enabled via the setting `mjwshared_refundpaymentui` which is available via
*Administer->CiviContribute->Stripe Settings: Enable refund payment via UI (experimental)?*
It allows you to issue refunds for `Completed` payments if the payment processor supports it (eg. Stripe).
It also allows you to choose whether to cancel the event registration if there are any linked to the contribution (via line-items).
To access the refunds UI you must have "edit contributions" permission.
......@@ -9,6 +9,10 @@ Releases use the following numbering system:
* **[BC]**: Items marked with [BC] indicate a breaking change that will require updates to your code if you are using that code in your extension.
## Release 0.9.7 (not yet released 2020-10-26)
* Add support for issuing refunds via the payment UI for payment processors that support refunds (eg. Stripe).
## Release 0.9.6
* Fix [Stripe#271](https://lab.civicrm.org/extensions/stripe/-/issues/271) Can't submit credit card memberships: Uncaught (in promise) TypeError: this.form is null
......
......@@ -14,9 +14,9 @@
<url desc="Release Notes">https://lab.civicrm.org/extensions/mjwshared/-/blob/master/docs/releasenotes.md</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2020-10-24</releaseDate>
<version>0.9.6</version>
<develStage>stable</develStage>
<releaseDate>2020-10-26</releaseDate>
<version>0.9.7-dev</version>
<develStage>beta</develStage>
<compatibility>
<ver>5.28</ver>
</compatibility>
......
......@@ -197,5 +197,58 @@ function mjwshared_symfony_civicrm_buildAsset($event, $hook) {
$event->mimeType = $event->params['mimetype'];
}
}
}
/**
* Implements hook_civicrm_links
* Add links to membership list on contacts tab to view/setup direct debit
*
* @param $op
* @param $objectName
* @param $objectId
* @param $links
* @param $mask
* @param $values
*/
function mjwshared_civicrm_links($op, $objectName, $objectId, &$links, &$mask, &$values) {
if ($objectName === 'Payment' && $op === 'Payment.edit.action') {
if ((boolean)\Civi::settings()->get('mjwshared_refundpaymentui') === FALSE) {
return;
}
if (!CRM_Core_Permission::check('edit contributions')) {
return;
}
try {
$contribution = reset(civicrm_api3('Mjwpayment', 'get_contribution', [
'payment_id' => $values['id'],
'contribution_test' => ['IS NOT NULL' => 1],
])['values']);
// Don't allow refunds if contribution status is "Refunded"
if ((int)$contribution['contribution_status_id'] === CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Refunded')) {
return;
}
$payment = $contribution['payments'][$values['id']];
// Don't allow refunds if payment status is not "Completed"
if ((int)$payment['status_id'] !== CRM_Core_PseudoConstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed')) {
return;
}
$paymentProcessor = \Civi\Payment\System::singleton()
->getById($payment['payment_processor_id']);
if ($paymentProcessor->supportsRefund()) {
// Add the refund link to the payment
$links[] = [
'name' => 'Refund Payment',
'icon' => 'fa-undo',
'url' => 'civicrm/mjwpayment/refund',
'class' => 'medium-popup',
'qs' => 'reset=1&id=%%id%%&contribution_id=%%contribution_id%%',
'title' => 'Refund Payment',
];
}
}
catch (Exception $e) {
// Do nothing. We just don't add the "refund" link.
}
}
}
......@@ -18,4 +18,5 @@ markdown_extensions:
nav:
- Overview: index.md
- API: api.md
- Refund UI: refunds.md
- Release Notes: releasenotes.md
<?php
/*
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC. All rights reserved. |
| |
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/
use CRM_Stripe_ExtensionUtil as E;
return [
'mjwshared_refundpaymentui' => [
'name' => 'mjwshared_refundpaymentui',
'type' => 'Boolean',
'html_type' => 'checkbox',
'default' => 0,
'is_domain' => 1,
'is_contact' => 0,
'title' => E::ts('Enable refund payment via UI (experimental)?'),
'description' => E::ts('Enables a "Refund payment" option next to the edit payment option on Payments. Find payments by expanding contributions.'),
'html_attributes' => [],
'settings_pages' => [
'stripe' => [
'weight' => 50,
]
],
],
];
{crmScope extensionKey='mjwshared'}
<div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="top"}
</div>
<div class="help">{ts}Click "refund" to refund this payment{/ts}</div>
{if $participants}
<div class="description">{ts}Do you want to cancel the following event registrations when you refund the payment?{/ts}</div>
{foreach from=$participants item=participant}
<div class="crm-section">
<div class="label">Registration</div>
<div class="content">{$participant.event_title} - {$participant.participant_status}</div>
<div class="clear"></div>
</div>
{/foreach}
<div class="crm-section">
<div class="label">{$form.cancel_participants.label}</div>
<div class="content">{$form.cancel_participants.html}</div>
<div class="clear"></div>
</div>
{/if}
<div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="bottom"}
</div>
{/crmScope}
<?xml version="1.0"?>
<menu>
<item>
<path>civicrm/mjwpayment/refund</path>
<page_callback>CRM_Mjwshared_Form_PaymentRefund</page_callback>
<title>PaymentRefund</title>
<access_arguments>edit contributions</access_arguments>
</item>
</menu>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment