PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
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.10.18, at includes/Services/Receipt_Data_Schema.php

1,591 lines 65.7 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.2.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 'discount_type' => array(
816 'type' => 'string',
817 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Coupon Type', 'woocommerce-pos' ),
818 ),
819 'total' => array(
820 'type' => 'money',
821 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
822 ),
823 'total_incl' => array(
824 'type' => 'money',
825 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ),
826 ),
827 'total_excl' => array(
828 'type' => 'money',
829 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ),
830 ),
831 ),
832 ),
833 'totals' => array(
834 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Totals', 'woocommerce-pos' ),
835 'fields' => array(
836 'subtotal' => array(
837 'type' => 'money',
838 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal', 'woocommerce-pos' ),
839 ),
840 'subtotal_incl' => array(
841 'type' => 'money',
842 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (incl tax)', 'woocommerce-pos' ),
843 ),
844 'subtotal_excl' => array(
845 'type' => 'money',
846 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (excl tax)', 'woocommerce-pos' ),
847 ),
848 'discount_total' => array(
849 'type' => 'money',
850 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total', 'woocommerce-pos' ),
851 ),
852 'discount_total_incl' => array(
853 'type' => 'money',
854 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (incl tax)', 'woocommerce-pos' ),
855 ),
856 'discount_total_excl' => array(
857 'type' => 'money',
858 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (excl tax)', 'woocommerce-pos' ),
859 ),
860 'sale_savings_total' => array(
861 'type' => 'money',
862 'nullable' => true,
863 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total', 'woocommerce-pos' ),
864 ),
865 'sale_savings_total_incl' => array(
866 'type' => 'money',
867 'nullable' => true,
868 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (incl tax)', 'woocommerce-pos' ),
869 ),
870 'sale_savings_total_excl' => array(
871 'type' => 'money',
872 'nullable' => true,
873 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (excl tax)', 'woocommerce-pos' ),
874 ),
875 'total_saved' => array(
876 'type' => 'money',
877 'nullable' => true,
878 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts)', 'woocommerce-pos' ),
879 ),
880 'total_saved_incl' => array(
881 'type' => 'money',
882 'nullable' => true,
883 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, incl tax)', 'woocommerce-pos' ),
884 ),
885 'total_saved_excl' => array(
886 'type' => 'money',
887 'nullable' => true,
888 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, excl tax)', 'woocommerce-pos' ),
889 ),
890 'total_saved_complete' => array(
891 'type' => 'boolean',
892 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved Is Complete', 'woocommerce-pos' ),
893 ),
894 'tax_total' => array(
895 'type' => 'money',
896 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Total', 'woocommerce-pos' ),
897 ),
898 'total' => array(
899 'type' => 'money',
900 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ),
901 ),
902 'total_incl' => array(
903 'type' => 'money',
904 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl. tax)', 'woocommerce-pos' ),
905 ),
906 'total_excl' => array(
907 'type' => 'money',
908 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl. tax)', 'woocommerce-pos' ),
909 ),
910 'paid_total' => array(
911 'type' => 'money',
912 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Paid Total', 'woocommerce-pos' ),
913 ),
914 'change_total' => array(
915 'type' => 'money',
916 'label' => /* translators: Template-editor field label for total cash returned to the customer after payments; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ),
917 ),
918 'refund_total' => array(
919 'type' => 'money',
920 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Total', 'woocommerce-pos' ),
921 ),
922 'net_total' => array(
923 'type' => 'money',
924 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Net Total', 'woocommerce-pos' ),
925 ),
926 'total_qty' => array(
927 'type' => 'number',
928 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Quantity', 'woocommerce-pos' ),
929 ),
930 'line_count' => array(
931 'type' => 'number',
932 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Count', 'woocommerce-pos' ),
933 ),
934 ),
935 ),
936
937 'tax' => array(
938 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax', 'woocommerce-pos' ),
939 'fields' => array(
940 'display' => array(
941 'type' => 'string',
942 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Mode', 'woocommerce-pos' ),
943 ),
944 'display_incl' => array(
945 'type' => 'boolean',
946 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Inclusive', 'woocommerce-pos' ),
947 ),
948 'display_excl' => array(
949 'type' => 'boolean',
950 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Exclusive', 'woocommerce-pos' ),
951 ),
952 'breakdown' => array(
953 'type' => 'string',
954 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Mode', 'woocommerce-pos' ),
955 ),
956 'breakdown_hidden' => array(
957 'type' => 'boolean',
958 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Hidden', 'woocommerce-pos' ),
959 ),
960 'breakdown_single' => array(
961 'type' => 'boolean',
962 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Single', 'woocommerce-pos' ),
963 ),
964 'breakdown_itemized' => array(
965 'type' => 'boolean',
966 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Itemized', 'woocommerce-pos' ),
967 ),
968 ),
969 ),
970 'has_tax_summary' => array(
971 'type' => 'boolean',
972 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Has Tax Summary', 'woocommerce-pos' ),
973 'fields' => array(),
974 ),
975 'tax_summary' => array(
976 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Summary', 'woocommerce-pos' ),
977 'is_array' => true,
978 'fields' => array(
979 'code' => array(
980 'type' => 'string',
981 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Code', 'woocommerce-pos' ),
982 ),
983 'label' => array(
984 'type' => 'string',
985 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Label', 'woocommerce-pos' ),
986 ),
987 'rate' => array(
988 'type' => 'number',
989 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Rate (%)', 'woocommerce-pos' ),
990 ),
991 'compound' => array(
992 'type' => 'boolean',
993 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Compound Tax', 'woocommerce-pos' ),
994 ),
995 'taxable_amount_excl' => array(
996 'type' => 'money',
997 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (excl)', 'woocommerce-pos' ),
998 ),
999 'tax_amount' => array(
1000 'type' => 'money',
1001 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Amount', 'woocommerce-pos' ),
1002 ),
1003 'taxable_amount_incl' => array(
1004 'type' => 'money',
1005 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (incl)', 'woocommerce-pos' ),
1006 ),
1007 ),
1008 ),
1009 'payments' => array(
1010 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payments', 'woocommerce-pos' ),
1011 'is_array' => true,
1012 'fields' => array(
1013 'method_id' => array(
1014 'type' => 'string',
1015 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Method ID', 'woocommerce-pos' ),
1016 ),
1017 'method_title' => array(
1018 'type' => 'string',
1019 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payment Method', 'woocommerce-pos' ),
1020 ),
1021 'amount' => array(
1022 'type' => 'money',
1023 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Amount', 'woocommerce-pos' ),
1024 ),
1025 'tendered' => array(
1026 'type' => 'money',
1027 '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' ),
1028 ),
1029 'change' => array(
1030 'type' => 'money',
1031 'label' => /* translators: Template-editor field label for cash returned to the customer after payment; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ),
1032 ),
1033 'transaction_id' => array(
1034 'type' => 'string',
1035 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Transaction ID', 'woocommerce-pos' ),
1036 ),
1037 ),
1038 ),
1039 'refunds' => array(
1040 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunds', 'woocommerce-pos' ),
1041 'is_array' => true,
1042 'fields' => array(
1043 'id' => array(
1044 'type' => 'number',
1045 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund ID', 'woocommerce-pos' ),
1046 ),
1047 'date' => array(
1048 'type' => 'object',
1049 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Date', 'woocommerce-pos' ),
1050 ),
1051 'amount' => array(
1052 'type' => 'money',
1053 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Amount', 'woocommerce-pos' ),
1054 ),
1055 'subtotal' => array(
1056 'type' => 'money',
1057 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Subtotal', 'woocommerce-pos' ),
1058 ),
1059 'tax_total' => array(
1060 'type' => 'money',
1061 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Tax Total', 'woocommerce-pos' ),
1062 ),
1063 'shipping_total' => array(
1064 'type' => 'money',
1065 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ),
1066 ),
1067 'shipping_tax' => array(
1068 'type' => 'money',
1069 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Tax', 'woocommerce-pos' ),
1070 ),
1071 'reason' => array(
1072 'type' => 'string',
1073 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Reason', 'woocommerce-pos' ),
1074 ),
1075 'refunded_by_id' => array(
1076 'type' => 'number',
1077 'nullable' => true,
1078 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (User ID)', 'woocommerce-pos' ),
1079 ),
1080 'refunded_by_name' => array(
1081 'type' => 'string',
1082 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (Name)', 'woocommerce-pos' ),
1083 ),
1084 'refunded_payment' => array(
1085 'type' => 'boolean',
1086 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded Payment', 'woocommerce-pos' ),
1087 ),
1088 'destination' => array(
1089 'type' => 'string',
1090 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Destination', 'woocommerce-pos' ),
1091 ),
1092 'gateway_id' => array(
1093 'type' => 'string',
1094 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway ID', 'woocommerce-pos' ),
1095 ),
1096 'gateway_title' => array(
1097 'type' => 'string',
1098 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway Title', 'woocommerce-pos' ),
1099 ),
1100 'processing_mode' => array(
1101 'type' => 'string',
1102 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Processing Mode', 'woocommerce-pos' ),
1103 ),
1104 'lines' => array(
1105 'type' => 'array',
1106 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Lines', 'woocommerce-pos' ),
1107 'is_array' => true,
1108 'fields' => array(
1109 'name' => array(
1110 'type' => 'string',
1111 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Name', 'woocommerce-pos' ),
1112 ),
1113 'sku' => array(
1114 'type' => 'string',
1115 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'SKU', 'woocommerce-pos' ),
1116 ),
1117 'qty' => array(
1118 'type' => 'number',
1119 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity', 'woocommerce-pos' ),
1120 ),
1121 'total' => array(
1122 'type' => 'money',
1123 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total', 'woocommerce-pos' ),
1124 ),
1125 'total_incl' => array(
1126 'type' => 'money',
1127 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (incl tax)', 'woocommerce-pos' ),
1128 ),
1129 'total_excl' => array(
1130 'type' => 'money',
1131 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (excl tax)', 'woocommerce-pos' ),
1132 ),
1133 'taxes' => array(
1134 'type' => 'array',
1135 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Taxes', 'woocommerce-pos' ),
1136 ),
1137 ),
1138 ),
1139 'fees' => array(
1140 'type' => 'array',
1141 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fees', 'woocommerce-pos' ),
1142 'is_array' => true,
1143 'fields' => array(
1144 'label' => array(
1145 'type' => 'string',
1146 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Label', 'woocommerce-pos' ),
1147 ),
1148 'total' => array(
1149 'type' => 'money',
1150 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total', 'woocommerce-pos' ),
1151 ),
1152 'total_incl' => array(
1153 'type' => 'money',
1154 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (incl tax)', 'woocommerce-pos' ),
1155 ),
1156 'total_excl' => array(
1157 'type' => 'money',
1158 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (excl tax)', 'woocommerce-pos' ),
1159 ),
1160 'taxes' => array(
1161 'type' => 'array',
1162 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Taxes', 'woocommerce-pos' ),
1163 ),
1164 ),
1165 ),
1166 'shipping' => array(
1167 'type' => 'array',
1168 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping', 'woocommerce-pos' ),
1169 'is_array' => true,
1170 'fields' => array(
1171 'label' => array(
1172 'type' => 'string',
1173 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Label', 'woocommerce-pos' ),
1174 ),
1175 'method_id' => array(
1176 'type' => 'string',
1177 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Method ID', 'woocommerce-pos' ),
1178 ),
1179 'total' => array(
1180 'type' => 'money',
1181 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ),
1182 ),
1183 'total_incl' => array(
1184 'type' => 'money',
1185 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (incl tax)', 'woocommerce-pos' ),
1186 ),
1187 'total_excl' => array(
1188 'type' => 'money',
1189 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (excl tax)', 'woocommerce-pos' ),
1190 ),
1191 'taxes' => array(
1192 'type' => 'array',
1193 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Taxes', 'woocommerce-pos' ),
1194 ),
1195 ),
1196 ),
1197 ),
1198 ),
1199 'fiscal' => array(
1200 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fiscal', 'woocommerce-pos' ),
1201 'fields' => array(
1202 'immutable_id' => array(
1203 'type' => 'string',
1204 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Immutable ID', 'woocommerce-pos' ),
1205 ),
1206 'receipt_number' => array(
1207 'type' => 'string',
1208 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Receipt Number', 'woocommerce-pos' ),
1209 ),
1210 'sequence' => array(
1211 'type' => 'number',
1212 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sequence', 'woocommerce-pos' ),
1213 ),
1214 'hash' => array(
1215 'type' => 'string',
1216 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Hash', 'woocommerce-pos' ),
1217 ),
1218 'qr_payload' => array(
1219 'type' => 'string',
1220 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'QR Payload', 'woocommerce-pos' ),
1221 ),
1222 'tax_agency_code' => array(
1223 'type' => 'string',
1224 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Agency Code', 'woocommerce-pos' ),
1225 ),
1226 'signed_at' => array(
1227 'type' => 'string',
1228 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signed At', 'woocommerce-pos' ),
1229 ),
1230 'signature_excerpt' => array(
1231 'type' => 'string',
1232 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signature Excerpt', 'woocommerce-pos' ),
1233 ),
1234 'document_label' => array(
1235 'type' => 'string',
1236 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Document Label', 'woocommerce-pos' ),
1237 ),
1238 'is_reprint' => array(
1239 'type' => 'boolean',
1240 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Is Reprint', 'woocommerce-pos' ),
1241 ),
1242 'reprint_count' => array(
1243 'type' => 'number',
1244 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Reprint Count', 'woocommerce-pos' ),
1245 ),
1246 'extra_fields' => array(
1247 'type' => 'array',
1248 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Extra Fields', 'woocommerce-pos' ),
1249 'is_array' => true,
1250 'fields' => array(
1251 'label' => array(
1252 'type' => 'string',
1253 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ),
1254 ),
1255 'value' => array(
1256 'type' => 'string',
1257 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ),
1258 ),
1259 ),
1260 ),
1261 ),
1262 ),
1263 'i18n' => array(
1264 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Labels (i18n)', 'woocommerce-pos' ),
1265 'fields' => self::get_i18n_field_tree_fields(),
1266 ),
1267 );
1268 }
1269
1270
1271 /**
1272 * Get field metadata for every canonical i18n receipt label.
1273 *
1274 * @return array<string, array{type: string, label: string}>
1275 */
1276 private static function get_i18n_field_tree_fields(): array {
1277 $fields = array();
1278
1279 foreach ( Receipt_I18n_Labels::get_labels() as $key => $label ) {
1280 $fields[ $key ] = array(
1281 'type' => 'string',
1282 'label' => $label,
1283 );
1284 }
1285
1286 return $fields;
1287 }
1288
1289 /**
1290 * Get field metadata for a semantic date section.
1291 *
1292 * @return array<string, array{type: string, label: string}>
1293 */
1294 private static function get_date_field_tree_fields(): array {
1295 return array(
1296 'datetime' => array(
1297 'type' => 'string',
1298 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date & Time', 'woocommerce-pos' ),
1299 ),
1300 'date' => array(
1301 'type' => 'string',
1302 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date', 'woocommerce-pos' ),
1303 ),
1304 'time' => array(
1305 'type' => 'string',
1306 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Time', 'woocommerce-pos' ),
1307 ),
1308 'datetime_short' => array(
1309 'type' => 'string',
1310 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date & Time', 'woocommerce-pos' ),
1311 ),
1312 'datetime_long' => array(
1313 'type' => 'string',
1314 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date & Time', 'woocommerce-pos' ),
1315 ),
1316 'datetime_full' => array(
1317 'type' => 'string',
1318 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date & Time', 'woocommerce-pos' ),
1319 ),
1320 'date_short' => array(
1321 'type' => 'string',
1322 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date', 'woocommerce-pos' ),
1323 ),
1324 'date_long' => array(
1325 'type' => 'string',
1326 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date', 'woocommerce-pos' ),
1327 ),
1328 'date_full' => array(
1329 'type' => 'string',
1330 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date', 'woocommerce-pos' ),
1331 ),
1332 'date_ymd' => array(
1333 'type' => 'string',
1334 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'YYYY-MM-DD', 'woocommerce-pos' ),
1335 ),
1336 'date_dmy' => array(
1337 'type' => 'string',
1338 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'DD/MM/YYYY', 'woocommerce-pos' ),
1339 ),
1340 'date_mdy' => array(
1341 'type' => 'string',
1342 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'MM/DD/YYYY', 'woocommerce-pos' ),
1343 ),
1344 'weekday_short' => array(
1345 'type' => 'string',
1346 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Short', 'woocommerce-pos' ),
1347 ),
1348 'weekday_long' => array(
1349 'type' => 'string',
1350 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Long', 'woocommerce-pos' ),
1351 ),
1352 'day' => array(
1353 'type' => 'string',
1354 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Day', 'woocommerce-pos' ),
1355 ),
1356 'month' => array(
1357 'type' => 'string',
1358 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Number', 'woocommerce-pos' ),
1359 ),
1360 'month_short' => array(
1361 'type' => 'string',
1362 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Short', 'woocommerce-pos' ),
1363 ),
1364 'month_long' => array(
1365 'type' => 'string',
1366 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Long', 'woocommerce-pos' ),
1367 ),
1368 'year' => array(
1369 'type' => 'string',
1370 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Year', 'woocommerce-pos' ),
1371 ),
1372 );
1373 }
1374
1375 /**
1376 * Get the JSON Schema for canonical receipt_data payloads.
1377 *
1378 * PHP remains the source of truth in this repository. This export is used by
1379 * generated TypeScript artifacts and downstream renderer/studio checks.
1380 *
1381 * @return array<string, mixed> JSON-Schema-compatible receipt data schema.
1382 */
1383 public static function get_json_schema(): array {
1384 $schema = array(
1385 '$schema' => 'https://json-schema.org/draft/2020-12/schema',
1386 '$id' => 'https://wcpos.com/schemas/receipt-data.schema.json',
1387 'x-schema-version' => self::SCHEMA_VERSION,
1388 'title' => 'ReceiptData',
1389 'type' => 'object',
1390 'additionalProperties' => true,
1391 'required' => self::REQUIRED_KEYS,
1392 'properties' => array(),
1393 );
1394
1395 foreach ( self::REQUIRED_KEYS as $key ) {
1396 $schema['properties'][ $key ] = self::get_default_section_schema( $key );
1397 }
1398
1399 foreach ( self::get_field_tree() as $path => $section ) {
1400 self::merge_field_tree_section_schema( $schema, $path, $section );
1401 }
1402
1403 unset( $schema['properties']['presentation_hints']['properties'] );
1404
1405 return $schema;
1406 }
1407
1408 /**
1409 * Get a default schema for a top-level receipt section.
1410 *
1411 * @param string $key Top-level receipt data key.
1412 *
1413 * @return array<string, mixed>
1414 */
1415 private static function get_default_section_schema( string $key ): array {
1416 if ( 'has_tax_summary' === $key ) {
1417 return array(
1418 'type' => 'boolean',
1419 'description' => 'Whether tax_summary contains at least one row.',
1420 );
1421 }
1422
1423 if ( \in_array( $key, array( 'lines', 'fees', 'shipping', 'discounts', 'tax_summary', 'payments', 'refunds' ), true ) ) {
1424 return array(
1425 'type' => 'array',
1426 'items' => array(
1427 'type' => 'object',
1428 'additionalProperties' => true,
1429 'properties' => array(),
1430 ),
1431 'additionalProperties' => true,
1432 );
1433 }
1434
1435 return array(
1436 'type' => 'object',
1437 'additionalProperties' => true,
1438 'properties' => array(),
1439 );
1440 }
1441
1442 /**
1443 * Merge a template editor field-tree section into the JSON Schema.
1444 *
1445 * @param array<string, mixed> $schema Full schema, passed by reference.
1446 * @param string $path Dot path for the field-tree section.
1447 * @param array<string, mixed> $section Field-tree section metadata.
1448 *
1449 * @return void
1450 */
1451 private static function merge_field_tree_section_schema( array &$schema, string $path, array $section ): void {
1452 $segments = explode( '.', $path );
1453 $top_key = array_shift( $segments );
1454
1455 if ( ! \is_string( $top_key ) || '' === $top_key ) {
1456 return;
1457 }
1458
1459 if ( ! isset( $schema['properties'][ $top_key ] ) ) {
1460 $schema['properties'][ $top_key ] = self::get_default_section_schema( $top_key );
1461 }
1462
1463 $target =& $schema['properties'][ $top_key ];
1464 $target_is_collection_item = false;
1465
1466 if ( 'array' === ( $target['type'] ?? null ) ) {
1467 $target =& $target['items'];
1468 $target_is_collection_item = true;
1469 }
1470
1471 foreach ( $segments as $segment ) {
1472 if ( ! isset( $target['properties'][ $segment ] ) ) {
1473 $target['properties'][ $segment ] = array(
1474 'type' => 'object',
1475 'additionalProperties' => true,
1476 'properties' => array(),
1477 );
1478 }
1479 $target =& $target['properties'][ $segment ];
1480 }
1481
1482 $target['description'] = isset( $section['label'] ) ? (string) $section['label'] : $path;
1483
1484 if ( isset( $section['type'] ) && empty( $section['fields'] ) && empty( $segments ) ) {
1485 $target = self::field_metadata_to_json_schema( $section );
1486 return;
1487 }
1488
1489 if ( ! empty( $section['is_array'] ) && ! $target_is_collection_item ) {
1490 $target['type'] = 'array';
1491 $target['items'] = $target['items'] ?? array(
1492 'type' => 'object',
1493 'additionalProperties' => true,
1494 'properties' => array(),
1495 );
1496
1497 // Strip object-only keywords that may have been seeded by the
1498 // segment-walk above; they are invalid on an array schema.
1499 unset( $target['additionalProperties'], $target['properties'] );
1500 } else {
1501 $target['type'] = 'object';
1502 $target['additionalProperties'] = true;
1503 $target['properties'] = $target['properties'] ?? array();
1504 }
1505
1506 $field_target =& $target;
1507 if ( 'array' === ( $target['type'] ?? null ) ) {
1508 $field_target =& $target['items'];
1509 }
1510
1511 foreach ( $section['fields'] ?? array() as $field_name => $field ) {
1512 $field_target['properties'][ $field_name ] = self::field_metadata_to_json_schema( $field );
1513 }
1514 }
1515
1516 /**
1517 * Convert one field-tree field to a JSON Schema property.
1518 *
1519 * @param array<string,mixed> $field Field metadata.
1520 *
1521 * @return array<string,mixed>
1522 */
1523 private static function field_metadata_to_json_schema( array $field ): array {
1524 $type = isset( $field['type'] ) ? (string) $field['type'] : 'string';
1525 $schema_type = self::field_type_to_json_type( $type );
1526
1527 // Honor a nullable flag from the field-tree metadata: producers may
1528 // legitimately emit null for a field whose JSON Schema type would
1529 // otherwise reject it (e.g. refunds[].refunded_by_id when no user).
1530 if ( ! empty( $field['nullable'] ) ) {
1531 if ( \is_array( $schema_type ) ) {
1532 if ( ! \in_array( 'null', $schema_type, true ) ) {
1533 $schema_type[] = 'null';
1534 }
1535 } else {
1536 $schema_type = array( $schema_type, 'null' );
1537 }
1538 }
1539
1540 $schema = array(
1541 'type' => $schema_type,
1542 'description' => isset( $field['label'] ) ? (string) $field['label'] : '',
1543 );
1544
1545 if ( \in_array( $type, array( 'string[]', 'array' ), true ) ) {
1546 $schema['items'] = array( 'type' => 'string[]' === $type ? 'string' : array( 'string', 'number', 'boolean', 'object', 'array', 'null' ) );
1547 }
1548
1549 if ( ! empty( $field['is_array'] ) && ! empty( $field['fields'] ) ) {
1550 $schema['type'] = 'array';
1551 $schema['items'] = array(
1552 'type' => 'object',
1553 'additionalProperties' => true,
1554 'properties' => array(),
1555 );
1556
1557 foreach ( $field['fields'] as $child_name => $child_field ) {
1558 $schema['items']['properties'][ $child_name ] = self::field_metadata_to_json_schema( $child_field );
1559 }
1560 }
1561
1562 return $schema;
1563 }
1564
1565 /**
1566 * Map template field-tree scalar types to JSON Schema types.
1567 *
1568 * @param string $type Field-tree type.
1569 *
1570 * @return string|array<int,string>
1571 */
1572 private static function field_type_to_json_type( string $type ) {
1573 switch ( $type ) {
1574 case 'number':
1575 return 'number';
1576 case 'boolean':
1577 return 'boolean';
1578 case 'money':
1579 return array( 'number', 'string' );
1580 case 'object':
1581 return 'object';
1582 case 'array':
1583 case 'string[]':
1584 return 'array';
1585 case 'string':
1586 default:
1587 return 'string';
1588 }
1589 }
1590 }
1591