Skip to content
Snippets Groups Projects
Commit 0888c2c2 authored by mattwire's avatar mattwire
Browse files

Support partial refunds

parent 11b91993
No related branches found
No related tags found
No related merge requests found
<?php
use Civi\Api4\LineItem;
use Civi\Api4\Participant;
use Civi\Api4\PaymentProcessor;
use Civi\Payment\Exception\PaymentProcessorException;
use CRM_Mjwshared_ExtensionUtil as E;
use Brick\Money\Money;
use Brick\Money\Context\DefaultContext;
use Brick\Math\RoundingMode;
/**
* Form controller class
......@@ -20,47 +27,88 @@ class CRM_Mjwshared_Form_PaymentRefund extends CRM_Core_Form {
*/
private $contributionID;
/**
* @var array $financialTrxn
*/
private $financialTrxn;
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->addFormRule(['CRM_Mjwshared_Form_PaymentRefund', 'formRule'], $this);
$this->setTitle('Refund payment');
$this->paymentID = CRM_Utils_Request::retrieveValue('id', 'Positive', NULL, FALSE, 'GET');
$this->paymentID = CRM_Utils_Request::retrieveValue('payment_id', 'Positive', NULL, FALSE, 'REQUEST');
if (!$this->paymentID) {
CRM_Core_Error::statusBounce('Payment not found!');
}
$this->contributionID = CRM_Utils_Request::retrieveValue('contribution_id', 'Positive', NULL, FALSE, 'GET');
$this->contributionID = CRM_Utils_Request::retrieveValue('contribution_id', 'Positive', NULL, FALSE, 'REQUEST');
if (!$this->contributionID) {
CRM_Core_Error::statusBounce('Contribution not found!');
}
$financialTrxn = reset(civicrm_api3('Mjwpayment', 'get_payment', [
'financial_trxn_id' => $this->paymentID,
])['values']);
if ((int)$financialTrxn['contribution_id'] !== $this->contributionID) {
CRM_Core_Error::statusBounce('Contribution / Payment does not match');
}
$financialTrxn['order_reference'] = $financialTrxn['order_reference'] ?? NULL;
$paymentProcessor = PaymentProcessor::get(FALSE)
->addWhere('id', '=', $financialTrxn['payment_processor_id'])
->execute()
->first();
$financialTrxn['payment_processor_title'] = $paymentProcessor['title'] ?? $paymentProcessor['name'];
$this->assign('paymentInfo', $financialTrxn);
$this->financialTrxn = $financialTrxn;
$this->add('hidden', 'payment_id');
$this->add('hidden', 'contribution_id');
$participantIDs = $membershipIDs = [];
$lineItems = civicrm_api3('LineItem', 'get', [
'contribution_id' => $this->contributionID,
])['values'];
foreach ($lineItems as $lineItemID => $lineItemDetails) {
$lineItems = LineItem::get(FALSE)
->addWhere('contribution_id', '=', $this->contributionID)
->execute();
foreach ($lineItems as $lineItemDetails) {
switch ($lineItemDetails['entity_table']) {
case 'civicrm_participant':
$participantIDs[] = $lineItemDetails['entity_id'];
break;
case 'civicrm_membership':
$membershipIDs[] = $lineItemDetails['entity_id'];
}
}
if (!empty($participantIDs)) {
$participantsForAssign = [];
$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);
$participants = Participant::get()
->addSelect('*', 'event_id.title', 'status_id:label', 'contact_id.display_name')
->addWhere('id', 'IN', $participantIDs)
->execute();
foreach ($participants->getArrayCopy() as $participant) {
$participant['status'] = $participant['status_id:label'];
$participant['event_title'] = $participant['event_id.title'];
$participant['display_name'] = $participant['contact_id.display_name'];
$participantsForAssign[] = $participant;
}
$this->addYesNo('cancel_participants', E::ts('Do you want to cancel these registrations when you refund the payment?'), NULL, TRUE);
}
$this->assign('participants', $participantsForAssign ?? NULL);
$this->addMoney('refund_amount',
ts('Refund Amount'),
TRUE,
[],
TRUE, 'currency', NULL, TRUE
);
$this->addButtons([
[
......@@ -79,25 +127,70 @@ class CRM_Mjwshared_Form_PaymentRefund extends CRM_Core_Form {
if ($this->paymentID) {
$this->_defaults['payment_id'] = $this->paymentID;
$this->set('payment_id', $this->paymentID);
$this->_defaults['contribution_id'] = $this->contributionID;
$this->set('contribution_id', $this->contributionID);
$this->_defaults['refund_amount'] = $this->financialTrxn['total_amount'];
}
return $this->_defaults;
}
/**
* Global form rule.
*
* @param array $fields
* The input form values.
* @param array $files
* The uploaded files if any.
* @param CRM_Core_Form $form
*
* @return bool|array
* true if no errors, else array of errors
*/
public static function formRule($fields, $files, $form) {
$errors = [];
$formValues = $form->getSubmitValues();
$paymentID = $form->get('payment_id');
$payment = reset(civicrm_api3('Mjwpayment', 'get_payment', ['id' => $paymentID])['values']);
// Check refund amount
$refundAmount = Money::of($formValues['refund_amount'], $payment['currency'], new DefaultContext(), RoundingMode::CEILING);
$paymentAmount = Money::of($payment['total_amount'], $payment['currency'], new DefaultContext(), RoundingMode::CEILING);
if ($refundAmount->isGreaterThan($paymentAmount)) {
$errors['refund_amount'] = 'Cannot refund more than the original amount';
}
if ($refundAmount->isNegativeOrZero()) {
$errors['refund_amount'] = 'Cannot refund zero or negative amount';
}
return $errors;
}
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');
}
$cancelParticipants = $formValues['cancel_participants'] ?? FALSE;
try {
$payment = reset(civicrm_api3('Mjwpayment', 'get_payment', ['id' => $paymentID])['values']);
// Check refund amount
$refundAmount = Money::of($formValues['refund_amount'], $payment['currency'], new DefaultContext(), RoundingMode::CEILING);
$paymentAmount = Money::of($payment['total_amount'], $payment['currency'], new DefaultContext(), RoundingMode::CEILING);
if ($refundAmount->isGreaterThan($paymentAmount)) {
throw new PaymentProcessorException('Cannot refund more than the original amount');
}
if ($refundAmount->isNegativeOrZero()) {
throw new PaymentProcessorException('Cannot refund zero or negative amount');
}
$refundParams = [
'payment_processor_id' => $payment['payment_processor_id'],
'amount' => $payment['total_amount'],
'amount' => $refundAmount->getAmount()->toFloat(),
'currency' => $payment['currency'],
'trxn_id' => $payment['trxn_id'],
];
......@@ -106,8 +199,8 @@ class CRM_Mjwshared_Form_PaymentRefund extends CRM_Core_Form {
$refundPaymentParams = [
'contribution_id' => $payment['contribution_id'],
'trxn_id' => $refund['refund_trxn_id'],
'order_reference' => $payment['order_reference'],
'total_amount' => 0 - abs($payment['total_amount']),
'order_reference' => $payment['order_reference'] ?? NULL,
'total_amount' => 0 - abs($refundAmount->getAmount()->toFloat()),
'fee_amount' => 0 - abs($refund['fee_amount']),
'payment_processor_id' => $payment['payment_processor_id'],
];
......@@ -128,7 +221,7 @@ class CRM_Mjwshared_Form_PaymentRefund extends CRM_Core_Form {
$lock->release();
$message = E::ts('Refund was processed successfully.');
if ($formValues['cancel_participants'] && !empty($participantIDs)) {
if ($cancelParticipants && !empty($participantIDs)) {
foreach ($participantIDs as $participantID) {
civicrm_api3('Participant', 'create', [
'id' => $participantID,
......
......@@ -188,6 +188,27 @@ function mjwshared_symfony_civicrm_buildAsset($event, $hook) {
* @param $values
*/
function mjwshared_civicrm_links($op, $objectName, $objectId, &$links, &$mask, &$values) {
if ($objectName === 'Contribution' && $op === 'Contribution.edit.action') {
$bob =1;
foreach ($links as &$link) {
switch ($link['title']) {
case 'Record Refund':
$link['title'] = 'Add refund';
break;
case 'Record Payment':
$link['title'] = 'Add payment';
break;
case 'Submit Credit Card payment':
$link['title'] = 'Add payment using payment processor';
break;
}
if ($link['title'] === 'Record Refund') {
$link['title'] = 'Add refund';
}
}
}
if ($objectName === 'Payment' && $op === 'Payment.edit.action') {
if ((boolean)\Civi::settings()->get('mjwshared_refundpaymentui') === FALSE) {
return;
......@@ -226,7 +247,7 @@ function mjwshared_civicrm_links($op, $objectName, $objectId, &$links, &$mask, &
'icon' => 'fa-undo',
'url' => 'civicrm/mjwpayment/refund',
'class' => 'medium-popup',
'qs' => 'reset=1&id=%%id%%&contribution_id=%%contribution_id%%',
'qs' => 'reset=1&payment_id=%%id%%&contribution_id=%%contribution_id%%',
'title' => 'Refund Payment',
];
}
......
{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>
<h3>Payment details</h3>
<div class="crm-section crm-mjwshared-paymentrefund-paymentinfo">
<div class="label">{ts}Amount{/ts}</div><div class="content">{$paymentInfo.total_amount|crmMoney:$paymentInfo.currency}</div>
<div class="label">{ts}Payment date{/ts}</div><div class="content">{$paymentInfo.trxn_date|crmDate}</div>
{if $paymentInfo.trxn_id}<div class="label">{ts}Transaction ID{/ts}</div><div class="content">{$paymentInfo.trxn_id}</div>{/if}
{if $paymentInfo.order_reference}<div class="label">{ts}Order Reference{/ts}</div><div class="content">{$paymentInfo.order_reference}</div>{/if}
{if $paymentInfo.payment_processor_title}<div class="label">{ts}Payment Processor{/ts}</div><div class="content">{$paymentInfo.payment_processor_title}</div>{/if}
</div>
{if $participants}
<div class="description">{ts}Do you want to cancel the following event registrations when you refund the payment?{/ts}</div>
<h3>{ts}This payment was used to register the following participants:{/ts}</h3>
<div class="crm-section crm-mjwshared-paymentrefund-participants">
<br />
<ul>
{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>
<li>{$participant.display_name}: {$participant.event_title} (<em>{$participant.status}</em>)</li>
{/foreach}
<div class="crm-section">
</ul>
</div>
<br />
<div class="crm-section crm-mjwshared-paymentrefund-canceloption">
<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-section crm-mjwshared-paymentrefund-canceloption">
<div class="label">{$form.refund_amount.label}</div>
<div class="content">
<span id='totalAmount'>{$form.currency.html|crmAddClass:eight}&nbsp;{$form.refund_amount.html|crmAddClass:eight}</span>
</div>
<div class="clear"></div>
</div>
<div class="help">{ts}Click "refund" to refund this payment{/ts}</div>
<div class="crm-submit-buttons">
{include file="CRM/common/formButtons.tpl" location="bottom"}
</div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment