PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.16
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 / Receipt_Store_Resolver.php

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

368 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared receipt store setting resolver.
4 *
5 * @package WCPOS\WooCommercePOS\Services
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 use DateTimeZone;
11
12 /**
13 * Receipt_Store_Resolver class.
14 */
15 class Receipt_Store_Resolver {
16 /**
17 * POS store object.
18 *
19 * @var object
20 */
21 private $pos_store;
22
23 /**
24 * Constructor.
25 *
26 * @param object $pos_store POS store object.
27 */
28 public function __construct( $pos_store ) {
29 $this->pos_store = $pos_store;
30 }
31
32 /**
33 * Safely read a value from a POS store object.
34 *
35 * @param string $getter Getter method name.
36 * @param mixed $fallback Fallback value.
37 *
38 * @return mixed
39 */
40 public function get_store_value( string $getter, $fallback ) {
41 if ( ! \is_object( $this->pos_store ) || ! method_exists( $this->pos_store, $getter ) ) {
42 return $fallback;
43 }
44
45 return $this->pos_store->{$getter}();
46 }
47
48 /**
49 * Resolve a string setting from the store with a WooCommerce fallback.
50 *
51 * Empty strings are preserved as explicit overrides.
52 *
53 * @param string $getter Store getter method.
54 * @param mixed $fallback Fallback value.
55 *
56 * @return string
57 */
58 public function resolve_store_string( string $getter, $fallback ): string {
59 $value = $this->get_store_value( $getter, null );
60
61 return null !== $value ? (string) $value : (string) $fallback;
62 }
63
64 /**
65 * Resolve an enum-like store setting with a WooCommerce fallback.
66 *
67 * Empty strings fall back because these values must be valid option tokens.
68 *
69 * @param string $getter Store getter method.
70 * @param mixed $fallback Fallback value.
71 *
72 * @return string
73 */
74 public function resolve_store_option_string( string $getter, $fallback ): string {
75 $value = $this->get_store_value( $getter, null );
76
77 return null !== $value && '' !== (string) $value ? (string) $value : (string) $fallback;
78 }
79
80 /**
81 * Resolve the receipt timezone from the store, falling back to the site timezone.
82 *
83 * @return DateTimeZone
84 */
85 public function resolve_store_timezone(): DateTimeZone {
86 $timezone = (string) $this->get_store_value( 'get_timezone', '' );
87
88 if ( '' !== $timezone ) {
89 try {
90 return new DateTimeZone( $timezone );
91 } catch ( \Exception $error ) {
92 return wp_timezone();
93 }
94 }
95
96 return wp_timezone();
97 }
98
99 /**
100 * Resolve the store locale with site fallback.
101 *
102 * @return string
103 */
104 public function resolve_locale(): string {
105 $store_locale = (string) $this->get_store_value( 'get_locale', '' );
106
107 return '' !== $store_locale ? $store_locale : get_locale();
108 }
109
110 /**
111 * Resolve the number of price decimals from the store with WC fallback.
112 *
113 * @return int
114 */
115 public function resolve_price_num_decimals(): int {
116 $value = $this->get_store_value( 'get_price_number_of_decimals', wc_get_price_decimals() );
117
118 return is_numeric( $value ) && (float) $value >= 0 ? (int) $value : wc_get_price_decimals();
119 }
120
121 /**
122 * Build template-facing tax mode signals.
123 *
124 * @return array<string,mixed>
125 */
126 public function build_tax_section(): array {
127 $display = 'incl' === $this->resolve_store_option_string(
128 'get_tax_display_cart',
129 get_option( 'woocommerce_tax_display_cart', 'excl' )
130 ) ? 'incl' : 'excl';
131
132 $tax_enabled = 'yes' === $this->resolve_store_option_string(
133 'get_calc_taxes',
134 get_option( 'woocommerce_calc_taxes', 'no' )
135 );
136 $breakdown = $tax_enabled ? $this->resolve_store_option_string(
137 'get_tax_total_display',
138 get_option( 'woocommerce_tax_total_display', 'itemized' )
139 ) : 'hidden';
140
141 if ( ! in_array( $breakdown, array( 'hidden', 'single', 'itemized' ), true ) ) {
142 $breakdown = 'itemized';
143 }
144
145 return array(
146 'display' => $display,
147 'display_incl' => 'incl' === $display,
148 'display_excl' => 'excl' === $display,
149 'breakdown' => $breakdown,
150 'breakdown_hidden' => 'hidden' === $breakdown,
151 'breakdown_single' => 'single' === $breakdown,
152 'breakdown_itemized' => 'itemized' === $breakdown,
153 );
154 }
155
156 /**
157 * Assemble the template-facing `store` section from the POS store object.
158 *
159 * Every field is read through get_store_value(), so partial store objects —
160 * including the bare \stdClass used for orders whose store has since been
161 * deleted — fall back field by field instead of fataling. Blank values count
162 * as "not set" for the fields that accept a fallback, which is how both
163 * builders have always treated empty store settings.
164 *
165 * @param array<string,mixed> $fallbacks Optional per-field fallbacks. Recognised keys:
166 * `id` (int, default 0),
167 * `name` (string, default site title),
168 * `opening_hours` (array, default empty — renders null),
169 * `opening_hours_notes`, `personal_notes`,
170 * `policies_and_conditions` and `footer_imprint`
171 * (string|null, default null).
172 *
173 * @return array<string,mixed>
174 */
175 public function build_store_section( array $fallbacks = array() ): array {
176 $address = array(
177 'address_1' => (string) $this->get_store_value( 'get_store_address', '' ),
178 'address_2' => (string) $this->get_store_value( 'get_store_address_2', '' ),
179 'city' => (string) $this->get_store_value( 'get_store_city', '' ),
180 'state' => (string) $this->get_store_value( 'get_store_state', '' ),
181 'postcode' => (string) $this->get_store_value( 'get_store_postcode', '' ),
182 'country' => (string) $this->get_store_value( 'get_store_country', '' ),
183 );
184
185 $tax_ids = $this->get_store_value( 'get_tax_ids', array() );
186 if ( ! \is_array( $tax_ids ) ) {
187 $tax_ids = array();
188 }
189
190 // Resolved lazily so the bloginfo filters only run when they are actually needed.
191 $name = $this->resolve_optional_text( 'get_name', null );
192 if ( null === $name ) {
193 $name = isset( $fallbacks['name'] ) ? (string) $fallbacks['name'] : (string) get_bloginfo( 'name' );
194 }
195
196 $store = array(
197 'id' => (int) $this->get_store_value( 'get_id', $fallbacks['id'] ?? 0 ),
198 'name' => $name,
199 // Structured address parts mirror customer.billing_address — templates that
200 // want country-specific layouts compose from these. address_lines[] is the
201 // pre-formatted default for templates that just iterate, composed via
202 // WC_Countries::get_formatted_address() so per-country layouts are honoured.
203 'address' => $address,
204 'address_lines' => self::compose_address_lines( $address ),
205 'tax_ids' => self::with_store_tax_id_labels( $tax_ids, $this->resolve_locale() ),
206 'phone' => (string) $this->get_store_value( 'get_phone', '' ),
207 'email' => (string) $this->get_store_value( 'get_email', '' ),
208 'logo' => Store_Logo_Resolver::resolve( $this->pos_store ),
209 );
210
211 $opening_hours = $this->get_store_value( 'get_opening_hours', array() );
212 $has_structured_hours = \is_array( $opening_hours ) && ! empty( $opening_hours );
213 $has_legacy_hours = \is_string( $opening_hours ) && '' !== trim( $opening_hours );
214 if ( ! $has_structured_hours && ! $has_legacy_hours ) {
215 $opening_hours = $fallbacks['opening_hours'] ?? array();
216 $has_structured_hours = \is_array( $opening_hours ) && ! empty( $opening_hours );
217 }
218
219 if ( $has_structured_hours ) {
220 // Same locale the order timestamps use, so both render in one convention.
221 $hours_locale = $this->resolve_locale();
222 $store['opening_hours'] = Opening_Hours_Formatter::format_compact( $opening_hours, $hours_locale );
223 $store['opening_hours_vertical'] = Opening_Hours_Formatter::format_vertical( $opening_hours, $hours_locale );
224 $store['opening_hours_inline'] = Opening_Hours_Formatter::format_inline( $opening_hours, $hours_locale );
225 } elseif ( $has_legacy_hours ) {
226 // Legacy free-text hours have no per-day structure to reformat.
227 $store['opening_hours'] = $opening_hours;
228 $store['opening_hours_vertical'] = null;
229 $store['opening_hours_inline'] = null;
230 } else {
231 $store['opening_hours'] = null;
232 $store['opening_hours_vertical'] = null;
233 $store['opening_hours_inline'] = null;
234 }
235
236 $store['opening_hours_notes'] = $this->resolve_optional_text( 'get_opening_hours_notes', $fallbacks['opening_hours_notes'] ?? null );
237 $store['personal_notes'] = $this->resolve_optional_text( 'get_personal_notes', $fallbacks['personal_notes'] ?? null );
238 $store['policies_and_conditions'] = $this->resolve_optional_text( 'get_policies_and_conditions', $fallbacks['policies_and_conditions'] ?? null );
239 $store['footer_imprint'] = $this->resolve_optional_text( 'get_footer_imprint', $fallbacks['footer_imprint'] ?? null );
240
241 return $store;
242 }
243
244 /**
245 * Build price, currency, and locale presentation hints for renderers.
246 *
247 * @param string $currency Currency code.
248 * @param bool|null $prices_include_tax Optional precomputed prices-include-tax flag.
249 *
250 * @return array<string,mixed>
251 */
252 public function build_presentation_hints( string $currency, ?bool $prices_include_tax = null ): array {
253 if ( null === $prices_include_tax ) {
254 $prices_include_tax = 'yes' === $this->resolve_store_option_string(
255 'get_prices_include_tax',
256 wc_prices_include_tax() ? 'yes' : 'no'
257 );
258 }
259
260 return array(
261 'prices_entered_with_tax' => $prices_include_tax,
262 'rounding_mode' => $this->resolve_store_option_string(
263 'get_tax_round_at_subtotal',
264 get_option( 'woocommerce_tax_round_at_subtotal', 'no' )
265 ),
266 'locale' => $this->resolve_locale(),
267 'timezone' => $this->resolve_store_timezone()->getName(),
268 'currency_position' => $this->resolve_store_option_string(
269 'get_currency_position',
270 get_option( 'woocommerce_currency_pos', 'left' )
271 ),
272 'currency_symbol' => get_woocommerce_currency_symbol( $currency ),
273 'price_thousand_separator' => $this->resolve_store_string(
274 'get_price_thousand_separator',
275 wc_get_price_thousand_separator()
276 ),
277 'price_decimal_separator' => $this->resolve_store_string(
278 'get_price_decimal_separator',
279 wc_get_price_decimal_separator()
280 ),
281 'price_num_decimals' => $this->resolve_price_num_decimals(),
282 'price_display_suffix' => $this->resolve_store_string(
283 'get_price_display_suffix',
284 get_option( 'woocommerce_price_display_suffix', '' )
285 ),
286 );
287 }
288
289 /**
290 * Compose `address_lines[]` using the country's WC address format.
291 *
292 * @param array<string,string> $fields Store address fields.
293 *
294 * @return array<int,string>
295 */
296 public static function compose_address_lines( array $fields ): array {
297 $country = isset( $fields['country'] ) ? (string) $fields['country'] : '';
298 $formatted = WC()->countries->get_formatted_address(
299 array(
300 'first_name' => '',
301 'last_name' => '',
302 'company' => '',
303 'address_1' => $fields['address_1'] ?? '',
304 'address_2' => $fields['address_2'] ?? '',
305 'city' => $fields['city'] ?? '',
306 'state' => $fields['state'] ?? '',
307 'postcode' => $fields['postcode'] ?? '',
308 'country' => $country,
309 ),
310 "\n"
311 );
312
313 $lines = preg_split( '/\r?\n/', (string) $formatted );
314 if ( ! is_array( $lines ) ) {
315 return array();
316 }
317
318 return array_values(
319 array_filter(
320 array_map( 'trim', $lines ),
321 static function ( string $line ): bool {
322 return '' !== $line;
323 }
324 )
325 );
326 }
327
328 /**
329 * Ensure store tax IDs include display labels for logicless templates.
330 *
331 * @param array<int,array<string,mixed>> $tax_ids Store tax IDs.
332 * @param string $locale Receipt locale.
333 * @return array<int,array<string,mixed>>
334 */
335 public static function with_store_tax_id_labels( array $tax_ids, string $locale = '' ): array {
336 $labels = Receipt_I18n_Labels::get_labels( $locale );
337
338 return array_map(
339 static function ( array $tax_id ) use ( $labels ): array {
340 if ( ! empty( $tax_id['label'] ) ) {
341 return $tax_id;
342 }
343
344 $type = isset( $tax_id['type'] ) ? (string) $tax_id['type'] : 'other';
345 $key = 'store_tax_id_label_' . $type;
346 $tax_id['label'] = $labels[ $key ] ?? $labels['store_tax_id_label_other'];
347
348 return $tax_id;
349 },
350 $tax_ids
351 );
352 }
353
354 /**
355 * Resolve an optional free-text store field, treating a blank value as unset.
356 *
357 * @param string $getter Store getter method.
358 * @param string|null $fallback Value used when the store has nothing set.
359 *
360 * @return string|null
361 */
362 private function resolve_optional_text( string $getter, ?string $fallback ): ?string {
363 $value = (string) $this->get_store_value( $getter, '' );
364
365 return '' !== $value ? $value : $fallback;
366 }
367 }
368