diff --git a/.github/scripts/release.php b/.github/scripts/release.php index ad1e516..8be0810 100644 --- a/.github/scripts/release.php +++ b/.github/scripts/release.php @@ -132,14 +132,14 @@ function run( $command, &$result_code = null ) { line( sprintf( - 'WooCommerce Subscriptions Version: %s', + 'WooCommerce EU VAT Number Version: %s', $version ) ); line( sprintf( - 'WooCommerce Subscriptions ZIP URL: %s', + 'WooCommerce EU VAT Number ZIP URL: %s', $zip_url ) ); @@ -161,9 +161,9 @@ function run( $command, &$result_code = null ) { mkdir( $archives_dir ); mkdir( $plugins_dir ); -$plugin_dir = $plugins_dir . '/woocommerce-subscriptions'; +$plugin_dir = $plugins_dir . '/woocommerce-eu-vat-number'; -$zip_file = $archives_dir . '/woocommerce-subscriptions-' . $version . '.zip'; +$zip_file = $archives_dir . '/woocommerce-eu-vat-number-' . $version . '.zip'; /** * Download ZIP. diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 94b09fc..16b2383 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -1,25 +1,25 @@ -#wc_eu_vat .inside { - margin: 0; - padding: 0; -} -#wc_eu_vat p { - margin: 12px !important; -} -.wc-eu-vat-table { - width: 100%; - margin: 5px 0; -} -.wc-eu-vat-table th, .wc-eu-vat-table td { - text-align: left; - padding: 7px 12px; -} -.eu-vat-overview { - margin: 0; -} -.column-eu_vat { - width: 10%; - color: #555; -} -.column-eu_vat ul li { - color: #555; +#wc_eu_vat .inside { + margin: 0; + padding: 0; +} +#wc_eu_vat p { + margin: 12px !important; +} +.wc-eu-vat-table { + width: 100%; + margin: 5px 0; +} +.wc-eu-vat-table th, .wc-eu-vat-table td { + text-align: left; + padding: 7px 12px; +} +.eu-vat-overview { + margin: 0; +} +.column-eu_vat { + width: 10%; + color: #555; +} +.column-eu_vat ul li { + color: #555; } \ No newline at end of file diff --git a/assets/js/admin.js b/assets/js/admin.js index db2a5a4..c6e0968 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,10 +1,10 @@ -( function( $ ) { - $(document.body).on( 'order-totals-recalculate-before', function( e, data ) { - if( data && $( '#_billing_vat_number' ).length ) { - data._billing_vat_number = $( '#_billing_vat_number' ).val(); - data._billing_country = $( '#_billing_country' ).val(); - data._shipping_country = $( '#_shipping_country' ).val(); - data._billing_postcode = $( '#_billing_postcode' ).val(); - } - }); -}( jQuery ) ); +( function( $ ) { + $(document.body).on( 'order-totals-recalculate-before', function( e, data ) { + if( data && $( '#_billing_vat_number' ).length ) { + data._billing_vat_number = $( '#_billing_vat_number' ).val(); + data._billing_country = $( '#_billing_country' ).val(); + data._shipping_country = $( '#_shipping_country' ).val(); + data._billing_postcode = $( '#_billing_postcode' ).val(); + } + }); +}( jQuery ) ); diff --git a/assets/js/eu-vat.js b/assets/js/eu-vat.js index 1cdfa93..48db22f 100644 --- a/assets/js/eu-vat.js +++ b/assets/js/eu-vat.js @@ -1,74 +1,74 @@ -jQuery(function(){ - function field_is_required( field, is_required ) { - if ( is_required ) { - field.find( 'label .optional' ).remove(); - field.addClass( 'validate-required' ); - - if ( field.find( 'label .required' ).length === 0 ) { - field.find( 'label' ).append( - ' *' - ); - } - } else { - field.find( 'label .required' ).remove(); - field.removeClass( 'validate-required woocommerce-invalid woocommerce-invalid-required-field' ); - - if ( field.find( 'label .optional' ).length === 0 ) { - field.find( 'label' ).append( ' (' + wc_address_i18n_params.i18n_optional_text + ')' ); - } - } - } - - jQuery( 'form.checkout, form#order_review').on( 'change', '#billing_country', function() { - var country = jQuery( '#billing_country' ).val(); - var check_countries = wc_eu_vat_params.eu_countries; - var b2b_enabled = wc_eu_vat_params.b2b_required; - - field_is_required( jQuery( '#woocommerce_eu_vat_number_field' ), false ); - - if ( country && jQuery.inArray( country, check_countries ) >= 0 ) { - jQuery( '#woocommerce_eu_vat_number_field' ).fadeIn(); - if ( 'yes' === b2b_enabled ) { - field_is_required( jQuery( '#woocommerce_eu_vat_number_field' ), true ); - } - } else { - jQuery( '#woocommerce_eu_vat_number_field' ).fadeOut(); - } - }); - jQuery( '#billing_country' ).trigger( 'change' ); - - /* Validate EU VAT Number field only on change event */ - jQuery( 'form.checkout, form#order_review' ).on( 'change', '#woocommerce_eu_vat_number', function() { - jQuery( 'body' ).trigger( 'update_checkout' ); - } ); - - /** - * Handles checkout field UI when VAT field validation fails. - */ - jQuery( document.body ).on( 'updated_checkout', function( e, data ) { - $vat_field = jQuery( '#woocommerce_eu_vat_number' ); - - if ( ! $vat_field.is( ':visible' ) ) { - return; - } - - $vat_code = $vat_field.val(); - $vat_field_wrapper = $vat_field.closest( '.form-row' ); - - if ( 'success' === data.result ) { - if ( ! $vat_code.length ) { - $vat_field_wrapper.removeClass( 'woocommerce-validated' ); - } - - return; - } - - /** If the message includes the VAT number, then highlight the VAT field in red. */ - if ( data.messages.length && data.messages.includes( $vat_code.toUpperCase() ) ) { - $vat_field_wrapper.removeClass( 'woocommerce-validated' ); - $vat_field_wrapper.addClass( 'woocommerce-invalid' ); - } - } ) -}); +jQuery(function(){ + function field_is_required( field, is_required ) { + if ( is_required ) { + field.find( 'label .optional' ).remove(); + field.addClass( 'validate-required' ); + + if ( field.find( 'label .required' ).length === 0 ) { + field.find( 'label' ).append( + ' *' + ); + } + } else { + field.find( 'label .required' ).remove(); + field.removeClass( 'validate-required woocommerce-invalid woocommerce-invalid-required-field' ); + + if ( field.find( 'label .optional' ).length === 0 ) { + field.find( 'label' ).append( ' (' + wc_address_i18n_params.i18n_optional_text + ')' ); + } + } + } + + jQuery( 'form.checkout, form#order_review').on( 'change', '#billing_country', function() { + var country = jQuery( '#billing_country' ).val(); + var check_countries = wc_eu_vat_params.eu_countries; + var b2b_enabled = wc_eu_vat_params.b2b_required; + + field_is_required( jQuery( '#woocommerce_eu_vat_number_field' ), false ); + + if ( country && jQuery.inArray( country, check_countries ) >= 0 ) { + jQuery( '#woocommerce_eu_vat_number_field' ).fadeIn(); + if ( 'yes' === b2b_enabled ) { + field_is_required( jQuery( '#woocommerce_eu_vat_number_field' ), true ); + } + } else { + jQuery( '#woocommerce_eu_vat_number_field' ).fadeOut(); + } + }); + jQuery( '#billing_country' ).trigger( 'change' ); + + /* Validate EU VAT Number field only on change event */ + jQuery( 'form.checkout, form#order_review' ).on( 'change', '#woocommerce_eu_vat_number', function() { + jQuery( 'body' ).trigger( 'update_checkout' ); + } ); + + /** + * Handles checkout field UI when VAT field validation fails. + */ + jQuery( document.body ).on( 'updated_checkout', function( e, data ) { + $vat_field = jQuery( '#woocommerce_eu_vat_number' ); + + if ( ! $vat_field.is( ':visible' ) ) { + return; + } + + $vat_code = $vat_field.val(); + $vat_field_wrapper = $vat_field.closest( '.form-row' ); + + if ( 'success' === data.result ) { + if ( ! $vat_code.length ) { + $vat_field_wrapper.removeClass( 'woocommerce-validated' ); + } + + return; + } + + /** If the message includes the VAT number, then highlight the VAT field in red. */ + if ( data.messages.length && data.messages.includes( $vat_code.toUpperCase() ) ) { + $vat_field_wrapper.removeClass( 'woocommerce-validated' ); + $vat_field_wrapper.addClass( 'woocommerce-invalid' ); + } + } ) +}); diff --git a/block.json b/block.json index 5cb99de..2d4b597 100644 --- a/block.json +++ b/block.json @@ -1,26 +1,23 @@ -{ - "apiVersion": 2, - "name": "woocommerce/eu-vat-number", - "version": "1.0.0", - "title": "EU VAT Number", - "parent": [ "woocommerce/checkout-fields-block" ], - "category": "woocommerce", - "icon": "cart", - "description": "The EU VAT Number extension lets you collect and validate EU VAT numbers during checkout to identify B2B transactions verses B2C. IP Addresses can also be validated to ensure they match the billing address. EU businesses with a valid VAT number can have their VAT removed prior to payment.", - "supports": { - "html": false - }, - "textdomain": "woocommerce-eu-vat-number", - "editorScript": "file:./build/index.js", - "editorStyle": "file:./build/index.css", - "style": "file:./build/index.css", - "attributes": { - "lock": { - "type": "object", - "default": { - "remove": true, - "move": false - } - } - } -} +{ + "apiVersion": 2, + "name": "woocommerce/eu-vat-number", + "version": "1.0.0", + "title": "EU VAT Number", + "parent": [ "woocommerce/checkout-fields-block" ], + "category": "woocommerce", + "icon": "cart", + "description": "The EU VAT Number extension lets you collect and validate EU VAT numbers during checkout to identify B2B transactions verses B2C. IP Addresses can also be validated to ensure they match the billing address. EU businesses with a valid VAT number can have their VAT removed prior to payment.", + "supports": { + "html": false + }, + "textdomain": "woocommerce-eu-vat-number", + "attributes": { + "lock": { + "type": "object", + "default": { + "remove": true, + "move": false + } + } + } +} diff --git a/build/frontend.asset.php b/build/frontend.asset.php index 1e62301..e7ada63 100644 --- a/build/frontend.asset.php +++ b/build/frontend.asset.php @@ -1 +1 @@ - array('wc-blocks-checkout', 'wc-settings', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '87d45ecf17216813b6d1215de074478a'); \ No newline at end of file + array('wc-blocks-checkout', 'wc-settings', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => 'ece70d3d4ce2de6be1778d017be033ee'); \ No newline at end of file diff --git a/build/frontend.js b/build/frontend.js index e8c5c94..c0539c4 100644 --- a/build/frontend.js +++ b/build/frontend.js @@ -1,5 +1,5 @@ -(window.webpackJsonp_woocommerce_eu_vat_number=window.webpackJsonp_woocommerce_eu_vat_number||[]).push([[2],{20:function(e,t,n){}}]),function(e){function t(t){for(var r,i,a=t[0],u=t[1],s=t[2],d=0,p=[];de.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n%s.","woocommerce-eu-vat-number"),b),{strong:Object(u.createElement)("strong",null)}),checked:y,onChange:function(e){S||A(!0),x(e),p("woocommerce-eu-vat-number","location_confirmation",e)}})),null==C||!C.message||null!=C&&C.hidden?null:Object(u.createElement)("div",{className:"wc-eu-vat-checkout-ip-notice__error wc-block-components-validation-error"},C.message))},O=(n(20),n(5)),g=function(e){var t=e.title,n=e.stepHeadingContent;return Object(u.createElement)("div",{className:"wc-block-components-checkout-step__heading"},Object(u.createElement)("h2",{"aria-hidden":"true",className:"wc-block-components-checkout-step__title wc-block-components-title"},t),!!n&&Object(u.createElement)("span",{className:"wc-block-components-checkout-step__heading-content"},n))},j=(r=Object(O.a)({}),function(e){return function(t){var n=function(e,t){var n=[];return Object.keys(e).forEach((function(r){if(void 0!==t[r])switch(e[r].type){case"boolean":n[r]="false"!==t[r]&&!1!==t[r];break;case"number":n[r]=parseInt(t[r],10);break;case"array":case"object":n[r]=JSON.parse(t[r]);break;default:n[r]=t[r]}else n[r]=e[r].default})),n}(r,t);return Object(u.createElement)(e,c()({},t,n))}})((function(e){var t=e.id,n=e.className,r=e.title,o=e.legend,c=e.description,i=e.children,a=e.disabled,s=void 0!==a&&a,l=e.showStepNumber,d=void 0===l||l,p=e.stepHeadingContent,m=void 0===p?function(){}:p,b=o||r?"fieldset":"div";return Object(u.createElement)(b,{className:h()(n,"wc-block-components-checkout-step",{"wc-block-components-checkout-step--with-step-number":d,"wc-block-components-checkout-step--disabled":s}),id:t,disabled:s},!(!o&&!r)&&Object(u.createElement)("legend",{className:"screen-reader-text"},o||r),!!r&&Object(u.createElement)(g,{title:r,stepHeadingContent:m()}),Object(u.createElement)("div",{className:"wc-block-components-checkout-step__container"},!!c&&Object(u.createElement)("p",{className:"wc-block-components-checkout-step__description"},c),Object(u.createElement)("div",{className:"wc-block-components-checkout-step__content"},i)))})),y=Object(b.withInstanceId)((function(e){var t,n,r,o=e.validation,c=o.setValidationErrors,i=o.clearValidationError,d=o.getValidationError,p=e.checkoutExtensionData,b=Object(_.useSelect)((function(e){var t=e("wc/store/cart").getCartData();return{cartItems:t.items,billingAddress:t.billingAddress,extensions:t.extensions,needsShipping:t.needsShipping}})),v=b.billingAddress,O=b.extensions,g=wc_eu_vat_params,y=g.b2b_required,x=g.eu_countries,E=g.uk_ni_notice,k=g.input_label,S=g.input_description,A=g.failure_handler,N=e.title,P=void 0===N?Object(l.__)("VAT Number","woocommerce-eu-vat-number"):N,T=e.description,C=void 0===T?"":T,V=e.showStepNumber,I=void 0===V||V,D=Object(f.getSetting)("woocommerce-eu-vat-number_data"),M=D.woocommerce_eu_vat_number_validate_ip,B=D.ip_address,U=D.ip_country,Z="yes"===M,q=Object(u.useState)(!1),H=m()(q,2),J=H[0],L=H[1],F=Object(u.useState)(null===(t=O["woocommerce-eu-vat-number"])||void 0===t?void 0:t.vat_number),R=m()(F,2),G=R[0],$=R[1],K=Object(u.useState)(null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.vat_number),Y=m()(K,2),X=Y[0],z=Y[1],Q=Object(u.useState)("yes"===y&&-1!==x.indexOf(v.country)),W=m()(Q,1)[0],ee=Object(u.useState)(-1!==x.indexOf(v.country)),te=m()(ee,2),ne=te[0],re=te[1],oe=Object(u.useState)("GB"===v.country),ce=m()(oe,2),ie=ce[0],ae=ce[1],ue="billing_vat_number_error",se=d(ue),le=!1===(null==se?void 0:se.hidden)&&""!==(null==se?void 0:se.message);return Object(u.useEffect)((function(){p.setExtensionData("woocommerce-eu-vat-number","location_confirmation",!1)}),[p.setExtensionData]),Object(u.useEffect)((function(){re(-1!==x.indexOf(v.country)),ae("GB"===v.country)}),[x,v.country]),Object(u.useEffect)((function(){!W||"string"==typeof G&&G.length>0||c(a()({},ue,{message:Object(l.__)("VAT number is required.","woocommerce-eu-vat-number"),hidden:!0}))}),[W,c,ue]),Object(u.useEffect)((function(){var e,t,n,r=d(ue);!G&&null!=r&&r.hidden||((W||G)&&null!==(e=O["woocommerce-eu-vat-number"])&&void 0!==e&&null!==(t=e.validation)&&void 0!==t&&t.error?c(a()({},ue,{message:null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.validation.error,hidden:!1})):i(ue))}),[null===(r=O["woocommerce-eu-vat-number"])||void 0===r?void 0:r.validation.error]),Object(u.useEffect)((function(){var e,t,n,r,o,i,u;"string"==typeof G&&G.length>0&&(o=v.country,i=G,(!(u={AT:"U[A-Z\\d]{8}",BE:"0\\d{9}",BG:"\\d{9,10}",CY:"\\d{8}[A-Z]",CZ:"\\d{8,10}",DE:"\\d{9}",DK:"(\\d{2} ?){3}\\d{2}",EE:"\\d{9}",EL:"\\d{9}",ES:"[A-Z]\\d{7}[A-Z]|\\d{8}[A-Z]|[A-Z]\\d{8}",FI:"\\d{8}",FR:"([A-Z]{2}|[A-Z0-9]{2})\\d{9}",XI:"\\d{9}|\\d{12}|(GD|HA)\\d{3}",HR:"\\d{11}",HU:"\\d{8}",IE:"[A-Z\\d]{8,10}",IT:"\\d{11}",LT:"(\\d{9}|\\d{12})",LU:"\\d{8}",LV:"\\d{11}",MT:"\\d{8}",NL:"\\d{9}B\\d{2}",PL:"\\d{10}",PT:"\\d{9}",RO:"\\d{2,10}",SE:"\\d{12}",SI:"\\d{8}",SK:"\\d{10}"})[o]||null===new RegExp(u[o]).exec(i))&&"reject"===A||(null===(e=O["woocommerce-eu-vat-number"])||void 0===e||null===(t=e.validation)||void 0===t||!t.valid)&&G===(null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.vat_number)&&"reject"===A)&&c(a()({},ue,{message:null===(r=O["woocommerce-eu-vat-number"])||void 0===r?void 0:r.validation.error,hidden:!1}))}),[]),ne?Object(u.createElement)(j,{id:"shipping-fields",className:h()("wc-block-checkout__shipping-fields","eu-vat-extra-css"),title:P,description:C,showStepNumber:I},Object(u.createElement)("div",null,Object(u.createElement)("div",{className:h()("wc-block-components-text-input","eu-vat-extra-css",{"is-active":J||!!G},{"has-error":le})},Object(u.createElement)("input",{type:"text","aria-label":k,id:"billing_vat_number",value:G||"",onChange:function(e){!function(e){var t=e.target.value;i(ue),"string"==typeof t&&0===t.length&&W&&c(a()({},ue,{message:Object(l.__)("VAT Number is required.","woocommerce-eu-vat-number"),hidden:!1})),$(t)}(e)},onFocus:function(){return L(!0)},onBlur:function(){L(!1),G!==X&&(i(ue),W&&(null==G||"string"==typeof G&&0===G.length)&&c(a()({},ue,{message:Object(l.__)("VAT number is required.","woocommerce-eu-vat-number"),hidden:!1})),Object(s.extensionCartUpdate)({namespace:"woocommerce-eu-vat-number",data:{vat_number:G},cartPropsToReceive:["extensions"]}).then((function(){z(G),W||G||i(ue)})))},"aria-invalid":!0===le,disabled:!1,required:W}),Object(u.createElement)("label",{htmlFor:"billing_vat_number"},k,!0===W?null:" (optional)"),Object(u.createElement)((function(){var e;return le?Object(u.createElement)("div",{className:"wc-block-components-validation-error",role:"alert"},Object(u.createElement)("p",{id:ue},null===(e=d(ue))||void 0===e?void 0:e.message)):null}),null),Object(u.createElement)("div",{className:"wc-eu-vat-checkout-uk-notice"},Object(u.createElement)("div",null,Object(u.createElement)("span",null,S)),Object(u.createElement)("span",null,ie?E:null))),!G&&"no"===y||!G&&!y?Object(u.createElement)(w,{validation:e.validation,ipAddress:B,ipCountry:U,billingCountry:v.country,shouldValidateIp:Z,checkoutExtensionData:p}):null)):Object(u.createElement)(u.Fragment,null)}));function x(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function E(e){for(var t=1;t%s.","woocommerce-eu-vat-number"),b),{strong:Object(u.createElement)("strong",null)}),checked:y,onChange:function(e){S||A(!0),x(e),p("woocommerce-eu-vat-number","location_confirmation",e)}})),null==C||!C.message||null!=C&&C.hidden?null:Object(u.createElement)("div",{className:"wc-eu-vat-checkout-ip-notice__error wc-block-components-validation-error"},C.message))},O=(n(20),n(5)),g=function(e){var t=e.title,n=e.stepHeadingContent;return Object(u.createElement)("div",{className:"wc-block-components-checkout-step__heading"},Object(u.createElement)("h2",{"aria-hidden":"true",className:"wc-block-components-checkout-step__title wc-block-components-title"},t),!!n&&Object(u.createElement)("span",{className:"wc-block-components-checkout-step__heading-content"},n))},j=(r=Object(O.a)({}),function(e){return function(t){var n=function(e,t){var n=[];return Object.keys(e).forEach((function(r){if(void 0!==t[r])switch(e[r].type){case"boolean":n[r]="false"!==t[r]&&!1!==t[r];break;case"number":n[r]=parseInt(t[r],10);break;case"array":case"object":n[r]=JSON.parse(t[r]);break;default:n[r]=t[r]}else n[r]=e[r].default})),n}(r,t);return Object(u.createElement)(e,c()({},t,n))}})((function(e){var t=e.id,n=e.className,r=e.title,o=e.legend,c=e.description,a=e.children,i=e.disabled,s=void 0!==i&&i,l=e.showStepNumber,d=void 0===l||l,p=e.stepHeadingContent,m=void 0===p?function(){}:p,b=o||r?"fieldset":"div";return Object(u.createElement)(b,{className:h()(n,"wc-block-components-checkout-step",{"wc-block-components-checkout-step--with-step-number":d,"wc-block-components-checkout-step--disabled":s}),id:t,disabled:s},!(!o&&!r)&&Object(u.createElement)("legend",{className:"screen-reader-text"},o||r),!!r&&Object(u.createElement)(g,{title:r,stepHeadingContent:m()}),Object(u.createElement)("div",{className:"wc-block-components-checkout-step__container"},!!c&&Object(u.createElement)("p",{className:"wc-block-components-checkout-step__description"},c),Object(u.createElement)("div",{className:"wc-block-components-checkout-step__content"},a)))})),y=Object(b.withInstanceId)((function(e){var t,n,r,o=e.validation,c=o.setValidationErrors,a=o.clearValidationError,d=o.getValidationError,p=e.checkoutExtensionData,b=Object(_.useSelect)((function(e){var t=e("wc/store/cart").getCartData();return{cartItems:t.items,billingAddress:t.billingAddress,extensions:t.extensions,needsShipping:t.needsShipping}})),v=b.billingAddress,O=b.extensions,g=wc_eu_vat_params,y=g.b2b_required,x=g.eu_countries,E=g.uk_ni_notice,k=g.input_label,S=g.input_description,A=g.failure_handler,N=e.title,P=void 0===N?Object(l.__)("VAT Number","woocommerce-eu-vat-number"):N,T=e.description,C=void 0===T?"":T,V=e.showStepNumber,I=void 0===V||V,D=Object(f.getSetting)("woocommerce-eu-vat-number_data"),M=D.woocommerce_eu_vat_number_validate_ip,B=D.ip_address,U=D.ip_country,Z="yes"===M,q=Object(u.useState)(!1),H=m()(q,2),J=H[0],L=H[1],F=Object(u.useState)(null===(t=O["woocommerce-eu-vat-number"])||void 0===t?void 0:t.vat_number),R=m()(F,2),G=R[0],$=R[1],K=Object(u.useState)(null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.vat_number),Y=m()(K,2),X=Y[0],z=Y[1],Q=Object(u.useState)("yes"===y&&-1!==x.indexOf(v.country)),W=m()(Q,1)[0],ee=Object(u.useState)(-1!==x.indexOf(v.country)),te=m()(ee,2),ne=te[0],re=te[1],oe=Object(u.useState)("GB"===v.country),ce=m()(oe,2),ae=ce[0],ie=ce[1],ue="billing_vat_number_error",se=d(ue),le=!1===(null==se?void 0:se.hidden)&&""!==(null==se?void 0:se.message);return Object(u.useEffect)((function(){p.setExtensionData("woocommerce-eu-vat-number","location_confirmation",!1)}),[p.setExtensionData]),Object(u.useEffect)((function(){re(-1!==x.indexOf(v.country)),ie("GB"===v.country)}),[x,v.country]),Object(u.useEffect)((function(){!W||"string"==typeof G&&G.length>0||c(i()({},ue,{message:Object(l.__)("VAT number is required.","woocommerce-eu-vat-number"),hidden:!0}))}),[W,c,ue]),Object(u.useEffect)((function(){var e,t,n,r=d(ue);!G&&null!=r&&r.hidden||((W||G)&&null!==(e=O["woocommerce-eu-vat-number"])&&void 0!==e&&null!==(t=e.validation)&&void 0!==t&&t.error?c(i()({},ue,{message:null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.validation.error,hidden:!1})):a(ue))}),[null===(r=O["woocommerce-eu-vat-number"])||void 0===r?void 0:r.validation.error]),Object(u.useEffect)((function(){var e,t,n,r,o,a,u;"string"==typeof G&&G.length>0&&(o=v.country,a=G,(!(u={AT:"U[A-Z\\d]{8}",BE:"0\\d{9}",BG:"\\d{9,10}",CY:"\\d{8}[A-Z]",CZ:"\\d{8,10}",DE:"\\d{9}",DK:"(\\d{2} ?){3}\\d{2}",EE:"\\d{9}",EL:"\\d{9}",ES:"[A-Z]\\d{7}[A-Z]|\\d{8}[A-Z]|[A-Z]\\d{8}",FI:"\\d{8}",FR:"([A-Z]{2}|[A-Z0-9]{2})\\d{9}",XI:"\\d{9}|\\d{12}|(GD|HA)\\d{3}",HR:"\\d{11}",HU:"\\d{8}",IE:"[A-Z\\d]{8,10}",IT:"\\d{11}",LT:"(\\d{9}|\\d{12})",LU:"\\d{8}",LV:"\\d{11}",MT:"\\d{8}",NL:"\\d{9}B\\d{2}",PL:"\\d{10}",PT:"\\d{9}",RO:"\\d{2,10}",SE:"\\d{12}",SI:"\\d{8}",SK:"\\d{10}"})[o]||null===new RegExp(u[o]).exec(a))&&"reject"===A||(null===(e=O["woocommerce-eu-vat-number"])||void 0===e||null===(t=e.validation)||void 0===t||!t.valid)&&G===(null===(n=O["woocommerce-eu-vat-number"])||void 0===n?void 0:n.vat_number)&&"reject"===A)&&c(i()({},ue,{message:null===(r=O["woocommerce-eu-vat-number"])||void 0===r?void 0:r.validation.error,hidden:!1}))}),[]),ne?Object(u.createElement)(j,{id:"shipping-fields",className:h()("wc-block-checkout__shipping-fields","eu-vat-extra-css"),title:P,description:C,showStepNumber:I},Object(u.createElement)("div",null,Object(u.createElement)("div",{className:h()("wc-block-components-text-input","eu-vat-extra-css",{"is-active":J||!!G},{"has-error":le})},Object(u.createElement)("input",{type:"text","aria-label":k,id:"billing_vat_number",value:G||"",onChange:function(e){!function(e){var t=e.target.value;a(ue),"string"==typeof t&&0===t.length&&W&&c(i()({},ue,{message:Object(l.__)("VAT Number is required.","woocommerce-eu-vat-number"),hidden:!1})),$(t)}(e)},onFocus:function(){return L(!0)},onBlur:function(){L(!1),G!==X&&(a(ue),W&&(null==G||"string"==typeof G&&0===G.length)&&c(i()({},ue,{message:Object(l.__)("VAT number is required.","woocommerce-eu-vat-number"),hidden:!1})),Object(s.extensionCartUpdate)({namespace:"woocommerce-eu-vat-number",data:{vat_number:G},cartPropsToReceive:["extensions"]}).then((function(){z(G),W||G||a(ue)})))},"aria-invalid":!0===le,disabled:!1,required:W}),Object(u.createElement)("label",{htmlFor:"billing_vat_number"},k,!0===W?null:" (optional)"),Object(u.createElement)((function(){var e;return le?Object(u.createElement)("div",{className:"wc-block-components-validation-error",role:"alert"},Object(u.createElement)("p",{id:ue},null===(e=d(ue))||void 0===e?void 0:e.message)):null}),null),Object(u.createElement)("div",{className:"wc-eu-vat-checkout-uk-notice"},Object(u.createElement)("div",null,Object(u.createElement)("span",null,S)),Object(u.createElement)("span",null,ae?E:null))),!G&&"no"===y||!G&&!y?Object(u.createElement)(w,{validation:e.validation,ipAddress:B,ipCountry:U,billingCountry:v.country,shouldValidateIp:Z,checkoutExtensionData:p}):null)):Object(u.createElement)(u.Fragment,null)}));function x(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function E(e){for(var t=1;t array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'c216817065c367a517a53cf1583f8d97'); \ No newline at end of file + array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '669f781aab7fd0be57f74d2127dcf4e1'); \ No newline at end of file diff --git a/build/index.js b/build/index.js index 3f2af78..d8517cf 100644 --- a/build/index.js +++ b/build/index.js @@ -1 +1 @@ -!function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)o.d(n,r,function(t){return e[t]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=22)}([function(e,t){e.exports=window.wp.element},function(e,t){e.exports=window.wp.i18n},function(e,t){e.exports=function(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e},e.exports.__esModule=!0,e.exports.default=e.exports},function(e,t,o){var n;!function(){"use strict";var o={}.hasOwnProperty;function r(){for(var e=[],t=0;t VAT Number endpoint when My Account is set as homepage. PR#204 - -2021-04-07 - version 2.3.26 -* Update - Fixes documentation and support links. -* Fix - Trigger VAT number validation only on change. -* Fix - Properly handle EU VAT privacy data erasure. -* Update - JS updated for jQuery 3 compatibility. -* Fix - Modified VIES API request to use HTTPS - -2020-09-30 - version 2.3.25 -* Fix - VAT Number required when B2B not enabled. - -2020-09-30 - version 2.3.24 -* Fix - Automatically populate VAT Number at checkout if saved to user profile. -* Fix - VAT Number showing as required for non-EU countries when switching billing country. -* Tweak - Lower VAT Number transient expiration time from 7 days to 1 day. -* Tweak - Improve error message when saving VAT Number on My Account if billing address is missing. - -2020-08-19 - version 2.3.23 -* Tweak - WordPress 5.5 compatibility. - -2020-07-14 - version 2.3.22 -* Fix - PHP error on checkout page if order total is zero. - -2020-07-01 - version 2.3.21 -* Add - VAT number field to user/customer profile. -* Add - VAT number of a customer when adding a new order from admin. -* Fix - VAT not validating correctly in checkout in all acceptance types. -* Tweak - Use EL country code for Greece when showing tax reports. -* Tweak - Treat Isle of Man the same as UK in regards to tax exemption. - -2020-06-10 - version 2.3.20 -* Tweak - WC 4.2 compatibility. - -2020-04-29 - version 2.3.19 -* Tweak - Remove legacy code. -* Tweak - WC 4.0 compatibility. -* Fix - VAT incorrectly being removed when Recalculate button is clicked on the order. - -2020-02-04 - version 2.3.18 -* Fix - Use proper escaping for attributes. - -2020-01-13 - version 2.3.17 -* Tweak - WC 3.9 compatibility. - -2019-11-05 - version 2.3.16 -* Fix - Update WC header. - -2019-11-05 - version 2.3.15 -* Tweak - WC 3.8 compatibility. - -2019-08-08 - version 2.3.14 -* Fix - Adding/editing VAT in admin new order or edit order not updating VAT amount. -* Tweak - WC 3.7 compatibility. - -2019-04-16 - version 2.3.13 -* Tweak - WC 3.6 compatibility. - -2019-01-07 - version 2.3.12 -* Fix - Always show VAT number field if b2b is enabled. - -2018-09-26 - version 2.3.11 -* Update - WC tested up to 3.5 - -2018-08-21 - version 2.3.10 -* Fix - Re-deployment to fix VAT numbers being removed when order is saved. - -2018-08-21 - version 2.3.9 -* Fix - VAT numbers were removed when order is saved. - -2018-08-02 - version 2.3.8 -* Fix - Properly handle shipping address depending on 'Ship to different address' checked or not. - -2018-05-22 - version 2.3.7 -* Fix - PHP deprecation notice for 'parse_str'. -* Fix - Increasing timeout for SOAP requests. -* Fix - Show SOAP requirement notice and disable VAT lookups if needed. -* Add - Added woocommerce_eu_vat_number_soap_parameters filter for modifying SOAP request parameters. -* Fix - Use proper parse_str format in ajax_update_checkout_totals. -* Update - WC tested up to 3.4 -* Update - Privacy policy notification. -* Update - Export/erasure hooks added. - -2018-02-05 - version 2.3.6 -* Fix - Cached VAT lookups not applying proper exemptions -* New - Adds a filter to be able to control which countries charge VAT, via code. - -2018-01-17 - version 2.3.5 -* Fix - Properly handle shipping address depending on 'Ship to different address' checked or not. -* Fix - Make SOAP request to VIES directly to help prevent IPs being blocked. - -2017-12-13 - version 2.3.4 -* Fix - WC 3.3 compatibility. - -2017-10-09 - version 2.3.3 -* Fix - Issue where exempt check uses both shipping and billing country for matching base country. - -2017-08-27 - version 2.3.2 -* Fix - Issue where VAT removal from cart is applied even if the country is the same as the base country. -* Fix - Issue where VAT number from account is always used when cart page is set to checkout. -* Fix - Fatal error that might be thrown when updating / activating. - -2017-07-25 - version 2.3.1 -* Update - Don't show vat number field in checkout if payment is not needed. -* Fix - When valid VAT is in account, remove VAT from cart. -* Fix - When b2b setting is enable, apply to only EU countries. - -2017-06-06 - version 2.3.0 -* Fix - VAT now applies to shipping country as well. -* Fix - Validate VAT against user submitted shipping and billing instead of whats on record. -* Fix - Undefined variable notices on reports. -* Fix - Shipping tax amount were not showing in reports. -* Fix - Tax by code report not showing all types of refunds. -* Remove - Switzerland from VAT definition. -* Add - Enable b2b setting to force VAT to be entered. -* Add - VAT validation on My Account VAT page. - -2017-04-13 - version 2.2.0 -* Renamed main file. -* PHP 7.1 compatibility. -* Stopped saving legacy VAT Number meta key - use _vat_number. -* Tweaked display of numbers in backend. -* Fix - error message shown when validation fails. -* Fix - Translation wrong gettext function used. - -2017-01-25 - version 2.1.14 -* Fix - Increase validation request timeout from 5 to 30 seconds. - -2016-10-08 - version 2.1.13 -* Fix - Issue where My Account endpoint doesn't initially work after activation. -* Fix - Issue where my VAT number is cleared when billing country is changed. - -2016-06-09 - version 2.1.12 -* Tweak - Support for tabbed My Account page introduced in WooCommerce 2.6 -* Tweak - Include VAT Number in order endpoints responses within REST API - -2016-04-13 - version 2.1.11 -* Fix issue where VAT number is not saved to user meta after checkout -* Fix issue of missing text domain in plugin header -* Tweak - Added filter to alter translation file location - -2016-03-02 - version 2.1.10 -* Fix incorrect IP address to display when IP is validated. This change also - introduces `wc_eu_vat_self_declared_ip_address` hook to filter the IP - address. - -2015-09-21 - version 2.1.9 -* Add VAT number field to My Account page -* Add Switzerland to list of VAT countries - -2015-08-06 - version 2.1.8 -* run vat_number_is_valid if the cached result was 0 or false. - -2015-03-12 - version 2.1.7 -* Add report for non-eu VAT taxes in the same format as the EU VAT report. Useful for accounting. - -2015-03-12 - version 2.1.6 -* Fix report ranges. - -2015-03-12 - version 2.1.5 -* Fix textdomain - -2015-03-08 - version 2.1.4 -* Textdomain and POT update. - -2015-01-08 - version 2.1.3 -* Fix localisation support. -* Tweaked what data gets saved when dealing with regular products to avoid showing IP info when not required. - -2015-01-08 - version 2.1.2 -* Set excemption regardless of base country (maybe_set_vat_excempt method). - -2015-01-01 - version 2.1.1 -* Updated geolocation class for CloudFlare support. -* When dealing with digital goods, validate users who state that they are outside of the EU, if the IP address is inside the EU. -* Fix options when saving other tax settings. - -2014-12-30 - version 2.1.0 -* Added new geolocation class with more reliable GeoIP functionality. -* Only collect IP when dealing with digital goods in the cart. -* Force total refresh so checkbox is displayed. - -2014-10-28 - version 2.0.0 -* Refactored the plugin. -* Added additonal options for handling of orders with non-validated VAT numbers. -* Added IP validation option for orders with digital goods (in preperation for the EU place of supply laws in January). -* Changed the placement and look and feel of the VAT Number field to make it fit within the WC billing section. -* Made the VAT ID append itself to formatted billing addresses automatically. -* Added template file to control the display of the field itself. -* Added new meta box for viewing collected information. -* Added column in orders panel to show IP/VAT/other evidence. -* Added reports for EC Sales and EU Vat taxes. -* Mixed carts - force digital goods to use taxable billing address (must use tax class named 'Digital Goods') -* Added requirement for WC 2.2.9 to support the EU VAT rates. - -2014-09-01 - version 1.7.2 -* Handle SERVER_BUSY responses. -* Use transient instead of wp_cache. - -2014-09-01 - version 1.7.1 -* Base excemption on configured tax address in tax settings. - -2014-06-10 - version 1.7.0 -* Croatia -* Set meta data when validated, so times when the API fails can be tracked. - -2014-04-02 - version 1.6.0 -* Use our own woo api - -2014-02-23 - version 1.5.9 -* Option to store unvalidated numbers as-is - -2014-02-14 - version 1.5.8 -* Only add errors when you press the checkout process button - -2014-02-14 - version 1.5.7 -* Fix notices after success - -2014-02-13 - version 1.5.6 -* Clear notices before adding new ones - -2014-01-17 - version 1.5.5 -* 2.1 Compat - -2013-08-07 - version 1.5.4 -* Pass saved strings though __() - -2013-03-15 - version 1.5.3 -* Greece uses EL instead of GR - -2013-03-15 - version 1.5.1 -* Update on change field - -2013-01-11 - version 1.5.0 -* WC 2.0 Compat -* General cleanup - -2012-12-04 - version 1.4.1 -* New updater - -2012-04-10 - version 1.4 -* Added option to optionally deduct tax for shop base country if a valid number is added - -2012-04-10 - version 1.3 -* Option to show field when the customer is in the base country -* Handling for if isvat service is down - allow vat number through. - -2012-03-11 - version 1.2.2 -* Add inline JS for scripts - -2012-03-01 - version 1.2.1 -* Fixed check after removing the number -* Remove full stops from number - -2012-01-26 - version 1.2 -* WC 1.4 Compatibility -* Added Woo Updater - -2011-11-15 - version 1.1.1 -* Changed textdomain - -1.1 -* Renamed plugin since exempt is not the correct terminology -* Show/hide VAT Number form based on country -* If user is in the same country then don't allow zero rate -* VAT Number added to emails - -2011-10-26 - version 1.0.1 -* Added form-row-wide class -* Added heading + option for title -* Moved to shipping column, below order details - -2011-09-27 - version 1.0 -* First Release +* Fix - Fatal errors if SOAP extension is not installed. +* Fix - Plugin functions are not available when dependency requirements are not met. + +2023-03-13 - version 2.8.2 +* Fix – Ensure WC Blocks compatibility scripts are only loaded when on a page containing WC Blocks. +* Dev – Bump PHP minimum supported version from 7.0 to 7.2. +* Dev – Bump WooCommerce minimum supported version from 6.0 to 6.8. +* Dev – Bump WooCommerce “tested up to” version 7.4. +* Dev – Resolve deprecation warning in SCSS source. + +2023-02-27 - version 2.8.1 +* Fix – UK VAT number validation issue. +* Tweak – Bump WooCommerce tested up to 7.3.0 + +2022-12-01 - version 2.8.0 +* Add – Ability to Collect and Validate Evidence in the WooCommerce Blocks Checkout. +* Add – Add the EU VAT Number block as a form step in the WooCommerce Blocks Checkout. +* Add – Support for High-performance Order Storage (“HPOS”) (formerly known as Custom Order Tables, “COT”). +* Add – Output a zero tax line item with the reason why it’s zero in both the order email and invoice. +* Add – New filter wc_eu_vat_number_zero_tax_reason used to modify the zero tax reason text. +* Add – Implement support for automatic translation files delivery. +* Add – Declare support for High-performance Order Systems (“HPOS”). +* Fix – Issue with updating VAT code from the My Account screen. +* Fix – Errors in PHP Coding Standards. +* Fix – Checkout VAT field validation on failure. +* Tweak – WC tested up to 7.1. +* Tweak – Bump minimum WP version to 5.6. +* Tweak – Bump minimum PHP version to 7.0. +* Tweak – Bump minimum WC version to 6.0. +* Tweak – Simplified notices text. +* Dev – Update node version from 8.9.3 to 16.13.0. +* Dev – Update npm version from 5.5.1 to 8.0.0. + +2022-11-01 - version 2.7.0 +* Add - Support for checking out with a EU vat number field using the checkout block from WooCommerce Blocks. + +2022-09-06 - version 2.6.0 +* Add - Support for UK VAT Number validation (using UK VAT Number API). +* Fix - VAT validation when the field is empty. + +2022-07-25 - version 2.5.0 +* Fix – Add float Type Casting to refunded tax to prevent the “non-numeric value” warning. +* Fix – Display product price without VAT if a valid VAT number is saved in the customer account. +* Update – Display a more specific message for error in connect to vies server from local environments. + +2022-06-20 - version 2.4.3 +* Tweak - Bump Tested up to + +2022-02-22 - version 2.4.2 +* New - Add support for Subscriptions with WooCommerce Payments. +* Fix - Removes VAT charge from the scheduled orders. +* Fix - PHP notice when placing an order with no VAT number. +* Fix - "Failed Validation Handling" setting not working properly for manual admin order. +* Fix - POT file not included in release zip. +* Fix - Apply EU VAT straight away when creating a manual order. + +2021-08-04 - version 2.4.1 +* Fix - Rendering the VAT field when store is selling to only one country. PR#207 +* Fix - Changing the country from non-EU to EU doesn't display the VAT field. PR#207 +* Fix - Compatibility with WooCommerce One Page Checkout. PR#208 +* Fix - VAT field not displayed on WooCommerce Subscriptions switch orders. PR#208 +* Fix - VAT validation for WooCommerce Subscripitons products with a free trial. PR#208 + +2021-07-22 - version 2.4.0 +* New - [Brexit] Add Northern Ireland (XI) VAT validation and remove GB VAT validation. PR#202 +* Fix - Properly display error messages if server IP is blocked by VIES. PR#202 +* Fix - VAT Validation on orders created via WP Admin dashboard. PR#202 +* Fix - Support for checkout field editor plugins. PR#200 +* Fix - Issues with My Account -> VAT Number endpoint when My Account is set as homepage. PR#204 + +2021-04-07 - version 2.3.26 +* Update - Fixes documentation and support links. +* Fix - Trigger VAT number validation only on change. +* Fix - Properly handle EU VAT privacy data erasure. +* Update - JS updated for jQuery 3 compatibility. +* Fix - Modified VIES API request to use HTTPS + +2020-09-30 - version 2.3.25 +* Fix - VAT Number required when B2B not enabled. + +2020-09-30 - version 2.3.24 +* Fix - Automatically populate VAT Number at checkout if saved to user profile. +* Fix - VAT Number showing as required for non-EU countries when switching billing country. +* Tweak - Lower VAT Number transient expiration time from 7 days to 1 day. +* Tweak - Improve error message when saving VAT Number on My Account if billing address is missing. + +2020-08-19 - version 2.3.23 +* Tweak - WordPress 5.5 compatibility. + +2020-07-14 - version 2.3.22 +* Fix - PHP error on checkout page if order total is zero. + +2020-07-01 - version 2.3.21 +* Add - VAT number field to user/customer profile. +* Add - VAT number of a customer when adding a new order from admin. +* Fix - VAT not validating correctly in checkout in all acceptance types. +* Tweak - Use EL country code for Greece when showing tax reports. +* Tweak - Treat Isle of Man the same as UK in regards to tax exemption. + +2020-06-10 - version 2.3.20 +* Tweak - WC 4.2 compatibility. + +2020-04-29 - version 2.3.19 +* Tweak - Remove legacy code. +* Tweak - WC 4.0 compatibility. +* Fix - VAT incorrectly being removed when Recalculate button is clicked on the order. + +2020-02-04 - version 2.3.18 +* Fix - Use proper escaping for attributes. + +2020-01-13 - version 2.3.17 +* Tweak - WC 3.9 compatibility. + +2019-11-05 - version 2.3.16 +* Fix - Update WC header. + +2019-11-05 - version 2.3.15 +* Tweak - WC 3.8 compatibility. + +2019-08-08 - version 2.3.14 +* Fix - Adding/editing VAT in admin new order or edit order not updating VAT amount. +* Tweak - WC 3.7 compatibility. + +2019-04-16 - version 2.3.13 +* Tweak - WC 3.6 compatibility. + +2019-01-07 - version 2.3.12 +* Fix - Always show VAT number field if b2b is enabled. + +2018-09-26 - version 2.3.11 +* Update - WC tested up to 3.5 + +2018-08-21 - version 2.3.10 +* Fix - Re-deployment to fix VAT numbers being removed when order is saved. + +2018-08-21 - version 2.3.9 +* Fix - VAT numbers were removed when order is saved. + +2018-08-02 - version 2.3.8 +* Fix - Properly handle shipping address depending on 'Ship to different address' checked or not. + +2018-05-22 - version 2.3.7 +* Fix - PHP deprecation notice for 'parse_str'. +* Fix - Increasing timeout for SOAP requests. +* Fix - Show SOAP requirement notice and disable VAT lookups if needed. +* Add - Added woocommerce_eu_vat_number_soap_parameters filter for modifying SOAP request parameters. +* Fix - Use proper parse_str format in ajax_update_checkout_totals. +* Update - WC tested up to 3.4 +* Update - Privacy policy notification. +* Update - Export/erasure hooks added. + +2018-02-05 - version 2.3.6 +* Fix - Cached VAT lookups not applying proper exemptions +* New - Adds a filter to be able to control which countries charge VAT, via code. + +2018-01-17 - version 2.3.5 +* Fix - Properly handle shipping address depending on 'Ship to different address' checked or not. +* Fix - Make SOAP request to VIES directly to help prevent IPs being blocked. + +2017-12-13 - version 2.3.4 +* Fix - WC 3.3 compatibility. + +2017-10-09 - version 2.3.3 +* Fix - Issue where exempt check uses both shipping and billing country for matching base country. + +2017-08-27 - version 2.3.2 +* Fix - Issue where VAT removal from cart is applied even if the country is the same as the base country. +* Fix - Issue where VAT number from account is always used when cart page is set to checkout. +* Fix - Fatal error that might be thrown when updating / activating. + +2017-07-25 - version 2.3.1 +* Update - Don't show vat number field in checkout if payment is not needed. +* Fix - When valid VAT is in account, remove VAT from cart. +* Fix - When b2b setting is enable, apply to only EU countries. + +2017-06-06 - version 2.3.0 +* Fix - VAT now applies to shipping country as well. +* Fix - Validate VAT against user submitted shipping and billing instead of whats on record. +* Fix - Undefined variable notices on reports. +* Fix - Shipping tax amount were not showing in reports. +* Fix - Tax by code report not showing all types of refunds. +* Remove - Switzerland from VAT definition. +* Add - Enable b2b setting to force VAT to be entered. +* Add - VAT validation on My Account VAT page. + +2017-04-13 - version 2.2.0 +* Renamed main file. +* PHP 7.1 compatibility. +* Stopped saving legacy VAT Number meta key - use _vat_number. +* Tweaked display of numbers in backend. +* Fix - error message shown when validation fails. +* Fix - Translation wrong gettext function used. + +2017-01-25 - version 2.1.14 +* Fix - Increase validation request timeout from 5 to 30 seconds. + +2016-10-08 - version 2.1.13 +* Fix - Issue where My Account endpoint doesn't initially work after activation. +* Fix - Issue where my VAT number is cleared when billing country is changed. + +2016-06-09 - version 2.1.12 +* Tweak - Support for tabbed My Account page introduced in WooCommerce 2.6 +* Tweak - Include VAT Number in order endpoints responses within REST API + +2016-04-13 - version 2.1.11 +* Fix issue where VAT number is not saved to user meta after checkout +* Fix issue of missing text domain in plugin header +* Tweak - Added filter to alter translation file location + +2016-03-02 - version 2.1.10 +* Fix incorrect IP address to display when IP is validated. This change also + introduces `wc_eu_vat_self_declared_ip_address` hook to filter the IP + address. + +2015-09-21 - version 2.1.9 +* Add VAT number field to My Account page +* Add Switzerland to list of VAT countries + +2015-08-06 - version 2.1.8 +* run vat_number_is_valid if the cached result was 0 or false. + +2015-03-12 - version 2.1.7 +* Add report for non-eu VAT taxes in the same format as the EU VAT report. Useful for accounting. + +2015-03-12 - version 2.1.6 +* Fix report ranges. + +2015-03-12 - version 2.1.5 +* Fix textdomain + +2015-03-08 - version 2.1.4 +* Textdomain and POT update. + +2015-01-08 - version 2.1.3 +* Fix localisation support. +* Tweaked what data gets saved when dealing with regular products to avoid showing IP info when not required. + +2015-01-08 - version 2.1.2 +* Set excemption regardless of base country (maybe_set_vat_excempt method). + +2015-01-01 - version 2.1.1 +* Updated geolocation class for CloudFlare support. +* When dealing with digital goods, validate users who state that they are outside of the EU, if the IP address is inside the EU. +* Fix options when saving other tax settings. + +2014-12-30 - version 2.1.0 +* Added new geolocation class with more reliable GeoIP functionality. +* Only collect IP when dealing with digital goods in the cart. +* Force total refresh so checkbox is displayed. + +2014-10-28 - version 2.0.0 +* Refactored the plugin. +* Added additonal options for handling of orders with non-validated VAT numbers. +* Added IP validation option for orders with digital goods (in preperation for the EU place of supply laws in January). +* Changed the placement and look and feel of the VAT Number field to make it fit within the WC billing section. +* Made the VAT ID append itself to formatted billing addresses automatically. +* Added template file to control the display of the field itself. +* Added new meta box for viewing collected information. +* Added column in orders panel to show IP/VAT/other evidence. +* Added reports for EC Sales and EU Vat taxes. +* Mixed carts - force digital goods to use taxable billing address (must use tax class named 'Digital Goods') +* Added requirement for WC 2.2.9 to support the EU VAT rates. + +2014-09-01 - version 1.7.2 +* Handle SERVER_BUSY responses. +* Use transient instead of wp_cache. + +2014-09-01 - version 1.7.1 +* Base excemption on configured tax address in tax settings. + +2014-06-10 - version 1.7.0 +* Croatia +* Set meta data when validated, so times when the API fails can be tracked. + +2014-04-02 - version 1.6.0 +* Use our own woo api + +2014-02-23 - version 1.5.9 +* Option to store unvalidated numbers as-is + +2014-02-14 - version 1.5.8 +* Only add errors when you press the checkout process button + +2014-02-14 - version 1.5.7 +* Fix notices after success + +2014-02-13 - version 1.5.6 +* Clear notices before adding new ones + +2014-01-17 - version 1.5.5 +* 2.1 Compat + +2013-08-07 - version 1.5.4 +* Pass saved strings though __() + +2013-03-15 - version 1.5.3 +* Greece uses EL instead of GR + +2013-03-15 - version 1.5.1 +* Update on change field + +2013-01-11 - version 1.5.0 +* WC 2.0 Compat +* General cleanup + +2012-12-04 - version 1.4.1 +* New updater + +2012-04-10 - version 1.4 +* Added option to optionally deduct tax for shop base country if a valid number is added + +2012-04-10 - version 1.3 +* Option to show field when the customer is in the base country +* Handling for if isvat service is down - allow vat number through. + +2012-03-11 - version 1.2.2 +* Add inline JS for scripts + +2012-03-01 - version 1.2.1 +* Fixed check after removing the number +* Remove full stops from number + +2012-01-26 - version 1.2 +* WC 1.4 Compatibility +* Added Woo Updater + +2011-11-15 - version 1.1.1 +* Changed textdomain + +1.1 +* Renamed plugin since exempt is not the correct terminology +* Show/hide VAT Number form based on country +* If user is in the same country then don't allow zero rate +* VAT Number added to emails + +2011-10-26 - version 1.0.1 +* Added form-row-wide class +* Added heading + option for title +* Moved to shipping column, below order details + +2011-09-27 - version 1.0 +* First Release diff --git a/eu-vat-number.php b/eu-vat-number.php index 8b88f2a..3933827 100644 --- a/eu-vat-number.php +++ b/eu-vat-number.php @@ -1,19 +1,19 @@ - $active_plugin ) { - if ( strstr( $active_plugin, '/eu-vat-number.php' ) ) { - $active_plugins[ $key ] = str_replace( '/eu-vat-number.php', '/woocommerce-eu-vat-number.php', $active_plugin ); - } -} -update_option( 'active_plugins', $active_plugins ); + $active_plugin ) { + if ( strstr( $active_plugin, '/eu-vat-number.php' ) ) { + $active_plugins[ $key ] = str_replace( '/eu-vat-number.php', '/woocommerce-eu-vat-number.php', $active_plugin ); + } +} +update_option( 'active_plugins', $active_plugins ); diff --git a/includes/class-wc-eu-vat-admin.php b/includes/class-wc-eu-vat-admin.php index 8f22d21..73d226d 100644 --- a/includes/class-wc-eu-vat-admin.php +++ b/includes/class-wc-eu-vat-admin.php @@ -1,508 +1,518 @@ - get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), - 'show' => false, - 'id' => '_billing_vat_number', - 'value' => $vat_number, - ); - return $fields; - } - - /** - * Add Meta Boxes. - */ - public static function add_meta_boxes() { - if ( class_exists( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ) ) { - $screen = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() - ? wc_get_page_screen_id( 'shop-order' ) - : 'shop_order'; - } else { - $screen = 'shop_order'; - } - - add_meta_box( 'wc_eu_vat', __( 'EU VAT', 'woocommerce-eu-vat-number' ), array( __CLASS__, 'output' ), $screen, 'side' ); - } - - /** - * Enqueue admin styles and scripts - * - * @param string $hook The current admin page. - * @return void - */ - public static function styles_and_scripts( $hook ) { - global $post; - - $is_order_edit_screen = false; - - if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['id'] ) ) { - $is_order_edit_screen = true; - } else if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['action'] ) && 'new' === wp_unslash( sanitize_text_field( $_GET['action'] ) ) ) { - $is_order_edit_screen = true; - } else if ( in_array( $hook, array( 'post-new.php', 'post.php' ), true ) && $post && 'shop_order' === $post->post_type ) { - $is_order_edit_screen = true; - } - - // Load admin style. - wp_enqueue_style( 'wc_eu_vat_admin_css', plugins_url( 'assets/css/admin.css', WC_EU_VAT_FILE ), array(), WC_EU_VAT_VERSION ); - - // Load script only on add/edit order page. - if ( $is_order_edit_screen ) { - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; - wp_enqueue_script( 'wc-eu-vat-admin', WC_EU_VAT_PLUGIN_URL . '/assets/js/admin' . $suffix . '.js', array( 'jquery' ), WC_EU_VAT_VERSION, true ); - } - } - - /** - * Is this is an EU order? - * - * @param WC_Order $order The order object. - * @return boolean - */ - protected static function is_eu_order( $order ) { - return in_array( $order->get_billing_country(), WC_EU_VAT_Number::get_eu_countries(), true ); - } - - /** - * Get order VAT Number data in one object/array. - * - * @param WC_Order $order The order object. - * @return object - */ - protected static function get_order_vat_data( $order ) { - return (object) array( - 'vat_number' => wc_eu_vat_get_vat_from_order( $order ), - 'valid' => wc_string_to_bool( $order->get_meta( '_vat_number_is_valid', true ) ), - 'validated' => wc_string_to_bool( $order->get_meta( '_vat_number_is_validated', true ) ), - 'billing_country' => $order->get_billing_country(), - 'ip_address' => $order->get_customer_ip_address(), - 'ip_country' => $order->get_meta( '_customer_ip_country', true ), - 'self_declared' => wc_string_to_bool( $order->get_meta( '_customer_self_declared_country', true ) ), - ); - } - - /** - * Output meta box. - */ - public static function output() { - global $post, $theorder; - - if ( ! is_object( $theorder ) ) { - $theorder = wc_get_order( $post->ID ); - } - - // We only need this box for EU orders. - if ( ! self::is_eu_order( $theorder ) ) { - ?> -

- -

- countries->get_countries(); - ?> - - - - - - - - - vat_number ) : ?> - - - - - - - - - - - - - - - - - - - - - - - -
vat_number ? esc_html__( 'Yes', 'woocommerce-eu-vat-number' ) : esc_html__( 'No', 'woocommerce-eu-vat-number' ); ?>
vat_number ); ?> - validated ) { - echo '?'; - } else { - echo $data->valid ? '✔' : '✘'; - } - ?> -
ip_address ? esc_html( $data->ip_address ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?>
- ip_country ) { - echo esc_html( $countries[ $data->billing_country ] ) . ' '; - - if ( $data->billing_country === $data->ip_country ) { - echo ''; - } elseif ( $data->self_declared ) { - esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); - } else { - echo ''; - } - } else { - esc_html_e( 'Unknown', 'woocommerce-eu-vat-number' ); - } - ?> -
billing_country ? esc_html( $countries[ $data->billing_country ] ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?>
- $existing_column ) { - $columns[ $existing_column_key ] = $existing_column; - - if ( 'shipping_address' === $existing_column_key ) { - $columns['eu_vat'] = __( 'EU VAT', 'woocommerce-eu-vat-number' ); - } - } - - return $columns; - } - - /** - * Show Column. - * - * @param string $column Column being shown. - */ - public static function show_column( $column, $order ) { - - if ( is_numeric( $order ) ) { - $order = wc_get_order( $order ); - } - - if ( 'eu_vat' === $column ) { - echo '

'; - - if ( ! self::is_eu_order( $order ) ) { - echo ''; - } else { - $data = self::get_order_vat_data( $order ); - - if ( $data->vat_number ) { - echo esc_html( $data->vat_number ) . ' '; - - if ( $data->validated && $data->valid ) { - echo ''; - } elseif ( ! $data->validated ) { - esc_html_e( '(validation failed)', 'woocommerce-eu-vat-number' ); - } else { - echo ''; - } - } else { - $countries = WC()->countries->get_countries(); - - echo esc_html( $countries[ $data->billing_country ] ) . ' '; - - if ( $data->billing_country === $data->ip_country ) { - echo ''; - } elseif ( $data->self_declared ) { - esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); - } else { - echo ''; - } - } - } - echo '

'; - } - } - - /** - * Handles VAT when order is created/edited within admin manually. - * - * @since 2.3.14 - * @param array $args Additional arguments. - * @param object $order WooCommerce Order Object. - * @throws Exception Error message if VAT validation fails. - */ - public static function admin_order( $args, $order ) { - if ( ! is_object( $order ) ) { - return; - } - - /* - * First try and get the billing country from the - * address form (adding new order). If it is not - * found, get it from the order (editing the order). - */ - $billing_country = isset( $_POST['_billing_country'] ) ? wc_clean( wp_unslash( $_POST['_billing_country'] ) ) : $order->get_billing_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - $shipping_country = isset( $_POST['_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['_shipping_country'] ) ) : $order->get_shipping_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - $billing_postcode = isset( $_POST['_billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['_billing_postcode'] ) ) : $order->get_billing_postcode(); // phpcs:ignore WordPress.Security.NonceVerification.Missing - - /* - * First try and get the VAT number from the - * address form (adding new order). If it is not - * found, get it from the order (editing the order). - */ - $vat_number = isset( $_POST['_billing_vat_number'] ) ? wc_clean( wp_unslash( $_POST['_billing_vat_number'] ) ) : wc_eu_vat_get_vat_from_order( $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - - // Ignore empty VAT Number and countries outside EU. - if ( empty( $vat_number ) || ! in_array( $billing_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { - return; - } - - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - $base_country_match = WC_EU_VAT_Number::is_base_country_match( $billing_country, $shipping_country ); - - if ( 'no' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) && $base_country_match ) { - add_filter( 'woocommerce_order_is_vat_exempt', '__return_false' ); - return; - } - - $order->update_meta_data( '_vat_number_is_validated', 'true' ); - - try { - if ( true === $valid ) { - $order->update_meta_data( '_vat_number_is_valid', 'true' ); - add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); - return; - } - - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - // Exempt VAT even if VAT number is not valid as per "Failed Validation Handling" settings. - if ( 'accept' === $fail_handler ) { - add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); - } - if ( is_wp_error( $valid ) ) { - throw new Exception( $valid->get_error_message() ); - } - - if ( ! $valid ) { - // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. - throw new Exception( sprintf( esc_html__( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); - } - } catch ( Exception $e ) { - $order->update_meta_data( '_vat_number_is_valid', 'false' ); - echo ''; - } - } - - /** - * Adds custom fields to user profile. - * - * @since 2.3.21 - * @param array $fields WC defined user fields. - * @return array $fields Modified user fields. - */ - public static function add_customer_meta_fields( $fields ) { - $fields['billing']['fields']['vat_number'] = array( - 'label' => esc_html__( 'VAT number', 'woocommerce-eu-vat-number' ), - 'description' => '', - ); - - return $fields; - } - - /** - * Return VAT information to get customer details via AJAX. - * - * @since 2.3.21 - * @param array $data The customer's data in context. - * @param object $customer The customer object in context. - * @param int $user_id The user ID in context. - * @return array $data Modified user data. - */ - public static function get_customer_details( $data, $customer, $user_id ) { - $data['billing']['vat_number'] = get_user_meta( $user_id, 'vat_number', true ); - - return $data; - } - - /** - * Display admin notice for EU VAT Number. - */ - public static function maybe_show_admin_notice() { - - $screen = get_current_screen(); - $screen_id = $screen ? $screen->id : ''; - - if ( 'woocommerce_page_wc-settings' !== $screen_id ) { - return; - } - - // Check whether disclaimer is already dismissed or taxes are not enabled. - if ( ! wc_tax_enabled() || 'yes' === get_option( 'woocommerce_eu_vat_number_dismiss_disclaimer', 'no' ) ) { - return; - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( isset( $_GET[ 'tab' ] ) && 'tax' === $_GET[ 'tab' ] ) { - $screen_id .= '_tax'; - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended - if ( ! isset( $_GET[ 'tab' ] ) || ( isset( $_GET[ 'tab' ] ) && 'general' === $_GET[ 'tab' ] ) ) { - $screen_id .= '_general'; - } - - $show_on_screens = array( - 'woocommerce_page_wc-settings_tax', - 'woocommerce_page_wc-settings_general', - ); - - if ( ! in_array( $screen_id, $show_on_screens, true ) ) { - return; - } - - $base_country = WC()->countries->get_base_country(); - $base_postcode = WC()->countries->get_base_postcode(); - - // Create URL to dismiss the disclaimer. - $dismiss_url = add_query_arg( - array( - 'dismiss_eu_vat_disclaimer' => 'yes', - ), - wc_get_current_admin_url() - ); - ?> - -
- -

- ', '', '
' ); - ?> -

-
- - -

- ', '' ); - ?> -

- -

- -

-
- get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'show' => false, + 'id' => '_billing_vat_number', + 'value' => $vat_number, + ); + return $fields; + } + + /** + * Add Meta Boxes. + */ + public static function add_meta_boxes() { + if ( class_exists( 'Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController' ) ) { + $screen = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() + ? wc_get_page_screen_id( 'shop-order' ) + : 'shop_order'; + } else { + $screen = 'shop_order'; + } + + add_meta_box( 'wc_eu_vat', __( 'EU VAT', 'woocommerce-eu-vat-number' ), array( __CLASS__, 'output' ), $screen, 'side' ); + } + + /** + * Enqueue admin styles and scripts + * + * @param string $hook The current admin page. + * @return void + */ + public static function styles_and_scripts( $hook ) { + global $post; + + $is_order_edit_screen = false; + + if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['id'] ) ) { + $is_order_edit_screen = true; + } else if ( 'woocommerce_page_wc-orders' === $hook && isset( $_GET['action'] ) && 'new' === wp_unslash( sanitize_text_field( $_GET['action'] ) ) ) { + $is_order_edit_screen = true; + } else if ( in_array( $hook, array( 'post-new.php', 'post.php' ), true ) && $post && 'shop_order' === $post->post_type ) { + $is_order_edit_screen = true; + } + + // Load admin style. + wp_enqueue_style( 'wc_eu_vat_admin_css', plugins_url( 'assets/css/admin.css', WC_EU_VAT_FILE ), array(), WC_EU_VAT_VERSION ); + + // Load script only on add/edit order page. + if ( $is_order_edit_screen ) { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + wp_enqueue_script( 'wc-eu-vat-admin', WC_EU_VAT_PLUGIN_URL . '/assets/js/admin' . $suffix . '.js', array( 'jquery' ), WC_EU_VAT_VERSION, true ); + } + } + + /** + * Is this is an EU order? + * + * @param WC_Order $order The order object. + * @return boolean + */ + protected static function is_eu_order( $order ) { + return in_array( $order->get_billing_country(), WC_EU_VAT_Number::get_eu_countries(), true ); + } + + /** + * Get order VAT Number data in one object/array. + * + * @param WC_Order $order The order object. + * @return object + */ + protected static function get_order_vat_data( $order ) { + return (object) array( + 'vat_number' => wc_eu_vat_get_vat_from_order( $order ), + 'valid' => wc_string_to_bool( $order->get_meta( '_vat_number_is_valid', true ) ), + 'validated' => wc_string_to_bool( $order->get_meta( '_vat_number_is_validated', true ) ), + 'billing_country' => $order->get_billing_country(), + 'ip_address' => $order->get_customer_ip_address(), + 'ip_country' => $order->get_meta( '_customer_ip_country', true ), + 'self_declared' => wc_string_to_bool( $order->get_meta( '_customer_self_declared_country', true ) ), + ); + } + + /** + * Output meta box. + */ + public static function output() { + global $post, $theorder; + + if ( ! is_object( $theorder ) ) { + $theorder = wc_get_order( $post->ID ); + } + + // We only need this box for EU orders. + if ( ! self::is_eu_order( $theorder ) ) { + ?> +

+ +

+ countries->get_countries(); + ?> + + + + + + + + + vat_number ) : ?> + + + + + + + + + + + + + + + + + + + + + + + +
vat_number ? esc_html__( 'Yes', 'woocommerce-eu-vat-number' ) : esc_html__( 'No', 'woocommerce-eu-vat-number' ); ?>
vat_number ); ?> + validated ) { + echo '?'; + } else { + echo $data->valid ? '✔' : '✘'; + } + ?> +
ip_address ? esc_html( $data->ip_address ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?>
+ ip_country ) { + echo esc_html( $countries[ $data->billing_country ] ) . ' '; + + if ( $data->billing_country === $data->ip_country ) { + echo ''; + } elseif ( $data->self_declared ) { + esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); + } else { + echo ''; + } + } else { + esc_html_e( 'Unknown', 'woocommerce-eu-vat-number' ); + } + ?> +
billing_country ? esc_html( $countries[ $data->billing_country ] ) : esc_html__( 'Unknown', 'woocommerce-eu-vat-number' ); ?>
+ $existing_column ) { + $columns[ $existing_column_key ] = $existing_column; + + if ( 'shipping_address' === $existing_column_key ) { + $columns['eu_vat'] = __( 'EU VAT', 'woocommerce-eu-vat-number' ); + } + } + + return $columns; + } + + /** + * Show Column. + * + * @param string $column Column being shown. + */ + public static function show_column( $column, $order ) { + + if ( is_numeric( $order ) ) { + $order = wc_get_order( $order ); + } + + if ( 'eu_vat' === $column ) { + echo '

'; + + if ( ! self::is_eu_order( $order ) ) { + echo ''; + } else { + $data = self::get_order_vat_data( $order ); + + if ( $data->vat_number ) { + echo esc_html( $data->vat_number ) . ' '; + + if ( $data->validated && $data->valid ) { + echo ''; + } elseif ( ! $data->validated ) { + esc_html_e( '(validation failed)', 'woocommerce-eu-vat-number' ); + } else { + echo ''; + } + } else { + $countries = WC()->countries->get_countries(); + + echo esc_html( $countries[ $data->billing_country ] ) . ' '; + + if ( $data->billing_country === $data->ip_country ) { + echo ''; + } elseif ( $data->self_declared ) { + esc_html_e( '(self-declared)', 'woocommerce-eu-vat-number' ); + } else { + echo ''; + } + } + } + echo '

'; + } + } + + /** + * Handles VAT when order is created/edited within admin manually. + * + * @since 2.3.14 + * @param array $args Additional arguments. + * @param object $order WooCommerce Order Object. + * @throws Exception Error message if VAT validation fails. + */ + public static function admin_order( $args, $order ) { + if ( ! is_object( $order ) ) { + return; + } + + /* + * First try and get the billing country from the + * address form (adding new order). If it is not + * found, get it from the order (editing the order). + */ + $billing_country = isset( $_POST['_billing_country'] ) ? wc_clean( wp_unslash( $_POST['_billing_country'] ) ) : $order->get_billing_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $shipping_country = isset( $_POST['_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['_shipping_country'] ) ) : $order->get_shipping_country(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + $billing_postcode = isset( $_POST['_billing_postcode'] ) ? wc_clean( wp_unslash( $_POST['_billing_postcode'] ) ) : $order->get_billing_postcode(); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + /* + * First try and get the VAT number from the + * address form (adding new order). If it is not + * found, get it from the order (editing the order). + */ + $vat_number = isset( $_POST['_billing_vat_number'] ) ? wc_clean( wp_unslash( $_POST['_billing_vat_number'] ) ) : wc_eu_vat_get_vat_from_order( $order ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + + // Ignore empty VAT Number and countries outside EU. + if ( empty( $vat_number ) || ! in_array( $billing_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { + return; + } + + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + $base_country_match = WC_EU_VAT_Number::is_base_country_match( $billing_country, $shipping_country ); + + if ( 'no' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) && $base_country_match ) { + add_filter( 'woocommerce_order_is_vat_exempt', '__return_false' ); + return; + } + + $order->update_meta_data( '_vat_number_is_validated', 'true' ); + + try { + if ( true === $valid ) { + $order->update_meta_data( '_vat_number_is_valid', 'true' ); + add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); + return; + } + + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + // Exempt VAT even if VAT number is not valid as per "Failed Validation Handling" settings. + if ( 'accept' === $fail_handler ) { + add_filter( 'woocommerce_order_is_vat_exempt', '__return_true' ); + } + if ( is_wp_error( $valid ) ) { + throw new Exception( $valid->get_error_message() ); + } + + if ( ! $valid ) { + // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. + throw new Exception( sprintf( esc_html__( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); + } + } catch ( Exception $e ) { + $order->update_meta_data( '_vat_number_is_valid', 'false' ); + echo ''; + } + } + + /** + * Adds custom fields to user profile. + * + * @since 2.3.21 + * @param array $fields WC defined user fields. + * @return array $fields Modified user fields. + */ + public static function add_customer_meta_fields( $fields ) { + $fields['billing']['fields']['vat_number'] = array( + 'label' => esc_html__( 'VAT number', 'woocommerce-eu-vat-number' ), + 'description' => '', + ); + + return $fields; + } + + /** + * Return VAT information to get customer details via AJAX. + * + * @since 2.3.21 + * @param array $data The customer's data in context. + * @param object $customer The customer object in context. + * @param int $user_id The user ID in context. + * @return array $data Modified user data. + */ + public static function get_customer_details( $data, $customer, $user_id ) { + $data['billing']['vat_number'] = get_user_meta( $user_id, 'vat_number', true ); + + return $data; + } + + /** + * Display admin notice for EU VAT Number. + */ + public static function maybe_show_admin_notice() { + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'woocommerce_page_wc-settings' !== $screen_id ) { + return; + } + + // Check whether disclaimer is already dismissed or taxes are not enabled. + if ( ! wc_tax_enabled() || 'yes' === get_option( 'woocommerce_eu_vat_number_dismiss_disclaimer', 'no' ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET[ 'tab' ] ) && 'tax' === $_GET[ 'tab' ] ) { + $screen_id .= '_tax'; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_GET[ 'tab' ] ) || ( isset( $_GET[ 'tab' ] ) && 'general' === $_GET[ 'tab' ] ) ) { + $screen_id .= '_general'; + } + + $show_on_screens = array( + 'woocommerce_page_wc-settings_tax', + 'woocommerce_page_wc-settings_general', + ); + + if ( ! in_array( $screen_id, $show_on_screens, true ) ) { + return; + } + + $base_country = WC()->countries->get_base_country(); + $base_postcode = WC()->countries->get_base_postcode(); + + // Create URL to dismiss the disclaimer. + $dismiss_url = add_query_arg( + array( + 'dismiss_eu_vat_disclaimer' => 'yes', + 'dismiss_eu_vat_disclaimer_nonce' => wp_create_nonce( 'dismiss_eu_vat_disclaimer' ), + ), + wc_get_current_admin_url() + ); + ?> + +
+ +

+ ', '', '
' ); + ?> +

+
+ + +

+ ', '' ); + ?> +

+ +

+ +

+
+ extend_rest_api(); - } - - /** - * When an order is completed, woocommerce_store_api_checkout_update_order_from_request is fired. This action allows - * extensions to update the customer's order. In this method, we set the customer's self-delared country and the - * country we think they are from based on their IP. - * - * @param WC_Order $order The current customer's order object. - * @param WP_REST_Request $request The API request currently being processed. - * @return void - */ - public function update_order_from_request( $order, $request ) { - if ( false !== WC_EU_VAT_Number::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); - $order->update_meta_data( '_customer_self_declared_country', ! empty( $request['extensions']['woocommerce-eu-vat-number']['location_confirmation'] ) ? 'true' : 'false' ); - } - } - - /** - * Returns an array of script handles to enqueue in the frontend context. - * - * @return string[] - */ - public function get_script_handles() { - return array( 'wc-blocks-eu-vat-scripts-frontend' ); - } - - /** - * Returns an array of script handles to enqueue in the editor context. - * - * @return string[] - */ - public function get_editor_script_handles() { - return array( 'wc-blocks-eu-vat-scripts-frontend' ); - } - - - /** - * An array of key, value pairs of data made available to the block on the client side. - * - * @return array - */ - public function get_script_data() { - return array( - 'ip_country' => WC_EU_VAT_Number::get_ip_country(), - 'ip_address' => apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ), - 'woocommerce_eu_vat_number_validate_ip' => get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ), - ); - } - - /** - * Stores Rest Extending instance. - * - * @var ExtendRestApi - */ - private static $extend; - - /** - * Adding order meta data that are part of the plugin of orders created using the - * checkout block. - * - * @param WC_Order $order The order being placed. - * @return void - */ - public function update_order_meta( $order ) { - - if ( ! $order instanceof \WC_Order ) { - return; - } - - $vat_number = WC()->session->get( 'vat-number' ); - if ( ! $vat_number ) { - return; - } - - $data = $this::validate(); - $order->update_meta_data( '_vat_number_is_validated', ! is_null( $data['validation']['valid'] ) ? 'true' : 'false' ); - $order->update_meta_data( '_vat_number_is_valid', true === $data['validation']['valid'] ? 'true' : 'false' ); - $order->update_meta_data( '_billing_vat_number', $vat_number ); - - if ( false !== WC_EU_VAT_Number::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); - } - - $this->maybe_apply_exemption(); - } - - /** - * Registers extensions to two rest API endpoints: - * 1 - a cart update endpoint to get the VAT number after it has been typed and check for - * vat excepmption while on the checkout form. - * 2 - a checkout endpoint extension to inform our frontend component about the result of - * the validity of the VAT number and react accordingly. - * - * @return void - */ - public function extend_rest_api() { - - /** - * A cart update endpoint to get the VAT number after it has been typed and check for - * vat exemption while on the checkout form. - */ - $extend = StoreApi::container()->get( ExtendSchema::class ); - - $extend->register_update_callback( - array( - 'namespace' => 'woocommerce-eu-vat-number', - 'callback' => function( $data ) { - if ( isset( $data['vat_number'] ) ) { - if ( empty( $data['vat_number'] ) ) { - WC()->session->set( 'vat-number', null ); - WC()->customer->set_is_vat_exempt( false ); - } else { - WC()->session->set( 'vat-number', strtoupper( $data['vat_number'] ) ); - $this->maybe_apply_exemption( false ); - } - } else { - WC()->session->set( 'vat-number', null ); - WC()->customer->set_is_vat_exempt( false ); - } - }, - ) - ); - - /** - * A checkout endpoint extension to inform our frontend component about the result of - * the validity of the VAT number and react accordingly. - */ - $extend->register_endpoint_data( - array( - 'endpoint' => CartSchema::IDENTIFIER, - 'namespace' => 'woocommerce-eu-vat-number', - 'data_callback' => array( $this, 'vat_number_information' ), - 'schema_callback' => array( $this, 'schema_for_vat_number_information' ), - 'schema_type' => ARRAY_A, - ) - ); - - /** - * A checkout endpoint to accept the location_confirmation key. - */ - $extend->register_endpoint_data( - array( - 'endpoint' => CheckoutSchema::IDENTIFIER, - 'namespace' => 'woocommerce-eu-vat-number', - 'schema_callback' => function() { - return array( - 'location_confirmation' => array( - 'description' => __( 'Location confirmation.', 'woocommerce-eu-vat-number' ), - 'type' => 'boolean', - 'context' => array(), - ), - ); - }, - 'schema_type' => ARRAY_A, - ) - ); - } - - /** - * Information about the status of the given VAT Number. - * - * @return Array Information about the validity of the VAT Number. - */ - public function vat_number_information() { - return array_merge( $this::validate(), array( 'cart_has_digital_goods' => WC_EU_VAT_Number::cart_has_digital_goods() ) ); - } - - /** - * Checks if VAT number is formatted correctly. - * - * @return Array Information about the result of the validation. - */ - public function validate() { - $data = array(); - $vat_number = WC()->session->get( 'vat-number' ); - $country = WC()->customer->get_billing_country(); - $postcode = WC()->customer->get_billing_postcode(); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country, $postcode ); - - if ( is_wp_error( $valid ) ) { - $data['vat_number'] = $vat_number; - $data['validation'] = array( - 'valid' => false, - 'error' => $valid->get_error_message(), - ); - return $data; - } - - $vat_number_formatted = WC_EU_VAT_Number::get_formatted_vat_number( $vat_number ); - $data['vat_number'] = $valid ? WC_EU_VAT_Number::get_vat_number_prefix( $country ) . $vat_number_formatted : $vat_number; - $data['validation'] = array( - 'valid' => $valid, - 'error' => false, - ); - - if ( 'reject' === $fail_handler && ( ! $valid || ! $vat_number ) ) { - $data['validation']['error'] = ! $valid ? __( 'Invalid VAT number.', 'woocommerce-eu-vat-number' ) : false; - } - - return $data; - } - - /** - * Validates VAT Number and tries to apply the exemption given the information. - * - * @param boolean $with_notices Indicates whether to add notices or just run without any feedback. - * This is used while chaning the field on the checkout for and when submitting the order, hence - * the two separate use cases. - * - * @return void - */ - public function maybe_apply_exemption( $with_notices = true ) { - - $vat_number = WC()->session->get( 'vat-number' ); - - if ( ! $vat_number ) { - return; - } - - $validation = $this::validate(); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - - if ( false === (bool) $validation['validation']['valid'] && $with_notices ) { - switch ( $fail_handler ) { - case 'accept_with_vat': - wc_add_notice( $validation['validation']['error'], 'error' ); - break; - case 'accept': - break; - default: - wc_add_notice( $validation['validation']['error'], 'error' ); - break; - } - } - - $this->set_vat_exemption( $validation ); - } - - /** - * Tries to apply the exemption given the information. - * - * @param mixed $validation Result of the validation of the VAT Number. - * @return void - */ - private function set_vat_exemption( $validation ) { - - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $b_country = WC()->customer->get_billing_country(); - $s_country = WC()->customer->get_shipping_country(); - - if ( true === (bool) $validation['validation']['valid'] ) { - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); - } else { - switch ( $fail_handler ) { - case 'accept_with_vat': - WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); - break; - case 'accept': - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); - break; - default: - WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); - break; - } - } - } - - /** - * Schema for the information about the VAT Number. - * - * @return Array Information about this vat number. - */ - public function schema_for_vat_number_information() { - return array( - 'vat_data' => array( - 'description' => __( 'VAT Data', 'woocommerce-eu-vat-number' ), - 'type' => 'array', - 'readonly' => true, - ), - ); - } -} +extend_rest_api(); + } + + /** + * When an order is completed, woocommerce_store_api_checkout_update_order_from_request is fired. This action allows + * extensions to update the customer's order. In this method, we set the customer's self-delared country and the + * country we think they are from based on their IP. + * + * @param WC_Order $order The current customer's order object. + * @param WP_REST_Request $request The API request currently being processed. + * @return void + */ + public function update_order_from_request( $order, $request ) { + if ( false !== WC_EU_VAT_Number::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); + $order->update_meta_data( '_customer_self_declared_country', ! empty( $request['extensions']['woocommerce-eu-vat-number']['location_confirmation'] ) ? 'true' : 'false' ); + } + } + + /** + * Returns an array of script handles to enqueue in the frontend context. + * + * @return string[] + */ + public function get_script_handles() { + return array( 'wc-blocks-eu-vat-scripts-frontend' ); + } + + /** + * Returns an array of script handles to enqueue in the editor context. + * + * @return string[] + */ + public function get_editor_script_handles() { + return array( 'wc-blocks-eu-vat-scripts-frontend', 'wc-blocks-eu-vat-scripts-index' ); + } + + + /** + * An array of key, value pairs of data made available to the block on the client side. + * + * @return array + */ + public function get_script_data() { + return array( + 'ip_country' => WC_EU_VAT_Number::get_ip_country(), + 'ip_address' => apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ), + 'woocommerce_eu_vat_number_validate_ip' => get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ), + ); + } + + /** + * Stores Rest Extending instance. + * + * @var ExtendRestApi + */ + private static $extend; + + /** + * Adding order meta data that are part of the plugin of orders created using the + * checkout block. + * + * @param WC_Order $order The order being placed. + * @return void + */ + public function update_order_meta( $order ) { + + if ( ! $order instanceof \WC_Order ) { + return; + } + + $vat_number = WC()->session->get( 'vat-number' ); + if ( ! $vat_number ) { + return; + } + + $data = $this::validate(); + $order->update_meta_data( '_vat_number_is_validated', ! is_null( $data['validation']['valid'] ) ? 'true' : 'false' ); + $order->update_meta_data( '_vat_number_is_valid', true === $data['validation']['valid'] ? 'true' : 'false' ); + $order->update_meta_data( '_billing_vat_number', $vat_number ); + $customer_id = $order->get_customer_id(); + + if ( $customer_id ) { + $customer = new \WC_Customer( $customer_id ); + $customer->update_meta_data( 'vat_number', $vat_number ); + $customer->save_meta_data(); + } + + if ( false !== WC_EU_VAT_Number::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', WC_EU_VAT_Number::get_ip_country() ); + } + + $this->maybe_apply_exemption(); + } + + /** + * Registers extensions to two rest API endpoints: + * 1 - a cart update endpoint to get the VAT number after it has been typed and check for + * vat excepmption while on the checkout form. + * 2 - a checkout endpoint extension to inform our frontend component about the result of + * the validity of the VAT number and react accordingly. + * + * @return void + */ + public function extend_rest_api() { + + /** + * A cart update endpoint to get the VAT number after it has been typed and check for + * vat exemption while on the checkout form. + */ + $extend = StoreApi::container()->get( ExtendSchema::class ); + + $extend->register_update_callback( + array( + 'namespace' => 'woocommerce-eu-vat-number', + 'callback' => function( $data ) { + if ( isset( $data['vat_number'] ) ) { + if ( empty( $data['vat_number'] ) ) { + WC()->session->set( 'vat-number', null ); + WC()->customer->set_is_vat_exempt( false ); + } else { + WC()->session->set( 'vat-number', strtoupper( $data['vat_number'] ) ); + $this->maybe_apply_exemption( false ); + } + } else { + WC()->session->set( 'vat-number', null ); + WC()->customer->set_is_vat_exempt( false ); + } + }, + ) + ); + + /** + * A checkout endpoint extension to inform our frontend component about the result of + * the validity of the VAT number and react accordingly. + */ + $extend->register_endpoint_data( + array( + 'endpoint' => CartSchema::IDENTIFIER, + 'namespace' => 'woocommerce-eu-vat-number', + 'data_callback' => array( $this, 'vat_number_information' ), + 'schema_callback' => array( $this, 'schema_for_vat_number_information' ), + 'schema_type' => ARRAY_A, + ) + ); + + /** + * A checkout endpoint to accept the location_confirmation key. + */ + $extend->register_endpoint_data( + array( + 'endpoint' => CheckoutSchema::IDENTIFIER, + 'namespace' => 'woocommerce-eu-vat-number', + 'schema_callback' => function() { + return array( + 'location_confirmation' => array( + 'description' => __( 'Location confirmation.', 'woocommerce-eu-vat-number' ), + 'type' => 'boolean', + 'context' => array(), + ), + ); + }, + 'schema_type' => ARRAY_A, + ) + ); + } + + /** + * Information about the status of the given VAT Number. + * + * @return Array Information about the validity of the VAT Number. + */ + public function vat_number_information() { + return array_merge( $this::validate(), array( 'cart_has_digital_goods' => WC_EU_VAT_Number::cart_has_digital_goods() ) ); + } + + /** + * Checks if VAT number is formatted correctly. + * + * @return Array Information about the result of the validation. + */ + public function validate() { + $data = array(); + $vat_number = WC()->session->get( 'vat-number' ); + $country = WC()->customer->get_billing_country(); + $postcode = WC()->customer->get_billing_postcode(); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $country, $postcode ); + + if ( is_wp_error( $valid ) ) { + $data['vat_number'] = $vat_number; + $data['validation'] = array( + 'valid' => false, + 'error' => $valid->get_error_message(), + ); + return $data; + } + + $vat_number_formatted = WC_EU_VAT_Number::get_formatted_vat_number( $vat_number ); + $data['vat_number'] = $valid ? WC_EU_VAT_Number::get_vat_number_prefix( $country ) . $vat_number_formatted : $vat_number; + $data['validation'] = array( + 'valid' => $valid, + 'error' => false, + ); + + if ( 'reject' === $fail_handler && ( ! $valid || ! $vat_number ) ) { + $data['validation']['error'] = ! $valid ? __( 'Invalid VAT number.', 'woocommerce-eu-vat-number' ) : false; + } + + return $data; + } + + /** + * Validates VAT Number and tries to apply the exemption given the information. + * + * @param boolean $with_notices Indicates whether to add notices or just run without any feedback. + * This is used while chaning the field on the checkout for and when submitting the order, hence + * the two separate use cases. + * + * @return void + */ + public function maybe_apply_exemption( $with_notices = true ) { + + $vat_number = WC()->session->get( 'vat-number' ); + + if ( ! $vat_number ) { + return; + } + + $validation = $this::validate(); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + + if ( false === (bool) $validation['validation']['valid'] && $with_notices ) { + switch ( $fail_handler ) { + case 'accept_with_vat': + wc_add_notice( $validation['validation']['error'], 'error' ); + break; + case 'accept': + break; + default: + wc_add_notice( $validation['validation']['error'], 'error' ); + break; + } + } + + $this->set_vat_exemption( $validation ); + } + + /** + * Tries to apply the exemption given the information. + * + * @param mixed $validation Result of the validation of the VAT Number. + * @return void + */ + private function set_vat_exemption( $validation ) { + + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $b_country = WC()->customer->get_billing_country(); + $s_country = WC()->customer->get_shipping_country(); + + if ( true === (bool) $validation['validation']['valid'] ) { + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); + } else { + switch ( $fail_handler ) { + case 'accept_with_vat': + WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); + break; + case 'accept': + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $b_country, $s_country ); + break; + default: + WC_EU_VAT_Number::maybe_set_vat_exempt( false, $b_country, $s_country ); + break; + } + } + } + + /** + * Schema for the information about the VAT Number. + * + * @return Array Information about this vat number. + */ + public function schema_for_vat_number_information() { + return array( + 'vat_data' => array( + 'description' => __( 'VAT Data', 'woocommerce-eu-vat-number' ), + 'type' => 'array', + 'readonly' => true, + ), + ); + } +} diff --git a/includes/class-wc-eu-vat-my-account.php b/includes/class-wc-eu-vat-my-account.php index 71209a8..e164c54 100644 --- a/includes/class-wc-eu-vat-my-account.php +++ b/includes/class-wc-eu-vat-my-account.php @@ -1,261 +1,261 @@ -= 2.6. - add_action( 'init', array( $this, 'add_endpoints' ) ); - add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_vars' ), 0 ); - - // Change My Account page title. - add_filter( 'the_title', array( $this, 'endpoint_title' ) ); - - // Inserting new tab/page into My Account page. - add_filter( 'woocommerce_account_menu_items', array( $this, 'new_menu_items' ) ); - add_action( 'woocommerce_account_' . $this->endpoint . '_endpoint', array( $this, 'endpoint_content' ) ); - - // Save a VAT number from My Account form if one is submitted. - if ( isset( $_POST['action'] ) && 'edit_vat_number' === wc_clean( wp_unslash( $_POST['action'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verifying nonce inside function. - $this->save_vat_number(); - } - - add_action( 'woocommerce_init', array( $this, 'maybe_remove_vat' ) ); - } - - /** - * Checks to see if we need to remove vat from displaying in the cart and from product itself. - * - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/71 - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/74 - * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/233 - */ - public function maybe_remove_vat() { - // Ignore checkout page as on checkout page VAT exempt based on VAT number from billing fields. - if ( ( is_admin() && ! defined( 'DOING_AJAX' ) ) || ! wc_tax_enabled() || is_checkout() || ! is_user_logged_in() ) { - return; - } - - $vat_number = get_user_meta( get_current_user_id(), 'vat_number', true ); - if ( empty( $vat_number ) || empty( WC()->customer ) ) { - return; - } - - // Validate if VAT is valid. If valid, check for VAT exempt. - try { - $billing_country = WC()->customer->get_billing_country(); - $shipping_country = WC()->customer->get_shipping_country(); - - if ( $this->validate( $vat_number, $billing_country ) ) { - WC_EU_VAT_Number::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - } - } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch - // Ignore Exception. - } - } - - /** - * Register new endpoint to use inside My Account page. - * - * @since 2.1.12 - * - * @see https://developer.wordpress.org/reference/functions/add_rewrite_endpoint/ - */ - public function add_endpoints() { - add_rewrite_endpoint( $this->endpoint, EP_ROOT | EP_PAGES ); - } - - /** - * Add new query var. - * - * @since 2.1.12 - * - * @param array $vars Query vars. - * @return array - */ - public function add_query_vars( $vars ) { - $vars[] = $this->endpoint; - - return $vars; - } - - /** - * Set endpoint title. - * - * @since 2.1.12 - * - * @param string $title Endpoint title. - * @return string - */ - public function endpoint_title( $title ) { - global $wp_query; - - $is_endpoint = isset( $wp_query->query_vars[ $this->endpoint ] ); - - if ( $is_endpoint && ! is_admin() && is_main_query() && in_the_loop() && is_account_page() ) { - $title = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); - - remove_filter( 'the_title', array( $this, 'endpoint_title' ) ); - } - - return $title; - } - - /** - * Insert new endpoint into My Account menu. - * - * @since 2.1.12 - * - * @param array $items Menu items. - * @return array Menu items. - */ - public function new_menu_items( $items ) { - // Remove logout menu item. - $logout = $items['customer-logout']; - unset( $items['customer-logout'] ); - - // Insert VAT Number. - $items[ $this->endpoint ] = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); - - // Insert back logout item. - $items['customer-logout'] = $logout; - - return $items; - } - - /** - * Endpoint HTML content. - * - * @since 2.1.12 - */ - public function endpoint_content() { - $this->render_my_vat_number_content(); - } - - /** - * Render My VAT Number content. - * - * @since 2.1.12 - */ - public function render_my_vat_number_content() { - $vars = array( - 'vat_number' => get_user_meta( get_current_user_id(), 'vat_number', true ), - 'messages' => $this->messages, - ); - - wc_get_template( - 'my-account/my-vat-number.php', - $vars, - 'woocommerce-eu-vat-number', - untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' - ); - } - - /** - * Validate a VAT number. - * - * @version 2.3.0 - * @since 2.3.0 - * @param string $vat_number VAT number passed by the form. - * @param string $billing_country Billing country of the order. - * @param string $billing_postcode Billing postcode of the order. - * @param string $current_vat VAT number saved in database. - * - * @return boolean - * @throws Exception For invalid VAT Number. - */ - public function validate( $vat_number, $billing_country, $billing_postcode = '', $current_vat = '' ) { - if ( empty( $vat_number ) ) { - if ( empty( $current_vat ) ) { - throw new Exception( __( 'VAT number cannot be empty.', 'woocommerce-eu-vat-number' ) ); - } - // Allow empty input to clear VAT field. - return true; - } - - if ( empty( $billing_country ) ) { - /* translators: 1: VAT Number */ - throw new Exception( sprintf( __( '%1$s can not be validated because the billing country is missing. Please update your billing address.', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '' ) ); - } - - $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - - if ( is_wp_error( $valid ) ) { - throw new Exception( $valid->get_error_message() ); - } - - if ( ! $valid ) { - // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. - throw new Exception( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); - } - - return true; - } - - /** - * Function to save VAT number from the my account form. - */ - public function save_vat_number() { - if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'woocommerce-edit_vat_number' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized - try { - $current_user_id = get_current_user_id(); - $vat_number = isset( $_POST['vat_number'] ) ? wc_clean( wp_unslash( $_POST['vat_number'] ) ) : ''; - $posted_vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); - $user = get_userdata( $current_user_id ); - $current_vat = $user->vat_number; - $billing_country = $user->billing_country; - $billing_postcode = $user->billing_postcode; - - $this->validate( $posted_vat, $billing_country, $billing_postcode, $current_vat ); - - update_user_meta( $current_user_id, 'vat_number', $posted_vat ); - update_user_meta( $current_user_id, 'billing_vat_number', $posted_vat ); - - if ( empty( $posted_vat ) ) { - $message = __( 'VAT number removed successfully!', 'woocommerce-eu-vat-number' ); - } elseif ( empty( $current_vat ) ) { - $message = __( 'VAT number saved successfully!', 'woocommerce-eu-vat-number' ); - } else { - $message = __( 'VAT number updated successfully!', 'woocommerce-eu-vat-number' ); - } - $this->messages = array( 'message' => $message, 'status' => 'info' ); - } catch ( Exception $e ) { - $this->messages = array( - 'message' => $e->getMessage(), - 'status' => 'error', - ); - } - } - } -} - -$wc_eu_vat_my_account = new WC_EU_VAT_My_Account(); += 2.6. + add_action( 'init', array( $this, 'add_endpoints' ) ); + add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_vars' ), 0 ); + + // Change My Account page title. + add_filter( 'the_title', array( $this, 'endpoint_title' ) ); + + // Inserting new tab/page into My Account page. + add_filter( 'woocommerce_account_menu_items', array( $this, 'new_menu_items' ) ); + add_action( 'woocommerce_account_' . $this->endpoint . '_endpoint', array( $this, 'endpoint_content' ) ); + + // Save a VAT number from My Account form if one is submitted. + if ( isset( $_POST['action'] ) && 'edit_vat_number' === wc_clean( wp_unslash( $_POST['action'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verifying nonce inside function. + $this->save_vat_number(); + } + + add_action( 'woocommerce_init', array( $this, 'maybe_remove_vat' ) ); + } + + /** + * Checks to see if we need to remove vat from displaying in the cart and from product itself. + * + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/71 + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/74 + * @see https://github.com/woocommerce/woocommerce-eu-vat-number/issues/233 + */ + public function maybe_remove_vat() { + // Ignore checkout page as on checkout page VAT exempt based on VAT number from billing fields. + if ( ( is_admin() && ! defined( 'DOING_AJAX' ) ) || ! wc_tax_enabled() || is_checkout() || ! is_user_logged_in() ) { + return; + } + + $vat_number = get_user_meta( get_current_user_id(), 'vat_number', true ); + if ( empty( $vat_number ) || empty( WC()->customer ) ) { + return; + } + + // Validate if VAT is valid. If valid, check for VAT exempt. + try { + $billing_country = WC()->customer->get_billing_country(); + $shipping_country = WC()->customer->get_shipping_country(); + + if ( $this->validate( $vat_number, $billing_country ) ) { + WC_EU_VAT_Number::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + } + } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // Ignore Exception. + } + } + + /** + * Register new endpoint to use inside My Account page. + * + * @since 2.1.12 + * + * @see https://developer.wordpress.org/reference/functions/add_rewrite_endpoint/ + */ + public function add_endpoints() { + add_rewrite_endpoint( $this->endpoint, EP_ROOT | EP_PAGES ); + } + + /** + * Add new query var. + * + * @since 2.1.12 + * + * @param array $vars Query vars. + * @return array + */ + public function add_query_vars( $vars ) { + $vars[] = $this->endpoint; + + return $vars; + } + + /** + * Set endpoint title. + * + * @since 2.1.12 + * + * @param string $title Endpoint title. + * @return string + */ + public function endpoint_title( $title ) { + global $wp_query; + + $is_endpoint = isset( $wp_query->query_vars[ $this->endpoint ] ); + + if ( $is_endpoint && ! is_admin() && is_main_query() && in_the_loop() && is_account_page() ) { + $title = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); + + remove_filter( 'the_title', array( $this, 'endpoint_title' ) ); + } + + return $title; + } + + /** + * Insert new endpoint into My Account menu. + * + * @since 2.1.12 + * + * @param array $items Menu items. + * @return array Menu items. + */ + public function new_menu_items( $items ) { + // Remove logout menu item. + $logout = $items['customer-logout']; + unset( $items['customer-logout'] ); + + // Insert VAT Number. + $items[ $this->endpoint ] = get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ); + + // Insert back logout item. + $items['customer-logout'] = $logout; + + return $items; + } + + /** + * Endpoint HTML content. + * + * @since 2.1.12 + */ + public function endpoint_content() { + $this->render_my_vat_number_content(); + } + + /** + * Render My VAT Number content. + * + * @since 2.1.12 + */ + public function render_my_vat_number_content() { + $vars = array( + 'vat_number' => get_user_meta( get_current_user_id(), 'vat_number', true ), + 'messages' => $this->messages, + ); + + wc_get_template( + 'my-account/my-vat-number.php', + $vars, + 'woocommerce-eu-vat-number', + untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' + ); + } + + /** + * Validate a VAT number. + * + * @version 2.3.0 + * @since 2.3.0 + * @param string $vat_number VAT number passed by the form. + * @param string $billing_country Billing country of the order. + * @param string $billing_postcode Billing postcode of the order. + * @param string $current_vat VAT number saved in database. + * + * @return boolean + * @throws Exception For invalid VAT Number. + */ + public function validate( $vat_number, $billing_country, $billing_postcode = '', $current_vat = '' ) { + if ( empty( $vat_number ) ) { + if ( empty( $current_vat ) ) { + throw new Exception( __( 'VAT number cannot be empty.', 'woocommerce-eu-vat-number' ) ); + } + // Allow empty input to clear VAT field. + return true; + } + + if ( empty( $billing_country ) ) { + /* translators: 1: VAT Number */ + throw new Exception( sprintf( __( '%1$s can not be validated because the billing country is missing. Please update your billing address.', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '' ) ); + } + + $valid = WC_EU_VAT_Number::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + + if ( is_wp_error( $valid ) ) { + throw new Exception( $valid->get_error_message() ); + } + + if ( ! $valid ) { + // translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing Country. + throw new Exception( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), $vat_number, $billing_country ) ); + } + + return true; + } + + /** + * Function to save VAT number from the my account form. + */ + public function save_vat_number() { + if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_POST['_wpnonce'] ), 'woocommerce-edit_vat_number' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + try { + $current_user_id = get_current_user_id(); + $vat_number = isset( $_POST['vat_number'] ) ? wc_clean( wp_unslash( $_POST['vat_number'] ) ) : ''; + $posted_vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); + $user = get_userdata( $current_user_id ); + $current_vat = $user->vat_number; + $billing_country = $user->billing_country; + $billing_postcode = $user->billing_postcode; + + $this->validate( $posted_vat, $billing_country, $billing_postcode, $current_vat ); + + update_user_meta( $current_user_id, 'vat_number', $posted_vat ); + update_user_meta( $current_user_id, 'billing_vat_number', $posted_vat ); + + if ( empty( $posted_vat ) ) { + $message = __( 'VAT number removed successfully!', 'woocommerce-eu-vat-number' ); + } elseif ( empty( $current_vat ) ) { + $message = __( 'VAT number saved successfully!', 'woocommerce-eu-vat-number' ); + } else { + $message = __( 'VAT number updated successfully!', 'woocommerce-eu-vat-number' ); + } + $this->messages = array( 'message' => $message, 'status' => 'info' ); + } catch ( Exception $e ) { + $this->messages = array( + 'message' => $e->getMessage(), + 'status' => 'error', + ); + } + } + } +} + +$wc_eu_vat_my_account = new WC_EU_VAT_My_Account(); diff --git a/includes/class-wc-eu-vat-number.php b/includes/class-wc-eu-vat-number.php index 62e4e54..cd78df9 100644 --- a/includes/class-wc-eu-vat-number.php +++ b/includes/class-wc-eu-vat-number.php @@ -1,745 +1,781 @@ - 'U[A-Z\d]{8}', - 'BE' => '0\d{9}', - 'BG' => '\d{9,10}', - 'CY' => '\d{8}[A-Z]', - 'CZ' => '\d{8,10}', - 'DE' => '\d{9}', - 'DK' => '(\d{2} ?){3}\d{2}', - 'EE' => '\d{9}', - 'EL' => '\d{9}', - 'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}', - 'FI' => '\d{8}', - 'FR' => '([A-Z]{2}|[A-Z0-9]{2})\d{9}', - 'GB' => '\d{9}|\d{12}|(GD|HA)\d{3}', - 'XI' => '\d{9}|\d{12}|(GD|HA)\d{3}', - 'HR' => '\d{11}', - 'HU' => '\d{8}', - 'IE' => '[A-Z\d]{8,10}', - 'IT' => '\d{11}', - 'LT' => '(\d{9}|\d{12})', - 'LU' => '\d{8}', - 'LV' => '\d{11}', - 'MT' => '\d{8}', - 'NL' => '\d{9}B\d{2}', - 'PL' => '\d{10}', - 'PT' => '\d{9}', - 'RO' => '\d{2,10}', - 'SE' => '\d{12}', - 'SI' => '\d{8}', - 'SK' => '\d{10}', - ); - - /** - * VAT Number data. - * - * @var array - */ - private static $data = array( - 'vat_number' => false, - 'validation' => array( - 'valid' => null, - 'error' => false, - ), - ); - - /** - * Stores the current IP Address' country code after geolocation. - * - * @var boolean - */ - private static $ip_country = false; - - /** - * Init. - */ - public static function init() { - // Add fields to checkout process. - add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) ); - add_filter( 'woocommerce_billing_fields', array( __CLASS__, 'vat_number_field' ) ); - add_action( 'woocommerce_checkout_process', array( __CLASS__, 'process_checkout' ) ); - add_action( 'woocommerce_checkout_update_order_review', array( __CLASS__, 'ajax_update_checkout_totals' ) ); - add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'location_confirmation' ) ); - add_action( 'woocommerce_deposits_after_scheduled_order_props_set', array( __CLASS__, 'set_vat_details_for_scheduled_orders' ), 10, 2 ); - - add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'set_order_data' ) ); - add_action( 'woocommerce_checkout_update_customer', array( __CLASS__, 'set_customer_data' ) ); - add_action( 'woocommerce_create_refund', array( __CLASS__, 'set_refund_data' ) ); - - // Add VAT to addresses. - add_filter( 'woocommerce_order_formatted_billing_address', array( __CLASS__, 'formatted_billing_address' ), 10, 2 ); - add_filter( 'woocommerce_formatted_address_replacements', array( __CLASS__, 'output_company_vat_number' ), 10, 2 ); - add_filter( 'woocommerce_localisation_address_formats', array( __CLASS__, 'localisation_address_formats' ), 10, 2 ); - - // Digital goods taxable location. - add_filter( 'woocommerce_get_tax_location', array( __CLASS__, 'woocommerce_get_tax_location' ), 10, 2 ); - - // Add VAT Number in order endpoint (REST API). - add_filter( 'woocommerce_api_order_response', array( __CLASS__, 'add_vat_number_to_order_response' ) ); - add_filter( 'woocommerce_rest_prepare_shop_order', array( __CLASS__, 'add_vat_number_to_order_response' ) ); - } - - /** - * Load scripts used on the checkout. - */ - public static function load_scripts() { - if ( is_checkout() ) { - $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; - - wp_enqueue_script( 'wc-eu-vat', WC_EU_VAT_PLUGIN_URL . '/assets/js/eu-vat' . $suffix . '.js', array( 'jquery', 'wc-checkout' ), WC_EU_VAT_VERSION, true ); - self::localize_wc_eu_vat_params('wc-eu-vat'); - } - } - - public static function localize_wc_eu_vat_params($script_handle) { - wp_localize_script( - $script_handle, - 'wc_eu_vat_params', - array( - 'eu_countries' => self::get_eu_countries(), - 'b2b_required' => get_option( 'woocommerce_eu_vat_number_b2b', 'false' ), - 'input_label' => get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), - 'input_description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), - 'failure_handler' => get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ), - ) - ); - } - - /** - * Get EU Country codes. - * - * @return array - */ - public static function get_eu_countries() { - if ( empty( self::$eu_countries ) ) { - self::$eu_countries = include 'data/eu-country-codes.php'; - } - return self::$eu_countries; - } - - /** - * Reset number. - */ - public static function reset() { - WC()->customer->set_is_vat_exempt( false ); - self::$data = array( - 'vat_number' => false, - 'validation' => array( - 'valid' => null, - 'error' => false, - ), - ); - } - - /** - * Show the VAT field on the checkout. - * - * @since 1.0.0 - * @version 2.3.1 - * @param array $fields Billing Fields. - * @return array - */ - public static function vat_number_field( $fields ) { - $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); - $user_id = get_current_user_id(); - - // If on edit address page, unset vat number field. - if ( is_wc_endpoint_url( 'edit-address' ) ) { - if ( isset( $fields['billing_vat_number'] ) ) { - unset( $fields['billing_vat_number'] ); - } - return $fields; - } - - $fields['billing_vat_number'] = array( - 'label' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), - 'default' => $user_id > 0 ? get_user_meta( $user_id, 'vat_number', true ) : '', - 'required' => false, - 'class' => array( - 'form-row-wide', - ), - 'description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), - 'id' => 'woocommerce_eu_vat_number', - 'priority' => 120, - ); - - return $fields; - } - - /** - * Return the vat number prefix. - * - * @param string $country Country Code. - * @return string - */ - public static function get_vat_number_prefix( $country ) { - switch ( $country ) { - case 'GR': - $vat_prefix = 'EL'; - break; - case 'MC': - $vat_prefix = 'FR'; - break; - case 'IM': - $vat_prefix = 'GB'; - break; - default: - $vat_prefix = $country; - break; - } - return $vat_prefix; - } - - /** - * Remove unwanted chars and the prefix from a VAT number. - * - * @param string $vat VAT Number. - * @return string - */ - public static function get_formatted_vat_number( $vat ) { - $vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat ) ); - - if ( in_array( substr( $vat, 0, 2 ), array_merge( self::get_eu_countries(), array( 'EL', 'XI' ) ), true ) ) { - $vat = substr( $vat, 2 ); - } - - return $vat; - } - - /** - * Get IP address country for user. - * - * @return string - */ - public static function get_ip_country() { - if ( false === self::$ip_country ) { - $geoip = WC_Geolocation::geolocate_ip(); - self::$ip_country = $geoip['country']; - } - return self::$ip_country; - } - - /** - * Validate a number. - * - * @param string $vat_number VAT Number. - * @param string $country CountryCode. - * @param string $postcode Postcode. - * @return bool|WP_Error if valid/not valid, WP_ERROR if validation failed - */ - public static function vat_number_is_valid( $vat_number, $country, $postcode = '' ) { - $vat_prefix = self::get_vat_number_prefix( $country ); - $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); - $transient_name = 'vat_number_' . $vat_prefix . $vat_number_formatted; - $cached_result = get_transient( $transient_name ); - - - // Keep supporting prefix 'XI' for Northern Ireland. - if ( 'GB' === $country && ! empty( $postcode ) && preg_match( '/^(bt).*$/i', $postcode ) && 'XI' === substr( $vat_number, 0, 2 ) ) { - $vat_prefix = 'XI'; - } - - // The StoreAPI will set $vat_number to null if the user does not enter it. We should show an error in this case. - if ( null === $vat_number ) { - return new WP_Error( 'api', __( 'VAT number is required.', 'woocommerce-eu-vat-number' ) ); - } - - // Return error if VAT Country Code doesn't match or exist. - if ( ! isset( self::$country_codes_patterns[ $vat_prefix ] ) || ( $vat_prefix . $vat_number_formatted !== $vat_number ) ) { - // translators: %1$s - VAT number field label, %2$s - VAT Number from user, %3$s - Billing country. - return new WP_Error( 'api', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) ); - } - - if ( ! empty( $cached_result ) ) { - return 'yes' === $cached_result; - } - - $is_valid = false; - if ( in_array( $country, array( 'GB', 'IM' ), true ) ) { - // For United Kingdom (UK) (Isle of Man included) check VAT number with UK VAT Number API. - try { - $uk_vat_api = new WC_EU_VAT_UK_Number_API(); - $is_valid = $uk_vat_api->check_vat_number( $vat_number_formatted ); - } catch ( Exception $e ) { - return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); - } - } else { - // Check rest of EU countries with VIES. - $vies = new VIES_Client(); - $soap_client = $vies->get_soap_client(); - - // Return error if any error occurs in getting the SOAP client. - if ( is_wp_error( $soap_client ) ) { - return $soap_client; - } - - if ( $soap_client ) { - try { - $vies_req = $vies->check_vat( $vat_prefix, $vat_number_formatted ); - $is_valid = $vies_req->is_valid(); - - } catch ( SoapFault $e ) { - return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); - } - } - } - - /** - * Filter whether the VAT number is valid or not. - * - * @since 2.4.2 - * @hook woocommerce_eu_vat_number_is_valid - * - * @param {boolean} $is_valid Whether the VAT number is valid or not. - * @param {string} $vat_number VAT number. - * @param {string} $country Country. - * - * @return {boolean} - */ - $is_valid = apply_filters( 'woocommerce_eu_vat_number_is_valid', $is_valid, $vat_number, $country ); - - set_transient( $transient_name, $is_valid ? 'yes' : 'no', DAY_IN_SECONDS ); - return $is_valid; - } - - /** - * Validate a number and store the result. - * - * @param string $vat_number VAT Number. - * @param string $billing_country Billing CountryCode. - * @param string $billing_postcode Billing PostCode. - * @return void - */ - public static function validate( $vat_number, $billing_country, $billing_postcode = '' ) { - $valid = self::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); - $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); - - if ( is_wp_error( $valid ) ) { - self::$data['vat_number'] = $vat_number; - self::$data['validation'] = array( - 'valid' => null, - 'error' => $valid->get_error_message(), - ); - } else { - self::$data['vat_number'] = $valid ? self::get_vat_number_prefix( $billing_country ) . $vat_number_formatted : $vat_number; - self::$data['validation'] = array( - 'valid' => $valid, - 'error' => false, - ); - } - } - - /** - * Whether the base country match with the billing/shipping country. - * - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - * @return bool - */ - public static function is_base_country_match( $billing_country, $shipping_country ) { - /* - * Special handling needs to be done - * for Isle of Man. Technically Isle of Man - * is separate from UK however in the context - * of VAT, it is considered within UK. - * Ref: https://www.gov.im/categories/tax-vat-and-your-money/customs-and-excise/international-trade-and-the-isle-of-man-requirements-and-standards/ - */ - $base_country = WC()->countries->get_base_country(); - $tax_based_on = get_option( 'woocommerce_tax_based_on', 'billing' ); - $base_country_is_uk = in_array( $base_country, array( 'GB', 'IM' ), true ); - - if ( 'billing' === $tax_based_on ) { - if ( $base_country_is_uk && in_array( $billing_country, array( 'GB', 'IM' ), true ) ) { - return true; - } - return ( $base_country === $billing_country ); - } elseif ( 'shipping' === $tax_based_on ) { - if ( $base_country_is_uk && in_array( $shipping_country, array( 'GB', 'IM' ), true ) ) { - return true; - } - return ( $base_country === $shipping_country ); - } - - return in_array( $base_country, array( $billing_country, $shipping_country ), true ); - } - - /** - * Set tax exception based on countries. - * - * @param bool $exempt Are they exempt?. - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - */ - public static function maybe_set_vat_exempt( $exempt, $billing_country, $shipping_country ) { - $base_country_match = self::is_base_country_match( $billing_country, $shipping_country ); - - if ( ( $base_country_match && 'yes' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) ) || ! $base_country_match ) { - /** - * Filters the VAT exception. - * - * @since 2.3.6 - * - * @param bool $exempt Are they exempt?. - * @param bool $base_country_match Is Base coutry match?. - * @param string $billing_country Billing country of customer. - * @param string $shipping_country Shipping country of customer. - */ - $exempt = apply_filters( 'woocommerce_eu_vat_number_set_is_vat_exempt', $exempt, $base_country_match, $billing_country, $shipping_country ); - WC()->customer->set_is_vat_exempt( $exempt ); - } - } - - /** - * Validate the VAT number when the checkout form is processed. - * - * For B2C transactions, validate the IP only if this is a digital order. - */ - public static function process_checkout() { - self::reset(); - - self::validate_checkout( $_POST, true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - } - - /** - * See if we need the user to self-declare location. - * - * This is needed when: - * The IP country cannot be detected - * The IP country is inside the EU OR - * The Billing country is inside the EU AND - * The IP doesn't match the billing country. - * - * @param string $ip_country IP Country of customer. - * @param string $billing_country Billig Country code. - * @return boolean - */ - public static function is_self_declaration_required( $ip_country = null, $billing_country = null ) { - if ( is_null( $ip_country ) ) { - $ip_country = self::get_ip_country(); - } - if ( is_null( $billing_country ) ) { - $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); - } - - return ( empty( $ip_country ) || in_array( $ip_country, self::get_eu_countries(), true ) || in_array( $billing_country, self::get_eu_countries(), true ) ) && $ip_country !== $billing_country; - } - - /** - * Show checkbox for customer to confirm their location (location evidence for B2C) - */ - public static function location_confirmation() { - if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { - if ( false === self::$data['vat_number'] && self::is_self_declaration_required() ) { - wc_get_template( - 'location-confirmation-field.php', - array( - 'location_confirmation_is_checked' => isset( $_POST['location_confirmation'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing - 'countries' => WC()->countries->get_countries(), - ), - 'woocommerce-eu-vat-number', - untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' - ); - } - } - } - - /** - * Support method for WooCommerce Deposits. - * - * Sets the VAT related meta whenever a new scheduled order is created. - * - * @param WC_Order $new_order The scheduled order object. - * @param WC_Order $original_order The original order object. - */ - public static function set_vat_details_for_scheduled_orders( $new_order, $original_order ) { - $vat_number = $original_order->get_meta( '_billing_vat_number' ); - $is_vat_exempt = $original_order->get_meta( 'is_vat_exempt' ); - $is_vat_validated = $original_order->get_meta( '_vat_number_is_validated' ); - $is_vat_valid = $original_order->get_meta( '_vat_number_is_valid' ); - - if ( ! empty( $vat_number ) ) { - $new_order->update_meta_data( '_billing_vat_number', $vat_number ); - } - - if ( ! empty( $is_vat_exempt ) ) { - $new_order->update_meta_data( 'is_vat_exempt', $is_vat_exempt ); - } - - if ( ! empty( $is_vat_validated ) ) { - $new_order->update_meta_data( '_vat_number_is_validated', $is_vat_validated ); - } - - if ( ! empty( $is_vat_valid ) ) { - $new_order->update_meta_data( '_vat_number_is_valid', $is_vat_valid ); - } - } - - /** - * Triggered when the totals are updated on the checkout. - * - * @since 1.0.0 - * @version 2.3.1 - * @param array $form_data Checkout Form data. - */ - public static function ajax_update_checkout_totals( $form_data ) { - parse_str( $form_data, $form_data ); - - self::reset(); - - if ( empty( $form_data['billing_country'] ) && empty( $form_data['shipping_country'] ) || empty( $form_data['billing_vat_number'] ) ) { - return; - } - - self::validate_checkout( $form_data ); - } - - /** - * Sees if a cart contains anything non-shippable. Thanks EU, I hate you. - * - * @return bool - */ - public static function cart_has_digital_goods() { - $has_digital_goods = false; - - if ( WC()->cart->get_cart() ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { - $_product = $values['data']; - if ( ! $_product->needs_shipping() ) { - $has_digital_goods = true; - } - } - } - - /** - * Filters if cart has digital goods. - * - * @since 2.1.2 - * - * @param bool $has_digital_goods Is it Digital good? - */ - return apply_filters( 'woocommerce_cart_has_digital_goods', $has_digital_goods ); - } - - /** - * Add VAT ID to the formatted address array - * - * @param array $address Address Array. - * @param WC_Order $order WC Order Object. - * @return array - */ - public static function formatted_billing_address( $address, $order ) { - $vat_id = wc_eu_vat_get_vat_from_order( $order ); - - if ( $vat_id ) { - $address['vat_id'] = $vat_id; - } - return $address; - } - - /** - * Add {vat_id} placeholder - * - * @param array $formats Address formats. - * @param array $args Arguments. - * @return array - */ - public static function output_company_vat_number( $formats, $args ) { - if ( isset( $args['vat_id'] ) ) { - /* translators: %s: VAT Number */ - $formats['{vat_id}'] = sprintf( __( 'VAT Number: %s', 'woocommerce-eu-vat-number' ), $args['vat_id'] ); - } else { - $formats['{vat_id}'] = ''; - } - return $formats; - } - - /** - * Address formats. - * - * @param array $formats Address formats. - * @return array - */ - public static function localisation_address_formats( $formats ) { - foreach ( $formats as $key => $format ) { - if ( 'default' === $key || in_array( $key, self::get_eu_countries(), true ) ) { - $formats[ $key ] .= "\n{vat_id}"; - } - } - return $formats; - } - - /** - * Force Digital Goods tax class to use billing address - * - * @param array $location Location. - * @param string $tax_class Tax Class. - * @return array - */ - public static function woocommerce_get_tax_location( $location, $tax_class = '' ) { - if ( ! empty( WC()->customer ) && in_array( sanitize_title( $tax_class ), get_option( 'woocommerce_eu_vat_number_digital_tax_classes', array() ), true ) ) { - return array( - WC()->customer->get_billing_country(), - WC()->customer->get_billing_state(), - WC()->customer->get_billing_postcode(), - WC()->customer->get_billing_city(), - ); - } - return $location; - } - - /** - * Add VAT Number to order endpoint response. - * - * @since 2.1.12 - * - * @param WP_REST_Response $response The response object. - * - * @return WP_REST_Response The response object with VAT number - */ - public static function add_vat_number_to_order_response( $response ) { - if ( is_a( $response, 'WP_REST_Response' ) ) { - $order = wc_get_order( (int) $response->data['id'] ); - $response->data['vat_number'] = $order->get_meta( '_billing_vat_number', true ); - } elseif ( is_array( $response ) && ! empty( $response['id'] ) ) { - // Legacy endpoint. - $order = wc_get_order( (int) $response['id'] ); - $response['vat_number'] = $order->get_meta( '_billing_vat_number', true ); - } - return $response; - } - - /** - * Save VAT Number to the order during checkout (WC 2.7.x). - * - * @param WC_Order $order WC Order. - */ - public static function set_order_data( $order ) { - $order->update_meta_data( '_billing_vat_number', self::$data['vat_number'] ); - $order->update_meta_data( '_vat_number_is_validated', ! is_null( self::$data['validation']['valid'] ) ? 'true' : 'false' ); - $order->update_meta_data( '_vat_number_is_valid', true === self::$data['validation']['valid'] ? 'true' : 'false' ); - - if ( false !== self::get_ip_country() ) { - $order->update_meta_data( '_customer_ip_country', self::get_ip_country() ); - $order->update_meta_data( '_customer_self_declared_country', ! empty( $_POST['location_confirmation'] ) ? 'true' : 'false' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing - } - } - - /** - * Save VAT Number to the customer during checkout (WC 2.7.x). - * - * @param WC_Customer $customer Customer Object. - */ - public static function set_customer_data( $customer ) { - $customer->update_meta_data( 'vat_number', self::$data['vat_number'] ); - } - - /** - * Save VAT Number to the customer during checkout (WC 2.7.x). - * - * @param WC_Order $refund Refund Order. - */ - public static function set_refund_data( $refund ) { - $order = wc_get_order( $refund->get_parent_id() ); - $refund->update_meta_data( '_billing_vat_number', wc_eu_vat_get_vat_from_order( $order ) ); - } - - /** - * Validate AJAX Order Review / Checkout & add errors if any. - * - * @param array $data Checkout field data. - * @param boolean $doing_checkout True if doing checkout. False if AJAX order review. - */ - public static function validate_checkout( $data, $doing_checkout = false ) { - $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); - $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); - $billing_country = wc_clean( $data['billing_country'] ); - $shipping_country = wc_clean( ! empty( $data['shipping_country'] ) && ! empty( $data['ship_to_different_address'] ) ? $data['shipping_country'] : $data['billing_country'] ); - $billing_vat_number = wc_clean( $data['billing_vat_number'] ); - $billing_postcode = wc_clean( $data['billing_postcode'] ); - - // Replace unwanted chars on VAT Number. - $billing_vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $billing_vat_number ) ); - - if ( in_array( $billing_country, self::get_eu_countries(), true ) && ! empty( $billing_vat_number ) ) { - self::validate( $billing_vat_number, $billing_country, $billing_postcode ); - - if ( true === (bool) self::$data['validation']['valid'] ) { - self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - } else { - switch ( $fail_handler ) { - case 'accept_with_vat': - self::maybe_set_vat_exempt( false, $billing_country, $shipping_country ); - break; - case 'accept': - self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); - break; - default: - if ( false === self::$data['validation']['valid'] ) { - /* translators: 1: VAT number field label, 2: VAT Number, 3: Billing country */ - wc_add_notice( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), self::$data['vat_number'], $billing_country ), 'error' ); - } else { - wc_add_notice( self::$data['validation']['error'], 'error' ); - } - break; - } - } - } - - // If doing checkout, check for additional conditions. - if ( $doing_checkout ) { - if ( in_array( $billing_country, self::get_eu_countries(), true ) && empty( $billing_vat_number ) ) { - if ( 'yes' === $b2b_vat_enabled ) { - /* translators: 1: VAT number field label, 2: Billing country */ - wc_add_notice( sprintf( __( '%1$s is a required field for your billing country (%2$s).', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '', $billing_country ), 'error' ); - } - - if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { - if ( self::is_self_declaration_required( self::get_ip_country(), $billing_country ) && empty( $data['location_confirmation'] ) ) { - - /** - * Filters the self declared IP address. - * - * @since 2.1.10 - */ - $ip_address = apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ); - /* translators: 1: Ip Address. */ - wc_add_notice( sprintf( __( 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox below.', 'woocommerce-eu-vat-number' ), $ip_address, $billing_country ), 'error' ); - } - } - } - } - } -} - -WC_EU_VAT_Number::init(); + 'U[A-Z\d]{8}', + 'BE' => '0\d{9}', + 'BG' => '\d{9,10}', + 'CY' => '\d{8}[A-Z]', + 'CZ' => '\d{8,10}', + 'DE' => '\d{9}', + 'DK' => '(\d{2} ?){3}\d{2}', + 'EE' => '\d{9}', + 'EL' => '\d{9}', + 'ES' => '[A-Z]\d{7}[A-Z]|\d{8}[A-Z]|[A-Z]\d{8}', + 'FI' => '\d{8}', + 'FR' => '([A-Z]{2}|[A-Z0-9]{2})\d{9}', + 'GB' => '\d{9}|\d{12}|(GD|HA)\d{3}', + 'XI' => '\d{9}|\d{12}|(GD|HA)\d{3}', + 'HR' => '\d{11}', + 'HU' => '\d{8}', + 'IE' => '[A-Z\d]{8,10}', + 'IT' => '\d{11}', + 'LT' => '(\d{9}|\d{12})', + 'LU' => '\d{8}', + 'LV' => '\d{11}', + 'MT' => '\d{8}', + 'NL' => '\d{9}B\d{2}', + 'PL' => '\d{10}', + 'PT' => '\d{9}', + 'RO' => '\d{2,10}', + 'SE' => '\d{12}', + 'SI' => '\d{8}', + 'SK' => '\d{10}', + ); + + /** + * VAT Number data. + * + * @var array + */ + private static $data = array( + 'vat_number' => false, + 'validation' => array( + 'valid' => null, + 'error' => false, + ), + ); + + /** + * Stores the current IP Address' country code after geolocation. + * + * @var boolean + */ + private static $ip_country = false; + + /** + * Init. + */ + public static function init() { + // Add fields to checkout process. + add_action( 'wp_enqueue_scripts', array( __CLASS__, 'load_scripts' ) ); + add_filter( 'woocommerce_billing_fields', array( __CLASS__, 'vat_number_field' ) ); + add_action( 'woocommerce_checkout_process', array( __CLASS__, 'process_checkout' ) ); + add_action( 'woocommerce_checkout_update_order_review', array( __CLASS__, 'ajax_update_checkout_totals' ) ); + add_action( 'woocommerce_review_order_before_submit', array( __CLASS__, 'location_confirmation' ) ); + add_action( 'woocommerce_deposits_after_scheduled_order_props_set', array( __CLASS__, 'set_vat_details_for_scheduled_orders' ), 10, 2 ); + + add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'set_order_data' ) ); + add_action( 'woocommerce_checkout_update_customer', array( __CLASS__, 'set_customer_data' ) ); + add_action( 'woocommerce_create_refund', array( __CLASS__, 'set_refund_data' ) ); + + // Add VAT to addresses. + add_filter( 'woocommerce_order_formatted_billing_address', array( __CLASS__, 'formatted_billing_address' ), 10, 2 ); + add_filter( 'woocommerce_formatted_address_replacements', array( __CLASS__, 'output_company_vat_number' ), 10, 2 ); + add_filter( 'woocommerce_localisation_address_formats', array( __CLASS__, 'localisation_address_formats' ), 10, 2 ); + + // Digital goods taxable location. + add_filter( 'woocommerce_get_tax_location', array( __CLASS__, 'woocommerce_get_tax_location' ), 10, 2 ); + + // Add VAT Number in order endpoint (REST API). + add_filter( 'woocommerce_api_order_response', array( __CLASS__, 'add_vat_number_to_order_response' ) ); + add_filter( 'woocommerce_rest_prepare_shop_order', array( __CLASS__, 'add_vat_number_to_order_response' ) ); + } + + /** + * Load scripts used on the checkout. + */ + public static function load_scripts() { + if ( is_checkout() ) { + $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min'; + + wp_enqueue_script( 'wc-eu-vat', WC_EU_VAT_PLUGIN_URL . '/assets/js/eu-vat' . $suffix . '.js', array( 'jquery', 'wc-checkout' ), WC_EU_VAT_VERSION, true ); + self::localize_wc_eu_vat_params('wc-eu-vat'); + } + } + + public static function localize_wc_eu_vat_params($script_handle) { + wp_localize_script( + $script_handle, + 'wc_eu_vat_params', + array( + 'eu_countries' => self::get_eu_countries(), + 'b2b_required' => get_option( 'woocommerce_eu_vat_number_b2b', 'false' ), + 'input_label' => get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), + 'input_description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), + 'failure_handler' => get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ), + ) + ); + } + + /** + * Get EU Country codes. + * + * @return array + */ + public static function get_eu_countries() { + if ( empty( self::$eu_countries ) ) { + self::$eu_countries = include 'data/eu-country-codes.php'; + } + return self::$eu_countries; + } + + /** + * Reset number. + */ + public static function reset() { + WC()->customer->set_is_vat_exempt( false ); + self::$data = array( + 'vat_number' => false, + 'validation' => array( + 'valid' => null, + 'error' => false, + ), + ); + } + + /** + * Show the VAT field on the checkout. + * + * @since 1.0.0 + * @version 2.3.1 + * @param array $fields Billing Fields. + * @return array + */ + public static function vat_number_field( $fields ) { + $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); + $user_id = get_current_user_id(); + + // If on edit address page, unset vat number field. + if ( is_wc_endpoint_url( 'edit-address' ) ) { + if ( isset( $fields['billing_vat_number'] ) ) { + unset( $fields['billing_vat_number'] ); + } + return $fields; + } + + $fields['billing_vat_number'] = array( + 'label' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'default' => $user_id > 0 ? get_user_meta( $user_id, 'vat_number', true ) : '', + 'required' => false, + 'class' => array( + 'form-row-wide', + ), + 'description' => get_option( 'woocommerce_eu_vat_number_field_description', '' ), + 'id' => 'woocommerce_eu_vat_number', + 'priority' => 120, + ); + + return $fields; + } + + /** + * Return the vat number prefix. + * + * @param string $country Country Code. + * @return string + */ + public static function get_vat_number_prefix( $country ) { + switch ( $country ) { + case 'GR': + $vat_prefix = 'EL'; + break; + case 'MC': + $vat_prefix = 'FR'; + break; + case 'IM': + $vat_prefix = 'GB'; + break; + default: + $vat_prefix = $country; + break; + } + return $vat_prefix; + } + + /** + * Remove unwanted chars and the prefix from a VAT number. + * + * @param string $vat VAT Number. + * @return string + */ + public static function get_formatted_vat_number( $vat ) { + $vat = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat ) ); + + if ( in_array( substr( $vat, 0, 2 ), array_merge( self::get_eu_countries(), array( 'EL', 'XI' ) ), true ) ) { + $vat = substr( $vat, 2 ); + } + + return $vat; + } + + /** + * Get IP address country for user. + * + * @return string + */ + public static function get_ip_country() { + if ( false === self::$ip_country ) { + $geoip = WC_Geolocation::geolocate_ip(); + self::$ip_country = $geoip['country']; + } + return self::$ip_country; + } + + /** + * Validate a number. + * + * @param string $vat_number VAT Number. + * @param string $country CountryCode. + * @param string $postcode Postcode. + * @return bool|WP_Error if valid/not valid, WP_ERROR if validation failed + */ + public static function vat_number_is_valid( $vat_number, $country, $postcode = '' ) { + // Replace unwanted chars on VAT Number. + $vat_number = strtoupper( str_replace( array( ' ', '.', '-', ',', ', ' ), '', $vat_number ) ); + + $vat_prefix = self::get_vat_number_prefix( $country ); + $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); + $transient_name = 'vat_number_' . $vat_prefix . $vat_number_formatted; + $cached_result = get_transient( $transient_name ); + + // Keep supporting prefix 'XI' for Northern Ireland. + if ( 'GB' === $country && ! empty( $postcode ) && preg_match( '/^(bt).*$/i', $postcode ) && 'XI' === substr( $vat_number, 0, 2 ) ) { + $vat_prefix = 'XI'; + } + + // The StoreAPI will set $vat_number to null if the user does not enter it. We should show an error in this case. + if ( null === $vat_number ) { + return new WP_Error( 'api', __( 'VAT number is required.', 'woocommerce-eu-vat-number' ) ); + } + + // Return error if VAT Country Code doesn't match or exist. + if ( ! isset( self::$country_codes_patterns[ $vat_prefix ] ) || ( $vat_prefix . $vat_number_formatted !== $vat_number ) ) { + // translators: %1$s - VAT number field label, %2$s - VAT Number from user, %3$s - Billing country. + return new WP_Error( 'api', sprintf( __( 'You have entered an invalid country code for %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', 'VAT number' ), $vat_number, $country ) ); + } + + if ( ! empty( $cached_result ) ) { + return 'yes' === $cached_result; + } + + $is_valid = false; + if ( in_array( $country, array( 'GB', 'IM' ), true ) ) { + // For United Kingdom (UK) (Isle of Man included) check VAT number with UK VAT Number API. + try { + $uk_vat_api = new WC_EU_VAT_UK_Number_API(); + $is_valid = $uk_vat_api->check_vat_number( $vat_number_formatted ); + } catch ( Exception $e ) { + return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); + } + } else { + // Check rest of EU countries with VIES. + $vies = new VIES_Client(); + $soap_client = $vies->get_soap_client(); + + // Return error if any error occurs in getting the SOAP client. + if ( is_wp_error( $soap_client ) ) { + return $soap_client; + } + + if ( $soap_client ) { + try { + $vies_req = $vies->check_vat( $vat_prefix, $vat_number_formatted ); + $is_valid = $vies_req->is_valid(); + + } catch ( SoapFault $e ) { + return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); + } + } + } + + /** + * Filter whether the VAT number is valid or not. + * + * @since 2.4.2 + * @hook woocommerce_eu_vat_number_is_valid + * + * @param {boolean} $is_valid Whether the VAT number is valid or not. + * @param {string} $vat_number VAT number. + * @param {string} $country Country. + * + * @return {boolean} + */ + $is_valid = apply_filters( 'woocommerce_eu_vat_number_is_valid', $is_valid, $vat_number, $country ); + + set_transient( $transient_name, $is_valid ? 'yes' : 'no', DAY_IN_SECONDS ); + return $is_valid; + } + + /** + * Validate a number and store the result. + * + * @param string $vat_number VAT Number. + * @param string $billing_country Billing CountryCode. + * @param string $billing_postcode Billing PostCode. + * @return void + */ + public static function validate( $vat_number, $billing_country, $billing_postcode = '' ) { + $valid = self::vat_number_is_valid( $vat_number, $billing_country, $billing_postcode ); + $vat_number_formatted = self::get_formatted_vat_number( $vat_number ); + + if ( is_wp_error( $valid ) ) { + self::$data['vat_number'] = $vat_number; + self::$data['validation'] = array( + 'valid' => null, + 'error' => $valid->get_error_message(), + ); + } else { + self::$data['vat_number'] = $valid ? self::get_vat_number_prefix( $billing_country ) . $vat_number_formatted : $vat_number; + self::$data['validation'] = array( + 'valid' => $valid, + 'error' => false, + ); + } + } + + /** + * Whether the base country match with the billing/shipping country. + * + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + * @return bool + */ + public static function is_base_country_match( $billing_country, $shipping_country ) { + /* + * Special handling needs to be done + * for Isle of Man. Technically Isle of Man + * is separate from UK however in the context + * of VAT, it is considered within UK. + * Ref: https://www.gov.im/categories/tax-vat-and-your-money/customs-and-excise/international-trade-and-the-isle-of-man-requirements-and-standards/ + */ + $base_country = WC()->countries->get_base_country(); + $tax_based_on = get_option( 'woocommerce_tax_based_on', 'billing' ); + $base_country_is_uk = in_array( $base_country, array( 'GB', 'IM' ), true ); + + if ( 'billing' === $tax_based_on ) { + if ( $base_country_is_uk && in_array( $billing_country, array( 'GB', 'IM' ), true ) ) { + return true; + } + return ( $base_country === $billing_country ); + } elseif ( 'shipping' === $tax_based_on ) { + if ( $base_country_is_uk && in_array( $shipping_country, array( 'GB', 'IM' ), true ) ) { + return true; + } + return ( $base_country === $shipping_country ); + } + + return in_array( $base_country, array( $billing_country, $shipping_country ), true ); + } + + /** + * Set tax exception based on countries. + * + * @param bool $exempt Are they exempt?. + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + */ + public static function maybe_set_vat_exempt( $exempt, $billing_country, $shipping_country ) { + $base_country_match = self::is_base_country_match( $billing_country, $shipping_country ); + + if ( ( $base_country_match && 'yes' === get_option( 'woocommerce_eu_vat_number_deduct_in_base', 'yes' ) ) || ! $base_country_match ) { + /** + * Filters the VAT exception. + * + * @since 2.3.6 + * + * @param bool $exempt Are they exempt?. + * @param bool $base_country_match Is Base coutry match?. + * @param string $billing_country Billing country of customer. + * @param string $shipping_country Shipping country of customer. + */ + $exempt = apply_filters( 'woocommerce_eu_vat_number_set_is_vat_exempt', $exempt, $base_country_match, $billing_country, $shipping_country ); + WC()->customer->set_is_vat_exempt( $exempt ); + } + } + + /** + * Validate the VAT number when the checkout form is processed. + * + * For B2C transactions, validate the IP only if this is a digital order. + */ + public static function process_checkout() { + self::reset(); + + self::validate_checkout( $_POST, true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + + /** + * See if we need the user to self-declare location. + * + * This is needed when: + * The IP country cannot be detected + * The IP country is inside the EU OR + * The Billing country is inside the EU AND + * The IP doesn't match the billing country. + * + * @param string $ip_country IP Country of customer. + * @param string $billing_country Billig Country code. + * @return boolean + */ + public static function is_self_declaration_required( $ip_country = null, $billing_country = null ) { + if ( is_null( $ip_country ) ) { + $ip_country = self::get_ip_country(); + } + if ( is_null( $billing_country ) ) { + $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); + } + + return ( empty( $ip_country ) || in_array( $ip_country, self::get_eu_countries(), true ) || in_array( $billing_country, self::get_eu_countries(), true ) ) && $ip_country !== $billing_country; + } + + /** + * Show checkbox for customer to confirm their location (location evidence for B2C) + */ + public static function location_confirmation() { + if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { + if ( false === self::$data['vat_number'] && self::is_self_declaration_required() ) { + wc_get_template( + 'location-confirmation-field.php', + array( + 'location_confirmation_is_checked' => isset( $_POST['location_confirmation'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing + 'countries' => WC()->countries->get_countries(), + ), + 'woocommerce-eu-vat-number', + untrailingslashit( plugin_dir_path( WC_EU_VAT_FILE ) ) . '/templates/' + ); + } + } + } + + /** + * Support method for WooCommerce Deposits. + * + * Sets the VAT related meta whenever a new scheduled order is created. + * + * @param WC_Order $new_order The scheduled order object. + * @param WC_Order $original_order The original order object. + */ + public static function set_vat_details_for_scheduled_orders( $new_order, $original_order ) { + $vat_number = $original_order->get_meta( '_billing_vat_number' ); + $is_vat_exempt = $original_order->get_meta( 'is_vat_exempt' ); + $is_vat_validated = $original_order->get_meta( '_vat_number_is_validated' ); + $is_vat_valid = $original_order->get_meta( '_vat_number_is_valid' ); + + if ( ! empty( $vat_number ) ) { + $new_order->update_meta_data( '_billing_vat_number', $vat_number ); + } + + if ( ! empty( $is_vat_exempt ) ) { + $new_order->update_meta_data( 'is_vat_exempt', $is_vat_exempt ); + } + + if ( ! empty( $is_vat_validated ) ) { + $new_order->update_meta_data( '_vat_number_is_validated', $is_vat_validated ); + } + + if ( ! empty( $is_vat_valid ) ) { + $new_order->update_meta_data( '_vat_number_is_valid', $is_vat_valid ); + } + } + + /** + * Triggered when the totals are updated on the checkout. + * + * @since 1.0.0 + * @version 2.3.1 + * @param array $form_data Checkout Form data. + */ + public static function ajax_update_checkout_totals( $form_data ) { + parse_str( $form_data, $form_data ); + + self::reset(); + + if ( empty( $form_data['billing_country'] ) && empty( $form_data['shipping_country'] ) || empty( $form_data['billing_vat_number'] ) ) { + return; + } + + self::validate_checkout( $form_data ); + } + + /** + * Sees if a cart contains anything non-shippable. Thanks EU, I hate you. + * + * @return bool + */ + public static function cart_has_digital_goods() { + $has_digital_goods = false; + + if ( WC()->cart->get_cart() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { + $_product = $values['data']; + if ( ! $_product->needs_shipping() ) { + $has_digital_goods = true; + } + } + } + + /** + * Filters if cart has digital goods. + * + * @since 2.1.2 + * + * @param bool $has_digital_goods Is it Digital good? + */ + return apply_filters( 'woocommerce_cart_has_digital_goods', $has_digital_goods ); + } + + /** + * Add VAT ID to the formatted address array + * + * @param array $address Address Array. + * @param WC_Order $order WC Order Object. + * @return array + */ + public static function formatted_billing_address( $address, $order ) { + $vat_id = wc_eu_vat_get_vat_from_order( $order ); + + if ( $vat_id ) { + $address['vat_id'] = $vat_id; + } + return $address; + } + + /** + * Add {vat_id} placeholder + * + * @param array $formats Address formats. + * @param array $args Arguments. + * @return array + */ + public static function output_company_vat_number( $formats, $args ) { + if ( isset( $args['vat_id'] ) ) { + /* translators: %s: VAT Number */ + $formats['{vat_id}'] = sprintf( __( 'VAT Number: %s', 'woocommerce-eu-vat-number' ), $args['vat_id'] ); + } else { + $formats['{vat_id}'] = ''; + } + return $formats; + } + + /** + * Address formats. + * + * @param array $formats Address formats. + * @return array + */ + public static function localisation_address_formats( $formats ) { + foreach ( $formats as $key => $format ) { + if ( 'default' === $key || in_array( $key, self::get_eu_countries(), true ) ) { + $formats[ $key ] .= "\n{vat_id}"; + } + } + return $formats; + } + + /** + * Force Digital Goods tax class to use billing address + * + * @param array $location Location. + * @param string $tax_class Tax Class. + * @return array + */ + public static function woocommerce_get_tax_location( $location, $tax_class = '' ) { + if ( ! empty( WC()->customer ) && in_array( sanitize_title( $tax_class ), get_option( 'woocommerce_eu_vat_number_digital_tax_classes', array() ), true ) ) { + return array( + WC()->customer->get_billing_country(), + WC()->customer->get_billing_state(), + WC()->customer->get_billing_postcode(), + WC()->customer->get_billing_city(), + ); + } + return $location; + } + + /** + * Add VAT Number to order endpoint response. + * + * @since 2.1.12 + * + * @param WP_REST_Response $response The response object. + * + * @return WP_REST_Response The response object with VAT number + */ + public static function add_vat_number_to_order_response( $response ) { + if ( is_a( $response, 'WP_REST_Response' ) ) { + $order = wc_get_order( (int) $response->data['id'] ); + $response->data['vat_number'] = $order->get_meta( '_billing_vat_number', true ); + } elseif ( is_array( $response ) && ! empty( $response['id'] ) ) { + // Legacy endpoint. + $order = wc_get_order( (int) $response['id'] ); + $response['vat_number'] = $order->get_meta( '_billing_vat_number', true ); + } + return $response; + } + + /** + * Save VAT Number to the order during checkout (WC 2.7.x). + * + * @param WC_Order $order WC Order. + */ + public static function set_order_data( $order ) { + $order->update_meta_data( '_billing_vat_number', self::$data['vat_number'] ); + $order->update_meta_data( '_vat_number_is_validated', ! is_null( self::$data['validation']['valid'] ) ? 'true' : 'false' ); + $order->update_meta_data( '_vat_number_is_valid', true === self::$data['validation']['valid'] ? 'true' : 'false' ); + + if ( false !== self::get_ip_country() ) { + $order->update_meta_data( '_customer_ip_country', self::get_ip_country() ); + $order->update_meta_data( '_customer_self_declared_country', ! empty( $_POST['location_confirmation'] ) ? 'true' : 'false' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + } + + /** + * Save VAT Number to the customer during checkout (WC 2.7.x). + * + * @param WC_Customer $customer Customer Object. + */ + public static function set_customer_data( $customer ) { + $customer->update_meta_data( 'vat_number', self::$data['vat_number'] ); + } + + /** + * Save VAT Number to the customer during checkout (WC 2.7.x). + * + * @param WC_Order $refund Refund Order. + */ + public static function set_refund_data( $refund ) { + $order = wc_get_order( $refund->get_parent_id() ); + $refund->update_meta_data( '_billing_vat_number', wc_eu_vat_get_vat_from_order( $order ) ); + } + + /** + * Validate AJAX Order Review / Checkout & add errors if any. + * + * @param array $data Checkout field data. + * @param boolean $doing_checkout True if doing checkout. False if AJAX order review. + */ + public static function validate_checkout( $data, $doing_checkout = false ) { + $b2b_vat_enabled = get_option( 'woocommerce_eu_vat_number_b2b', 'no' ); + $fail_handler = get_option( 'woocommerce_eu_vat_number_failure_handling', 'reject' ); + $billing_country = wc_clean( $data['billing_country'] ); + $shipping_country = wc_clean( ! empty( $data['shipping_country'] ) && ! empty( $data['ship_to_different_address'] ) ? $data['shipping_country'] : $data['billing_country'] ); + $billing_vat_number = wc_clean( $data['billing_vat_number'] ); + $billing_postcode = wc_clean( $data['billing_postcode'] ); + + if ( in_array( $billing_country, self::get_eu_countries(), true ) && ! empty( $billing_vat_number ) ) { + self::validate( $billing_vat_number, $billing_country, $billing_postcode ); + + if ( true === (bool) self::$data['validation']['valid'] ) { + self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + } else { + switch ( $fail_handler ) { + case 'accept_with_vat': + self::maybe_set_vat_exempt( false, $billing_country, $shipping_country ); + break; + case 'accept': + self::maybe_set_vat_exempt( true, $billing_country, $shipping_country ); + break; + default: + if ( false === self::$data['validation']['valid'] ) { + /* translators: 1: VAT number field label, 2: VAT Number, 3: Billing country */ + wc_add_notice( sprintf( __( 'You have entered an invalid %1$s (%2$s) for your billing country (%3$s).', 'woocommerce-eu-vat-number' ), get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), self::$data['vat_number'], $billing_country ), 'error' ); + } else { + wc_add_notice( self::$data['validation']['error'], 'error' ); + } + break; + } + } + } + + // If doing checkout, check for additional conditions. + if ( $doing_checkout ) { + if ( in_array( $billing_country, self::get_eu_countries(), true ) && empty( $billing_vat_number ) ) { + if ( 'yes' === $b2b_vat_enabled ) { + /* translators: 1: VAT number field label, 2: Billing country */ + wc_add_notice( sprintf( __( '%1$s is a required field for your billing country (%2$s).', 'woocommerce-eu-vat-number' ), '' . get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ) . '', $billing_country ), 'error' ); + } + + if ( 'yes' === get_option( 'woocommerce_eu_vat_number_validate_ip', 'no' ) && self::cart_has_digital_goods() ) { + if ( self::is_self_declaration_required( self::get_ip_country(), $billing_country ) && empty( $data['location_confirmation'] ) ) { + + /** + * Filters the self declared IP address. + * + * @since 2.1.10 + */ + $ip_address = apply_filters( 'wc_eu_vat_self_declared_ip_address', WC_Geolocation::get_ip_address() ); + /* translators: 1: Ip Address. */ + wc_add_notice( sprintf( __( 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox below.', 'woocommerce-eu-vat-number' ), $ip_address, $billing_country ), 'error' ); + } + } + } + } + } + + /** + * Load script with all required dependencies. + * + * @param string $handler Script handler. + * @param string $script Script name. + * @param array $dependencies Additional dependencies. + * + * @return void + */ + public static function register_script_with_dependencies( string $handler, string $script, array $dependencies = [] ) { + $script_file = $script . '.js'; + $script_src_url = plugins_url( $script_file, __DIR__ ); + $script_asset_path = WC_EU_ABSPATH . $script . '.asset.php'; + $script_asset = file_exists( $script_asset_path ) ? require $script_asset_path : [ 'dependencies' => [] ]; + $script_asset['dependencies'] = array_merge( $script_asset['dependencies'], $dependencies ); + wp_register_script( + $handler, + $script_src_url, + $script_asset['dependencies'], + self::get_file_version( $script_file ), + true + ); + } + + /** + * Gets the file modified time as a cache buster if we're in dev mode, or the plugin version otherwise. + * + * @param string $file Local path to the file. + * @return string The cache buster value to use for the given file. + */ + public static function get_file_version( $file ): string { + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( WC_EU_ABSPATH . $file ) ) { + return (string) filemtime( WC_EU_ABSPATH . trim( $file, '/' ) ); + } + return WC_EU_VAT_VERSION; + } +} + +WC_EU_VAT_Number::init(); diff --git a/includes/class-wc-eu-vat-privacy.php b/includes/class-wc-eu-vat-privacy.php index 33da520..8f70395 100644 --- a/includes/class-wc-eu-vat-privacy.php +++ b/includes/class-wc-eu-vat-privacy.php @@ -1,293 +1,293 @@ -add_exporter( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_exporter' ) ); - $this->add_exporter( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_exporter' ) ); - - $this->add_eraser( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_eraser' ) ); - $this->add_eraser( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_eraser' ) ); - - if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' ) ) { - $this->add_eraser( 'woocommerce-eu-vat-number-subscriptions-data', __( 'WooCommerce EU VAT Subscriptions Data', 'woocommerce-eu-vat-number' ), array( $this, 'subscriptions_data_eraser' ) ); - } - } - - /** - * Returns a list of orders. - * - * @param string $email_address Email address of customer. - * @param int $page Current page. - * @param string $post_type Default: order. Set to 'subscription' if querying for subscription orders. - * - * @return array WP_Post - */ - protected function get_orders( $email_address, $page, $post_type = 'order' ) { - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - // Default Query Args. - $order_query = array( - 'status' => 'any', - 'limit' => 10, - 'page' => $page, - ); - - // Additional query args to retrieve WooCommerce orders. - if ( $user instanceof WP_User ) { - $order_query['customer_id'] = (int) $user->ID; - } else { - $order_query['billing_email'] = $email_address; - } - - // Using wc_get_orders for WC orders & data store for subscriptions. - if ( 'subscription' === $post_type ) { - $result = WC_Data_Store::load( $post_type )->get_orders( $order_query ); - } else { - $result = wc_get_orders( $order_query ); - } - - return $result; - } - - /** - * Gets the message of the privacy to display. - */ - public function get_privacy_message() { - // translators: Privacy Docs URL. - return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-eu-vat-number' ), 'https://docs.woocommerce.com/document/marketplace-privacy/#woocommerce-eu-vat-number' ) ); - } - - /** - * Handle exporting data for Orders. - * - * @param string $email_address E-mail address to export. - * @param int $page Pagination of data. - * - * @return array - */ - public function order_data_exporter( $email_address, $page = 1 ) { - $done = false; - $data_to_export = array(); - - $orders = $this->get_orders( $email_address, (int) $page ); - - $done = true; - - if ( 0 < count( $orders ) ) { - - /** @var \WC_Order $order */ - foreach ( $orders as $order ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_orders', - 'group_label' => __( 'Orders', 'woocommerce-eu-vat-number' ), - 'item_id' => 'order-' . $order->get_id(), - 'data' => array( - array( - 'name' => __( 'EU VAT number', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_vat_number', true ), - ), - array( - 'name' => __( 'EU Billing VAT number', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_billing_vat_number', true ), - ), - array( - 'name' => __( 'EU VAT country', 'woocommerce-eu-vat-number' ), - 'value' => $order->get_meta( '_customer_ip_country', true ), - ), - ), - ); - } - - $done = 10 > count( $orders ); - } - - return array( - 'data' => $data_to_export, - 'done' => $done, - ); - } - - /** - * Finds and exports customer data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * - * @return array An array of personal data in name value pairs - */ - public function customer_data_exporter( $email_address, $page ) { - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - $data_to_export = array(); - - if ( $user instanceof WP_User ) { - $data_to_export[] = array( - 'group_id' => 'woocommerce_customer', - 'group_label' => __( 'Customer Data', 'woocommerce-eu-vat-number' ), - 'item_id' => 'user', - 'data' => array( - array( - 'name' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), - 'value' => get_user_meta( $user->ID, 'vat_number', true ), - ), - ), - ); - } - - return array( - 'data' => $data_to_export, - 'done' => true, - ); - } - - /** - * Finds and erases customer data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * - * @return array An array of personal data in name value pairs - */ - public function customer_data_eraser( $email_address, $page ) { - $page = (int) $page; - $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. - - $vat_number = get_user_meta( $user->ID, 'vat_number', true ); - - $items_removed = false; - $messages = array(); - - if ( ! empty( $vat_number ) ) { - $items_removed = true; - delete_user_meta( $user->ID, 'vat_number' ); - $messages[] = __( 'EU VAT User Data Erased.', 'woocommerce-eu-vat-number' ); - } - - return array( - 'items_removed' => $items_removed, - 'items_retained' => false, - 'messages' => $messages, - 'done' => true, - ); - } - - /** - * Finds and erases order data by email address. - * - * @since 3.4.0 - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs - */ - public function order_data_eraser( $email_address, $page ) { - $orders = $this->get_orders( $email_address, (int) $page ); - return $this->maybe_erase_order_data( $orders ); - } - - /** - * Finds and erases Subscription order data by email address. - * - * @since 2.3.26 - * - * @param string $email_address The user email address. - * @param int $page Page. - * @return array An array of personal data in name value pairs. - */ - public function subscriptions_data_eraser( $email_address, $page ) { - $subscriptions = $this->get_orders( $email_address, (int) $page, 'subscription' ); - return $this->maybe_erase_order_data( $subscriptions ); - } - - /** - * Erase EU VAT data from WooCommerce/Subscription orders. - * - * @since 2.3.26 - * - * @param array $orders WooCommerce orders. - * - * @return array - */ - protected function maybe_erase_order_data( $orders ) { - $items_removed = false; - $items_retained = false; - $messages = array(); - $done = true; - - foreach ( (array) $orders as $order ) { - list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); - - $items_removed |= $removed; - $items_retained |= $retained; - $messages = array_merge( $messages, $msgs ); - } - - // Tell core if we have more orders to work on still. - $done = count( $orders ) < 10; - - return array( - 'items_removed' => $items_removed, - 'items_retained' => $items_retained, - 'messages' => $messages, - 'done' => $done, - ); - } - - /** - * Handle eraser of data tied to Orders - * - * @param WC_Order $order WooCommerce order/subscription object. - * - * @return array Array of order data removed/retained along with corresponding message. - */ - protected function maybe_handle_order( $order ) { - $order_id = $order->get_id(); - $message = array(); - - $vat_number = $order->get_meta( '_billing_vat_number', true ); - $vat_number = ( ! empty( $vat_number ) ) ? $vat_number : $order->get_meta( '_vat_number', true ); // Compat with version < 2.3.21. - $ip_country = $order->get_meta( '_customer_ip_country', true ); - - if ( empty( $vat_number ) && empty( $ip_country ) ) { - return array( false, false, array() ); - } - - $order->delete_meta_data( '_billing_vat_number' ); - $order->delete_meta_data( '_vat_number' ); // Compat with version < 2.3.21. - $order->delete_meta_data( '_customer_ip_country' ); - - $order->save(); - - // Set default post type to 'order'. - $post_type = 'order'; - - // Check whether the order is a subscription. - if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { - $post_type = 'subscription'; - } - - // translators: Post type, Order ID. - $message[] = sprintf( __( 'Removed EU VAT data from %1$s %2$s.', 'woocommerce-eu-vat-number' ), $post_type, $order_id ); - - return array( true, false, $message ); - } -} - -new WC_EU_VAT_Privacy(); +add_exporter( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_exporter' ) ); + $this->add_exporter( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_exporter' ) ); + + $this->add_eraser( 'woocommerce-eu-vat-number-customer-data', __( 'WooCommerce EU VAT Customer Data', 'woocommerce-eu-vat-number' ), array( $this, 'customer_data_eraser' ) ); + $this->add_eraser( 'woocommerce-eu-vat-number-order-data', __( 'WooCommerce EU VAT Order Data', 'woocommerce-eu-vat-number' ), array( $this, 'order_data_eraser' ) ); + + if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Subscriptions_Core_Plugin' ) ) { + $this->add_eraser( 'woocommerce-eu-vat-number-subscriptions-data', __( 'WooCommerce EU VAT Subscriptions Data', 'woocommerce-eu-vat-number' ), array( $this, 'subscriptions_data_eraser' ) ); + } + } + + /** + * Returns a list of orders. + * + * @param string $email_address Email address of customer. + * @param int $page Current page. + * @param string $post_type Default: order. Set to 'subscription' if querying for subscription orders. + * + * @return array WP_Post + */ + protected function get_orders( $email_address, $page, $post_type = 'order' ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + // Default Query Args. + $order_query = array( + 'status' => 'any', + 'limit' => 10, + 'page' => $page, + ); + + // Additional query args to retrieve WooCommerce orders. + if ( $user instanceof WP_User ) { + $order_query['customer_id'] = (int) $user->ID; + } else { + $order_query['billing_email'] = $email_address; + } + + // Using wc_get_orders for WC orders & data store for subscriptions. + if ( 'subscription' === $post_type ) { + $result = WC_Data_Store::load( $post_type )->get_orders( $order_query ); + } else { + $result = wc_get_orders( $order_query ); + } + + return $result; + } + + /** + * Gets the message of the privacy to display. + */ + public function get_privacy_message() { + // translators: Privacy Docs URL. + return wpautop( sprintf( __( 'By using this extension, you may be storing personal data or sharing data with an external service. Learn more about how this works, including what you may want to include in your privacy policy.', 'woocommerce-eu-vat-number' ), 'https://docs.woocommerce.com/document/marketplace-privacy/#woocommerce-eu-vat-number' ) ); + } + + /** + * Handle exporting data for Orders. + * + * @param string $email_address E-mail address to export. + * @param int $page Pagination of data. + * + * @return array + */ + public function order_data_exporter( $email_address, $page = 1 ) { + $done = false; + $data_to_export = array(); + + $orders = $this->get_orders( $email_address, (int) $page ); + + $done = true; + + if ( 0 < count( $orders ) ) { + + /** @var \WC_Order $order */ + foreach ( $orders as $order ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_orders', + 'group_label' => __( 'Orders', 'woocommerce-eu-vat-number' ), + 'item_id' => 'order-' . $order->get_id(), + 'data' => array( + array( + 'name' => __( 'EU VAT number', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_vat_number', true ), + ), + array( + 'name' => __( 'EU Billing VAT number', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_billing_vat_number', true ), + ), + array( + 'name' => __( 'EU VAT country', 'woocommerce-eu-vat-number' ), + 'value' => $order->get_meta( '_customer_ip_country', true ), + ), + ), + ); + } + + $done = 10 > count( $orders ); + } + + return array( + 'data' => $data_to_export, + 'done' => $done, + ); + } + + /** + * Finds and exports customer data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * + * @return array An array of personal data in name value pairs + */ + public function customer_data_exporter( $email_address, $page ) { + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + $data_to_export = array(); + + if ( $user instanceof WP_User ) { + $data_to_export[] = array( + 'group_id' => 'woocommerce_customer', + 'group_label' => __( 'Customer Data', 'woocommerce-eu-vat-number' ), + 'item_id' => 'user', + 'data' => array( + array( + 'name' => get_option( 'woocommerce_eu_vat_number_field_label', __( 'VAT number', 'woocommerce-eu-vat-number' ) ), + 'value' => get_user_meta( $user->ID, 'vat_number', true ), + ), + ), + ); + } + + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + /** + * Finds and erases customer data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * + * @return array An array of personal data in name value pairs + */ + public function customer_data_eraser( $email_address, $page ) { + $page = (int) $page; + $user = get_user_by( 'email', $email_address ); // Check if user has an ID in the DB to load stored personal data. + + $vat_number = get_user_meta( $user->ID, 'vat_number', true ); + + $items_removed = false; + $messages = array(); + + if ( ! empty( $vat_number ) ) { + $items_removed = true; + delete_user_meta( $user->ID, 'vat_number' ); + $messages[] = __( 'EU VAT User Data Erased.', 'woocommerce-eu-vat-number' ); + } + + return array( + 'items_removed' => $items_removed, + 'items_retained' => false, + 'messages' => $messages, + 'done' => true, + ); + } + + /** + * Finds and erases order data by email address. + * + * @since 3.4.0 + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs + */ + public function order_data_eraser( $email_address, $page ) { + $orders = $this->get_orders( $email_address, (int) $page ); + return $this->maybe_erase_order_data( $orders ); + } + + /** + * Finds and erases Subscription order data by email address. + * + * @since 2.3.26 + * + * @param string $email_address The user email address. + * @param int $page Page. + * @return array An array of personal data in name value pairs. + */ + public function subscriptions_data_eraser( $email_address, $page ) { + $subscriptions = $this->get_orders( $email_address, (int) $page, 'subscription' ); + return $this->maybe_erase_order_data( $subscriptions ); + } + + /** + * Erase EU VAT data from WooCommerce/Subscription orders. + * + * @since 2.3.26 + * + * @param array $orders WooCommerce orders. + * + * @return array + */ + protected function maybe_erase_order_data( $orders ) { + $items_removed = false; + $items_retained = false; + $messages = array(); + $done = true; + + foreach ( (array) $orders as $order ) { + list( $removed, $retained, $msgs ) = $this->maybe_handle_order( $order ); + + $items_removed |= $removed; + $items_retained |= $retained; + $messages = array_merge( $messages, $msgs ); + } + + // Tell core if we have more orders to work on still. + $done = count( $orders ) < 10; + + return array( + 'items_removed' => $items_removed, + 'items_retained' => $items_retained, + 'messages' => $messages, + 'done' => $done, + ); + } + + /** + * Handle eraser of data tied to Orders + * + * @param WC_Order $order WooCommerce order/subscription object. + * + * @return array Array of order data removed/retained along with corresponding message. + */ + protected function maybe_handle_order( $order ) { + $order_id = $order->get_id(); + $message = array(); + + $vat_number = $order->get_meta( '_billing_vat_number', true ); + $vat_number = ( ! empty( $vat_number ) ) ? $vat_number : $order->get_meta( '_vat_number', true ); // Compat with version < 2.3.21. + $ip_country = $order->get_meta( '_customer_ip_country', true ); + + if ( empty( $vat_number ) && empty( $ip_country ) ) { + return array( false, false, array() ); + } + + $order->delete_meta_data( '_billing_vat_number' ); + $order->delete_meta_data( '_vat_number' ); // Compat with version < 2.3.21. + $order->delete_meta_data( '_customer_ip_country' ); + + $order->save(); + + // Set default post type to 'order'. + $post_type = 'order'; + + // Check whether the order is a subscription. + if ( function_exists( 'wcs_is_subscription' ) && wcs_is_subscription( $order ) ) { + $post_type = 'subscription'; + } + + // translators: Post type, Order ID. + $message[] = sprintf( __( 'Removed EU VAT data from %1$s %2$s.', 'woocommerce-eu-vat-number' ), $post_type, $order_id ); + + return array( true, false, $message ); + } +} + +new WC_EU_VAT_Privacy(); diff --git a/includes/class-wc-eu-vat-report-ec-sales-list.php b/includes/class-wc-eu-vat-report-ec-sales-list.php index 273cfcd..d757c60 100644 --- a/includes/class-wc-eu-vat-report-ec-sales-list.php +++ b/includes/class-wc-eu-vat-report-ec-sales-list.php @@ -1,244 +1,244 @@ - - - - - __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), - 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), - 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), - ); - - $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { - $current_range = 'quarter'; - } - - $this->calculate_current_range( $current_range ); - - $hide_sidebar = true; - - include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; - } - - /** - * Get the current range and calculate the start and end dates - * - * @param string $current_range Report Range. - */ - public function calculate_current_range( $current_range ) { - $this->chart_groupby = 'month'; - $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); - $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); - - switch ( $current_range ) { - case 'prev_quarter': - $quarter = $quarter - 2; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } elseif ( -1 === $quarter ) { - $quarter = 3; - $year --; - } - break; - case 'last_quarter': - --$quarter; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } - break; - case 'custom': - parent::calculate_current_range( $current_range ); - return; - } - - if ( 1 === $quarter ) { - $this->start_date = strtotime( $year . '-01-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); - } elseif ( 2 === $quarter ) { - $this->start_date = strtotime( $year . '-04-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); - } elseif ( 3 === $quarter ) { - $this->start_date = strtotime( $year . '-07-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); - } elseif ( 4 === $quarter ) { - $this->start_date = strtotime( $year . '-10-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); - } - } - - /** - * Get the main chart - * - * @return void - */ - public function get_main_chart() { - $ec_sales1 = $this->get_order_report_data( - array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales', - ), - '_billing_vat_number' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_vat_number', - ), - '_billing_country' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_country', - ), - '_order_currency' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_order_currency', - ), - ), - 'where' => array( - array( - 'key' => 'meta__billing_country.meta_value', - 'value' => WC_EU_VAT_Number::get_eu_countries(), - 'operator' => 'in', - ), - array( - 'key' => 'meta__billing_vat_number.meta_value', - 'value' => '', - 'operator' => '!=', - ), - ), - 'group_by' => '_billing_vat_number', - 'order_by' => '_billing_vat_number ASC', - 'query_type' => 'get_results', - 'order_status' => array( 'completed' ), - 'filter_range' => true, - ) - ); - $ec_sales2 = $this->get_order_report_data( - array( - 'data' => array( - '_order_total' => array( - 'type' => 'meta', - 'function' => 'SUM', - 'name' => 'total_sales', - ), - '_vat_number' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_vat_number', - ), - '_billing_country' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_billing_country', - ), - '_order_currency' => array( - 'type' => 'meta', - 'function' => '', - 'name' => '_order_currency', - ), - ), - 'where' => array( - array( - 'key' => 'meta__billing_country.meta_value', - 'value' => WC_EU_VAT_Number::get_eu_countries(), - 'operator' => 'in', - ), - array( - 'key' => 'meta__vat_number.meta_value', - 'value' => '', - 'operator' => '!=', - ), - ), - 'group_by' => '_vat_number', - 'order_by' => '_vat_number ASC', - 'query_type' => 'get_results', - 'order_status' => array( 'completed' ), - 'filter_range' => true, - ) - ); - - $ec_sales = array_merge( $ec_sales1, $ec_sales2 ); - ?> - - - - - - - - - - - _billing_vat_number ) ? $ec_sale->_billing_vat_number : $ec_sale->_vat_number; - ?> - - - - - - - - - - - - - - -
_billing_country ); ?>_billing_country, '', $vat_number ) ); ?>total_sales, array( 'currency', $ec_sale->_order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
- + + + + __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), + 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), + 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), + ); + + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { + $current_range = 'quarter'; + } + + $this->calculate_current_range( $current_range ); + + $hide_sidebar = true; + + include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; + } + + /** + * Get the current range and calculate the start and end dates + * + * @param string $current_range Report Range. + */ + public function calculate_current_range( $current_range ) { + $this->chart_groupby = 'month'; + $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); + $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); + + switch ( $current_range ) { + case 'prev_quarter': + $quarter = $quarter - 2; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } elseif ( -1 === $quarter ) { + $quarter = 3; + $year --; + } + break; + case 'last_quarter': + --$quarter; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } + break; + case 'custom': + parent::calculate_current_range( $current_range ); + return; + } + + if ( 1 === $quarter ) { + $this->start_date = strtotime( $year . '-01-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); + } elseif ( 2 === $quarter ) { + $this->start_date = strtotime( $year . '-04-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); + } elseif ( 3 === $quarter ) { + $this->start_date = strtotime( $year . '-07-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); + } elseif ( 4 === $quarter ) { + $this->start_date = strtotime( $year . '-10-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); + } + } + + /** + * Get the main chart + * + * @return void + */ + public function get_main_chart() { + $ec_sales1 = $this->get_order_report_data( + array( + 'data' => array( + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_sales', + ), + '_billing_vat_number' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_billing_vat_number', + ), + '_billing_country' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_billing_country', + ), + '_order_currency' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_order_currency', + ), + ), + 'where' => array( + array( + 'key' => 'meta__billing_country.meta_value', + 'value' => WC_EU_VAT_Number::get_eu_countries(), + 'operator' => 'in', + ), + array( + 'key' => 'meta__billing_vat_number.meta_value', + 'value' => '', + 'operator' => '!=', + ), + ), + 'group_by' => '_billing_vat_number', + 'order_by' => '_billing_vat_number ASC', + 'query_type' => 'get_results', + 'order_status' => array( 'completed' ), + 'filter_range' => true, + ) + ); + $ec_sales2 = $this->get_order_report_data( + array( + 'data' => array( + '_order_total' => array( + 'type' => 'meta', + 'function' => 'SUM', + 'name' => 'total_sales', + ), + '_vat_number' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_vat_number', + ), + '_billing_country' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_billing_country', + ), + '_order_currency' => array( + 'type' => 'meta', + 'function' => '', + 'name' => '_order_currency', + ), + ), + 'where' => array( + array( + 'key' => 'meta__billing_country.meta_value', + 'value' => WC_EU_VAT_Number::get_eu_countries(), + 'operator' => 'in', + ), + array( + 'key' => 'meta__vat_number.meta_value', + 'value' => '', + 'operator' => '!=', + ), + ), + 'group_by' => '_vat_number', + 'order_by' => '_vat_number ASC', + 'query_type' => 'get_results', + 'order_status' => array( 'completed' ), + 'filter_range' => true, + ) + ); + + $ec_sales = array_merge( $ec_sales1, $ec_sales2 ); + ?> + + + + + + + + + + + _billing_vat_number ) ? $ec_sale->_billing_vat_number : $ec_sale->_vat_number; + ?> + + + + + + + + + + + + + + +
_billing_country ); ?>_billing_country, '', $vat_number ) ); ?>total_sales, array( 'currency', $ec_sale->_order_currency ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+ - - - - __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), - 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), - 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), - ); - - $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { - $current_range = 'quarter'; - } - - $this->calculate_current_range( $current_range ); - - $hide_sidebar = true; - - include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; - } - - /** - * Get the current range and calculate the start and end dates - * - * @param string $current_range Report Range. - */ - public function calculate_current_range( $current_range ) { - $this->chart_groupby = 'month'; - $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); - $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); - - switch ( $current_range ) { - case 'prev_quarter': - $quarter = $quarter - 2; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } elseif ( -1 === $quarter ) { - $quarter = 3; - $year --; - } - break; - case 'last_quarter': - --$quarter; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } - break; - case 'custom': - parent::calculate_current_range( $current_range ); - return; - } - - if ( 1 === $quarter ) { - $this->start_date = strtotime( $year . '-01-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); - } elseif ( 2 === $quarter ) { - $this->start_date = strtotime( $year . '-04-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); - } elseif ( 3 === $quarter ) { - $this->start_date = strtotime( $year . '-07-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); - } elseif ( 4 === $quarter ) { - $this->start_date = strtotime( $year . '-10-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); - } - } - - /** - * Get the main chart - * - * @return void - */ - public function get_main_chart() { - global $wpdb; - - $debug = false; - - // If debug is enabled, don't use cached data. - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - $debug = true; - } - - $line_data = $this->get_order_report_data( - array( - 'data' => array( - '_line_total' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_total', - ), - '_line_tax_data' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_tax_data', - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'refund_id', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'completed', 'refunded' ), - 'nocache' => $debug, - ) - ); - - $grouped_tax_rows = array(); - - foreach ( $line_data as $data ) { - $line_total = $data->_line_total; - $line_tax_data = maybe_unserialize( $data->_line_tax_data ); - - foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - if ( $line_total < 0 ) { - $grouped_tax_rows[ $tax_id ]->refunded_amount += $line_total; - } else { - $grouped_tax_rows[ $tax_id ]->amount += $line_total; - } - - if ( $tax_value < 0 ) { - $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( $tax_value ); - } else { - $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); - } - } - } - - $refunded_line_data = $this->get_order_report_data( - array( - 'data' => array( - '_line_total' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_total', - ), - '_line_tax_data' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_tax_data', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'refunded' ), - 'nocache' => $debug, - ) - ); - - foreach ( $refunded_line_data as $data ) { - $line_total = $data->_line_total; - $line_tax_data = maybe_unserialize( $data->_line_tax_data ); - - foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - $grouped_tax_rows[ $tax_id ]->refunded_amount += ( $line_total * -1 ); - $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( (float) $tax_value * -1 ); - } - } - - $shipping_tax_amount = $this->get_order_report_data( - array( - 'data' => array( - 'rate_id' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => 'rate_id', - ), - 'shipping_tax_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'tax', - 'function' => '', - 'name' => 'shipping_tax_amount', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'completed' ), - 'nocache' => $debug, - ) - ); - - foreach ( $shipping_tax_amount as $data ) { - $tax_value = $data->shipping_tax_amount; - $tax_id = $data->rate_id; - - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); - } - - $refund_amounts = $this->get_order_report_data( - array( - 'data' => array( - 'ID' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'refund_id', - ), - 'post_parent' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'order_id', - ), - '_refund_amount' => array( - 'type' => 'meta', - 'function' => '', - 'name' => 'total_refund', - ), - ), - 'group_by' => 'refund_id', - 'query_type' => 'get_results', - 'filter_range' => true, - 'order_status' => false, - 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ), - 'nocache' => $debug, - ) - ); - - foreach ( $refund_amounts as $refund_data ) { - $order = wc_get_order( $refund_data->order_id ); - - if ( is_object( $order ) ) { - $cached_results = get_transient( strtolower( get_class( $this ) . '_' . $refund_data->order_id ) ); - - if ( false === $cached_results ) { - $cached_results = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON order_itemmeta.order_item_id = order_items.order_item_id WHERE order_items.order_id = %d AND order_items.order_item_type = %s AND order_itemmeta.meta_key = %s", $refund_data->order_id, 'tax', 'rate_id' ) ); - - set_transient( strtolower( get_class( $this ) . '_' . $refund_data->order_id ), $cached_results, DAY_IN_SECONDS ); - } - - $tax_id = $cached_results; - - if ( isset( $grouped_tax_rows[ $tax_id ] ) ) { - - $total_refund = $refund_data->total_refund; - - // Subtract any line items from this total. - foreach ( $line_data as $data ) { - if ( $refund_data->refund_id === $data->refund_id ) { - $total_refund += $data->_line_total; - $line_tax_data = maybe_unserialize( $data->_line_tax_data ); - - foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { - $total_refund += wc_round_tax_total( $tax_value ); - } - } - } - - $grouped_tax_rows[ $tax_id ]->refunded_amount += -$total_refund; - } - } - } - ?> - - - - - - - - - - - - - - - - - $tax_row ) { - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); - - if ( ! is_object( $rate ) || ! in_array( $rate->tax_rate_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { - continue; - } - - /* - * We need to look for Greece (GR) code and - * convert it to (EL) as that is the standard - * country code when in VAT context. This is needed - * because WC core uses (GR) as the country code. - */ - $tax_rate_country = 'GR' === $rate->tax_rate_country ? 'EL' : $rate->tax_rate_country; - - /** - * Filters tax rate for the reports - * - * @since 2.0.0 - * - * @param string $rate->tax_rate Tax Rate. - * @param int $rate_id Rate Id. - * @param object $tax_row Tax Row. - */ - $tax_rate = apply_filters( 'woocommerce_reports_taxes_rate', $rate->tax_rate, $rate_id, $tax_row ); - // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped - ?> - - - - - - - - - - - - - - - - - - - - -
countries->countries[ $rate->tax_rate_country ] ); ?>%amount ); ?>refunded_amount * -1 ); ?>amount + $tax_row->refunded_amount ); ?>tax_amount ); ?>refunded_tax_amount * -1 ); ?>tax_amount + $tax_row->refunded_tax_amount ); ?>
- + + + + __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), + 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), + 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), + ); + + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { + $current_range = 'quarter'; + } + + $this->calculate_current_range( $current_range ); + + $hide_sidebar = true; + + include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; + } + + /** + * Get the current range and calculate the start and end dates + * + * @param string $current_range Report Range. + */ + public function calculate_current_range( $current_range ) { + $this->chart_groupby = 'month'; + $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); + $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); + + switch ( $current_range ) { + case 'prev_quarter': + $quarter = $quarter - 2; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } elseif ( -1 === $quarter ) { + $quarter = 3; + $year --; + } + break; + case 'last_quarter': + --$quarter; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } + break; + case 'custom': + parent::calculate_current_range( $current_range ); + return; + } + + if ( 1 === $quarter ) { + $this->start_date = strtotime( $year . '-01-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); + } elseif ( 2 === $quarter ) { + $this->start_date = strtotime( $year . '-04-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); + } elseif ( 3 === $quarter ) { + $this->start_date = strtotime( $year . '-07-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); + } elseif ( 4 === $quarter ) { + $this->start_date = strtotime( $year . '-10-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); + } + } + + /** + * Get the main chart + * + * @return void + */ + public function get_main_chart() { + global $wpdb; + + $debug = false; + + // If debug is enabled, don't use cached data. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + $debug = true; + } + + $line_data = $this->get_order_report_data( + array( + 'data' => array( + '_line_total' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_total', + ), + '_line_tax_data' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_tax_data', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'refund_id', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'completed', 'refunded' ), + 'nocache' => $debug, + ) + ); + + $grouped_tax_rows = array(); + + foreach ( $line_data as $data ) { + $line_total = $data->_line_total; + $line_tax_data = maybe_unserialize( $data->_line_tax_data ); + + foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + if ( $line_total < 0 ) { + $grouped_tax_rows[ $tax_id ]->refunded_amount += $line_total; + } else { + $grouped_tax_rows[ $tax_id ]->amount += $line_total; + } + + if ( $tax_value < 0 ) { + $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( $tax_value ); + } else { + $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); + } + } + } + + $refunded_line_data = $this->get_order_report_data( + array( + 'data' => array( + '_line_total' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_total', + ), + '_line_tax_data' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_tax_data', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'refunded' ), + 'nocache' => $debug, + ) + ); + + foreach ( $refunded_line_data as $data ) { + $line_total = $data->_line_total; + $line_tax_data = maybe_unserialize( $data->_line_tax_data ); + + foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + $grouped_tax_rows[ $tax_id ]->refunded_amount += ( $line_total * -1 ); + $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( (float) $tax_value * -1 ); + } + } + + $shipping_tax_amount = $this->get_order_report_data( + array( + 'data' => array( + 'rate_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => 'rate_id', + ), + 'shipping_tax_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'tax', + 'function' => '', + 'name' => 'shipping_tax_amount', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'completed' ), + 'nocache' => $debug, + ) + ); + + foreach ( $shipping_tax_amount as $data ) { + $tax_value = $data->shipping_tax_amount; + $tax_id = $data->rate_id; + + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); + } + + $refund_amounts = $this->get_order_report_data( + array( + 'data' => array( + 'ID' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'refund_id', + ), + 'post_parent' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'order_id', + ), + '_refund_amount' => array( + 'type' => 'meta', + 'function' => '', + 'name' => 'total_refund', + ), + ), + 'group_by' => 'refund_id', + 'query_type' => 'get_results', + 'filter_range' => true, + 'order_status' => false, + 'parent_order_status' => array( 'completed', 'processing', 'on-hold' ), + 'nocache' => $debug, + ) + ); + + foreach ( $refund_amounts as $refund_data ) { + $order = wc_get_order( $refund_data->order_id ); + + if ( is_object( $order ) ) { + $cached_results = get_transient( strtolower( get_class( $this ) . '_' . $refund_data->order_id ) ); + + if ( false === $cached_results ) { + $cached_results = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta LEFT JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON order_itemmeta.order_item_id = order_items.order_item_id WHERE order_items.order_id = %d AND order_items.order_item_type = %s AND order_itemmeta.meta_key = %s", $refund_data->order_id, 'tax', 'rate_id' ) ); + + set_transient( strtolower( get_class( $this ) . '_' . $refund_data->order_id ), $cached_results, DAY_IN_SECONDS ); + } + + $tax_id = $cached_results; + + if ( isset( $grouped_tax_rows[ $tax_id ] ) ) { + + $total_refund = $refund_data->total_refund; + + // Subtract any line items from this total. + foreach ( $line_data as $data ) { + if ( $refund_data->refund_id === $data->refund_id ) { + $total_refund += $data->_line_total; + $line_tax_data = maybe_unserialize( $data->_line_tax_data ); + + foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { + $total_refund += wc_round_tax_total( $tax_value ); + } + } + } + + $grouped_tax_rows[ $tax_id ]->refunded_amount += -$total_refund; + } + } + } + ?> + + + + + + + + + + + + + + + + + $tax_row ) { + $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); + + if ( ! is_object( $rate ) || ! in_array( $rate->tax_rate_country, WC_EU_VAT_Number::get_eu_countries(), true ) ) { + continue; + } + + /* + * We need to look for Greece (GR) code and + * convert it to (EL) as that is the standard + * country code when in VAT context. This is needed + * because WC core uses (GR) as the country code. + */ + $tax_rate_country = 'GR' === $rate->tax_rate_country ? 'EL' : $rate->tax_rate_country; + + /** + * Filters tax rate for the reports + * + * @since 2.0.0 + * + * @param string $rate->tax_rate Tax Rate. + * @param int $rate_id Rate Id. + * @param object $tax_row Tax Row. + */ + $tax_rate = apply_filters( 'woocommerce_reports_taxes_rate', $rate->tax_rate, $rate_id, $tax_row ); + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped + ?> + + + + + + + + + + + + + + + + + + + + +
countries->countries[ $rate->tax_rate_country ] ); ?>%amount ); ?>refunded_amount * -1 ); ?>amount + $tax_row->refunded_amount ); ?>tax_amount ); ?>refunded_tax_amount * -1 ); ?>tax_amount + $tax_row->refunded_tax_amount ); ?>
+ __( 'EC Sales List', 'woocommerce-eu-vat-number' ), - 'description' => '', - 'hide_title' => true, - 'callback' => array( __CLASS__, 'ec_sales_list' ), - ); - $reports['taxes']['reports']['eu_vat'] = array( - 'title' => __( 'EU VAT by state', 'woocommerce-eu-vat-number' ), - 'description' => '', - 'hide_title' => true, - 'callback' => array( __CLASS__, 'eu_vat' ), - ); - $reports['taxes']['reports']['non_eu_vat'] = array( - 'title' => __( 'Non EU Sales', 'woocommerce-eu-vat-number' ), - 'description' => '', - 'hide_title' => true, - 'callback' => array( __CLASS__, 'non_eu_vat' ), - ); - } - return $reports; - } - - /** - * Get a report - */ - public static function ec_sales_list() { - include_once 'class-wc-eu-vat-report-ec-sales-list.php'; - $report = new WC_EU_VAT_Report_EC_Sales_List(); - $report->output_report(); - } - - /** - * Get a report - */ - public static function eu_vat() { - include_once 'class-wc-eu-vat-report-eu-vat.php'; - $report = new WC_EU_VAT_Report_EU_VAT(); - $report->output_report(); - } - - /** - * Get a report - */ - public static function non_eu_vat() { - include_once 'class-wc-non-eu-sales-report.php'; - $report = new WC_Non_EU_Sales_Report(); - $report->output_report(); - } -} - -WC_EU_VAT_Reports::init(); + __( 'EC Sales List', 'woocommerce-eu-vat-number' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'ec_sales_list' ), + ); + $reports['taxes']['reports']['eu_vat'] = array( + 'title' => __( 'EU VAT by state', 'woocommerce-eu-vat-number' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'eu_vat' ), + ); + $reports['taxes']['reports']['non_eu_vat'] = array( + 'title' => __( 'Non EU Sales', 'woocommerce-eu-vat-number' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'non_eu_vat' ), + ); + } + return $reports; + } + + /** + * Get a report + */ + public static function ec_sales_list() { + include_once 'class-wc-eu-vat-report-ec-sales-list.php'; + $report = new WC_EU_VAT_Report_EC_Sales_List(); + $report->output_report(); + } + + /** + * Get a report + */ + public static function eu_vat() { + include_once 'class-wc-eu-vat-report-eu-vat.php'; + $report = new WC_EU_VAT_Report_EU_VAT(); + $report->output_report(); + } + + /** + * Get a report + */ + public static function non_eu_vat() { + include_once 'class-wc-non-eu-sales-report.php'; + $report = new WC_Non_EU_Sales_Report(); + $report->output_report(); + } +} + +WC_EU_VAT_Reports::init(); diff --git a/includes/class-wc-eu-vat-uk-number-api.php b/includes/class-wc-eu-vat-uk-number-api.php index 4b96c69..c7e0a13 100644 --- a/includes/class-wc-eu-vat-uk-number-api.php +++ b/includes/class-wc-eu-vat-uk-number-api.php @@ -1,70 +1,70 @@ -api_url . $vat_number; - $response = wp_remote_get( - esc_url_raw( $api_url ), - array( - 'timeout' => 30, - 'headers' => array( - 'Accept' => "application/vnd.hmrc.{$this->api_version}+json", - ), - 'user-agent' => 'WooCommerce/' . WC()->version, - ) - ); - - if ( is_wp_error( $response ) ) { - throw new Exception( $response->get_error_message() ); - } - - // Check if VAT number is valid. - $response_code = wp_remote_retrieve_response_code( $response ); - if ( 200 === $response_code ) { - $results = json_decode( wp_remote_retrieve_body( $response ), true ); - if ( isset( $results['target'] ) && isset( $results['target']['vatNumber'] ) ) { - return true; - } - } - return false; - } -} +api_url . $vat_number; + $response = wp_remote_get( + esc_url_raw( $api_url ), + array( + 'timeout' => 30, + 'headers' => array( + 'Accept' => "application/vnd.hmrc.{$this->api_version}+json", + ), + 'user-agent' => 'WooCommerce/' . WC()->version, + ) + ); + + if ( is_wp_error( $response ) ) { + throw new Exception( $response->get_error_message() ); + } + + // Check if VAT number is valid. + $response_code = wp_remote_retrieve_response_code( $response ); + if ( 200 === $response_code ) { + $results = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( isset( $results['target'] ) && isset( $results['target']['vatNumber'] ) ) { + return true; + } + } + return false; + } +} diff --git a/includes/class-wc-non-eu-sales-report.php b/includes/class-wc-non-eu-sales-report.php index 10eb04f..011e7ca 100644 --- a/includes/class-wc-non-eu-sales-report.php +++ b/includes/class-wc-non-eu-sales-report.php @@ -1,409 +1,409 @@ - - - - __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), - 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), - 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), - ); - - $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { - $current_range = 'quarter'; - } - - $this->calculate_current_range( $current_range ); - - $hide_sidebar = true; - - include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; - } - - /** - * Get the current range and calculate the start and end dates - * - * @param string $current_range Report Range. - */ - public function calculate_current_range( $current_range ) { - $this->chart_groupby = 'month'; - $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); - $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); - - switch ( $current_range ) { - case 'prev_quarter': - $quarter = $quarter - 2; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } elseif ( -1 === $quarter ) { - $quarter = 3; - $year --; - } - break; - case 'last_quarter': - --$quarter; - if ( 0 === $quarter ) { - $quarter = 4; - $year --; - } - break; - case 'custom': - parent::calculate_current_range( $current_range ); - return; - } - - if ( 1 === $quarter ) { - $this->start_date = strtotime( $year . '-01-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); - } elseif ( 2 === $quarter ) { - $this->start_date = strtotime( $year . '-04-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); - } elseif ( 3 === $quarter ) { - $this->start_date = strtotime( $year . '-07-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); - } elseif ( 4 === $quarter ) { - $this->start_date = strtotime( $year . '-10-01' ); - $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); - } - } - - /** - * Get the main chart - * - * @return void - */ - public function get_main_chart() { - global $wpdb; - - $line_data = $this->get_order_report_data( - array( - 'data' => array( - '_line_total' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_total', - ), - '_line_tax_data' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_tax_data', - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'ID', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'completed' ), - ) - ); - - $grouped_tax_rows = array(); - - foreach ( $line_data as $data ) { - $line_total = $data->_line_total; - $line_tax_data = maybe_unserialize( $data->_line_tax_data ); - - if ( $line_tax_data['total'] ) { - foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - if ( $line_total < 0 ) { - $grouped_tax_rows[ $tax_id ]->refunded_amount += $line_total; - } else { - $grouped_tax_rows[ $tax_id ]->amount += $line_total; - } - - if ( $tax_value < 0 ) { - $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( $tax_value ); - } else { - $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); - } - } - } else { - $order = wc_get_order( $data->ID ); - $country = $order->get_meta( '_billing_country', true ); - - if ( $country ) { - if ( ! isset( $grouped_tax_rows[ $country ] ) ) { - $grouped_tax_rows[ $country ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - if ( $line_total < 0 ) { - $grouped_tax_rows[ $country ]->refunded_amount += $line_total; - } else { - $grouped_tax_rows[ $country ]->amount += $line_total; - } - } - } - } - - $refunded_line_data = $this->get_order_report_data( - array( - 'data' => array( - '_line_total' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_total', - ), - '_line_tax_data' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => '_line_tax_data', - ), - 'ID' => array( - 'type' => 'post_data', - 'function' => '', - 'name' => 'ID', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'refunded' ), - ) - ); - - foreach ( $refunded_line_data as $data ) { - $line_total = $data->_line_total; - $line_tax_data = maybe_unserialize( $data->_line_tax_data ); - - if ( $line_tax_data['total'] ) { - foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - $grouped_tax_rows[ $tax_id ]->refunded_amount += ( $line_total * -1 ); - $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += ( wc_round_tax_total( $tax_value ) * -1 ); - } - } else { - $order = wc_get_order( $data->ID ); - $country = $order->get_meta( '_billing_country', true ); - - if ( $country ) { - if ( ! isset( $grouped_tax_rows[ $country ] ) ) { - $grouped_tax_rows[ $country ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - $grouped_tax_rows[ $country ]->refunded_amount += ( $line_total * -1 ); - } - } - } - - $shipping_tax_amount = $this->get_order_report_data( - array( - 'data' => array( - 'rate_id' => array( - 'type' => 'order_item_meta', - 'order_item_type' => '', - 'function' => '', - 'name' => 'rate_id', - ), - 'shipping_tax_amount' => array( - 'type' => 'order_item_meta', - 'order_item_type' => 'tax', - 'function' => '', - 'name' => 'shipping_tax_amount', - ), - ), - 'filter_range' => true, - 'query_type' => 'get_results', - 'group_by' => '', - 'order_types' => array( 'shop_order', 'shop_order_refund' ), - 'order_status' => array( 'completed' ), - ) - ); - - foreach ( $shipping_tax_amount as $data ) { - $tax_value = $data->shipping_tax_amount; - $tax_id = $data->rate_id; - - if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { - $grouped_tax_rows[ $tax_id ] = (object) array( - 'amount' => 0, - 'refunded_amount' => 0, - 'tax_amount' => 0, - 'refunded_tax_amount' => 0, - ); - } - - $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); - } - ?> - - - - - - - - - - - - - - - - $tax_row ) { - if ( is_numeric( $rate_id ) ) { - $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); - - if ( ! is_object( $rate ) ) { - continue; - } - - $country = $rate->tax_rate_country; - - /** - * Filters tax rate for the reports - * - * @since 2.1.7 - * - * @param string $rate->tax_rate Tax Rate. - * @param int $rate_id Rate Id. - * @param object $tax_row Tax Row. - */ - $tax_rate = apply_filters( 'woocommerce_reports_taxes_rate', $rate->tax_rate, $rate_id, $tax_row ) . '%'; - } else { - $country = $rate_id; - $tax_rate = '-'; - } - - if ( in_array( $country, WC_EU_VAT_Number::get_eu_countries(), true ) || empty( WC()->countries->countries[ $country ] ) ) { - continue; - } - - $found = true; - $total_amount += $tax_row->amount; - $total_refunded_amount += $tax_row->refunded_amount; - $total_final_amount += $tax_row->amount + $tax_row->refunded_amount; - $total_tax_amount += $tax_row->tax_amount; - $total_refunded_tax_amount += $tax_row->refunded_tax_amount; - $total_final_tax_amount += $tax_row->tax_amount + $tax_row->refunded_tax_amount; - // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
countries->countries[ $country ] ); ?>amount ); ?>refunded_amount * -1 ); ?>amount + $tax_row->refunded_amount ); ?>tax_amount ); ?>refunded_tax_amount * -1 ); ?>tax_amount + $tax_row->refunded_tax_amount ); ?>
- + + + __( 'Previous Quarter', 'woocommerce-eu-vat-number' ), + 'last_quarter' => __( 'Last Quarter', 'woocommerce-eu-vat-number' ), + 'quarter' => __( 'This Quarter', 'woocommerce-eu-vat-number' ), + ); + + $current_range = ! empty( $_GET['range'] ) ? sanitize_text_field( wp_unslash( $_GET['range'] ) ) : 'quarter'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! in_array( $current_range, array( 'custom', 'prev_quarter', 'last_quarter', 'quarter' ), true ) ) { + $current_range = 'quarter'; + } + + $this->calculate_current_range( $current_range ); + + $hide_sidebar = true; + + include WC()->plugin_path() . '/includes/admin/views/html-report-by-date.php'; + } + + /** + * Get the current range and calculate the start and end dates + * + * @param string $current_range Report Range. + */ + public function calculate_current_range( $current_range ) { + $this->chart_groupby = 'month'; + $quarter = absint( ceil( date( 'm', current_time( 'timestamp' ) ) / 3 ) ); + $year = absint( date( 'Y', current_time( 'timestamp' ) ) ); + + switch ( $current_range ) { + case 'prev_quarter': + $quarter = $quarter - 2; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } elseif ( -1 === $quarter ) { + $quarter = 3; + $year --; + } + break; + case 'last_quarter': + --$quarter; + if ( 0 === $quarter ) { + $quarter = 4; + $year --; + } + break; + case 'custom': + parent::calculate_current_range( $current_range ); + return; + } + + if ( 1 === $quarter ) { + $this->start_date = strtotime( $year . '-01-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-03-01' ) ) ); + } elseif ( 2 === $quarter ) { + $this->start_date = strtotime( $year . '-04-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-06-01' ) ) ); + } elseif ( 3 === $quarter ) { + $this->start_date = strtotime( $year . '-07-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-09-01' ) ) ); + } elseif ( 4 === $quarter ) { + $this->start_date = strtotime( $year . '-10-01' ); + $this->end_date = strtotime( date( 'Y-m-t', strtotime( $year . '-12-01' ) ) ); + } + } + + /** + * Get the main chart + * + * @return void + */ + public function get_main_chart() { + global $wpdb; + + $line_data = $this->get_order_report_data( + array( + 'data' => array( + '_line_total' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_total', + ), + '_line_tax_data' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_tax_data', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'ID', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'completed' ), + ) + ); + + $grouped_tax_rows = array(); + + foreach ( $line_data as $data ) { + $line_total = $data->_line_total; + $line_tax_data = maybe_unserialize( $data->_line_tax_data ); + + if ( $line_tax_data['total'] ) { + foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + if ( $line_total < 0 ) { + $grouped_tax_rows[ $tax_id ]->refunded_amount += $line_total; + } else { + $grouped_tax_rows[ $tax_id ]->amount += $line_total; + } + + if ( $tax_value < 0 ) { + $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += wc_round_tax_total( $tax_value ); + } else { + $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); + } + } + } else { + $order = wc_get_order( $data->ID ); + $country = $order->get_meta( '_billing_country', true ); + + if ( $country ) { + if ( ! isset( $grouped_tax_rows[ $country ] ) ) { + $grouped_tax_rows[ $country ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + if ( $line_total < 0 ) { + $grouped_tax_rows[ $country ]->refunded_amount += $line_total; + } else { + $grouped_tax_rows[ $country ]->amount += $line_total; + } + } + } + } + + $refunded_line_data = $this->get_order_report_data( + array( + 'data' => array( + '_line_total' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_total', + ), + '_line_tax_data' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => '_line_tax_data', + ), + 'ID' => array( + 'type' => 'post_data', + 'function' => '', + 'name' => 'ID', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'refunded' ), + ) + ); + + foreach ( $refunded_line_data as $data ) { + $line_total = $data->_line_total; + $line_tax_data = maybe_unserialize( $data->_line_tax_data ); + + if ( $line_tax_data['total'] ) { + foreach ( $line_tax_data['total'] as $tax_id => $tax_value ) { + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + $grouped_tax_rows[ $tax_id ]->refunded_amount += ( $line_total * -1 ); + $grouped_tax_rows[ $tax_id ]->refunded_tax_amount += ( wc_round_tax_total( $tax_value ) * -1 ); + } + } else { + $order = wc_get_order( $data->ID ); + $country = $order->get_meta( '_billing_country', true ); + + if ( $country ) { + if ( ! isset( $grouped_tax_rows[ $country ] ) ) { + $grouped_tax_rows[ $country ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + $grouped_tax_rows[ $country ]->refunded_amount += ( $line_total * -1 ); + } + } + } + + $shipping_tax_amount = $this->get_order_report_data( + array( + 'data' => array( + 'rate_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => '', + 'function' => '', + 'name' => 'rate_id', + ), + 'shipping_tax_amount' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'tax', + 'function' => '', + 'name' => 'shipping_tax_amount', + ), + ), + 'filter_range' => true, + 'query_type' => 'get_results', + 'group_by' => '', + 'order_types' => array( 'shop_order', 'shop_order_refund' ), + 'order_status' => array( 'completed' ), + ) + ); + + foreach ( $shipping_tax_amount as $data ) { + $tax_value = $data->shipping_tax_amount; + $tax_id = $data->rate_id; + + if ( ! isset( $grouped_tax_rows[ $tax_id ] ) ) { + $grouped_tax_rows[ $tax_id ] = (object) array( + 'amount' => 0, + 'refunded_amount' => 0, + 'tax_amount' => 0, + 'refunded_tax_amount' => 0, + ); + } + + $grouped_tax_rows[ $tax_id ]->tax_amount += wc_round_tax_total( $tax_value ); + } + ?> + + + + + + + + + + + + + + + + $tax_row ) { + if ( is_numeric( $rate_id ) ) { + $rate = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $rate_id ) ); + + if ( ! is_object( $rate ) ) { + continue; + } + + $country = $rate->tax_rate_country; + + /** + * Filters tax rate for the reports + * + * @since 2.1.7 + * + * @param string $rate->tax_rate Tax Rate. + * @param int $rate_id Rate Id. + * @param object $tax_row Tax Row. + */ + $tax_rate = apply_filters( 'woocommerce_reports_taxes_rate', $rate->tax_rate, $rate_id, $tax_row ) . '%'; + } else { + $country = $rate_id; + $tax_rate = '-'; + } + + if ( in_array( $country, WC_EU_VAT_Number::get_eu_countries(), true ) || empty( WC()->countries->countries[ $country ] ) ) { + continue; + } + + $found = true; + $total_amount += $tax_row->amount; + $total_refunded_amount += $tax_row->refunded_amount; + $total_final_amount += $tax_row->amount + $tax_row->refunded_amount; + $total_tax_amount += $tax_row->tax_amount; + $total_refunded_tax_amount += $tax_row->refunded_tax_amount; + $total_final_tax_amount += $tax_row->tax_amount + $tax_row->refunded_tax_amount; + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
countries->countries[ $country ] ); ?>amount ); ?>refunded_amount * -1 ); ?>amount + $tax_row->refunded_amount ); ?>tax_amount ); ?>refunded_tax_amount * -1 ); ?>tax_amount + $tax_row->refunded_tax_amount ); ?>
+ 'sectionend', - ), - array( - 'type' => 'title', - 'title' => __( 'EU VAT Number Handling', 'woocommerce-eu-vat-number' ), - 'id' => 'vat_number', - ), - array( - 'name' => __( 'VAT Number Field Label', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'The label that appears at checkout for the VAT number field.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_field_label', - 'type' => 'text', - 'default' => _x( 'VAT number', 'Default Field Label', 'woocommerce-eu-vat-number' ), - 'placeholder' => _x( 'VAT number', 'Default Field Label', 'woocommerce-eu-vat-number' ), - 'desc_tip' => true, - ), - array( - 'name' => __( 'VAT Number Field Description', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'The description that appears at checkout below the VAT number field.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_field_description', - 'type' => 'text', - 'desc_tip' => true, - ), - array( - 'name' => __( 'Remove VAT for Businesses in Your Base Country', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'Remove the VAT from the order even when the customer is in your base country.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_deduct_in_base', - 'type' => 'checkbox', - 'default' => 'yes', - ), - array( - 'name' => __( 'Failed Validation Handling', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'This option determines how orders are handled if the VAT number does not pass validation.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_failure_handling', - 'desc_tip' => true, - 'type' => 'select', - 'options' => array( - 'reject' => __( 'Reject the order and show the customer an error message', 'woocommerce-eu-vat-number' ), - 'accept_with_vat' => __( 'Accept the order, but do not remove VAT.', 'woocommerce-eu-vat-number' ), - 'accept' => __( 'Accept the order and remove VAT.', 'woocommerce-eu-vat-number' ), - ), - ), - array( - 'name' => __( 'Enable B2B Transactions', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'This will force users to check out with a VAT number, useful for sites that transact purely from B2B.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_b2b', - 'type' => 'checkbox', - 'default' => 'no', - ), - array( - 'type' => 'sectionend', - ), - array( - 'type' => 'title', - 'title' => __( 'EU VAT Digital Goods Handling', 'woocommerce-eu-vat-number' ), - /* translators: %1$s Opening anchor tag, %2$s Closing anchor tag */ - 'desc' => sprintf( __( 'EU VAT laws are changing for digital goods from the 1st Jan 2015 (affecting B2C transactions only). The VAT on digital goods must be calculated based on the customer location, and you need to collect evidence of this (IP address and Billing Address). You also need to setup VAT rates to charge the correct amount. %1$sRead this guide%2$s for instructions on doing this.', 'woocommerce-eu-vat-number' ), '', '' ), - 'id' => 'vat_number_digital_goods', - ), - array( - 'name' => __( 'Tax Classes for Digital Goods', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'This option tells the plugin which of your tax classes are for digital goods. This affects the taxable location of the user as of 1st Jan 2015.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_digital_tax_classes', - 'desc_tip' => true, - 'type' => 'multiselect', - 'class' => 'chosen_select wp-enhanced-select', - 'css' => 'width: 450px;', - 'default' => '', - 'options' => $classes_options, - 'custom_attributes' => array( - 'data-placeholder' => __( 'Select some tax classes', 'woocommerce-eu-vat-number' ), - ), - ), - array( - 'name' => __( 'Collect and Validate Evidence', 'woocommerce-eu-vat-number' ), - 'desc' => __( 'This validates the customer IP address against their billing address, and prompts the customer to self-declare their address if they do not match. Applies to digital goods and services only.', 'woocommerce-eu-vat-number' ), - 'id' => 'woocommerce_eu_vat_number_validate_ip', - 'type' => 'checkbox', - 'default' => 'no', - ), -); + 'sectionend', + ), + array( + 'type' => 'title', + 'title' => __( 'EU VAT Number Handling', 'woocommerce-eu-vat-number' ), + 'id' => 'vat_number', + ), + array( + 'name' => __( 'VAT Number Field Label', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'The label that appears at checkout for the VAT number field.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_field_label', + 'type' => 'text', + 'default' => _x( 'VAT number', 'Default Field Label', 'woocommerce-eu-vat-number' ), + 'placeholder' => _x( 'VAT number', 'Default Field Label', 'woocommerce-eu-vat-number' ), + 'desc_tip' => true, + ), + array( + 'name' => __( 'VAT Number Field Description', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'The description that appears at checkout below the VAT number field.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_field_description', + 'type' => 'text', + 'desc_tip' => true, + ), + array( + 'name' => __( 'Remove VAT for Businesses in Your Base Country', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'Remove the VAT from the order even when the customer is in your base country.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_deduct_in_base', + 'type' => 'checkbox', + 'default' => 'yes', + ), + array( + 'name' => __( 'Failed Validation Handling', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'This option determines how orders are handled if the VAT number does not pass validation.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_failure_handling', + 'desc_tip' => true, + 'type' => 'select', + 'options' => array( + 'reject' => __( 'Reject the order and show the customer an error message', 'woocommerce-eu-vat-number' ), + 'accept_with_vat' => __( 'Accept the order, but do not remove VAT.', 'woocommerce-eu-vat-number' ), + 'accept' => __( 'Accept the order and remove VAT.', 'woocommerce-eu-vat-number' ), + ), + ), + array( + 'name' => __( 'Enable B2B Transactions', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'This will force users to check out with a VAT number, useful for sites that transact purely from B2B.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_b2b', + 'type' => 'checkbox', + 'default' => 'no', + ), + array( + 'type' => 'sectionend', + ), + array( + 'type' => 'title', + 'title' => __( 'EU VAT Digital Goods Handling', 'woocommerce-eu-vat-number' ), + /* translators: %1$s Opening anchor tag, %2$s Closing anchor tag */ + 'desc' => sprintf( __( 'EU VAT laws are changing for digital goods from the 1st Jan 2015 (affecting B2C transactions only). The VAT on digital goods must be calculated based on the customer location, and you need to collect evidence of this (IP address and Billing Address). You also need to setup VAT rates to charge the correct amount. %1$sRead this guide%2$s for instructions on doing this.', 'woocommerce-eu-vat-number' ), '', '' ), + 'id' => 'vat_number_digital_goods', + ), + array( + 'name' => __( 'Tax Classes for Digital Goods', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'This option tells the plugin which of your tax classes are for digital goods. This affects the taxable location of the user as of 1st Jan 2015.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_digital_tax_classes', + 'desc_tip' => true, + 'type' => 'multiselect', + 'class' => 'chosen_select wp-enhanced-select', + 'css' => 'width: 450px;', + 'default' => '', + 'options' => $classes_options, + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select some tax classes', 'woocommerce-eu-vat-number' ), + ), + ), + array( + 'name' => __( 'Collect and Validate Evidence', 'woocommerce-eu-vat-number' ), + 'desc' => __( 'This validates the customer IP address against their billing address, and prompts the customer to self-declare their address if they do not match. Applies to digital goods and services only.', 'woocommerce-eu-vat-number' ), + 'id' => 'woocommerce_eu_vat_number_validate_ip', + 'type' => 'checkbox', + 'default' => 'no', + ), +); diff --git a/includes/vies/class-vies-client.php b/includes/vies/class-vies-client.php index a35541f..9a00707 100644 --- a/includes/vies/class-vies-client.php +++ b/includes/vies/class-vies-client.php @@ -1,100 +1,100 @@ - 'VIES_Response', - ); - - /** - * Check VAT - * - * @param string $country_code Country code. - * @param string $vat_number VAT number. - * - * @return VIES_Response - */ - public function check_vat( $country_code, $vat_number ) { - return $this->get_soap_client()->checkVat( - array( - 'countryCode' => $country_code, - 'vatNumber' => $vat_number, - ) - ); - } - - /** - * Get SOAP client - * - * @return SoapClient - */ - public function get_soap_client() { - - /** - * Filters SoapClient Parameters. - * - * @since 2.3.7 - * - * @param array $args SoapClient Parameters. - */ - $soap_parameters = apply_filters( - 'woocommerce_eu_vat_number_soap_parameters', - array( - 'classmap' => $this->classmap, - 'cache_wsdl' => WSDL_CACHE_BOTH, - 'connection_timeout' => 45, - 'user_agent' => 'Mozilla', // the request fails unless a (dummy) user agent is specified. - ) - ); - - if ( null === $this->soapclient ) { - try { - $this->soapclient = new SoapClient( - $this->wsdl, - $soap_parameters - ); - } catch ( Exception $e ) { - return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); - } - } - - return $this->soapclient; - - } -} - + 'VIES_Response', + ); + + /** + * Check VAT + * + * @param string $country_code Country code. + * @param string $vat_number VAT number. + * + * @return VIES_Response + */ + public function check_vat( $country_code, $vat_number ) { + return $this->get_soap_client()->checkVat( + array( + 'countryCode' => $country_code, + 'vatNumber' => $vat_number, + ) + ); + } + + /** + * Get SOAP client + * + * @return SoapClient + */ + public function get_soap_client() { + + /** + * Filters SoapClient Parameters. + * + * @since 2.3.7 + * + * @param array $args SoapClient Parameters. + */ + $soap_parameters = apply_filters( + 'woocommerce_eu_vat_number_soap_parameters', + array( + 'classmap' => $this->classmap, + 'cache_wsdl' => WSDL_CACHE_BOTH, + 'connection_timeout' => 45, + 'user_agent' => 'Mozilla', // the request fails unless a (dummy) user agent is specified. + ) + ); + + if ( null === $this->soapclient ) { + try { + $this->soapclient = new SoapClient( + $this->wsdl, + $soap_parameters + ); + } catch ( Exception $e ) { + return new WP_Error( 'api', __( 'Error communicating with the VAT validation server - please try again.', 'woocommerce-eu-vat-number' ) ); + } + } + + return $this->soapclient; + + } +} + diff --git a/includes/vies/class-vies-response.php b/includes/vies/class-vies-response.php index 965e3a4..8686a3a 100644 --- a/includes/vies/class-vies-response.php +++ b/includes/vies/class-vies-response.php @@ -1,117 +1,117 @@ -countryCode; - } - - /** - * Get the VAT Number - * - * @return string - */ - public function get_vat_number() { - return $this->vatNumber; - } - - /** - * Get the date of the request - * - * @return DateTime - */ - public function get_request_date() { - if ( ! $this->requestDate instanceof DateTime ) { - $this->requestDate = new DateTime( $this->requestDate ); - } - return $this->requestDate; - } - - /** - * Whether the number is valid or not - * - * @return boolean - */ - public function is_valid() { - return $this->valid; - } - - /** - * Get the name - * - * @return string - */ - public function get_name() { - return $this->name; - } - - /** - * Get the address - * - * @return string - */ - public function get_address() { - return $this->address; - } - -} - +countryCode; + } + + /** + * Get the VAT Number + * + * @return string + */ + public function get_vat_number() { + return $this->vatNumber; + } + + /** + * Get the date of the request + * + * @return DateTime + */ + public function get_request_date() { + if ( ! $this->requestDate instanceof DateTime ) { + $this->requestDate = new DateTime( $this->requestDate ); + } + return $this->requestDate; + } + + /** + * Whether the number is valid or not + * + * @return boolean + */ + public function is_valid() { + return $this->valid; + } + + /** + * Get the name + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * Get the address + * + * @return string + */ + public function get_address() { + return $this->address; + } + +} + diff --git a/includes/wc-eu-vat-functions.php b/includes/wc-eu-vat-functions.php index 717c06a..2fb70a0 100644 --- a/includes/wc-eu-vat-functions.php +++ b/includes/wc-eu-vat-functions.php @@ -1,79 +1,79 @@ -get_meta( '_billing_vat_number', true ) ? $order->get_meta( '_billing_vat_number', true ) : ''; - - if ( ! $vat ) { - $vat = $order->get_meta( '_vat_number', true ) ? $order->get_meta( '_vat_number', true ) : ''; - } - - return $vat; -} - -/** - * Display 0.00% VAT line item and reason. - * - * @param array $total_rows Order item totals array. - * @param WC_Order $order WC_Order object. - * @param string $tax_display Tax display (incl or excl). - */ -function wc_eu_vat_maybe_add_zero_tax_display( $total_rows, $order, $tax_display ) { - // Display in Email and Invoice only. - if ( is_account_page() ) { - return $total_rows; - } - - $is_vat_exempt = ( 'yes' === $order->get_meta( 'is_vat_exempt' ) ); - $is_valid = wc_string_to_bool( $order->get_meta( '_vat_number_is_valid', true ) ); - - // Check if VAT number is valid and tax is exempted. - if ( wc_tax_enabled() && $is_vat_exempt && $is_valid && empty( $order->get_tax_totals() ) ) { - /** - * Filters the reason for zero tax. - * - * @since 2.8.1 - */ - $zero_tax_reason = apply_filters( 'wc_eu_vat_number_zero_tax_reason', __( 'Supply of services subject to reverse charge', 'woocommerce-eu-vat-number' ) ); - $display_tax_reason = '
' . esc_html( $zero_tax_reason ) . ''; - - if ( 'excl' === $tax_display ) { - if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { - $tax_line_item = array( - 'tax' => array( - 'label' => WC()->countries->tax_or_vat() . ':', - 'value' => wc_price( $order->get_total_tax(), array( 'currency' => $order->get_currency() ) ) . $display_tax_reason, - ), - ); - - // Add zero tax line item before grand total. - array_splice( $total_rows, count( $total_rows ) - 1, 0, $tax_line_item ); - } elseif ( isset( $total_rows['tax'] ) && isset( $total_rows['tax']['value'] ) ) { - $total_rows['tax']['value'] = $total_rows['tax']['value'] . $display_tax_reason; - } - } elseif ( 'incl' === $tax_display ) { - // translators: %1$s: Tax label (VAT or Tax). - $append_zero_tax = sprintf( esc_html__( ' (inc. 0.00%% %1$s) ', 'woocommerce-eu-vat-number' ), WC()->countries->tax_or_vat() ) . $display_tax_reason; - - // Append zero tax details to Grand Total. - $total_rows['order_total']['value'] = $total_rows['order_total']['value'] . $append_zero_tax; - } - } - - return $total_rows; -} +get_meta( '_billing_vat_number', true ) ? $order->get_meta( '_billing_vat_number', true ) : ''; + + if ( ! $vat ) { + $vat = $order->get_meta( '_vat_number', true ) ? $order->get_meta( '_vat_number', true ) : ''; + } + + return $vat; +} + +/** + * Display 0.00% VAT line item and reason. + * + * @param array $total_rows Order item totals array. + * @param WC_Order $order WC_Order object. + * @param string $tax_display Tax display (incl or excl). + */ +function wc_eu_vat_maybe_add_zero_tax_display( $total_rows, $order, $tax_display ) { + // Display in Email and Invoice only. + if ( is_account_page() ) { + return $total_rows; + } + + $is_vat_exempt = ( 'yes' === $order->get_meta( 'is_vat_exempt' ) ); + $is_valid = wc_string_to_bool( $order->get_meta( '_vat_number_is_valid', true ) ); + + // Check if VAT number is valid and tax is exempted. + if ( wc_tax_enabled() && $is_vat_exempt && $is_valid && empty( $order->get_tax_totals() ) ) { + /** + * Filters the reason for zero tax. + * + * @since 2.8.1 + */ + $zero_tax_reason = apply_filters( 'wc_eu_vat_number_zero_tax_reason', __( 'Supply of services subject to reverse charge', 'woocommerce-eu-vat-number' ) ); + $display_tax_reason = '
' . esc_html( $zero_tax_reason ) . ''; + + if ( 'excl' === $tax_display ) { + if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { + $tax_line_item = array( + 'tax' => array( + 'label' => WC()->countries->tax_or_vat() . ':', + 'value' => wc_price( $order->get_total_tax(), array( 'currency' => $order->get_currency() ) ) . $display_tax_reason, + ), + ); + + // Add zero tax line item before grand total. + array_splice( $total_rows, count( $total_rows ) - 1, 0, $tax_line_item ); + } elseif ( isset( $total_rows['tax'] ) && isset( $total_rows['tax']['value'] ) ) { + $total_rows['tax']['value'] = $total_rows['tax']['value'] . $display_tax_reason; + } + } elseif ( 'incl' === $tax_display ) { + // translators: %1$s: Tax label (VAT or Tax). + $append_zero_tax = sprintf( esc_html__( ' (inc. 0.00%% %1$s) ', 'woocommerce-eu-vat-number' ), WC()->countries->tax_or_vat() ) . $display_tax_reason; + + // Append zero tax details to Grand Total. + $total_rows['order_total']['value'] = $total_rows['order_total']['value'] . $append_zero_tax; + } + } + + return $total_rows; +} diff --git a/languages/woocommerce-eu-vat-number.pot b/languages/woocommerce-eu-vat-number.pot index 4e5e090..9e352d3 100644 --- a/languages/woocommerce-eu-vat-number.pot +++ b/languages/woocommerce-eu-vat-number.pot @@ -2,10 +2,10 @@ # This file is distributed under the GNU General Public License v3.0. msgid "" msgstr "" -"Project-Id-Version: WooCommerce EU VAT Number 2.8.3\n" +"Project-Id-Version: WooCommerce EU VAT Number 2.8.8\n" "Report-Msgid-Bugs-To: " "https://wordpress.org/support/plugin/woocommerce-eu-vat-number\n" -"POT-Creation-Date: 2023-04-03 17:24:12+00:00\n" +"POT-Creation-Date: 2023-09-18 02:12:29+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -22,8 +22,8 @@ msgstr "" #: includes/class-wc-eu-vat-my-account.php:207 #: includes/class-wc-eu-vat-my-account.php:218 #: includes/class-wc-eu-vat-number.php:186 -#: includes/class-wc-eu-vat-number.php:710 -#: includes/class-wc-eu-vat-number.php:724 +#: includes/class-wc-eu-vat-number.php:709 +#: includes/class-wc-eu-vat-number.php:723 #: includes/class-wc-eu-vat-privacy.php:147 #: includes/class-wc-eu-vat-report-ec-sales-list.php:215 msgid "VAT number" @@ -83,14 +83,14 @@ msgstr "" #: includes/class-wc-eu-vat-admin.php:368 #: includes/class-wc-eu-vat-my-account.php:218 -#: includes/class-wc-eu-vat-number.php:710 +#: includes/class-wc-eu-vat-number.php:709 #. translators: %1$s VAT number field label, %2$s VAT number, %3$s Billing #. Country. #. translators: 1: VAT number field label, 2: VAT Number, 3: Billing country msgid "You have entered an invalid %1$s (%2$s) for your billing country (%3$s)." msgstr "" -#: includes/class-wc-eu-vat-admin.php:463 +#: includes/class-wc-eu-vat-admin.php:464 #. translators: %1$s Opening strong tag, %2$s Closing strong tag, %3$s Break #. tag. msgid "" @@ -102,7 +102,7 @@ msgid "" "from %1$sNorthern Ireland%2$s." msgstr "" -#: includes/class-wc-eu-vat-admin.php:474 +#: includes/class-wc-eu-vat-admin.php:475 #. translators: %1$s Opening strong tag, %2$s Closing strong tag. msgid "" "By using %1$sWooCommerce EU VAT Number%2$s plugin, you've agreed that the " @@ -111,19 +111,19 @@ msgid "" "tax specific questions." msgstr "" -#: includes/class-wc-eu-vat-admin.php:479 +#: includes/class-wc-eu-vat-admin.php:480 msgid "I understand" msgstr "" -#: includes/class-wc-eu-vat-blocks.php:196 +#: includes/class-wc-eu-vat-blocks.php:197 msgid "Location confirmation." msgstr "" -#: includes/class-wc-eu-vat-blocks.php:246 +#: includes/class-wc-eu-vat-blocks.php:247 msgid "Invalid VAT number." msgstr "" -#: includes/class-wc-eu-vat-blocks.php:325 +#: includes/class-wc-eu-vat-blocks.php:326 msgid "VAT Data" msgstr "" @@ -150,11 +150,11 @@ msgstr "" msgid "VAT number updated successfully!" msgstr "" -#: includes/class-wc-eu-vat-number.php:275 +#: includes/class-wc-eu-vat-number.php:277 msgid "VAT number is required." msgstr "" -#: includes/class-wc-eu-vat-number.php:281 +#: includes/class-wc-eu-vat-number.php:283 #. translators: %1$s - VAT number field label, %2$s - VAT Number from user, #. %3$s - Billing country. msgid "" @@ -162,23 +162,23 @@ msgid "" "country (%3$s)." msgstr "" -#: includes/class-wc-eu-vat-number.php:295 -#: includes/class-wc-eu-vat-number.php:313 +#: includes/class-wc-eu-vat-number.php:297 +#: includes/class-wc-eu-vat-number.php:315 #: includes/vies/class-vies-client.php:92 msgid "Error communicating with the VAT validation server - please try again." msgstr "" -#: includes/class-wc-eu-vat-number.php:580 +#: includes/class-wc-eu-vat-number.php:582 #. translators: %s: VAT Number msgid "VAT Number: %s" msgstr "" -#: includes/class-wc-eu-vat-number.php:724 +#: includes/class-wc-eu-vat-number.php:723 #. translators: 1: VAT number field label, 2: Billing country msgid "%1$s is a required field for your billing country (%2$s)." msgstr "" -#: includes/class-wc-eu-vat-number.php:737 +#: includes/class-wc-eu-vat-number.php:736 #. translators: 1: Ip Address. msgid "" "Your IP Address (%1$s) does not match your billing country (%2$s). European " @@ -466,27 +466,27 @@ msgstr "" msgid "Save" msgstr "" -#: woocommerce-eu-vat-number.php:154 woocommerce-eu-vat-number.php:166 -#: woocommerce-eu-vat-number.php:177 +#: woocommerce-eu-vat-number.php:171 woocommerce-eu-vat-number.php:183 +#: woocommerce-eu-vat-number.php:194 woocommerce-eu-vat-number.php:241 #. translators: %1$s: Plugin page link start %2$s Link end #. translators: %1$s: Minimum version %2$s: Plugin page link start %3$s Link #. end msgid "WooCommerce EU VAT Number is inactive." msgstr "" -#: woocommerce-eu-vat-number.php:154 +#: woocommerce-eu-vat-number.php:171 msgid "" "The WooCommerce plugin must be active for EU VAT Number to work. %1$sPlease " "install and activate WooCommerce%2$s." msgstr "" -#: woocommerce-eu-vat-number.php:166 +#: woocommerce-eu-vat-number.php:183 msgid "" "The WooCommerce plugin must be at least version %1$s for EU VAT Number to " "work. %2$sPlease upgrade WooCommerce%3$s." msgstr "" -#: woocommerce-eu-vat-number.php:177 +#: woocommerce-eu-vat-number.php:194 msgid "" "Your server does not provide SOAP support which is required functionality " "for communicating with VIES. You will need to reach out to your web hosting " @@ -494,31 +494,39 @@ msgid "" "server." msgstr "" -#: woocommerce-eu-vat-number.php:254 +#: woocommerce-eu-vat-number.php:245 +#. translators: %1$s: Settings link start %2$s: Link end +msgid "" +"The EU VAT Number extension functionality requires taxes be enabled on your " +"store. To do so, go to %1$sWooCommerce > Settings%2$s, check the Enable tax " +"rates and calculations checkbox, and click the Save changes button." +msgstr "" + +#: woocommerce-eu-vat-number.php:325 msgid "Settings" msgstr "" -#: woocommerce-eu-vat-number.php:274 +#: woocommerce-eu-vat-number.php:345 msgid "View Plugin Documentation" msgstr "" -#: woocommerce-eu-vat-number.php:274 +#: woocommerce-eu-vat-number.php:345 msgid "Docs" msgstr "" -#: woocommerce-eu-vat-number.php:280 +#: woocommerce-eu-vat-number.php:351 msgid "View Plugin Changelog" msgstr "" -#: woocommerce-eu-vat-number.php:280 +#: woocommerce-eu-vat-number.php:351 msgid "Changelog" msgstr "" -#: woocommerce-eu-vat-number.php:286 +#: woocommerce-eu-vat-number.php:357 msgid "Support" msgstr "" -#: woocommerce-eu-vat-number.php:323 +#: woocommerce-eu-vat-number.php:394 msgid "VAT Number" msgstr "" diff --git a/src/components/IpAddressNotice/index.js b/src/components/IpAddressNotice/index.js index 6efc8da..fcc180f 100644 --- a/src/components/IpAddressNotice/index.js +++ b/src/components/IpAddressNotice/index.js @@ -1,130 +1,130 @@ -/** - * External dependencies - */ -import { sprintf, __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { CheckboxControl } from '@woocommerce/blocks-checkout'; -import { - useState, - createInterpolateElement, - useEffect, -} from '@wordpress/element'; -import { getSetting } from '@woocommerce/settings'; - -/** - * Renders a warning when the customer's IP address does not match the billing country they chose. - * - * @param {Object} props Incoming props for the component. - * @param {boolean} props.shouldValidateIp Whether WC EU VAT Number is set up to validate the customer's IP address. - * @param {string} props.billingCountry The customer's billing country. - * @param {string} props.ipAddress The customer's IP address. - * @param {string} props.ipCountry The country that the customer's IP is from. - * @param {Object} props.validation Object containing WooCommerce Blocks validation methods. - * @param {Object} props.checkoutExtensionData Object containing setCheckoutData to allow us to pass data to the checkout endpoint. - * @return {JSX.Element|null} The component to render, or null if there's nothing to render. - */ -export const IpAddressNotice = ( { - shouldValidateIp, - billingCountry, - ipAddress, - ipCountry, - validation, - checkoutExtensionData, -} ) => { - const countryLocales = getSetting( 'allowedCountries', {} ); - const { setExtensionData } = checkoutExtensionData; - let countryName = billingCountry; - if ( !! countryLocales[ billingCountry ] ) { - countryName = countryLocales[ billingCountry ]; - } - const { extensions } = useSelect( ( select ) => - select( 'wc/store/cart' ).getCartData() - ); - const { cart_has_digital_goods: cartHasDigitalGoods } = - extensions[ 'woocommerce-eu-vat-number' ]; - - const { setValidationErrors, clearValidationError, getValidationError } = - validation; - - const [ isChecked, setIsChecked ] = useState( false ); - const [ isDirty, setIsDirty ] = useState( false ); - const validationErrorId = 'billing_vat_number_ip_address_notice'; - - const validationErrorMessage = sprintf( - /* translators: %1$s is the user's IP address, %2$s is the billing country name */ - __( - 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox above.', - 'woocommerce-eu-vat-number' - ), - ipAddress, - countryName - ); - - const isSelfDeclarationRequired = - shouldValidateIp && cartHasDigitalGoods && billingCountry !== ipCountry; - - useEffect( () => { - if ( ! isChecked && isSelfDeclarationRequired ) { - setValidationErrors( { - [ validationErrorId ]: { - message: validationErrorMessage, - hidden: ! isDirty, - }, - } ); - } - if ( isChecked ) { - clearValidationError( validationErrorId ); - } - - // When unmounting we need to clear the error to allow checkout to continue. - return () => { - clearValidationError( validationErrorId ); - }; - }, [ setValidationErrors, isChecked, isDirty ] ); - - if ( ! isSelfDeclarationRequired ) { - return null; - } - const validationError = getValidationError( validationErrorId ); - - return ( -
-
- %s.', - 'woocommerce-eu-vat-number' - ), - countryName - ), - { strong: } - ) } - checked={ isChecked } - onChange={ ( checked ) => { - if ( ! isDirty ) { - setIsDirty( true ); - } - setIsChecked( checked ); - setExtensionData( - 'woocommerce-eu-vat-number', - 'location_confirmation', - checked - ); - } } - /> -
- { validationError?.message && ! validationError?.hidden ? ( -
- { validationError.message } -
- ) : null } -
- ); -}; +/** + * External dependencies + */ +import { sprintf, __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { + useState, + createInterpolateElement, + useEffect, +} from '@wordpress/element'; +import { getSetting } from '@woocommerce/settings'; + +/** + * Renders a warning when the customer's IP address does not match the billing country they chose. + * + * @param {Object} props Incoming props for the component. + * @param {boolean} props.shouldValidateIp Whether WC EU VAT Number is set up to validate the customer's IP address. + * @param {string} props.billingCountry The customer's billing country. + * @param {string} props.ipAddress The customer's IP address. + * @param {string} props.ipCountry The country that the customer's IP is from. + * @param {Object} props.validation Object containing WooCommerce Blocks validation methods. + * @param {Object} props.checkoutExtensionData Object containing setCheckoutData to allow us to pass data to the checkout endpoint. + * @return {JSX.Element|null} The component to render, or null if there's nothing to render. + */ +export const IpAddressNotice = ( { + shouldValidateIp, + billingCountry, + ipAddress, + ipCountry, + validation, + checkoutExtensionData, +} ) => { + const countryLocales = getSetting( 'allowedCountries', {} ); + const { setExtensionData } = checkoutExtensionData; + let countryName = billingCountry; + if ( !! countryLocales[ billingCountry ] ) { + countryName = countryLocales[ billingCountry ]; + } + const { extensions } = useSelect( ( select ) => + select( 'wc/store/cart' ).getCartData() + ); + const { cart_has_digital_goods: cartHasDigitalGoods } = + extensions[ 'woocommerce-eu-vat-number' ]; + + const { setValidationErrors, clearValidationError, getValidationError } = + validation; + + const [ isChecked, setIsChecked ] = useState( false ); + const [ isDirty, setIsDirty ] = useState( false ); + const validationErrorId = 'billing_vat_number_ip_address_notice'; + + const validationErrorMessage = sprintf( + /* translators: %1$s is the user's IP address, %2$s is the billing country name */ + __( + 'Your IP Address (%1$s) does not match your billing country (%2$s). European VAT laws require your IP address to match your billing country when purchasing digital goods in the EU. Please confirm you are located within your billing country using the checkbox above.', + 'woocommerce-eu-vat-number' + ), + ipAddress, + countryName + ); + + const isSelfDeclarationRequired = + shouldValidateIp && cartHasDigitalGoods && billingCountry !== ipCountry; + + useEffect( () => { + if ( ! isChecked && isSelfDeclarationRequired ) { + setValidationErrors( { + [ validationErrorId ]: { + message: validationErrorMessage, + hidden: ! isDirty, + }, + } ); + } + if ( isChecked ) { + clearValidationError( validationErrorId ); + } + + // When unmounting we need to clear the error to allow checkout to continue. + return () => { + clearValidationError( validationErrorId ); + }; + }, [ setValidationErrors, isChecked, isDirty ] ); + + if ( ! isSelfDeclarationRequired ) { + return null; + } + const validationError = getValidationError( validationErrorId ); + + return ( +
+
+ %s.', + 'woocommerce-eu-vat-number' + ), + countryName + ), + { strong: } + ) } + checked={ isChecked } + onChange={ ( checked ) => { + if ( ! isDirty ) { + setIsDirty( true ); + } + setIsChecked( checked ); + setExtensionData( + 'woocommerce-eu-vat-number', + 'location_confirmation', + checked + ); + } } + /> +
+ { validationError?.message && ! validationError?.hidden ? ( +
+ { validationError.message } +
+ ) : null } +
+ ); +}; diff --git a/src/components/form-step-block/attributes.js b/src/components/form-step-block/attributes.js index 9213e80..b3b2405 100644 --- a/src/components/form-step-block/attributes.js +++ b/src/components/form-step-block/attributes.js @@ -1,28 +1,28 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -const attributes = ( { - defaultTitle = __( 'Step', 'woocommerce-eu-vat-number' ), - defaultDescription = __( - 'Step description text.', - 'woocommerce-eu-vat-number' - ), - defaultShowStepNumber = true, -} ) => ( { - title: { - type: 'string', - default: defaultTitle, - }, - description: { - type: 'string', - default: defaultDescription, - }, - showStepNumber: { - type: 'boolean', - default: defaultShowStepNumber, - }, -} ); - -export default attributes; +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +const attributes = ( { + defaultTitle = __( 'Step', 'woocommerce-eu-vat-number' ), + defaultDescription = __( + 'Step description text.', + 'woocommerce-eu-vat-number' + ), + defaultShowStepNumber = true, +} ) => ( { + title: { + type: 'string', + default: defaultTitle, + }, + description: { + type: 'string', + default: defaultDescription, + }, + showStepNumber: { + type: 'boolean', + default: defaultShowStepNumber, + }, +} ); + +export default attributes; diff --git a/src/components/form-step-block/editor.scss b/src/components/form-step-block/editor.scss index d1439af..5c1c3f6 100644 --- a/src/components/form-step-block/editor.scss +++ b/src/components/form-step-block/editor.scss @@ -1,12 +1,12 @@ - -.wc-block-checkout__additional_fields { - margin: 1.5em 0 0; -} -.wc-block-components-checkout-step__description-placeholder { - opacity: 0.5; -} - -.wc-block-components-checkout-step__title { - display: flex; - width: 100%; -} + +.wc-block-checkout__additional_fields { + margin: 1.5em 0 0; +} +.wc-block-components-checkout-step__description-placeholder { + opacity: 0.5; +} + +.wc-block-components-checkout-step__title { + display: flex; + width: 100%; +} diff --git a/src/components/form-step-block/form-step-block.js b/src/components/form-step-block/form-step-block.js index 95f8615..97f1ebb 100644 --- a/src/components/form-step-block/form-step-block.js +++ b/src/components/form-step-block/form-step-block.js @@ -1,91 +1,91 @@ -/* eslint-disable jsdoc/require-param */ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import classnames from 'classnames'; -import { - PlainText, - InspectorControls, - useBlockProps, -} from '@wordpress/block-editor'; -import { PanelBody, ToggleControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import FormStepHeading from './form-step-heading'; - -/** - * Form Step Block for use in the editor. - */ -export const FormStepBlock = ( { - attributes, - setAttributes, - className = '', - children, -} ) => { - const { title = '', description = '', showStepNumber = true } = attributes; - const blockProps = useBlockProps( { - className: classnames( 'wc-block-components-checkout-step', className, { - 'wc-block-components-checkout-step--with-step-number': - showStepNumber, - } ), - } ); - return ( -
- - - - setAttributes( { - showStepNumber: ! showStepNumber, - } ) - } - /> - - - - setAttributes( { title: value } ) } - /> - </FormStepHeading> - <div className="wc-block-components-checkout-step__container"> - <p className="wc-block-components-checkout-step__description"> - <PlainText - className={ - ! description - ? 'wc-block-components-checkout-step__description-placeholder' - : '' - } - value={ description } - placeholder={ __( - 'Optional text for this form step.', - 'woocommerce-eu-vat-number' - ) } - onChange={ ( value ) => - setAttributes( { - description: value, - } ) - } - /> - </p> - <div className="wc-block-components-checkout-step__content"> - { children } - </div> - </div> - </div> - ); -}; +/* eslint-disable jsdoc/require-param */ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { + PlainText, + InspectorControls, + useBlockProps, +} from '@wordpress/block-editor'; +import { PanelBody, ToggleControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import FormStepHeading from './form-step-heading'; + +/** + * Form Step Block for use in the editor. + */ +export const FormStepBlock = ( { + attributes, + setAttributes, + className = '', + children, +} ) => { + const { title = '', description = '', showStepNumber = true } = attributes; + const blockProps = useBlockProps( { + className: classnames( 'wc-block-components-checkout-step', className, { + 'wc-block-components-checkout-step--with-step-number': + showStepNumber, + } ), + } ); + return ( + <div { ...blockProps }> + <InspectorControls> + <PanelBody + title={ __( + 'Form Step Options', + 'woocommerce-eu-vat-number' + ) } + > + <ToggleControl + label={ __( + 'Show step number', + 'woocommerce-eu-vat-number' + ) } + checked={ showStepNumber } + onChange={ () => + setAttributes( { + showStepNumber: ! showStepNumber, + } ) + } + /> + </PanelBody> + </InspectorControls> + <FormStepHeading> + <PlainText + className={ '' } + value={ title } + onChange={ ( value ) => setAttributes( { title: value } ) } + /> + </FormStepHeading> + <div className="wc-block-components-checkout-step__container"> + <p className="wc-block-components-checkout-step__description"> + <PlainText + className={ + ! description + ? 'wc-block-components-checkout-step__description-placeholder' + : '' + } + value={ description } + placeholder={ __( + 'Optional text for this form step.', + 'woocommerce-eu-vat-number' + ) } + onChange={ ( value ) => + setAttributes( { + description: value, + } ) + } + /> + </p> + <div className="wc-block-components-checkout-step__content"> + { children } + </div> + </div> + </div> + ); +}; diff --git a/src/components/form-step-block/form-step-heading.js b/src/components/form-step-block/form-step-heading.js index 8fb0619..cd19efc 100644 --- a/src/components/form-step-block/form-step-heading.js +++ b/src/components/form-step-block/form-step-heading.js @@ -1,25 +1,25 @@ -/* eslint-disable jsdoc/require-param */ -/** - * External dependencies - */ - -/** - * Step Heading Component - */ -const FormStepHeading = ( { children, stepHeadingContent } ) => ( - <div className="wc-block-components-checkout-step__heading"> - <h2 - aria-hidden="true" - className="wc-block-components-checkout-step__title wc-block-components-title" - > - { children } - </h2> - { !! stepHeadingContent && ( - <span className="wc-block-components-checkout-step__heading-content"> - { stepHeadingContent } - </span> - ) } - </div> -); - -export default FormStepHeading; +/* eslint-disable jsdoc/require-param */ +/** + * External dependencies + */ + +/** + * Step Heading Component + */ +const FormStepHeading = ( { children, stepHeadingContent } ) => ( + <div className="wc-block-components-checkout-step__heading"> + <h2 + aria-hidden="true" + className="wc-block-components-checkout-step__title wc-block-components-title" + > + { children } + </h2> + { !! stepHeadingContent && ( + <span className="wc-block-components-checkout-step__heading-content"> + { stepHeadingContent } + </span> + ) } + </div> +); + +export default FormStepHeading; diff --git a/src/components/form-step-block/index.js b/src/components/form-step-block/index.js index 57c29ca..739fa6b 100644 --- a/src/components/form-step-block/index.js +++ b/src/components/form-step-block/index.js @@ -1,3 +1,3 @@ -export * from './attributes'; -export * from './form-step-block'; -export * from './form-step-heading'; +export * from './attributes'; +export * from './form-step-block'; +export * from './form-step-heading'; diff --git a/src/components/form-step/index.js b/src/components/form-step/index.js index 7933400..ade9418 100644 --- a/src/components/form-step/index.js +++ b/src/components/form-step/index.js @@ -1,82 +1,82 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * Internal dependencies - */ -import './style.scss'; -import { withFilteredAttributes } from '../../utils'; -import attributes from '../form-step-block/attributes'; - -const StepHeading = ( { title, stepHeadingContent } ) => ( - <div className="wc-block-components-checkout-step__heading"> - <h2 - aria-hidden="true" - className="wc-block-components-checkout-step__title wc-block-components-title" - > - { title } - </h2> - { !! stepHeadingContent && ( - <span className="wc-block-components-checkout-step__heading-content"> - { stepHeadingContent } - </span> - ) } - </div> -); -const FormStep = ( { - id, - className, - title, - legend, - description, - children, - disabled = false, - showStepNumber = true, - stepHeadingContent = () => undefined, -} ) => { - // If the form step doesn't have a legend or title, render a <div> instead - // of a <fieldset>. - const Element = legend || title ? 'fieldset' : 'div'; - - return ( - <Element - className={ classnames( - className, - 'wc-block-components-checkout-step', - { - 'wc-block-components-checkout-step--with-step-number': - showStepNumber, - 'wc-block-components-checkout-step--disabled': disabled, - } - ) } - id={ id } - disabled={ disabled } - > - { !! ( legend || title ) && ( - <legend className="screen-reader-text"> - { legend || title } - </legend> - ) } - { !! title && ( - <StepHeading - title={ title } - stepHeadingContent={ stepHeadingContent() } - /> - ) } - <div className="wc-block-components-checkout-step__container"> - { !! description && ( - <p className="wc-block-components-checkout-step__description"> - { description } - </p> - ) } - <div className="wc-block-components-checkout-step__content"> - { children } - </div> - </div> - </Element> - ); -}; - -export default withFilteredAttributes( attributes( {} ) )( FormStep ); +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { withFilteredAttributes } from '../../utils'; +import attributes from '../form-step-block/attributes'; + +const StepHeading = ( { title, stepHeadingContent } ) => ( + <div className="wc-block-components-checkout-step__heading"> + <h2 + aria-hidden="true" + className="wc-block-components-checkout-step__title wc-block-components-title" + > + { title } + </h2> + { !! stepHeadingContent && ( + <span className="wc-block-components-checkout-step__heading-content"> + { stepHeadingContent } + </span> + ) } + </div> +); +const FormStep = ( { + id, + className, + title, + legend, + description, + children, + disabled = false, + showStepNumber = true, + stepHeadingContent = () => undefined, +} ) => { + // If the form step doesn't have a legend or title, render a <div> instead + // of a <fieldset>. + const Element = legend || title ? 'fieldset' : 'div'; + + return ( + <Element + className={ classnames( + className, + 'wc-block-components-checkout-step', + { + 'wc-block-components-checkout-step--with-step-number': + showStepNumber, + 'wc-block-components-checkout-step--disabled': disabled, + } + ) } + id={ id } + disabled={ disabled } + > + { !! ( legend || title ) && ( + <legend className="screen-reader-text"> + { legend || title } + </legend> + ) } + { !! title && ( + <StepHeading + title={ title } + stepHeadingContent={ stepHeadingContent() } + /> + ) } + <div className="wc-block-components-checkout-step__container"> + { !! description && ( + <p className="wc-block-components-checkout-step__description"> + { description } + </p> + ) } + <div className="wc-block-components-checkout-step__content"> + { children } + </div> + </div> + </Element> + ); +}; + +export default withFilteredAttributes( attributes( {} ) )( FormStep ); diff --git a/src/components/form-step/style.scss b/src/components/form-step/style.scss index 52a203b..3962307 100644 --- a/src/components/form-step/style.scss +++ b/src/components/form-step/style.scss @@ -1,129 +1,129 @@ -.wc-block-components-form { - counter-reset: checkout-step; -} - -.wc-block-components-form .wc-block-components-checkout-step { - position: relative; - border: none; - padding: 0 0 0 $gap-large; - background: none; - margin: 0; - - .is-mobile &, - .is-small & { - padding-left: 0; - } -} - -.wc-block-components-checkout-step--disabled { - opacity: 0.6; -} - -.wc-block-components-checkout-step__container { - position: relative; -} - -.wc-block-components-checkout-step__content > * { - margin-bottom: em($gap); -} -.wc-block-components-checkout-step--with-step-number .wc-block-components-checkout-step__content > :last-child { - margin-bottom: 0; - padding-bottom: em($gap-large); -} - -.wc-block-components-checkout-step__heading { - display: flex; - justify-content: space-between; - align-content: center; - flex-wrap: wrap; - margin: em($gap-small) 0 em($gap); - position: relative; - align-items: center; - gap: em($gap); - - .wc-block-components-express-payment-continue-rule + .wc-block-components-checkout-step & { - margin-top: 0; - } -} - -.wc-block-components-checkout-step:first-child .wc-block-components-checkout-step__heading { - margin-top: 0; -} - -.wc-block-components-checkout-step__title { - margin: 0 $gap-small 0 0; -} - -.wc-block-components-checkout-step__heading-content { - font-size: 0.75em; - - a { - font-weight: bold; - color: inherit; - } -} - -.wc-block-components-checkout-step__description { - font-size: 0.875em; - line-height: 1.25; - margin-bottom: $gap; -} - -.wc-block-components-checkout-step--with-step-number { - .wc-block-components-checkout-step__title::before { - @include reset-box(); - background: transparent; - counter-increment: checkout-step; - content: "\00a0" counter(checkout-step) "."; - content: "\00a0" counter(checkout-step) "." / ""; - position: absolute; - width: $gap-large; - left: -$gap-large; - top: 0; - text-align: center; - transform: translateX(-50%); - - .is-mobile &, - .is-small & { - position: static; - transform: none; - left: auto; - top: auto; - content: counter(checkout-step) ".\00a0"; - content: counter(checkout-step) ".\00a0" / ""; - } - } - - .wc-block-components-checkout-step__container::after { - content: ""; - height: 100%; - border-left: 1px solid; - opacity: 0.3; - position: absolute; - left: -$gap-large; - top: 0; - } - - .is-mobile &, - .is-small & { - .wc-block-components-checkout-step__title::before { - position: static; - transform: none; - left: auto; - top: auto; - content: counter(checkout-step) ".\00a0"; - content: counter(checkout-step) ".\00a0" / ""; - } - .wc-block-components-checkout-step__container::after { - content: unset; - } - } -} - -.editor-styles-wrapper { - .wp-block h4.wc-block-components-checkout-step__title { - font-size: 1em; - line-height: 24px; - margin: 0 $gap-small 0 0; - } -} +.wc-block-components-form { + counter-reset: checkout-step; +} + +.wc-block-components-form .wc-block-components-checkout-step { + position: relative; + border: none; + padding: 0 0 0 $gap-large; + background: none; + margin: 0; + + .is-mobile &, + .is-small & { + padding-left: 0; + } +} + +.wc-block-components-checkout-step--disabled { + opacity: 0.6; +} + +.wc-block-components-checkout-step__container { + position: relative; +} + +.wc-block-components-checkout-step__content > * { + margin-bottom: em($gap); +} +.wc-block-components-checkout-step--with-step-number .wc-block-components-checkout-step__content > :last-child { + margin-bottom: 0; + padding-bottom: em($gap-large); +} + +.wc-block-components-checkout-step__heading { + display: flex; + justify-content: space-between; + align-content: center; + flex-wrap: wrap; + margin: em($gap-small) 0 em($gap); + position: relative; + align-items: center; + gap: em($gap); + + .wc-block-components-express-payment-continue-rule + .wc-block-components-checkout-step & { + margin-top: 0; + } +} + +.wc-block-components-checkout-step:first-child .wc-block-components-checkout-step__heading { + margin-top: 0; +} + +.wc-block-components-checkout-step__title { + margin: 0 $gap-small 0 0; +} + +.wc-block-components-checkout-step__heading-content { + font-size: 0.75em; + + a { + font-weight: bold; + color: inherit; + } +} + +.wc-block-components-checkout-step__description { + font-size: 0.875em; + line-height: 1.25; + margin-bottom: $gap; +} + +.wc-block-components-checkout-step--with-step-number { + .wc-block-components-checkout-step__title::before { + @include reset-box(); + background: transparent; + counter-increment: checkout-step; + content: "\00a0" counter(checkout-step) "."; + content: "\00a0" counter(checkout-step) "." / ""; + position: absolute; + width: $gap-large; + left: -$gap-large; + top: 0; + text-align: center; + transform: translateX(-50%); + + .is-mobile &, + .is-small & { + position: static; + transform: none; + left: auto; + top: auto; + content: counter(checkout-step) ".\00a0"; + content: counter(checkout-step) ".\00a0" / ""; + } + } + + .wc-block-components-checkout-step__container::after { + content: ""; + height: 100%; + border-left: 1px solid; + opacity: 0.3; + position: absolute; + left: -$gap-large; + top: 0; + } + + .is-mobile &, + .is-small & { + .wc-block-components-checkout-step__title::before { + position: static; + transform: none; + left: auto; + top: auto; + content: counter(checkout-step) ".\00a0"; + content: counter(checkout-step) ".\00a0" / ""; + } + .wc-block-components-checkout-step__container::after { + content: unset; + } + } +} + +.editor-styles-wrapper { + .wp-block h4.wc-block-components-checkout-step__title { + font-size: 1em; + line-height: 24px; + margin: 0 $gap-small 0 0; + } +} diff --git a/src/components/vat-input/index.js b/src/components/vat-input/index.js index 585e4a6..3dd9863 100644 --- a/src/components/vat-input/index.js +++ b/src/components/vat-input/index.js @@ -1,323 +1,323 @@ -/** - * External dependencies - */ -import { useState, useEffect } from '@wordpress/element'; - -import { withInstanceId } from '@wordpress/compose'; -import { extensionCartUpdate } from '@woocommerce/blocks-checkout'; -import { getSetting } from '@woocommerce/settings'; -import classnames from 'classnames'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { useStoreCart } from '../../hooks'; -import { validateCountryVatNumberFormat } from '../../utils'; -import { IpAddressNotice } from '../IpAddressNotice'; -import FormStep from '../form-step'; - -const VatInput = ( props ) => { - const { setValidationErrors, clearValidationError, getValidationError } = - props.validation; - const { checkoutExtensionData } = props; - - const { billingAddress, extensions } = useStoreCart(); - const { - b2b_required, - eu_countries, - uk_ni_notice, - input_label, - input_description, - failure_handler, - } = wc_eu_vat_params; - - const { - title = __( 'VAT Number', 'woocommerce-eu-vat-number' ), - description = '', - showStepNumber = true, - } = props; - - const { - woocommerce_eu_vat_number_validate_ip, - ip_address: ipAddress, - ip_country: ipCountry, - } = getSetting( 'woocommerce-eu-vat-number_data' ); - const shouldValidateIp = woocommerce_eu_vat_number_validate_ip === 'yes'; - - const [ isActive, setIsActive ] = useState( false ); - const [ vat, setVat ] = useState( - extensions[ 'woocommerce-eu-vat-number' ]?.vat_number - ); - const [ previousVat, setPreviousVat ] = useState( - extensions[ 'woocommerce-eu-vat-number' ]?.vat_number - ); - const [ required ] = useState( - b2b_required === 'yes' && - eu_countries.indexOf( billingAddress.country ) !== -1 - ); - const [ available, setAvailable ] = useState( - eu_countries.indexOf( billingAddress.country ) !== -1 - ); - const [ showGBNotice, setShowGBNotice ] = useState( - billingAddress.country === 'GB' - ); - - const textInputId = 'billing_vat_number'; - const validationErrorId = 'billing_vat_number_error'; - const className = 'eu-vat-extra-css'; - - const error = getValidationError( validationErrorId ); - const hasError = error?.hidden === false && error?.message !== ''; - - /** - * This effect sets location_confirmation, it is required regardless of shouldValidateIp, or else the API will give - * us an error due to missing parameters. - */ - useEffect( () => { - checkoutExtensionData.setExtensionData( - 'woocommerce-eu-vat-number', - 'location_confirmation', - false - ); - }, [ checkoutExtensionData.setExtensionData ] ); - - /** - * On country change, check if the country is in the EU, if not then set the VAT Input to not available. - * Also update whether the GB Notice should show. - */ - useEffect( () => { - setAvailable( eu_countries.indexOf( billingAddress.country ) !== -1 ); - setShowGBNotice( billingAddress.country === 'GB' ); - }, [ eu_countries, billingAddress.country ] ); - - /** - * This effect handles setting the validation error immediately. - */ - useEffect( () => { - if ( ! required || ( typeof vat === 'string' && vat.length > 0 ) ) { - return; - } - // Instantly set the validation error when loading the component to ensure submissions without touching the - // field cause an error to show when submitting. It is hidden at first because we don't want to show the error - // until the user has touched the field. - setValidationErrors( { - [ validationErrorId ]: { - message: __( - 'VAT number is required.', - 'woocommerce-eu-vat-number' - ), - hidden: true, - }, - } ); - }, [ required, setValidationErrors, validationErrorId ] ); - - const verifyVat = () => { - if ( vat === previousVat ) { - return; - } - clearValidationError( validationErrorId ); - // If required is true,and vat is null, undefined, or an empty string. - if ( - required && - ( vat === null || - typeof vat === 'undefined' || - ( typeof vat === 'string' && vat.length === 0 ) ) - ) { - setValidationErrors( { - [ validationErrorId ]: { - message: __( - 'VAT number is required.', - 'woocommerce-eu-vat-number' - ), - hidden: false, - }, - } ); - } - extensionCartUpdate( { - namespace: 'woocommerce-eu-vat-number', - data: { - vat_number: vat, - }, - cartPropsToReceive: [ 'extensions' ], - } ).then( () => { - setPreviousVat( vat ); - // If we get here and VAT is empty and not required then remove the error message. This is because an empty - // VAT Number still causes an erorr when we try to update the server, but we need to update the server - // to tell it the VAT Number is empty... - if ( ! required && ! vat ) { - clearValidationError( validationErrorId ); - } - } ); - }; - - const init = () => { - if ( - typeof vat === 'string' && - vat.length > 0 && - ( ( ! validateCountryVatNumberFormat( - billingAddress.country, - vat - ) && - failure_handler === 'reject' ) || - ( ! extensions[ 'woocommerce-eu-vat-number' ]?.validation - ?.valid && - vat === - extensions[ 'woocommerce-eu-vat-number' ]?.vat_number && - failure_handler === 'reject' ) ) - ) { - setValidationErrors( { - [ validationErrorId ]: { - message: - extensions[ 'woocommerce-eu-vat-number' ]?.validation - .error, - hidden: false, - }, - } ); - } - }; - - /** - * This effect runs when extensions[ 'woocommerce-eu-vat-number' ]?.validation.error changes. We can set the error - * on the front-end based on this, or clear it if it's empty. - */ - useEffect( () => { - // If vat number is empty AND this is the first render, skip showing the error. The error will have hidden: true - // if it's the first render. Subsequent interactions will unhide the error if the state is invalid. - // If vat is not empty, then the state can be considered 'dirty' and we continue to show the error. - const validationError = getValidationError( validationErrorId ); - if ( ! vat && validationError?.hidden ) { - return; - } - - if ( ! required && ! vat ) { - clearValidationError( validationErrorId ); - return; - } - if ( extensions[ 'woocommerce-eu-vat-number' ]?.validation?.error ) { - setValidationErrors( { - [ validationErrorId ]: { - message: - extensions[ 'woocommerce-eu-vat-number' ]?.validation - .error, - hidden: false, - }, - } ); - return; - } - clearValidationError( validationErrorId ); - }, [ extensions[ 'woocommerce-eu-vat-number' ]?.validation.error ] ); - - /** - * This effect kicks off the init function when the component mounts for the first time. - */ - useEffect( init, [] ); - - const onChange = ( event ) => { - const { value: nextValue } = event.target; - clearValidationError( validationErrorId ); - if ( - typeof nextValue === 'string' && - nextValue.length === 0 && - required - ) { - setValidationErrors( { - [ validationErrorId ]: { - message: __( - 'VAT Number is required.', - 'woocommerce-eu-vat-number' - ), - hidden: false, - }, - } ); - } - setVat( nextValue ); - }; - - const HasError = () => { - if ( ! hasError ) return null; - return ( - <div className="wc-block-components-validation-error" role="alert"> - <p id={ validationErrorId }> - { getValidationError( validationErrorId )?.message } - </p> - </div> - ); - }; - - if ( ! available ) { - return <></>; - } - - return ( - <FormStep - id="shipping-fields" - className={ classnames( - 'wc-block-checkout__shipping-fields', - className - ) } - title={ title } - description={ description } - showStepNumber={ showStepNumber } - > - <div> - <div - className={ classnames( - 'wc-block-components-text-input', - className, - { - 'is-active': isActive || !! vat, - }, - { - 'has-error': hasError, - } - ) } - > - <input - type="text" - // eslint-disable-next-line camelcase - aria-label={ input_label } - id={ textInputId } - value={ vat || '' } - onChange={ ( event ) => { - onChange( event ); - } } - onFocus={ () => setIsActive( true ) } - onBlur={ () => { - setIsActive( false ); - verifyVat(); - } } - aria-invalid={ hasError === true } - disabled={ false } - required={ required } - /> - - <label htmlFor={ textInputId }> - { input_label } - { required === true ? null : ' (optional)' } - </label> - <HasError /> - <div className="wc-eu-vat-checkout-uk-notice"> - <div> - <span>{ input_description }</span> - </div> - <span>{ showGBNotice ? uk_ni_notice : null }</span> - </div> - </div> - { ( ! vat && b2b_required === 'no' ) || - ( ! vat && ! b2b_required ) ? ( - <IpAddressNotice - validation={ props.validation } - ipAddress={ ipAddress } - ipCountry={ ipCountry } - billingCountry={ billingAddress.country } - shouldValidateIp={ shouldValidateIp } - checkoutExtensionData={ checkoutExtensionData } - /> - ) : null } - </div> - </FormStep> - ); -}; - -export default withInstanceId( VatInput ); +/** + * External dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +import { withInstanceId } from '@wordpress/compose'; +import { extensionCartUpdate } from '@woocommerce/blocks-checkout'; +import { getSetting } from '@woocommerce/settings'; +import classnames from 'classnames'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useStoreCart } from '../../hooks'; +import { validateCountryVatNumberFormat } from '../../utils'; +import { IpAddressNotice } from '../IpAddressNotice'; +import FormStep from '../form-step'; + +const VatInput = ( props ) => { + const { setValidationErrors, clearValidationError, getValidationError } = + props.validation; + const { checkoutExtensionData } = props; + + const { billingAddress, extensions } = useStoreCart(); + const { + b2b_required, + eu_countries, + uk_ni_notice, + input_label, + input_description, + failure_handler, + } = wc_eu_vat_params; + + const { + title = __( 'VAT Number', 'woocommerce-eu-vat-number' ), + description = '', + showStepNumber = true, + } = props; + + const { + woocommerce_eu_vat_number_validate_ip, + ip_address: ipAddress, + ip_country: ipCountry, + } = getSetting( 'woocommerce-eu-vat-number_data' ); + const shouldValidateIp = woocommerce_eu_vat_number_validate_ip === 'yes'; + + const [ isActive, setIsActive ] = useState( false ); + const [ vat, setVat ] = useState( + extensions[ 'woocommerce-eu-vat-number' ]?.vat_number + ); + const [ previousVat, setPreviousVat ] = useState( + extensions[ 'woocommerce-eu-vat-number' ]?.vat_number + ); + const [ required ] = useState( + b2b_required === 'yes' && + eu_countries.indexOf( billingAddress.country ) !== -1 + ); + const [ available, setAvailable ] = useState( + eu_countries.indexOf( billingAddress.country ) !== -1 + ); + const [ showGBNotice, setShowGBNotice ] = useState( + billingAddress.country === 'GB' + ); + + const textInputId = 'billing_vat_number'; + const validationErrorId = 'billing_vat_number_error'; + const className = 'eu-vat-extra-css'; + + const error = getValidationError( validationErrorId ); + const hasError = error?.hidden === false && error?.message !== ''; + + /** + * This effect sets location_confirmation, it is required regardless of shouldValidateIp, or else the API will give + * us an error due to missing parameters. + */ + useEffect( () => { + checkoutExtensionData.setExtensionData( + 'woocommerce-eu-vat-number', + 'location_confirmation', + false + ); + }, [ checkoutExtensionData.setExtensionData ] ); + + /** + * On country change, check if the country is in the EU, if not then set the VAT Input to not available. + * Also update whether the GB Notice should show. + */ + useEffect( () => { + setAvailable( eu_countries.indexOf( billingAddress.country ) !== -1 ); + setShowGBNotice( billingAddress.country === 'GB' ); + }, [ eu_countries, billingAddress.country ] ); + + /** + * This effect handles setting the validation error immediately. + */ + useEffect( () => { + if ( ! required || ( typeof vat === 'string' && vat.length > 0 ) ) { + return; + } + // Instantly set the validation error when loading the component to ensure submissions without touching the + // field cause an error to show when submitting. It is hidden at first because we don't want to show the error + // until the user has touched the field. + setValidationErrors( { + [ validationErrorId ]: { + message: __( + 'VAT number is required.', + 'woocommerce-eu-vat-number' + ), + hidden: true, + }, + } ); + }, [ required, setValidationErrors, validationErrorId ] ); + + const verifyVat = () => { + if ( vat === previousVat ) { + return; + } + clearValidationError( validationErrorId ); + // If required is true,and vat is null, undefined, or an empty string. + if ( + required && + ( vat === null || + typeof vat === 'undefined' || + ( typeof vat === 'string' && vat.length === 0 ) ) + ) { + setValidationErrors( { + [ validationErrorId ]: { + message: __( + 'VAT number is required.', + 'woocommerce-eu-vat-number' + ), + hidden: false, + }, + } ); + } + extensionCartUpdate( { + namespace: 'woocommerce-eu-vat-number', + data: { + vat_number: vat, + }, + cartPropsToReceive: [ 'extensions' ], + } ).then( () => { + setPreviousVat( vat ); + // If we get here and VAT is empty and not required then remove the error message. This is because an empty + // VAT Number still causes an erorr when we try to update the server, but we need to update the server + // to tell it the VAT Number is empty... + if ( ! required && ! vat ) { + clearValidationError( validationErrorId ); + } + } ); + }; + + const init = () => { + if ( + typeof vat === 'string' && + vat.length > 0 && + ( ( ! validateCountryVatNumberFormat( + billingAddress.country, + vat + ) && + failure_handler === 'reject' ) || + ( ! extensions[ 'woocommerce-eu-vat-number' ]?.validation + ?.valid && + vat === + extensions[ 'woocommerce-eu-vat-number' ]?.vat_number && + failure_handler === 'reject' ) ) + ) { + setValidationErrors( { + [ validationErrorId ]: { + message: + extensions[ 'woocommerce-eu-vat-number' ]?.validation + .error, + hidden: false, + }, + } ); + } + }; + + /** + * This effect runs when extensions[ 'woocommerce-eu-vat-number' ]?.validation.error changes. We can set the error + * on the front-end based on this, or clear it if it's empty. + */ + useEffect( () => { + // If vat number is empty AND this is the first render, skip showing the error. The error will have hidden: true + // if it's the first render. Subsequent interactions will unhide the error if the state is invalid. + // If vat is not empty, then the state can be considered 'dirty' and we continue to show the error. + const validationError = getValidationError( validationErrorId ); + if ( ! vat && validationError?.hidden ) { + return; + } + + if ( ! required && ! vat ) { + clearValidationError( validationErrorId ); + return; + } + if ( extensions[ 'woocommerce-eu-vat-number' ]?.validation?.error ) { + setValidationErrors( { + [ validationErrorId ]: { + message: + extensions[ 'woocommerce-eu-vat-number' ]?.validation + .error, + hidden: false, + }, + } ); + return; + } + clearValidationError( validationErrorId ); + }, [ extensions[ 'woocommerce-eu-vat-number' ]?.validation.error ] ); + + /** + * This effect kicks off the init function when the component mounts for the first time. + */ + useEffect( init, [] ); + + const onChange = ( event ) => { + const { value: nextValue } = event.target; + clearValidationError( validationErrorId ); + if ( + typeof nextValue === 'string' && + nextValue.length === 0 && + required + ) { + setValidationErrors( { + [ validationErrorId ]: { + message: __( + 'VAT Number is required.', + 'woocommerce-eu-vat-number' + ), + hidden: false, + }, + } ); + } + setVat( nextValue ); + }; + + const HasError = () => { + if ( ! hasError ) return null; + return ( + <div className="wc-block-components-validation-error" role="alert"> + <p id={ validationErrorId }> + { getValidationError( validationErrorId )?.message } + </p> + </div> + ); + }; + + if ( ! available ) { + return <></>; + } + + return ( + <FormStep + id="shipping-fields" + className={ classnames( + 'wc-block-checkout__shipping-fields', + className + ) } + title={ title } + description={ description } + showStepNumber={ showStepNumber } + > + <div> + <div + className={ classnames( + 'wc-block-components-text-input', + className, + { + 'is-active': isActive || !! vat, + }, + { + 'has-error': hasError, + } + ) } + > + <input + type="text" + // eslint-disable-next-line camelcase + aria-label={ input_label } + id={ textInputId } + value={ vat || '' } + onChange={ ( event ) => { + onChange( event ); + } } + onFocus={ () => setIsActive( true ) } + onBlur={ () => { + setIsActive( false ); + verifyVat(); + } } + aria-invalid={ hasError === true } + disabled={ false } + required={ required } + /> + + <label htmlFor={ textInputId }> + { input_label } + { required === true ? null : ' (optional)' } + </label> + <HasError /> + <div className="wc-eu-vat-checkout-uk-notice"> + <div> + <span>{ input_description }</span> + </div> + <span>{ showGBNotice ? uk_ni_notice : null }</span> + </div> + </div> + { ( ! vat && b2b_required === 'no' ) || + ( ! vat && ! b2b_required ) ? ( + <IpAddressNotice + validation={ props.validation } + ipAddress={ ipAddress } + ipCountry={ ipCountry } + billingCountry={ billingAddress.country } + shouldValidateIp={ shouldValidateIp } + checkoutExtensionData={ checkoutExtensionData } + /> + ) : null } + </div> + </FormStep> + ); +}; + +export default withInstanceId( VatInput ); diff --git a/src/css/_mixins.scss b/src/css/_mixins.scss index af886d1..c5db5fa 100644 --- a/src/css/_mixins.scss +++ b/src/css/_mixins.scss @@ -1,24 +1,24 @@ -@mixin reset-typography() { - color: inherit; - font-family: inherit; - font-size: inherit; - font-style: inherit; - font-weight: inherit; - letter-spacing: inherit; - line-height: inherit; - text-decoration: inherit; - text-transform: inherit; -} - -// Converts a px unit to em. -@function em($size, $base: 16px) { - @return calc($size / $base) * 1em; -} -@mixin reset-box() { - border: 0; - border-radius: 0; - margin: 0; - padding: 0; - vertical-align: baseline; -} - +@mixin reset-typography() { + color: inherit; + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + text-decoration: inherit; + text-transform: inherit; +} + +// Converts a px unit to em. +@function em($size, $base: 16px) { + @return calc($size / $base) * 1em; +} +@mixin reset-box() { + border: 0; + border-radius: 0; + margin: 0; + padding: 0; + vertical-align: baseline; +} + diff --git a/src/css/_sizes.scss b/src/css/_sizes.scss index b1750fb..4f6b608 100644 --- a/src/css/_sizes.scss +++ b/src/css/_sizes.scss @@ -1,5 +1,5 @@ -$gap-largest : 48px; -$gap-large : 24px; -$gap : 16px; -$gap-small : 12px; +$gap-largest : 48px; +$gap-large : 24px; +$gap : 16px; +$gap-small : 12px; $gap-smallest: 4px; \ No newline at end of file diff --git a/src/frontend.js b/src/frontend.js index 39edad0..e4ec232 100644 --- a/src/frontend.js +++ b/src/frontend.js @@ -1,31 +1,31 @@ -/** - * External dependencies - */ -import { registerCheckoutBlock } from '@woocommerce/blocks-checkout'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import blockMetadata from '../block.json'; -import VatInput from './components/vat-input'; -import formStepAttributes from './components/form-step-block/attributes'; - -const options = { - metadata: { - ...blockMetadata, - attributes: { - ...blockMetadata.attributes, - ...formStepAttributes( { - defaultTitle: __( 'VAT Number', 'woocommerce-eu-vat-number' ), - defaultDescription: '', - defaultShowStepNumber: true, - } ), - }, - }, - component: ( props ) => { - return <VatInput isEditing={ false } { ...props } />; - }, -}; - -registerCheckoutBlock( options ); +/** + * External dependencies + */ +import { registerCheckoutBlock } from '@woocommerce/blocks-checkout'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import blockMetadata from '../block.json'; +import VatInput from './components/vat-input'; +import formStepAttributes from './components/form-step-block/attributes'; + +const options = { + metadata: { + ...blockMetadata, + attributes: { + ...blockMetadata.attributes, + ...formStepAttributes( { + defaultTitle: __( 'VAT Number', 'woocommerce-eu-vat-number' ), + defaultDescription: '', + defaultShowStepNumber: true, + } ), + }, + }, + component: ( props ) => { + return <VatInput isEditing={ false } { ...props } />; + }, +}; + +registerCheckoutBlock( options ); diff --git a/src/hooks.js b/src/hooks.js index 1631587..b1d0583 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -1,21 +1,21 @@ -/** - * External dependencies - */ -import { useSelect } from '@wordpress/data'; - -export const useStoreCart = () => { - const CART_STORE_KEY = 'wc/store/cart'; - - const results = useSelect( ( select ) => { - const store = select( CART_STORE_KEY ); - const cartData = store.getCartData(); - return { - cartItems: cartData.items, - billingAddress: cartData.billingAddress, - extensions: cartData.extensions, - needsShipping: cartData.needsShipping, - }; - } ); - - return results; -}; +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +export const useStoreCart = () => { + const CART_STORE_KEY = 'wc/store/cart'; + + const results = useSelect( ( select ) => { + const store = select( CART_STORE_KEY ); + const cartData = store.getCartData(); + return { + cartItems: cartData.items, + billingAddress: cartData.billingAddress, + extensions: cartData.extensions, + needsShipping: cartData.needsShipping, + }; + } ); + + return results; +}; diff --git a/src/index.js b/src/index.js index 89e56d8..ef398a2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,62 +1,62 @@ -/** - * External dependencies - */ -import { registerBlockType } from '@wordpress/blocks'; -import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; -import classnames from 'classnames'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import './index.scss'; -import block from '../block.json'; -import { FormStepBlock } from './components/form-step-block'; -import formStepAttributes from './components/form-step-block/attributes'; - -const Edit = ( props ) => { - const blockProps = useBlockProps(); - return ( - <div { ...blockProps }> - <FormStepBlock - attributes={ props.attributes } - setAttributes={ props.setAttributes } - className={ classnames( - 'wc-block-checkout__vat-input', - props.attributes?.className - ) } - > - <div className="wc-block-components-text-input"> - <input - type="text" - aria-label="VAT Number" - id="woocommerce-eu-vat-number" - /> - <label htmlFor="woocommerce-eu-vat-number"> - { __( 'VAT Number', 'woocommerce-eu-vat-number' ) } - </label> - </div> - </FormStepBlock> - </div> - ); -}; -const Save = () => { - return ( - <div { ...useBlockProps.save() }> - <InnerBlocks.Content /> - </div> - ); -}; - -registerBlockType( block, { - edit: Edit, - attributes: { - ...block.attributes, - ...formStepAttributes( { - defaultTitle: __( 'VAT Number', 'woocommerce-eu-vat-number' ), - defaultDescription: '', - defaultShowStepNumber: true, - } ), - }, - save: Save, -} ); +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; +import classnames from 'classnames'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './index.scss'; +import block from '../block.json'; +import { FormStepBlock } from './components/form-step-block'; +import formStepAttributes from './components/form-step-block/attributes'; + +const Edit = ( props ) => { + const blockProps = useBlockProps(); + return ( + <div { ...blockProps }> + <FormStepBlock + attributes={ props.attributes } + setAttributes={ props.setAttributes } + className={ classnames( + 'wc-block-checkout__vat-input', + props.attributes?.className + ) } + > + <div className="wc-block-components-text-input"> + <input + type="text" + aria-label="VAT Number" + id="woocommerce-eu-vat-number" + /> + <label htmlFor="woocommerce-eu-vat-number"> + { __( 'VAT Number', 'woocommerce-eu-vat-number' ) } + </label> + </div> + </FormStepBlock> + </div> + ); +}; +const Save = () => { + return ( + <div { ...useBlockProps.save() }> + <InnerBlocks.Content /> + </div> + ); +}; + +registerBlockType( block, { + edit: Edit, + attributes: { + ...block.attributes, + ...formStepAttributes( { + defaultTitle: __( 'VAT Number', 'woocommerce-eu-vat-number' ), + defaultDescription: '', + defaultShowStepNumber: true, + } ), + }, + save: Save, +} ); diff --git a/src/index.scss b/src/index.scss index 8037408..99980fd 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,15 +1,15 @@ -.wc-eu-vat-checkout-uk-notice { - padding-top: 10px; - white-space: normal; -} - -.eu-vat-extra-css > input { - text-transform: uppercase; -} - -.wc-eu-vat-checkout-ip-notice { - - .wc-eu-vat-checkout-ip-notice__error { - margin-top: 1em; - } -} +.wc-eu-vat-checkout-uk-notice { + padding-top: 10px; + white-space: normal; +} + +.eu-vat-extra-css > input { + text-transform: uppercase; +} + +.wc-eu-vat-checkout-ip-notice { + + .wc-eu-vat-checkout-ip-notice__error { + margin-top: 1em; + } +} diff --git a/src/utils.js b/src/utils.js index a5974d9..18c3efd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,98 +1,98 @@ -export const validateCountryVatNumberFormat = ( country, vatNumber ) => { - const regexExpressions = { - AT: 'U[A-Z\\d]{8}', - BE: '0\\d{9}', - BG: '\\d{9,10}', - CY: '\\d{8}[A-Z]', - CZ: '\\d{8,10}', - DE: '\\d{9}', - DK: '(\\d{2} ?){3}\\d{2}', - EE: '\\d{9}', - EL: '\\d{9}', - ES: '[A-Z]\\d{7}[A-Z]|\\d{8}[A-Z]|[A-Z]\\d{8}', - FI: '\\d{8}', - FR: '([A-Z]{2}|[A-Z0-9]{2})\\d{9}', - XI: '\\d{9}|\\d{12}|(GD|HA)\\d{3}', - HR: '\\d{11}', - HU: '\\d{8}', - IE: '[A-Z\\d]{8,10}', - IT: '\\d{11}', - LT: '(\\d{9}|\\d{12})', - LU: '\\d{8}', - LV: '\\d{11}', - MT: '\\d{8}', - NL: '\\d{9}B\\d{2}', - PL: '\\d{10}', - PT: '\\d{9}', - RO: '\\d{2,10}', - SE: '\\d{12}', - SI: '\\d{8}', - SK: '\\d{10}', - }; - - if ( regexExpressions[ country ] ) { - const regex = new RegExp( regexExpressions[ country ] ); - const match = regex.exec( vatNumber ); - return match !== null; - } - return false; -}; - -/** - * Given some block attributes, gets attributes from the dataset or uses defaults. - * - * @param {Object} blockAttributes Object containing block attributes. - * @param {Array} rawAttributes Dataset from DOM. - * @return {Array} Array of parsed attributes. - */ -export const getValidBlockAttributes = ( blockAttributes, rawAttributes ) => { - const attributes = []; - - Object.keys( blockAttributes ).forEach( ( key ) => { - if ( typeof rawAttributes[ key ] !== 'undefined' ) { - switch ( blockAttributes[ key ].type ) { - case 'boolean': - attributes[ key ] = - rawAttributes[ key ] !== 'false' && - rawAttributes[ key ] !== false; - break; - case 'number': - attributes[ key ] = parseInt( rawAttributes[ key ], 10 ); - break; - case 'array': - case 'object': - attributes[ key ] = JSON.parse( rawAttributes[ key ] ); - break; - default: - attributes[ key ] = rawAttributes[ key ]; - break; - } - } else { - attributes[ key ] = blockAttributes[ key ].default; - } - } ); - - return attributes; -}; - -/** - * HOC that filters given attributes by valid block attribute values, or uses defaults if undefined. - * - * @param {Object} blockAttributes Component being wrapped. - */ -export const withFilteredAttributes = - ( blockAttributes ) => ( OriginalComponent ) => { - return ( ownProps ) => { - const validBlockAttributes = getValidBlockAttributes( - blockAttributes, - ownProps - ); - - return ( - <OriginalComponent - { ...ownProps } - { ...validBlockAttributes } - /> - ); - }; - }; +export const validateCountryVatNumberFormat = ( country, vatNumber ) => { + const regexExpressions = { + AT: 'U[A-Z\\d]{8}', + BE: '0\\d{9}', + BG: '\\d{9,10}', + CY: '\\d{8}[A-Z]', + CZ: '\\d{8,10}', + DE: '\\d{9}', + DK: '(\\d{2} ?){3}\\d{2}', + EE: '\\d{9}', + EL: '\\d{9}', + ES: '[A-Z]\\d{7}[A-Z]|\\d{8}[A-Z]|[A-Z]\\d{8}', + FI: '\\d{8}', + FR: '([A-Z]{2}|[A-Z0-9]{2})\\d{9}', + XI: '\\d{9}|\\d{12}|(GD|HA)\\d{3}', + HR: '\\d{11}', + HU: '\\d{8}', + IE: '[A-Z\\d]{8,10}', + IT: '\\d{11}', + LT: '(\\d{9}|\\d{12})', + LU: '\\d{8}', + LV: '\\d{11}', + MT: '\\d{8}', + NL: '\\d{9}B\\d{2}', + PL: '\\d{10}', + PT: '\\d{9}', + RO: '\\d{2,10}', + SE: '\\d{12}', + SI: '\\d{8}', + SK: '\\d{10}', + }; + + if ( regexExpressions[ country ] ) { + const regex = new RegExp( regexExpressions[ country ] ); + const match = regex.exec( vatNumber ); + return match !== null; + } + return false; +}; + +/** + * Given some block attributes, gets attributes from the dataset or uses defaults. + * + * @param {Object} blockAttributes Object containing block attributes. + * @param {Array} rawAttributes Dataset from DOM. + * @return {Array} Array of parsed attributes. + */ +export const getValidBlockAttributes = ( blockAttributes, rawAttributes ) => { + const attributes = []; + + Object.keys( blockAttributes ).forEach( ( key ) => { + if ( typeof rawAttributes[ key ] !== 'undefined' ) { + switch ( blockAttributes[ key ].type ) { + case 'boolean': + attributes[ key ] = + rawAttributes[ key ] !== 'false' && + rawAttributes[ key ] !== false; + break; + case 'number': + attributes[ key ] = parseInt( rawAttributes[ key ], 10 ); + break; + case 'array': + case 'object': + attributes[ key ] = JSON.parse( rawAttributes[ key ] ); + break; + default: + attributes[ key ] = rawAttributes[ key ]; + break; + } + } else { + attributes[ key ] = blockAttributes[ key ].default; + } + } ); + + return attributes; +}; + +/** + * HOC that filters given attributes by valid block attribute values, or uses defaults if undefined. + * + * @param {Object} blockAttributes Component being wrapped. + */ +export const withFilteredAttributes = + ( blockAttributes ) => ( OriginalComponent ) => { + return ( ownProps ) => { + const validBlockAttributes = getValidBlockAttributes( + blockAttributes, + ownProps + ); + + return ( + <OriginalComponent + { ...ownProps } + { ...validBlockAttributes } + /> + ); + }; + }; diff --git a/templates/location-confirmation-field.php b/templates/location-confirmation-field.php index e1c8abd..679dbe8 100644 --- a/templates/location-confirmation-field.php +++ b/templates/location-confirmation-field.php @@ -1,19 +1,19 @@ -<?php -/** - * Location confirmation field template. - * - * @package woocommerce-eu-vat-number/templates - * @since 2.2.0 - */ - -?> -<p class="form-row location_confirmation terms"> - <label for="location_confirmation" class="checkbox"><input type="checkbox" class="input-checkbox" name="location_confirmation" <?php checked( $location_confirmation_is_checked, true ); ?> id="location_confirmation" /> <span> - <?php - $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); - /* translators: %s billing country */ - echo wp_kses_post( sprintf( __( 'I am established, have my permanent address, or usually reside within <strong>%s</strong>.', 'woocommerce-eu-vat-number' ), $countries[ $billing_country ] ) ); - ?> - </span> - </label> -</p> +<?php +/** + * Location confirmation field template. + * + * @package woocommerce-eu-vat-number/templates + * @since 2.2.0 + */ + +?> +<p class="form-row location_confirmation terms"> + <label for="location_confirmation" class="checkbox"><input type="checkbox" class="input-checkbox" name="location_confirmation" <?php checked( $location_confirmation_is_checked, true ); ?> id="location_confirmation" /> <span> + <?php + $billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country(); + /* translators: %s billing country */ + echo wp_kses_post( sprintf( __( 'I am established, have my permanent address, or usually reside within <strong>%s</strong>.', 'woocommerce-eu-vat-number' ), $countries[ $billing_country ] ) ); + ?> + </span> + </label> +</p> diff --git a/templates/my-account/my-vat-number.php b/templates/my-account/my-vat-number.php index 3030c7a..302aed7 100644 --- a/templates/my-account/my-vat-number.php +++ b/templates/my-account/my-vat-number.php @@ -1,29 +1,29 @@ -<?php -/** - * My Vat Number - * - * @package woocommerce-eu-vat-number/templates - * @version 2.2.0 - */ - -if ( ! defined( 'ABSPATH' ) ) { - exit; -} -?> - -<?php if ( ! empty( $messages ) ) { ?> - <ul class="woocommerce-<?php echo esc_attr( $messages['status'] ); ?>"> - <li><?php echo wp_kses_post( $messages['message'] ); ?></li> - </ul> -<?php } ?> -<form method="post"> - <p class="form-row form-row form-row-first"> - <input type="text" value="<?php echo esc_attr( $vat_number ); ?>" id="vat_number" name="vat_number" class="input-text" /> - </p> - <div class="clear"></div> - <p> - <input type="submit" value="<?php echo esc_attr( __( 'Save', 'woocommerce-eu-vat-number' ) ); ?>" class="button" /> - <?php wp_nonce_field( 'woocommerce-edit_vat_number' ); ?> - <input type="hidden" name="action" value="edit_vat_number" /> - </p> -</form> +<?php +/** + * My Vat Number + * + * @package woocommerce-eu-vat-number/templates + * @version 2.2.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} +?> + +<?php if ( ! empty( $messages ) ) { ?> + <ul class="woocommerce-<?php echo esc_attr( $messages['status'] ); ?>"> + <li><?php echo wp_kses_post( $messages['message'] ); ?></li> + </ul> +<?php } ?> +<form method="post"> + <p class="form-row form-row form-row-first"> + <input type="text" value="<?php echo esc_attr( $vat_number ); ?>" id="vat_number" name="vat_number" class="input-text" /> + </p> + <div class="clear"></div> + <p> + <input type="submit" value="<?php echo esc_attr( __( 'Save', 'woocommerce-eu-vat-number' ) ); ?>" class="button" /> + <?php wp_nonce_field( 'woocommerce-edit_vat_number' ); ?> + <input type="hidden" name="action" value="edit_vat_number" /> + </p> +</form> diff --git a/woocommerce-eu-vat-number.php b/woocommerce-eu-vat-number.php index 45b0872..2569842 100644 --- a/woocommerce-eu-vat-number.php +++ b/woocommerce-eu-vat-number.php @@ -1,349 +1,420 @@ -<?php -/** - * Plugin Name: WooCommerce EU VAT Number - * Plugin URI: https://woocommerce.com/products/eu-vat-number/ - * Description: The EU VAT Number extension lets you collect and validate EU VAT numbers during checkout to identify B2B transactions verses B2C. IP Addresses can also be validated to ensure they match the billing address. EU businesses with a valid VAT number can have their VAT removed prior to payment. - * Version: 2.8.3 - * Author: WooCommerce - * Author URI: https://woocommerce.com/ - * Text Domain: woocommerce-eu-vat-number - * Domain Path: /languages - * Requires at least: 5.6 - * Tested up to: 6.1 - * WC requires at least: 6.8 - * WC tested up to: 7.4 - * Requires PHP: 7.2 - * +<?php +/** + * Plugin Name: WooCommerce EU VAT Number + * Plugin URI: https://woocommerce.com/products/eu-vat-number/ + * Description: The EU VAT Number extension lets you collect and validate EU VAT numbers during checkout to identify B2B transactions verses B2C. IP Addresses can also be validated to ensure they match the billing address. EU businesses with a valid VAT number can have their VAT removed prior to payment. + * Version: 2.8.8 + * Author: WooCommerce + * Author URI: https://woocommerce.com/ + * Text Domain: woocommerce-eu-vat-number + * Domain Path: /languages + * Requires at least: 6.1 + * Tested up to: 6.3 + * WC requires at least: 7.7 + * WC tested up to: 7.9 + * Requires PHP: 7.3 + * * Copyright: © 2023 WooCommerce - * License: GNU General Public License v3.0 - * License URI: http://www.gnu.org/licenses/gpl-3.0.html - * - * @package woocommerce-eu-vat-number - * Woo: 18592:d2720c4b4bb8d6908e530355b7a2d734 - */ - -// phpcs:disable WordPress.Files.FileName - -define( 'WC_EU_VAT_VERSION', '2.8.3' ); // WRCS: DEFINED_VERSION. -define( 'WC_EU_VAT_FILE', __FILE__ ); -define( 'WC_EU_VAT_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) ); - -/** - * WC_EU_VAT_Number_Init class. - */ -class WC_EU_VAT_Number_Init { - - /** - * Min version of WooCommerce supported. - * - * @var string - */ - const WC_MIN_VERSION = '6.8'; - - /** - * Constructor. - */ - public function __construct() { - add_action( 'plugins_loaded', array( $this, 'init' ) ); - - add_action( 'plugins_loaded', array( $this, 'localization' ), 0 ); - - // Subscribe to automated translations. - add_filter( 'woocommerce_translations_updates_for_' . basename( __FILE__, '.php' ), '__return_true' ); - - register_activation_hook( __FILE__, array( $this, 'install' ) ); - } - - /** - * Checks that WooCommerce is loaded before doing anything else. - * - * @return bool True if supported - */ - private function check_dependencies() { - $dependencies = array( - 'wc_installed' => array( - 'callback' => array( $this, 'is_woocommerce_active' ), - 'notice_callback' => array( $this, 'woocommerce_inactive_notice' ), - ), - 'wc_minimum_version' => array( - 'callback' => array( $this, 'is_woocommerce_version_supported' ), - 'notice_callback' => array( $this, 'woocommerce_wrong_version_notice' ), - ), - 'soap_required' => array( - 'callback' => array( $this, 'is_soap_supported' ), - 'notice_callback' => array( $this, 'requires_soap_notice' ), - ), - ); - foreach ( $dependencies as $check ) { - if ( ! call_user_func( $check['callback'] ) ) { - add_action( 'admin_notices', $check['notice_callback'] ); - return false; - } - } - return true; - } - - /** - * Checks if the WooCommerce plugin is active. - * Note: Must be run after the "plugins_loaded" action fires. - * - * @since 1.0 - * @return bool - */ - public function is_woocommerce_active() { - return class_exists( 'woocommerce' ); - } - - /** - * Checks if the current WooCommerce version is supported. - * Note: Must be run after the "plugins_loaded" action fires. - * - * @since 1.0 - * @return bool - */ - public function is_woocommerce_version_supported() { - return version_compare( - get_option( 'woocommerce_db_version' ), - self::WC_MIN_VERSION, - '>=' - ); - } - - /** - * Checks if the WooCommerce Blocks is active. - * Note: Must be run after the "plugins_loaded" action fires. - * - * @return bool - */ - public function is_woocommerce_blocks_active() { - return class_exists( 'Automattic\WooCommerce\Blocks\Package' ); - } - - /** - * Checks if the current WooCommerce Blocks version is supported. - * Note: Must be run after the "plugins_loaded" action fires. - * - * @return bool - */ - public function is_woocommerce_blocks_version_supported() { - return version_compare( - \Automattic\WooCommerce\Blocks\Package::get_version(), - '7.3.0', - '>=' - ); - } - - /** - * Checks if the server supports SOAP. - * - * @since 2.3.7 - * @return bool - */ - public function is_soap_supported() { - return class_exists( 'SoapClient' ); - } - - /** - * WC inactive notice. - * - * @since 1.0.0 - */ - public function woocommerce_inactive_notice() { - if ( current_user_can( 'activate_plugins' ) ) { - /* translators: %1$s: Plugin page link start %2$s Link end */ - echo '<div class="error"><p><strong>' . wp_kses_post( __( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . sprintf( __( 'The WooCommerce plugin must be active for EU VAT Number to work. %1$sPlease install and activate WooCommerce%2$s.', 'woocommerce-eu-vat-number' ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' ) ) . '</p></div>'; - } - } - - /** - * Wrong version notice. - * - * @since 1.0.0 - */ - public function woocommerce_wrong_version_notice() { - if ( current_user_can( 'activate_plugins' ) ) { - /* translators: %1$s: Minimum version %2$s: Plugin page link start %3$s Link end */ - echo '<div class="error"><p><strong>' . wp_kses_post( __( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . sprintf( __( 'The WooCommerce plugin must be at least version %1$s for EU VAT Number to work. %2$sPlease upgrade WooCommerce%3$s.', 'woocommerce-eu-vat-number' ), self::WC_MIN_VERSION, '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' ) ) . '</p></div>'; - } - } - - /** - * No SOAP support notice. - * - * @since 2.3.7 - */ - public function requires_soap_notice() { - if ( current_user_can( 'activate_plugins' ) ) { - echo '<div class="error"><p><strong>' . esc_html__( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . esc_html__( 'Your server does not provide SOAP support which is required functionality for communicating with VIES. You will need to reach out to your web hosting provider to get information on how to enable this functionality on your server.', 'woocommerce-eu-vat-number' ) . '</p></div>'; - } - } - - /** - * Init the plugin once WP is loaded. - */ - public function init() { - if ( $this->check_dependencies() ) { - if ( version_compare( get_option( 'woocommerce_eu_vat_version', 0 ), WC_EU_VAT_VERSION, '<' ) ) { - add_action( 'init', array( $this, 'install' ) ); - } - - include_once __DIR__ . '/includes/wc-eu-vat-functions.php'; - include_once __DIR__ . '/includes/class-wc-eu-vat-privacy.php'; - - if ( ! class_exists( 'WC_EU_VAT_Number' ) ) { - include_once __DIR__ . '/includes/class-wc-eu-vat-number.php'; - include_once __DIR__ . '/includes/class-wc-eu-vat-my-account.php'; - } - - if ( is_admin() ) { - include_once __DIR__ . '/includes/class-wc-eu-vat-admin.php'; - include_once __DIR__ . '/includes/class-wc-eu-vat-reports.php'; - } - - add_action( 'before_woocommerce_init', array( $this, 'declare_hpos_compatibility' ) ); - add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'plugin_action_links' ) ); - add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); - add_filter( 'woocommerce_get_order_item_totals', 'wc_eu_vat_maybe_add_zero_tax_display', 10, 3 ); - add_action( 'init', array( $this, 'wc_eu_vat_number_block_init' ) ); - add_filter( - '__experimental_woocommerce_blocks_add_data_attributes_to_block', - function ( $allowed_blocks ) { - if ( $this->is_woocommerce_blocks_active() && $this->is_woocommerce_blocks_version_supported() ) { - $allowed_blocks[] = 'woocommerce/eu-vat-number'; - } - return $allowed_blocks; - }, - 10, - 1 - ); - } - } - - /** - * Load translations. - */ - public function localization() { - /** - * Filters plugin locale. - * - * @since 2.1.11 - */ - $locale = apply_filters( 'plugin_locale', get_locale(), 'woocommerce-eu-vat-number' ); - $dir = trailingslashit( WP_LANG_DIR ); - load_textdomain( 'woocommerce-eu-vat-number', $dir . 'woocommerce-eu-vat-number/woocommerce-eu-vat-number-' . $locale . '.mo' ); - load_plugin_textdomain( 'woocommerce-eu-vat-number', false, dirname( plugin_basename( WC_EU_VAT_FILE ) ) . '/languages' ); - } - - /** - * Installer - */ - public function install() { - update_option( 'woocommerce_eu_vat_version', WC_EU_VAT_VERSION ); - add_rewrite_endpoint( 'vat-number', EP_ROOT | EP_PAGES ); - flush_rewrite_rules(); - } - - /** - * Add custom action links on the plugin screen. - * - * @param mixed $actions Plugin Actions Links. - * @return array - */ - public function plugin_action_links( $actions ) { - $custom_actions = array( - 'settings' => sprintf( '<a href="%s">%s</a>', admin_url( 'admin.php?page=wc-settings&tab=tax' ), __( 'Settings', 'woocommerce-eu-vat-number' ) ), - ); - return array_merge( $custom_actions, $actions ); - } - - /** - * Show row meta on the plugin screen. - * - * @param mixed $links Plugin Row Meta. - * @param mixed $file Plugin Base file. - * @return array - */ - public function plugin_row_meta( $links, $file ) { - if ( 'woocommerce-eu-vat-number/woocommerce-eu-vat-number.php' === $file ) { - $row_meta = array( - /** - * Filters the plugin docs URL. - * - * @since 2.1.8 - */ - 'docs' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_docs_url', 'https://docs.woocommerce.com/document/eu-vat-number/' ) ) . '" title="' . esc_attr( __( 'View Plugin Documentation', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Docs', 'woocommerce-eu-vat-number' ) . '</a>', - /** - * Filters the plugin changelog URL. - * - * @since 2.1.8 - */ - 'changelog' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_changelog', 'https://woocommerce.com/changelogs/woocommerce-eu-vat-number/changelog.txt' ) ) . '" title="' . esc_attr( __( 'View Plugin Changelog', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Changelog', 'woocommerce-eu-vat-number' ) . '</a>', - /** - * Filters the plugin support URL. - * - * @since 2.1.8 - */ - 'support' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_support_url', 'https://woocommerce.com/my-account/create-a-ticket?select=18592' ) ) . '" title="' . esc_attr( __( 'Support', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Support', 'woocommerce-eu-vat-number' ) . '</a>', - ); - return array_merge( $links, $row_meta ); - } - return (array) $links; - } - - /** - * Registers block type and registers with WC Blocks Integration Interface. - */ - public function wc_eu_vat_number_block_init() { - if ( $this->is_woocommerce_blocks_active() && $this->is_woocommerce_blocks_version_supported() ) { - include_once __DIR__ . '/includes/class-wc-eu-vat-blocks.php'; - add_action( - 'woocommerce_blocks_checkout_block_registration', - function( $integration_registry ) { - $integration_registry->register( new WC_EU_VAT_Blocks_Integration() ); - } - ); - - if ( ! class_exists( 'WC_EU_VAT_Number' ) ) { - include_once __DIR__ . '/includes/class-wc-eu-vat-number.php'; - include_once __DIR__ . '/includes/class-wc-eu-vat-my-account.php'; - } - add_action( - 'wp_enqueue_scripts', - function() { - WC_EU_VAT_Number::localize_wc_eu_vat_params( 'wc-blocks-eu-vat-scripts-frontend' ); - } - ); - - register_block_type( - __DIR__ . '/block.json', - array( - 'attributes' => array( - 'title' => array( - 'type' => 'string', - 'default' => __( 'VAT Number', 'woocommerce-eu-vat-number' ), - ), - 'description' => array( - 'type' => 'string', - 'default' => '', - ), - 'showStepNumber' => array( - 'type' => 'boolean', - 'default' => true, - ), - ), - ) - ); - } - } - - /** - * Declares support for HPOS. - */ - public function declare_hpos_compatibility() { - if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { - \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); - } - } -} - -new WC_EU_VAT_Number_Init(); + * License: GNU General Public License v3.0 + * License URI: http://www.gnu.org/licenses/gpl-3.0.html + * + * @package woocommerce-eu-vat-number + * Woo: 18592:d2720c4b4bb8d6908e530355b7a2d734 + */ + +// phpcs:disable WordPress.Files.FileName + +define( 'WC_EU_VAT_VERSION', '2.8.8' ); // WRCS: DEFINED_VERSION. +define( 'WC_EU_VAT_FILE', __FILE__ ); +define( 'WC_EU_ABSPATH', __DIR__ . '/' ); +define( 'WC_EU_VAT_PLUGIN_URL', untrailingslashit( plugins_url( basename( plugin_dir_path( __FILE__ ) ), basename( __FILE__ ) ) ) ); + +/** + * WC_EU_VAT_Number_Init class. + */ +class WC_EU_VAT_Number_Init { + + /** + * Min version of WooCommerce supported. + * + * @var string + */ + const WC_MIN_VERSION = '7.7'; + + /** + * Constructor. + */ + public function __construct() { + add_action( 'plugins_loaded', array( $this, 'init' ) ); + add_action( 'woocommerce_blocks_loaded', array( $this, 'wc_eu_vat_number_block_init' ) ); + + add_action( 'plugins_loaded', array( $this, 'localization' ), 0 ); + + // Subscribe to automated translations. + add_filter( 'woocommerce_translations_updates_for_' . basename( __FILE__, '.php' ), '__return_true' ); + + register_activation_hook( __FILE__, array( $this, 'install' ) ); + } + + /** + * Checks that WooCommerce is loaded before doing anything else. + * + * @return bool True if supported + */ + private function check_dependencies() { + $dependencies = array( + 'wc_installed' => array( + 'callback' => array( $this, 'is_woocommerce_active' ), + 'notice_callback' => array( $this, 'woocommerce_inactive_notice' ), + ), + 'wc_minimum_version' => array( + 'callback' => array( $this, 'is_woocommerce_version_supported' ), + 'notice_callback' => array( $this, 'woocommerce_wrong_version_notice' ), + ), + 'soap_required' => array( + 'callback' => array( $this, 'is_soap_supported' ), + 'notice_callback' => array( $this, 'requires_soap_notice' ), + ), + 'wc_tax_enabled' => array( + 'callback' => array( $this, 'is_taxes_enabled' ), + 'notice_callback' => array( $this, 'woocommerce_tax_disabled_notice' ), + ), + ); + foreach ( $dependencies as $check ) { + if ( ! call_user_func( $check['callback'] ) ) { + add_action( 'admin_notices', $check['notice_callback'] ); + return false; + } + } + return true; + } + + /** + * Checks if the WooCommerce plugin is active. + * Note: Must be run after the "plugins_loaded" action fires. + * + * @since 1.0 + * @return bool + */ + public function is_woocommerce_active() { + return class_exists( 'woocommerce' ); + } + + /** + * Checks if the current WooCommerce version is supported. + * Note: Must be run after the "plugins_loaded" action fires. + * + * @since 1.0 + * @return bool + */ + public function is_woocommerce_version_supported() { + return version_compare( + get_option( 'woocommerce_db_version' ), + self::WC_MIN_VERSION, + '>=' + ); + } + + /** + * Checks if the WooCommerce Blocks is active. + * Note: Must be run after the "plugins_loaded" action fires. + * + * @return bool + */ + public function is_woocommerce_blocks_active() { + return class_exists( 'Automattic\WooCommerce\Blocks\Package' ); + } + + /** + * Checks if WooCommerce taxes are enabled. + * + * @since 2.8.8 + * + * @return bool + */ + public function is_taxes_enabled() { + return function_exists( 'wc_tax_enabled' ) && wc_tax_enabled(); + } + + /** + * Checks if the current WooCommerce Blocks version is supported. + * Note: Must be run after the "plugins_loaded" action fires. + * + * @return bool + */ + public function is_woocommerce_blocks_version_supported() { + return version_compare( + \Automattic\WooCommerce\Blocks\Package::get_version(), + '7.3.0', + '>=' + ); + } + + /** + * Checks if the server supports SOAP. + * + * @since 2.3.7 + * @return bool + */ + public function is_soap_supported() { + return class_exists( 'SoapClient' ); + } + + /** + * WC inactive notice. + * + * @since 1.0.0 + */ + public function woocommerce_inactive_notice() { + if ( current_user_can( 'activate_plugins' ) ) { + /* translators: %1$s: Plugin page link start %2$s Link end */ + echo '<div class="error"><p><strong>' . wp_kses_post( __( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . sprintf( __( 'The WooCommerce plugin must be active for EU VAT Number to work. %1$sPlease install and activate WooCommerce%2$s.', 'woocommerce-eu-vat-number' ), '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' ) ) . '</p></div>'; + } + } + + /** + * Wrong version notice. + * + * @since 1.0.0 + */ + public function woocommerce_wrong_version_notice() { + if ( current_user_can( 'activate_plugins' ) ) { + /* translators: %1$s: Minimum version %2$s: Plugin page link start %3$s Link end */ + echo '<div class="error"><p><strong>' . wp_kses_post( __( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . sprintf( __( 'The WooCommerce plugin must be at least version %1$s for EU VAT Number to work. %2$sPlease upgrade WooCommerce%3$s.', 'woocommerce-eu-vat-number' ), self::WC_MIN_VERSION, '<a href="' . esc_url( admin_url( 'plugins.php' ) ) . '">', '</a>' ) ) . '</p></div>'; + } + } + + /** + * No SOAP support notice. + * + * @since 2.3.7 + */ + public function requires_soap_notice() { + if ( current_user_can( 'activate_plugins' ) ) { + echo '<div class="error"><p><strong>' . esc_html__( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> ' . esc_html__( 'Your server does not provide SOAP support which is required functionality for communicating with VIES. You will need to reach out to your web hosting provider to get information on how to enable this functionality on your server.', 'woocommerce-eu-vat-number' ) . '</p></div>'; + } + } + + /** + * Taxes disabled notice. + * + * @since 2.8.8 + */ + public function woocommerce_tax_disabled_notice() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } + + /* + * Recheck if the notice is needed. + * + * This is needed on the settings page as the options are updated after the + * `init` hook fires. This means that the notice can be shown on the settings + * page immediately after taxes are enabled and save is clicked. + */ + if ( $this->is_taxes_enabled() ) { + return; + } + + /** + * Filters whether to show the tax disabled notice. + * + * This allows developers to prevent the taxes disabled notice from + * being shown. In most cases it is recommended the extension be + * deactivated if taxes are intentionally disabled. + * + * This hook is targeted at multisite set ups in which the extension is + * globally enabled but individual sites may or may not need taxes to + * be enabled. + * + * @since 2.8.8 + * + * @param bool $show_notice Whether to show the notice. + */ + $show_notice = apply_filters( 'woocommerce_eu_vat_show_tax_disabled_notice', true ); + + if ( ! $show_notice ) { + return; + } + + echo '<div class="error">'; + echo '<p><strong>' . esc_html__( 'WooCommerce EU VAT Number is inactive.', 'woocommerce-eu-vat-number' ) . '</strong> '; + + printf( + /* translators: %1$s: Settings link start %2$s: Link end */ + esc_html__( 'The EU VAT Number extension functionality requires taxes be enabled on your store. To do so, go to %1$sWooCommerce > Settings%2$s, check the Enable tax rates and calculations checkbox, and click the Save changes button.', 'woocommerce-eu-vat-number' ), + '<a href="' . esc_url( admin_url( 'admin.php?page=wc-settings' ) ) . '">', + '</a>' + ); + + echo '</div>'; + } + + /** + * Init the plugin once WP is loaded. + */ + public function init() { + if ( $this->check_dependencies() ) { + if ( version_compare( get_option( 'woocommerce_eu_vat_version', 0 ), WC_EU_VAT_VERSION, '<' ) ) { + add_action( 'init', array( $this, 'install' ) ); + } + + include_once __DIR__ . '/includes/wc-eu-vat-functions.php'; + include_once __DIR__ . '/includes/class-wc-eu-vat-privacy.php'; + + if ( ! class_exists( 'WC_EU_VAT_Number' ) ) { + include_once __DIR__ . '/includes/class-wc-eu-vat-number.php'; + include_once __DIR__ . '/includes/class-wc-eu-vat-my-account.php'; + } + + if ( is_admin() ) { + include_once __DIR__ . '/includes/class-wc-eu-vat-admin.php'; + include_once __DIR__ . '/includes/class-wc-eu-vat-reports.php'; + } + + add_action( 'before_woocommerce_init', array( $this, 'declare_hpos_compatibility' ) ); + add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array( $this, 'plugin_action_links' ) ); + add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); + add_filter( 'woocommerce_get_order_item_totals', 'wc_eu_vat_maybe_add_zero_tax_display', 10, 3 ); + add_filter( + '__experimental_woocommerce_blocks_add_data_attributes_to_block', + function ( $allowed_blocks ) { + if ( $this->is_woocommerce_blocks_active() && $this->is_woocommerce_blocks_version_supported() ) { + $allowed_blocks[] = 'woocommerce/eu-vat-number'; + } + return $allowed_blocks; + }, + 10, + 1 + ); + } + } + + /** + * Load translations. + */ + public function localization() { + /** + * Filters plugin locale. + * + * @since 2.1.11 + */ + $locale = apply_filters( 'plugin_locale', get_locale(), 'woocommerce-eu-vat-number' ); + $dir = trailingslashit( WP_LANG_DIR ); + load_textdomain( 'woocommerce-eu-vat-number', $dir . 'woocommerce-eu-vat-number/woocommerce-eu-vat-number-' . $locale . '.mo' ); + load_plugin_textdomain( 'woocommerce-eu-vat-number', false, dirname( plugin_basename( WC_EU_VAT_FILE ) ) . '/languages' ); + } + + /** + * Installer + */ + public function install() { + update_option( 'woocommerce_eu_vat_version', WC_EU_VAT_VERSION ); + add_rewrite_endpoint( 'vat-number', EP_ROOT | EP_PAGES ); + flush_rewrite_rules(); + } + + /** + * Add custom action links on the plugin screen. + * + * @param mixed $actions Plugin Actions Links. + * @return array + */ + public function plugin_action_links( $actions ) { + $custom_actions = array( + 'settings' => sprintf( '<a href="%s">%s</a>', admin_url( 'admin.php?page=wc-settings&tab=tax' ), __( 'Settings', 'woocommerce-eu-vat-number' ) ), + ); + return array_merge( $custom_actions, $actions ); + } + + /** + * Show row meta on the plugin screen. + * + * @param mixed $links Plugin Row Meta. + * @param mixed $file Plugin Base file. + * @return array + */ + public function plugin_row_meta( $links, $file ) { + if ( 'woocommerce-eu-vat-number/woocommerce-eu-vat-number.php' === $file ) { + $row_meta = array( + /** + * Filters the plugin docs URL. + * + * @since 2.1.8 + */ + 'docs' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_docs_url', 'https://docs.woocommerce.com/document/eu-vat-number/' ) ) . '" title="' . esc_attr( __( 'View Plugin Documentation', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Docs', 'woocommerce-eu-vat-number' ) . '</a>', + /** + * Filters the plugin changelog URL. + * + * @since 2.1.8 + */ + 'changelog' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_changelog', 'https://woocommerce.com/changelogs/woocommerce-eu-vat-number/changelog.txt' ) ) . '" title="' . esc_attr( __( 'View Plugin Changelog', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Changelog', 'woocommerce-eu-vat-number' ) . '</a>', + /** + * Filters the plugin support URL. + * + * @since 2.1.8 + */ + 'support' => '<a href="' . esc_url( apply_filters( 'wc_eu_vat_number_support_url', 'https://woocommerce.com/my-account/create-a-ticket?select=18592' ) ) . '" title="' . esc_attr( __( 'Support', 'woocommerce-eu-vat-number' ) ) . '">' . __( 'Support', 'woocommerce-eu-vat-number' ) . '</a>', + ); + return array_merge( $links, $row_meta ); + } + return (array) $links; + } + + /** + * Registers block type and registers with WC Blocks Integration Interface. + */ + public function wc_eu_vat_number_block_init() { + if ( $this->check_dependencies() && $this->is_woocommerce_blocks_active() && $this->is_woocommerce_blocks_version_supported() ) { + include_once __DIR__ . '/includes/class-wc-eu-vat-blocks.php'; + add_action( + 'woocommerce_blocks_checkout_block_registration', + function( $integration_registry ) { + $integration_registry->register( new WC_EU_VAT_Blocks_Integration() ); + } + ); + + if ( ! class_exists( 'WC_EU_VAT_Number' ) ) { + include_once __DIR__ . '/includes/class-wc-eu-vat-number.php'; + include_once __DIR__ . '/includes/class-wc-eu-vat-my-account.php'; + } + add_action( + 'wp_enqueue_scripts', + function() { + WC_EU_VAT_Number::localize_wc_eu_vat_params( 'wc-blocks-eu-vat-scripts-frontend' ); + } + ); + + register_block_type( + __DIR__ . '/block.json', + array( + 'attributes' => array( + 'title' => array( + 'type' => 'string', + 'default' => __( 'VAT Number', 'woocommerce-eu-vat-number' ), + ), + 'description' => array( + 'type' => 'string', + 'default' => '', + ), + 'showStepNumber' => array( + 'type' => 'boolean', + 'default' => true, + ), + ), + ) + ); + } + } + + /** + * Declares support for HPOS. + */ + public function declare_hpos_compatibility() { + if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { + \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); + } + } +} + +new WC_EU_VAT_Number_Init();