-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Extract DB access from Stripe API client class This logic is required by other parts of the code base, so extracting it out allows it to be shared. The unit tests have been left as is for now, but this also allows for database access to be mocked more easily. * Add REST endpoint for handling webhooks from WP.com The initial implementation just handles refund updated events and adds an order note in the event of a failure. * Turn off Squiz.Commenting.FunctionCommentThrowTag.WrongNumber PHPCS rule This rule is generating some false positives. We can try turning it on again if it gets fixed in a later PHPCS version. Alternatively we could start relying on a static code analyser for more robust checks on how we're using exceptions.
- Loading branch information
Showing
9 changed files
with
532 additions
and
40 deletions.
There are no files selected for viewing
166 changes: 166 additions & 0 deletions
166
includes/admin/class-wc-rest-payments-webhook-controller.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
<?php | ||
/** | ||
* Class WC_REST_Payments_Webhook_Controller | ||
* | ||
* @package WooCommerce\Payments\Admin | ||
*/ | ||
|
||
use WCPay\Exceptions\WC_Payments_Rest_Request_Exception; | ||
use WCPay\Logger; | ||
|
||
defined( 'ABSPATH' ) || exit; | ||
|
||
/** | ||
* REST controller for webhooks. | ||
*/ | ||
class WC_REST_Payments_Webhook_Controller extends WC_Payments_REST_Controller { | ||
|
||
/** | ||
* Result codes for returning to the WCPay server API. They don't have any special meaning, but can will be logged | ||
* and are therefore useful when debugging how we reacted to a webhook. | ||
*/ | ||
const RESULT_SUCCESS = 'success'; | ||
const RESULT_BAD_REQUEST = 'bad_request'; | ||
const RESULT_ERROR = 'error'; | ||
|
||
/** | ||
* Endpoint path. | ||
* | ||
* @var string | ||
*/ | ||
protected $rest_base = 'payments/webhook'; | ||
|
||
/** | ||
* DB wrapper. | ||
* | ||
* @var WC_Payments_DB | ||
*/ | ||
private $wcpay_db; | ||
|
||
/** | ||
* WC_REST_Payments_Webhook_Controller constructor. | ||
* | ||
* @param WC_Payments_API_Client $api_client WC_Payments_API_Client instance. | ||
* @param WC_Payments_DB $wcpay_db WC_Payments_DB instance. | ||
*/ | ||
public function __construct( WC_Payments_API_Client $api_client, WC_Payments_DB $wcpay_db ) { | ||
parent::__construct( $api_client ); | ||
$this->wcpay_db = $wcpay_db; | ||
} | ||
|
||
/** | ||
* Configure REST API routes. | ||
*/ | ||
public function register_routes() { | ||
register_rest_route( | ||
$this->namespace, | ||
'/' . $this->rest_base, | ||
array( | ||
'methods' => WP_REST_Server::CREATABLE, | ||
'callback' => array( $this, 'handle_webhook' ), | ||
'permission_callback' => array( $this, 'check_permission' ), | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* Retrieve transactions to respond with via API. | ||
* | ||
* @param WP_REST_Request $request Full data about the request. | ||
* | ||
* @return WP_REST_Response | ||
*/ | ||
public function handle_webhook( $request ) { | ||
$body = $request->get_json_params(); | ||
|
||
try { | ||
// Extract information about the webhook event. | ||
$event_type = $this->read_rest_property( $body, 'type' ); | ||
$event_data = $this->read_rest_property( $body, 'data' ); | ||
$event_object = $this->read_rest_property( $event_data, 'object' ); | ||
|
||
switch ( $event_type ) { | ||
case 'charge.refund.updated': | ||
$this->process_webhook_refund_updated( $event_object ); | ||
break; | ||
} | ||
} catch ( WC_Payments_Rest_Request_Exception $e ) { | ||
Logger::error( $e ); | ||
return new WP_REST_Response( array( 'result' => self::RESULT_BAD_REQUEST ), 400 ); | ||
} catch ( Exception $e ) { | ||
Logger::error( $e ); | ||
return new WP_REST_Response( array( 'result' => self::RESULT_ERROR ), 500 ); | ||
} | ||
|
||
return new WP_REST_Response( array( 'result' => self::RESULT_SUCCESS ) ); | ||
} | ||
|
||
/** | ||
* Process webhook refund updated. | ||
* | ||
* @param array $event_object The event that triggered the webhook. | ||
* | ||
* @throws WC_Payments_Rest_Request_Exception Required parameters not found. | ||
* @throws Exception Unable to resolve charge ID to order. | ||
*/ | ||
private function process_webhook_refund_updated( $event_object ) { | ||
// First, check the reason for the update. We're only interesting in a status of failed. | ||
$status = $this->read_rest_property( $event_object, 'status' ); | ||
if ( 'failed' !== $status ) { | ||
return; | ||
} | ||
|
||
// Fetch the details of the failed refund so that we can find the associated order and write a note. | ||
$charge_id = $this->read_rest_property( $event_object, 'charge' ); | ||
$refund_id = $this->read_rest_property( $event_object, 'id' ); | ||
$amount = $this->read_rest_property( $event_object, 'amount' ); | ||
|
||
// Look up the order related to this charge. | ||
$order = $this->wcpay_db->order_from_charge_id( $charge_id ); | ||
if ( ! $order ) { | ||
throw new Exception( | ||
sprintf( | ||
/* translators: %1: charge ID */ | ||
__( 'Could not find order via charge ID: %1$s', 'woocommerce-payments' ), | ||
$charge_id | ||
) | ||
); | ||
} | ||
|
||
$note = sprintf( | ||
WC_Payments_Utils::esc_interpolated_html( | ||
/* translators: %1: the refund amount, %2: ID of the refund */ | ||
__( 'A refund of %1$s was <strong>unsuccessful</strong> using WooCommerce Payments (<code>%2$s</code>).', 'woocommerce-payments' ), | ||
array( | ||
'strong' => '<strong>', | ||
'code' => '<code>', | ||
) | ||
), | ||
wc_price( $amount / 100 ), | ||
$refund_id | ||
); | ||
$order->add_order_note( $note ); | ||
} | ||
|
||
/** | ||
* Safely get a value from the REST request body array. | ||
* | ||
* @param array $array Array to read from. | ||
* @param string $key ID to fetch on. | ||
* | ||
* @return string|array | ||
* @throws WC_Payments_Rest_Request_Exception Thrown if ID not set. | ||
*/ | ||
private function read_rest_property( $array, $key ) { | ||
if ( ! isset( $array[ $key ] ) ) { | ||
throw new WC_Payments_Rest_Request_Exception( | ||
sprintf( | ||
/* translators: %1: ID being fetched */ | ||
__( '%1$s not found in array', 'woocommerce-payments' ), | ||
$key | ||
) | ||
); | ||
} | ||
return $array[ $key ]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<?php | ||
/** | ||
* WC_Payments_DB class | ||
* | ||
* @package WooCommerce\Payments | ||
*/ | ||
|
||
defined( 'ABSPATH' ) || exit; | ||
|
||
/** | ||
* Wrapper class for accessing the database. | ||
*/ | ||
class WC_Payments_DB { | ||
/** | ||
* Retrieve an order from the DB using a corresponding Stripe charge ID. | ||
* | ||
* @param string $charge_id Charge ID corresponding to an order ID. | ||
* | ||
* @return boolean|WC_Order|WC_Order_Refund | ||
*/ | ||
public function order_from_charge_id( $charge_id ) { | ||
$order_id = $this->order_id_from_charge_id( $charge_id ); | ||
|
||
if ( $order_id ) { | ||
return wc_get_order( $order_id ); | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Retrieve an order ID from the DB using a corresponding Stripe charge ID. | ||
* | ||
* @param string $charge_id Charge ID corresponding to an order ID. | ||
* | ||
* @return null|string | ||
*/ | ||
private function order_id_from_charge_id( $charge_id ) { | ||
global $wpdb; | ||
|
||
// The order ID is saved to DB in `WC_Payment_Gateway_WCPay::process_payment()`. | ||
$order_id = $wpdb->get_var( | ||
$wpdb->prepare( | ||
"SELECT DISTINCT ID FROM $wpdb->posts as posts LEFT JOIN $wpdb->postmeta as meta ON posts.ID = meta.post_id WHERE meta.meta_value = %s AND meta.meta_key = '_charge_id'", | ||
$charge_id | ||
) | ||
); | ||
return $order_id; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
includes/exceptions/class-wc-payments-rest-request-exception.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
/** | ||
* Class WC_Payments_Rest_Exception | ||
* | ||
* @package WooCommerce\Payments | ||
*/ | ||
|
||
namespace WCPay\Exceptions; | ||
|
||
use Exception; | ||
|
||
defined( 'ABSPATH' ) || exit; | ||
|
||
/** | ||
* Exception for throwing errors in REST API controllers (e.g. issues with missing parameters in requests). | ||
*/ | ||
class WC_Payments_Rest_Request_Exception extends Exception {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.