Skip to content

Commit f888f7e

Browse files
author
WooCommerce
committed
Updates to 3.1.0
1 parent 7dd06ac commit f888f7e

11 files changed

+432
-257
lines changed

build/frontend.asset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-settings', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '35ce60746dde2dba624c');
1+
<?php return array('dependencies' => array('react', 'wc-blocks-checkout', 'wc-settings', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '6cc28b57af85188a9770');

build/frontend.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

changelog.txt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
*** EU VAT Number Changelog ***
22

3-
2025-02-09 - version 3.0.4
3+
2026-03-16 - version 3.1.0
4+
* Fix - Ensure that orders without shipping address fields (e.g., virtual products) correctly appear in the EC Sales List report.
5+
* Fix - Ensure that the Create Order REST API response does not get corrupted when VAT validation fails.
6+
* Fix – Resolve an issue where the Blocks checkout would fail silently after switching from an EU country to a non-EU country when a VAT number had been entered.
7+
* Fix - Respect "Failed Validation Handling" setting when VIES service is unavailable for a member state.
8+
* Fix - Optimized admin dashboard performance by reducing unnecessary background checks.
9+
* Fix - Ensure that the `{vat_id}` placeholder is replaced with the VAT number correctly on block checkout.
10+
* Fix - Improved the translation system to ensure all text is correctly displayed in the selected language.
11+
* Fix – Make the “optional” string translatable.
12+
* Fix - Ensure that Greece VAT number validation correctly uses “EL” instead of “GR” as the VAT prefix in the shortcode checkout when the country code prefix is not required.
13+
* Fix - More specific error message for checkout when two-character VAT number is missing.
14+
* Dev - Bump WooCommerce "tested up to" version 10.6.
15+
* Dev - Bump WooCommerce minimum supported version to 10.4.
16+
* Dev - Bump Wordpress minimum supported version to 6.8.
17+
18+
2026-02-09 - version 3.0.4
419
* Fix - Ensure that VAT number validation applies only to EU countries.
520
* Fix - VAT number not verified on manually created order.
621
* Fix - Inconsistent value between `vat_number`, `billing_vat_number` and `shipping_vat_number` usermeta fields.

includes/class-wc-eu-vat-admin.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,11 @@ public static function admin_order( $args, $order ) {
396396
return;
397397
}
398398

399+
// This should only run when triggered by the admin ajax call for woocommerce_calc_line_taxes.
400+
if ( did_action( 'wp_ajax_woocommerce_calc_line_taxes' ) === 0 ) {
401+
return;
402+
}
403+
399404
/*
400405
* First try and get the billing country from the
401406
* address form (adding new order). If it is not

includes/class-wc-eu-vat-extend-store-endpoint.php

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ public function update_order_meta( $order, $request ) {
6464
}
6565

6666
if ( ! empty( $request['extensions']['woocommerce-eu-vat-number']['vat_number'] ) ) {
67-
$vat_number = $request['extensions']['woocommerce-eu-vat-number']['vat_number'];
67+
$vat_number = $request['extensions']['woocommerce-eu-vat-number']['vat_number'];
68+
69+
// Normalize the VAT number prefix before validation (handles cases like Greece where GR != EL).
70+
$vat_number = WC_EU_VAT_Number::get_normalized_vat_number( $vat_number, $country );
71+
6872
$is_valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country, $postcode );
6973
$fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' );
7074

@@ -75,7 +79,7 @@ public function update_order_meta( $order, $request ) {
7579

7680
$customer_id = $order->get_customer_id();
7781

78-
if ( $customer_id ) {
82+
if ( $customer_id && ! empty( $vat_number ) ) {
7983
$customer = new \WC_Customer( $customer_id );
8084
$customer->update_meta_data( 'vat_number', $vat_number );
8185
$customer->save_meta_data();
@@ -96,6 +100,11 @@ public function update_order_meta( $order, $request ) {
96100
$order->update_meta_data( '_vat_number_is_valid', true === $data['validation']['valid'] ? 'true' : 'false' );
97101
$order->update_meta_data( '_billing_vat_number', $vat_number );
98102

103+
// Add detailed order note about VAT validation result.
104+
$vat_number = $data['vat_number'] ?? '';
105+
$is_valid = $data['validation']['valid'] ?? null;
106+
wc_eu_vat_maybe_add_order_note( $order, $vat_number, $is_valid );
107+
99108
$this->maybe_apply_exemption();
100109
}
101110

@@ -274,7 +283,11 @@ public function validate( $vat_number = null ) {
274283
);
275284
}
276285

277-
$fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' );
286+
$fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' );
287+
288+
// Normalize the VAT number prefix before validation (handles cases like Greece where GR != EL).
289+
$vat_number = WC_EU_VAT_Number::get_normalized_vat_number( $vat_number, $country );
290+
278291
$is_format_valid = WC_EU_VAT_Number::validate_vat_format( $vat_number, $country );
279292

280293
if ( is_wp_error( $is_format_valid ) ) {
@@ -290,12 +303,16 @@ public function validate( $vat_number = null ) {
290303

291304
$is_registered_valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country );
292305

306+
// Handle WP_Error (API communication errors) according to fail_handler setting.
293307
if ( is_wp_error( $is_registered_valid ) ) {
308+
$error_code = $is_registered_valid->get_error_code();
309+
$error_message = $is_registered_valid->get_error_message();
310+
294311
$data['vat_number'] = $vat_number;
295312
$data['validation'] = array(
296313
'valid' => false,
297-
'error' => $is_registered_valid->get_error_message(),
298-
'code' => $is_registered_valid->get_error_code(),
314+
'code' => $error_code,
315+
'error' => 'reject' === $fail_handler ? $error_message : false,
299316
);
300317

301318
return $data;
@@ -345,11 +362,11 @@ public function maybe_apply_exemption( $with_notices = true ) {
345362
if ( false === (bool) $validation['validation']['valid'] && $with_notices ) {
346363
switch ( $fail_handler ) {
347364
case 'accept_with_vat':
348-
wc_add_notice( $validation['validation']['error'], 'error' );
349-
break;
350365
case 'accept':
366+
// Don't add an error notice - order should be accepted.
351367
break;
352368
default:
369+
// 'reject' - show error and block checkout.
353370
wc_add_notice( $validation['validation']['error'], 'error' );
354371
break;
355372
}
@@ -372,16 +389,17 @@ private function maybe_set_vat_exemption( $validation ) {
372389
} else {
373390
switch ( $fail_handler ) {
374391
case 'accept_with_vat':
392+
// Accept the order but keep VAT charged.
375393
WC_EU_VAT_Number::maybe_apply_vat_exemption( $vat_number, false );
376394
break;
377395
case 'accept':
378-
$error_code = $validation['validation']['code'] ?? false;
379-
380-
if ( 'wc-eu-vat-api-error' !== $error_code ) {
381-
WC_EU_VAT_Number::maybe_apply_vat_exemption( $vat_number, true );
382-
}
396+
// Accept the order and remove VAT.
397+
// This applies whether validation failed due to invalid number OR API error.
398+
// The merchant has explicitly chosen to trust customers when validation fails.
399+
WC_EU_VAT_Number::maybe_apply_vat_exemption( $vat_number, true );
383400
break;
384401
default:
402+
// 'reject' - don't exempt VAT (order will be blocked anyway).
385403
WC_EU_VAT_Number::maybe_apply_vat_exemption( $vat_number, false );
386404
break;
387405
}

includes/class-wc-eu-vat-number.php

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ public static function reset() {
211211
'validation' => array(
212212
'valid' => null,
213213
'error' => false,
214+
'code' => false,
214215
),
215216
);
216217
}
@@ -329,6 +330,36 @@ public static function get_vat_number_prefix( $country ) {
329330
return apply_filters( 'woocommerce_eu_vat_number_get_vat_number_prefix', $vat_prefix );
330331
}
331332

333+
/**
334+
* Normalize a VAT number by cleaning it and ensuring the correct country prefix.
335+
*
336+
* Strips unwanted characters, uppercases, and fixes the prefix for countries
337+
* where the VAT prefix differs from the country code (e.g., Greece uses 'EL'
338+
* as VAT prefix but 'GR' as country code).
339+
*
340+
* @since 3.1.0
341+
*
342+
* @param string $vat_number The raw VAT number.
343+
* @param string $country The country code.
344+
* @return string The normalized VAT number with the correct prefix.
345+
*/
346+
public static function get_normalized_vat_number( $vat_number, $country ) {
347+
$vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) );
348+
$vat_number_formatted = self::get_formatted_vat_number( $vat_number );
349+
$vat_prefix = self::get_vat_number_prefix( $country );
350+
351+
// If the VAT number starts with the country code (e.g., GR) instead of the VAT prefix (e.g., EL),
352+
// replace it with the correct VAT prefix. This handles cases like Greece where GR != EL.
353+
if ( $vat_prefix !== $country && str_starts_with( $vat_number, $country ) ) {
354+
$vat_number = $vat_prefix . $vat_number_formatted;
355+
} elseif ( ! str_starts_with( $vat_number, $vat_prefix ) && $vat_number_formatted === $vat_number ) {
356+
// No recognized prefix was provided, add the correct VAT prefix.
357+
$vat_number = $vat_prefix . $vat_number;
358+
}
359+
360+
return $vat_number;
361+
}
362+
332363
/**
333364
* Extract the 2-letter country code from a VAT number.
334365
*
@@ -415,15 +446,15 @@ public static function vat_number_is_valid( $vat_number, $country, $postcode = '
415446
}
416447

417448
if ( empty( $vat_number_formatted ) ) {
418-
return new WP_Error( 'wc-eu-vat-api-error', __( 'The VAT number is incomplete.', 'woocommerce-eu-vat-number' ) );
449+
return new WP_Error( 'wc-eu-vat-validation-error', __( 'The VAT number is incomplete.', 'woocommerce-eu-vat-number' ) );
419450
}
420451

421452
$country_codes_patterns = self::get_country_code_patterns();
422453

423454
// Return error if VAT Country Code doesn't match or exist.
424455
if ( ! isset( $country_codes_patterns[ $vat_prefix ] ) || ( $vat_prefix . $vat_number_formatted !== $vat_number ) ) {
425456
// translators: %1$s - VAT number field label, %2$s - VAT Number from user, %3$s - Billing country.
426-
return new WP_Error( 'wc-eu-vat-api-error', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) );
457+
return new WP_Error( 'wc-eu-vat-validation-error', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) );
427458
}
428459

429460
if ( ! empty( $cached_result ) ) {
@@ -453,8 +484,33 @@ public static function vat_number_is_valid( $vat_number, $country, $postcode = '
453484
try {
454485
$vies_req = $vies->check_vat( $vat_prefix, $vat_number_formatted );
455486
$is_valid = $vies_req->is_valid();
456-
457487
} catch ( SoapFault $e ) {
488+
$fault_string = $e->getMessage();
489+
490+
// Check for specific VIES error codes that indicate member state unavailability.
491+
if ( stripos( $fault_string, 'MS_UNAVAILABLE' ) !== false ) {
492+
return new WP_Error(
493+
'wc-eu-vat-api-error',
494+
__( 'The VAT validation service for this country is temporarily unavailable. Please try again later.', 'woocommerce-eu-vat-number' )
495+
);
496+
}
497+
498+
if ( stripos( $fault_string, 'SERVICE_UNAVAILABLE' ) !== false || stripos( $fault_string, 'MS_MAX_CONCURRENT_REQ' ) !== false ) {
499+
return new WP_Error(
500+
'wc-eu-vat-api-error',
501+
__( 'The VAT validation service is temporarily unavailable due to high load. Please try again later.', 'woocommerce-eu-vat-number' )
502+
);
503+
}
504+
505+
if ( stripos( $fault_string, 'TIMEOUT' ) !== false ) {
506+
return new WP_Error(
507+
'wc-eu-vat-api-error',
508+
__( 'The VAT validation service timed out. Please try again later.', 'woocommerce-eu-vat-number' )
509+
);
510+
}
511+
512+
return new WP_Error( 'wc-eu-vat-api-error', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) );
513+
} catch ( Exception $e ) {
458514
return new WP_Error( 'wc-eu-vat-api-error', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) );
459515
}
460516
}
@@ -541,12 +597,14 @@ public static function validate( $vat_number, $country, $postcode = '' ) {
541597
self::$data['validation'] = array(
542598
'valid' => false,
543599
'error' => $valid->get_error_message(),
600+
'code' => $valid->get_error_code(),
544601
);
545602
} else {
546603
self::$data['vat_number'] = $valid ? self::get_vat_number_prefix( $country ) . $vat_number_formatted : $vat_number;
547604
self::$data['validation'] = array(
548605
'valid' => $valid,
549606
'error' => false,
607+
'code' => false,
550608
);
551609
}
552610
}
@@ -1018,6 +1076,13 @@ public static function set_order_data( $order ) {
10181076
$order->update_meta_data( '_customer_ip_country', self::get_ip_country() );
10191077
$order->update_meta_data( '_customer_self_declared_country', ! empty( $_POST['location_confirmation'] ) ? 'true' : 'false' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
10201078
}
1079+
1080+
// Add detailed order note about VAT validation result.
1081+
wc_eu_vat_maybe_add_order_note(
1082+
$order,
1083+
self::$data['vat_number'],
1084+
self::$data['validation']['valid'] ?? null
1085+
);
10211086
}
10221087

10231088
/**
@@ -1056,6 +1121,11 @@ public static function validate_vat_format( $vat_number = '', $address_country_c
10561121
$vat_number = $country_code . $vat_number;
10571122
}
10581123

1124+
// If country prefix is required but not provided, show a specific error.
1125+
if ( empty( $country_code ) && self::is_country_prefix_required() ) {
1126+
return new WP_Error( 'wc-eu-vat-country-code-required', __( 'A valid two character country code is required at the start of the VAT number.', 'woocommerce-eu-vat-number' ) );
1127+
}
1128+
10591129
$vat_number = self::get_formatted_vat_number( $vat_number );
10601130
$regex_patterns = self::get_country_code_patterns();
10611131

@@ -1104,12 +1174,8 @@ public static function validate_checkout( $data, $doing_checkout = false ) {
11041174
}
11051175

11061176
if ( in_array( $vat_country, self::get_eu_countries(), true ) && ! empty( $billing_vat_number ) ) {
1107-
$billing_vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $billing_vat_number ) );
1108-
$is_format_valid = self::validate_vat_format( $billing_vat_number, $vat_country );
1109-
$vat_number_formatted = self::get_formatted_vat_number( $billing_vat_number );
1110-
if ( ! str_starts_with( $billing_vat_number, $vat_country ) && $vat_number_formatted === $billing_vat_number ) {
1111-
$billing_vat_number = $vat_country . $billing_vat_number;
1112-
}
1177+
$billing_vat_number = self::get_normalized_vat_number( $billing_vat_number, $vat_country );
1178+
$is_format_valid = self::validate_vat_format( $billing_vat_number, $vat_country );
11131179

11141180
if ( is_wp_error( $is_format_valid ) ) {
11151181
wc_add_notice( $is_format_valid->get_error_message(), 'error' );
@@ -1119,21 +1185,26 @@ public static function validate_checkout( $data, $doing_checkout = false ) {
11191185
self::validate( $billing_vat_number, $vat_country, $postcode );
11201186

11211187
if ( true === (bool) self::$data['validation']['valid'] ) {
1188+
// VAT number validated successfully.
11221189
self::maybe_apply_vat_exemption( $billing_vat_number, true );
11231190
WC()->session->set( 'vat_number', $billing_vat_number );
1124-
} elseif ( ! empty( self::$data['validation']['error'] ) ) {
1125-
wc_add_notice( self::$data['validation']['error'], 'error' );
1126-
WC()->session->set( 'vat_number', null );
11271191
} else {
1192+
// Validation failed (either API error or invalid number).
1193+
// Handle according to fail_handler setting.
11281194
switch ( $fail_handler ) {
11291195
case 'accept_with_vat':
1196+
// Accept the order but keep VAT charged.
11301197
self::maybe_apply_vat_exemption( $billing_vat_number, false );
11311198
break;
11321199
case 'accept':
1200+
// Accept the order and remove VAT.
11331201
self::maybe_apply_vat_exemption( $billing_vat_number, true );
11341202
break;
11351203
default:
1136-
if ( false === self::$data['validation']['valid'] ) {
1204+
// 'reject' - show error and block the order.
1205+
if ( ! empty( self::$data['validation']['error'] ) ) {
1206+
wc_add_notice( self::$data['validation']['error'], 'error' );
1207+
} elseif ( false === self::$data['validation']['valid'] ) {
11371208
wc_add_notice(
11381209
sprintf(
11391210
/* translators: 1: VAT number field label, 2: VAT Number, 3: Address type, 4: Country */
@@ -1145,10 +1216,8 @@ public static function validate_checkout( $data, $doing_checkout = false ) {
11451216
),
11461217
'error'
11471218
);
1148-
} else {
1149-
wc_add_notice( self::$data['validation']['error'], 'error' );
1150-
WC()->session->set( 'vat_number', null );
11511219
}
1220+
WC()->session->set( 'vat_number', null );
11521221
break;
11531222
}
11541223
}

