309 lines
11 KiB
PHP
309 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ESI_PEPPOL\controllers;
|
|
|
|
use ESI_PEPPOL\models\PEPPOL_Main_model;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
|
|
/**
|
|
* Contrôleur pour gérer la réception des webhooks depuis l'API ESI Peppol.
|
|
*
|
|
* Les webhooks permettent de recevoir des notifications automatiques
|
|
* lorsque le statut d'un document change.
|
|
*/
|
|
class PEPPOL_Webhook_controller {
|
|
|
|
/**
|
|
* Enregistre les routes REST API pour les webhooks.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function register_routes(): void {
|
|
register_rest_route(
|
|
'esi-peppol/v1',
|
|
'/webhook',
|
|
[
|
|
'methods' => 'POST',
|
|
'callback' => [self::class, 'handle_webhook'],
|
|
'permission_callback' => [self::class, 'permission_check'],
|
|
'args' => [
|
|
'event' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
'validate_callback' => [self::class, 'validate_event'],
|
|
],
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Vérifie les permissions pour accéder à l'endpoint webhook.
|
|
*
|
|
* Les webhooks sont authentifiés via les headers personnalisés
|
|
* X-ESIPeppol-Event, X-ESIPeppol-Document-ID, X-ESIPeppol-Company-ID.
|
|
*
|
|
* @param WP_REST_Request $request Requête REST.
|
|
* @return bool|WP_Error
|
|
*/
|
|
public static function permission_check(WP_REST_Request $request) {
|
|
// Vérifier la présence des headers requis
|
|
$event_header = $request->get_header('X-ESIPeppol-Event');
|
|
$document_id_header = $request->get_header('X-ESIPeppol-Document-ID');
|
|
$company_id_header = $request->get_header('X-ESIPeppol-Company-ID');
|
|
|
|
if (empty($event_header) || empty($document_id_header) || empty($company_id_header)) {
|
|
return new WP_Error(
|
|
'missing_headers',
|
|
__('Headers webhook manquants.', 'esi_peppol'),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
// Optionnel : vérifier que le Company-ID correspond à celui configuré
|
|
// Pour l'instant, on accepte tous les webhooks avec les headers requis
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Valide le type d'événement webhook.
|
|
*
|
|
* @param string $value Valeur à valider.
|
|
* @param WP_REST_Request $request Requête REST.
|
|
* @param string $param Nom du paramètre.
|
|
* @return bool|WP_Error
|
|
*/
|
|
public static function validate_event($value, WP_REST_Request $request, string $param) {
|
|
$allowed_events = ['document.sent', 'document.error', 'document.completed'];
|
|
|
|
if (!in_array($value, $allowed_events, true)) {
|
|
return new WP_Error(
|
|
'invalid_event',
|
|
sprintf(
|
|
/* translators: %s: liste des événements autorisés */
|
|
__('Type d\'événement invalide. Événements autorisés : %s', 'esi_peppol'),
|
|
implode(', ', $allowed_events)
|
|
),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Traite la réception d'un webhook.
|
|
*
|
|
* @param WP_REST_Request $request Requête REST contenant les données du webhook.
|
|
* @return WP_REST_Response|WP_Error
|
|
*/
|
|
public static function handle_webhook(WP_REST_Request $request) {
|
|
// Récupérer les headers
|
|
$event = $request->get_header('X-ESIPeppol-Event');
|
|
$document_id = $request->get_header('X-ESIPeppol-Document-ID');
|
|
$company_id = $request->get_header('X-ESIPeppol-Company-ID');
|
|
|
|
// Récupérer le body JSON
|
|
$body = $request->get_json_params();
|
|
|
|
if (empty($body)) {
|
|
return new WP_Error(
|
|
'invalid_body',
|
|
__('Corps de la requête invalide ou vide.', 'esi_peppol'),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
// Valider la structure du payload
|
|
if (!isset($body['event']) || !isset($body['document'])) {
|
|
return new WP_Error(
|
|
'invalid_payload',
|
|
__('Structure du payload invalide. Champs requis : event, document.', 'esi_peppol'),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
// Vérifier la cohérence entre le header et le body
|
|
if ($body['event'] !== $event) {
|
|
return new WP_Error(
|
|
'event_mismatch',
|
|
__('Le type d\'événement dans le header ne correspond pas à celui du body.', 'esi_peppol'),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
// Extraire les données du document
|
|
$document_data = $body['document'];
|
|
|
|
// Le document_id peut être dans document.document_id (UUID) ou document.id (entier)
|
|
// On privilégie document_id (UUID) car c'est celui stocké dans notre base
|
|
$document_id_from_body = '';
|
|
if (isset($document_data['document_id']) && !empty($document_data['document_id'])) {
|
|
$document_id_from_body = (string) $document_data['document_id'];
|
|
} elseif (isset($document_data['id'])) {
|
|
// Fallback sur l'ID numérique si document_id n'est pas disponible
|
|
$document_id_from_body = (string) $document_data['id'];
|
|
}
|
|
|
|
// Utiliser le document_id du body si disponible, sinon celui du header
|
|
$final_document_id = !empty($document_id_from_body) ? $document_id_from_body : $document_id;
|
|
|
|
// Log de réception du webhook
|
|
if (class_exists(PEPPOL_Plugin::class)) {
|
|
PEPPOL_Plugin::write_debug_file(
|
|
[
|
|
'event' => 'webhook_received',
|
|
'event_type' => $event,
|
|
'document_id' => $final_document_id,
|
|
'company_id' => $company_id,
|
|
'payload' => $body,
|
|
],
|
|
'INFO'
|
|
);
|
|
}
|
|
|
|
// Traiter selon le type d'événement
|
|
$result = self::process_webhook_event($event, $final_document_id, $body);
|
|
|
|
if (is_wp_error($result)) {
|
|
// Log de l'erreur
|
|
if (class_exists(PEPPOL_Plugin::class)) {
|
|
PEPPOL_Plugin::write_debug_file(
|
|
[
|
|
'event' => 'webhook_error',
|
|
'event_type' => $event,
|
|
'document_id' => $final_document_id,
|
|
'error' => $result->get_error_message(),
|
|
'error_code' => $result->get_error_code(),
|
|
],
|
|
'ERROR'
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
// Réponse de succès (code 200-299 requis par la documentation)
|
|
return new WP_REST_Response(
|
|
[
|
|
'success' => true,
|
|
'message' => __('Webhook traité avec succès.', 'esi_peppol'),
|
|
],
|
|
200
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Traite un événement webhook spécifique.
|
|
*
|
|
* @param string $event Type d'événement (document.sent, document.error, document.completed).
|
|
* @param string $document_id ID du document.
|
|
* @param array $payload Données complètes du webhook.
|
|
* @return bool|WP_Error
|
|
*/
|
|
private static function process_webhook_event(string $event, string $document_id, array $payload) {
|
|
$document_data = $payload['document'] ?? [];
|
|
|
|
// Déterminer le nouveau statut selon l'événement
|
|
$new_status = null;
|
|
if ($event === 'document.sent') {
|
|
$new_status = 'sent';
|
|
} elseif ($event === 'document.error') {
|
|
$new_status = 'error';
|
|
} elseif ($event === 'document.completed') {
|
|
$new_status = 'completed';
|
|
}
|
|
|
|
if ($new_status === null) {
|
|
return new WP_Error(
|
|
'unknown_event',
|
|
sprintf(
|
|
/* translators: %s: type d'événement */
|
|
__('Type d\'événement inconnu : %s', 'esi_peppol'),
|
|
$event
|
|
),
|
|
['status' => 400]
|
|
);
|
|
}
|
|
|
|
// Extraire le peppol_document_id si disponible
|
|
$peppol_document_id = isset($document_data['peppol_document_id'])
|
|
? (string) $document_data['peppol_document_id']
|
|
: '';
|
|
|
|
// Extraire le message d'erreur si présent
|
|
$message = '';
|
|
if ($event === 'document.error' && isset($payload['error']['message'])) {
|
|
$message = (string) $payload['error']['message'];
|
|
}
|
|
|
|
// Mettre à jour le document dans la base de données
|
|
$updated = PEPPOL_Main_model::update_by_document_id(
|
|
$document_id,
|
|
$new_status,
|
|
$peppol_document_id,
|
|
$message,
|
|
$payload
|
|
);
|
|
|
|
if (!$updated) {
|
|
// Document non trouvé - ce n'est pas forcément une erreur critique
|
|
// (le document pourrait avoir été créé manuellement dans l'interface ESI Peppol)
|
|
if (class_exists(PEPPOL_Plugin::class)) {
|
|
PEPPOL_Plugin::write_debug_file(
|
|
[
|
|
'event' => 'webhook_document_not_found',
|
|
'document_id' => $document_id,
|
|
'status' => $new_status,
|
|
'message' => __('Document non trouvé dans la base de données WordPress. Il a peut-être été créé manuellement dans l\'interface ESI Peppol.', 'esi_peppol'),
|
|
],
|
|
'WARNING'
|
|
);
|
|
}
|
|
|
|
// On retourne quand même true car le webhook a été reçu et traité
|
|
// (même si le document n'existe pas dans notre base)
|
|
return true;
|
|
}
|
|
|
|
// Log de succès
|
|
if (class_exists(PEPPOL_Plugin::class)) {
|
|
PEPPOL_Plugin::write_debug_file(
|
|
[
|
|
'event' => 'webhook_processed',
|
|
'event_type' => $event,
|
|
'document_id' => $document_id,
|
|
'new_status' => $new_status,
|
|
'peppol_document_id' => $peppol_document_id,
|
|
],
|
|
'INFO'
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Retourne l'URL complète de l'endpoint webhook.
|
|
*
|
|
* Cette URL doit être configurée dans l'interface d'administration Filament
|
|
* de l'API ESI Peppol.
|
|
*
|
|
* @return string URL du webhook.
|
|
*/
|
|
public static function get_webhook_url(): string {
|
|
$rest_url = rest_url('esi-peppol/v1/webhook');
|
|
|
|
/**
|
|
* Filtre pour modifier l'URL du webhook.
|
|
*
|
|
* @param string $rest_url URL REST par défaut.
|
|
*/
|
|
return (string) apply_filters('esi_peppol_webhook_url', $rest_url);
|
|
}
|
|
}
|