load_actions(); } /** * Méthode appelée lors de l'activation du plugin. * * Elle délègue au modèle principal la création de la table * `esi_peppol_invoices` et détecte automatiquement les champs TVA. * * @return void */ public static function activate(): void { // Création / mise à jour de la table principale des factures PEPPOL_Main_model::create_table(); // Détection automatique des champs TVA (billing -> shipping) self::auto_detect_vat_field_on_install(); } public function desactivate() { } public static function register_routes() { } public function load_actions() { // Enregistrement des hooks WooCommerce if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class)) { \ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::register_hooks(); } // Notice globale pour le numéro de TVA \add_action('admin_notices', [self::class, 'maybe_show_vat_notice']); // Ajouter une colonne "Statut Peppol" dans le listing des commandes WooCommerce if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class)) { \add_filter('manage_edit-shop_order_columns', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'add_peppol_status_column'], 20); \add_filter('manage_woocommerce_page_wc-orders_columns', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'add_peppol_status_column'], 20); \add_action('manage_shop_order_posts_custom_column', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'show_peppol_status_column'], 10, 2); \add_action('manage_woocommerce_page_wc-orders_custom_column', [\ESI_PEPPOL\controllers\PEPPOL_Woocommerce_controller::class, 'show_peppol_status_column'], 10, 2); } } /** * Enregistre le menu et les sous-menus dans l'admin WordPress. * * @return void */ public static function add_admin_menu(): void { // Page principale du plugin add_menu_page( __('ESI Peppol', 'esi_peppol'), __('ESI Peppol', 'esi_peppol'), 'manage_options', 'esi-peppol', [self::class, 'render_dashboard_page'], 'dashicons-migrate', 56 ); // Sous-menus (cibles des boutons de la page d'accueil) add_submenu_page( 'esi-peppol', __('Configuration', 'esi_peppol'), __('Configuration', 'esi_peppol'), 'manage_options', 'esi-peppol-settings', [self::class, 'render_settings_page'] ); add_submenu_page( 'esi-peppol', __('Journal', 'esi_peppol'), __('Journal', 'esi_peppol'), 'manage_options', 'esi-peppol-logs', [self::class, 'render_logs_page'] ); } public static function load_filters() { } public function load_shortcodes() { } public function load_frontend_assets() { self::enqueue_admin_assets(); self::enqueue_front_assets(); } public function load_admin_assets() { } /** * Page d'accueil du plugin (dashboard interne ESI Peppol). * * Contient un texte explicatif et deux boutons vers les sous-menus. * * @return void */ public static function render_dashboard_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } $settings_url = admin_url('admin.php?page=esi-peppol-settings'); $logs_url = admin_url('admin.php?page=esi-peppol-logs'); $template = ESI_PEPPOL_DIR . 'templates/admin/dashboard.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } /** * Page de configuration (stub pour l'instant). * * @return void */ public static function render_settings_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } $notice = null; $error = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = isset($_POST['esi_peppol_action']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_action'])) : ''; if (!isset($_POST['esi_peppol_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['esi_peppol_nonce'])), 'esi_peppol_save_settings')) { $error = __('Sécurité : nonce invalide.', 'esi_peppol'); } else { $api_key = isset($_POST['esi_peppol_api_key']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_api_key'])) : ''; $password = isset($_POST['esi_peppol_password']) ? sanitize_text_field(wp_unslash($_POST['esi_peppol_password'])) : ''; $email = isset($_POST['esi_peppol_email']) ? sanitize_email(wp_unslash($_POST['esi_peppol_email'])) : ''; $logo_email_id = isset($_POST['esi_peppol_logo_email_id']) ? absint(wp_unslash($_POST['esi_peppol_logo_email_id'])) : 0; $vat_number_format = get_option('esi_peppol_vat_number_format', ''); if (isset($_POST['esi_peppol_vat_number_format'])) { $vat_number_format = sanitize_text_field(wp_unslash($_POST['esi_peppol_vat_number_format'])); } // Sauvegarde systématique des options update_option('esi_peppol_api_key', $api_key); update_option('esi_peppol_password', $password); update_option('esi_peppol_email', $email); update_option('esi_peppol_logo_email_id', $logo_email_id); update_option('esi_peppol_vat_number_format', $vat_number_format); if ($action === 'save') { $notice = __('Paramètres enregistrés avec succès.', 'esi_peppol'); } elseif ($action === 'test') { // Ici on pourrait appeler un endpoint de "ping" de l\'API ESIPeppol. // Pour l\'instant : simple message de succès basé sur la présence des credentials. if ($api_key && $password) { $notice = __('Test de connexion simulé : les identifiants semblent corrects (vérification API réelle à implémenter).', 'esi_peppol'); } else { $error = __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'); } } } } $template = ESI_PEPPOL_DIR . 'templates/admin/settings.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } /** * Page de journal / logs (stub pour l'instant). * * @return void */ public static function render_logs_page(): void { if (!current_user_can('manage_options')) { wp_die(esc_html__('Vous n\'avez pas les permissions nécessaires pour accéder à cette page.', 'esi_peppol')); } // Vérifier si on affiche la page de détail $detail_id = isset($_GET['detail']) ? (int) $_GET['detail'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ($detail_id > 0) { // Afficher la page de détail $row = PEPPOL_Main_model::get_by_id($detail_id); if (!$row) { wp_die(esc_html__('Aucun enregistrement Peppol trouvé pour cet ID.', 'esi_peppol')); } // Préparer les données pour l'affichage (même logique que ajax_get_log_details) $data_sent = $row->data_sent ?? null; $response_data = $row->response_data ?? null; // Formater les données JSON si possible $data_sent_formatted = ''; if ($data_sent) { if (is_array($data_sent) || is_object($data_sent)) { $data_sent_formatted = wp_json_encode($data_sent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data_sent_formatted = (string) $data_sent; } } $response_data_formatted = ''; if ($response_data) { if (is_array($response_data) || is_object($response_data)) { $response_data_formatted = wp_json_encode($response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $response_data_formatted = (string) $response_data; } } // Extraire les informations financières depuis data_sent $invoice_totals = null; $vat_totals = null; $customer_data = null; if ($data_sent && (is_array($data_sent) || is_object($data_sent))) { // Convertir en tableau si c'est un objet $data_array = is_object($data_sent) ? (array) $data_sent : $data_sent; // Extraire invoice_totals if (isset($data_array['invoice_totals']) && is_array($data_array['invoice_totals'])) { $invoice_totals = $data_array['invoice_totals']; } // Extraire vat_totals if (isset($data_array['vat_totals']) && is_array($data_array['vat_totals'])) { $vat_totals = $data_array['vat_totals']; } // Extraire les données client (buyer) if (isset($data_array['parties']['buyer']) && is_array($data_array['parties']['buyer'])) { $customer_data = $data_array['parties']['buyer']; } } // Récupérer les informations de la commande si disponible $order_info = null; $order_totals = null; if (!empty($row->id_order)) { $order = wc_get_order($row->id_order); if ($order) { $order_info = [ 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'status' => $order->get_status(), 'total' => $order->get_total(), 'billing_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'billing_email' => $order->get_billing_email(), 'billing_phone' => $order->get_billing_phone(), 'billing_company' => $order->get_billing_company(), 'billing_address_1' => $order->get_billing_address_1(), 'billing_address_2' => $order->get_billing_address_2(), 'billing_city' => $order->get_billing_city(), 'billing_postcode' => $order->get_billing_postcode(), 'billing_country' => $order->get_billing_country(), 'billing_vat_number' => $order->get_meta('_billing_vat_number') ?: '', 'edit_link' => get_edit_post_link($row->id_order), ]; // Calculer les totaux depuis la commande si invoice_totals n'est pas disponible if (!$invoice_totals) { $order_totals = [ 'total_amount_excluding_vat' => $order->get_total() - $order->get_total_tax(), 'total_vat_amount' => $order->get_total_tax(), 'total_amount_including_vat' => $order->get_total(), ]; } } } // Extraire le message d'erreur depuis response_data $error_message_to_display = $row->message ?? ''; if (!empty($response_data) && (is_array($response_data) || is_object($response_data))) { // Convertir en tableau si c'est un objet $error_data = is_object($response_data) ? (array) $response_data : $response_data; if (is_array($error_data)) { // Structure avec error.message if (isset($error_data['error']['message'])) { $error_message_to_display = (string) $error_data['error']['message']; } // Structure avec details.validation_error elseif (isset($error_data['details']['validation_error'])) { $error_message_to_display = (string) $error_data['details']['validation_error']; } // Structure avec validation_error directement elseif (isset($error_data['validation_error'])) { $error_message_to_display = (string) $error_data['validation_error']; } // Structure avec message directement elseif (isset($error_data['message'])) { $error_message_to_display = (string) $error_data['message']; } } } // Préparer les données pour le template $log_data = [ 'id' => $row->id, 'id_order' => $row->id_order ?? 0, 'order_info' => $order_info, 'document_id' => $row->document_id ?? '', 'peppol_document_id' => $row->peppol_document_id ?? '', 'status' => $row->status ?? '', 'success' => !empty($row->success), 'message' => $error_message_to_display, 'http_code' => $row->http_code ?? null, 'date_add' => $row->date_add ?? '', 'date_update' => $row->date_update ?? '', 'invoice_totals' => $invoice_totals ?? $order_totals, 'vat_totals' => $vat_totals, 'customer_data' => $customer_data, 'data_sent' => $data_sent_formatted, 'response_data' => $response_data_formatted, ]; $template = ESI_PEPPOL_DIR . 'templates/admin/logs-detail.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } else { wp_die(esc_html__('Template de détail introuvable.', 'esi_peppol')); } } else { // Afficher la liste des logs // Récupération des dernières lignes de la table custom $rows = PEPPOL_Main_model::get_recent(50); $template = ESI_PEPPOL_DIR . 'templates/admin/logs.php'; if (file_exists($template)) { /** @noinspection PhpIncludeInspection */ include $template; } } } public static function enqueue_admin_assets() { $screen = function_exists('get_current_screen') ? get_current_screen() : null; // Ne charger les assets que sur les pages du plugin ou la page des commandes WooCommerce $is_peppol_page = isset($_GET['page']) && strpos((string) $_GET['page'], 'esi-peppol') === 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_orders_page = $screen && ( strpos((string) $screen->id, 'shop_order') !== false || strpos((string) $screen->id, 'woocommerce_page_wc-orders') !== false || $screen->id === 'edit-shop_order' ); if (!$is_peppol_page && !$is_orders_page && $screen && strpos((string) $screen->base, 'esi-peppol') === false) { return; } $suffix = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG ? '' : '.min'; // Styles admin généraux wp_enqueue_style( 'esi-peppol-admin', ESI_PEPPOL_URL . 'assets/css/admin.css', [], ESI_PEPPOL_VERSION ); // DataTables (utilisé pour le tableau du journal des échanges) // Utiliser le CDN DataTables pour une meilleure compatibilité avec WordPress wp_enqueue_style( 'esi-peppol-datatables', 'https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css', ['esi-peppol-admin'], '1.13.8' ); // S'assurer que jQuery est chargé en premier wp_enqueue_script('jquery'); // Utiliser le CDN DataTables au lieu du fichier local pour éviter les problèmes de compatibilité wp_enqueue_script( 'esi-peppol-datatables', 'https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js', ['jquery'], '1.13.8', true ); // Enqueue wp.media pour la gestion de la médiathèque (logo email) wp_enqueue_media(); // Script admin principal, dépendant de DataTables wp_enqueue_script( 'esi-peppol-admin', ESI_PEPPOL_URL . 'assets/js/admin.js', ['jquery', 'esi-peppol-datatables'], ESI_PEPPOL_VERSION, true ); wp_localize_script( 'esi-peppol-admin', 'esiPeppolAdmin', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('esi_peppol_test_connection'), 'logs_nonce_resend' => wp_create_nonce('esi_peppol_resend_invoice'), 'logs_nonce_status' => wp_create_nonce('esi_peppol_check_invoice_status'), 'logs_nonce_details' => wp_create_nonce('esi_peppol_get_log_details'), 'vat_detect_nonce' => wp_create_nonce('esi_peppol_detect_vat_fields'), 'vat_save_nonce' => wp_create_nonce('esi_peppol_save_vat_field'), 'i18n_missing' => __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'), 'i18n_success' => __('Connexion réussie à l\'API ESIPeppol.', 'esi_peppol'), 'i18n_error' => __('La connexion a échoué. Veuillez vérifier vos identifiants.', 'esi_peppol'), 'i18n_network' => __('Erreur de communication avec le serveur WordPress.', 'esi_peppol'), 'i18n_logs_ok_title' => __('OK', 'esi_peppol'), 'i18n_logs_ko_title' => __('Pas OK', 'esi_peppol'), 'i18n_logs_resend_ok' => __('Document renvoyé avec succès à l\'API ESIPeppol.', 'esi_peppol'), 'i18n_logs_resend_ko' => __('L\'envoi du document a échoué.', 'esi_peppol'), 'i18n_logs_status_lbl' => __('Statut actuel du document', 'esi_peppol'), 'i18n_logs_detail_lbl' => __('Détail retour API', 'esi_peppol'), 'i18n_logs_general_info' => __('Informations générales', 'esi_peppol'), 'i18n_logs_totals' => __('Totaux', 'esi_peppol'), 'i18n_logs_vat_details' => __('Détails TVA', 'esi_peppol'), 'i18n_logs_customer_data' => __('Données client', 'esi_peppol'), 'i18n_logs_data_sent' => __('Données envoyées', 'esi_peppol'), 'i18n_logs_response_data' => __('Données de réponse', 'esi_peppol'), 'i18n_logs_error' => __('Erreur lors du chargement des détails.', 'esi_peppol'), ] ); } /** * Handler AJAX pour le test de connexion à l'API ESIPeppol. * * @return void */ public static function ajax_test_connection(): void { if (!check_ajax_referer('esi_peppol_test_connection', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer ce test.', 'esi_peppol'), ], 403 ); } $api_key = isset($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($api_key === '' || $password === '') { wp_send_json_error( [ 'message' => __('Veuillez renseigner l\'API Key et le Password avant de tester la connexion.', 'esi_peppol'), ], 400 ); } $result = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::status_with_credentials($api_key, $password); if (!empty($result['success'])) { $company = ''; if (is_array($result['data'] ?? null) && isset($result['data']['company'])) { $company = (string) $result['data']['company']; } $message = $company ? sprintf(__('Connexion réussie. Entreprise: %s', 'esi_peppol'), $company) : __('Connexion réussie à l\'API ESIPeppol.', 'esi_peppol'); wp_send_json_success( [ 'message' => $message, 'http_code' => $result['http_code'] ?? 200, ] ); } $error_message = $result['message'] ?? ''; if ($error_message === '' && is_array($result['data'] ?? null) && isset($result['data']['message'])) { $error_message = (string) $result['data']['message']; } if ($error_message === '') { $error_message = __('La connexion a échoué. Veuillez vérifier vos identifiants.', 'esi_peppol'); } wp_send_json_error( [ 'message' => $error_message, 'http_code' => $result['http_code'] ?? 0, ] ); } /** * Handler AJAX pour renvoyer un document/facture vers l'API ESIPeppol * à partir d'un ID de commande WooCommerce. * * @return void */ public static function ajax_resend_invoice(): void { if (!check_ajax_referer('esi_peppol_resend_invoice', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($order_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de commande invalide.', 'esi_peppol'), ], 400 ); } $order = wc_get_order($order_id); if (!$order instanceof \WC_Order) { wp_send_json_error( [ 'message' => __('Commande introuvable.', 'esi_peppol'), ], 404 ); } // Vérifier que la clé API est configurée $api_key = get_option('esi_peppol_api_key', ''); if (empty($api_key)) { wp_send_json_error( [ 'message' => __('La clé API n\'est pas configurée. Veuillez configurer la clé API dans les paramètres du plugin.', 'esi_peppol'), ], 400 ); } // Reconstruire le payload et renvoyer le document via l'API ESIPeppol $payload = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::build_payload_from_order($order); $result = \ESI_PEPPOL\controllers\PEPPOL_peppol_controller::upload_json($payload, $order_id); // Mettre à jour quelques métadonnées sur la commande pour le suivi $order->update_meta_data('_esi_peppol_last_manual_action', 'resend'); $order->update_meta_data('_esi_peppol_last_peppol_status', !empty($result['success']) ? 'success' : 'error'); $order->update_meta_data('_esi_peppol_last_http_code', $result['http_code'] ?? 0); $order->update_meta_data('_esi_peppol_last_message', $result['message'] ?? ''); $order->save(); $detail_message = ''; if (is_array($result['data'] ?? null) && isset($result['data']['message'])) { $detail_message = (string) $result['data']['message']; } if ($detail_message === '' && isset($result['message'])) { $detail_message = (string) $result['message']; } $payload_response = [ 'success' => !empty($result['success']), 'message' => !empty($result['success']) ? __('Document renvoyé avec succès à l\'API ESIPeppol.', 'esi_peppol') : __('L\'envoi du document a échoué.', 'esi_peppol'), 'detail' => $detail_message, 'http_code' => $result['http_code'] ?? 0, ]; if (!empty($result['success'])) { wp_send_json_success($payload_response); } wp_send_json_error($payload_response, $result['http_code'] ?? 400); } /** * Handler AJAX pour consulter le statut courant d'un document Peppol * à partir de l'ID de commande (lecture depuis la table custom). * * @return void */ public static function ajax_check_invoice_status(): void { if (!check_ajax_referer('esi_peppol_check_invoice_status', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $order_id = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($order_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de commande invalide.', 'esi_peppol'), ], 400 ); } $row = \ESI_PEPPOL\models\PEPPOL_Main_model::get_by_order_id($order_id); if (!$row) { wp_send_json_error( [ 'message' => __('Aucun enregistrement Peppol trouvé pour cette commande.', 'esi_peppol'), ], 404 ); } $detail_message = ''; if (isset($row->response_data)) { $data = $row->response_data; if (is_array($data) && isset($data['message'])) { $detail_message = (string) $data['message']; } elseif (is_string($data) && $data !== '') { $detail_message = $data; } } if ($detail_message === '' && isset($row->message)) { $detail_message = (string) $row->message; } $payload_response = [ 'success' => !empty($row->success), 'status' => isset($row->status) ? (string) $row->status : '', 'message' => sprintf( /* translators: %s: statut Peppol actuel */ __('Statut actuel du document : %s', 'esi_peppol'), isset($row->status) ? (string) $row->status : __('inconnu', 'esi_peppol') ), 'detail' => $detail_message, 'http_code' => isset($row->http_code) ? (int) $row->http_code : 0, ]; if (!empty($row->success)) { wp_send_json_success($payload_response); } // On renvoie tout de même une réponse structurée même en cas d'échec métier wp_send_json_error($payload_response, 200); } /** * Détecte automatiquement les champs TVA lors de l'installation. * Limite la recherche aux champs billing et shipping dans cet ordre. * Utilise la fonction helper PEPPOL_Woo_Helper::esi_detect_vat_fields_billing_shipping(). * * @return string|null La clé du champ TVA détecté, ou null si aucun trouvé */ public static function auto_detect_vat_field_on_install(): ?string { // Ne pas détecter si un champ est déjà sauvegardé $existing_field = get_option('esi_peppol_vat_field_key', ''); if ($existing_field !== '') { return $existing_field; } // Vérifier si WooCommerce est disponible if (!function_exists('wc_get_orders')) { // Marquer que la détection a été tentée (même si WooCommerce n'est pas disponible) update_option('esi_peppol_vat_field_auto_detected', true); return null; } // Récupérer les dernières commandes pour scanner les champs TVA $orders = wc_get_orders([ 'limit' => 50, 'orderby' => 'date', 'order' => 'DESC', 'status' => ['completed', 'processing', 'on-hold'], ]); $vat_fields_found = []; // Utiliser la fonction helper pour détecter les champs TVA dans chaque commande foreach ($orders as $order) { if (!$order instanceof \WC_Order) { continue; } $fields = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_detect_vat_fields_billing_shipping($order); // Compter les occurrences de chaque champ foreach ($fields as $field) { $key = $field['key']; if (!isset($vat_fields_found[$key])) { $vat_fields_found[$key] = [ 'key' => $key, 'count' => 0, 'group' => $field['group'], ]; } $vat_fields_found[$key]['count']++; } } // Si aucun champ trouvé, marquer que la détection a été effectuée et retourner null if (empty($vat_fields_found)) { update_option('esi_peppol_vat_field_auto_detected', true); return null; } // Trier : d'abord par groupe (billing avant shipping), puis par nombre d'occurrences usort($vat_fields_found, function ($a, $b) { // Priorité au groupe billing if ($a['group'] === 'billing' && $b['group'] !== 'billing') { return -1; } if ($a['group'] !== 'billing' && $b['group'] === 'billing') { return 1; } // Si même groupe, trier par nombre d'occurrences return $b['count'] - $a['count']; }); // Prendre le premier champ trouvé (priorité billing) $selected_field = reset($vat_fields_found); $vat_field_key = $selected_field['key']; // Sauvegarder le champ détecté update_option('esi_peppol_vat_field_key', $vat_field_key); // Marquer que la détection a été effectuée à l'installation update_option('esi_peppol_vat_field_auto_detected', true); return $vat_field_key; } /** * Handler AJAX pour détecter les champs TVA dans les commandes WooCommerce. * * @return void */ public static function ajax_detect_vat_fields(): void { if (!check_ajax_referer('esi_peppol_detect_vat_fields', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } // Récupérer les dernières commandes pour scanner les champs TVA $orders = wc_get_orders([ 'limit' => 50, 'orderby' => 'date', 'order' => 'DESC', 'status' => ['completed', 'processing', 'on-hold'], ]); $vat_fields_found = []; foreach ($orders as $order) { if (!$order instanceof \WC_Order) { continue; } // Utiliser la fonction helper pour scanner tous les champs TVA possibles $vat_candidates = []; // Scanner les meta keys contenant vat/tva foreach ($order->get_meta_data() as $meta) { $k = (string) $meta->key; if (preg_match('/\b(vat|tva)\b/i', $k) || preg_match('/(vat|tva)/i', $k)) { $v = $meta->value; if (is_string($v)) { $v = trim($v); } if (!empty($v)) { if (!isset($vat_candidates[$k])) { $vat_candidates[$k] = [ 'key' => $k, 'value' => $v, 'count' => 0, ]; } $vat_candidates[$k]['count']++; } } } // Scanner les champs de checkout if (function_exists('WC') && WC()->checkout()) { $fields = WC()->checkout()->get_checkout_fields(); $patterns = ['vat', 'tva', 'btw', 'ust', 'mwst', 'piva', 'moms', 'mva']; foreach ($fields as $group_fields) { foreach ($group_fields as $key => $def) { $k = strtolower($key); $found = false; foreach ($patterns as $p) { if (strpos($k, $p) !== false) { $found = true; break; } } if (!$found) { $label = strtolower((string) ($def['label'] ?? '')); foreach ($patterns as $p) { if (preg_match('/\b' . $p . '\b/i', $label)) { $found = true; break; } } } if ($found) { foreach ([$key, '_' . $key] as $meta_key) { $val = $order->get_meta($meta_key, true); if (!empty($val)) { if (!isset($vat_candidates[$meta_key])) { $vat_candidates[$meta_key] = [ 'key' => $meta_key, 'value' => trim((string) $val), 'count' => 0, ]; } $vat_candidates[$meta_key]['count']++; } } } } } } // Ajouter les candidats trouvés foreach ($vat_candidates as $candidate) { $key = $candidate['key']; if (!isset($vat_fields_found[$key])) { $vat_fields_found[$key] = [ 'key' => $key, 'count' => 0, 'sample_value' => $candidate['value'], ]; } $vat_fields_found[$key]['count'] += $candidate['count']; } } // Trier par nombre d'occurrences (décroissant) usort($vat_fields_found, function ($a, $b) { return $b['count'] - $a['count']; }); wp_send_json_success([ 'fields' => array_values($vat_fields_found), 'count' => count($vat_fields_found), ]); } /** * Handler AJAX pour sauvegarder le champ TVA sélectionné. * * @return void */ public static function ajax_save_vat_field(): void { if (!check_ajax_referer('esi_peppol_save_vat_field', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $vat_field_key = isset($_POST['vat_field_key']) ? sanitize_text_field(wp_unslash($_POST['vat_field_key'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($vat_field_key === '') { wp_send_json_error( [ 'message' => __('Clé de champ TVA invalide.', 'esi_peppol'), ], 400 ); } // Sauvegarder le champ sélectionné update_option('esi_peppol_vat_field_key', $vat_field_key); wp_send_json_success([ 'message' => __('Champ TVA enregistré avec succès.', 'esi_peppol'), 'vat_field_key' => $vat_field_key, ]); } /** * Handler AJAX pour récupérer les détails complets d'un log Peppol * à partir de l'ID du log. * * @return void */ public static function ajax_get_log_details(): void { if (!check_ajax_referer('esi_peppol_get_log_details', 'nonce', false)) { wp_send_json_error( [ 'message' => __('Sécurité : nonce invalide.', 'esi_peppol'), ], 400 ); } if (!current_user_can('manage_woocommerce') && !current_user_can('manage_options')) { wp_send_json_error( [ 'message' => __('Vous n\'avez pas les permissions nécessaires pour effectuer cette action.', 'esi_peppol'), ], 403 ); } $log_id = isset($_POST['log_id']) ? (int) $_POST['log_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ($log_id <= 0) { wp_send_json_error( [ 'message' => __('Identifiant de log invalide.', 'esi_peppol'), ], 400 ); } $row = \ESI_PEPPOL\models\PEPPOL_Main_model::get_by_id($log_id); if (!$row) { wp_send_json_error( [ 'message' => __('Aucun enregistrement Peppol trouvé pour cet ID.', 'esi_peppol'), ], 404 ); } // Préparer les données pour l'affichage $data_sent = $row->data_sent ?? null; $response_data = $row->response_data ?? null; // Formater les données JSON si possible $data_sent_formatted = ''; if ($data_sent) { if (is_array($data_sent) || is_object($data_sent)) { $data_sent_formatted = wp_json_encode($data_sent, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $data_sent_formatted = (string) $data_sent; } } $response_data_formatted = ''; if ($response_data) { if (is_array($response_data) || is_object($response_data)) { $response_data_formatted = wp_json_encode($response_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } else { $response_data_formatted = (string) $response_data; } } // Extraire les informations financières depuis data_sent $invoice_totals = null; $vat_totals = null; $customer_data = null; if ($data_sent && (is_array($data_sent) || is_object($data_sent))) { // Convertir en tableau si c'est un objet $data_array = is_object($data_sent) ? (array) $data_sent : $data_sent; // Extraire invoice_totals if (isset($data_array['invoice_totals']) && is_array($data_array['invoice_totals'])) { $invoice_totals = $data_array['invoice_totals']; } // Extraire vat_totals if (isset($data_array['vat_totals']) && is_array($data_array['vat_totals'])) { $vat_totals = $data_array['vat_totals']; } // Extraire les données client (buyer) if (isset($data_array['parties']['buyer']) && is_array($data_array['parties']['buyer'])) { $customer_data = $data_array['parties']['buyer']; } } // Récupérer les informations de la commande si disponible $order_info = null; $order_totals = null; if (!empty($row->id_order)) { $order = wc_get_order($row->id_order); if ($order) { $order_info = [ 'id' => $order->get_id(), 'number' => $order->get_order_number(), 'status' => $order->get_status(), 'total' => $order->get_total(), 'billing_name' => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()), 'billing_email' => $order->get_billing_email(), 'billing_phone' => $order->get_billing_phone(), 'billing_company' => $order->get_billing_company(), 'billing_address_1' => $order->get_billing_address_1(), 'billing_address_2' => $order->get_billing_address_2(), 'billing_city' => $order->get_billing_city(), 'billing_postcode' => $order->get_billing_postcode(), 'billing_country' => $order->get_billing_country(), 'billing_vat_number' => $order->get_meta('_billing_vat_number') ?: '', 'edit_link' => get_edit_post_link($row->id_order), ]; // Calculer les totaux depuis la commande si invoice_totals n'est pas disponible if (!$invoice_totals) { $order_totals = [ 'total_amount_excluding_vat' => $order->get_total() - $order->get_total_tax(), 'total_vat_amount' => $order->get_total_tax(), 'total_amount_including_vat' => $order->get_total(), ]; } } } // Extraire le message d'erreur depuis response_data de la même manière que le template email // pour garantir la cohérence entre la modal et l'email $error_message_to_display = $row->message ?? ''; if (!empty($response_data) && (is_array($response_data) || is_object($response_data))) { // Convertir en tableau si c'est un objet $error_data = is_object($response_data) ? (array) $response_data : $response_data; if (is_array($error_data)) { // Structure avec error.message if (isset($error_data['error']['message'])) { $error_message_to_display = (string) $error_data['error']['message']; } // Structure avec details.validation_error elseif (isset($error_data['details']['validation_error'])) { $error_message_to_display = (string) $error_data['details']['validation_error']; } // Structure avec validation_error directement elseif (isset($error_data['validation_error'])) { $error_message_to_display = (string) $error_data['validation_error']; } // Structure avec message directement elseif (isset($error_data['message'])) { $error_message_to_display = (string) $error_data['message']; } } } $payload_response = [ 'id' => $row->id, 'id_order' => $row->id_order ?? 0, 'order_info' => $order_info, 'document_id' => $row->document_id ?? '', 'peppol_document_id' => $row->peppol_document_id ?? '', 'status' => $row->status ?? '', 'success' => !empty($row->success), 'message' => $error_message_to_display, 'http_code' => $row->http_code ?? null, 'date_add' => $row->date_add ?? '', 'date_update' => $row->date_update ?? '', 'invoice_totals' => $invoice_totals ?? $order_totals, 'vat_totals' => $vat_totals, 'customer_data' => $customer_data, 'data_sent' => $data_sent_formatted, 'response_data' => $response_data_formatted, ]; wp_send_json_success($payload_response); } public static function enqueue_front_assets() { } /** * Ajoute un champ "Numéro de TVA de la boutique" dans les réglages * WooCommerce > Général, juste après le code postal du magasin. * * @param array $settings * @return array */ public static function filter_woocommerce_general_settings(array $settings): array { $new_settings = []; foreach ($settings as $setting) { $new_settings[] = $setting; if (isset($setting['id']) && $setting['id'] === 'woocommerce_store_postcode') { $new_settings[] = [ 'title' => __('Numéro de TVA de la boutique', 'esi_peppol'), 'desc' => __('Utilisé pour les factures Peppol (champ vendeur).', 'esi_peppol'), 'id' => 'woocommerce_store_vat_number', 'type' => 'text', 'default' => '', 'desc_tip' => true, ]; } } return $new_settings; } /** * Retourne un message d'avertissement à propos du numéro de TVA * de la boutique si celui-ci n'est pas encore renseigné dans * les réglages WooCommerce, ou si aucun champ TVA client n'a été détecté. * * - Message d'invitation initial : aucune configuration ESI Peppol * n'a encore été saisie. * - Message de rappel : les identifiants ESI Peppol sont déjà * enregistrés mais la TVA reste vide. * - Message si aucun champ TVA client détecté : proposer la détection dans settings. * * @return string|null */ /** * Récupère toutes les notices TVA à afficher * * @return array Tableau de notices, chaque notice contient 'message' et 'type' (info, warning, etc.) */ public static function get_vat_notice_message(): array { $notices = []; // Vérifier d'abord si un champ TVA client a été détecté $vat_field_key = (string) get_option('esi_peppol_vat_field_key', ''); $auto_detected = (bool) get_option('esi_peppol_vat_field_auto_detected', false); // Si aucun champ TVA détecté, proposer la détection dans settings if ($vat_field_key === '' && $auto_detected) { $settings_url = admin_url('admin.php?page=esi-peppol-settings'); $notices[] = [ 'message' => sprintf( /* translators: %s: URL vers la page de réglages */ __('Aucun champ TVA client n\'a été détecté automatiquement. Vous pouvez utiliser l\'outil de détection dans ESI Peppol > Configuration pour rechercher manuellement les champs TVA dans vos commandes.', 'esi_peppol'), esc_url($settings_url) ), 'type' => 'warning' ]; } $vat_number = (string) get_option('woocommerce_store_vat_number', ''); // Si le numéro de TVA est renseigné, on ne vérifie pas les autres notices liées au TVA if ($vat_number === '') { $api_key = (string) get_option('esi_peppol_api_key', ''); $password = (string) get_option('esi_peppol_password', ''); if ($api_key === '' && $password === '') { // Invitation initiale $notices[] = [ 'message' => __( 'Pour finaliser la mise en place de Peppol, pensez à renseigner le numéro de TVA de la boutique dans WooCommerce > Réglages > Général.', 'esi_peppol' ), 'type' => 'info' ]; } else { // Rappel après configuration du connecteur $notices[] = [ 'message' => __( 'Votre connecteur ESI Peppol est configuré, mais le numéro de TVA de la boutique n\'est pas encore renseigné dans WooCommerce > Réglages > Général. Sans ce numéro, les factures Peppol risquent d\'être incomplètes.', 'esi_peppol' ), 'type' => 'warning' ]; } } return $notices; } /** * Affiche un message d'avertissement dans l'admin WordPress * si le numéro de TVA de la boutique n'est pas renseigné ou * si aucun champ TVA client n'a été détecté. * * Hooké sur admin_notices. * * @return void */ public static function maybe_show_vat_notice(): void { if (!\current_user_can('manage_options')) { return; } $vat_notices = self::get_vat_notice_message(); if (empty($vat_notices)) { return; } foreach ($vat_notices as $notice) { $notice_class = 'notice-' . $notice['type']; echo '

' . \wp_kses_post($notice['message']) . '

'; } } /** * Vérifie l'état de configuration des settings ESI Peppol * * @return array Tableau avec l'état de chaque étape du setup */ public static function check_settings_status(): array { $status = [ 'vat_number' => [ 'completed' => false, 'value' => '', 'label' => __('Numéro de TVA de la boutique', 'esi_peppol'), 'description' => __('Le numéro de TVA de votre boutique doit être renseigné dans WooCommerce > Réglages > Général', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=wc-settings&tab=general'), 'action_label' => __('Configurer dans WooCommerce', 'esi_peppol'), ], 'vat_field' => [ 'completed' => false, 'value' => '', 'label' => __('Champ TVA client', 'esi_peppol'), 'description' => __('Le champ utilisé pour récupérer le numéro de TVA du client dans les commandes', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=esi-peppol-settings'), 'action_label' => __('Détecter le champ TVA', 'esi_peppol'), ], 'vat_number_format' => [ 'completed' => false, 'value' => '', 'label' => __('Format num TVA', 'esi_peppol'), 'description' => __('Format du numéro de TVA (backup si aucun format détecté automatiquement)', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=esi-peppol-settings'), 'action_label' => __('Configurer le format', 'esi_peppol'), ], 'api_credentials' => [ 'completed' => false, 'value' => '', 'label' => __('Identifiants API', 'esi_peppol'), 'description' => __('Votre clé API et mot de passe pour vous connecter à la plateforme ESI Peppol', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=esi-peppol-settings'), 'action_label' => __('Configurer les identifiants', 'esi_peppol'), ], 'webhook' => [ 'completed' => true, // Toujours disponible 'value' => '', 'label' => __('URL Webhook', 'esi_peppol'), 'description' => __('URL à configurer dans votre profil d\'entreprise sur la plateforme', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=esi-peppol-settings'), 'action_label' => __('Voir l\'URL webhook', 'esi_peppol'), ], 'email' => [ 'completed' => false, 'optional' => true, 'value' => '', 'label' => __('Email de notification', 'esi_peppol'), 'description' => __('Adresse email pour recevoir les notifications en cas d\'erreur d\'envoi des factures', 'esi_peppol'), 'action_url' => admin_url('admin.php?page=esi-peppol-settings'), 'action_label' => __('Configurer l\'email', 'esi_peppol'), ], ]; // Vérifier le numéro de TVA de la boutique $vat_number = (string) get_option('woocommerce_store_vat_number', ''); if ($vat_number !== '') { $status['vat_number']['completed'] = true; $status['vat_number']['value'] = $vat_number; } // Vérifier le champ TVA client $vat_field_key = (string) get_option('esi_peppol_vat_field_key', ''); if ($vat_field_key !== '') { $status['vat_field']['completed'] = true; $status['vat_field']['value'] = $vat_field_key; } // Vérifier le format num TVA (détecté ou backup) $detected_format = ''; if (\ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_has_wpo_invoice_numbers()) { $detected_format = \ESI_PEPPOL\helpers\PEPPOL_Woo_Helper::esi_get_wpo_invoice_number_format_label(); } $backup_format = (string) get_option('esi_peppol_vat_number_format', ''); if ($detected_format !== '') { $status['vat_number_format']['completed'] = true; $status['vat_number_format']['value'] = sprintf( __('Format détecté : %s', 'esi_peppol'), $detected_format ); } elseif ($backup_format !== '') { $status['vat_number_format']['completed'] = true; $status['vat_number_format']['value'] = $backup_format; } // Vérifier les identifiants API $api_key = (string) get_option('esi_peppol_api_key', ''); $password = (string) get_option('esi_peppol_password', ''); if ($api_key !== '' && $password !== '') { $status['api_credentials']['completed'] = true; $status['api_credentials']['value'] = substr($api_key, 0, 8) . '...'; // Masquer partiellement } // Récupérer l'URL webhook if (class_exists(\ESI_PEPPOL\controllers\PEPPOL_Webhook_controller::class)) { $webhook_url = \ESI_PEPPOL\controllers\PEPPOL_Webhook_controller::get_webhook_url(); $status['webhook']['value'] = $webhook_url; } // Vérifier l'email $email = (string) get_option('esi_peppol_email', ''); if ($email !== '') { $status['email']['completed'] = true; $status['email']['value'] = $email; } return $status; } public static function write_log($message, $level = 'INFO') { $message = self::format_message_for_log($message); $log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message); error_log($log_entry); } /** * Écrit un message dans un fichier debug.txt sous le dossier uploads * Chemin: wp-content/uploads/crvi-debug.txt */ public static function write_debug_file($message, $level = 'INFO') { $message = self::format_message_for_log($message); $log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message); $upload_dir = wp_upload_dir(); $file_path = trailingslashit($upload_dir['basedir']) . 'crvi-debug.txt'; // Tente d'écrire le log. En cas d'échec, fallback sur error_log @file_put_contents($file_path, $log_entry, FILE_APPEND); // Écrire également dans le log PHP (wp-content/debug.log si WP_DEBUG_LOG actif, sinon error_log du serveur) error_log($log_entry); } private static function format_message_for_log($message) { if (is_array($message)) { return print_r($message, true); } elseif (is_object($message)) { return print_r($message, true); } elseif (is_bool($message)) { return $message ? 'TRUE' : 'FALSE'; } elseif (is_numeric($message)) { return (string) $message; } elseif (is_null($message)) { return 'NULL'; } else { return var_export($message, true); } } /** * Envoie un email de notification en cas d'erreur API Peppol. * * @param int $order_id ID de la commande WooCommerce. * @param array $api_result Résultat de l'appel API (success, http_code, message, data). * @param array $payload Payload envoyé à l'API (optionnel, pour debug). * * @return bool True si l'email a été envoyé avec succès, false sinon. */ public static function send_api_error_email(int $order_id, array $api_result, array $payload = []): bool { // Vérifier si l'email est configuré $email = \get_option('esi_peppol_email', ''); if (empty($email) || !\is_email($email)) { return false; } // Récupérer la commande $order = \wc_get_order($order_id); if (!$order instanceof \WC_Order) { return false; } // Préparer les variables pour le template $order_number = $order->get_order_number(); $http_code = isset($api_result['http_code']) ? (int) $api_result['http_code'] : 0; $error_message = isset($api_result['message']) && $api_result['message'] !== '' ? (string) $api_result['message'] : \__('Erreur inconnue lors de l\'envoi vers l\'API Peppol.', 'esi_peppol'); // Extraire les données d'erreur supplémentaires $error_data = []; if (isset($api_result['data']) && is_array($api_result['data'])) { $error_data = $api_result['data']; } // Si la structure error existe directement dans api_result, l'inclure aussi if (isset($api_result['error']) && is_array($api_result['error'])) { $error_data['error'] = $api_result['error']; // Si le message d'erreur n'est pas défini, utiliser celui de error.message if (empty($error_message) && isset($api_result['error']['message'])) { $error_message = (string) $api_result['error']['message']; } } // Récupérer le template email $template_path = ESI_PEPPOL_DIR . 'templates/email/api-error.php'; if (!file_exists($template_path)) { return false; } // Capturer le contenu du template ob_start(); include $template_path; $email_body = ob_get_clean(); // Préparer les en-têtes de l'email $site_name = \get_bloginfo('name'); $headers = [ 'Content-Type: text/html; charset=UTF-8', 'From: ' . $site_name . ' <' . \get_option('admin_email') . '>', ]; // Sujet de l'email $subject = \sprintf( /* translators: %1$s: site name, %2$s: order number */ \__('[%1$s] Erreur API Peppol - Commande %2$s', 'esi_peppol'), $site_name, $order_number ); // Envoyer l'email $sent = \wp_mail($email, $subject, $email_body, $headers); // Log de l'envoi if ($sent) { self::write_debug_file( [ 'event' => 'api_error_email_sent', 'order_id' => $order_id, 'email' => $email, 'http_code' => $http_code, 'error_message' => $error_message, ], 'INFO' ); } else { self::write_debug_file( [ 'event' => 'api_error_email_failed', 'order_id' => $order_id, 'email' => $email, 'error' => 'wp_mail returned false', ], 'ERROR' ); } return $sent; } }