includes/class-wc-eu-vat-report-ec-sales-list.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,10 @@ public function get_main_chart() {
138138
'name' => '_billing_country',
139139
),
140140
'_shipping_country' => array(
141-
'type' => 'meta',
142-
'function' => '',
143-
'name' => '_shipping_country',
141+
'type' => 'meta',
142+
'function' => '',
143+
'name' => '_shipping_country',
144+
'join_type' => 'LEFT',
144145
),
145146
'_order_currency' => array(
146147
'type' => 'meta',
@@ -181,9 +182,10 @@ public function get_main_chart() {
181182
'name' => '_billing_country',
182183
),
183184
'_shipping_country' => array(
184-
'type' => 'meta',
185-
'function' => '',
186-
'name' => '_shipping_country',
185+
'type' => 'meta',
186+
'function' => '',
187+
'name' => '_shipping_country',
188+
'join_type' => 'LEFT',
187189
),
188190
'_order_currency' => array(
189191
'type' => 'meta',

includes/class-wc-eu-vat-reports.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ public static function display_hpos_incompatibility_notice() {
110110
* @return array
111111
*/
112112
public static function init_reports( $reports ) {
113+
// Only check sync status when actually viewing reports page.
114+
// This prevents the expensive query from running on every admin page.
115+
// Check if get_current_screen() is available (may not be in AJAX/REST/CLI contexts).
116+
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
117+
if ( ! $screen || 'woocommerce_page_wc-reports' !== $screen->id ) {
118+
// Skip the expensive check on non-reports pages.
119+
return $reports;
120+
}
121+
113122
// The EU VAT reports are incompatible with stores running HPOS with syncing disabled.
114123
if ( self::is_cot_enabled() && ! self::is_cot_sync_enabled() ) {
115124
return $reports;

0 commit comments

Comments
 (0)