Skip to content

Commit 46a1e17

Browse files
committed
Move IPN Handling to a service
1 parent 45540f7 commit 46a1e17

File tree

8 files changed

+304
-152
lines changed

8 files changed

+304
-152
lines changed

commerce_paypal.services.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
commerce_paypal.logger:
3+
class: Drupal\Core\Logger\LoggerChannel
4+
factory: logger.factory:get
5+
arguments: ['commerce_paypal']
6+
commerce_paypal.ipn_handler:
7+
class: Drupal\commerce_paypal\IPNHandler
8+
arguments: ['@entity_type.manager', '@commerce_paypal.logger', '@http_client']

src/IPNHandler.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace Drupal\commerce_paypal;
4+
5+
use Drupal\Core\Entity\EntityTypeManagerInterface;
6+
use GuzzleHttp\ClientInterface;
7+
use Psr\Log\LoggerInterface;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
10+
11+
class IPNHandler implements IPNHandlerInterface {
12+
13+
/**
14+
* The entity type manager.
15+
*
16+
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
17+
*/
18+
protected $entityTypeManager;
19+
20+
/**
21+
* The logger.
22+
*
23+
* @var \Drupal\Core\Logger\LoggerChannelInterface
24+
*/
25+
protected $logger;
26+
27+
/**
28+
* The HTTP client.
29+
*
30+
* @var \GuzzleHttp\Client
31+
*/
32+
protected $httpClient;
33+
34+
/**
35+
* Constructs a new PaymentGatewayBase object.
36+
*
37+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
38+
* The entity type manager.
39+
* @param \Psr\Log\LoggerInterface $logger
40+
* The logger channel.
41+
* @param \GuzzleHttp\ClientInterface $client
42+
* The client.
43+
*/
44+
public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger, ClientInterface $client) {
45+
$this->entityTypeManager = $entity_type_manager;
46+
$this->logger = $logger;
47+
$this->httpClient = $client;
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function process(Request $request) {
54+
// Get IPN request data.
55+
$ipn_data = $this->getRequestDataArray($request->getContent());
56+
57+
// Exit now if the $_POST was empty.
58+
if (empty($ipn_data)) {
59+
$this->logger->warning('IPN URL accessed with no POST data submitted.');
60+
throw new BadRequestHttpException('IPN URL accessed with no POST data submitted.');
61+
}
62+
63+
// Make PayPal request for IPN validation.
64+
$url = $this->getIpnValidationUrl($ipn_data);
65+
$validate_ipn = 'cmd=_notify-validate&' . $request->getContent();
66+
$request = $this->httpClient->post($url, [
67+
'body' => $validate_ipn,
68+
])->getBody();
69+
$paypal_response = $this->getRequestDataArray($request->getContents());
70+
71+
// If the IPN was invalid, log a message and exit.
72+
if (isset($paypal_response['INVALID'])) {
73+
$this->logger->alert('Invalid IPN received and ignored.');
74+
throw new BadRequestHttpException('Invalid IPN received and ignored.');
75+
}
76+
77+
return $ipn_data;
78+
}
79+
80+
/**
81+
* Get data array from a request content.
82+
*
83+
* @param string $request_content
84+
* The Request content.
85+
*
86+
* @return array
87+
* The request data array.
88+
*/
89+
protected function getRequestDataArray($request_content) {
90+
parse_str(html_entity_decode($request_content), $ipn_data);
91+
return $ipn_data;
92+
}
93+
94+
/**
95+
* Gets the IPN URL to be used for validation for IPN data.
96+
*
97+
* @param array $ipn_data
98+
* The IPN request data from PayPal.
99+
*
100+
* @return string
101+
* The IPN validation URL.
102+
*/
103+
protected function getIpnValidationUrl(array $ipn_data) {
104+
if (!empty($ipn_data['test_ipn']) && $ipn_data['test_ipn'] == 1) {
105+
return 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
106+
}
107+
else {
108+
return 'https://ipnpb.paypal.com/cgi-bin/webscr';
109+
}
110+
}
111+
112+
}

