Skip to content

Instantly share code, notes, and snippets.

@senadir
Created September 24, 2023 13:52
Show Gist options
  • Save senadir/711d4a973cf64e288c2da1ba9a69e8ed to your computer and use it in GitHub Desktop.
Save senadir/711d4a973cf64e288c2da1ba9a69e8ed to your computer and use it in GitHub Desktop.
<?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