Created
September 24, 2023 13:52
-
-
Save senadir/711d4a973cf64e288c2da1ba9a69e8ed to your computer and use it in GitHub Desktop.
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
<?php | |
/** | |
* A class to add support for webhooks endpoints. | |
*/ | |
class Yalidine_Shipping_Webhooks { | |
/** | |
* The processing statuses. | |
* | |
* @var array | |
*/ | |
const processing_statuses = array( | |
'Pas encore expédié', | |
'A vérifier', | |
'En préparation', | |
'Pas encore ramassé', | |
'Prêt à expédier', | |
); | |
/** | |
* The shipping statuses. | |
* | |
* @var array | |
*/ | |
const shipping_statuses = array( | |
'Ramassé', | |
'Transfert', | |
'Expédié', | |
'Centre', | |
'Vers Wilaya', | |
'Reçu à Wilaya', | |
'En attente du client', | |
'Sorti en livraison', | |
'En attente', | |
'En alerte', | |
'Alerte résolue', | |
'Tentative échouée', | |
'Echèc livraison', | |
); | |
/** | |
* The completed statuses. | |
* | |
* @var array | |
*/ | |
const completed_statuses = array( | |
'Livré', | |
); | |
/** | |
* The failed statuses. | |
* | |
* @var array | |
*/ | |
const failed_statuses = array( | |
'Retour vers centre', | |
'Retourné au centre', | |
'Retour transfert', | |
'Retour groupé', | |
'Retour à retirer', | |
'Retour vers vendeur', | |
'Retourné au vendeur', | |
'Echange échoué', | |
); | |
public function __construct() { | |
// Register the webhook endpoint. | |
add_action( 'rest_api_init', array( $this, 'register_webhook_endpoint' ) ); | |
// Add a new field for webhook secret in Yalidine shipping settings. | |
add_filter( 'woocommerce_settings_api_form_fields_yalidine-shipping-home', array( $this, 'add_webhook_secret_field' ) ); | |
// Hook to process order updates after being scheduled. | |
add_action( 'yalidine_update_order_status', array( $this, 'update_order_status' ), 10, 4 ); | |
} | |
/** | |
* Register the webhook endpoint. | |
*/ | |
public function register_webhook_endpoint() { | |
// This is where we're going to receive the webhook payload. | |
register_rest_route( | |
'yalidine/v1/webhook', | |
'/parcel/update', | |
array( | |
'methods' => 'POST', | |
'callback' => array( $this, 'handle_update_parcel' ), | |
'permission_callback' => array( $this, 'verify_webhook' ), | |
'args' => array( | |
'type' => array( | |
'required' => true, | |
'type' => 'string', | |
), | |
'events' => array( | |
'required' => true, | |
'type' => 'array', | |
'items' => array( | |
'type' => 'object', | |
'properties' => array( | |
'event_id' => array( | |
'type' => 'string', | |
'required' => true, | |
), | |
'occurred_at' => array( | |
'type' => 'string', | |
), | |
'data' => array( | |
'type' => 'object', | |
'properties' => array( | |
'tracking' => array( | |
'type' => 'string', | |
'required' => true, | |
), | |
'status' => array( | |
'type' => 'string', | |
'required' => true, | |
), | |
'reason' => array( | |
'type' => array( 'string', 'null' ), | |
), | |
), | |
), | |
), | |
), | |
), | |
), | |
) | |
); | |
// This is mean for CRC validation only. | |
register_rest_route( | |
'yalidine/v1/webhook', | |
'/parcel/update/', | |
array( | |
'methods' => 'GET', | |
'callback' => array( $this, 'handle_crc' ), | |
'permission_callback' => '__return_true', | |
'args' => array( | |
'crc_token' => array( | |
'required' => true, | |
'type' => 'string', | |
), | |
'subscribe' => array( | |
'required' => false, | |
), | |
), | |
) | |
); | |
} | |
/** | |
* Handle the webhook request. | |
* | |
* @param \WP_REST_Request $request The request object. | |
* | |
* @return \WP_REST_Response 200 OK unless something has gone wrong. | |
*/ | |
public function handle_update_parcel( \WP_REST_Request $request ) { | |
// We only handle this for now. | |
if ( $request['type'] === 'parcel_status_updated' ) { | |
$parcels = $request['events']; | |
foreach ( $parcels as $parcel ) { | |
$this->handle_single_parcel( $parcel ); | |
} | |
} | |
return new \WP_REST_Response( | |
array( | |
'success' => true, | |
), | |
200 | |
); | |
} | |
/** | |
* Handle a single parcel. | |
* | |
* @param array $parcel The parcel data. | |
*/ | |
private function handle_single_parcel( array $parcel ) { | |
$event_id = $parcel['event_id']; | |
$tracking_number = $parcel['data']['tracking']; | |
$new_status = $parcel['data']['status']; | |
$reason = $parcel['data']['reason']; | |
// Unique prop should handle the case where we receive the same event twice, but we still check. | |
if ( as_has_scheduled_action( | |
'yalidine_update_order_status', | |
array( | |
'event_id' => $event_id, | |
'tracking_number' => $tracking_number, | |
'new_status' => $new_status, | |
'reason' => $reason, | |
), | |
'yalidine', | |
true | |
) ) { | |
return; | |
} | |
// Schedule the action. | |
as_enqueue_async_action( | |
'yalidine_update_order_status', | |
array( | |
'event_id' => $event_id, | |
'tracking_number' => $tracking_number, | |
'new_status' => $new_status, | |
'reason' => $reason, | |
), | |
'yalidine' | |
); | |
} | |
/** | |
* Update the order status. | |
* | |
* @param string $event_id The event ID. | |
* @param string $tracking_number The tracking number. | |
* @param string $new_yalidine_status The new yalidine status. | |
* @param string $reason The reason for the status change (if any) | |
*/ | |
public function update_order_status( $event_id, $tracking_number, $new_yalidine_status, $reason ) { | |
$orders = wc_get_orders( | |
array( | |
'meta_key' => 'yalidine_tracking_number', | |
'meta_value' => $tracking_number, | |
'meta_compare' => '=', | |
'limit' => 1, | |
) | |
); | |
if ( ! $orders ) { | |
return; | |
} | |
$order = $orders[0]; | |
// We map the yalidine status to a WC status. | |
$new_status = $this->get_wc_status_from_yalidine_status( $new_yalidine_status ); | |
// If the order is already in the correct status, we just add a note if we have a reason. | |
if ( $new_status === $order->get_status() ) { | |
if ( $reason ) { | |
$order->add_order_note( | |
sprintf( | |
/* translators: %s: reason */ | |
__( 'Yalidine status at risk: %s', 'yalidine-shipping' ), | |
$reason | |
) | |
); | |
} | |
return; | |
} | |
// Otherwise we update the status. | |
$message = $reason ? sprintf( __( 'Via yalidine.\nReason: %2$s', 'yalidine-shipping' ), $new_status, $reason ) : sprintf( __( 'Via yalidine.', 'yalidine-shipping' ), $new_status ); | |
$order->update_status( $new_status, $message ); | |
$order->save(); | |
return; | |
} | |
/** | |
* Verify the webhook request. | |
* | |
* @param \WP_REST_Request $request The request object. | |
* | |
* @return \WP_REST_Response|true 401 if no signature provided, or validation failed, otherwise true. | |
*/ | |
public function verify_webhook( \WP_REST_Request $request ) { | |
$signature = $request->get_header( 'HTTP_X_YALIDINE_SIGNATURE' ) || $request->get_header( 'X_YALIDINE_SIGNATURE' ); | |
if ( ! $signature ) { | |
return new \WP_REST_Response( | |
array( | |
'success' => false, | |
'error' => 'No signature provided', | |
), | |
401 | |
); | |
} | |
$secret = get_option( 'woocommerce_yalidine-shipping-home_webhook_secret' ); | |
if ( ! $secret ) { | |
return new \WP_REST_Response( | |
array( | |
'success' => false, | |
'error' => 'No webhook secret set', | |
), | |
401 | |
); | |
} | |
$payload = $request->get_body(); | |
$computed_signature = hash_hmac( 'sha256', $payload, $secret ); | |
if ( $signature !== $computed_signature ) { | |
return new \WP_REST_Response( | |
array( | |
'success' => false, | |
'error' => 'Invalid signature', | |
), | |
401 | |
); | |
} | |
return true; | |
} | |
/** | |
* Add a new field for webhook secret in Yalidine shipping settings. | |
* | |
* @param array $fields The fields. | |
* @return array The fields. | |
*/ | |
public function add_webhook_secret_field( $fields ) { | |
$fields['webhook_secret'] = array( | |
'title' => __( 'Webhook Secret', 'yalidine-shipping' ), | |
'type' => 'text', | |
'description' => __( 'The webhook secret is used to verify that webhook requests are coming from Yalidine.', 'yalidine-shipping' ), | |
'desc_tip' => true, | |
); | |
return $fields; | |
} | |
/** | |
* Handle CRC validation. | |
* | |
* @param \WP_REST_Request $request The request object. | |
* | |
* @return void | |
*/ | |
public function handle_crc( \WP_REST_Request $request ) { | |
if ( isset( $request['subscribe'] ) && isset( $request['crc_token'] ) ) { | |
header( 'Content-Type: text/plain' ); | |
// We sadly have to echo because WP_REST_Response return a parsed string and we need the raw one. | |
echo $request['crc_token']; | |
exit; | |
} | |
} | |
/** | |
* Get the WC status from the Yalidine status. | |
* | |
* @param string $yalidine_status The Yalidine status. | |
* | |
* @return string The WC status. | |
*/ | |
private function get_wc_status_from_yalidine_status( $yalidine_status ) { | |
switch ( $yalidine_status ) { | |
case in_array( $yalidine_status, self::processing_statuses ): | |
return 'processing'; | |
case in_array( $yalidine_status, self::shipping_statuses ): | |
return 'yalidine-shipping'; | |
case in_array( $yalidine_status, self::completed_statuses ): | |
return 'completed'; | |
case in_array( $yalidine_status, self::failed_statuses ): | |
return 'failed'; | |
default: | |
return 'processing'; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment