PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.14
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.14
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 1.9.14 All 163 releases
woocommerce-pos / includes / Services / Preview_Receipt_Builder.php

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

1,328 lines 43.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Preview receipt data builder service.
4 *
5 * Generates realistic sample receipt data for template gallery/editor
6 * previews using the store's actual WooCommerce settings.
7 *
8 * @package WCPOS\WooCommercePOS\Services
9 */
10
11 namespace WCPOS\WooCommercePOS\Services;
12
13 use WCPOS\WooCommercePOS\Abstracts\Store;
14
15 /**
16 * Preview_Receipt_Builder class.
17 *
18 * Builds a sample receipt payload that mirrors the real Receipt_Data_Builder
19 * output but uses the store's catalog products, tax rates, and settings
20 * instead of data from an actual order.
21 */
22 class Preview_Receipt_Builder {
23
24 /**
25 * POS store object used to populate store fields.
26 *
27 * @var object
28 */
29 private $pos_store;
30
31 /**
32 * Shared store resolver.
33 *
34 * @var Receipt_Store_Resolver
35 */
36 private Receipt_Store_Resolver $store_resolver;
37
38 /**
39 * Fallback tax rate percentage when no WooCommerce tax rates are configured.
40 *
41 * @var float
42 */
43 const FALLBACK_TAX_RATE = 10.0;
44
45 /**
46 * Quantities assigned to each line item (up to 3 products).
47 *
48 * @var int[]
49 */
50 const LINE_QUANTITIES = array( 2, 1, 1 );
51
52 /**
53 * Minimum number of line items in the preview.
54 *
55 * @var int
56 */
57 const MIN_LINES = 2;
58
59 /**
60 * Sample customer data keyed by ISO 3166-1 alpha-2 country code.
61 *
62 * Each entry provides first_name, last_name, address_1, address_2, email,
63 * and phone fields. City, state, postcode, and country are filled from the
64 * store's WooCommerce settings at runtime. The 'US' entry is the fallback
65 * used for any country not listed here.
66 *
67 * @var array<string, array<string, string>>
68 */
69 const SAMPLE_CUSTOMERS = array(
70 'US' => array(
71 'first_name' => 'Sarah',
72 'last_name' => 'Johnson',
73 'address_1' => '456 Oak Avenue',
74 'address_2' => 'Suite 200',
75 'email' => 'sarah.johnson@example.com',
76 'phone' => '(555) 987-6543',
77 ),
78 'GB' => array(
79 'first_name' => 'James',
80 'last_name' => 'Smith',
81 'address_1' => '12 Baker Street',
82 'address_2' => 'Flat 3',
83 'email' => 'james.smith@example.co.uk',
84 'phone' => '07700 900123',
85 ),
86 'DE' => array(
87 'first_name' => 'Anna',
88 'last_name' => 'Mueller',
89 'address_1' => 'Hauptstraße 42',
90 'address_2' => '',
91 'email' => 'anna.mueller@example.de',
92 'phone' => '030 12345678',
93 ),
94 'FR' => array(
95 'first_name' => 'Marie',
96 'last_name' => 'Dupont',
97 'address_1' => '15 Rue de Rivoli',
98 'address_2' => 'Apt 4B',
99 'email' => 'marie.dupont@example.fr',
100 'phone' => '01 23 45 67 89',
101 ),
102 'AU' => array(
103 'first_name' => 'Liam',
104 'last_name' => 'Wilson',
105 'address_1' => '88 George Street',
106 'address_2' => '',
107 'email' => 'liam.wilson@example.com.au',
108 'phone' => '02 9876 5432',
109 ),
110 'JP' => array(
111 'first_name' => 'Yuki',
112 'last_name' => 'Tanaka',
113 'address_1' => '1-2-3 Shibuya',
114 'address_2' => '',
115 'email' => 'yuki.tanaka@example.jp',
116 'phone' => '03-1234-5678',
117 ),
118 'NL' => array(
119 'first_name' => 'Lars',
120 'last_name' => 'de Vries',
121 'address_1' => 'Keizersgracht 123',
122 'address_2' => '',
123 'email' => 'lars.devries@example.nl',
124 'phone' => '020 123 4567',
125 ),
126 'ES' => array(
127 'first_name' => 'Carmen',
128 'last_name' => 'García',
129 'address_1' => 'Calle Mayor 7',
130 'address_2' => '',
131 'email' => 'carmen.garcia@example.es',
132 'phone' => '91 234 56 78',
133 ),
134 'IT' => array(
135 'first_name' => 'Marco',
136 'last_name' => 'Rossi',
137 'address_1' => 'Via Roma 55',
138 'address_2' => '',
139 'email' => 'marco.rossi@example.it',
140 'phone' => '06 1234 5678',
141 ),
142 'CA' => array(
143 'first_name' => 'Emma',
144 'last_name' => 'Brown',
145 'address_1' => '200 Bay Street',
146 'address_2' => 'Unit 1500',
147 'email' => 'emma.brown@example.ca',
148 'phone' => '(416) 555-0123',
149 ),
150 'NZ' => array(
151 'first_name' => 'Oliver',
152 'last_name' => 'Taylor',
153 'address_1' => '42 Lambton Quay',
154 'address_2' => '',
155 'email' => 'oliver.taylor@example.co.nz',
156 'phone' => '04 123 4567',
157 ),
158 'IN' => array(
159 'first_name' => 'Priya',
160 'last_name' => 'Sharma',
161 'address_1' => '14 MG Road',
162 'address_2' => '',
163 'email' => 'priya.sharma@example.in',
164 'phone' => '98765 43210',
165 ),
166 'BR' => array(
167 'first_name' => 'Ana',
168 'last_name' => 'Silva',
169 'address_1' => 'Rua Augusta 1200',
170 'address_2' => 'Apto 42',
171 'email' => 'ana.silva@example.com.br',
172 'phone' => '(11) 98765-4321',
173 ),
174 'MX' => array(
175 'first_name' => 'Sofía',
176 'last_name' => 'Hernández',
177 'address_1' => 'Av. Reforma 222',
178 'address_2' => 'Piso 3',
179 'email' => 'sofia.hernandez@example.com.mx',
180 'phone' => '55 1234 5678',
181 ),
182 'SE' => array(
183 'first_name' => 'Erik',
184 'last_name' => 'Lindqvist',
185 'address_1' => 'Drottninggatan 50',
186 'address_2' => '',
187 'email' => 'erik.lindqvist@example.se',
188 'phone' => '08-123 456 78',
189 ),
190 'NO' => array(
191 'first_name' => 'Ingrid',
192 'last_name' => 'Hansen',
193 'address_1' => 'Karl Johans gate 10',
194 'address_2' => '',
195 'email' => 'ingrid.hansen@example.no',
196 'phone' => '22 12 34 56',
197 ),
198 'DK' => array(
199 'first_name' => 'Mads',
200 'last_name' => 'Nielsen',
201 'address_1' => 'Strøget 28',
202 'address_2' => '',
203 'email' => 'mads.nielsen@example.dk',
204 'phone' => '33 12 34 56',
205 ),
206 'FI' => array(
207 'first_name' => 'Aino',
208 'last_name' => 'Virtanen',
209 'address_1' => 'Mannerheimintie 15',
210 'address_2' => '',
211 'email' => 'aino.virtanen@example.fi',
212 'phone' => '09 123 4567',
213 ),
214 'BE' => array(
215 'first_name' => 'Lucas',
216 'last_name' => 'Peeters',
217 'address_1' => 'Meir 48',
218 'address_2' => '',
219 'email' => 'lucas.peeters@example.be',
220 'phone' => '03 123 45 67',
221 ),
222 'AT' => array(
223 'first_name' => 'Katharina',
224 'last_name' => 'Gruber',
225 'address_1' => 'Kärntner Straße 21',
226 'address_2' => '',
227 'email' => 'katharina.gruber@example.at',
228 'phone' => '01 234 5678',
229 ),
230 'CH' => array(
231 'first_name' => 'Lukas',
232 'last_name' => 'Meier',
233 'address_1' => 'Bahnhofstrasse 30',
234 'address_2' => '',
235 'email' => 'lukas.meier@example.ch',
236 'phone' => '044 123 45 67',
237 ),
238 'PT' => array(
239 'first_name' => 'Maria',
240 'last_name' => 'Santos',
241 'address_1' => 'Rua Augusta 85',
242 'address_2' => '',
243 'email' => 'maria.santos@example.pt',
244 'phone' => '21 123 4567',
245 ),
246 'PL' => array(
247 'first_name' => 'Katarzyna',
248 'last_name' => 'Nowak',
249 'address_1' => 'ul. Marszałkowska 12',
250 'address_2' => '',
251 'email' => 'katarzyna.nowak@example.pl',
252 'phone' => '22 123 45 67',
253 ),
254 'CZ' => array(
255 'first_name' => 'Tereza',
256 'last_name' => 'Nováková',
257 'address_1' => 'Václavské náměstí 30',
258 'address_2' => '',
259 'email' => 'tereza.novakova@example.cz',
260 'phone' => '221 234 567',
261 ),
262 'IE' => array(
263 'first_name' => 'Aoife',
264 'last_name' => 'Murphy',
265 'address_1' => '22 Grafton Street',
266 'address_2' => '',
267 'email' => 'aoife.murphy@example.ie',
268 'phone' => '01 234 5678',
269 ),
270 'ZA' => array(
271 'first_name' => 'Thandiwe',
272 'last_name' => 'Ndlovu',
273 'address_1' => '100 Long Street',
274 'address_2' => '',
275 'email' => 'thandiwe.ndlovu@example.co.za',
276 'phone' => '021 123 4567',
277 ),
278 'SG' => array(
279 'first_name' => 'Wei Lin',
280 'last_name' => 'Tan',
281 'address_1' => '1 Raffles Place',
282 'address_2' => '#08-01',
283 'email' => 'weilin.tan@example.sg',
284 'phone' => '6123 4567',
285 ),
286 'HK' => array(
287 'first_name' => 'Wing',
288 'last_name' => 'Chan',
289 'address_1' => '88 Queens Road Central',
290 'address_2' => 'Flat 12A',
291 'email' => 'wing.chan@example.hk',
292 'phone' => '2123 4567',
293 ),
294 'KR' => array(
295 'first_name' => 'Jimin',
296 'last_name' => 'Park',
297 'address_1' => '25 Gangnam-daero',
298 'address_2' => '',
299 'email' => 'jimin.park@example.kr',
300 'phone' => '02-1234-5678',
301 ),
302 'MY' => array(
303 'first_name' => 'Nurul',
304 'last_name' => 'Ahmad',
305 'address_1' => 'Jalan Bukit Bintang 55',
306 'address_2' => '',
307 'email' => 'nurul.ahmad@example.my',
308 'phone' => '03-1234 5678',
309 ),
310 'TH' => array(
311 'first_name' => 'Siriporn',
312 'last_name' => 'Suksawat',
313 'address_1' => '99 Sukhumvit Road',
314 'address_2' => '',
315 'email' => 'siriporn.s@example.co.th',
316 'phone' => '02 123 4567',
317 ),
318 'PH' => array(
319 'first_name' => 'Maria',
320 'last_name' => 'Reyes',
321 'address_1' => '123 Ayala Avenue',
322 'address_2' => '',
323 'email' => 'maria.reyes@example.ph',
324 'phone' => '02 8123 4567',
325 ),
326 'AE' => array(
327 'first_name' => 'Fatima',
328 'last_name' => 'Al Maktoum',
329 'address_1' => 'Sheikh Zayed Road',
330 'address_2' => 'Tower 1, Office 501',
331 'email' => 'fatima.m@example.ae',
332 'phone' => '04 123 4567',
333 ),
334 'SA' => array(
335 'first_name' => 'Nora',
336 'last_name' => 'Al-Rashid',
337 'address_1' => 'King Fahd Road',
338 'address_2' => '',
339 'email' => 'nora.alrashid@example.sa',
340 'phone' => '011 234 5678',
341 ),
342 'IL' => array(
343 'first_name' => 'Noa',
344 'last_name' => 'Cohen',
345 'address_1' => 'Rothschild Blvd 40',
346 'address_2' => '',
347 'email' => 'noa.cohen@example.co.il',
348 'phone' => '03-123-4567',
349 ),
350 'TR' => array(
351 'first_name' => 'Elif',
352 'last_name' => 'Yılmaz',
353 'address_1' => 'İstiklal Caddesi 45',
354 'address_2' => '',
355 'email' => 'elif.yilmaz@example.com.tr',
356 'phone' => '0212 123 4567',
357 ),
358 'RU' => array(
359 'first_name' => 'Anastasia',
360 'last_name' => 'Ivanova',
361 'address_1' => 'Tverskaya Ulitsa 12',
362 'address_2' => 'Kv. 5',
363 'email' => 'anastasia.ivanova@example.ru',
364 'phone' => '+7 495 123-45-67',
365 ),
366 'GR' => array(
367 'first_name' => 'Eleni',
368 'last_name' => 'Papadopoulos',
369 'address_1' => 'Ermou 25',
370 'address_2' => '',
371 'email' => 'eleni.p@example.gr',
372 'phone' => '210 123 4567',
373 ),
374 'RO' => array(
375 'first_name' => 'Andreea',
376 'last_name' => 'Popescu',
377 'address_1' => 'Calea Victoriei 100',
378 'address_2' => '',
379 'email' => 'andreea.popescu@example.ro',
380 'phone' => '021 123 4567',
381 ),
382 'HU' => array(
383 'first_name' => 'Anna',
384 'last_name' => 'Szabó',
385 'address_1' => 'Andrássy út 60',
386 'address_2' => '',
387 'email' => 'anna.szabo@example.hu',
388 'phone' => '1 234 5678',
389 ),
390 'ID' => array(
391 'first_name' => 'Siti',
392 'last_name' => 'Rahayu',
393 'address_1' => 'Jl. Sudirman No. 52',
394 'address_2' => '',
395 'email' => 'siti.rahayu@example.co.id',
396 'phone' => '021-1234567',
397 ),
398 'TW' => array(
399 'first_name' => 'Mei-Ling',
400 'last_name' => 'Chen',
401 'address_1' => 'Zhongxiao East Road Sec 4, No 12',
402 'address_2' => '3F',
403 'email' => 'meiling.chen@example.tw',
404 'phone' => '02-2771-1234',
405 ),
406 'CN' => array(
407 'first_name' => 'Xiao',
408 'last_name' => 'Wang',
409 'address_1' => 'Nanjing Road 200',
410 'address_2' => '',
411 'email' => 'xiao.wang@example.cn',
412 'phone' => '021-6234-5678',
413 ),
414 'VN' => array(
415 'first_name' => 'Linh',
416 'last_name' => 'Nguyen',
417 'address_1' => '45 Le Loi Street',
418 'address_2' => '',
419 'email' => 'linh.nguyen@example.vn',
420 'phone' => '028 1234 5678',
421 ),
422 'AR' => array(
423 'first_name' => 'Valentina',
424 'last_name' => 'Fernández',
425 'address_1' => 'Av. Corrientes 1234',
426 'address_2' => 'Piso 5',
427 'email' => 'valentina.f@example.com.ar',
428 'phone' => '11 1234-5678',
429 ),
430 'CO' => array(
431 'first_name' => 'Camila',
432 'last_name' => 'Rodríguez',
433 'address_1' => 'Carrera 7 No. 71-52',
434 'address_2' => '',
435 'email' => 'camila.r@example.co',
436 'phone' => '601 234 5678',
437 ),
438 'CL' => array(
439 'first_name' => 'Catalina',
440 'last_name' => 'Muñoz',
441 'address_1' => 'Av. Providencia 1200',
442 'address_2' => 'Of. 301',
443 'email' => 'catalina.munoz@example.cl',
444 'phone' => '2 2345 6789',
445 ),
446 );
447
448 /**
449 * Fallback product definitions when the catalog is empty.
450 *
451 * @var array[]
452 */
453 const FALLBACK_PRODUCTS = array(
454 array(
455 'name' => 'Premium Widget',
456 'price' => 29.99,
457 'regular_price' => 34.99,
458 'sku' => 'WIDGET-001',
459 'meta' => array(
460 array(
461 'key' => 'Size',
462 'value' => 'Large',
463 ),
464 array(
465 'key' => 'Color',
466 'value' => 'Midnight Blue',
467 ),
468 ),
469 'attributes' => array(
470 array(
471 'key' => 'Size',
472 'value' => 'Large',
473 ),
474 array(
475 'key' => 'Color',
476 'value' => 'Midnight Blue',
477 ),
478 ),
479 ),
480 array(
481 'name' => 'Standard Gadget',
482 'price' => 15.50,
483 'regular_price' => 15.50,
484 'sku' => 'GADGET-002',
485 'meta' => array(
486 array(
487 'key' => 'Material',
488 'value' => 'Stainless Steel',
489 ),
490 ),
491 'attributes' => array(
492 array(
493 'key' => 'Material',
494 'value' => 'Stainless Steel',
495 ),
496 ),
497 ),
498 array(
499 'name' => 'Deluxe Component',
500 'price' => 42.00,
501 'regular_price' => 42.00,
502 'sku' => 'COMP-003',
503 'meta' => array(),
504 'attributes' => array(),
505 ),
506 );
507
508 /**
509 * Build a preview receipt payload.
510 *
511 * Returns an array matching the receipt data schema with all sections
512 * populated using the store's real settings, products, and tax rates.
513 *
514 * @param object|null $pos_store POS store object. Falls back to default store.
515 *
516 * @return array Complete receipt data array.
517 */
518 public function build( $pos_store = null ): array {
519 $resolved_store = null === $pos_store ? wcpos_get_store() : $pos_store;
520 if ( ! \is_object( $resolved_store ) ) {
521 $resolved_store = wcpos_get_store();
522 }
523 $this->pos_store = \is_object( $resolved_store ) ? $resolved_store : new Store();
524 $this->store_resolver = new Receipt_Store_Resolver( $this->pos_store );
525 $currency = $this->resolve_currency();
526 $display_incl = 'incl' === $this->store_resolver->resolve_store_option_string(
527 'get_tax_display_cart',
528 get_option( 'woocommerce_tax_display_cart', 'excl' )
529 );
530 $tax_enabled = 'yes' === $this->store_resolver->resolve_store_option_string(
531 'get_calc_taxes',
532 get_option( 'woocommerce_calc_taxes', 'no' )
533 );
534 $tax_config = $this->get_tax_config( $tax_enabled );
535 $tax_rate = $tax_config['rate'];
536 $tax_label = $tax_config['label'];
537 $tax_code = $tax_config['code'];
538
539 $raw_products = $this->get_products();
540 $prices_include_tax = 'yes' === $this->store_resolver->resolve_store_option_string(
541 'get_prices_include_tax',
542 wc_prices_include_tax() ? 'yes' : 'no'
543 );
544 $dp = $this->store_resolver->resolve_price_num_decimals();
545
546 // Build line items.
547 $lines = array();
548 $lines_total_excl = 0.0;
549 $lines_total_incl = 0.0;
550
551 foreach ( $raw_products as $index => $product ) {
552 $qty = self::LINE_QUANTITIES[ $index ] ?? 1;
553 $base_price = (float) $product['price'];
554 $regular_base_price = (float) $product['regular_price'];
555
556 if ( $prices_include_tax ) {
557 $unit_incl = $base_price;
558 $unit_excl = $base_price / ( 1 + $tax_rate / 100 );
559 $regular_unit_incl = $regular_base_price;
560 $regular_unit_excl = $regular_base_price / ( 1 + $tax_rate / 100 );
561 } else {
562 $unit_excl = $base_price;
563 $unit_incl = $base_price * ( 1 + $tax_rate / 100 );
564 $regular_unit_excl = $regular_base_price;
565 $regular_unit_incl = $regular_base_price * ( 1 + $tax_rate / 100 );
566 }
567
568 $selling_unit_incl = round( $unit_incl, $dp );
569 $selling_unit_excl = round( $unit_excl, $dp );
570 $regular_unit_incl = round( $regular_unit_incl, $dp );
571 $regular_unit_excl = round( $regular_unit_excl, $dp );
572 $unit_savings_incl = max( 0.0, $regular_unit_incl - $selling_unit_incl );
573 $unit_savings_excl = max( 0.0, $regular_unit_excl - $selling_unit_excl );
574 $line_total_incl = round( $selling_unit_incl * $qty, $dp );
575 $line_total_excl = round( $selling_unit_excl * $qty, $dp );
576 $line_regular_incl = round( $regular_unit_incl * $qty, $dp );
577 $line_regular_excl = round( $regular_unit_excl * $qty, $dp );
578 $line_savings_incl = round( $unit_savings_incl * $qty, $dp );
579 $line_savings_excl = round( $unit_savings_excl * $qty, $dp );
580
581 $unit_price_rounded = $display_incl ? $selling_unit_incl : $selling_unit_excl;
582
583 $lines[] = array(
584 'key' => (string) ( $index + 1 ),
585 'sku' => $product['sku'],
586 'name' => $product['name'],
587 'qty' => (float) $qty,
588 'qty_refunded' => 0.0,
589 'regular_price' => $display_incl ? $regular_unit_incl : $regular_unit_excl,
590 'regular_price_incl' => $regular_unit_incl,
591 'regular_price_excl' => $regular_unit_excl,
592 'selling_price' => $unit_price_rounded,
593 'selling_price_incl' => $selling_unit_incl,
594 'selling_price_excl' => $selling_unit_excl,
595 'unit_savings' => $display_incl ? $unit_savings_incl : $unit_savings_excl,
596 'unit_savings_incl' => $unit_savings_incl,
597 'unit_savings_excl' => $unit_savings_excl,
598 'line_regular_total' => $display_incl ? $line_regular_incl : $line_regular_excl,
599 'line_regular_total_incl' => $line_regular_incl,
600 'line_regular_total_excl' => $line_regular_excl,
601 'line_selling_total' => $display_incl ? $line_total_incl : $line_total_excl,
602 'line_selling_total_incl' => $line_total_incl,
603 'line_selling_total_excl' => $line_total_excl,
604 'line_savings' => $display_incl ? $line_savings_incl : $line_savings_excl,
605 'line_savings_incl' => $line_savings_incl,
606 'line_savings_excl' => $line_savings_excl,
607 'savings_in_discounts' => false,
608 'unit_subtotal' => $unit_price_rounded,
609 'unit_subtotal_incl' => $selling_unit_incl,
610 'unit_subtotal_excl' => $selling_unit_excl,
611 'unit_price' => $unit_price_rounded,
612 'unit_price_incl' => $selling_unit_incl,
613 'unit_price_excl' => $selling_unit_excl,
614 'line_subtotal' => $display_incl ? $line_total_incl : $line_total_excl,
615 'line_subtotal_incl' => $line_total_incl,
616 'line_subtotal_excl' => $line_total_excl,
617 'discounts' => 0.0,
618 'discounts_incl' => 0.0,
619 'discounts_excl' => 0.0,
620 'line_total' => $display_incl ? $line_total_incl : $line_total_excl,
621 'line_total_incl' => $line_total_incl,
622 'line_total_excl' => $line_total_excl,
623 'total_refunded' => 0.0,
624 'taxes' => array(),
625 'meta' => $product['meta'] ?? array(),
626 'attributes' => $product['attributes'] ?? array(),
627 );
628
629 $lines_total_excl += $line_total_excl;
630 $lines_total_incl += $line_total_incl;
631 }
632
633 // Fee (excl tax).
634 $fee_excl = 2.50;
635 $fee_tax = round( $fee_excl * $tax_rate / 100, $dp );
636 $fee_incl = $fee_excl + $fee_tax;
637 $fee_label = /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Gift Wrapping', 'woocommerce-pos' );
638
639 // Shipping (excl tax).
640 $shipping_excl = 10.00;
641 $shipping_tax = round( $shipping_excl * $tax_rate / 100, $dp );
642 $shipping_incl = $shipping_excl + $shipping_tax;
643 $shipping_label = /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Flat Rate Shipping', 'woocommerce-pos' );
644
645 // Discount: 10% of line items excl total.
646 $discount_rate = 10.0;
647 $discount_excl = round( $lines_total_excl * $discount_rate / 100, $dp );
648 $discount_tax = round( $discount_excl * $tax_rate / 100, $dp );
649 $discount_incl = $discount_excl + $discount_tax;
650 $discount_label = strtr(
651 /* translators: %s: discount percentage */
652 __( 'Summer Sale (%s%%)', 'woocommerce-pos' ),
653 array(
654 '%1$s' => (string) (int) $discount_rate,
655 '%s' => (string) (int) $discount_rate,
656 '%%' => '%',
657 )
658 );
659
660 // Distribute discount proportionally across line items with remainder correction.
661 $sum_discount_excl = 0.0;
662 $sum_discount_incl = 0.0;
663 $last_index = count( $lines ) - 1;
664
665 foreach ( $lines as $i => &$line ) {
666 if ( $lines_total_excl > 0 ) {
667 $share = $line['line_subtotal_excl'] / $lines_total_excl;
668 } else {
669 $share = 1.0 / count( $lines );
670 }
671
672 if ( $i < $last_index ) {
673 $line['discounts_excl'] = round( $discount_excl * $share, $dp );
674 $line['discounts_incl'] = round( $discount_incl * $share, $dp );
675 $sum_discount_excl += $line['discounts_excl'];
676 $sum_discount_incl += $line['discounts_incl'];
677 } else {
678 // Assign remainder to last item so distributed totals match exactly.
679 $line['discounts_excl'] = round( $discount_excl - $sum_discount_excl, $dp );
680 $line['discounts_incl'] = round( $discount_incl - $sum_discount_incl, $dp );
681 }
682
683 $line['discounts'] = $display_incl ? $line['discounts_incl'] : $line['discounts_excl'];
684 $line['line_total_excl'] = round( $line['line_subtotal_excl'] - $line['discounts_excl'], $dp );
685 $line['line_total_incl'] = round( $line['line_subtotal_incl'] - $line['discounts_incl'], $dp );
686 $line['line_total'] = $display_incl ? $line['line_total_incl'] : $line['line_total_excl'];
687 $line['unit_price_incl'] = round( $line['line_total_incl'] / $line['qty'], $dp );
688 $line['unit_price_excl'] = round( $line['line_total_excl'] / $line['qty'], $dp );
689 $line['unit_price'] = $display_incl ? $line['unit_price_incl'] : $line['unit_price_excl'];
690 }
691 unset( $line );
692
693 // Totals.
694 $subtotal_excl = $lines_total_excl;
695 $subtotal_incl = $lines_total_incl;
696
697 // Item count summaries — matches the real builder's totals.total_qty
698 // / totals.line_count so preview mirrors the live receipt schema.
699 $total_qty = (float) array_sum( array_column( $lines, 'qty' ) );
700 $line_count = \count( $lines );
701
702 // Taxable base: line items - discount + shipping + fee (all excl).
703 $taxable_excl = $subtotal_excl - $discount_excl + $shipping_excl + $fee_excl;
704 $total_tax = round( $taxable_excl * $tax_rate / 100, $dp );
705
706 $total_excl = $subtotal_excl - $discount_excl + $shipping_excl + $fee_excl;
707 $total_incl = $total_excl + $total_tax;
708
709 // Payment: cash rounded up to nearest 5.
710 $tendered = (float) ( ceil( $total_incl / 5 ) * 5 );
711 $change_total = round( $tendered - $total_incl, $dp );
712
713 $created_timestamp = strtotime( '2024-01-15 10:30:00 UTC' );
714 $paid_timestamp = strtotime( '2024-01-15 10:35:00 UTC' );
715 $completed_timestamp = strtotime( '2024-01-15 10:42:00 UTC' );
716 $date_timezone = $this->store_resolver->resolve_store_timezone();
717 $date_locale = $this->store_resolver->resolve_locale();
718
719 $order = array(
720 'id' => 1234,
721 'number' => '1234',
722 'currency' => $currency,
723 'customer_note' => __( 'Please gift wrap this order. Thank you!', 'woocommerce-pos' ),
724 'wc_status' => 'completed',
725 'created_via' => 'woocommerce-pos',
726 'created' => Receipt_Date_Formatter::from_timestamp( $created_timestamp, $date_timezone, $date_locale ),
727 'paid' => Receipt_Date_Formatter::from_timestamp( $paid_timestamp, $date_timezone, $date_locale ),
728 'completed' => Receipt_Date_Formatter::from_timestamp( $completed_timestamp, $date_timezone, $date_locale ),
729 'printed' => Receipt_Date_Formatter::from_timestamp( time(), $date_timezone, $date_locale ),
730 'needs_payment' => false,
731 'payment_url' => '',
732 );
733
734 $store = $this->get_store_info();
735
736 $cashier = $this->get_cashier();
737
738 $customer = $this->get_customer();
739
740 $fees = array(
741 array(
742 'label' => $fee_label,
743 'total' => $display_incl ? $fee_incl : $fee_excl,
744 'total_incl' => $fee_incl,
745 'total_excl' => $fee_excl,
746 'taxes' => array(),
747 'meta' => array(),
748 ),
749 );
750
751 $shipping = array(
752 array(
753 'label' => $shipping_label,
754 'method_id' => 'flat_rate',
755 'total' => $display_incl ? $shipping_incl : $shipping_excl,
756 'total_incl' => $shipping_incl,
757 'total_excl' => $shipping_excl,
758 'taxes' => array(),
759 'meta' => array(),
760 ),
761 );
762
763 $discounts = array(
764 array(
765 'label' => $discount_label,
766 'code' => 'SUMMER10',
767 'total' => $display_incl ? $discount_incl : $discount_excl,
768 'total_incl' => $discount_incl,
769 'total_excl' => $discount_excl,
770 ),
771 );
772
773 $sale_savings_total_incl = (float) array_sum( array_column( $lines, 'line_savings_incl' ) );
774 $sale_savings_total_excl = (float) array_sum( array_column( $lines, 'line_savings_excl' ) );
775 $total_saved_incl = $discount_incl + $sale_savings_total_incl;
776 $total_saved_excl = $discount_excl + $sale_savings_total_excl;
777
778 $totals = array(
779 'subtotal' => $display_incl ? $subtotal_incl : $subtotal_excl,
780 'subtotal_incl' => $subtotal_incl,
781 'subtotal_excl' => $subtotal_excl,
782 'discount_total' => $display_incl ? $discount_incl : $discount_excl,
783 'discount_total_incl' => $discount_incl,
784 'discount_total_excl' => $discount_excl,
785 'sale_savings_total' => $display_incl ? $sale_savings_total_incl : $sale_savings_total_excl,
786 'sale_savings_total_incl' => $sale_savings_total_incl,
787 'sale_savings_total_excl' => $sale_savings_total_excl,
788 'total_saved' => $display_incl ? $total_saved_incl : $total_saved_excl,
789 'total_saved_incl' => $total_saved_incl,
790 'total_saved_excl' => $total_saved_excl,
791 'total_saved_complete' => true,
792 'tax_total' => $total_tax,
793 'total' => $display_incl ? $total_incl : $total_excl,
794 'total_incl' => $total_incl,
795 'total_excl' => $total_excl,
796 'paid_total' => $total_incl,
797 'change_total' => $change_total,
798 'total_qty' => $total_qty,
799 'line_count' => $line_count,
800 );
801
802 $taxable_amount_incl = $taxable_excl + $total_tax;
803
804 if ( $tax_rate > 0 ) {
805 $tax_summary = array(
806 array(
807 'code' => $tax_code,
808 'rate' => $tax_rate,
809 'label' => $tax_label,
810 'compound' => false,
811 'taxable_amount_excl' => $taxable_excl,
812 'tax_amount' => $total_tax,
813 'taxable_amount_incl' => $taxable_amount_incl,
814 ),
815 );
816 } else {
817 $tax_summary = array();
818 }
819
820 $payments = array(
821 array(
822 'method_id' => 'pos_cash',
823 'method_title' => /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Cash', 'woocommerce-pos' ),
824 'amount' => $total_incl,
825 'transaction_id' => '',
826 'tendered' => $tendered,
827 'change' => $change_total,
828 ),
829 );
830
831 $refunds = array();
832
833 $presentation_hints = $this->store_resolver->build_presentation_hints( $currency, $prices_include_tax );
834 $tax = $this->store_resolver->build_tax_section();
835
836 $fiscal = array(
837 'immutable_id' => '12345:42',
838 'receipt_number' => '00042',
839 'sequence' => 42,
840 'hash' => 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
841 'qr_payload' => 'https://example.com/verify?id=SAMPLE-001',
842 'tax_agency_code' => 'SAMPLE',
843 'signed_at' => gmdate( 'Y-m-d\TH:i:s\Z' ),
844 'signature_excerpt' => 'A1B2',
845 'document_label' => /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Tax Receipt', 'woocommerce-pos' ),
846 'is_reprint' => false,
847 'reprint_count' => 0,
848 'extra_fields' => array(
849 array(
850 'label' => /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Tax ID', 'woocommerce-pos' ),
851 'value' => 'XX-1234567',
852 ),
853 array(
854 'label' => /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Auth Code', 'woocommerce-pos' ),
855 'value' => 'ABC-789',
856 ),
857 ),
858 );
859
860 return array(
861 'order' => $order,
862 'store' => $store,
863 'cashier' => $cashier,
864 'customer' => $customer,
865 'lines' => $lines,
866 'fees' => $fees,
867 'shipping' => $shipping,
868 'discounts' => $discounts,
869 'totals' => $totals,
870 'tax' => $tax,
871 'tax_summary' => $tax_summary,
872 'has_tax_summary' => ! empty( $tax_summary ),
873 'payments' => $payments,
874 'refunds' => $refunds,
875 'fiscal' => $fiscal,
876 'presentation_hints' => $presentation_hints,
877 'i18n' => Receipt_I18n_Labels::get_labels( $presentation_hints['locale'] ?? '' ),
878 );
879 }
880
881 /**
882 * Get store information from the POS store object.
883 *
884 * @return array Store data with name, address, branding, and policy fields.
885 */
886 private function get_store_info(): array {
887 $pos_store = $this->pos_store;
888 $store_id = (int) $this->store_resolver->get_store_value( 'get_id', 0 );
889 $store_name = (string) $this->store_resolver->get_store_value( 'get_name', '' );
890 $store_address = (string) $this->store_resolver->get_store_value( 'get_store_address', '' );
891 $store_address_2 = (string) $this->store_resolver->get_store_value( 'get_store_address_2', '' );
892 $store_city = (string) $this->store_resolver->get_store_value( 'get_store_city', '' );
893 $store_postcode = (string) $this->store_resolver->get_store_value( 'get_store_postcode', '' );
894 $store_country = (string) $this->store_resolver->get_store_value( 'get_store_country', '' );
895 $store_state = (string) $this->store_resolver->get_store_value( 'get_store_state', '' );
896 $store_phone = (string) $this->store_resolver->get_store_value( 'get_phone', '' );
897 $store_email = (string) $this->store_resolver->get_store_value( 'get_email', '' );
898 $store_tax_ids = $this->store_resolver->get_store_value( 'get_tax_ids', array() );
899 $store_tax_ids = is_array( $store_tax_ids ) ? $store_tax_ids : array();
900 $store_tax_ids = Receipt_Store_Resolver::with_store_tax_id_labels( $store_tax_ids, $this->store_resolver->resolve_locale() );
901
902 $store_address_parts = array(
903 'address_1' => $store_address,
904 'address_2' => $store_address_2,
905 'city' => $store_city,
906 'state' => $store_state,
907 'postcode' => $store_postcode,
908 'country' => $store_country,
909 );
910
911 $store = array(
912 'id' => $store_id,
913 'name' => '' !== $store_name ? $store_name : get_bloginfo( 'name' ),
914 'address' => $store_address_parts,
915 'address_lines' => Receipt_Store_Resolver::compose_address_lines( $store_address_parts ),
916 'tax_ids' => $store_tax_ids,
917 'phone' => $store_phone,
918 'email' => $store_email,
919 );
920
921 $opening_hours_raw = $this->store_resolver->get_store_value( 'get_opening_hours', array() );
922 $personal_notes = (string) $this->store_resolver->get_store_value( 'get_personal_notes', '' );
923 $policies_and_conditions = (string) $this->store_resolver->get_store_value( 'get_policies_and_conditions', '' );
924 $footer_imprint = (string) $this->store_resolver->get_store_value( 'get_footer_imprint', '' );
925
926 if ( ! empty( $opening_hours_raw ) && \is_array( $opening_hours_raw ) ) {
927 $store['opening_hours'] = Opening_Hours_Formatter::format_compact( $opening_hours_raw );
928 $store['opening_hours_vertical'] = Opening_Hours_Formatter::format_vertical( $opening_hours_raw );
929 $store['opening_hours_inline'] = Opening_Hours_Formatter::format_inline( $opening_hours_raw );
930 } elseif ( \is_string( $opening_hours_raw ) && '' !== trim( $opening_hours_raw ) ) {
931 $store['opening_hours'] = $opening_hours_raw;
932 $store['opening_hours_vertical'] = null;
933 $store['opening_hours_inline'] = null;
934 } else {
935 // Sample hours for preview when none configured.
936 $opening_hours_raw = array(
937 '0' => array( '09:00', '17:00' ),
938 '1' => array( '09:00', '17:00' ),
939 '2' => array( '09:00', '17:00' ),
940 '3' => array( '09:00', '17:00' ),
941 '4' => array( '09:00', '17:00' ),
942 '5' => array( '10:00', '16:00' ),
943 '6' => array(),
944 );
945 $store['opening_hours'] = Opening_Hours_Formatter::format_compact( $opening_hours_raw );
946 $store['opening_hours_vertical'] = Opening_Hours_Formatter::format_vertical( $opening_hours_raw );
947 $store['opening_hours_inline'] = Opening_Hours_Formatter::format_inline( $opening_hours_raw );
948 }
949 $store['logo'] = Store_Logo_Resolver::resolve( $pos_store );
950 $opening_hours_notes = (string) $this->store_resolver->get_store_value( 'get_opening_hours_notes', '' );
951 $store['opening_hours_notes'] = '' !== $opening_hours_notes ? $opening_hours_notes : null;
952 $store['personal_notes'] = ( null !== $personal_notes && '' !== $personal_notes )
953 ? $personal_notes
954 : __( 'Thank you for shopping with us! We appreciate your business.', 'woocommerce-pos' );
955 $store['policies_and_conditions'] = ( null !== $policies_and_conditions && '' !== $policies_and_conditions )
956 ? $policies_and_conditions
957 : __( 'Returns accepted within 30 days with original receipt. Items must be unused and in original packaging.', 'woocommerce-pos' );
958 $store['footer_imprint'] = ( null !== $footer_imprint && '' !== $footer_imprint )
959 ? $footer_imprint
960 : __( 'Thank you for your purchase!', 'woocommerce-pos' );
961
962 return $store;
963 }
964
965
966 /**
967 * Resolve the currency code for the preview, preferring the store's
968 * configured currency over the global WooCommerce default. Sample-data
969 * previews should reflect what the chosen store will charge.
970 *
971 * @return string ISO 4217 currency code (e.g. "USD", "EUR").
972 */
973 private function resolve_currency(): string {
974 $fallback = get_option( 'woocommerce_currency', 'USD' );
975 $store_currency = (string) $this->store_resolver->get_store_value( 'get_currency', '' );
976
977 return '' !== $store_currency ? $store_currency : $fallback;
978 }
979
980
981 /**
982 * Get cashier data from the current logged-in user.
983 *
984 * @return array Cashier data with id and name.
985 */
986 private function get_cashier(): array {
987 $current_user = wp_get_current_user();
988
989 if ( $current_user->exists() ) {
990 return array(
991 'id' => $current_user->ID,
992 'name' => $current_user->display_name,
993 );
994 }
995
996 return array(
997 'id' => 0,
998 'name' => 'Jane Smith',
999 );
1000 }
1001
1002 /**
1003 * Build a locale-aware sample customer using the store's country settings.
1004 *
1005 * Reads woocommerce_default_country (format "CC" or "CC:SS") to select a
1006 * matching entry from SAMPLE_CUSTOMERS, falling back to the US entry for
1007 * unknown countries. City, state, postcode, and country are filled from
1008 * the store's WooCommerce settings so the preview reflects real store data.
1009 *
1010 * @return array Customer data with id, name, billing_address, shipping_address, and tax_id.
1011 */
1012 private function get_customer(): array {
1013 $raw = get_option( 'woocommerce_default_country', 'US' );
1014 $parts = explode( ':', $raw );
1015 $country = $parts[0] ?? 'US';
1016 $state = $parts[1] ?? '';
1017
1018 $sample = self::SAMPLE_CUSTOMERS[ $country ] ?? self::SAMPLE_CUSTOMERS['US'];
1019
1020 $city = get_option( 'woocommerce_store_city', '' );
1021 $postcode = get_option( 'woocommerce_store_postcode', '' );
1022
1023 $billing = array(
1024 'first_name' => $sample['first_name'],
1025 'last_name' => $sample['last_name'],
1026 'company' => '',
1027 'address_1' => $sample['address_1'],
1028 'address_2' => $sample['address_2'],
1029 'city' => $city,
1030 'state' => $state,
1031 'postcode' => $postcode,
1032 'country' => $country,
1033 'email' => $sample['email'],
1034 'phone' => $sample['phone'],
1035 );
1036
1037 $shipping = array(
1038 'first_name' => $sample['first_name'],
1039 'last_name' => $sample['last_name'],
1040 'company' => '',
1041 'address_1' => $sample['address_1'],
1042 'address_2' => $sample['address_2'],
1043 'city' => $city,
1044 'state' => $state,
1045 'postcode' => $postcode,
1046 'country' => $country,
1047 );
1048
1049 $tax_ids = self::sample_tax_ids_for_country( $country, $this->store_resolver->resolve_locale() );
1050
1051 return array(
1052 'id' => 42,
1053 'name' => $sample['first_name'] . ' ' . $sample['last_name'],
1054 'billing_address' => $billing,
1055 'shipping_address' => $shipping,
1056 'tax_ids' => $tax_ids,
1057 );
1058 }
1059
1060 /**
1061 * Sample tax ID list for the given country, used by the receipt preview.
1062 *
1063 * Values are realistic-shape but fictitious. Returns an empty array for
1064 * countries without a representative sample.
1065 *
1066 * @param string $country ISO 3166-1 alpha-2 country code.
1067 * @param string $locale Receipt locale.
1068 *
1069 * @return array<int,array<string,mixed>>
1070 */
1071 private static function sample_tax_ids_for_country( string $country, string $locale = '' ): array {
1072 $country = strtoupper( $country );
1073
1074 $samples = array(
1075 'GB' => array(
1076 array(
1077 'type' => Tax_Id_Types::TYPE_GB_VAT,
1078 'value' => 'GB123456789',
1079 'country' => 'GB',
1080 ),
1081 ),
1082 'DE' => array(
1083 array(
1084 'type' => Tax_Id_Types::TYPE_EU_VAT,
1085 'value' => 'DE123456789',
1086 'country' => 'DE',
1087 ),
1088 ),
1089 'FR' => array(
1090 array(
1091 'type' => Tax_Id_Types::TYPE_EU_VAT,
1092 'value' => 'FR12345678901',
1093 'country' => 'FR',
1094 ),
1095 ),
1096 'NL' => array(
1097 array(
1098 'type' => Tax_Id_Types::TYPE_EU_VAT,
1099 'value' => 'NL123456789B01',
1100 'country' => 'NL',
1101 ),
1102 ),
1103 'ES' => array(
1104 array(
1105 'type' => Tax_Id_Types::TYPE_ES_NIF,
1106 'value' => 'B12345678',
1107 'country' => 'ES',
1108 ),
1109 ),
1110 'IT' => array(
1111 array(
1112 'type' => Tax_Id_Types::TYPE_IT_PIVA,
1113 'value' => '12345678901',
1114 'country' => 'IT',
1115 ),
1116 ),
1117 'AU' => array(
1118 array(
1119 'type' => Tax_Id_Types::TYPE_AU_ABN,
1120 'value' => '53004085616',
1121 'country' => 'AU',
1122 ),
1123 ),
1124 'CA' => array(
1125 array(
1126 'type' => Tax_Id_Types::TYPE_CA_GST_HST,
1127 'value' => '123456789RT0001',
1128 'country' => 'CA',
1129 ),
1130 ),
1131 'IN' => array(
1132 array(
1133 'type' => Tax_Id_Types::TYPE_IN_GST,
1134 'value' => '29ABCDE1234F1Z5',
1135 'country' => 'IN',
1136 ),
1137 ),
1138 'BR' => array(
1139 array(
1140 'type' => Tax_Id_Types::TYPE_BR_CPF,
1141 'value' => '12345678909',
1142 'country' => 'BR',
1143 ),
1144 ),
1145 'US' => array(
1146 array(
1147 'type' => Tax_Id_Types::TYPE_US_EIN,
1148 'value' => '12-3456789',
1149 'country' => 'US',
1150 ),
1151 ),
1152 );
1153
1154 $tax_ids = $samples[ $country ] ?? array();
1155 $labels = Receipt_I18n_Labels::get_labels( $locale );
1156
1157 return array_map(
1158 static function ( array $tax_id ) use ( $labels ): array {
1159 $type = (string) $tax_id['type'];
1160 $tax_id['label'] = $labels[ 'customer_tax_id_label_' . $type ] ?? $labels['customer_tax_id_label_other'];
1161
1162 return $tax_id;
1163 },
1164 $tax_ids
1165 );
1166 }
1167
1168 /**
1169 * Get product data for line items.
1170 *
1171 * Queries up to 3 published simple/variable products from the catalog.
1172 * Falls back to hardcoded product definitions if the catalog is empty.
1173 * Pads the result to at least MIN_LINES items.
1174 *
1175 * @return array[] Array of product arrays with name, price, and sku keys.
1176 */
1177 private function get_products(): array {
1178 $products = wc_get_products(
1179 array(
1180 'status' => 'publish',
1181 'type' => array( 'simple', 'variable' ),
1182 'limit' => 3,
1183 'return' => 'objects',
1184 )
1185 );
1186
1187 $result = array();
1188
1189 if ( ! empty( $products ) ) {
1190 foreach ( $products as $product ) {
1191 $meta = array();
1192
1193 if ( $product->is_type( 'variable' ) ) {
1194 // Use the first visible variation's attributes (excludes hidden/disabled).
1195 $children = $product->get_visible_children();
1196 if ( ! empty( $children ) ) {
1197 $variation = wc_get_product( $children[0] );
1198 if ( $variation instanceof \WC_Product_Variation ) {
1199 $product = $variation;
1200 foreach ( $variation->get_variation_attributes() as $attr_key => $attr_value ) {
1201 if ( '' !== $attr_value ) {
1202 $taxonomy = str_replace( 'attribute_', '', $attr_key );
1203 $label = wc_attribute_label( $taxonomy, $variation );
1204 $value = $variation->get_attribute( $taxonomy );
1205 $meta[] = array(
1206 'key' => wp_strip_all_tags( $label ),
1207 'value' => wp_strip_all_tags( '' !== $value ? $value : $attr_value ),
1208 );
1209 }
1210 }
1211 }
1212 }
1213 }
1214
1215 $price = (float) $product->get_price();
1216 if ( $price <= 0 ) {
1217 $price = 19.99;
1218 }
1219 $regular_price = (float) $product->get_regular_price();
1220 if ( $regular_price <= 0 ) {
1221 $regular_price = $price;
1222 }
1223
1224 $result[] = array(
1225 'name' => $product->get_name(),
1226 'price' => $price,
1227 'regular_price' => $regular_price,
1228 'sku' => $product->get_sku() ? $product->get_sku() : '',
1229 'meta' => $meta,
1230 'attributes' => $meta,
1231 );
1232 }
1233 }
1234
1235 // Fall back to hardcoded products if catalog is empty.
1236 if ( empty( $result ) ) {
1237 $result = self::FALLBACK_PRODUCTS;
1238 }
1239
1240 // Pad to at least MIN_LINES items.
1241 $fallback_index = 0;
1242 $fallback_count = count( self::FALLBACK_PRODUCTS );
1243 $result_count = count( $result );
1244 while ( $result_count < self::MIN_LINES ) {
1245 $result[] = self::FALLBACK_PRODUCTS[ $fallback_index % $fallback_count ];
1246 ++$fallback_index;
1247 ++$result_count;
1248 }
1249
1250 $result = array_slice( $result, 0, 3 );
1251
1252 // Preview data intentionally demonstrates the savings fields even when
1253 // the store's sampled catalog products are all at regular price.
1254 if ( isset( $result[0] ) && (float) $result[0]['regular_price'] <= (float) $result[0]['price'] ) {
1255 $result[0]['regular_price'] = (float) $result[0]['price'] + 5.0;
1256 }
1257
1258 return $result;
1259 }
1260
1261 /**
1262 * Get tax configuration from WooCommerce tax rates.
1263 *
1264 * Uses WC_Tax::find_rates() with the store's base location to find
1265 * the primary tax rate. Falls back to a default rate if no rates
1266 * are configured.
1267 *
1268 * @param bool $tax_enabled Whether taxes are enabled for this store.
1269 *
1270 * @return array Tax config with rate (float), label (string), and code (string).
1271 */
1272
1273 /**
1274 * Get tax configuration from WooCommerce tax rates.
1275 *
1276 * Uses WC_Tax::find_rates() with the store's base location to find
1277 * the primary tax rate. Falls back to a default rate if no rates
1278 * are configured.
1279 *
1280 * @param bool $tax_enabled Whether taxes are enabled for this store.
1281 *
1282 * @return array Tax config with rate (float), label (string), and code (string).
1283 */
1284 private function get_tax_config( bool $tax_enabled ): array {
1285 if ( ! $tax_enabled ) {
1286 return array(
1287 'rate' => 0.0,
1288 'label' => '',
1289 'code' => '',
1290 );
1291 }
1292
1293 $default = array(
1294 'rate' => self::FALLBACK_TAX_RATE,
1295 'label' => /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Tax', 'woocommerce-pos' ),
1296 'code' => '1',
1297 );
1298
1299 $raw = get_option( 'woocommerce_default_country', '' );
1300 $parts = explode( ':', $raw );
1301 $country = $parts[0] ?? '';
1302 $state = $parts[1] ?? '';
1303
1304 $rates = \WC_Tax::find_rates(
1305 array(
1306 'country' => $country,
1307 'state' => $state,
1308 'postcode' => get_option( 'woocommerce_store_postcode', '' ),
1309 'city' => get_option( 'woocommerce_store_city', '' ),
1310 'tax_class' => '',
1311 )
1312 );
1313
1314 if ( ! empty( $rates ) ) {
1315 $first_rate = reset( $rates );
1316 $rate_id = key( $rates );
1317
1318 return array(
1319 'rate' => (float) $first_rate['rate'],
1320 'label' => $first_rate['label'] ? $first_rate['label'] : /* translators: Sample receipt label or value used in receipt template previews. */ __( 'Tax', 'woocommerce-pos' ),
1321 'code' => (string) $rate_id,
1322 );
1323 }
1324
1325 return $default;
1326 }
1327 }
1328