Plugin Directory

Changeset 3698250


Ignore:
Timestamp:
09/16/2026 09:19:10 AM (6 weeks ago)
Author:
abdullahalmulhim1
Message:

Release 1.1.0: rewrite listing copy around merchant outcomes, add FAQ, add settings Test Connection button and order-screen status panel, correct documented retry/default-invoice behavior

Location:
saudi-vat-pro-for-woocommerce/trunk
Files:
2 added
5 edited

Legend:

Unmodified
Added
Removed
  • saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-admin.php

    r3677646 r3698250  
    1010        add_action( 'woocommerce_settings_tabs_saudi_vat_pro', [ __CLASS__, 'render_settings_tab' ] );
    1111        add_action( 'woocommerce_update_options_saudi_vat_pro', [ __CLASS__, 'save_settings' ] );
     12        add_action( 'woocommerce_admin_field_saudivatpro_test_connection', [ __CLASS__, 'render_test_connection_field' ] );
     13        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_settings_assets' ] );
     14        add_action( 'wp_ajax_saudivatpro_wc_test_connection', [ __CLASS__, 'ajax_test_connection' ] );
     15    }
     16
     17    /**
     18     * Load the Test Connection button script only on our Settings tab.
     19     */
     20    public static function enqueue_settings_assets() {
     21        $screen = get_current_screen();
     22        if ( ! $screen || 'woocommerce_page_wc-settings' !== $screen->id ) {
     23            return;
     24        }
     25        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab check, no state change.
     26        if ( ! isset( $_GET['tab'] ) || 'saudi_vat_pro' !== sanitize_key( wp_unslash( $_GET['tab'] ) ) ) {
     27            return;
     28        }
     29
     30        wp_enqueue_script(
     31            'saudivatpro-wc-settings',
     32            SAUDIVATPRO_WC_PLUGIN_URL . 'assets/js/settings.js',
     33            [ 'jquery' ],
     34            SAUDIVATPRO_WC_VERSION,
     35            true
     36        );
     37        wp_localize_script( 'saudivatpro-wc-settings', 'SaudiVATProSettings', [
     38            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     39            'nonce'   => wp_create_nonce( 'saudivatpro_wc_test_connection' ),
     40            'i18n'    => [
     41                'testing' => __( 'Testing…', 'saudi-vat-pro-for-woocommerce' ),
     42                'success' => __( 'Connection verified — the webhook URL and secret are valid.', 'saudi-vat-pro-for-woocommerce' ),
     43                'missing' => __( 'Enter the webhook URL and secret above first.', 'saudi-vat-pro-for-woocommerce' ),
     44            ],
     45        ] );
     46    }
     47
     48    /**
     49     * Render the "Test Connection" button and result area as a WooCommerce
     50     * settings field row. Registered via the 'saudivatpro_test_connection'
     51     * custom field type declared in get_settings().
     52     */
     53    public static function render_test_connection_field() {
     54        ?>
     55        <tr valign="top">
     56            <th scope="row" class="titledesc"></th>
     57            <td class="forminp">
     58                <button type="button" class="button" id="saudivatpro-wc-test-connection">
     59                    <?php esc_html_e( 'Test Connection', 'saudi-vat-pro-for-woocommerce' ); ?>
     60                </button>
     61                <span id="saudivatpro-wc-test-connection-result" style="margin-left:10px;"></span>
     62                <p class="description">
     63                    <?php esc_html_e( 'Verifies the webhook URL and secret above without creating a test invoice or touching a live order.', 'saudi-vat-pro-for-woocommerce' ); ?>
     64                </p>
     65            </td>
     66        </tr>
     67        <?php
     68    }
     69
     70    /**
     71     * AJAX handler for the Test Connection button. Verifies the currently
     72     * entered (not necessarily saved) webhook URL and secret against the
     73     * Saudi VAT Pro API's connection-verification endpoint.
     74     */
     75    public static function ajax_test_connection() {
     76        check_ajax_referer( 'saudivatpro_wc_test_connection', 'nonce' );
     77        if ( ! current_user_can( 'manage_woocommerce' ) ) {
     78            wp_send_json_error( [ 'message' => __( 'You do not have permission to do this.', 'saudi-vat-pro-for-woocommerce' ) ], 403 );
     79        }
     80
     81        $webhook_url = isset( $_POST['webhook_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_url'] ) ) : '';
     82        $secret      = isset( $_POST['secret'] ) ? sanitize_text_field( wp_unslash( $_POST['secret'] ) ) : '';
     83
     84        $result = SaudiVATPro_WC_API_Client::test_connection( $webhook_url, $secret );
     85
     86        if ( is_wp_error( $result ) ) {
     87            wp_send_json_error( [ 'message' => $result->get_error_message() ] );
     88        }
     89
     90        wp_send_json_success( [ 'message' => __( 'Connection verified — the webhook URL and secret are valid.', 'saudi-vat-pro-for-woocommerce' ) ] );
    1291    }
    1392
    … …  
    57136            ],
    58137            [
     138                'type' => 'saudivatpro_test_connection',
     139                'id'   => 'saudivatpro_wc_test_connection',
     140            ],
     141            [
    59142                'title'    => __( 'Show VAT Number field at checkout', 'saudi-vat-pro-for-woocommerce' ),
    60143                'type'     => 'checkbox',
  • saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-api-client.php

    r3677646 r3698250  
    264264    }
    265265
     266    /**
     267     * Verify a webhook URL + secret pair without creating any invoice, order,
     268     * or credit-note record. Used by the Settings screen's "Test Connection"
     269     * button so merchants can confirm setup before placing a real order.
     270     *
     271     * @param string $webhook_url Candidate webhook URL (may be unsaved).
     272     * @param string $secret      Candidate webhook secret (may be unsaved).
     273     * @return true|WP_Error
     274     */
     275    public static function test_connection( $webhook_url, $secret ) {
     276        $webhook_url = esc_url_raw( $webhook_url );
     277        if ( ! self::is_valid_webhook_url( $webhook_url ) ) {
     278            return new WP_Error( 'saudivatpro_wc_not_configured', __( 'Enter a valid HTTPS Saudi VAT Pro webhook URL before testing.', 'saudi-vat-pro-for-woocommerce' ) );
     279        }
     280
     281        $secret = sanitize_text_field( $secret );
     282        if ( '' === $secret ) {
     283            return new WP_Error( 'saudivatpro_wc_not_configured', __( 'Enter the webhook secret before testing.', 'saudi-vat-pro-for-woocommerce' ) );
     284        }
     285
     286        $url  = rtrim( $webhook_url, '/' ) . '/test-connection';
     287        $body = wp_json_encode( [ 'test' => true, 'site' => get_site_url() ] );
     288
     289        $headers = [
     290            'Content-Type'         => 'application/json',
     291            'X-SaudiVATPro-Source' => 'woocommerce/' . SAUDIVATPRO_WC_VERSION,
     292        ];
     293        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- benign: base64 is the wire format for an HMAC-SHA256 webhook signature header, not obfuscation.
     294        $headers['X-WC-Webhook-Signature'] = base64_encode( hash_hmac( 'sha256', $body, $secret, true ) );
     295
     296        $response = wp_remote_post( $url, [
     297            'headers'     => $headers,
     298            'body'        => $body,
     299            'timeout'     => 15,
     300            'data_format' => 'body',
     301        ] );
     302
     303        if ( is_wp_error( $response ) ) {
     304            return $response;
     305        }
     306
     307        $code = wp_remote_retrieve_response_code( $response );
     308        $data = json_decode( wp_remote_retrieve_body( $response ), true );
     309
     310        if ( $code < 200 || $code >= 300 ) {
     311            $message = isset( $data['message'] )
     312                ? sanitize_text_field( $data['message'] )
     313                : sprintf(
     314                    /* translators: %d: HTTP status code returned by the Saudi VAT Pro API. */
     315                    __( 'Saudi VAT Pro returned HTTP %d.', 'saudi-vat-pro-for-woocommerce' ),
     316                    $code
     317                );
     318            return new WP_Error( 'saudivatpro_wc_api_error', $message );
     319        }
     320
     321        return true;
     322    }
     323
    266324    private static function validate_invoice_response( $response ) {
    267325        if ( is_wp_error( $response ) ) {
  • saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-order-handler.php

    r3677646 r3698250  
    2626        add_action( 'saudivatpro_wc_retry_submission', [ __CLASS__, 'retry_submission' ] );
    2727        add_action( 'saudivatpro_wc_retry_refund_submission', [ __CLASS__, 'retry_refund_submission' ], 10, 2 );
     28        // Order edit screen status panel (classic and HPOS).
     29        add_action( 'add_meta_boxes', [ __CLASS__, 'register_status_meta_box' ] );
     30        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_order_assets' ] );
     31        add_action( 'wp_ajax_saudivatpro_wc_retry_order', [ __CLASS__, 'ajax_retry_order' ] );
     32    }
     33
     34    /**
     35     * The order edit screen ID differs between classic (post-based) orders and
     36     * HPOS custom-table orders. wc_get_page_screen_id() (WC 8.0+) returns the
     37     * correct one; older WooCommerce versions register on both since we cannot
     38     * otherwise detect which storage is active.
     39     *
     40     * @return string[]
     41     */
     42    private static function get_order_screen_ids() {
     43        if ( function_exists( 'wc_get_page_screen_id' ) ) {
     44            return [ wc_get_page_screen_id( 'shop-order' ) ];
     45        }
     46        return [ 'shop_order', 'woocommerce_page_wc-orders' ];
     47    }
     48
     49    /**
     50     * Register the "Saudi VAT Pro" status panel meta box on the order edit screen.
     51     */
     52    public static function register_status_meta_box() {
     53        add_meta_box(
     54            'saudivatpro-wc-status',
     55            __( 'Saudi VAT Pro', 'saudi-vat-pro-for-woocommerce' ),
     56            [ __CLASS__, 'render_status_meta_box' ],
     57            self::get_order_screen_ids(),
     58            'side',
     59            'high'
     60        );
     61    }
     62
     63    /**
     64     * Load the retry-button script only on the order edit screen.
     65     */
     66    public static function enqueue_order_assets() {
     67        $screen = get_current_screen();
     68        if ( ! $screen || ! in_array( $screen->id, self::get_order_screen_ids(), true ) ) {
     69            return;
     70        }
     71
     72        wp_enqueue_script(
     73            'saudivatpro-wc-order-status',
     74            SAUDIVATPRO_WC_PLUGIN_URL . 'assets/js/order-status.js',
     75            [ 'jquery' ],
     76            SAUDIVATPRO_WC_VERSION,
     77            true
     78        );
     79        wp_localize_script( 'saudivatpro-wc-order-status', 'SaudiVATProOrderStatus', [
     80            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     81            'nonce'   => wp_create_nonce( 'saudivatpro_wc_retry_order' ),
     82            'i18n'    => [
     83                'retrying' => __( 'Retrying…', 'saudi-vat-pro-for-woocommerce' ),
     84                'failed'   => __( 'Retry failed. Check the error message above.', 'saudi-vat-pro-for-woocommerce' ),
     85            ],
     86        ] );
     87    }
     88
     89    /**
     90     * Meta box callback. WooCommerce passes a WP_Post on classic order-admin
     91     * screens and the WC_Order object itself on HPOS screens.
     92     *
     93     * @param WP_Post|WC_Order $post_or_order
     94     */
     95    public static function render_status_meta_box( $post_or_order ) {
     96        $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order );
     97        if ( ! $order ) {
     98            esc_html_e( 'Order not found.', 'saudi-vat-pro-for-woocommerce' );
     99            return;
     100        }
     101
     102        $data = self::get_status_panel_data( $order );
     103
     104        $status_colors = [
     105            'submitted'     => '#1a7f37',
     106            'failed'        => '#cc1818',
     107            'not_submitted' => '#646970',
     108        ];
     109        $color = isset( $status_colors[ $data['status'] ] ) ? $status_colors[ $data['status'] ] : '#646970';
     110        ?>
     111        <div class="saudivatpro-wc-status-panel" data-order-id="<?php echo esc_attr( $order->get_id() ); ?>">
     112            <p>
     113                <strong><?php esc_html_e( 'Status:', 'saudi-vat-pro-for-woocommerce' ); ?></strong>
     114                <span class="saudivatpro-wc-status-label" style="color: <?php echo esc_attr( $color ); ?>; font-weight:600;">
     115                    <?php echo esc_html( $data['status_label'] ); ?>
     116                </span>
     117            </p>
     118            <?php if ( $data['invoice_id'] ) : ?>
     119                <p>
     120                    <strong><?php esc_html_e( 'Invoice ID:', 'saudi-vat-pro-for-woocommerce' ); ?></strong>
     121                    <?php echo esc_html( $data['invoice_id'] ); ?>
     122                </p>
     123            <?php endif; ?>
     124            <?php if ( $data['pdf_url'] ) : ?>
     125                <p>
     126                    <a href="<?php echo esc_url( $data['pdf_url'] ); ?>" target="_blank" rel="noopener noreferrer" class="button">
     127                        <?php esc_html_e( 'View Invoice PDF', 'saudi-vat-pro-for-woocommerce' ); ?>
     128                    </a>
     129                </p>
     130            <?php endif; ?>
     131            <?php if ( $data['error'] ) : ?>
     132                <p class="saudivatpro-wc-status-error-message" style="color: #cc1818;">
     133                    <strong><?php esc_html_e( 'Last error:', 'saudi-vat-pro-for-woocommerce' ); ?></strong><br />
     134                    <?php echo esc_html( $data['error'] ); ?>
     135                </p>
     136            <?php endif; ?>
     137            <?php if ( ! $data['invoice_id'] ) : ?>
     138                <p>
     139                    <button type="button" class="button saudivatpro-wc-retry-button">
     140                        <?php esc_html_e( 'Retry submission', 'saudi-vat-pro-for-woocommerce' ); ?>
     141                    </button>
     142                    <span class="saudivatpro-wc-retry-result" style="margin-left:8px;"></span>
     143                </p>
     144            <?php endif; ?>
     145        </div>
     146        <?php
     147    }
     148
     149    /**
     150     * Collect the status-panel fields for one order: not yet submitted,
     151     * submitted, or failed, with invoice ID, PDF link, and last error.
     152     *
     153     * @param WC_Order $order
     154     * @return array
     155     */
     156    private static function get_status_panel_data( $order ) {
     157        $invoice_id = saudivatpro_wc_get_order_meta( $order, 'invoice_id' );
     158        $pdf_url    = saudivatpro_wc_get_order_meta( $order, 'invoice_url' );
     159        $error      = saudivatpro_wc_get_order_meta( $order, 'error' );
     160
     161        if ( $invoice_id ) {
     162            $status       = 'submitted';
     163            $status_label = __( 'Submitted', 'saudi-vat-pro-for-woocommerce' );
     164        } elseif ( $error ) {
     165            $status       = 'failed';
     166            $status_label = __( 'Failed', 'saudi-vat-pro-for-woocommerce' );
     167        } else {
     168            $status       = 'not_submitted';
     169            $status_label = __( 'Not yet submitted', 'saudi-vat-pro-for-woocommerce' );
     170        }
     171
     172        return [
     173            'status'       => $status,
     174            'status_label' => $status_label,
     175            'invoice_id'   => $invoice_id,
     176            'pdf_url'      => $pdf_url,
     177            'error'        => $error,
     178        ];
     179    }
     180
     181    /**
     182     * AJAX handler for the status panel's "Retry submission" button. Reuses
     183     * admin_retry_action(), which already refuses to resubmit an order that
     184     * has an invoice, so this stays safe against duplicate submissions.
     185     */
     186    public static function ajax_retry_order() {
     187        check_ajax_referer( 'saudivatpro_wc_retry_order', 'nonce' );
     188        if ( ! current_user_can( 'edit_shop_orders' ) ) {
     189            wp_send_json_error( [ 'message' => __( 'You do not have permission to do this.', 'saudi-vat-pro-for-woocommerce' ) ], 403 );
     190        }
     191
     192        $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0;
     193        $order    = $order_id ? wc_get_order( $order_id ) : false;
     194        if ( ! $order ) {
     195            wp_send_json_error( [ 'message' => __( 'Order not found.', 'saudi-vat-pro-for-woocommerce' ) ], 404 );
     196        }
     197
     198        self::admin_retry_action( $order );
     199
     200        $order = wc_get_order( $order_id );
     201        wp_send_json_success( self::get_status_panel_data( $order ) );
    28202    }
    29203
    … …  
    76250            $order->update_meta_data( '_saudivatpro_wc_invoice_id', sanitize_text_field( $result['invoiceId'] ) );
    77251        }
    78         if ( isset( $result['pdfUrl'] ) ) {
    79             $order->update_meta_data( '_saudivatpro_wc_invoice_url', esc_url_raw( $result['pdfUrl'] ) );
     252        $invoice_url = isset( $result['pdfUrl'] ) && is_string( $result['pdfUrl'] ) ? esc_url_raw( $result['pdfUrl'] ) : '';
     253        if ( $invoice_url ) {
     254            // The API returns a customer-safe, expiring document URL. Keep it on the
     255            // order using the established invoice URL meta key.
     256            $order->update_meta_data( '_saudivatpro_wc_invoice_url', $invoice_url );
    80257        }
    81258        $order->delete_meta_data( '_saudivatpro_wc_error' );
    … …  
    147324
    148325        $state['credit_note_id'] = sanitize_text_field( $result['invoiceId'] );
    149         $state['pdf_url']        = isset( $result['pdfUrl'] ) ? esc_url_raw( $result['pdfUrl'] ) : '';
     326        // Preserve one customer-safe URL per refund state so partial refunds retain
     327        // their own credit-note documents.
     328        $state['pdf_url']        = isset( $result['pdfUrl'] ) && is_string( $result['pdfUrl'] ) ? esc_url_raw( $result['pdfUrl'] ) : '';
    150329        $state['error']          = '';
    151330        $state['retry_count']    = 0;
    … …  
    206385
    207386    /**
    208      * Show "Download Tax Invoice" button on My Account → Order details.
     387     * Return the customer-visible invoice and credit-note documents on an order.
     388     *
     389     * @return array
     390     */
     391    private static function get_customer_documents( $order ) {
     392        $documents  = [];
     393        $invoice_url = esc_url_raw( (string) saudivatpro_wc_get_order_meta( $order, 'invoice_url' ) );
     394        if ( $invoice_url ) {
     395            $documents[] = [
     396                'label' => __( 'Tax Invoice (PDF)', 'saudi-vat-pro-for-woocommerce' ),
     397                'url'   => $invoice_url,
     398            ];
     399        }
     400
     401        $states = $order->get_meta( '_saudivatpro_wc_refund_states', true );
     402        $states = is_array( $states ) ? $states : [];
     403        foreach ( $states as $refund_id => $state ) {
     404            if ( ! is_array( $state ) ) {
     405                continue;
     406            }
     407            $credit_note_url = isset( $state['pdf_url'] ) && is_string( $state['pdf_url'] ) ? esc_url_raw( $state['pdf_url'] ) : '';
     408            if ( ! $credit_note_url || ! absint( $refund_id ) ) {
     409                continue;
     410            }
     411            $documents[] = [
     412                /* translators: %d: WooCommerce refund ID. */
     413                'label' => sprintf( __( 'Credit Note for Refund #%d (PDF)', 'saudi-vat-pro-for-woocommerce' ), absint( $refund_id ) ),
     414                'url'   => $credit_note_url,
     415            ];
     416        }
     417
     418        return $documents;
     419    }
     420
     421    /**
     422     * Show customer documents on the authorized My Account order details page.
    209423     */
    210424    public static function show_invoice_link( $order ) {
    211         $url = saudivatpro_wc_get_order_meta( $order, 'invoice_url' );
    212         if ( ! $url ) {
    213             return;
    214         }
    215 
    216         echo '<section class="woocommerce-order-saudivatpro-invoice" style="margin-top:20px;">';
    217         echo '<h2 class="woocommerce-order-details__title">' . esc_html__( 'Tax Invoice', 'saudi-vat-pro-for-woocommerce' ) . '</h2>';
    218         echo '<p>' . esc_html__( 'Your ZATCA-compliant tax invoice is ready.', 'saudi-vat-pro-for-woocommerce' ) . '</p>';
    219         echo '<a href="' . esc_url( $url ) . '" class="button" download>';
    220         echo esc_html__( 'Download Tax Invoice (PDF)', 'saudi-vat-pro-for-woocommerce' );
    221         echo '</a>';
     425        $documents = self::get_customer_documents( $order );
     426        if ( empty( $documents ) ) {
     427            return;
     428        }
     429
     430        echo '<section class="woocommerce-order-saudivatpro-documents" style="margin-top:20px;">';
     431        echo '<h2 class="woocommerce-order-details__title">' . esc_html__( 'Documents', 'saudi-vat-pro-for-woocommerce' ) . '</h2>';
     432        echo '<ul>';
     433        foreach ( $documents as $document ) {
     434            echo '<li><a href="' . esc_url( $document['url'] ) . '" class="button" download>';
     435            echo esc_html( $document['label'] );
     436            echo '</a></li>';
     437        }
     438        echo '</ul>';
    222439        echo '</section>';
    223440    }
    … …  
    228445    // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $email is unused but required so this matches the 4-argument woocommerce_email_after_order_table hook signature.
    229446    public static function add_invoice_link_to_email( $order, $sent_to_admin, $plain_text, $email ) {
    230         $url = saudivatpro_wc_get_order_meta( $order, 'invoice_url' );
    231         if ( ! $url ) {
     447        if ( $sent_to_admin ) {
     448            return;
     449        }
     450        $documents = self::get_customer_documents( $order );
     451        if ( empty( $documents ) ) {
    232452            return;
    233453        }
    234454
    235455        if ( $plain_text ) {
    236             echo "\n" . esc_html__( 'Tax Invoice (ZATCA):', 'saudi-vat-pro-for-woocommerce' ) . ' ' . esc_url( $url ) . "\n";
     456            echo "\n" . __( 'Documents:', 'saudi-vat-pro-for-woocommerce' ) . "\n";
     457            foreach ( $documents as $document ) {
     458                echo wp_strip_all_tags( $document['label'] ) . ': ' . $document['url'] . "\n";
     459            }
    237460        } else {
    238             echo '<p style="margin:20px 0 0;">';
    239             echo '<strong>' . esc_html__( 'Your ZATCA Tax Invoice:', 'saudi-vat-pro-for-woocommerce' ) . '</strong> ';
    240             echo '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Download PDF', 'saudi-vat-pro-for-woocommerce' ) . '</a>';
    241             echo '</p>';
     461            echo '<div style="margin:20px 0 0;">';
     462            echo '<strong>' . esc_html__( 'Documents:', 'saudi-vat-pro-for-woocommerce' ) . '</strong>';
     463            echo '<ul>';
     464            foreach ( $documents as $document ) {
     465                echo '<li><a href="' . esc_url( $document['url'] ) . '">' . esc_html( $document['label'] ) . '</a></li>';
     466            }
     467            echo '</ul>';
     468            echo '</div>';
    242469        }
    243470    }
  • saudi-vat-pro-for-woocommerce/trunk/readme.txt

    r3677646 r3698250  
    44Requires at least: 6.0
    55Tested up to: 7.1
    6 Stable tag: 1.0.1
     6Stable tag: 1.1.0
    77Requires PHP: 8.0
    88WC requires at least: 7.0
    … …  
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1212
    13 Automatically send paid store orders and refunds to Saudi VAT Pro for electronic invoicing in Saudi Arabia.
     13Automatic ZATCA-compliant invoices for every paid order, a customer PDF link, and credit notes on refund — no manual invoicing.
    1414
    1515== Description ==
    1616
    17 Saudi VAT Pro sends paid orders and refunds to your Saudi VAT Pro WooCommerce integration endpoint. It can collect an optional 15-digit Saudi buyer VAT number for B2B invoices, show a returned PDF link, and submit a credit note for each refund.
     17Saudi VAT Pro turns every paid WooCommerce order into a ZATCA-compliant tax invoice automatically. The customer gets a link to download their invoice PDF, and refunds generate a matching credit note — no manual invoicing, no exporting orders to a spreadsheet.
     18
     19= How it works =
     20
     21* **Trigger:** as soon as an order's payment is confirmed — either through your payment gateway's payment-complete event, or when the order status changes to Processing or Completed — the plugin sends it to your connected Saudi VAT Pro account.
     22* **Customer experience:** with a ZATCA certificate (CSID) enrolled, Saudi VAT Pro generates the invoice signed, hashed, and QR-coded, then submits it to ZATCA for reporting or clearance. The PDF link is returned to the order — and made available to the customer — as soon as that processing finishes, whatever the resulting ZATCA outcome; a rejected submission or a missing certificate is flagged in your Saudi VAT Pro dashboard rather than blocking the order.
     23* **Refunds:** when you refund an order, in full or in part, the plugin submits the refunded items to Saudi VAT Pro, which issues a matching ZATCA credit note referencing the original invoice.
     24* **B2B vs. B2C:** business buyers can enter their 15-digit Saudi VAT number at checkout to receive a Standard (B2B) tax invoice; buyers who leave it blank get the invoice type you've set as the account default (usually Simplified/B2C, but configurable to Standard) in Saudi VAT Pro.
     25
     26= Connect your store =
     27
     28The plugin connects to Saudi VAT Pro using a webhook URL and a secret generated from your Saudi VAT Pro account — there is no API key to manage. Connect your store in two steps, then place a test order:
     29
     301. In Saudi VAT Pro, go to Settings → Integrations → WooCommerce and copy the Webhook URL and Webhook Secret.
     312. In WordPress, go to WooCommerce → Settings → Saudi VAT Pro, paste both values, and save.
     32
     33= ZATCA scope =
     34
     35This plugin covers ZATCA Phase 2 (Integration Phase) e-invoicing. With a ZATCA certificate (CSID) enrolled in your Saudi VAT Pro account, every invoice is generated as signed, hashed, QR-coded UBL XML, then Simplified (B2C) invoices are reported to ZATCA and Standard (B2B) invoices are submitted for clearance. Without a certificate enrolled, invoices are still generated (with a Phase 1 QR code) but are not signed, reported, or cleared until you enroll one. This plugin does not cover Phase 1-only (generation without reporting) setups as its intended operating mode.
    1836
    1937= Requirements =
    2038
    2139* WordPress 6.0+, PHP 8.0+, and WooCommerce 7.0+.
    22 * An active Saudi VAT Pro account at https://saudivatpro.com.
     40* An active Saudi VAT Pro account at https://saudivatpro.com with a ZATCA certificate (CSID) enrolled — without one, invoices are still generated (with a Phase 1 QR code) but are not signed, reported, or cleared.
    2341* A unique HTTPS WooCommerce webhook URL and its secret from Saudi VAT Pro → Settings → Integrations → WooCommerce.
    2442
    … …  
    3250= Configuration and usage =
    3351
    34 The plugin submits paid orders to Saudi VAT Pro for electronic invoicing and stores the returned invoice identifier and PDF link on the WooCommerce order. When a refund is created, the plugin submits the refunded items to create a matching credit note. Failed submissions can be retried from the order screen.
     52The plugin submits paid orders to Saudi VAT Pro for electronic invoicing and stores the returned invoice identifier and PDF link on the WooCommerce order. When a refund is created, the plugin submits the refunded items to create a matching credit note. If the connection to Saudi VAT Pro itself fails before an invoice is created (for example a timeout), the order is retried automatically up to three times, several minutes apart, and can also be retried manually at any time from the order screen using the "Retry Saudi VAT Pro submission" order action. Once an invoice has been created for an order, that order won't be resubmitted from WooCommerce; a ZATCA rejection or a missing certificate at that point is resolved from your Saudi VAT Pro dashboard.
    3553
    3654The classic shortcode checkout is supported. WooCommerce block checkout does not currently expose this plugin's optional VAT field; collect the VAT number by another compatible checkout extension until native block integration is released.
    … …  
    3957
    4058This release supports WordPress 6.0 or newer, PHP 8.0 or newer, and WooCommerce 7.0 or newer. It supports WooCommerce HPOS. The classic shortcode checkout is supported; the optional VAT field is not currently available in WooCommerce block checkout.
     59
     60== Frequently Asked Questions ==
     61
     62= What does this plugin actually do? =
     63
     64It watches your WooCommerce orders. When an order is paid, it sends the order to your Saudi VAT Pro account, which creates the invoice and — once a ZATCA certificate is enrolled — signs it and reports or clears it with ZATCA, then returns a PDF link you can share with the customer. When you refund an order, it does the same to create a matching credit note.
     65
     66= Are invoices generated on my WordPress server, or by the Saudi VAT Pro service? =
     67
     68By the Saudi VAT Pro service. This plugin only collects order and refund data and sends it, over your configured webhook, to your Saudi VAT Pro account. Invoice generation, XML signing, and ZATCA submission all happen there — none of that logic runs on your WordPress site.
     69
     70= Are refunds supported? =
     71
     72Yes, both full and partial refunds. When you create a refund in WooCommerce, the plugin sends the refunded line items to Saudi VAT Pro, which issues a ZATCA credit note referencing the original invoice for that exact amount.
     73
     74= What ZATCA phase and scope does this cover? =
     75
     76ZATCA Phase 2 (Integration Phase). With a ZATCA certificate (CSID) enrolled, every invoice is generated as signed, hashed, QR-coded UBL XML, then Simplified (B2C) invoices are reported to ZATCA and Standard (B2B) invoices are submitted for clearance. Without a certificate enrolled, invoices are still generated with a Phase 1 QR code but are not signed, reported, or cleared. It does not cover Phase 1-only (generation without reporting) setups as its intended operating mode. The PDF link is returned to the order as soon as this processing finishes, whatever the resulting outcome — a rejected submission or a missing certificate is flagged in your dashboard instead of failing silently.
     77
     78= What is the difference between a Simplified and a Standard (B2B) invoice? =
     79
     80A Simplified invoice is issued to a consumer buyer and reported to ZATCA. A Standard (B2B) invoice is issued to a business buyer who provides their 15-digit Saudi VAT number at checkout, and it is submitted to ZATCA for clearance instead of reporting. Enable the optional VAT number field in the plugin settings so business buyers can request a Standard invoice; orders without a VAT number get whichever invoice type you've set as the account default in Saudi VAT Pro (usually Simplified, but this is configurable to Standard).
     81
     82= What happens if a submission to ZATCA fails? =
     83
     84It depends on where the failure happens. If the connection to Saudi VAT Pro itself fails before an invoice is created (for example a timeout), the plugin retries automatically up to three times, several minutes apart, and you can also retry manually using the "Retry Saudi VAT Pro submission" action on the order screen — that action only resends orders that don't have an invoice yet. If Saudi VAT Pro creates the invoice but ZATCA then rejects it, or no certificate is enrolled yet, the order already has its invoice ID and PDF; that failure is flagged in your Saudi VAT Pro dashboard, where you fix and resubmit it.
     85
     86= Does this work with WooCommerce block checkout? =
     87
     88Not yet for the optional VAT number field — it currently only appears on the classic shortcode checkout. Orders placed through block checkout are still invoiced automatically using your account's default invoice type; business buyers checking out through blocks should use another compatible field to submit their VAT number until native block support ships.
    4189
    4290== Privacy Policy ==
    … …  
    5098== Screenshots ==
    5199
    52 No screenshots are included in this release.
     1001. The Saudi VAT Pro settings screen in WooCommerce → Settings, showing the Webhook URL and Webhook Secret connection fields.
     1012. The WooCommerce admin order screen showing the buyer's VAT number and the "Retry Saudi VAT Pro submission" order action.
     1023. The customer-facing order details page "Documents" section, showing the downloadable Tax Invoice PDF link.
     1034. The customer-facing order details page "Documents" section after a refund, showing the downloadable Credit Note PDF link.
    53104
    54105== Changelog ==
     106
     107= 1.1.0 =
     108* Added a "Test Connection" button on Settings that verifies the webhook URL and secret without creating a test invoice or touching a live order.
     109* Added a Saudi VAT Pro status panel on the order edit screen (classic and HPOS) showing submission status, invoice ID, PDF link, and last error, with a retry button.
     110* Rewrote the listing copy to lead with the merchant outcome — automatic ZATCA-compliant invoices, customer PDF access, and refund credit notes — instead of the webhook mechanism.
     111* Documented the exact ZATCA Phase 2 scope, including the difference between behavior with and without a ZATCA certificate (CSID) enrolled, and clarified that the PDF link is returned as soon as processing finishes, independent of the resulting outcome.
     112* Corrected the documented trigger to match the plugin's actual hooks (payment complete, or order status moving to Processing/Completed).
     113* Corrected the documented retry behavior: automatic and manual retries only resend orders that have not yet received an invoice; a ZATCA rejection or missing certificate after that point is resolved from the Saudi VAT Pro dashboard.
     114* Corrected the documented behavior for orders without a buyer VAT number: they use the account's configured default invoice type rather than always being Simplified.
     115* Added a Frequently Asked Questions section.
     116* Added screenshot captions describing the settings, admin order actions, and customer-facing document screens that exist today (image capture tracked separately).
    55117
    56118= 1.0.1 =
  • saudi-vat-pro-for-woocommerce/trunk/saudi-vat-pro-woocommerce.php

    r3677646 r3698250  
    33 * Plugin Name:       Saudi VAT Pro for WooCommerce
    44 * Plugin URI:        https://saudivatpro.com
    5  * Description:       Automatic ZATCA Phase 2 e-invoicing for WooCommerce. Every paid order generates a ZATCA-compliant invoice, B2B buyers enter their VAT number at checkout, and refunds create credit notes — all without lifting a finger.
    6  * Version:           1.0.1
     5 * Description:       Every paid WooCommerce order becomes a ZATCA-compliant invoice automatically — customers get a downloadable PDF, and refunds get a matching credit note. B2B buyers enter their VAT number at checkout. Powered by Saudi VAT Pro's ZATCA Phase 2 reporting and clearance.
     6 * Version:           1.1.0
    77 * Author:            Saudi VAT Pro
    88 * License:           GPL v2 or later
    … …  
    2020}
    2121
    22 define( 'SAUDIVATPRO_WC_VERSION', '1.0.1' );
     22define( 'SAUDIVATPRO_WC_VERSION', '1.1.0' );
    2323define( 'SAUDIVATPRO_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    2424define( 'SAUDIVATPRO_WC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
Note: See TracChangeset for help on using the changeset viewer.