PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Controllers / VatCheckController.php

VatCheckController.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at includes/Controllers/VatCheckController.php

173 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The "check this VAT number" affordance on the invoice form.
4 *
5 * @package Easy_Invoice
6 * @subpackage Controllers
7 */
8
9 namespace EasyInvoice\Controllers;
10
11 use EasyInvoice\Services\ViesValidator;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Puts a VIES check next to the customer VAT field.
19 *
20 * On demand rather than on save: VIES is a network call to somebody else's
21 * service, and it is slow and occasionally down. Wiring it into saving would
22 * mean an invoice that will not save because a foreign government's register is
23 * having an afternoon. The merchant asks when they want to know.
24 */
25 class VatCheckController {
26
27 /** AJAX action. */
28 const ACTION = 'easy_invoice_check_vat';
29
30 /**
31 * Wire it up.
32 *
33 * @return void
34 */
35 public static function init(): void {
36 add_action( 'wp_ajax_' . self::ACTION, [ __CLASS__, 'handle' ] );
37 add_action( 'admin_footer', [ __CLASS__, 'printScript' ] );
38 }
39
40 /**
41 * Answer a check request.
42 *
43 * @return void
44 */
45 public static function handle(): void {
46 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
47 if ( ! wp_verify_nonce( $nonce, self::ACTION ) ) {
48 wp_send_json_error( [ 'message' => __( 'Security check failed.', 'easy-invoice' ) ], 403 );
49 }
50
51 // The check reveals whether a given business is VAT-registered, and it
52 // consumes a shared public service on the site's behalf. Both are
53 // reasons not to leave it open to any logged-in user.
54 if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
55 wp_send_json_error( [ 'message' => __( 'You do not have permission to do that.', 'easy-invoice' ) ], 403 );
56 }
57
58 $vat = isset( $_POST['vat'] ) ? sanitize_text_field( wp_unslash( $_POST['vat'] ) ) : '';
59 $country = isset( $_POST['country'] ) ? sanitize_text_field( wp_unslash( $_POST['country'] ) ) : '';
60
61 $result = ViesValidator::check( $vat, $country );
62
63 if ( is_wp_error( $result ) ) {
64 // Deliberately a success response carrying an "unknown" state. This
65 // is not an error in the request — it is the register declining to
66 // answer, and the difference matters at the other end.
67 wp_send_json_success( [
68 'state' => 'unknown',
69 'message' => $result->get_error_message(),
70 ] );
71 }
72
73 if ( empty( $result['valid'] ) ) {
74 wp_send_json_success( [
75 'state' => 'invalid',
76 'message' => __( 'The EU VAT register does not recognise this number. Check it with your customer before treating this as a reverse-charge supply.', 'easy-invoice' ),
77 ] );
78 }
79
80 $message = __( 'Registered in the EU VAT register.', 'easy-invoice' );
81 if ( '' !== $result['name'] ) {
82 $message = sprintf(
83 /* translators: %s: registered trader name. */
84 __( 'Registered: %s', 'easy-invoice' ),
85 $result['name']
86 );
87 }
88
89 wp_send_json_success( [
90 'state' => 'valid',
91 'message' => $message,
92 ] );
93 }
94
95 /**
96 * Add the control beside the VAT field.
97 *
98 * Injected from the footer rather than added to the field registration
99 * because that layer describes data, not behaviour, and every field type it
100 * knows about renders the same way. This attaches to whatever the form
101 * produced.
102 *
103 * @return void
104 */
105 public static function printScript(): void {
106 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
107 if ( ! $screen || false === strpos( (string) $screen->id, 'easy-invoice' ) ) {
108 return;
109 }
110
111 if ( ! easy_invoice_user_can( 'ei_view_invoices' ) ) {
112 return;
113 }
114
115 $nonce = wp_create_nonce( self::ACTION );
116 ?>
117 <script>
118 (function () {
119 var field = document.querySelector('[name="customer_vat_number"]');
120 if (!field || field.dataset.eiVatCheck) { return; }
121 field.dataset.eiVatCheck = '1';
122
123 var wrap = document.createElement('div');
124 wrap.style.cssText = 'margin-top:6px;font-size:12px;display:flex;align-items:center;gap:8px;flex-wrap:wrap';
125
126 var button = document.createElement('button');
127 button.type = 'button';
128 button.textContent = <?php echo wp_json_encode( __( 'Check with VIES', 'easy-invoice' ) ); ?>;
129 button.style.cssText = 'cursor:pointer;border:1px solid #d1d5db;background:#fff;border-radius:4px;padding:3px 9px;font-size:12px';
130
131 var out = document.createElement('span');
132 wrap.appendChild(button);
133 wrap.appendChild(out);
134 field.parentNode.appendChild(wrap);
135
136 var COLOURS = { valid: '#047857', invalid: '#b91c1c', unknown: '#92400e' };
137
138 button.addEventListener('click', function () {
139 var vat = (field.value || '').trim();
140 if (!vat) { return; }
141
142 var countryField = document.querySelector('[name="customer_country"]');
143 button.disabled = true;
144 out.style.color = '#6b7280';
145 out.textContent = <?php echo wp_json_encode( __( 'Checking…', 'easy-invoice' ) ); ?>;
146
147 var body = new FormData();
148 body.append('action', <?php echo wp_json_encode( self::ACTION ); ?>);
149 body.append('nonce', <?php echo wp_json_encode( $nonce ); ?>);
150 body.append('vat', vat);
151 body.append('country', countryField ? (countryField.value || '') : '');
152
153 fetch(<?php echo wp_json_encode( admin_url( 'admin-ajax.php' ) ); ?>, {
154 method: 'POST', body: body, credentials: 'same-origin'
155 })
156 .then(function (r) { return r.json(); })
157 .then(function (r) {
158 var d = (r && r.data) || {};
159 out.style.color = COLOURS[d.state] || '#6b7280';
160 out.textContent = d.message || <?php echo wp_json_encode( __( 'The check could not be completed.', 'easy-invoice' ) ); ?>;
161 })
162 .catch(function () {
163 out.style.color = COLOURS.unknown;
164 out.textContent = <?php echo wp_json_encode( __( 'The check could not be completed.', 'easy-invoice' ) ); ?>;
165 })
166 .finally(function () { button.disabled = false; });
167 });
168 })();
169 </script>
170 <?php
171 }
172 }
173