src/IPNHandlerInterface.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Drupal\commerce_paypal;
4+
5+
use Symfony\Component\HttpFoundation\Request;
6+
7+
/**
8+
* Provides a handler for IPN requests from PayPal.
9+
*/
10+
interface IPNHandlerInterface {
11+
12+
/**
13+
* Processes an incoming IPN request.
14+
*
15+
* @param \Symfony\Component\HttpFoundation\Request $request
16+
* The request.
17+
*
18+
* @return mixed
19+
* The request data array.
20+
*
21+
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
22+
*/
23+
public function process(Request $request);
24+
25+
}

src/Plugin/Commerce/PaymentGateway/ExpressCheckout.php

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
namespace Drupal\commerce_paypal\Plugin\Commerce\PaymentGateway;
44

5+
use Drupal\commerce\TimeInterface;
56
use Drupal\commerce_order\Entity\OrderInterface;
67
use Drupal\commerce_payment\Entity\PaymentInterface;
78
use Drupal\commerce_payment\Exception\InvalidRequestException;
89
use Drupal\commerce_payment\Exception\PaymentGatewayException;
910
use Drupal\commerce_payment\PaymentMethodTypeManager;
1011
use Drupal\commerce_payment\PaymentTypeManager;
12+
use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayBase;
1113
use Drupal\commerce_price\Price;
1214
use Drupal\commerce_price\RounderInterface;
1315
use Drupal\Core\Entity\EntityTypeManagerInterface;
1416
use Drupal\Core\Form\FormStateInterface;
17+
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
1518
use GuzzleHttp\ClientInterface;
1619
use Symfony\Component\HttpFoundation\Request;
1720
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -32,7 +35,14 @@
3235
* },
3336
* )
3437
*/
35-
class ExpressCheckout extends PayPalIPNGatewayBase implements ExpressCheckoutInterface {
38+
class ExpressCheckout extends OffsitePaymentGatewayBase implements ExpressCheckoutInterface {
39+
40+
/**
41+
* The logger.
42+
*
43+
* @var \Drupal\Core\Logger\LoggerChannelInterface
44+
*/
45+
protected $logger;
3646

3747
/**
3848
* The HTTP client.
@@ -42,19 +52,49 @@ class ExpressCheckout extends PayPalIPNGatewayBase implements ExpressCheckoutInt
4252
protected $httpClient;
4353

4454
/**
45-
* The rounder.
55+
* The price rounder.
4656
*
4757
* @var \Drupal\commerce_price\RounderInterface
4858
*/
4959
protected $rounder;
5060

5161
/**
52-
* {@inheritdoc}
62+
* The time.
63+
*
64+
* @var \Drupal\commerce\TimeInterface
65+
*/
66+
protected $time;
67+
68+
/**
69+
* Constructs a new PaymentGatewayBase object.
70+
*
71+
* @param array $configuration
72+
* A configuration array containing information about the plugin instance.
73+
* @param string $plugin_id
74+
* The plugin_id for the plugin instance.
75+
* @param mixed $plugin_definition
76+
* The plugin implementation definition.
77+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
78+
* The entity type manager.
79+
* @param \Drupal\commerce_payment\PaymentTypeManager $payment_type_manager
80+
* The payment type manager.
81+
* @param \Drupal\commerce_payment\PaymentMethodTypeManager $payment_method_type_manager
82+
* The payment method type manager.
83+
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_channel_factory
84+
* The logger channel factory.
85+
* @param \GuzzleHttp\ClientInterface $client
86+
* The client.
87+
* @param \Drupal\commerce_price\RounderInterface $rounder
88+
* The price rounder.
89+
* @param \Drupal\commerce\TimeInterface $time
90+
* The time.
5391
*/
54-
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PaymentTypeManager $payment_type_manager, PaymentMethodTypeManager $payment_method_type_manager, ClientInterface $client, RounderInterface $rounder) {
92+
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, PaymentTypeManager $payment_type_manager, PaymentMethodTypeManager $payment_method_type_manager, LoggerChannelFactoryInterface $logger_channel_factory, ClientInterface $client, RounderInterface $rounder, TimeInterface $time) {
5593
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $payment_type_manager, $payment_method_type_manager);
94+
$this->logger = $logger_channel_factory->get('commerce_paypal');
5695
$this->httpClient = $client;
5796
$this->rounder = $rounder;
97+
$this->time = $time;
5898
}
5999

