Changeset 3698250
- Timestamp:
- 09/16/2026 09:19:10 AM (6 weeks ago)
- Location:
- saudi-vat-pro-for-woocommerce/trunk
- Files:
-
- 2 added
- 5 edited
-
assets/js/order-status.js (added)
-
assets/js/settings.js (added)
-
includes/class-svp-admin.php (modified) (2 diffs)
-
includes/class-svp-api-client.php (modified) (1 diff)
-
includes/class-svp-order-handler.php (modified) (5 diffs)
-
readme.txt (modified) (5 diffs)
-
saudi-vat-pro-woocommerce.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-admin.php
r3677646 r3698250 10 10 add_action( 'woocommerce_settings_tabs_saudi_vat_pro', [ __CLASS__, 'render_settings_tab' ] ); 11 11 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' ) ] ); 12 91 } 13 92 … … 57 136 ], 58 137 [ 138 'type' => 'saudivatpro_test_connection', 139 'id' => 'saudivatpro_wc_test_connection', 140 ], 141 [ 59 142 'title' => __( 'Show VAT Number field at checkout', 'saudi-vat-pro-for-woocommerce' ), 60 143 'type' => 'checkbox', -
saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-api-client.php
r3677646 r3698250 264 264 } 265 265 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 266 324 private static function validate_invoice_response( $response ) { 267 325 if ( is_wp_error( $response ) ) { -
saudi-vat-pro-for-woocommerce/trunk/includes/class-svp-order-handler.php
r3677646 r3698250 26 26 add_action( 'saudivatpro_wc_retry_submission', [ __CLASS__, 'retry_submission' ] ); 27 27 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 ) ); 28 202 } 29 203 … … 76 250 $order->update_meta_data( '_saudivatpro_wc_invoice_id', sanitize_text_field( $result['invoiceId'] ) ); 77 251 } 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 ); 80 257 } 81 258 $order->delete_meta_data( '_saudivatpro_wc_error' ); … … 147 324 148 325 $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'] ) : ''; 150 329 $state['error'] = ''; 151 330 $state['retry_count'] = 0; … … 206 385 207 386 /** 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. 209 423 */ 210 424 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>'; 222 439 echo '</section>'; 223 440 } … … 228 445 // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $email is unused but required so this matches the 4-argument woocommerce_email_after_order_table hook signature. 229 446 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 ) ) { 232 452 return; 233 453 } 234 454 235 455 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 } 237 460 } 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>'; 242 469 } 243 470 } -
saudi-vat-pro-for-woocommerce/trunk/readme.txt
r3677646 r3698250 4 4 Requires at least: 6.0 5 5 Tested up to: 7.1 6 Stable tag: 1. 0.16 Stable tag: 1.1.0 7 7 Requires PHP: 8.0 8 8 WC requires at least: 7.0 … … 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html 12 12 13 Automatic ally send paid store orders and refunds to Saudi VAT Pro for electronic invoicing in Saudi Arabia.13 Automatic ZATCA-compliant invoices for every paid order, a customer PDF link, and credit notes on refund — no manual invoicing. 14 14 15 15 == Description == 16 16 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. 17 Saudi 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 28 The 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 30 1. In Saudi VAT Pro, go to Settings → Integrations → WooCommerce and copy the Webhook URL and Webhook Secret. 31 2. In WordPress, go to WooCommerce → Settings → Saudi VAT Pro, paste both values, and save. 32 33 = ZATCA scope = 34 35 This 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. 18 36 19 37 = Requirements = 20 38 21 39 * 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. 23 41 * A unique HTTPS WooCommerce webhook URL and its secret from Saudi VAT Pro → Settings → Integrations → WooCommerce. 24 42 … … 32 50 = Configuration and usage = 33 51 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.52 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. 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. 35 53 36 54 The 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. … … 39 57 40 58 This 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 64 It 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 68 By 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 72 Yes, 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 76 ZATCA 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 80 A 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 84 It 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 88 Not 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. 41 89 42 90 == Privacy Policy == … … 50 98 == Screenshots == 51 99 52 No screenshots are included in this release. 100 1. The Saudi VAT Pro settings screen in WooCommerce → Settings, showing the Webhook URL and Webhook Secret connection fields. 101 2. The WooCommerce admin order screen showing the buyer's VAT number and the "Retry Saudi VAT Pro submission" order action. 102 3. The customer-facing order details page "Documents" section, showing the downloadable Tax Invoice PDF link. 103 4. The customer-facing order details page "Documents" section after a refund, showing the downloadable Credit Note PDF link. 53 104 54 105 == 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). 55 117 56 118 = 1.0.1 = -
saudi-vat-pro-for-woocommerce/trunk/saudi-vat-pro-woocommerce.php
r3677646 r3698250 3 3 * Plugin Name: Saudi VAT Pro for WooCommerce 4 4 * 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.15 * 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 7 7 * Author: Saudi VAT Pro 8 8 * License: GPL v2 or later … … 20 20 } 21 21 22 define( 'SAUDIVATPRO_WC_VERSION', '1. 0.1' );22 define( 'SAUDIVATPRO_WC_VERSION', '1.1.0' ); 23 23 define( 'SAUDIVATPRO_WC_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); 24 24 define( 'SAUDIVATPRO_WC_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)