PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 All 162 releases
woocommerce-pos / includes / Services / Tax_Id_Detector.php

Tax_Id_Detector.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.17, at includes/Services/Tax_Id_Detector.php

304 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Tax ID Detector.
4 *
5 * Detects which third-party tax-ID plugin (if any) is active on the site and
6 * builds a per-type "write map" — for each Tax_Id_Types type, the meta key that
7 * WCPOS should write to. Order of precedence:
8 *
9 * 1. Detected active third-party plugin (recognised by basename + populated keys).
10 * 2. Populated-key scan over recent orders (when no plugin is recognised).
11 * 3. WCPOS sensible defaults (see Tax_Id_Settings::default_write_map()).
12 *
13 * The result is consumed by Tax_Id_Writer. Pure-logic helpers are exposed as
14 * statics so the heuristics are unit-testable without WordPress.
15 *
16 * @package WCPOS\WooCommercePOS
17 */
18
19 namespace WCPOS\WooCommercePOS\Services;
20
21 /**
22 * Tax_Id_Detector class.
23 */
24 class Tax_Id_Detector {
25 /**
26 * Recognised plugin definitions. Each entry maps a "plugin id" used in the
27 * detection result to:
28 *
29 * - basename: the plugin file basename (matches `is_plugin_active()`).
30 * - alt_basenames: alternative folders/files seen in the wild.
31 * - keys: per-type meta keys that this plugin writes.
32 *
33 * @var array<string,array{basename:string,alt_basenames?:array<int,string>,keys:array<string,string>}>
34 */
35 const PLUGINS = array(
36 'wc_eu_vat_number' => array(
37 'basename' => 'woocommerce-eu-vat-number/woocommerce-eu-vat-number.php',
38 'alt_basenames' => array(),
39 'keys' => array(
40 Tax_Id_Types::TYPE_EU_VAT => '_billing_vat_number',
41 Tax_Id_Types::TYPE_GB_VAT => '_billing_vat_number',
42 ),
43 ),
44 'aelia_eu_vat' => array(
45 'basename' => 'aelia-eu-vat-assistant/aelia-eu-vat-assistant.php',
46 'alt_basenames' => array(),
47 'keys' => array(
48 Tax_Id_Types::TYPE_EU_VAT => '_eu_vat_data',
49 Tax_Id_Types::TYPE_GB_VAT => '_eu_vat_data',
50 ),
51 ),
52 'wpfactory_eu_vat' => array(
53 'basename' => 'eu-vat-for-woocommerce/eu-vat-for-woocommerce.php',
54 'alt_basenames' => array(
55 'wpfactory-eu-vat-number/wpfactory-eu-vat-number.php',
56 ),
57 'keys' => array(
58 Tax_Id_Types::TYPE_EU_VAT => '_billing_eu_vat_number',
59 Tax_Id_Types::TYPE_GB_VAT => '_billing_eu_vat_number',
60 ),
61 ),
62 'germanized' => array(
63 'basename' => 'woocommerce-germanized/woocommerce-germanized.php',
64 'alt_basenames' => array(
65 'woocommerce-germanized-pro/woocommerce-germanized-pro.php',
66 ),
67 'keys' => array(
68 Tax_Id_Types::TYPE_EU_VAT => '_billing_vat_id',
69 ),
70 ),
71 'br_market' => array(
72 'basename' => 'woocommerce-extra-checkout-fields-for-brazil/woocommerce-extra-checkout-fields-for-brazil.php',
73 'alt_basenames' => array(
74 'brazilian-market-on-woocommerce/brazilian-market-on-woocommerce.php',
75 ),
76 'keys' => array(
77 Tax_Id_Types::TYPE_BR_CPF => '_billing_cpf',
78 Tax_Id_Types::TYPE_BR_CNPJ => '_billing_cnpj',
79 ),
80 ),
81 'es_nif' => array(
82 'basename' => 'wc-apg-nif-cif-spain/wc-apg-nif-cif-spain.php',
83 'alt_basenames' => array(
84 'woocommerce-nif-cif-spain/woocommerce-nif-cif-spain.php',
85 ),
86 'keys' => array(
87 Tax_Id_Types::TYPE_ES_NIF => '_billing_nif',
88 ),
89 ),
90 );
91
92 /**
93 * Whether `is_plugin_active()` is callable in this request context.
94 * Loads `wp-admin/includes/plugin.php` lazily if necessary.
95 *
96 * @return bool
97 */
98 public static function ensure_plugin_helpers_loaded(): bool {
99 if ( \function_exists( 'is_plugin_active' ) ) {
100 return true;
101 }
102
103 $file = ABSPATH . 'wp-admin/includes/plugin.php';
104 if ( is_readable( $file ) ) {
105 require_once $file;
106 }
107
108 return \function_exists( 'is_plugin_active' );
109 }
110
111 /**
112 * Detect active recognised plugins.
113 *
114 * @return array<int,string> Plugin ids (keys of self::PLUGINS) that are active.
115 */
116 public static function active_plugin_ids(): array {
117 if ( ! self::ensure_plugin_helpers_loaded() ) {
118 return array();
119 }
120
121 $active = array();
122 foreach ( self::PLUGINS as $plugin_id => $def ) {
123 $candidates = array_merge( array( $def['basename'] ), $def['alt_basenames'] );
124 foreach ( $candidates as $basename ) {
125 if ( \is_plugin_active( $basename ) ) {
126 $active[] = $plugin_id;
127 break;
128 }
129 }
130 }
131
132 return $active;
133 }
134
135 /**
136 * Build the per-type write map by combining detection signals with defaults.
137 *
138 * Precedence (later entries overwrite earlier):
139 * 1. WCPOS defaults
140 * 2. Inferred from populated-key scan (if any)
141 * 3. Active plugin claims
142 * 4. User overrides (passed in)
143 *
144 * @param array<string,string> $defaults Default per-type → meta-key map.
145 * @param array<string,string> $inferred Per-type → meta-key map inferred from order scan.
146 * @param array<int,string> $active_plugins Plugin ids that are active.
147 * @param array<string,string> $overrides User-supplied per-type overrides.
148 *
149 * @return array<string,string>
150 */
151 public static function compose_write_map(
152 array $defaults,
153 array $inferred,
154 array $active_plugins,
155 array $overrides
156 ): array {
157 $map = $defaults;
158
159 foreach ( $inferred as $type => $key ) {
160 if ( Tax_Id_Types::is_valid_type( $type ) && \is_string( $key ) && '' !== $key ) {
161 $map[ $type ] = $key;
162 }
163 }
164
165 foreach ( $active_plugins as $plugin_id ) {
166 $plugin = self::PLUGINS[ $plugin_id ] ?? null;
167 if ( null === $plugin ) {
168 continue;
169 }
170 foreach ( $plugin['keys'] as $type => $key ) {
171 if ( ! Tax_Id_Types::is_valid_type( $type ) ) {
172 continue;
173 }
174 $map[ $type ] = $key;
175 }
176 }
177
178 foreach ( $overrides as $type => $key ) {
179 if ( Tax_Id_Types::is_valid_type( $type ) && \is_string( $key ) && '' !== $key ) {
180 $map[ $type ] = $key;
181 }
182 }
183
184 return $map;
185 }
186
187 /**
188 * Scan recent orders for populated tax-ID-like meta keys and return the
189 * per-type → meta-key map this implies.
190 *
191 * Heuristic: for each candidate key, count the number of populated rows in
192 * the last `$limit` orders. Pick the most-populated key per type.
193 *
194 * @param int $limit Max number of recent orders to inspect.
195 *
196 * @return array<string,string>
197 */
198 public static function infer_from_recent_orders( int $limit = 200 ): array {
199 if ( $limit <= 0 ) {
200 return array();
201 }
202
203 // Candidate key → type mapping for the scan. Reuses Tax_Id_Reader's fallback chain
204 // for direct types; generic VAT keys all map to TYPE_EU_VAT here (the inference is
205 // intentionally coarse — a per-row country prefix lookup is overkill for this).
206 $candidates = array(
207 '_billing_vat_number' => Tax_Id_Types::TYPE_EU_VAT,
208 '_billing_eu_vat_number' => Tax_Id_Types::TYPE_EU_VAT,
209 '_vat_number' => Tax_Id_Types::TYPE_EU_VAT,
210 '_billing_vat' => Tax_Id_Types::TYPE_EU_VAT,
211 '_billing_vat_id' => Tax_Id_Types::TYPE_EU_VAT,
212 '_billing_cpf' => Tax_Id_Types::TYPE_BR_CPF,
213 '_billing_cnpj' => Tax_Id_Types::TYPE_BR_CNPJ,
214 '_billing_gstin' => Tax_Id_Types::TYPE_IN_GST,
215 '_billing_cf' => Tax_Id_Types::TYPE_IT_CF,
216 '_billing_codice_fiscale' => Tax_Id_Types::TYPE_IT_CF,
217 '_billing_piva' => Tax_Id_Types::TYPE_IT_PIVA,
218 '_billing_partita_iva' => Tax_Id_Types::TYPE_IT_PIVA,
219 '_billing_nif' => Tax_Id_Types::TYPE_ES_NIF,
220 '_billing_cuit' => Tax_Id_Types::TYPE_AR_CUIT,
221 );
222
223 // Best-effort SQL: tolerate environments where wc_get_orders() / HPOS aren't
224 // available. We use the wc_get_orders() API for cross-store compatibility.
225 if ( ! \function_exists( 'wc_get_orders' ) ) {
226 return array();
227 }
228
229 $orders = \wc_get_orders(
230 array(
231 'limit' => $limit,
232 'orderby' => 'date',
233 'order' => 'DESC',
234 'status' => 'any',
235 'return' => 'ids',
236 )
237 );
238 if ( ! \is_array( $orders ) || empty( $orders ) ) {
239 return array();
240 }
241
242 // Tally populated rows per candidate.
243 $counts = array_fill_keys( array_keys( $candidates ), 0 );
244 foreach ( $orders as $order_id ) {
245 if ( \is_object( $order_id ) && \method_exists( $order_id, 'get_id' ) ) {
246 $order_id = $order_id->get_id();
247 }
248 $meta = \get_post_meta( (int) $order_id );
249 if ( ! \is_array( $meta ) || empty( $meta ) ) {
250 continue;
251 }
252 foreach ( $candidates as $meta_key => $_type ) {
253 $value = isset( $meta[ $meta_key ][0] ) ? $meta[ $meta_key ][0] : null;
254 if ( '' === $value || array() === $value || null === $value ) {
255 continue;
256 }
257 ++$counts[ $meta_key ];
258 }
259 }
260
261 // Pick the top-counted key per type.
262 $best = array();
263 foreach ( $candidates as $meta_key => $type ) {
264 if ( $counts[ $meta_key ] <= 0 ) {
265 continue;
266 }
267 if ( ! isset( $best[ $type ] ) || $counts[ $meta_key ] > $counts[ $best[ $type ] ] ) {
268 $best[ $type ] = $meta_key;
269 }
270 }
271
272 $inferred = array();
273 foreach ( $best as $type => $meta_key ) {
274 $inferred[ $type ] = $meta_key;
275 }
276
277 return $inferred;
278 }
279
280 /**
281 * Build the full detection summary for a request. Cached per-request.
282 *
283 * @return array{plugins:array<int,string>,write_map:array<string,string>}
284 */
285 public function summary(): array {
286 static $cache = null;
287 if ( null !== $cache ) {
288 return $cache;
289 }
290
291 $active = self::active_plugin_ids();
292 $inferred = empty( $active ) ? self::infer_from_recent_orders() : array();
293 $overrides = Tax_Id_Settings::get_overrides();
294 $defaults = Tax_Id_Settings::default_write_map();
295
296 $cache = array(
297 'plugins' => $active,
298 'write_map' => self::compose_write_map( $defaults, $inferred, $active, $overrides ),
299 );
300
301 return $cache;
302 }
303 }
304