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 / Receipt_Data_Schema.php

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

1,656 lines 68.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Receipt data schema constants.
4 *
5 * @package WCPOS\WooCommercePOS\Services
6 */
7
8 namespace WCPOS\WooCommercePOS\Services;
9
10 /**
11 * Receipt_Data_Schema class.
12 */
13 class Receipt_Data_Schema {
14 /**
15 * Top-level keys required in a receipt payload.
16 *
17 * `presentation_hints` is required but internal: builders use it for
18 * formatting inputs such as locale, timezone, currency placement, and
19 * decimal separators. Template-facing mode signals belong in subject
20 * sections like `tax` when they are data-derived, not freely choosable by
21 * the template author, and required for logicless template branching.
22 */
23 const REQUIRED_KEYS = array(
24 'order',
25 'store',
26 'cashier',
27 'customer',
28 'lines',
29 'fees',
30 'shipping',
31 'discounts',
32 'totals',
33 'tax',
34 'tax_summary',
35 'has_tax_summary',
36 'payments',
37 'refunds',
38 'fiscal',
39 'presentation_hints',
40 'i18n',
41 );
42
43 /**
44 * Receipt data schema contract version.
45 */
46 const SCHEMA_VERSION = '1.1.0';
47
48 /**
49 * Money keys that must be present in totals.
50 */
51 const TOTAL_MONEY_KEYS = array(
52 'subtotal',
53 'subtotal_incl',
54 'subtotal_excl',
55 'discount_total',
56 'discount_total_incl',
57 'discount_total_excl',
58 'sale_savings_total',
59 'sale_savings_total_incl',
60 'sale_savings_total_excl',
61 'total_saved',
62 'total_saved_incl',
63 'total_saved_excl',
64 'tax_total',
65 'total',
66 'total_incl',
67 'total_excl',
68 'paid_total',
69 'change_total',
70 'refund_total',
71 'net_total',
72 );
73
74 /**
75 * Money fields where a zero value should remain numeric 0 (Mustache-falsy)
76 * so that section guards like {{#change}}...{{/change}} treat them as empty.
77 *
78 * All other money fields are always formatted to a currency string,
79 * including when their value is zero (e.g. "$0.00").
80 */
81 const ZERO_FALSY_MONEY_FIELDS = array(
82 'change',
83 'tendered',
84 'discounts',
85 'discounts_incl',
86 'discounts_excl',
87 'unit_savings',
88 'unit_savings_incl',
89 'unit_savings_excl',
90 'line_savings',
91 'line_savings_incl',
92 'line_savings_excl',
93 'change_total',
94 'discount_total',
95 'discount_total_incl',
96 'discount_total_excl',
97 'sale_savings_total',
98 'sale_savings_total_incl',
99 'sale_savings_total_excl',
100 'total_saved',
101 'total_saved_incl',
102 'total_saved_excl',
103 'refund_total',
104 'total_refunded',
105 );
106
107 /**
108 * Field names (terminal key segment) that represent money values.
109 *
110 * Used by the logicless renderer to auto-format currency output.
111 * Matched against the last segment of dot-path keys during substitution.
112 */
113 const MONEY_FIELDS = array(
114 // Line items (display).
115 'regular_price',
116 'selling_price',
117 'unit_savings',
118 'line_regular_total',
119 'line_selling_total',
120 'line_savings',
121 'unit_subtotal',
122 'unit_price',
123 'line_subtotal',
124 'discounts',
125 'line_total',
126 // Line items (explicit incl/excl).
127 'unit_subtotal_incl',
128 'unit_subtotal_excl',
129 'unit_price_incl',
130 'unit_price_excl',
131 'line_subtotal_incl',
132 'line_subtotal_excl',
133 'discounts_incl',
134 'discounts_excl',
135 'line_total_incl',
136 'line_total_excl',
137 'regular_price_incl',
138 'regular_price_excl',
139 'selling_price_incl',
140 'selling_price_excl',
141 'unit_savings_incl',
142 'unit_savings_excl',
143 'line_regular_total_incl',
144 'line_regular_total_excl',
145 'line_selling_total_incl',
146 'line_selling_total_excl',
147 'line_savings_incl',
148 'line_savings_excl',
149 // Shared by fees / shipping / discounts arrays AND by totals — same key
150 // names appear in both contexts; listing once is enough since the
151 // `array_flip` lookup uses values as keys.
152 'total',
153 'total_incl',
154 'total_excl',
155 // Totals (display + explicit incl/excl).
156 'subtotal',
157 'subtotal_incl',
158 'subtotal_excl',
159 'discount_total',
160 'discount_total_incl',
161 'discount_total_excl',
162 'sale_savings_total',
163 'sale_savings_total_incl',
164 'sale_savings_total_excl',
165 'total_saved',
166 'total_saved_incl',
167 'total_saved_excl',
168 'tax_total',
169 'paid_total',
170 'change_total',
171 'refund_total',
172 'net_total',
173 // Per-line refund.
174 'total_refunded',
175 // Refund-level totals.
176 'shipping_total',
177 'shipping_tax',
178 // Tax summary.
179 'taxable_amount_excl',
180 'tax_amount',
181 'taxable_amount_incl',
182 // Payments.
183 'amount',
184 'tendered',
185 'change',
186 );
187
188 /**
189 * Format money fields in receipt data using wc_price().
190 *
191 * Recursively walks the data structure and replaces numeric values
192 * whose terminal key matches a known money field with a formatted
193 * currency string (e.g. "$29.99").
194 *
195 * @param array $data Receipt data array (or nested sub-array).
196 * @param string $currency WooCommerce currency code.
197 * @param array<string,mixed> $presentation_hints Receipt presentation hints.
198 *
199 * @return array Data with money fields formatted as strings.
200 */
201 public static function format_money_fields( array $data, string $currency = 'USD', array $presentation_hints = array() ): array {
202 static $lookup = null;
203 static $zero_falsy = null;
204 if ( null === $lookup ) {
205 $lookup = array_flip( self::MONEY_FIELDS );
206 $zero_falsy = array_flip( self::ZERO_FALSY_MONEY_FIELDS );
207 }
208
209 if ( empty( $presentation_hints ) && isset( $data['presentation_hints'] ) && \is_array( $data['presentation_hints'] ) ) {
210 $presentation_hints = $data['presentation_hints'];
211 }
212
213 $price_args = self::get_price_format_args( $currency, $presentation_hints );
214 $result = array();
215
216 foreach ( $data as $k => $value ) {
217 if ( \is_array( $value ) ) {
218 $result[ $k ] = self::format_money_fields( $value, $currency, $presentation_hints );
219 } elseif ( is_numeric( $value ) && isset( $lookup[ $k ] ) ) {
220 // ──────────────────────────────────────────────────────────
221 // Money fields: KEEP the numeric value at the bare key and
222 // ADD a `<key>_display` companion holding the locale-formatted
223 // currency string. Mirrors the JS `formatReceiptData` shape
224 // exactly so a single Mustache template renders the same way
225 // in both the studio (JS) and production print (PHP).
226 //
227 // DO NOT change this back to in-place replacement — bare keys
228 // must be numeric so app code can still do math on them, and
229 // templates have a stable `_display` variant to render
230 // currency without re-implementing wc_price() in JS.
231 // ──────────────────────────────────────────────────────────
232 $numeric = (float) $value;
233 $is_zero_falsy = ( 0.0 === $numeric && isset( $zero_falsy[ $k ] ) );
234 // Conditional-display fields (change, tendered, discounts, etc.) keep
235 // the bare key as integer 0 when the value is zero so Mustache section
236 // guards (`{{#change}}…{{/change}}`) treat them as falsy and the rest
237 // of the test suite's `assertSame( 0, … )` checks stay green.
238 $result[ $k ] = $is_zero_falsy ? 0 : $numeric;
239 $result[ $k . '_display' ] = $is_zero_falsy
240 ? ''
241 : html_entity_decode(
242 wp_strip_all_tags(
243 wc_price( $numeric, $price_args )
244 ),
245 ENT_QUOTES | ENT_SUBSTITUTE,
246 'UTF-8'
247 );
248 } else {
249 $result[ $k ] = $value;
250 }
251 }
252
253 return $result;
254 }
255
256 /**
257 * Build wc_price() arguments from presentation hints.
258 *
259 * @param string $currency WooCommerce currency code.
260 * @param array<string,mixed> $presentation_hints Receipt presentation hints.
261 *
262 * @return array<string,mixed>
263 */
264 private static function get_price_format_args( string $currency, array $presentation_hints ): array {
265 $args = array( 'currency' => $currency );
266
267 if ( isset( $presentation_hints['currency_position'] ) ) {
268 $args['price_format'] = self::get_price_format_for_position( (string) $presentation_hints['currency_position'] );
269 }
270 if ( isset( $presentation_hints['price_decimal_separator'] ) ) {
271 $args['decimal_separator'] = (string) $presentation_hints['price_decimal_separator'];
272 }
273 if ( isset( $presentation_hints['price_thousand_separator'] ) ) {
274 $args['thousand_separator'] = (string) $presentation_hints['price_thousand_separator'];
275 }
276 if ( isset( $presentation_hints['price_num_decimals'] ) && is_numeric( $presentation_hints['price_num_decimals'] ) ) {
277 $args['decimals'] = (int) $presentation_hints['price_num_decimals'];
278 }
279
280 return $args;
281 }
282
283 /**
284 * Convert a WooCommerce currency position option to a wc_price format.
285 *
286 * @param string $position WooCommerce currency position.
287 *
288 * @return string wc_price format.
289 */
290 private static function get_price_format_for_position( string $position ): string {
291 switch ( $position ) {
292 case 'right':
293 return '%2$s%1$s';
294 case 'left_space':
295 return '%1$s %2$s';
296 case 'right_space':
297 return '%2$s %1$s';
298 case 'left':
299 default:
300 return '%1$s%2$s';
301 }
302 }
303
304 /**
305 * Get the field tree for the template editor field picker.
306 *
307 * Returns a structured array describing template-picker sections and fields
308 * from the receipt_data contract. Used by the JS field picker sidebar.
309 * Internal sections (e.g. presentation_hints) are intentionally excluded.
310 *
311 * @return array<string, array{label: string, is_array?: bool, fields: array<string, array{type: string, label: string}>}>
312 */
313 public static function get_field_tree(): array {
314 return array(
315 'order' => array(
316 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order', 'woocommerce-pos' ),
317 'fields' => array(
318 'id' => array(
319 'type' => 'number',
320 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order ID', 'woocommerce-pos' ),
321 ),
322 'number' => array(
323 'type' => 'string',
324 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Number', 'woocommerce-pos' ),
325 ),
326 'currency' => array(
327 'type' => 'string',
328 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Currency', 'woocommerce-pos' ),
329 ),
330 'customer_note' => array(
331 'type' => 'string',
332 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Note', 'woocommerce-pos' ),
333 ),
334 'wc_status' => array(
335 'type' => 'string',
336 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'WC Status', 'woocommerce-pos' ),
337 ),
338 'status_label' => array(
339 'type' => 'string',
340 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Status Label', 'woocommerce-pos' ),
341 ),
342 'created_via' => array(
343 'type' => 'string',
344 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Created Via', 'woocommerce-pos' ),
345 ),
346 'needs_payment' => array(
347 'type' => 'boolean',
348 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Needs Payment', 'woocommerce-pos' ),
349 ),
350 'payment_url' => array(
351 'type' => 'string',
352 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payment URL', 'woocommerce-pos' ),
353 ),
354 ),
355 ),
356 'order.created' => array(
357 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Created', 'woocommerce-pos' ),
358 'fields' => self::get_date_field_tree_fields(),
359 ),
360 'order.paid' => array(
361 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Paid', 'woocommerce-pos' ),
362 'fields' => self::get_date_field_tree_fields(),
363 ),
364 'order.completed' => array(
365 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Completed', 'woocommerce-pos' ),
366 'fields' => self::get_date_field_tree_fields(),
367 ),
368 'order.printed' => array(
369 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Receipt Printed', 'woocommerce-pos' ),
370 'fields' => self::get_date_field_tree_fields(),
371 ),
372 'store' => array(
373 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store', 'woocommerce-pos' ),
374 'fields' => array(
375 'id' => array(
376 'type' => 'number',
377 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store ID', 'woocommerce-pos' ),
378 ),
379 'name' => array(
380 'type' => 'string',
381 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Name', 'woocommerce-pos' ),
382 ),
383 'address_lines' => array(
384 'type' => 'string[]',
385 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address Lines', 'woocommerce-pos' ),
386 ),
387 'phone' => array(
388 'type' => 'string',
389 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Phone', 'woocommerce-pos' ),
390 ),
391 'email' => array(
392 'type' => 'string',
393 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Email', 'woocommerce-pos' ),
394 ),
395 'logo' => array(
396 'type' => 'string',
397 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Logo URL', 'woocommerce-pos' ),
398 ),
399 'opening_hours' => array(
400 'type' => 'string',
401 'nullable' => true,
402 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours', 'woocommerce-pos' ),
403 ),
404 'opening_hours_vertical' => array(
405 'type' => 'string',
406 'nullable' => true,
407 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours (Vertical)', 'woocommerce-pos' ),
408 ),
409 'opening_hours_inline' => array(
410 'type' => 'string',
411 'nullable' => true,
412 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours (Inline)', 'woocommerce-pos' ),
413 ),
414 'opening_hours_notes' => array(
415 'type' => 'string',
416 'nullable' => true,
417 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours Notes', 'woocommerce-pos' ),
418 ),
419 'personal_notes' => array(
420 'type' => 'string',
421 'nullable' => true,
422 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Personal Notes', 'woocommerce-pos' ),
423 ),
424 'policies_and_conditions' => array(
425 'type' => 'string',
426 'nullable' => true,
427 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Policies & Conditions', 'woocommerce-pos' ),
428 ),
429 'footer_imprint' => array(
430 'type' => 'string',
431 'nullable' => true,
432 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Footer Imprint', 'woocommerce-pos' ),
433 ),
434 ),
435 ),
436 'cashier' => array(
437 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier', 'woocommerce-pos' ),
438 'fields' => array(
439 'id' => array(
440 'type' => 'number',
441 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier ID', 'woocommerce-pos' ),
442 ),
443 'name' => array(
444 'type' => 'string',
445 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier Name', 'woocommerce-pos' ),
446 ),
447 ),
448 ),
449 'customer' => array(
450 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer', 'woocommerce-pos' ),
451 'fields' => array(
452 'id' => array(
453 'type' => 'number',
454 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer ID', 'woocommerce-pos' ),
455 ),
456 'name' => array(
457 'type' => 'string',
458 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Name', 'woocommerce-pos' ),
459 ),
460 'billing_address' => array(
461 'type' => 'object',
462 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Billing Address', 'woocommerce-pos' ),
463 ),
464 'shipping_address' => array(
465 'type' => 'object',
466 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Address', 'woocommerce-pos' ),
467 ),
468 ),
469 ),
470 'customer.tax_ids' => array(
471 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Tax IDs', 'woocommerce-pos' ),
472 'is_array' => true,
473 'fields' => array(
474 'type' => array(
475 'type' => 'string',
476 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Type', 'woocommerce-pos' ),
477 ),
478 'value' => array(
479 'type' => 'string',
480 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ),
481 ),
482 'country' => array(
483 'type' => 'string',
484 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ),
485 ),
486 'label' => array(
487 'type' => 'string',
488 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ),
489 ),
490 ),
491 ),
492 'store.tax_ids' => array(
493 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Tax IDs', 'woocommerce-pos' ),
494 'is_array' => true,
495 'fields' => array(
496 'type' => array(
497 'type' => 'string',
498 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Type', 'woocommerce-pos' ),
499 ),
500 'value' => array(
501 'type' => 'string',
502 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ),
503 ),
504 'country' => array(
505 'type' => 'string',
506 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ),
507 ),
508 'label' => array(
509 'type' => 'string',
510 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ),
511 ),
512 ),
513 ),
514 'store.address' => array(
515 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Address', 'woocommerce-pos' ),
516 'fields' => array(
517 'address_1' => array(
518 'type' => 'string',
519 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address line 1', 'woocommerce-pos' ),
520 ),
521 'address_2' => array(
522 'type' => 'string',
523 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address line 2', 'woocommerce-pos' ),
524 ),
525 'city' => array(
526 'type' => 'string',
527 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'City', 'woocommerce-pos' ),
528 ),
529 'state' => array(
530 'type' => 'string',
531 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'State / Region', 'woocommerce-pos' ),
532 ),
533 'postcode' => array(
534 'type' => 'string',
535 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Postcode', 'woocommerce-pos' ),
536 ),
537 'country' => array(
538 'type' => 'string',
539 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ),
540 ),
541 ),
542 ),
543 'lines' => array(
544 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Items', 'woocommerce-pos' ),
545 'is_array' => true,
546 'fields' => array(
547 'key' => array(
548 'type' => 'string',
549 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Item Key', 'woocommerce-pos' ),
550 ),
551 'sku' => array(
552 'type' => 'string',
553 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'SKU', 'woocommerce-pos' ),
554 ),
555 'name' => array(
556 'type' => 'string',
557 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Name', 'woocommerce-pos' ),
558 ),
559 'qty' => array(
560 'type' => 'number',
561 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity', 'woocommerce-pos' ),
562 ),
563 'qty_refunded' => array(
564 'type' => 'number',
565 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity Refunded', 'woocommerce-pos' ),
566 ),
567 'regular_price' => array(
568 'type' => 'money',
569 'nullable' => true,
570 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item', 'woocommerce-pos' ),
571 ),
572 'regular_price_incl' => array(
573 'type' => 'money',
574 'nullable' => true,
575 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item (incl tax)', 'woocommerce-pos' ),
576 ),
577 'regular_price_excl' => array(
578 'type' => 'money',
579 'nullable' => true,
580 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item (excl tax)', 'woocommerce-pos' ),
581 ),
582 'selling_price' => array(
583 'type' => 'money',
584 'nullable' => true,
585 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons', 'woocommerce-pos' ),
586 ),
587 'selling_price_incl' => array(
588 'type' => 'money',
589 'nullable' => true,
590 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons (incl tax)', 'woocommerce-pos' ),
591 ),
592 'selling_price_excl' => array(
593 'type' => 'money',
594 'nullable' => true,
595 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons (excl tax)', 'woocommerce-pos' ),
596 ),
597 'unit_savings' => array(
598 'type' => 'money',
599 'nullable' => true,
600 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item', 'woocommerce-pos' ),
601 ),
602 'unit_savings_incl' => array(
603 'type' => 'money',
604 'nullable' => true,
605 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item (incl tax)', 'woocommerce-pos' ),
606 ),
607 'unit_savings_excl' => array(
608 'type' => 'money',
609 'nullable' => true,
610 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item (excl tax)', 'woocommerce-pos' ),
611 ),
612 'line_regular_total' => array(
613 'type' => 'money',
614 'nullable' => true,
615 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total', 'woocommerce-pos' ),
616 ),
617 'line_regular_total_incl' => array(
618 'type' => 'money',
619 'nullable' => true,
620 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total (incl tax)', 'woocommerce-pos' ),
621 ),
622 'line_regular_total_excl' => array(
623 'type' => 'money',
624 'nullable' => true,
625 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total (excl tax)', 'woocommerce-pos' ),
626 ),
627 'line_selling_total' => array(
628 'type' => 'money',
629 'nullable' => true,
630 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons', 'woocommerce-pos' ),
631 ),
632 'line_selling_total_incl' => array(
633 'type' => 'money',
634 'nullable' => true,
635 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons (incl tax)', 'woocommerce-pos' ),
636 ),
637 'line_selling_total_excl' => array(
638 'type' => 'money',
639 'nullable' => true,
640 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons (excl tax)', 'woocommerce-pos' ),
641 ),
642 'line_savings' => array(
643 'type' => 'money',
644 'nullable' => true,
645 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item', 'woocommerce-pos' ),
646 ),
647 'line_savings_incl' => array(
648 'type' => 'money',
649 'nullable' => true,
650 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item (incl tax)', 'woocommerce-pos' ),
651 ),
652 'line_savings_excl' => array(
653 'type' => 'money',
654 'nullable' => true,
655 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item (excl tax)', 'woocommerce-pos' ),
656 ),
657 'savings_in_discounts' => array(
658 'type' => 'boolean',
659 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings already included in WooCommerce discounts', 'woocommerce-pos' ),
660 ),
661 'unit_subtotal' => array(
662 'type' => 'money',
663 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal', 'woocommerce-pos' ),
664 ),
665 'unit_subtotal_incl' => array(
666 'type' => 'money',
667 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal (incl tax)', 'woocommerce-pos' ),
668 ),
669 'unit_subtotal_excl' => array(
670 'type' => 'money',
671 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal (excl tax)', 'woocommerce-pos' ),
672 ),
673 'unit_price' => array(
674 'type' => 'money',
675 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price', 'woocommerce-pos' ),
676 ),
677 'unit_price_incl' => array(
678 'type' => 'money',
679 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price (incl tax)', 'woocommerce-pos' ),
680 ),
681 'unit_price_excl' => array(
682 'type' => 'money',
683 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price (excl tax)', 'woocommerce-pos' ),
684 ),
685 'line_subtotal' => array(
686 'type' => 'money',
687 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal', 'woocommerce-pos' ),
688 ),
689 'line_subtotal_incl' => array(
690 'type' => 'money',
691 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (incl tax)', 'woocommerce-pos' ),
692 ),
693 'line_subtotal_excl' => array(
694 'type' => 'money',
695 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (excl tax)', 'woocommerce-pos' ),
696 ),
697 'discounts' => array(
698 'type' => 'money',
699 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts', 'woocommerce-pos' ),
700 ),
701 'discounts_incl' => array(
702 'type' => 'money',
703 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts (incl tax)', 'woocommerce-pos' ),
704 ),
705 'discounts_excl' => array(
706 'type' => 'money',
707 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts (excl tax)', 'woocommerce-pos' ),
708 ),
709 'line_total' => array(
710 'type' => 'money',
711 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total', 'woocommerce-pos' ),
712 ),
713 'line_total_incl' => array(
714 'type' => 'money',
715 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total (incl tax)', 'woocommerce-pos' ),
716 ),
717 'line_total_excl' => array(
718 'type' => 'money',
719 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total (excl tax)', 'woocommerce-pos' ),
720 ),
721 'total_refunded' => array(
722 'type' => 'money',
723 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Refunded', 'woocommerce-pos' ),
724 ),
725 'taxes' => array(
726 'type' => 'array',
727 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Taxes', 'woocommerce-pos' ),
728 ),
729 'meta' => array(
730 'type' => 'array',
731 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Item Meta', 'woocommerce-pos' ),
732 ),
733 'attributes' => array(
734 'type' => 'array',
735 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Attributes', 'woocommerce-pos' ),
736 ),
737 ),
738 ),
739 'fees' => array(
740 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fees', 'woocommerce-pos' ),
741 'is_array' => true,
742 'fields' => array(
743 'label' => array(
744 'type' => 'string',
745 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Label', 'woocommerce-pos' ),
746 ),
747 'total' => array(
748 'type' => 'money',
749 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
750 ),
751 'total_incl' => array(
752 'type' => 'money',
753 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ),
754 ),
755 'total_excl' => array(
756 'type' => 'money',
757 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ),
758 ),
759 'taxes' => array(
760 'type' => 'array',
761 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Taxes', 'woocommerce-pos' ),
762 ),
763 'meta' => array(
764 'type' => 'array',
765 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Meta', 'woocommerce-pos' ),
766 ),
767 ),
768 ),
769 'shipping' => array(
770 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping', 'woocommerce-pos' ),
771 'is_array' => true,
772 'fields' => array(
773 'label' => array(
774 'type' => 'string',
775 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Label', 'woocommerce-pos' ),
776 ),
777 'method_id' => array(
778 'type' => 'string',
779 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Method ID', 'woocommerce-pos' ),
780 ),
781 'total' => array(
782 'type' => 'money',
783 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
784 ),
785 'total_incl' => array(
786 'type' => 'money',
787 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ),
788 ),
789 'total_excl' => array(
790 'type' => 'money',
791 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ),
792 ),
793 'taxes' => array(
794 'type' => 'array',
795 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Taxes', 'woocommerce-pos' ),
796 ),
797 'meta' => array(
798 'type' => 'array',
799 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Meta', 'woocommerce-pos' ),
800 ),
801 ),
802 ),
803 'discounts' => array(
804 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts', 'woocommerce-pos' ),
805 'is_array' => true,
806 'fields' => array(
807 'label' => array(
808 'type' => 'string',
809 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Label', 'woocommerce-pos' ),
810 ),
811 'code' => array(
812 'type' => 'string',
813 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Coupon Code', 'woocommerce-pos' ),
814 ),
815 'total' => array(
816 'type' => 'money',
817 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
818 ),
819 'total_incl' => array(
820 'type' => 'money',
821 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ),
822 ),
823 'total_excl' => array(
824 'type' => 'money',
825 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ),
826 ),
827 ),
828 ),
829 'totals' => array(
830 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Totals', 'woocommerce-pos' ),
831 'fields' => array(
832 'subtotal' => array(
833 'type' => 'money',
834 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal', 'woocommerce-pos' ),
835 ),
836 'subtotal_incl' => array(
837 'type' => 'money',
838 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (incl tax)', 'woocommerce-pos' ),
839 ),
840 'subtotal_excl' => array(
841 'type' => 'money',
842 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (excl tax)', 'woocommerce-pos' ),
843 ),
844 'discount_total' => array(
845 'type' => 'money',
846 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total', 'woocommerce-pos' ),
847 ),
848 'discount_total_incl' => array(
849 'type' => 'money',
850 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (incl tax)', 'woocommerce-pos' ),
851 ),
852 'discount_total_excl' => array(
853 'type' => 'money',
854 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (excl tax)', 'woocommerce-pos' ),
855 ),
856 'sale_savings_total' => array(
857 'type' => 'money',
858 'nullable' => true,
859 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total', 'woocommerce-pos' ),
860 ),
861 'sale_savings_total_incl' => array(
862 'type' => 'money',
863 'nullable' => true,
864 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (incl tax)', 'woocommerce-pos' ),
865 ),
866 'sale_savings_total_excl' => array(
867 'type' => 'money',
868 'nullable' => true,
869 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (excl tax)', 'woocommerce-pos' ),
870 ),
871 'total_saved' => array(
872 'type' => 'money',
873 'nullable' => true,
874 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts)', 'woocommerce-pos' ),
875 ),
876 'total_saved_incl' => array(
877 'type' => 'money',
878 'nullable' => true,
879 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, incl tax)', 'woocommerce-pos' ),
880 ),
881 'total_saved_excl' => array(
882 'type' => 'money',
883 'nullable' => true,
884 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, excl tax)', 'woocommerce-pos' ),
885 ),
886 'total_saved_complete' => array(
887 'type' => 'boolean',
888 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved Is Complete', 'woocommerce-pos' ),
889 ),
890 'tax_total' => array(
891 'type' => 'money',
892 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Total', 'woocommerce-pos' ),
893 ),
894 'total' => array(
895 'type' => 'money',
896 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
897 ),
898 'total_incl' => array(
899 'type' => 'money',
900 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl. tax)', 'woocommerce-pos' ),
901 ),
902 'total_excl' => array(
903 'type' => 'money',
904 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl. tax)', 'woocommerce-pos' ),
905 ),
906 'paid_total' => array(
907 'type' => 'money',
908 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Paid Total', 'woocommerce-pos' ),
909 ),
910 'change_total' => array(
911 'type' => 'money',
912 'label' => /* translators: Template-editor field label for total cash returned to the customer after payments; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ),
913 ),
914 'refund_total' => array(
915 'type' => 'money',
916 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Total', 'woocommerce-pos' ),
917 ),
918 'net_total' => array(
919 'type' => 'money',
920 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Net Total', 'woocommerce-pos' ),
921 ),
922 'total_qty' => array(
923 'type' => 'number',
924 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Quantity', 'woocommerce-pos' ),
925 ),
926 'line_count' => array(
927 'type' => 'number',
928 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Count', 'woocommerce-pos' ),
929 ),
930 ),
931 ),
932
933 'tax' => array(
934 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax', 'woocommerce-pos' ),
935 'fields' => array(
936 'display' => array(
937 'type' => 'string',
938 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Mode', 'woocommerce-pos' ),
939 ),
940 'display_incl' => array(
941 'type' => 'boolean',
942 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Inclusive', 'woocommerce-pos' ),
943 ),
944 'display_excl' => array(
945 'type' => 'boolean',
946 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Exclusive', 'woocommerce-pos' ),
947 ),
948 'breakdown' => array(
949 'type' => 'string',
950 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Mode', 'woocommerce-pos' ),
951 ),
952 'breakdown_hidden' => array(
953 'type' => 'boolean',
954 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Hidden', 'woocommerce-pos' ),
955 ),
956 'breakdown_single' => array(
957 'type' => 'boolean',
958 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Single', 'woocommerce-pos' ),
959 ),
960 'breakdown_itemized' => array(
961 'type' => 'boolean',
962 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Itemized', 'woocommerce-pos' ),
963 ),
964 ),
965 ),
966 'has_tax_summary' => array(
967 'type' => 'boolean',
968 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Has Tax Summary', 'woocommerce-pos' ),
969 'fields' => array(),
970 ),
971 'tax_summary' => array(
972 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Summary', 'woocommerce-pos' ),
973 'is_array' => true,
974 'fields' => array(
975 'code' => array(
976 'type' => 'string',
977 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Code', 'woocommerce-pos' ),
978 ),
979 'label' => array(
980 'type' => 'string',
981 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Label', 'woocommerce-pos' ),
982 ),
983 'rate' => array(
984 'type' => 'number',
985 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Rate (%)', 'woocommerce-pos' ),
986 ),
987 'compound' => array(
988 'type' => 'boolean',
989 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Compound Tax', 'woocommerce-pos' ),
990 ),
991 'taxable_amount_excl' => array(
992 'type' => 'money',
993 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (excl)', 'woocommerce-pos' ),
994 ),
995 'tax_amount' => array(
996 'type' => 'money',
997 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Amount', 'woocommerce-pos' ),
998 ),
999 'taxable_amount_incl' => array(
1000 'type' => 'money',
1001 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (incl)', 'woocommerce-pos' ),
1002 ),
1003 ),
1004 ),
1005 'payments' => array(
1006 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payments', 'woocommerce-pos' ),
1007 'is_array' => true,
1008 'fields' => array(
1009 'method_id' => array(
1010 'type' => 'string',
1011 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Method ID', 'woocommerce-pos' ),
1012 ),
1013 'method_title' => array(
1014 'type' => 'string',
1015 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payment Method', 'woocommerce-pos' ),
1016 ),
1017 'amount' => array(
1018 'type' => 'money',
1019 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Amount', 'woocommerce-pos' ),
1020 ),
1021 'tendered' => array(
1022 'type' => 'money',
1023 'label' => /* translators: Template-editor field label for the money received from the customer at checkout; use the target-language equivalent of "Received", not procurement tender/bid language. */ __( 'Tendered', 'woocommerce-pos' ),
1024 ),
1025 'change' => array(
1026 'type' => 'money',
1027 'label' => /* translators: Template-editor field label for cash returned to the customer after payment; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ),
1028 ),
1029 'transaction_id' => array(
1030 'type' => 'string',
1031 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Transaction ID', 'woocommerce-pos' ),
1032 ),
1033 ),
1034 ),
1035 'refunds' => array(
1036 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunds', 'woocommerce-pos' ),
1037 'is_array' => true,
1038 'fields' => array(
1039 'id' => array(
1040 'type' => 'number',
1041 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund ID', 'woocommerce-pos' ),
1042 ),
1043 'date' => array(
1044 'type' => 'object',
1045 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Date', 'woocommerce-pos' ),
1046 ),
1047 'amount' => array(
1048 'type' => 'money',
1049 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Amount', 'woocommerce-pos' ),
1050 ),
1051 'subtotal' => array(
1052 'type' => 'money',
1053 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Subtotal', 'woocommerce-pos' ),
1054 ),
1055 'tax_total' => array(
1056 'type' => 'money',
1057 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Tax Total', 'woocommerce-pos' ),
1058 ),
1059 'shipping_total' => array(
1060 'type' => 'money',
1061 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ),
1062 ),
1063 'shipping_tax' => array(
1064 'type' => 'money',
1065 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Tax', 'woocommerce-pos' ),
1066 ),
1067 'reason' => array(
1068 'type' => 'string',
1069 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Reason', 'woocommerce-pos' ),
1070 ),
1071 'refunded_by_id' => array(
1072 'type' => 'number',
1073 'nullable' => true,
1074 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (User ID)', 'woocommerce-pos' ),
1075 ),
1076 'refunded_by_name' => array(
1077 'type' => 'string',
1078 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (Name)', 'woocommerce-pos' ),
1079 ),
1080 'refunded_payment' => array(
1081 'type' => 'boolean',
1082 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded Payment', 'woocommerce-pos' ),
1083 ),
1084 'destination' => array(
1085 'type' => 'string',
1086 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Destination', 'woocommerce-pos' ),
1087 ),
1088 'gateway_id' => array(
1089 'type' => 'string',
1090 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway ID', 'woocommerce-pos' ),
1091 ),
1092 'gateway_title' => array(
1093 'type' => 'string',
1094 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway Title', 'woocommerce-pos' ),
1095 ),
1096 'processing_mode' => array(
1097 'type' => 'string',
1098 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Processing Mode', 'woocommerce-pos' ),
1099 ),
1100 'lines' => array(
1101 'type' => 'array',
1102 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Lines', 'woocommerce-pos' ),
1103 'is_array' => true,
1104 'fields' => array(
1105 'name' => array(
1106 'type' => 'string',
1107 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Name', 'woocommerce-pos' ),
1108 ),
1109 'sku' => array(
1110 'type' => 'string',
1111 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'SKU', 'woocommerce-pos' ),
1112 ),
1113 'qty' => array(
1114 'type' => 'number',
1115 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity', 'woocommerce-pos' ),
1116 ),
1117 'total' => array(
1118 'type' => 'money',
1119 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total', 'woocommerce-pos' ),
1120 ),
1121 'total_incl' => array(
1122 'type' => 'money',
1123 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (incl tax)', 'woocommerce-pos' ),
1124 ),
1125 'total_excl' => array(
1126 'type' => 'money',
1127 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (excl tax)', 'woocommerce-pos' ),
1128 ),
1129 'taxes' => array(
1130 'type' => 'array',
1131 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Taxes', 'woocommerce-pos' ),
1132 ),
1133 ),
1134 ),
1135 'fees' => array(
1136 'type' => 'array',
1137 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fees', 'woocommerce-pos' ),
1138 'is_array' => true,
1139 'fields' => array(
1140 'label' => array(
1141 'type' => 'string',
1142 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Label', 'woocommerce-pos' ),
1143 ),
1144 'total' => array(
1145 'type' => 'money',
1146 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total', 'woocommerce-pos' ),
1147 ),
1148 'total_incl' => array(
1149 'type' => 'money',
1150 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (incl tax)', 'woocommerce-pos' ),
1151 ),
1152 'total_excl' => array(
1153 'type' => 'money',
1154 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (excl tax)', 'woocommerce-pos' ),
1155 ),
1156 'taxes' => array(
1157 'type' => 'array',
1158 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Taxes', 'woocommerce-pos' ),
1159 ),
1160 ),
1161 ),
1162 'shipping' => array(
1163 'type' => 'array',
1164 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping', 'woocommerce-pos' ),
1165 'is_array' => true,
1166 'fields' => array(
1167 'label' => array(
1168 'type' => 'string',
1169 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Label', 'woocommerce-pos' ),
1170 ),
1171 'method_id' => array(
1172 'type' => 'string',
1173 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Method ID', 'woocommerce-pos' ),
1174 ),
1175 'total' => array(
1176 'type' => 'money',
1177 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ),
1178 ),
1179 'total_incl' => array(
1180 'type' => 'money',
1181 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (incl tax)', 'woocommerce-pos' ),
1182 ),
1183 'total_excl' => array(
1184 'type' => 'money',
1185 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (excl tax)', 'woocommerce-pos' ),
1186 ),
1187 'taxes' => array(
1188 'type' => 'array',
1189 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Taxes', 'woocommerce-pos' ),
1190 ),
1191 ),
1192 ),
1193 ),
1194 ),
1195 'fiscal' => array(
1196 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fiscal', 'woocommerce-pos' ),
1197 'fields' => array(
1198 'immutable_id' => array(
1199 'type' => 'string',
1200 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Immutable ID', 'woocommerce-pos' ),
1201 ),
1202 'receipt_number' => array(
1203 'type' => 'string',
1204 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Receipt Number', 'woocommerce-pos' ),
1205 ),
1206 'sequence' => array(
1207 'type' => 'number',
1208 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sequence', 'woocommerce-pos' ),
1209 ),
1210 'hash' => array(
1211 'type' => 'string',
1212 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Hash', 'woocommerce-pos' ),
1213 ),
1214 'qr_payload' => array(
1215 'type' => 'string',
1216 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'QR Payload', 'woocommerce-pos' ),
1217 ),
1218 'tax_agency_code' => array(
1219 'type' => 'string',
1220 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Agency Code', 'woocommerce-pos' ),
1221 ),
1222 'signed_at' => array(
1223 'type' => 'string',
1224 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signed At', 'woocommerce-pos' ),
1225 ),
1226 'signature_excerpt' => array(
1227 'type' => 'string',
1228 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signature Excerpt', 'woocommerce-pos' ),
1229 ),
1230 'document_label' => array(
1231 'type' => 'string',
1232 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Document Label', 'woocommerce-pos' ),
1233 ),
1234 'is_reprint' => array(
1235 'type' => 'boolean',
1236 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Is Reprint', 'woocommerce-pos' ),
1237 ),
1238 'reprint_count' => array(
1239 'type' => 'number',
1240 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Reprint Count', 'woocommerce-pos' ),
1241 ),
1242 'extra_fields' => array(
1243 'type' => 'array',
1244 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Extra Fields', 'woocommerce-pos' ),
1245 'is_array' => true,
1246 'fields' => array(
1247 'label' => array(
1248 'type' => 'string',
1249 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ),
1250 ),
1251 'value' => array(
1252 'type' => 'string',
1253 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ),
1254 ),
1255 ),
1256 ),
1257 ),
1258 ),
1259 'i18n' => array(
1260 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Labels (i18n)', 'woocommerce-pos' ),
1261 'fields' => self::get_i18n_field_tree_fields(),
1262 ),
1263 );
1264 }
1265
1266
1267 /**
1268 * Get field metadata for every canonical i18n receipt label.
1269 *
1270 * @return array<string, array{type: string, label: string}>
1271 */
1272 private static function get_i18n_field_tree_fields(): array {
1273 $fields = array();
1274
1275 foreach ( Receipt_I18n_Labels::get_labels() as $key => $label ) {
1276 $fields[ $key ] = array(
1277 'type' => 'string',
1278 'label' => $label,
1279 );
1280 }
1281
1282 return $fields;
1283 }
1284
1285 /**
1286 * Get field metadata for a semantic date section.
1287 *
1288 * @return array<string, array{type: string, label: string}>
1289 */
1290 private static function get_date_field_tree_fields(): array {
1291 return array(
1292 'datetime' => array(
1293 'type' => 'string',
1294 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date & Time', 'woocommerce-pos' ),
1295 ),
1296 'date' => array(
1297 'type' => 'string',
1298 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date', 'woocommerce-pos' ),
1299 ),
1300 'time' => array(
1301 'type' => 'string',
1302 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Time', 'woocommerce-pos' ),
1303 ),
1304 'datetime_short' => array(
1305 'type' => 'string',
1306 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date & Time', 'woocommerce-pos' ),
1307 ),
1308 'datetime_long' => array(
1309 'type' => 'string',
1310 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date & Time', 'woocommerce-pos' ),
1311 ),
1312 'datetime_full' => array(
1313 'type' => 'string',
1314 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date & Time', 'woocommerce-pos' ),
1315 ),
1316 'date_short' => array(
1317 'type' => 'string',
1318 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date', 'woocommerce-pos' ),
1319 ),
1320 'date_long' => array(
1321 'type' => 'string',
1322 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date', 'woocommerce-pos' ),
1323 ),
1324 'date_full' => array(
1325 'type' => 'string',
1326 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date', 'woocommerce-pos' ),
1327 ),
1328 'date_ymd' => array(
1329 'type' => 'string',
1330 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'YYYY-MM-DD', 'woocommerce-pos' ),
1331 ),
1332 'date_dmy' => array(
1333 'type' => 'string',
1334 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'DD/MM/YYYY', 'woocommerce-pos' ),
1335 ),
1336 'date_mdy' => array(
1337 'type' => 'string',
1338 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'MM/DD/YYYY', 'woocommerce-pos' ),
1339 ),
1340 'weekday_short' => array(
1341 'type' => 'string',
1342 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Short', 'woocommerce-pos' ),
1343 ),
1344 'weekday_long' => array(
1345 'type' => 'string',
1346 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Long', 'woocommerce-pos' ),
1347 ),
1348 'day' => array(
1349 'type' => 'string',
1350 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Day', 'woocommerce-pos' ),
1351 ),
1352 'month' => array(
1353 'type' => 'string',
1354 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Number', 'woocommerce-pos' ),
1355 ),
1356 'month_short' => array(
1357 'type' => 'string',
1358 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Short', 'woocommerce-pos' ),
1359 ),
1360 'month_long' => array(
1361 'type' => 'string',
1362 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Long', 'woocommerce-pos' ),
1363 ),
1364 'year' => array(
1365 'type' => 'string',
1366 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Year', 'woocommerce-pos' ),
1367 ),
1368 );
1369 }
1370
1371 /**
1372 * Get the JSON Schema for canonical receipt_data payloads.
1373 *
1374 * PHP remains the source of truth in this repository. This export is used by
1375 * generated TypeScript artifacts and downstream renderer/studio checks.
1376 *
1377 * @return array<string, mixed> JSON-Schema-compatible receipt data schema.
1378 */
1379 public static function get_json_schema(): array {
1380 $schema = array(
1381 '$schema' => 'https://json-schema.org/draft/2020-12/schema',
1382 '$id' => 'https://wcpos.com/schemas/receipt-data.schema.json',
1383 'x-schema-version' => self::SCHEMA_VERSION,
1384 'title' => 'ReceiptData',
1385 'type' => 'object',
1386 'additionalProperties' => true,
1387 'required' => self::REQUIRED_KEYS,
1388 'properties' => array(),
1389 );
1390
1391 foreach ( self::REQUIRED_KEYS as $key ) {
1392 $schema['properties'][ $key ] = self::get_default_section_schema( $key );
1393 }
1394
1395 foreach ( self::get_field_tree() as $path => $section ) {
1396 self::merge_field_tree_section_schema( $schema, $path, $section );
1397 }
1398
1399 unset( $schema['properties']['presentation_hints']['properties'] );
1400
1401 return $schema;
1402 }
1403
1404 /**
1405 * Get a default schema for a top-level receipt section.
1406 *
1407 * @param string $key Top-level receipt data key.
1408 *
1409 * @return array<string, mixed>
1410 */
1411 private static function get_default_section_schema( string $key ): array {
1412 if ( 'has_tax_summary' === $key ) {
1413 return array(
1414 'type' => 'boolean',
1415 'description' => 'Whether tax_summary contains at least one row.',
1416 );
1417 }
1418
1419 if ( \in_array( $key, array( 'lines', 'fees', 'shipping', 'discounts', 'tax_summary', 'payments', 'refunds' ), true ) ) {
1420 return array(
1421 'type' => 'array',
1422 'items' => array(
1423 'type' => 'object',
1424 'additionalProperties' => true,
1425 'properties' => array(),
1426 ),
1427 'additionalProperties' => true,
1428 );
1429 }
1430
1431 return array(
1432 'type' => 'object',
1433 'additionalProperties' => true,
1434 'properties' => array(),
1435 );
1436 }
1437
1438 /**
1439 * Merge a template editor field-tree section into the JSON Schema.
1440 *
1441 * @param array<string, mixed> $schema Full schema, passed by reference.
1442 * @param string $path Dot path for the field-tree section.
1443 * @param array<string, mixed> $section Field-tree section metadata.
1444 *
1445 * @return void
1446 */
1447 private static function merge_field_tree_section_schema( array &$schema, string $path, array $section ): void {
1448 $segments = explode( '.', $path );
1449 $top_key = array_shift( $segments );
1450
1451 if ( ! \is_string( $top_key ) || '' === $top_key ) {
1452 return;
1453 }
1454
1455 if ( ! isset( $schema['properties'][ $top_key ] ) ) {
1456 $schema['properties'][ $top_key ] = self::get_default_section_schema( $top_key );
1457 }
1458
1459 $target =& $schema['properties'][ $top_key ];
1460 $target_is_collection_item = false;
1461
1462 if ( 'array' === ( $target['type'] ?? null ) ) {
1463 $target =& $target['items'];
1464 $target_is_collection_item = true;
1465 }
1466
1467 foreach ( $segments as $segment ) {
1468 if ( ! isset( $target['properties'][ $segment ] ) ) {
1469 $target['properties'][ $segment ] = array(
1470 'type' => 'object',
1471 'additionalProperties' => true,
1472 'properties' => array(),
1473 );
1474 }
1475 $target =& $target['properties'][ $segment ];
1476 }
1477
1478 $target['description'] = isset( $section['label'] ) ? (string) $section['label'] : $path;
1479
1480 if ( isset( $section['type'] ) && empty( $section['fields'] ) && empty( $segments ) ) {
1481 $target = self::field_metadata_to_json_schema( $section );
1482 return;
1483 }
1484
1485 if ( ! empty( $section['is_array'] ) && ! $target_is_collection_item ) {
1486 $target['type'] = 'array';
1487 $target['items'] = $target['items'] ?? array(
1488 'type' => 'object',
1489 'additionalProperties' => true,
1490 'properties' => array(),
1491 );
1492
1493 // Strip object-only keywords that may have been seeded by the
1494 // segment-walk above; they are invalid on an array schema.
1495 unset( $target['additionalProperties'], $target['properties'] );
1496 } else {
1497 $target['type'] = 'object';
1498 $target['additionalProperties'] = true;
1499 $target['properties'] = $target['properties'] ?? array();
1500 }
1501
1502 $field_target =& $target;
1503 if ( 'array' === ( $target['type'] ?? null ) ) {
1504 $field_target =& $target['items'];
1505 }
1506
1507 foreach ( $section['fields'] ?? array() as $field_name => $field ) {
1508 $field_target['properties'][ $field_name ] = self::field_metadata_to_json_schema( $field );
1509 }
1510 }
1511
1512 /**
1513 * Convert one field-tree field to a JSON Schema property.
1514 *
1515 * @param array<string,mixed> $field Field metadata.
1516 *
1517 * @return array<string,mixed>
1518 */
1519 private static function field_metadata_to_json_schema( array $field ): array {
1520 $type = isset( $field['type'] ) ? (string) $field['type'] : 'string';
1521 $schema_type = self::field_type_to_json_type( $type );
1522
1523 // Honor a nullable flag from the field-tree metadata: producers may
1524 // legitimately emit null for a field whose JSON Schema type would
1525 // otherwise reject it (e.g. refunds[].refunded_by_id when no user).
1526 if ( ! empty( $field['nullable'] ) ) {
1527 if ( \is_array( $schema_type ) ) {
1528 if ( ! \in_array( 'null', $schema_type, true ) ) {
1529 $schema_type[] = 'null';
1530 }
1531 } else {
1532 $schema_type = array( $schema_type, 'null' );
1533 }
1534 }
1535
1536 $schema = array(
1537 'type' => $schema_type,
1538 'description' => isset( $field['label'] ) ? (string) $field['label'] : '',
1539 );
1540
1541 if ( \in_array( $type, array( 'string[]', 'array' ), true ) ) {
1542 $schema['items'] = array( 'type' => 'string[]' === $type ? 'string' : array( 'string', 'number', 'boolean', 'object', 'array', 'null' ) );
1543 }
1544
1545 if ( ! empty( $field['is_array'] ) && ! empty( $field['fields'] ) ) {
1546 $schema['type'] = 'array';
1547 $schema['items'] = array(
1548 'type' => 'object',
1549 'additionalProperties' => true,
1550 'properties' => array(),
1551 );
1552
1553 foreach ( $field['fields'] as $child_name => $child_field ) {
1554 $schema['items']['properties'][ $child_name ] = self::field_metadata_to_json_schema( $child_field );
1555 }
1556 }
1557
1558 return $schema;
1559 }
1560
1561 /**
1562 * Map template field-tree scalar types to JSON Schema types.
1563 *
1564 * @param string $type Field-tree type.
1565 *
1566 * @return string|array<int,string>
1567 */
1568 private static function field_type_to_json_type( string $type ) {
1569 switch ( $type ) {
1570 case 'number':
1571 return 'number';
1572 case 'boolean':
1573 return 'boolean';
1574 case 'money':
1575 return array( 'number', 'string' );
1576 case 'object':
1577 return 'object';
1578 case 'array':
1579 case 'string[]':
1580 return 'array';
1581 case 'string':
1582 default:
1583 return 'string';
1584 }
1585 }
1586
1587 /**
1588 * Get mock receipt data for template preview.
1589 *
1590 * Returns a representative receipt payload with realistic values
1591 * for use in the template editor preview and tests.
1592 *
1593 * @return array Mock receipt data.
1594 */
1595 public static function get_mock_receipt_data(): array {
1596 $created = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:30:00 UTC' ) );
1597 $paid = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:35:00 UTC' ) );
1598 $completed = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:42:00 UTC' ) );
1599 $printed = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:45:00 UTC' ) );
1600
1601 return array(
1602 'has_tax_summary' => true,
1603 'order' => array(
1604 'id' => 1001,
1605 'number' => '1001',
1606 'currency' => 'USD',
1607 'customer_note' => '',
1608 'wc_status' => 'completed',
1609 'status_label' => 'Completed',
1610 'created_via' => 'woocommerce-pos',
1611 'created' => $created,
1612 'paid' => $paid,
1613 'completed' => $completed,
1614 'printed' => $printed,
1615 'needs_payment' => false,
1616 'payment_url' => '',
1617 ),
1618 'store' => array(
1619 'id' => 1,
1620 'name' => 'My Store',
1621 'address' => array(
1622 'address_1' => '123 Main St',
1623 'address_2' => '',
1624 'city' => 'Anytown',
1625 'state' => 'CA',
1626 'postcode' => '90210',
1627 'country' => 'US',
1628 ),
1629 'address_lines' => array( '123 Main St', 'Anytown, CA 90210' ),
1630 'tax_ids' => array(
1631 array(
1632 'type' => 'us_ein',
1633 'value' => '12-3456789',
1634 'country' => 'US',
1635 'label' => 'EIN',
1636 ),
1637 ),
1638 'phone' => '+1 (555) 123-4567',
1639 'email' => 'hello@mystore.com',
1640 'logo' => 'https://example.com/logo.png',
1641 'opening_hours' => "Mon\u{2013}Fri 9:00 AM \u{2013} 5:00 PM\nSat 10:00 AM \u{2013} 4:00 PM\nSun Closed",
1642 'opening_hours_vertical' => "Mon 9:00 AM \u{2013} 5:00 PM\nTue 9:00 AM \u{2013} 5:00 PM\nWed 9:00 AM \u{2013} 5:00 PM\nThu 9:00 AM \u{2013} 5:00 PM\nFri 9:00 AM \u{2013} 5:00 PM\nSat 10:00 AM \u{2013} 4:00 PM\nSun Closed",
1643 'opening_hours_inline' => "Mon\u{2013}Fri 9:00 AM \u{2013} 5:00 PM, Sat 10:00 AM \u{2013} 4:00 PM, Sun Closed",
1644 'opening_hours_notes' => 'Closed on public holidays',
1645 'personal_notes' => '',
1646 'policies_and_conditions' => '',
1647 'footer_imprint' => '',
1648 ),
1649 'cashier' => array(
1650 'id' => 1,
1651 'name' => 'Admin',
1652 ),
1653 );
1654 }
1655 }
1656