PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 1.10.19 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 All 164 releases
← All changes | includes/Services/Settings.php +311 -754 1.9.16 → 1.10.20 View file →
@@ -6,10 +6,20 @@
6 6 */
7 7
8 8 namespace WCPOS\WooCommercePOS\Services;
9 9
10 -use WC_Payment_Gateways;
11 10 use WP_Error;
11 +use WCPOS\WooCommercePOS\Interfaces\Settings_Section_Interface;
12 +use WCPOS\WooCommercePOS\Services\Settings\Access_Section;
13 +use WCPOS\WooCommercePOS\Services\Settings\Checkout_Section;
14 +use WCPOS\WooCommercePOS\Services\Settings\Cloud_Print_Section;
15 +use WCPOS\WooCommercePOS\Services\Settings\General_Section;
16 +use WCPOS\WooCommercePOS\Services\Settings\License_Section;
17 +use WCPOS\WooCommercePOS\Services\Settings\Section_Registry;
18 +use WCPOS\WooCommercePOS\Services\Settings\Tax_Ids_Section;
19 +use WCPOS\WooCommercePOS\Services\Settings\Tools_Section;
20 +use WCPOS\WooCommercePOS\Services\Settings\Payment_Gateways_Section;
21 +use WCPOS\WooCommercePOS\Services\Settings\Visibility_Section;
12 22 use const WCPOS\WooCommercePOS\VERSION;
13 23
14 24 /**
15 25 * Settings Service class.
@@ -22,116 +32,8 @@
22 32 */
23 33 protected static $db_prefix = 'woocommerce_pos_settings_';
24 34
25 35 /**
26 - * Default settings for all sections.
27 - *
28 - * @var array
29 - */
30 - protected static $default_settings = array(
31 - 'general' => array(
32 - 'pos_only_products' => false,
33 - 'decimal_qty' => false,
34 - 'force_ssl' => true,
35 - 'default_customer' => 0,
36 - 'default_customer_is_cashier' => false,
37 - 'barcode_field' => '_sku',
38 - 'generate_username' => true,
39 - 'restore_stock_on_delete' => true,
40 - 'storefront_receipt_enabled' => false,
41 - 'storefront_receipt_template' => '',
42 - 'tracking_consent' => 'undecided',
43 - 'store_name' => '',
44 - 'store_phone' => '',
45 - 'store_email' => '',
46 - 'policies_and_conditions' => '',
47 - 'store_tax_ids' => array(),
48 - ),
49 - 'tax_ids' => array(
50 - // Per-type meta-key write overrides. Empty by default: the composed
51 - // write_map (defaults + plugin detection + scan) is used.
52 - 'write_map' => array(),
53 - ),
54 - 'checkout' => array(
55 - 'receipt_default_mode' => 'fiscal',
56 - 'admin_emails' => array(
57 - 'enabled' => true,
58 - 'new_order' => true,
59 - 'cancelled_order' => true,
60 - 'failed_order' => true,
61 - ),
62 - 'customer_emails' => array(
63 - 'enabled' => true,
64 - 'customer_on_hold_order' => true,
65 - 'customer_processing_order' => true,
66 - 'customer_completed_order' => true,
67 - 'customer_refunded_order' => true,
68 - 'customer_failed_order' => true,
69 - ),
70 - 'cashier_emails' => array(
71 - 'enabled' => false,
72 - 'new_order' => true,
73 - ),
74 - // this is used in the POS, not in WP Admin (at the moment).
75 - 'dequeue_script_handles' => array(
76 - 'admin-bar',
77 - 'wc-add-to-cart',
78 - 'wc-stripe-upe-classic',
79 - ),
80 - 'dequeue_style_handles' => array(
81 - 'admin-bar',
82 - 'woocommerce-general',
83 - 'woocommerce-inline',
84 - 'woocommerce-layout',
85 - 'woocommerce-smallscreen',
86 - 'woocommerce-blocktheme',
87 - 'wp-block-library',
88 - ),
89 - 'disable_wp_head' => false,
90 - 'disable_wp_footer' => false,
91 - ),
92 - 'payment_gateways' => array(
93 - 'default_gateway' => 'pos_cash',
94 - 'gateways' => array(
95 - 'pos_cash' => array(
96 - 'order' => 0,
97 - 'enabled' => true,
98 - 'order_status' => 'wc-completed',
99 - ),
100 - 'pos_card' => array(
101 - 'order' => 1,
102 - 'enabled' => true,
103 - 'order_status' => 'wc-completed',
104 - ),
105 - ),
106 - ),
107 - 'tools' => array(
108 - 'use_jwt_as_param' => false,
109 - ),
110 - 'visibility' => array(
111 - 'products' => array(
112 - 'default' => array(
113 - 'pos_only' => array(
114 - 'ids' => array(),
115 - ),
116 - 'online_only' => array(
117 - 'ids' => array(),
118 - ),
119 - ),
120 - ),
121 - 'variations' => array(
122 - 'default' => array(
123 - 'pos_only' => array(
124 - 'ids' => array(),
125 - ),
126 - 'online_only' => array(
127 - 'ids' => array(),
128 - ),
129 - ),
130 - ),
131 - ),
132 - );
133 - /**
134 36 * The single instance of the class.
135 37 *
136 38 * @var null|Settings
137 39 */
@@ -137,48 +39,68 @@
137 39 */
138 40 private static $instance = null;
139 41
140 42 /**
141 - * Get capabilities grouped by type.
43 + * The Section Registry. Built lazily on first access so registrants can
44 + * hook `woocommerce_pos_register_settings_sections` during plugins_loaded.
142 45 *
143 - * WooCommerce 9.9 replaced promote_users with create_customers for
144 - * customer creation via the REST API. We show the correct capability
145 - * on the Access settings page based on the installed WC version.
46 + * @var null|Section_Registry
47 + */
48 + private $registry = null;
49 +
50 + /**
51 + * Get the Section Registry, building and populating it on first access.
146 52 *
147 - * @return array
53 + * @return Section_Registry
148 54 */
149 - private static function get_caps(): array {
150 - $customer_create_cap = version_compare( WC()->version, '9.9', '>=' )
151 - ? 'create_customers'
152 - : 'promote_users';
55 + public function sections(): Section_Registry {
56 + if ( null === $this->registry ) {
57 + // Assign before firing the action: a re-entrant settings read from
58 + // inside a registration callback gets the partially built registry
59 + // instead of recursing forever.
60 + $this->registry = new Section_Registry();
153 61
154 - return array(
155 - 'wcpos' => array(
156 - 'access_woocommerce_pos',
157 - 'manage_woocommerce_pos',
158 - ),
159 - 'wc' => array(
160 - $customer_create_cap,
161 - 'read_private_products',
162 - 'edit_product',
163 - 'edit_others_products',
164 - 'edit_published_products',
165 - 'read_private_shop_orders',
166 - 'publish_shop_orders',
167 - 'edit_shop_orders',
168 - 'edit_others_shop_orders',
169 - 'edit_users',
170 - 'list_users',
171 - 'manage_product_terms',
172 - 'read_private_shop_coupons',
173 - ),
174 - 'wp' => array(
175 - 'read',
176 - ),
177 - );
62 + // All core sections are registered here, before the action fires so
63 + // extensions can rely on core sections already being present.
64 + $this->registry->register( new General_Section() );
65 + $this->registry->register( new Checkout_Section() );
66 + $this->registry->register( new Tools_Section() );
67 + $this->registry->register( new Tax_Ids_Section() );
68 + $this->registry->register( new Visibility_Section() );
69 + $this->registry->register( new Payment_Gateways_Section() );
70 + $this->registry->register( new Access_Section() );
71 + $this->registry->register( new License_Section() );
72 + $this->registry->register( new Cloud_Print_Section() );
73 +
74 + /**
75 + * Fires when the Section Registry is built, letting Pro and
76 + * extensions register their own Settings Sections.
77 + *
78 + * Fires lazily on the FIRST settings read of the request. Hook
79 + * this action at plugin file load or early plugins_loaded —
80 + * callbacks added after the first read never run.
81 + *
82 + * @since 1.10.0
83 + *
84 + * @param Section_Registry $registry The Section Registry.
85 + *
86 + * @hook woocommerce_pos_register_settings_sections
87 + */
88 + do_action( 'woocommerce_pos_register_settings_sections', $this->registry );
89 + }
90 +
91 + return $this->registry;
178 92 }
179 93
180 94 /**
95 + * Drop the built registry so tests can exercise registration. Not for
96 + * production use.
97 + */
98 + public function reset_sections_for_testing(): void {
99 + $this->registry = null;
100 + }
101 +
102 + /**
181 103 * Constructor is private to prevent direct instantiation.
182 104 * Use woocommerce_pos_get_settings() instead.
183 105 * Or Settings::instance() if you must.
184 106 */
@@ -206,36 +128,31 @@
206 128 *
207 129 * @return null|array|mixed|WP_Error
208 130 */
209 131 public function get_settings( string $id, $key = null ) {
210 - $method_name = 'get_' . $id . '_settings';
132 + $section = $this->sections()->get( $id );
211 133
212 - if ( method_exists( $this, $method_name ) ) {
213 - $settings = $this->$method_name();
134 + if ( ! $section instanceof Settings_Section_Interface ) {
135 + return $this->unknown_section_error( $id );
136 + }
214 137
215 - // If key is not provided, return the entire settings.
216 - if ( ! \is_string( $key ) ) {
217 - return $settings;
218 - }
138 + $settings = $section->read();
219 139
220 - if ( ! isset( $settings[ $key ] ) ) {
221 - return new WP_Error(
222 - 'woocommerce_pos_settings_error',
223 - // translators: 1. %s: Settings group id, 2. %s: Settings key.
224 - \sprintf( __( 'Settings with id %1$s and key %2$s not found', 'woocommerce-pos' ), $id, $key ),
225 - array( 'status' => 400 )
226 - );
227 - }
140 + // If key is not provided, return the entire settings.
141 + if ( ! \is_string( $key ) ) {
142 + return $settings;
143 + }
228 144
229 - return $settings[ $key ];
145 + if ( ! isset( $settings[ $key ] ) ) {
146 + return new WP_Error(
147 + 'woocommerce_pos_settings_error',
148 + // translators: 1. %s: Settings group id, 2. %s: Settings key.
149 + \sprintf( __( 'Settings with id %1$s and key %2$s not found', 'woocommerce-pos' ), $id, $key ),
150 + array( 'status' => 400 )
151 + );
230 152 }
231 153
232 - return new WP_Error(
233 - 'woocommerce_pos_settings_error',
234 - // translators: %s: Settings group id, ie: 'general' or 'checkout'.
235 - \sprintf( __( 'Settings with id %s not found', 'woocommerce-pos' ), $id ),
236 - array( 'status' => 400 )
237 - );
154 + return $settings[ $key ];
238 155 }
239 156
240 157 /**
241 158 * Resolve the URL scheme for POS permalinks.
@@ -246,9 +163,9 @@
246 163 *
247 164 * @return null|string 'https' when force_ssl is enabled, null for the home scheme.
248 165 */
249 166 public function url_scheme(): ?string {
250 - return wp_validate_boolean( $this->get_settings( 'general', 'force_ssl' ) ) ? 'https' : null;
167 + return $this->force_ssl_enabled() ? 'https' : null;
251 168 }
252 169
253 170 /**
254 171 * Saves settings for a specific section.
@@ -258,269 +175,76 @@
258 175 *
259 176 * @return array|WP_Error Returns the updated settings array on success or WP_Error on failure.
260 177 */
261 178 public function save_settings( string $id, array $settings ) {
262 - $sanitize_method = 'sanitize_' . $id . '_settings';
263 - if ( method_exists( $this, $sanitize_method ) ) {
264 - $settings = $this->$sanitize_method( $settings );
265 - }
179 + $section = $this->sections()->get( $id );
266 180
267 - $settings = array_merge(
268 - $settings,
269 - array( 'date_modified_gmt' => current_time( 'mysql', true ) )
270 - );
271 -
272 - /**
273 - * Filters the settings before they are saved.
274 - *
275 - * Allows modification of the settings array for a specific section before it is saved to the database.
276 - *
277 - * @since 1.4.12
278 - *
279 - * @param array $settings The settings array about to be saved.
280 - * @param string $id The ID of the settings section being saved.
281 - */
282 - $settings = apply_filters( "woocommerce_pos_pre_save_{$id}_settings", $settings, $id );
283 -
284 - $option_name = static::$db_prefix . $id;
285 - $previous_value = get_option( $option_name, null );
286 - $success = update_option( $option_name, $settings, false );
287 -
288 - if ( ! $success ) {
289 - // update_option() returns false both when the value is unchanged (no DB write) and on
290 - // actual failure. Use the value read *before* the write attempt to avoid a post-write
291 - // race: a concurrent request could change the option between our write and a re-read.
292 - $is_noop = null !== $previous_value
293 - && maybe_serialize( $previous_value ) === maybe_serialize( $settings );
294 -
295 - if ( ! $is_noop ) {
296 - return new WP_Error(
297 - 'woocommerce_pos_settings_error',
298 - // translators: %s: Settings group id, ie: 'general' or 'checkout'.
299 - \sprintf( __( 'Can not save settings with id %s', 'woocommerce-pos' ), $id ),
300 - array( 'status' => 400 )
301 - );
302 - }
181 + if ( ! $section instanceof Settings_Section_Interface ) {
182 + return $this->unknown_section_error( $id );
303 183 }
304 184
305 - $saved_settings = $this->get_settings( $id );
306 -
307 - if ( $success ) {
308 - /*
309 - * Fires after settings for a specific section are successfully saved.
310 - *
311 - * Provides a way to execute additional logic after a specific settings section is updated.
312 - *
313 - * @since 1.4.12
314 - *
315 - * @param array $saved_settings The settings array that was just saved.
316 - * @param string $id The ID of the settings section that was saved.
317 - */
318 - do_action( "woocommerce_pos_saved_{$id}_settings", $saved_settings, $id );
319 - }
320 -
321 - return $saved_settings;
185 + return $section->write( $settings );
322 186 }
323 187
324 188 /**
325 - * Get general settings.
189 + * The error returned for a settings id with no registered Settings Section.
326 190 *
327 - * @return array
191 + * Registering a section through the
192 + * `woocommerce_pos_register_settings_sections` action is the only supported
193 + * way to add a settings group; there is no generic option fallback.
194 + *
195 + * @param string $id The settings section ID.
196 + *
197 + * @return WP_Error
328 198 */
329 - public function get_general_settings(): array {
330 - $default_settings = self::$default_settings['general'];
331 - $settings = get_option( self::$db_prefix . 'general', array() );
199 + private function unknown_section_error( string $id ): WP_Error {
200 + return new WP_Error(
201 + 'woocommerce_pos_settings_error',
202 + // translators: %s: Settings group id, ie: 'general' or 'checkout'.
203 + \sprintf( __( 'Settings with id %s not found', 'woocommerce-pos' ), $id ),
204 + array( 'status' => 400 )
205 + );
206 + }
332 207
333 - // Migrate tracking_consent from the legacy `tools` option if it was set there
334 - // before being moved to `general`. Only applies when the general option has no
335 - // value yet, so an explicit general-level choice always wins.
336 - if ( ! \array_key_exists( 'tracking_consent', $settings ) ) {
337 - $legacy_tools = get_option( self::$db_prefix . 'tools', array() );
338 - if ( \is_array( $legacy_tools ) && \array_key_exists( 'tracking_consent', $legacy_tools ) ) {
339 - $settings['tracking_consent'] = $legacy_tools['tracking_consent'];
340 - }
341 - }
208 + /*
209 + * Public get_{id}_settings() delegates are supported read API for Pro and
210 + * extensions. Keep them as non-deprecated facades until that public surface
211 + * is intentionally replaced.
212 + */
342 213
343 - // if the key does not exist in db settings, use the default settings.
344 - foreach ( $default_settings as $key => $value ) {
345 - if ( ! \array_key_exists( $key, $settings ) ) {
346 - $settings[ $key ] = $value;
347 - }
348 - }
349 - $settings['store_tax_ids'] = self::sanitize_store_tax_ids( $settings['store_tax_ids'] );
350 -
351 - // Expose resolved fallbacks so the React UI can render them as
352 - // placeholders for store_name / store_phone / store_email /
353 - // policies_and_conditions when the user has not entered a value.
354 - $settings['store_defaults'] = Store_Defaults::fallbacks();
355 -
356 - /*
357 - * Filters the general settings.
358 - *
359 - * @since 1.0.0
360 - *
361 - * @param array $settings
362 - *
363 - * @return array $settings
364 - *
365 - * @hook woocommerce_pos_general_settings
366 - */
367 - return apply_filters( 'woocommerce_pos_general_settings', $settings );
368 - }
369 -
370 214 /**
371 - * Sanitize general settings before persisting.
215 + * Get general settings.
372 216 *
373 - * @param array $settings General settings.
374 217 * @return array
375 218 */
376 - protected function sanitize_general_settings( array $settings ): array {
377 - if ( \array_key_exists( 'store_tax_ids', $settings ) ) {
378 - $settings['store_tax_ids'] = self::sanitize_store_tax_ids( $settings['store_tax_ids'] );
379 - }
219 + public function get_general_settings(): array {
220 + $section = $this->sections()->get( 'general' );
380 221
381 - foreach ( array( 'store_name', 'store_phone' ) as $key ) {
382 - if ( \array_key_exists( $key, $settings ) ) {
383 - $settings[ $key ] = \is_string( $settings[ $key ] )
384 - ? sanitize_text_field( $settings[ $key ] )
385 - : '';
386 - }
387 - }
388 -
389 - if ( \array_key_exists( 'store_email', $settings ) ) {
390 - $email = \is_string( $settings['store_email'] ) ? trim( $settings['store_email'] ) : '';
391 - $settings['store_email'] = ( '' !== $email && is_email( $email ) ) ? sanitize_email( $email ) : '';
392 - }
393 -
394 - if ( \array_key_exists( 'policies_and_conditions', $settings ) ) {
395 - $settings['policies_and_conditions'] = \is_string( $settings['policies_and_conditions'] )
396 - ? sanitize_textarea_field( $settings['policies_and_conditions'] )
397 - : '';
398 - }
399 -
400 - if ( \array_key_exists( 'storefront_receipt_enabled', $settings ) ) {
401 - $settings['storefront_receipt_enabled'] = wp_validate_boolean( $settings['storefront_receipt_enabled'] );
402 - }
403 -
404 - // Template IDs are numeric (database) or slug strings (virtual/gallery); an empty
405 - // string means "use the active receipt template". Store as a sanitized string and
406 - // let Storefront_Receipts pass it through the already-validated ?template= path.
407 - if ( \array_key_exists( 'storefront_receipt_template', $settings ) ) {
408 - $settings['storefront_receipt_template'] = \is_scalar( $settings['storefront_receipt_template'] )
409 - ? sanitize_text_field( (string) $settings['storefront_receipt_template'] )
410 - : '';
411 - }
412 -
413 - // store_defaults is a read-only computed field for the UI; never persist it.
414 - unset( $settings['store_defaults'] );
415 -
416 - return $settings;
222 + return $section ? $section->read() : array();
417 223 }
418 224
419 225 /**
420 226 * Sanitize the additional free-store tax IDs entered in General settings.
421 227 *
422 - * Drops malformed rows and keeps optional country/label fields only when
423 - * non-empty. Values are preserved verbatim apart from normal text-field
424 - * sanitization and surrounding whitespace.
228 + * Delegates to General_Section::sanitize_store_tax_ids(). Kept here as a
229 + * static façade because Store_Defaults::tax_ids() calls this method.
425 230 *
426 231 * @param mixed $tax_ids Raw tax IDs.
427 232 * @return array<int,array<string,string>>
428 233 */
429 234 public static function sanitize_store_tax_ids( $tax_ids ): array {
430 - if ( ! \is_array( $tax_ids ) ) {
431 - return array();
432 - }
433 -
434 - $sanitized = array();
435 - foreach ( $tax_ids as $tax_id ) {
436 - if ( ! \is_array( $tax_id ) ) {
437 - continue;
438 - }
439 -
440 - $type = isset( $tax_id['type'] ) && \is_string( $tax_id['type'] )
441 - ? sanitize_key( $tax_id['type'] )
442 - : '';
443 - $value = isset( $tax_id['value'] ) && \is_string( $tax_id['value'] )
444 - ? trim( sanitize_text_field( $tax_id['value'] ) )
445 - : '';
446 -
447 - if ( '' === $type || '' === $value ) {
448 - continue;
449 - }
450 -
451 - $entry = array(
452 - 'type' => $type,
453 - 'value' => $value,
454 - );
455 -
456 - $country = isset( $tax_id['country'] ) && \is_string( $tax_id['country'] )
457 - ? strtoupper( trim( sanitize_text_field( $tax_id['country'] ) ) )
458 - : '';
459 - if ( '' !== $country ) {
460 - $entry['country'] = $country;
461 - }
462 -
463 - $label = isset( $tax_id['label'] ) && \is_string( $tax_id['label'] )
464 - ? trim( sanitize_text_field( $tax_id['label'] ) )
465 - : '';
466 - if ( '' !== $label ) {
467 - $entry['label'] = $label;
468 - }
469 -
470 - $sanitized[] = $entry;
471 - }
472 -
473 - return $sanitized;
235 + return General_Section::sanitize_store_tax_ids( $tax_ids );
474 236 }
475 237
476 238 /**
477 239 * Get tax IDs settings.
478 240 *
479 - * Defaults are merged for any missing keys so the SPA always receives the
480 - * full subtree shape.
481 - *
482 241 * @return array
483 242 */
484 243 public function get_tax_ids_settings(): array {
485 - $default_settings = self::$default_settings['tax_ids'];
486 - $settings = get_option( self::$db_prefix . 'tax_ids', array() );
244 + $section = $this->sections()->get( 'tax_ids' );
487 245
488 - if ( ! \is_array( $settings ) ) {
489 - $settings = array();
490 - }
491 -
492 - if ( ! \array_key_exists( 'write_map', $settings ) ) {
493 - $legacy_general = get_option( self::$db_prefix . 'general', array() );
494 - $legacy_tax_ids = array();
495 -
496 - if (
497 - \is_array( $legacy_general )
498 - && isset( $legacy_general['tax_ids'] )
499 - && \is_array( $legacy_general['tax_ids'] )
500 - ) {
501 - $legacy_tax_ids = $legacy_general['tax_ids'];
502 - }
503 -
504 - if ( isset( $legacy_tax_ids['write_map'] ) && \is_array( $legacy_tax_ids['write_map'] ) ) {
505 - $settings['write_map'] = $legacy_tax_ids['write_map'];
506 - }
507 - }
508 -
509 - foreach ( $default_settings as $key => $value ) {
510 - if ( ! \array_key_exists( $key, $settings ) ) {
511 - $settings[ $key ] = $value;
512 - }
513 - }
514 -
515 - /*
516 - * Filters the tax IDs settings.
517 - *
518 - * @param {array} $settings
519 - * @returns {array} $settings
520 - * @hook woocommerce_pos_tax_ids_settings
521 - */
522 - return apply_filters( 'woocommerce_pos_tax_ids_settings', $settings );
246 + return $section ? $section->read() : array();
523 247 }
524 248
525 249 /**
526 250 * Get checkout settings.
@@ -527,36 +251,11 @@
527 251 *
528 252 * @return array
529 253 */
530 254 public function get_checkout_settings(): array {
531 - $default_settings = self::$default_settings['checkout'];
532 - $settings = get_option( self::$db_prefix . 'checkout', array() );
255 + $section = $this->sections()->get( 'checkout' );
533 256
534 - // if the key does not exist in db settings, use the default settings.
535 - foreach ( $default_settings as $key => $value ) {
536 - if ( ! \array_key_exists( $key, $settings ) ) {
537 - $settings[ $key ] = $value;
538 - }
539 - }
540 -
541 - // Migrate legacy boolean email settings to array format.
542 - foreach ( array( 'admin_emails', 'customer_emails' ) as $key ) {
543 - if ( isset( $settings[ $key ] ) && \is_bool( $settings[ $key ] ) ) {
544 - $defaults = $default_settings[ $key ];
545 - $defaults['enabled'] = $settings[ $key ];
546 - $settings[ $key ] = $defaults;
547 - }
548 - }
549 -
550 - /*
551 - * Filters the checkout settings.
552 - *
553 - * @param {array} $settings
554 - * @returns {array} $settings
555 - * @since 1.0.0
556 - * @hook woocommerce_pos_checkout_settings
557 - */
558 - return apply_filters( 'woocommerce_pos_checkout_settings', $settings );
257 + return $section ? $section->read() : array();
559 258 }
560 259
561 260 /**
562 261 * Get access settings with role capabilities.
@@ -563,44 +262,11 @@
563 262 *
564 263 * @return array
565 264 */
566 265 public function get_access_settings(): array {
567 - global $wp_roles;
568 - $role_caps = array();
569 - $caps = self::get_caps();
266 + $section = $this->sections()->get( 'access' );
570 267
571 - $roles = $wp_roles->roles;
572 - if ( $roles ) {
573 - foreach ( $roles as $slug => $role ) {
574 - $role_caps[ $slug ] = array(
575 - 'name' => $role['name'],
576 - 'capabilities' => array(
577 - 'wcpos' => array_intersect_key(
578 - array_merge( array_fill_keys( $caps['wcpos'], false ), $role['capabilities'] ),
579 - array_flip( $caps['wcpos'] )
580 - ),
581 - 'wc' => array_intersect_key(
582 - array_merge( array_fill_keys( $caps['wc'], false ), $role['capabilities'] ),
583 - array_flip( $caps['wc'] )
584 - ),
585 - 'wp' => array_intersect_key(
586 - array_merge( array_fill_keys( $caps['wp'], false ), $role['capabilities'] ),
587 - array_flip( $caps['wp'] )
588 - ),
589 - ),
590 - );
591 - }
592 - }
593 -
594 - /*
595 - * Filters the access settings.
596 - *
597 - * @param {array} $settings
598 - * @returns {array} $settings
599 - * @since 1.0.0
600 - * @hook woocommerce_pos_access_settings
601 - */
602 - return apply_filters( 'woocommerce_pos_access_settings', $role_caps );
268 + return $section ? $section->read() : array();
603 269 }
604 270
605 271 /**
606 272 * Get tools settings.
@@ -605,29 +271,15 @@
605 271 /**
606 272 * Get tools settings.
607 273 *
608 274 * @return array
275 + *
276 + * @hook woocommerce_pos_tools_settings
609 277 */
610 278 public function get_tools_settings(): array {
611 - $default_settings = self::$default_settings['tools'];
612 - $settings = get_option( self::$db_prefix . 'tools', array() );
279 + $section = $this->sections()->get( 'tools' );
613 280
614 - // if the key does not exist in db settings, use the default settings.
615 - foreach ( $default_settings as $key => $value ) {
616 - if ( ! \array_key_exists( $key, $settings ) ) {
617 - $settings[ $key ] = $value;
618 - }
619 - }
620 -
621 - /*
622 - * Filters the tools settings.
623 - *
624 - * @param {array} $settings
625 - * @returns {array} $settings
626 - * @since 1.3.6
627 - * @hook woocommerce_pos_general_settings
628 - */
629 - return apply_filters( 'woocommerce_pos_tools_settings', $settings );
281 + return $section ? $section->read() : array();
630 282 }
631 283
632 284 /**
633 285 * Get license settings.
@@ -633,18 +285,12 @@
633 285 * Get license settings.
634 286 *
635 287 * @return array
636 288 */
637 - public function get_license_settings() {
638 - /*
639 - * Filters the license settings.
640 - *
641 - * @param {array} $settings
642 - * @returns {array} $settings
643 - * @since 1.0.0
644 - * @hook woocommerce_pos_license_settings
645 - */
646 - return apply_filters( 'woocommerce_pos_license_settings', array() );
289 + public function get_license_settings(): array {
290 + $section = $this->sections()->get( 'license' );
291 +
292 + return $section ? $section->read() : array();
647 293 }
648 294
649 295 /**
650 296 * Get available barcode fields.
@@ -668,8 +314,10 @@
668 314 ORDER BY pm.meta_key
669 315 "
670 316 );
671 317
318 + $result = array_merge( $result, Barcode_Field::CORE_FIELDS );
319 +
672 320 if ( ! empty( $custom_field ) ) {
673 321 $result[] = $custom_field;
674 322 }
675 323
@@ -693,102 +341,21 @@
693 341 * Get payment gateways settings.
694 342 *
695 343 * @return array
696 344 */
697 - public function get_payment_gateways_settings() {
698 - // Note: I need to re-init the gateways here to pass the tests, but it seems to work fine in the app.
699 - WC_Payment_Gateways::instance()->init();
700 - $installed_gateways = WC_Payment_Gateways::instance()->payment_gateways();
701 - $raw_gw_option = get_option( self::$db_prefix . 'payment_gateways', array() );
702 - $gateways_settings = array_replace_recursive(
703 - self::$default_settings['payment_gateways'],
704 - $raw_gw_option
705 - );
345 + public function get_payment_gateways_settings(): array {
346 + $section = $this->sections()->get( 'payment_gateways' );
706 347
707 - // Migrate: if old global checkout order_status exists, apply to all gateways.
708 - $checkout_settings = get_option( self::$db_prefix . 'checkout', array() );
709 - if ( isset( $checkout_settings['order_status'] ) ) {
710 - $global_status = $checkout_settings['order_status'];
711 - if ( \is_string( $global_status ) && '' !== $global_status ) {
712 - foreach ( $gateways_settings['gateways'] as $gw_id => &$gw_data ) {
713 - // Check the raw DB value, not the merged value (which includes defaults).
714 - if ( ! isset( $raw_gw_option['gateways'][ $gw_id ]['order_status'] ) ) {
715 - $gw_data['order_status'] = $global_status;
716 - }
717 - }
718 - unset( $gw_data );
719 - }
720 - // Remove the old global setting.
721 - unset( $checkout_settings['order_status'] );
722 - update_option( self::$db_prefix . 'checkout', $checkout_settings );
723 - update_option( self::$db_prefix . 'payment_gateways', $gateways_settings );
724 - }
725 -
726 - // NOTE - gateways can be installed and uninstalled, so we need to assume the settings data is stale.
727 - $response = array(
728 - 'default_gateway' => $gateways_settings['default_gateway'],
729 - 'gateways' => array(),
730 - );
731 -
732 - // Gateways that represent deferred/unverified payment default to on-hold.
733 - $on_hold_gateways = array( 'bacs', 'cheque' );
734 -
735 - // loop through installed gateways and merge with saved settings.
736 - foreach ( $installed_gateways as $id => $gateway ) {
737 - // sanity check for gateway class.
738 - if ( ! is_a( $gateway, 'WC_Payment_Gateway' ) || 'pre_install_woocommerce_payments_promotion' === $id ) {
739 - continue;
740 - }
741 -
742 - $default_status = in_array( $id, $on_hold_gateways, true ) ? 'wc-on-hold' : 'wc-completed';
743 -
744 - $response['gateways'][ $id ] = array_replace_recursive(
745 - array(
746 - 'id' => $gateway->id,
747 - 'title' => $gateway->title,
748 - 'description' => $gateway->description,
749 - 'enabled' => false,
750 - 'order' => 999,
751 - 'order_status' => $default_status,
752 - ),
753 - $gateways_settings['gateways'][ $id ] ?? array()
754 - );
755 - }
756 -
757 - /*
758 - * Filters the payment gateways settings.
759 - *
760 - * @param {array} $settings
761 - * @returns {array} $settings
762 - * @since 1.0.0
763 - * @hook woocommerce_pos_payment_gateways_settings
764 - */
765 - return apply_filters( 'woocommerce_pos_payment_gateways_settings', $response );
348 + return $section ? $section->read() : array();
766 349 }
767 350
768 351 /**
769 352 * POS Visibility settings.
353 + *
354 + * @return array
770 355 */
771 - public function get_visibility_settings() {
772 - $default_settings = self::$default_settings['visibility'];
773 - $settings = get_option( self::$db_prefix . 'visibility', array() );
774 -
775 - // if the key does not exist in db settings, use the default settings.
776 - foreach ( $default_settings as $key => $value ) {
777 - if ( ! \array_key_exists( $key, $settings ) ) {
778 - $settings[ $key ] = $value;
779 - }
780 - }
781 -
782 - /*
783 - * Filters the visibility settings.
784 - *
785 - * @param {array} $settings
786 - * @returns {array} $settings
787 - * @since 1.0.0
788 - * @hook woocommerce_pos_visibility_settings
789 - */
790 - return apply_filters( 'woocommerce_pos_visibility_settings', $settings );
356 + public function get_visibility_settings(): array {
357 + return $this->visibility_section()->get_visibility_settings();
791 358 }
792 359
793 360 /**
794 361 * Update visibility settings.
@@ -797,69 +364,9 @@
797 364 *
798 365 * @return bool|WP_Error True on success, WP_Error on failure.
799 366 */
800 367 public function update_visibility_settings( array $args ) {
801 - // Validate and normalize arguments.
802 - if ( empty( $args['post_type'] ) || ! isset( $args['ids'] ) ) {
803 - return new WP_Error(
804 - 'woocommerce_pos_settings_error',
805 - /* translators: Error message shown when invalid arguments are provided. */
806 - __( 'Invalid arguments provided', 'woocommerce-pos' ),
807 - array( 'status' => 400 )
808 - );
809 - }
810 -
811 - // Define valid visibility options.
812 - $valid_options = array( 'pos_only', 'online_only', '' );
813 -
814 - // Check if visibility is set and valid.
815 - if ( ! isset( $args['visibility'] ) || ! \in_array( $args['visibility'], $valid_options, true ) ) {
816 - return new WP_Error(
817 - 'woocommerce_pos_settings_error',
818 - __( 'Invalid visibility option provided', 'woocommerce-pos' ),
819 - array( 'status' => 400 )
820 - );
821 - }
822 -
823 - $post_type = $args['post_type'];
824 - $scope = $args['scope'] ?? 'default';
825 - $visibility = $args['visibility'];
826 - $ids = \is_array( $args['ids'] ) ? $args['ids'] : array( $args['ids'] );
827 - $ids = array_filter( array_map( 'intval', $ids ) ); // Force to array of integers.
828 -
829 - // Get the current visibility settings.
830 - $current_settings = $this->get_visibility_settings();
831 -
832 - // Define the opposite visibility type.
833 - $opposite_visibility = ( 'pos_only' === $visibility ) ? 'online_only' : 'pos_only';
834 -
835 - // Add or remove IDs based on the visibility type.
836 - foreach ( $ids as $id ) {
837 - if ( '' === $visibility ) {
838 - // Remove from both pos_only and online_only.
839 - $current_settings[ $post_type ][ $scope ]['pos_only']['ids'] = $this->remove_id_from_visibility(
840 - $current_settings[ $post_type ][ $scope ]['pos_only']['ids'],
841 - $id
842 - );
843 - $current_settings[ $post_type ][ $scope ]['online_only']['ids'] = $this->remove_id_from_visibility(
844 - $current_settings[ $post_type ][ $scope ]['online_only']['ids'],
845 - $id
846 - );
847 - } else {
848 - // Add to the specified visibility type.
849 - $current_settings[ $post_type ][ $scope ][ $visibility ]['ids'] = $this->add_id_to_visibility(
850 - $current_settings[ $post_type ][ $scope ][ $visibility ]['ids'],
851 - $id
852 - );
853 - // Remove from the opposite visibility type.
854 - $current_settings[ $post_type ][ $scope ][ $opposite_visibility ]['ids'] = $this->remove_id_from_visibility(
855 - $current_settings[ $post_type ][ $scope ][ $opposite_visibility ]['ids'],
856 - $id
857 - );
858 - }
859 - }
860 -
861 - return $this->save_settings( 'visibility', $current_settings );
368 + return $this->visibility_section()->update_visibility_settings( $args );
862 369 }
863 370
864 371 /**
865 372 * Get product visibility settings.
@@ -868,19 +375,9 @@
868 375 *
869 376 * @return array $settings The product visibility settings, eg: { pos_only: { ids: [1, 2, 3] }, online_only: { ids: [4, 5, 6] }
870 377 */
871 378 public function get_product_visibility_settings( $scope = 'default' ) {
872 - $settings = $this->get_visibility_settings();
873 -
874 - /*
875 - * Filters the product visibility settings.
876 - *
877 - * @param {array} $settings
878 - * @returns {array} $settings
879 - * @since 1.0.0
880 - * @hook woocommerce_pos_product_visibility_settings
881 - */
882 - return apply_filters( 'woocommerce_pos_product_visibility_settings', $settings['products'][ $scope ], $scope );
379 + return $this->visibility_section()->get_product_visibility_settings( $scope );
883 380 }
884 381
885 382 /**
886 383 * Get product visibility settings.
@@ -889,19 +386,9 @@
889 386 *
890 387 * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] }
891 388 */
892 389 public function get_pos_only_product_visibility_settings( $scope = 'default' ) {
893 - $settings = $this->get_product_visibility_settings( $scope );
894 -
895 - /*
896 - * Filters the product visibility settings.
897 - *
898 - * @param {array} $settings
899 - * @returns {array} $settings
900 - * @since 1.0.0
901 - * @hook woocommerce_pos_product_visibility_settings
902 - */
903 - return apply_filters( 'woocommerce_pos_pos_only_product_visibility_settings', $settings['pos_only'], $scope );
390 + return $this->visibility_section()->get_pos_only_product_visibility_settings( $scope );
904 391 }
905 392
906 393 /**
907 394 * Get product visibility settings.
@@ -910,19 +397,9 @@
910 397 *
911 398 * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] }
912 399 */
913 400 public function get_online_only_product_visibility_settings( $scope = 'default' ) {
914 - $settings = $this->get_product_visibility_settings( $scope );
915 -
916 - /*
917 - * Filters the product visibility settings.
918 - *
919 - * @param {array} $settings
920 - * @returns {array} $settings
921 - * @since 1.0.0
922 - * @hook woocommerce_pos_product_visibility_settings
923 - */
924 - return apply_filters( 'woocommerce_pos_online_only_product_visibility_settings', $settings['online_only'], $scope );
401 + return $this->visibility_section()->get_online_only_product_visibility_settings( $scope );
925 402 }
926 403
927 404 /**
928 405 * Get product visibility settings.
@@ -931,19 +408,9 @@
931 408 *
932 409 * @return array $settings The product visibility settings, eg: { pos_only: { ids: [1, 2, 3] }, online_only: { ids: [4, 5, 6] }
933 410 */
934 411 public function get_variations_visibility_settings( $scope = 'default' ) {
935 - $settings = $this->get_visibility_settings();
936 -
937 - /*
938 - * Filters the product visibility settings.
939 - *
940 - * @param {array} $settings
941 - * @returns {array} $settings
942 - * @since 1.0.0
943 - * @hook woocommerce_pos_product_visibility_settings
944 - */
945 - return apply_filters( 'woocommerce_pos_variations_visibility_settings', $settings['variations'][ $scope ], $scope );
412 + return $this->visibility_section()->get_variations_visibility_settings( $scope );
946 413 }
947 414
948 415 /**
949 416 * Get product visibility settings.
@@ -952,19 +419,9 @@
952 419 *
953 420 * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] }
954 421 */
955 422 public function get_pos_only_variations_visibility_settings( $scope = 'default' ) {
956 - $settings = $this->get_variations_visibility_settings( $scope );
957 -
958 - /*
959 - * Filters the product visibility settings.
960 - *
961 - * @param {array} $settings
962 - * @returns {array} $settings
963 - * @since 1.0.0
964 - * @hook woocommerce_pos_product_visibility_settings
965 - */
966 - return apply_filters( 'woocommerce_pos_pos_only_variations_visibility_settings', $settings['pos_only'], $scope );
423 + return $this->visibility_section()->get_pos_only_variations_visibility_settings( $scope );
967 424 }
968 425
969 426 /**
970 427 * Get product visibility settings.
@@ -973,19 +430,9 @@
973 430 *
974 431 * @return array $settings The product visibility settings, eg: { ids: [1, 2, 3] }
975 432 */
976 433 public function get_online_only_variations_visibility_settings( $scope = 'default' ) {
977 - $settings = $this->get_variations_visibility_settings( $scope );
978 -
979 - /*
980 - * Filters the product visibility settings.
981 - *
982 - * @param {array} $settings
983 - * @returns {array} $settings
984 - * @since 1.0.0
985 - * @hook woocommerce_pos_product_visibility_settings
986 - */
987 - return apply_filters( 'woocommerce_pos_online_only_variations_visibility_settings', $settings['online_only'], $scope );
434 + return $this->visibility_section()->get_online_only_variations_visibility_settings( $scope );
988 435 }
989 436
990 437 /**
991 438 * Check if a product is POS only.
@@ -994,13 +441,9 @@
994 441 *
995 442 * @return bool
996 443 */
997 444 public function is_product_pos_only( $product_id ) {
998 - $product_id = (int) $product_id;
999 - $settings = $this->get_pos_only_product_visibility_settings();
1000 - $pos_only_ids = array_map( 'intval', (array) $settings['ids'] );
1001 -
1002 - return \in_array( $product_id, $pos_only_ids, true );
445 + return $this->visibility_section()->is_product_pos_only( $product_id );
1003 446 }
1004 447
1005 448 /**
1006 449 * Check if a product is Online only.
@@ -1009,13 +452,9 @@
1009 452 *
1010 453 * @return bool
1011 454 */
1012 455 public function is_product_online_only( $product_id ) {
1013 - $product_id = (int) $product_id;
1014 - $settings = $this->get_online_only_product_visibility_settings();
1015 - $online_only_ids = array_map( 'intval', (array) $settings['ids'] );
1016 -
1017 - return \in_array( $product_id, $online_only_ids, true );
456 + return $this->visibility_section()->is_product_online_only( $product_id );
1018 457 }
1019 458
1020 459 /**
1021 460 * Check if a variation is POS only.
@@ -1024,13 +463,9 @@
1024 463 *
1025 464 * @return bool
1026 465 */
1027 466 public function is_variation_pos_only( $variation_id ) {
1028 - $variation_id = (int) $variation_id;
1029 - $settings = $this->get_pos_only_variations_visibility_settings();
1030 - $pos_only_ids = array_map( 'intval', (array) $settings['ids'] );
1031 -
1032 - return \in_array( $variation_id, $pos_only_ids, true );
467 + return $this->visibility_section()->is_variation_pos_only( $variation_id );
1033 468 }
1034 469
1035 470 /**
1036 471 * Check if a variation is Online only.
@@ -1039,16 +474,20 @@
1039 474 *
1040 475 * @return bool
1041 476 */
1042 477 public function is_variation_online_only( $variation_id ) {
1043 - $variation_id = (int) $variation_id;
1044 - $settings = $this->get_online_only_variations_visibility_settings();
1045 - $online_only_ids = array_map( 'intval', (array) $settings['ids'] );
478 + return $this->visibility_section()->is_variation_online_only( $variation_id );
479 + }
1046 480
1047 - return \in_array( $variation_id, $online_only_ids, true );
481 + /**
482 + * Visibility behavior bound to the registered section's storage surface.
483 + */
484 + private function visibility_section(): Visibility_Section {
485 + $section = $this->sections()->get( 'visibility' );
486 +
487 + return $section instanceof Visibility_Section ? $section : new Visibility_Section( $section );
1048 488 }
1049 489
1050 -
1051 490 /**
1052 491 * Delete settings in WP options table.
1053 492 *
1054 493 * @param string $id The settings section ID.
@@ -1063,25 +502,8 @@
1063 502 return delete_option( self::$db_prefix . $id );
1064 503 }
1065 504
1066 505 /**
1067 - * Delete all settings in WP options table.
1068 - *
1069 - * @return bool|WP_Error
1070 - */
1071 - public static function delete_all_settings() {
1072 - if ( ! is_super_admin() && ! current_user_can( 'manage_woocommerce_pos' ) ) {
1073 - return new WP_Error( 'unauthorized', 'You do not have permission to delete this option.' );
1074 - }
1075 -
1076 - foreach ( self::$default_settings as $id => $settings ) {
1077 - delete_option( self::$db_prefix . $id );
1078 - }
1079 -
1080 - return true;
1081 - }
1082 -
1083 - /**
1084 506 * Get the database version.
1085 507 *
1086 508 * @return string
1087 509 */
@@ -1097,36 +519,171 @@
1097 519 update_option( 'woocommerce_pos_db_version', VERSION );
1098 520 }
1099 521
1100 522 /**
1101 - * Add an ID to a visibility type if it doesn't already exist.
523 + * Read one key from a section's filtered view, falling back to the
524 + * section default. Never returns WP_Error — typed accessors are the safe
525 + * read surface for PHP callers.
1102 526 *
1103 - * @param array $ids The current array of IDs.
1104 - * @param int $id The ID to add.
527 + * @param string $id Section id.
528 + * @param string $key Setting key.
1105 529 *
1106 - * @return array The updated array of IDs.
530 + * @return mixed
1107 531 */
1108 - private function add_id_to_visibility( array $ids, int $id ): array {
1109 - if ( ! \in_array( $id, $ids, true ) ) {
1110 - $ids[] = $id;
532 + private function section_value( string $id, string $key ) {
533 + $settings = $this->get_settings( $id );
534 + if ( \is_array( $settings ) && \array_key_exists( $key, $settings ) ) {
535 + return $settings[ $key ];
1111 536 }
1112 537
1113 - return $ids;
538 + $section = $this->sections()->get( $id );
539 + if ( $section instanceof Settings_Section_Interface ) {
540 + $defaults = $section->defaults();
541 +
542 + return $defaults[ $key ] ?? null;
543 + }
544 +
545 + return null;
1114 546 }
1115 547
1116 548 /**
1117 - * Remove an ID from a visibility type if it exists.
549 + * Whether the POS-only products feature is enabled.
550 + */
551 + public function pos_only_products_enabled(): bool {
552 + return (bool) $this->section_value( 'general', 'pos_only_products' );
553 + }
554 +
555 + /**
556 + * Whether decimal stock/cart quantities are enabled.
557 + */
558 + public function decimal_qty_enabled(): bool {
559 + return (bool) $this->section_value( 'general', 'decimal_qty' );
560 + }
561 +
562 + /**
563 + * Whether the POS frontend forces HTTPS.
564 + */
565 + public function force_ssl_enabled(): bool {
566 + return wp_validate_boolean( $this->section_value( 'general', 'force_ssl' ) );
567 + }
568 +
569 + /**
570 + * The product meta key used as the barcode field.
571 + */
572 + public function barcode_field(): string {
573 + return (string) $this->section_value( 'general', 'barcode_field' );
574 + }
575 +
576 + /**
577 + * The default customer id for new POS orders.
578 + */
579 + public function default_customer_id(): int {
580 + return (int) $this->section_value( 'general', 'default_customer' );
581 + }
582 +
583 + /**
584 + * Whether the logged-in cashier is the default customer.
585 + */
586 + public function default_customer_is_cashier(): bool {
587 + return (bool) $this->section_value( 'general', 'default_customer_is_cashier' );
588 + }
589 +
590 + /**
591 + * Whether usernames are auto-generated for new customers.
592 + */
593 + public function generate_username_enabled(): bool {
594 + return (bool) $this->section_value( 'general', 'generate_username' );
595 + }
596 +
597 + /**
598 + * Whether stock is restored when a POS order is deleted.
599 + */
600 + public function restore_stock_on_delete_enabled(): bool {
601 + return (bool) $this->section_value( 'general', 'restore_stock_on_delete' );
602 + }
603 +
604 + /**
605 + * The analytics tracking consent state: allowed | denied | undecided.
606 + */
607 + public function tracking_consent(): string {
608 + return (string) $this->section_value( 'general', 'tracking_consent' );
609 + }
610 +
611 + /**
612 + * The PERSISTED tracking consent, ignoring the settings read filter.
1118 613 *
1119 - * @param array $ids The current array of IDs.
1120 - * @param int $id The ID to remove.
1121 - *
1122 - * @return array The updated array of IDs.
614 + * Use this — never `tracking_consent()` — to decide whether data may be
615 + * sent off-site. See General_Section::raw_tracking_consent().
1123 616 */
1124 - private function remove_id_from_visibility( array $ids, int $id ): array {
1125 - return array_filter(
1126 - $ids,
1127 - function ( $existing_id ) use ( $id ) {
1128 - return $existing_id !== $id;
1129 - }
1130 - );
617 + public function raw_tracking_consent(): string {
618 + $section = $this->sections()->get( 'general' );
619 +
620 + if ( $section instanceof General_Section ) {
621 + return $section->raw_tracking_consent();
622 + }
623 +
624 + return '';
625 + }
626 +
627 + /**
628 + * Whether the JWT may be passed as a query parameter (Tools).
629 + */
630 + public function use_jwt_as_param_enabled(): bool {
631 + return (bool) $this->section_value( 'tools', 'use_jwt_as_param' );
632 + }
633 +
634 + /**
635 + * Admin email toggles for POS orders.
636 + */
637 + public function admin_emails(): array {
638 + return (array) $this->section_value( 'checkout', 'admin_emails' );
639 + }
640 +
641 + /**
642 + * Customer email toggles for POS orders.
643 + */
644 + public function customer_emails(): array {
645 + return (array) $this->section_value( 'checkout', 'customer_emails' );
646 + }
647 +
648 + /**
649 + * Cashier email toggles for POS orders.
650 + */
651 + public function cashier_emails(): array {
652 + return (array) $this->section_value( 'checkout', 'cashier_emails' );
653 + }
654 +
655 + /**
656 + * Script handles dequeued on the POS checkout pages.
657 + */
658 + public function dequeue_script_handles(): array {
659 + return (array) $this->section_value( 'checkout', 'dequeue_script_handles' );
660 + }
661 +
662 + /**
663 + * Style handles dequeued on the POS checkout pages.
664 + */
665 + public function dequeue_style_handles(): array {
666 + return (array) $this->section_value( 'checkout', 'dequeue_style_handles' );
667 + }
668 +
669 + /**
670 + * The default receipt mode: fiscal | live.
671 + */
672 + public function receipt_default_mode(): string {
673 + return (string) $this->section_value( 'checkout', 'receipt_default_mode' );
674 + }
675 +
676 + /**
677 + * Whether paid POS orders should be rejected when stock is unavailable.
678 + */
679 + public function prevent_overselling_enabled(): bool {
680 + return (bool) $this->section_value( 'checkout', 'prevent_overselling' );
681 + }
682 +
683 + /**
684 + * The user-override tax-ID write map (type => meta key).
685 + */
686 + public function tax_id_write_map(): array {
687 + return (array) $this->section_value( 'tax_ids', 'write_map' );
1131 688 }
1132 689 }