Skip to content

Instantly share code, notes, and snippets.

@clrockwell
Created September 21, 2017 13:43
Show Gist options
  • Save clrockwell/2df0553149d533aea2ccd51c9d9c8115 to your computer and use it in GitHub Desktop.
Save clrockwell/2df0553149d533aea2ccd51c9d9c8115 to your computer and use it in GitHub Desktop.
A sample endpoint for refunding a payment in Commerce 2.x
<?php
namespace Drupal\module\Plugin\rest\resource;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_payment\Entity\Payment;
use Drupal\commerce_payment\Exception\PaymentGatewayException;
use Drupal\commerce_payment\PaymentGatewayManager;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\PaymentGatewayBase;
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterface;
use Drupal\commerce_price\Price;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\scholarrx_api\OrderUtility;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
/**
* Provides a resource to get view modes by entity and bundle.
*
* @RestResource(
* id = "refund_payment",
* label = @Translation("Refund payment"),
* uri_paths = {
* "canonical" = "/api/orders/{order_id}/refunds",
* "https://www.drupal.org/link-relations/create" = "/api/orders/{order_id}/refunds"
* }
* )
*/
class RefundPayment extends ResourceBase {
/**
* A current user instance.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\commerce_payment\PaymentGatewayManager
*/
protected $paymentGatewayManager;
/**
* Constructs a new RefundPayment object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param array $serializer_formats
* The available serialization formats.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* A current user instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger,
AccountProxyInterface $current_user,
EntityTypeManagerInterface $entityTypeManager,
PaymentGatewayManager $paymentGatewayManager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->currentUser = $current_user;
$this->entityTypeManager = $entityTypeManager;
$this->paymentGatewayManager = $paymentGatewayManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('scholarrx_api'),
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('plugin.manager.commerce_payment_gateway')
);
}
/**
* Responds to GET requests.
*
* Returns a refunds for an order
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function get() {
throw new MethodNotAllowedHttpException('GET has not been implemented, please use the order endpoint to view any adjustments');
}
/**
* Responds to POST requests.
*
* @param Request $request
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Throws exception expected.
*/
public function post($order_id, $unknown = '', Request $request) {
$content = Json::decode($request->getContent());
$this->assertContent($content);
/** @var Price $refund_amount */
$refund_amount = new Price($content['amount'], 'USD');
$message = $content['message'];
/** @var Order $order */
$order = $this->entityTypeManager->getStorage('commerce_order')
->load($order_id);
$this->assertOrder($order);
$payments = $this->entityTypeManager->getStorage('commerce_payment')
->loadMultipleByOrder($order);
$this->assertPayments($payments);
$use = $this->getPaymentToUse($payments, $refund_amount);
$this->assertPayment($use);
$this->assertCardOnPayment($content['card'], $use);
/** @var SupportsRefundsInterface $gateway_plugin */
$gateway_plugin = $use->getPaymentGateway()->getPlugin();
$this->assertGatewayPlugin($gateway_plugin);
try {
$gateway_plugin->refundPayment($use, $refund_amount);
}
catch (PaymentGatewayException $exception) {
throw new PaymentGatewayException($exception->getMessage() . ' This is likely caused by a duplicate transaction: https://support.authorize.net/authkb/index?page=content&id=A425');
}
$this->logMessageToOrder($message, $order);
$payment = $this->entityTypeManager->getStorage('commerce_payment')->load($use->id());
return new ResourceResponse(OrderUtility::massagePayment($payment));
}
protected function getPaymentToUse($payments, Price $refund_amount) {
$use = FALSE;
/** @var Payment $payment */
foreach ($payments as $payment) {
/** @var Price $balance */
$balance = $payment->getBalance();
if ($refund_amount->greaterThan($balance)) {
continue;
}
else {
/** @var Payment $use */
$use = $payment;
}
}
return $use;
}
protected function logMessageToOrder($message, $order) {
$this->entityTypeManager->getStorage('commerce_log')->generate($order, 'api_refund', [
'message' => $message,
])->save();
}
protected function assertContent($content) {
$this->assertAmount($content);
$this->assertMessage($content);
$this->assertCard($content);
}
protected function assertAmount($content) {
if (!$content['amount']) {
throw new BadRequestHttpException('A refund amount must be specified.');
}
}
protected function assertMessage($content) {
if (!$content['message']) {
throw new BadRequestHttpException('A message for the refund must be specified.');
}
}
protected function assertCard($content) {
if (!$content['card']) {
throw new BadRequestHttpException('The last 4 digits of card must be specified.');
}
}
protected function assertOrder($order) {
if (!$order || !$order instanceof Order) {
throw new BadRequestHttpException('No order found');
}
}
protected function assertPayments($payments) {
if (empty($payments)) {
throw new BadRequestHttpException('There are no payments for this order');
}
}
protected function assertPayment($payment) {
if (!$payment || !$payment instanceof Payment) {
throw new BadRequestHttpException('There are no payments that are at least the amount of the refund request. Refunding an amount to multiple payments is not currently supported');
}
}
protected function assertCardOnPayment($card, Payment $payment) {
if ($card != $payment->getPaymentMethod()->get('card_number')->value) {
throw new AccessDeniedHttpException('The provided card does not match with an available payment method.');
}
}
protected function assertGatewayPlugin(PaymentGatewayBase $gateway) {
if (!$gateway instanceof SupportsRefundsInterface) {
throw new BadRequestHttpException('There is no refundPayment method on the gateway available');
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment