Skip to content

Commit 276790e

Browse files
Richard van Oosterhoutclaude
andcommitted
Add supportsRefund/doRefund for gateways whose Omnipay driver implements refund()
Implements CiviCRM's refund contract (PaymentProcessor.refund API / supportsRefund capability) generically: supportsRefund() resolves the Omnipay gateway class without instantiating it and reports TRUE only when that class exposes a refund() method. doRefund() sends the refund via the gateway (transactionReference = original trxn_id) and returns refund_trxn_id / refund_status per the core contract. Failures throw PaymentProcessorException so CiviCRM never records a refund the gateway rejected. Currency defaults to the original transaction's currency. Tested against Mollie (iDeal/Bancontact) on CiviCRM 6.x. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c717bad commit 276790e

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

CRM/Core/Payment/OmnipayMultiProcessor.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,85 @@ public function doPayment(&$params, $component = 'contribute'): array {
271271
}
272272
}
273273

274+
/**
275+
* Does this processor support refunds?
276+
*
277+
* Generic for any Omnipay gateway that exposes a refund() method
278+
* (the tested target is Mollie). The gateway class is resolved without
279+
* instantiating it, so this is safe to call in listing contexts.
280+
*
281+
* @return bool
282+
*/
283+
public function supportsRefund() {
284+
try {
285+
$this->ensurePaymentProcessorTypeIsSet();
286+
$shortName = str_replace('omnipay_', '', $this->_paymentProcessor['payment_processor_type']);
287+
$gatewayClass = \Omnipay\Common\Helper::getGatewayClassName($shortName);
288+
return class_exists($gatewayClass) && method_exists($gatewayClass, 'refund');
289+
}
290+
catch (\Exception $e) {
291+
return FALSE;
292+
}
293+
}
294+
295+
/**
296+
* Submit a refund to the payment processor.
297+
*
298+
* Generic for any Omnipay gateway that exposes a refund() method; the
299+
* tested target is Mollie. On failure an exception is thrown (rather than
300+
* returning a failure array) so that CiviCRM never records a refund that
301+
* the gateway rejected.
302+
*
303+
* @param array $params
304+
* Expected keys: trxn_id (the gateway's original transaction reference),
305+
* amount, and optionally currency (resolved from the original financial
306+
* transaction when omitted).
307+
*
308+
* @return array
309+
* refund_trxn_id, refund_status ('Completed'), fee_amount, trxn_date.
310+
*
311+
* @throws \Civi\Payment\Exception\PaymentProcessorException
312+
*/
313+
public function doRefund(&$params) {
314+
if (empty($params['trxn_id']) || empty($params['amount'])) {
315+
throw new \Civi\Payment\Exception\PaymentProcessorException('doRefund requires trxn_id and amount');
316+
}
317+
$currency = $params['currency'] ?? NULL;
318+
if (empty($currency)) {
319+
// Look up the currency of the original transaction so the refund is
320+
// issued in the same currency; fall back to the default currency.
321+
$currency = CRM_Core_DAO::singleValueQuery(
322+
'SELECT currency FROM civicrm_financial_trxn WHERE trxn_id = %1',
323+
[1 => [$params['trxn_id'], 'String']]
324+
) ?: $this->getCurrency($params);
325+
}
326+
$this->ensurePaymentProcessorTypeIsSet();
327+
$this->createGatewayObject();
328+
$this->setProcessorFields();
329+
330+
try {
331+
$response = $this->gateway->refund([
332+
'transactionReference' => $params['trxn_id'],
333+
'amount' => \Civi::format()->machineMoney($params['amount'], $currency),
334+
'currency' => $currency,
335+
])->send();
336+
}
337+
catch (\Exception $e) {
338+
throw new \Civi\Payment\Exception\PaymentProcessorException('Refund failed: ' . $e->getMessage());
339+
}
340+
if (!$response->isSuccessful()) {
341+
throw new \Civi\Payment\Exception\PaymentProcessorException('Refund failed: ' . $response->getMessage());
342+
}
343+
return [
344+
// For Mollie getTransactionId() is the new refund id (re_...) while
345+
// getTransactionReference() remains the original payment id (tr_...).
346+
'refund_trxn_id' => $response->getTransactionId(),
347+
'refund_status' => 'Completed',
348+
'fee_amount' => 0,
349+
'trxn_date' => date('YmdHis'),
350+
];
351+
}
352+
274353
/**
275354
* Initialize class variables.
276355
*

0 commit comments

Comments
 (0)