60100
/**
@@ -68,8 +108,10 @@ public static function create(ContainerInterface $container, array $configuratio
68108
$container->get('entity_type.manager'),
69109
$container->get('plugin.manager.commerce_payment_type'),
70110
$container->get('plugin.manager.commerce_payment_method_type'),
111+
$container->get('logger.factory'),
71112
$container->get('http_client'),
72-
$container->get('commerce_price.rounder')
113+
$container->get('commerce_price.rounder'),
114+
$container->get('commerce.time')
73115
);
74116
}
75117

@@ -195,13 +237,13 @@ public function onReturn(OrderInterface $order, Request $request) {
195237
$paypal_response = $this->doExpressCheckoutDetails($order);
196238

197239
// Nothing to do for failures for now - no payment saved.
198-
// ToDo - more about the failures.
240+
// @todo - more about the failures.
199241
if ($paypal_response['PAYMENTINFO_0_PAYMENTSTATUS'] == 'Failed') {
200242
throw new PaymentGatewayException($paypal_response['PAYMENTINFO_0_LONGMESSAGE'], $paypal_response['PAYMENTINFO_0_ERRORCODE']);
201243
}
202244

203245
$payment_storage = $this->entityTypeManager->getStorage('commerce_payment');
204-
$request_time = \Drupal::service('commerce.time')->getRequestTime();
246+
$request_time = $this->time->getRequestTime();
205247
$payment = $payment_storage->create([
206248
'state' => 'authorization',
207249
'amount' => $order->getTotalPrice(),
@@ -214,7 +256,7 @@ public function onReturn(OrderInterface $order, Request $request) {
214256
]);
215257

216258
// Process payment status received.
217-
// ToDo : payment updates if needed.
259+
// @todo payment updates if needed.
218260
// If we didn't get an approval response code...
219261
switch ($paypal_response['PAYMENTINFO_0_PAYMENTSTATUS']) {
220262
case 'Voided':
@@ -358,7 +400,7 @@ public function onNotify(Request $request) {
358400
// transaction IDs, indicating they are non-payment IPNs such as those used
359401
// for subscription signup requests.
360402
if (empty($ipn_data['txn_id'])) {
361-
\Drupal::logger('commerce_paypal')->alert('The IPN request does not have a transaction id. Ignored.');
403+
$this->logger->alert('The IPN request does not have a transaction id. Ignored.');
362404
return FALSE;
363405
}
364406
// Exit when we don't get a payment status we recognize.
@@ -372,7 +414,7 @@ public function onNotify(Request $request) {
372414
// If not, bail now because authorization transactions should be created
373415
// by the Express Checkout API request itself.
374416
if (!$payment) {
375-
\Drupal::logger('commerce_paypal')->warning('IPN for Order @order_number ignored: authorization transaction already created.', ['@order_number' => $ipn_data['invoice']]);
417+
$this->logger->warning('IPN for Order @order_number ignored: authorization transaction already created.', ['@order_number' => $ipn_data['invoice']]);
376418
return FALSE;
377419
}
378420
$amount = new Price($ipn_data['mc_gross'], $ipn_data['mc_currency']);
@@ -389,7 +431,7 @@ public function onNotify(Request $request) {
389431

390432
case 'Completed':
391433
$payment->state = 'capture_completed';
392-
$payment->setCapturedTime(REQUEST_TIME);
434+
$payment->setCapturedTime($this->time->getRequestTime());
393435
break;
394436
}
395437
// Update the remote id.
@@ -399,15 +441,14 @@ public function onNotify(Request $request) {
399441
// Get the corresponding parent transaction and refund it.
400442
$payment = $this->loadPaymentByRemoteId($ipn_data['parent_txn_id']);
401443
if (!$payment) {
402-
\Drupal::logger('commerce_paypal')->warning('IPN for Order @order_number ignored: the transaction to be refunded does not exist.', ['@order_number' => $ipn_data['invoice']]);
444+
$this->logger->warning('IPN for Order @order_number ignored: the transaction to be refunded does not exist.', ['@order_number' => $ipn_data['invoice']]);
403445
return FALSE;
404446
}
405447
elseif ($payment->getState() == 'capture_refunded') {
406-
\Drupal::logger('commerce_paypal')->warning('IPN for Order @order_number ignored: the transaction is already refunded.', ['@order_number' => $ipn_data['invoice']]);
448+
$this->logger->warning('IPN for Order @order_number ignored: the transaction is already refunded.', ['@order_number' => $ipn_data['invoice']]);
407449
return FALSE;
408450
}
409-
$amount_number = abs($ipn_data['mc_gross']);
410-
$amount = new Price((string) $amount_number, $ipn_data['mc_currency']);
451+
$amount = new Price((string) $ipn_data['mc_gross'], $ipn_data['mc_currency']);
411452
// Check if the Refund is partial or full.
412453
$old_refunded_amount = $payment->getRefundedAmount();
413454
$new_refunded_amount = $old_refunded_amount->add($amount);
@@ -425,7 +466,7 @@ public function onNotify(Request $request) {
425466
else {
426467
// In other circumstances, exit the processing, because we handle those
427468
// cases directly during API response processing.
428-
\Drupal::logger('commerce_paypal')->notice('IPN for Order @order_number ignored: this operation was accommodated in the direct API response.', ['@order_number' => $ipn_data['invoice']]);
469+
$this->logger->notice('IPN for Order @order_number ignored: this operation was accommodated in the direct API response.', ['@order_number' => $ipn_data['invoice']]);
429470
return FALSE;
430471
}
431472
if (isset($payment)) {
@@ -683,4 +724,23 @@ public function doRequest(array $nvp_data) {
683724
return $paypal_response;
684725
}
685726

727+
/**
728+
* Loads the payment for a given remote id.
729+
*
730+
* @param string $remote_id
731+
* The remote id property for a payment.
732+
*
733+
* @return \Drupal\commerce_payment\Entity\PaymentInterface
734+
* Payment object.
735+
*
736+
* @todo: to be replaced by Commerce core payment storage method
737+
* @see https://www.drupal.org/node/2856209
738+
*/
739+
protected function loadPaymentByRemoteId($remote_id) {
740+
/** @var \Drupal\commerce_payment\PaymentStorage $storage */
741+
$storage = $this->entityTypeManager->getStorage('commerce_payment');
742+
$payment_by_remote_id = $storage->loadByProperties(['remote_id' => $remote_id]);
743+
return reset($payment_by_remote_id);
744+
}
745+
686746
}

src/Plugin/Commerce/PaymentGateway/ExpressCheckoutInterface.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
/**
1111
* Provides the interface for the Express Checkout payment gateway.
1212
*/
13-
interface ExpressCheckoutInterface extends PayPalIPNGatewayBaseInterface, SupportsAuthorizationsInterface, SupportsRefundsInterface {
13+
interface ExpressCheckoutInterface extends SupportsAuthorizationsInterface, SupportsRefundsInterface {
1414

1515
/**
1616
* Gets the API URL.
@@ -35,12 +35,13 @@ public function doRequest(array $nvp_data);
3535

3636
/**
3737
* SetExpressCheckout API Operation (NVP) request.
38+
*
3839
* Builds the data for the request and make the request.
3940
*
4041
* @param \Drupal\commerce_payment\Entity\PaymentInterface $payment
4142
* The payment.
4243
* @param array $extra
43-
* Extra data needed for this request, ex.: cancel url, return url, transaction mode, etc....
44+
* Extra data needed for this request.
4445
*
4546
* @return array
4647
* PayPal response data.

0 commit comments

Comments
 (0)