peppol_display_checkout_block_fields(); $this->peppol_set_checkout_block_fields_value(); add_action( 'woocommerce_set_additional_field_value', array( $this, 'peppol_save_checkout_block_fields' ), 10, 4 ); add_action( 'woocommerce_store_api_checkout_order_processed', array( $this, 'peppol_remove_order_checkout_block_fields_meta' ), 10, 1 ); // Enqueue scripts for both classic and block checkout. add_action( 'wp_enqueue_scripts', array( $this, 'peppol_enqueue_checkout_scripts' ), 20 ); } /** * Add EDI Peppol Settings user account menu item * * @param array $items * @param array $endpoints * @return array */ public function peppol_account_menu_item( array $items, array $endpoints ): array { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'my_account' ) ) { return $items; } $last_key = array_key_last( $items ); $position = array_search( $last_key, array_keys( $items ), true ); return array_slice( $items, 0, $position, true ) + array( 'peppol' => 'Peppol' ) + array_slice( $items, $position, null, true ); } /** * Add EDI Peppol Settings to user account page. * * @return void */ public function peppol_settings_account_page(): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'my_account' ) ) { echo '
' . esc_html__( 'Peppol is not available.', 'woocommerce-pdf-invoices-packing-slips' ) . '
'; return; } $user_id = get_current_user_id(); $endpoint_id = (string) get_user_meta( $user_id, 'peppol_endpoint_id', true ); $endpoint_eas = (string) get_user_meta( $user_id, 'peppol_endpoint_eas', true ); $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); $endpoint_id_value = $endpoint_id; $eas_options = array(); // In "full" mode we show scheme:identifier directly in the text inputs. if ( 'full' === $input_mode ) { if ( '' !== $endpoint_eas && '' !== $endpoint_id ) { $endpoint_id_value = "{$endpoint_eas}:{$endpoint_id}"; } // In "select" mode we show the scheme in a dropdown and the identifier in a separate text input. } elseif ( 'select' === $input_mode ) { foreach ( EN16931::get_eas() as $code => $label ) { $eas_options[ $code ] = "[$code] $label"; } } ?> peppol_validate_identifier_value( $request['peppol_endpoint_id'] ); if ( is_wp_error( $result ) ) { wc_add_notice( $result->get_error_message(), 'error' ); return; } } $user_id = get_current_user_id(); wpo_ips_edi_peppol_save_customer_identifiers( $user_id, $request ); wc_add_notice( __( 'Peppol settings saved.', 'woocommerce-pdf-invoices-packing-slips' ), 'success' ); wp_safe_redirect( wc_get_account_endpoint_url( 'peppol' ) ); exit; } /** * Display EDI Peppol fields in the Checkout Block. * * @return void */ public function peppol_display_checkout_block_fields(): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return; } $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); $visibility_mode = $this->peppol_checkout_visibility_mode(); $can_use_hidden = ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9.0', '>=' ) ); // Register toggle field only when configured + supported. if ( $can_use_hidden && 'toggle' === $visibility_mode ) { woocommerce_register_additional_checkout_field( array( 'id' => 'wpo-ips-edi/peppol-invoice', 'label' => __( 'I need a Peppol invoice (business purchase)', 'woocommerce-pdf-invoices-packing-slips' ), 'location' => 'order', 'type' => 'checkbox', 'sanitize_callback' => static function ( $val ) { return (bool) $val; }, 'validate_callback' => static function ( $val ) { return true; }, ) ); } $conditional_hidden = ( $can_use_hidden ) ? $this->peppol_checkout_block_hidden_condition() : array(); // Endpoint ID $args = array( 'id' => 'wpo-ips-edi/peppol-endpoint-id', 'label' => __( 'Peppol identifier', 'woocommerce-pdf-invoices-packing-slips' ), 'location' => 'order', 'type' => 'text', 'sanitize_callback' => static function ( $val ) { return preg_replace( '/\s+/', '', trim( (string) $val ) ); }, 'validate_callback' => function ( $val ) { $result = $this->peppol_validate_identifier_value( (string) $val ); return is_wp_error( $result ) ? $result : true; }, ); if ( ! empty( $conditional_hidden ) ) { $args['hidden'] = $conditional_hidden; } woocommerce_register_additional_checkout_field( $args ); // EAS if ( 'select' === $input_mode ) { $eas = EN16931::get_eas(); $args = array( 'id' => 'wpo-ips-edi/peppol-endpoint-eas', 'label' => __( 'Endpoint Scheme (EAS)', 'woocommerce-pdf-invoices-packing-slips' ), 'location' => 'order', 'type' => 'select', 'options' => array_map( static fn ( $code, $label ) => array( 'value' => $code, 'label' => "[$code] $label", ), array_keys( $eas ), $eas ), 'validate_callback' => static function ( $val ) use ( $eas ) { if ( $val && ! isset( $eas[ $val ] ) ) { return new \WP_Error( 'invalid_eas', __( 'Invalid Endpoint Scheme.', 'woocommerce-pdf-invoices-packing-slips' ) ); } return true; }, ); if ( ! empty( $conditional_hidden ) ) { $args['hidden'] = $conditional_hidden; } woocommerce_register_additional_checkout_field( $args ); } } /** * Set default values for EDI Peppol fields in the Checkout Block. * * @return void */ public function peppol_set_checkout_block_fields_value(): void { $fields = array( 'wpo-ips-edi/peppol-endpoint-id', 'wpo-ips-edi/peppol-endpoint-eas', ); $visibility_mode = $this->peppol_checkout_visibility_mode(); $can_use_hidden = ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '9.9.0', '>=' ) ); if ( $can_use_hidden && 'toggle' === $visibility_mode ) { array_unshift( $fields, 'wpo-ips-edi/peppol-invoice' ); } foreach ( $fields as $field ) { add_filter( "woocommerce_get_default_value_for_{$field}", array( $this, 'peppol_prefill_checkout_block_field_from_user_meta' ), 10, 3 ); } } /** * Provide a default value for the Checkout Block additional field * using the value we store in user meta. * * @param null $value Current default (usually empty). * @param string $group 'billing' | 'shipping' | 'other'. Our field is in the 'order' location, so this will be 'other'. * @param \WC_Data $wc_object Object for which the default is being requested. * * @return string */ public function peppol_prefill_checkout_block_field_from_user_meta( $value, string $group, \WC_Data $wc_object ): string { if ( 'other' !== $group || ! $wc_object instanceof \WC_Customer ) { return (string) $value; } $user_id = $wc_object->get_id(); if ( ! $user_id ) { return (string) $value; } $key = str_replace( 'woocommerce_get_default_value_for_', '', current_filter() ); $meta_key = str_replace( '-', '_', substr( $key, strlen( 'wpo-ips-edi/' ) ) ); // Auto-enable toggle if we already have a stored identifier. if ( 'peppol_invoice' === $meta_key ) { $id = (string) get_user_meta( $user_id, 'peppol_endpoint_id', true ); $has_identifier = ( '' !== $id ); return $has_identifier ? '1' : ''; } $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); // If we’re in 'full' mode, compose scheme:identifier for the *text* fields. if ( 'full' === $input_mode ) { if ( 'peppol_endpoint_id' === $meta_key ) { $id = (string) get_user_meta( $user_id, 'peppol_endpoint_id', true ); $eas = (string) get_user_meta( $user_id, 'peppol_endpoint_eas', true ); return ( '' !== $eas && '' !== $id ) ? "{$eas}:{$id}" : (string) $value; } } // For other fields, just return the user meta value. return (string) get_user_meta( $user_id, $meta_key, true ); } /** * Save EDI Peppol fields from Checkout Block. * * @param string $key Field key. * @param mixed $value Field value. * @param string $group Group name. * @param object $wc_object WC object (e.g. order). * @return void */ public function peppol_save_checkout_block_fields( string $key, $value, string $group, object $wc_object ): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return; } $allowed = array( 'wpo-ips-edi/peppol-endpoint-id', 'wpo-ips-edi/peppol-endpoint-eas', ); if ( ! in_array( $key, $allowed, true ) ) { return; } $meta_key = str_replace( '-', '_', substr( $key, strlen( 'wpo-ips-edi/' ) ) ); $value = trim( sanitize_text_field( wp_unslash( $value ) ) ); if ( empty( $value ) ) { return; } $customer_id = is_callable( array( $wc_object, 'get_customer_id' ) ) ? absint( $wc_object->get_customer_id() ) : 0; wpo_ips_edi_peppol_save_customer_identifiers( $customer_id, array( $meta_key => $value ) ); if ( $wc_object instanceof \WC_Order ) { wpo_ips_edi_maybe_save_order_peppol_data( $wc_object, array( $meta_key => $value ) ); } } /** * Remove EDI Peppol fields from order meta after checkout. * * @param \WC_Abstract_Order $order * @return void */ public function peppol_remove_order_checkout_block_fields_meta( \WC_Abstract_Order $order ): void { $fields = array( 'wpo-ips-edi/peppol-endpoint-id', 'wpo-ips-edi/peppol-endpoint-eas', ); foreach ( $fields as $field ) { $order->delete_meta_data( '_wc_other/' . $field ); } $order->save_meta_data(); } /** * Display EDI Peppol fields on the Classic Checkout page. * * @param mixed $fields Checkout fields. * @return array Modified checkout fields with Peppol fields added. */ public function peppol_display_classic_checkout_fields( $fields ): array { if ( ! is_array( $fields ) ) { $fields = array(); } if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return $fields; } $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); $visibility_mode = $this->peppol_checkout_visibility_mode(); $placeholder_endpoint = ( 'select' !== $input_mode ) ? '0088:123456789' : '123456789'; $peppol_fields = array(); // Toggle checkbox only in toggle mode. if ( 'toggle' === $visibility_mode ) { $peppol_fields['peppol_invoice'] = array( 'type' => 'checkbox', 'label' => __( 'I need a Peppol invoice (business purchase)', 'woocommerce-pdf-invoices-packing-slips' ), 'required' => false, 'class' => array( 'form-row-wide' ), ); } $conditional_class = array( 'form-row-wide' ); if ( 'toggle' === $visibility_mode ) { $conditional_class[] = 'wpo-ips-peppol-conditional'; } elseif ( 'company' === $visibility_mode ) { $conditional_class[] = 'wpo-ips-peppol-company-conditional'; } $peppol_fields['peppol_endpoint_id'] = array( 'type' => 'text', 'label' => __( 'Peppol identifier', 'woocommerce-pdf-invoices-packing-slips' ), 'required' => false, 'class' => $conditional_class, 'placeholder' => $placeholder_endpoint, ); if ( 'select' === $input_mode ) { $peppol_fields['peppol_endpoint_eas'] = array( 'type' => 'select', 'label' => __( 'Peppol Endpoint Scheme (EAS)', 'woocommerce-pdf-invoices-packing-slips' ), 'required' => false, 'class' => $conditional_class, 'options' => ( function () { $options = array( '' => __( 'Select', 'woocommerce-pdf-invoices-packing-slips' ) . '...' ); foreach ( EN16931::get_eas() as $code => $label ) { $options[ $code ] = "[$code] $label"; } return $options; } )(), ); } $fields['order'] = $peppol_fields + ( $fields['order'] ?? array() ); return $fields; } /** * Set EDI Peppol fields values in the Classic Checkout page. * * @param null $value Current value. * @param string $input Input name. * @return mixed Modified value. */ public function peppol_set_classic_checkout_fields_value( $value, string $input ) { if ( ! in_array( $input, array( 'peppol_invoice', 'peppol_endpoint_id', 'peppol_endpoint_eas', ), true ) ) { return $value; } $visibility_mode = $this->peppol_checkout_visibility_mode(); if ( 'peppol_invoice' === $input && 'toggle' !== $visibility_mode ) { return $value; } $user_id = get_current_user_id(); if ( ! $user_id ) { return $value; } $endpoint_id = (string) get_user_meta( $user_id, 'peppol_endpoint_id', true ); $endpoint_eas = (string) get_user_meta( $user_id, 'peppol_endpoint_eas', true ); $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); switch ( $input ) { case 'peppol_invoice': return ( '' !== $endpoint_id ) ? '1' : ''; case 'peppol_endpoint_id': if ( 'full' === $input_mode && '' !== $endpoint_eas && '' !== $endpoint_id ) { return "{$endpoint_eas}:{$endpoint_id}"; } return $endpoint_id; case 'peppol_endpoint_eas': return $endpoint_eas; } return $value; } /** * Validate Peppol Endpoint / Legal‑ID pairs after WooCommerce * has normalised and sanitised all checkout data. * * @param mixed $data All posted checkout fields. * @param mixed $errors Errors object to add validation errors to. * @return void */ public function peppol_validate_classic_checkout_field_values( $data, $errors ): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) || ! $errors instanceof \WP_Error ) { return; } if ( ! is_array( $data ) ) { return; } // Endpoint ID if ( ! empty( $data['peppol_endpoint_id'] ) ) { $result = $this->peppol_validate_identifier_value( $data['peppol_endpoint_id'] ); if ( is_wp_error( $result ) ) { $errors->add( $result->get_error_code(), $result->get_error_message(), array( 'id' => 'peppol_endpoint_id' ) ); } } } /** * Save EDI Peppol fields from Classic Checkout page. * * @param int $order_id Order ID. * @param array $data Checkout data. * @return void */ public function peppol_save_classic_checkout_fields( int $order_id, array $data ): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return; } $visibility_mode = $this->peppol_checkout_visibility_mode(); $has_company = ! empty( $data['billing_company'] ); $has_id = ! empty( $data['peppol_endpoint_id'] ); if ( 'toggle' === $visibility_mode ) { $wants_peppol = ! empty( $data['peppol_invoice'] ) && $has_id; } elseif ( 'company' === $visibility_mode ) { $wants_peppol = $has_company && $has_id; } else { // always $wants_peppol = $has_id; } if ( ! $wants_peppol ) { return; } $order = wc_get_order( $order_id ); if ( empty( $order ) ) { return; } $customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? absint( $order->get_customer_id() ) : 0; wpo_ips_edi_peppol_save_customer_identifiers( $customer_id, $data ); wpo_ips_edi_maybe_save_order_peppol_data( $order, $data ); } /** * Enqueue Peppol scripts on the checkout page. * * @return void */ public function peppol_enqueue_checkout_scripts(): void { if ( is_admin() ) { return; } if ( wpo_ips_current_page_has_checkout_block() ) { $this->peppol_enqueue_block_checkout_script(); return; } if ( wpo_ips_current_page_has_checkout_shortcode() ) { $this->peppol_enqueue_classic_checkout_script(); return; } if ( ! wpo_ips_is_current_page_checkout_page() ) { return; } // In case the page has neither the block nor the shortcode, but is still the assigned checkout page. switch ( wpo_ips_edi_get_settings( 'peppol_checkout_script_type' ) ) { case 'classic': $this->peppol_enqueue_classic_checkout_script(); return; case 'block': $this->peppol_enqueue_block_checkout_script(); return; default: return; } } /** * Register REST API endpoint for Peppol identifier autofill based on VAT number. * * @return void */ public function peppol_register_checkout_autofill_endpoint_route(): void { if ( ! (bool) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation' ) ) { return; } register_rest_route( 'wpo-ips/v1', '/peppol-endpoint', array( 'methods' => 'POST', 'permission_callback' => function( \WP_REST_Request $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return (bool) wp_verify_nonce( $nonce, 'wp_rest' ); }, 'callback' => function( \WP_REST_Request $request ) { $billing_country = wc_strtoupper( wc_clean( (string) $request->get_param( 'billing_country' ) ) ); $value = wc_strtoupper( preg_replace( '/\s+/', '', wc_clean( (string) $request->get_param( 'vat' ) ) ) ); if ( empty( $billing_country ) || empty( $value ) || ! preg_match( '/^[A-Z]{2}$/', $billing_country ) ) { return rest_ensure_response( array( 'eas' => '', 'id' => '', ) ); } $result = wpo_ips_edi_build_peppol_endpoint_from_vat( $billing_country, $value ); if ( empty( $result['endpoint_id'] ) || empty( $result['eas'] ) ) { return rest_ensure_response( array( 'eas' => '', 'id' => '', ) ); } $eas = (string) $result['eas']; $id = (string) $result['endpoint_id']; return rest_ensure_response( array( 'eas' => $eas, 'id' => $id, ) ); }, ) ); } /** * Handle automatic Peppol Endpoint ID derivation on new order creation. * * @param int $order_id * @param \WC_Order $order * @return void */ public function peppol_handle_new_order_automatic_endpoint_id_derivation( int $order_id, $order ): void { if ( ! (bool) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation' ) || wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return; } if ( is_null( $order ) ) { $order = wc_get_order( $order_id ); } // check if we have an order object if ( empty( $order ) ) { return; } $billing_country = $order->get_billing_country(); $vat_number = wpo_wcpdf_get_order_customer_vat_number( $order ); if ( empty( $billing_country ) || empty( $vat_number ) ) { return; } $result = wpo_ips_edi_build_peppol_endpoint_from_vat( $billing_country, $vat_number ); if ( empty( $result['endpoint_id'] ) ) { return; } $validation = $this->peppol_validate_identifier_value( $result['endpoint_id'] ); if ( is_wp_error( $validation ) ) { return; } $data = array( 'peppol_endpoint_id' => $result['endpoint_id'], ); $customer_id = is_callable( array( $order, 'get_customer_id' ) ) ? absint( $order->get_customer_id() ) : 0; wpo_ips_edi_peppol_save_customer_identifiers( $customer_id, $data ); wpo_ips_edi_maybe_save_order_peppol_data( $order, $data ); } /** * Enqueue Peppol script for Classic Checkout page. * * @return void */ private function peppol_enqueue_classic_checkout_script(): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) ) { return; } $script_debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG; $suffix = $script_debug ? '' : '.min'; // Shared styles. wp_enqueue_style( 'wpo-ips-peppol-endpoint-derivation', WPO_WCPDF()->plugin_url() . '/assets/css/peppol-endpoint-derivation' . $suffix . '.css', array(), WPO_WCPDF_VERSION ); // Shared engine. wp_enqueue_script( 'wpo-ips-peppol-endpoint-derivation', WPO_WCPDF()->plugin_url() . '/assets/js/peppol-endpoint-derivation' . $suffix . '.js', array( 'wp-api-fetch' ), // engine uses wp.apiFetch by default WPO_WCPDF_VERSION, true ); // Classic checkout script. wp_enqueue_script( 'wpo-ips-peppol-classic-checkout', WPO_WCPDF()->plugin_url() . '/assets/js/peppol-classic-checkout' . $suffix . '.js', array( 'jquery', 'wpo-ips-peppol-endpoint-derivation', ), WPO_WCPDF_VERSION, true ); wp_localize_script( 'wpo-ips-peppol-classic-checkout', 'wpoIpsPeppol', array( 'visibilityMode' => $this->peppol_checkout_visibility_mode(), // always|toggle|company 'endpoint_derivation' => (bool) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation' ), 'countries' => (array) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation_countries' ), 'debug' => $script_debug, 'billing_country_selector' => apply_filters( 'wpo_ips_edi_peppol_classic_checkout_billing_country_selector', '#billing_country' ), 'peppol_input_wrapper_selector' => apply_filters( 'wpo_ips_edi_peppol_classic_checkout_input_wrapper_selector', '#peppol_endpoint_id_field' ), 'vat_field_selector' => \WPO_WCPDF()->vat_plugins->get_form_selector( 'classic' ), 'peppol_autofill_endpoint_route' => '/wpo-ips/v1/peppol-endpoint', 'override_link_text' => __( 'Override (edit manually)', 'woocommerce-pdf-invoices-packing-slips' ), ) ); } /** * Enqueue Peppol script for Block Checkout page. * * @return void */ private function peppol_enqueue_block_checkout_script(): void { if ( ! wpo_ips_edi_peppol_enabled_for_location( 'checkout' ) || ! (bool) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation' ) ) { return; } $script_debug = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG; $suffix = $script_debug ? '' : '.min'; // Shared styles. wp_enqueue_style( 'wpo-ips-peppol-endpoint-derivation', WPO_WCPDF()->plugin_url() . '/assets/css/peppol-endpoint-derivation' . $suffix . '.css', array(), WPO_WCPDF_VERSION ); // Shared engine. wp_enqueue_script( 'wpo-ips-peppol-endpoint-derivation', WPO_WCPDF()->plugin_url() . '/assets/js/peppol-endpoint-derivation' . $suffix . '.js', array( 'wp-api-fetch' ), WPO_WCPDF_VERSION, true ); // Block checkout script. wp_enqueue_script( 'wpo-ips-peppol-block-checkout', WPO_WCPDF()->plugin_url() . '/assets/js/peppol-block-checkout' . $suffix . '.js', array( 'wp-data', 'wp-dom-ready', 'wpo-ips-peppol-endpoint-derivation', ), WPO_WCPDF_VERSION, true ); wp_localize_script( 'wpo-ips-peppol-block-checkout', 'wpoIpsPeppol', array( 'endpoint_derivation' => true, 'countries' => (array) wpo_ips_edi_get_settings( 'peppol_automatic_endpoint_id_derivation_countries' ), 'debug' => $script_debug, 'billing_country_selector' => apply_filters( 'wpo_ips_edi_peppol_block_checkout_billing_country_selector', '#billing-country, select[name="billing_country"], .wc-block-components-address-form__country select, .wc-block-components-country-input select' ), 'peppol_input_wrapper_selector' => apply_filters( 'wpo_ips_edi_peppol_block_checkout_input_wrapper_selector', '.wc-block-components-address-form__wpo-ips-edi-peppol-endpoint-id' ), 'vat_field_selector' => \WPO_WCPDF()->vat_plugins->get_form_selector( 'block' ), 'peppol_autofill_endpoint_route' => '/wpo-ips/v1/peppol-endpoint', 'override_link_text' => __( 'Override (edit manually)', 'woocommerce-pdf-invoices-packing-slips' ), ) ); } /** * Get the configured visibility mode for the Peppol checkout fields. * * @return string One of: 'always', 'toggle', 'company'. */ private function peppol_checkout_visibility_mode(): string { $visibility_mode = (string) wpo_ips_edi_get_settings( 'peppol_endpoint_id_checkout_visibility' ); $allowed = array( 'always', 'toggle', 'company' ); if ( ! in_array( $visibility_mode, $allowed, true ) ) { $visibility_mode = 'always'; } return $visibility_mode; } /** * Build the Checkout Block "hidden" condition for the Peppol fields. * * @link https://developer.woocommerce.com/docs/block-development/tutorials/how-to-conditional-additional-fields/ * * @return array */ private function peppol_checkout_block_hidden_condition(): array { $visibility_mode = $this->peppol_checkout_visibility_mode(); if ( 'toggle' === $visibility_mode ) { return array( 'checkout' => array( 'properties' => array( 'additional_fields' => array( 'properties' => array( 'wpo-ips-edi/peppol-invoice' => array( 'not' => array( 'const' => true, ), ), ), ), ), ), ); } if ( 'company' === $visibility_mode ) { // Hide while company is empty. return array( 'customer' => array( 'properties' => array( 'billing_address' => array( 'properties' => array( 'company' => array( 'maxLength' => 0, ), ), ), ), ), ); } // always return array(); } /** * Validate a Peppol identifier value. * * @param string $raw_value Raw user input. * @return true|\WP_Error True if valid or should be accepted, WP_Error if invalid. */ private function peppol_validate_identifier_value( string $raw_value ) { $val = preg_replace( '/\s+/', '', trim( (string) $raw_value ) ); // Let "required" or other validation handle this elsewhere. if ( '' === $val ) { return true; } $input_mode = wpo_ips_edi_peppol_identifier_input_mode(); $has_scheme = ( false !== strpos( $val, ':' ) ); $use_directory_validation = (bool) wpo_ips_edi_get_settings( 'peppol_directory_validation' ); $directory_url = 'https://directory.peppol.eu/'; // If input mode is not "full", we do not enforce "scheme:value" here. if ( 'full' !== $input_mode ) { return true; } // Directory validation disabled if ( ! $use_directory_validation ) { if ( ! $has_scheme ) { return new \WP_Error( 'peppol_format_invalid', __( 'The identifier must be in "scheme:value" format (for example 0088:123456789).', 'woocommerce-pdf-invoices-packing-slips' ) ); } return true; } // Directory validation enabled $result = $this->peppol_directory_lookup( $val ); if ( is_wp_error( $result ) ) { if ( 'peppol_empty_endpoint' === $result->get_error_code() ) { return new \WP_Error( 'peppol_empty_endpoint', __( 'Peppol Endpoint ID is empty.', 'woocommerce-pdf-invoices-packing-slips' ) ); } // Network/response errors: do not block checkout. return true; } $matches = isset( $result['matches'] ) && is_array( $result['matches'] ) ? $result['matches'] : array(); $search_meta = isset( $result['search'] ) && is_array( $result['search'] ) ? $result['search'] : array(); $used_fallback = ! empty( $search_meta['used_fallback'] ); /** * No scheme provided (no ":"). * * We always warn that the scheme is required, but still show any found * participants as hints. */ if ( ! $has_scheme ) { $message = sprintf( /* translators: 1: entered identifier, 2: Peppol Directory URL */ __( 'The identifier "%1$s" was found without a scheme. Please enter it in "scheme:value" format. You can search for the correct scheme and identifier in the %2$s.', 'woocommerce-pdf-invoices-packing-slips' ), esc_html( $val ), '' . __( 'Peppol Directory', 'woocommerce-pdf-invoices-packing-slips' ) . '' ); if ( ! empty( $matches ) ) { $message .= $this->peppol_directory_render_matches_list( $matches, $val ); } return new \WP_Error( 'peppol_directory_scheme_required', $message ); } /** * Scheme + value provided. */ // No matches at all (for full query and fallback). if ( empty( $matches ) ) { $message = sprintf( /* translators: 1: entered identifier, 2: Peppol Directory URL */ __( 'No Peppol participant was found for "%1$s". Please confirm the scheme and identifier in the %2$s.', 'woocommerce-pdf-invoices-packing-slips' ), esc_html( $val ), '' . __( 'Peppol Directory', 'woocommerce-pdf-invoices-packing-slips' ) . '' ); return new \WP_Error( 'peppol_directory_no_match', $message ); } // If we did not use fallback, the full "scheme:value" query found matches, accept silently. if ( ! $used_fallback ) { return true; } // We used fallback (value-only), so "scheme:value" had no hits. // Show alternatives based on value-only search and warn about possible wrong scheme. $message = sprintf( /* translators: 1: entered identifier, 2: Peppol Directory URL */ __( 'We could not find a Peppol participant with this scheme and identifier. Please check the scheme or search in the %2$s. Below are participants found for this identifier value:', 'woocommerce-pdf-invoices-packing-slips' ), esc_html( $val ), '' . __( 'Peppol Directory', 'woocommerce-pdf-invoices-packing-slips' ) . '' ); $message .= $this->peppol_directory_render_matches_list( $matches, $val ); return new \WP_Error( 'peppol_directory_similar_found', $message ); } /** * Query the Peppol Directory for an endpoint. * * @param string $endpoint_id * @return array|\WP_Error */ private function peppol_directory_lookup( string $endpoint_id ) { $endpoint_id = trim( (string) $endpoint_id ); if ( '' === $endpoint_id ) { return new \WP_Error( 'peppol_empty_endpoint', __( 'Peppol Endpoint ID is empty.', 'woocommerce-pdf-invoices-packing-slips' ) ); } $has_colon = ( false !== strpos( $endpoint_id, ':' ) ); $primary_query = $endpoint_id; $fallback_query = ''; $used_fallback = false; // If we have "scheme:value", fallback query will be just "value". if ( $has_colon ) { list( , $fallback_query ) = explode( ':', $endpoint_id, 2 ); $fallback_query = trim( $fallback_query ); } // First attempt: full query (can be scheme:value, value, or name). $data = $this->peppol_directory_request( $primary_query ); if ( is_wp_error( $data ) ) { return $data; } $matches = isset( $data['matches'] ) && is_array( $data['matches'] ) ? $data['matches'] : array(); // Fallback: if we had "scheme:value" and got no matches, try "value" only. if ( $has_colon && empty( $matches ) && '' !== $fallback_query ) { $data = $this->peppol_directory_request( $fallback_query ); if ( is_wp_error( $data ) ) { return $data; } $matches = isset( $data['matches'] ) && is_array( $data['matches'] ) ? $data['matches'] : array(); $used_fallback = true; } $normalized_matches = array(); foreach ( $matches as $match ) { $participant_value = $match['participantID']['value'] ?? ''; $entity = isset( $match['entities'][0] ) && is_array( $match['entities'][0] ) ? $match['entities'][0] : array(); $name_entry = isset( $entity['name'][0] ) && is_array( $entity['name'][0] ) ? $entity['name'][0] : array(); $name = $name_entry['name'] ?? ''; $language = $name_entry['language'] ?? ''; $country = $entity['countryCode'] ?? ''; $reg_date = $entity['regDate'] ?? ''; // Collect all identifier values we can find (participant + entity identifiers). $identifier_values = array(); if ( '' !== $participant_value ) { $identifier_values[] = $participant_value; } if ( ! empty( $entity['identifiers'] ) && is_array( $entity['identifiers'] ) ) { foreach ( $entity['identifiers'] as $identifier ) { if ( ! empty( $identifier['value'] ) ) { $identifier_values[] = $identifier['value']; } } } $identifier_values = array_values( array_unique( $identifier_values ) ); $normalized_matches[] = array( 'value' => $participant_value, 'identifiers' => $identifier_values, 'name' => $name, 'language' => $language, 'country' => $country, 'reg_date' => $reg_date, ); } return array( 'total' => isset( $data['total-result-count'] ) ? (int) $data['total-result-count'] : count( $normalized_matches ), 'matches' => $normalized_matches, 'search' => array( 'query' => $primary_query, 'fallback_query' => $fallback_query, 'used_fallback' => $used_fallback, ), ); } /** * Perform a Peppol Directory request using the generic "q" parameter. * * @param string $query * @return array|\WP_Error */ private function peppol_directory_request( string $query ) { $base_url = 'https://directory.peppol.eu/search/1.0/json'; $query_args = array( 'q' => $query, 'beautify' => 'true', ); $url = add_query_arg( $query_args, $base_url ); $response = wp_remote_get( $url, array( 'timeout' => 5, ) ); if ( is_wp_error( $response ) ) { return new \WP_Error( 'peppol_directory_request_failed', sprintf( /* translators: %s: error message */ __( 'Peppol Directory request failed: %s', 'woocommerce-pdf-invoices-packing-slips' ), $response->get_error_message() ) ); } $code = wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( 200 !== $code ) { return new \WP_Error( 'peppol_directory_unexpected_status', sprintf( /* translators: %d: HTTP status code */ __( 'Peppol Directory returned an unexpected status code: %d', 'woocommerce-pdf-invoices-packing-slips' ), $code ) ); } $data = json_decode( $body, true ); if ( null === $data || ! is_array( $data ) ) { return new \WP_Error( 'peppol_directory_invalid_response', __( 'Peppol Directory returned an invalid JSON response.', 'woocommerce-pdf-invoices-packing-slips' ) ); } return $data; } /** * Render Peppol Directory matches as a simple text list. * * @param array $matches * @param string $endpoint * @return string HTML string. */ private function peppol_directory_render_matches_list( array $matches, string $endpoint ): string { if ( empty( $matches ) ) { return ''; } ob_start(); ?>