$presentation_hints Receipt presentation hints. * * @return array Data with money fields formatted as strings. */ public static function format_money_fields( array $data, string $currency = 'USD', array $presentation_hints = array() ): array { static $lookup = null; static $zero_falsy = null; if ( null === $lookup ) { $lookup = array_flip( self::MONEY_FIELDS ); $zero_falsy = array_flip( self::ZERO_FALSY_MONEY_FIELDS ); } if ( empty( $presentation_hints ) && isset( $data['presentation_hints'] ) && \is_array( $data['presentation_hints'] ) ) { $presentation_hints = $data['presentation_hints']; } $price_args = self::get_price_format_args( $currency, $presentation_hints ); $result = array(); foreach ( $data as $k => $value ) { if ( \is_array( $value ) ) { $result[ $k ] = self::format_money_fields( $value, $currency, $presentation_hints ); } elseif ( is_numeric( $value ) && isset( $lookup[ $k ] ) ) { // ────────────────────────────────────────────────────────── // Money fields: KEEP the numeric value at the bare key and // ADD a `_display` companion holding the locale-formatted // currency string. Mirrors the JS `formatReceiptData` shape // exactly so a single Mustache template renders the same way // in both the studio (JS) and production print (PHP). // // DO NOT change this back to in-place replacement — bare keys // must be numeric so app code can still do math on them, and // templates have a stable `_display` variant to render // currency without re-implementing wc_price() in JS. // ────────────────────────────────────────────────────────── $numeric = (float) $value; $is_zero_falsy = ( 0.0 === $numeric && isset( $zero_falsy[ $k ] ) ); // Conditional-display fields (change, tendered, discounts, etc.) keep // the bare key as integer 0 when the value is zero so Mustache section // guards (`{{#change}}…{{/change}}`) treat them as falsy and the rest // of the test suite's `assertSame( 0, … )` checks stay green. $result[ $k ] = $is_zero_falsy ? 0 : $numeric; $result[ $k . '_display' ] = $is_zero_falsy ? '' : html_entity_decode( wp_strip_all_tags( wc_price( $numeric, $price_args ) ), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' ); } else { $result[ $k ] = $value; } } return $result; } /** * Build wc_price() arguments from presentation hints. * * @param string $currency WooCommerce currency code. * @param array $presentation_hints Receipt presentation hints. * * @return array */ private static function get_price_format_args( string $currency, array $presentation_hints ): array { $args = array( 'currency' => $currency ); if ( isset( $presentation_hints['currency_position'] ) ) { $args['price_format'] = self::get_price_format_for_position( (string) $presentation_hints['currency_position'] ); } if ( isset( $presentation_hints['price_decimal_separator'] ) ) { $args['decimal_separator'] = (string) $presentation_hints['price_decimal_separator']; } if ( isset( $presentation_hints['price_thousand_separator'] ) ) { $args['thousand_separator'] = (string) $presentation_hints['price_thousand_separator']; } if ( isset( $presentation_hints['price_num_decimals'] ) && is_numeric( $presentation_hints['price_num_decimals'] ) ) { $args['decimals'] = (int) $presentation_hints['price_num_decimals']; } return $args; } /** * Convert a WooCommerce currency position option to a wc_price format. * * @param string $position WooCommerce currency position. * * @return string wc_price format. */ private static function get_price_format_for_position( string $position ): string { switch ( $position ) { case 'right': return '%2$s%1$s'; case 'left_space': return '%1$s %2$s'; case 'right_space': return '%2$s %1$s'; case 'left': default: return '%1$s%2$s'; } } /** * Get the field tree for the template editor field picker. * * Returns a structured array describing template-picker sections and fields * from the receipt_data contract. Used by the JS field picker sidebar. * Internal sections (e.g. presentation_hints) are intentionally excluded. * * @return array}> */ public static function get_field_tree(): array { return array( 'order' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order', 'woocommerce-pos' ), 'fields' => array( 'id' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order ID', 'woocommerce-pos' ), ), 'number' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Number', 'woocommerce-pos' ), ), 'currency' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Currency', 'woocommerce-pos' ), ), 'customer_note' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Note', 'woocommerce-pos' ), ), 'wc_status' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'WC Status', 'woocommerce-pos' ), ), 'status_label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Status Label', 'woocommerce-pos' ), ), 'created_via' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Created Via', 'woocommerce-pos' ), ), 'needs_payment' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Needs Payment', 'woocommerce-pos' ), ), 'payment_url' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payment URL', 'woocommerce-pos' ), ), ), ), 'order.created' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Created', 'woocommerce-pos' ), 'fields' => self::get_date_field_tree_fields(), ), 'order.paid' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Paid', 'woocommerce-pos' ), 'fields' => self::get_date_field_tree_fields(), ), 'order.completed' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Order Completed', 'woocommerce-pos' ), 'fields' => self::get_date_field_tree_fields(), ), 'order.printed' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Receipt Printed', 'woocommerce-pos' ), 'fields' => self::get_date_field_tree_fields(), ), 'store' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store', 'woocommerce-pos' ), 'fields' => array( 'id' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store ID', 'woocommerce-pos' ), ), 'name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Name', 'woocommerce-pos' ), ), 'address_lines' => array( 'type' => 'string[]', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address Lines', 'woocommerce-pos' ), ), 'phone' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Phone', 'woocommerce-pos' ), ), 'email' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Email', 'woocommerce-pos' ), ), 'logo' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Logo URL', 'woocommerce-pos' ), ), 'opening_hours' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours', 'woocommerce-pos' ), ), 'opening_hours_vertical' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours (Vertical)', 'woocommerce-pos' ), ), 'opening_hours_inline' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours (Inline)', 'woocommerce-pos' ), ), 'opening_hours_notes' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Opening Hours Notes', 'woocommerce-pos' ), ), 'personal_notes' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Personal Notes', 'woocommerce-pos' ), ), 'policies_and_conditions' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Policies & Conditions', 'woocommerce-pos' ), ), 'footer_imprint' => array( 'type' => 'string', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Footer Imprint', 'woocommerce-pos' ), ), ), ), 'cashier' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier', 'woocommerce-pos' ), 'fields' => array( 'id' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier ID', 'woocommerce-pos' ), ), 'name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Cashier Name', 'woocommerce-pos' ), ), ), ), 'customer' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer', 'woocommerce-pos' ), 'fields' => array( 'id' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer ID', 'woocommerce-pos' ), ), 'name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Name', 'woocommerce-pos' ), ), 'billing_address' => array( 'type' => 'object', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Billing Address', 'woocommerce-pos' ), ), 'shipping_address' => array( 'type' => 'object', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Address', 'woocommerce-pos' ), ), ), ), 'customer.tax_ids' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Customer Tax IDs', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'type' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Type', 'woocommerce-pos' ), ), 'value' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ), ), 'country' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ), ), 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ), ), ), ), 'store.tax_ids' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Tax IDs', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'type' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Type', 'woocommerce-pos' ), ), 'value' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ), ), 'country' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ), ), 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ), ), ), ), 'store.address' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Store Address', 'woocommerce-pos' ), 'fields' => array( 'address_1' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address line 1', 'woocommerce-pos' ), ), 'address_2' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Address line 2', 'woocommerce-pos' ), ), 'city' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'City', 'woocommerce-pos' ), ), 'state' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'State / Region', 'woocommerce-pos' ), ), 'postcode' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Postcode', 'woocommerce-pos' ), ), 'country' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Country', 'woocommerce-pos' ), ), ), ), 'lines' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Items', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'key' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Item Key', 'woocommerce-pos' ), ), 'sku' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'SKU', 'woocommerce-pos' ), ), 'name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Name', 'woocommerce-pos' ), ), 'qty' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity', 'woocommerce-pos' ), ), 'qty_refunded' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity Refunded', 'woocommerce-pos' ), ), 'regular_price' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item', 'woocommerce-pos' ), ), 'regular_price_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item (incl tax)', 'woocommerce-pos' ), ), 'regular_price_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular price per item (excl tax)', 'woocommerce-pos' ), ), 'selling_price' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons', 'woocommerce-pos' ), ), 'selling_price_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons (incl tax)', 'woocommerce-pos' ), ), 'selling_price_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling price per item, before coupons (excl tax)', 'woocommerce-pos' ), ), 'unit_savings' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item', 'woocommerce-pos' ), ), 'unit_savings_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item (incl tax)', 'woocommerce-pos' ), ), 'unit_savings_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings per item (excl tax)', 'woocommerce-pos' ), ), 'line_regular_total' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total', 'woocommerce-pos' ), ), 'line_regular_total_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total (incl tax)', 'woocommerce-pos' ), ), 'line_regular_total_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Regular-price total (excl tax)', 'woocommerce-pos' ), ), 'line_selling_total' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons', 'woocommerce-pos' ), ), 'line_selling_total_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons (incl tax)', 'woocommerce-pos' ), ), 'line_selling_total_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Selling-price total, before coupons (excl tax)', 'woocommerce-pos' ), ), 'line_savings' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item', 'woocommerce-pos' ), ), 'line_savings_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item (incl tax)', 'woocommerce-pos' ), ), 'line_savings_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total savings for this item (excl tax)', 'woocommerce-pos' ), ), 'savings_in_discounts' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Savings already included in WooCommerce discounts', 'woocommerce-pos' ), ), 'unit_subtotal' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal', 'woocommerce-pos' ), ), 'unit_subtotal_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal (incl tax)', 'woocommerce-pos' ), ), 'unit_subtotal_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Subtotal (excl tax)', 'woocommerce-pos' ), ), 'unit_price' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price', 'woocommerce-pos' ), ), 'unit_price_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price (incl tax)', 'woocommerce-pos' ), ), 'unit_price_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Unit Price (excl tax)', 'woocommerce-pos' ), ), 'line_subtotal' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal', 'woocommerce-pos' ), ), 'line_subtotal_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (incl tax)', 'woocommerce-pos' ), ), 'line_subtotal_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (excl tax)', 'woocommerce-pos' ), ), 'discounts' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts', 'woocommerce-pos' ), ), 'discounts_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts (incl tax)', 'woocommerce-pos' ), ), 'discounts_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts (excl tax)', 'woocommerce-pos' ), ), 'line_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total', 'woocommerce-pos' ), ), 'line_total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total (incl tax)', 'woocommerce-pos' ), ), 'line_total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Total (excl tax)', 'woocommerce-pos' ), ), 'total_refunded' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Refunded', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Taxes', 'woocommerce-pos' ), ), 'meta' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Item Meta', 'woocommerce-pos' ), ), 'attributes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Attributes', 'woocommerce-pos' ), ), ), ), 'fees' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fees', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Label', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Taxes', 'woocommerce-pos' ), ), 'meta' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Meta', 'woocommerce-pos' ), ), ), ), 'shipping' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Label', 'woocommerce-pos' ), ), 'method_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Method ID', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Taxes', 'woocommerce-pos' ), ), 'meta' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Meta', 'woocommerce-pos' ), ), ), ), 'discounts' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discounts', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Label', 'woocommerce-pos' ), ), 'code' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Coupon Code', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl tax)', 'woocommerce-pos' ), ), ), ), 'totals' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Totals', 'woocommerce-pos' ), 'fields' => array( 'subtotal' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal', 'woocommerce-pos' ), ), 'subtotal_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (incl tax)', 'woocommerce-pos' ), ), 'subtotal_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Subtotal (excl tax)', 'woocommerce-pos' ), ), 'discount_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total', 'woocommerce-pos' ), ), 'discount_total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (incl tax)', 'woocommerce-pos' ), ), 'discount_total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Discount Total (excl tax)', 'woocommerce-pos' ), ), 'sale_savings_total' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total', 'woocommerce-pos' ), ), 'sale_savings_total_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (incl tax)', 'woocommerce-pos' ), ), 'sale_savings_total_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sale Savings Total (excl tax)', 'woocommerce-pos' ), ), 'total_saved' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts)', 'woocommerce-pos' ), ), 'total_saved_incl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, incl tax)', 'woocommerce-pos' ), ), 'total_saved_excl' => array( 'type' => 'money', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved (sale savings + discounts, excl tax)', 'woocommerce-pos' ), ), 'total_saved_complete' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Saved Is Complete', 'woocommerce-pos' ), ), 'tax_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Total', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (incl. tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total (excl. tax)', 'woocommerce-pos' ), ), 'paid_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Paid Total', 'woocommerce-pos' ), ), 'change_total' => array( 'type' => 'money', 'label' => /* translators: Template-editor field label for total cash returned to the customer after payments; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ), ), 'refund_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Total', 'woocommerce-pos' ), ), 'net_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Net Total', 'woocommerce-pos' ), ), 'total_qty' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Total Quantity', 'woocommerce-pos' ), ), 'line_count' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Line Count', 'woocommerce-pos' ), ), ), ), 'tax' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax', 'woocommerce-pos' ), 'fields' => array( 'display' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Mode', 'woocommerce-pos' ), ), 'display_incl' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Inclusive', 'woocommerce-pos' ), ), 'display_excl' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Display Is Exclusive', 'woocommerce-pos' ), ), 'breakdown' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Mode', 'woocommerce-pos' ), ), 'breakdown_hidden' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Hidden', 'woocommerce-pos' ), ), 'breakdown_single' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Single', 'woocommerce-pos' ), ), 'breakdown_itemized' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Breakdown Itemized', 'woocommerce-pos' ), ), ), ), 'has_tax_summary' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Has Tax Summary', 'woocommerce-pos' ), 'fields' => array(), ), 'tax_summary' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Summary', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'code' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Code', 'woocommerce-pos' ), ), 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Label', 'woocommerce-pos' ), ), 'rate' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Rate (%)', 'woocommerce-pos' ), ), 'compound' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Compound Tax', 'woocommerce-pos' ), ), 'taxable_amount_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (excl)', 'woocommerce-pos' ), ), 'tax_amount' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Amount', 'woocommerce-pos' ), ), 'taxable_amount_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Taxable Amount (incl)', 'woocommerce-pos' ), ), ), ), 'payments' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payments', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'method_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Method ID', 'woocommerce-pos' ), ), 'method_title' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Payment Method', 'woocommerce-pos' ), ), 'amount' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Amount', 'woocommerce-pos' ), ), 'tendered' => array( 'type' => 'money', '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' ), ), 'change' => array( 'type' => 'money', 'label' => /* translators: Template-editor field label for cash returned to the customer after payment; not "change" meaning modify. */ __( 'Change', 'woocommerce-pos' ), ), 'transaction_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Transaction ID', 'woocommerce-pos' ), ), ), ), 'refunds' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunds', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'id' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund ID', 'woocommerce-pos' ), ), 'date' => array( 'type' => 'object', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Date', 'woocommerce-pos' ), ), 'amount' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Amount', 'woocommerce-pos' ), ), 'subtotal' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Subtotal', 'woocommerce-pos' ), ), 'tax_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Tax Total', 'woocommerce-pos' ), ), 'shipping_total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ), ), 'shipping_tax' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Tax', 'woocommerce-pos' ), ), 'reason' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Reason', 'woocommerce-pos' ), ), 'refunded_by_id' => array( 'type' => 'number', 'nullable' => true, 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (User ID)', 'woocommerce-pos' ), ), 'refunded_by_name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded By (Name)', 'woocommerce-pos' ), ), 'refunded_payment' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refunded Payment', 'woocommerce-pos' ), ), 'destination' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Destination', 'woocommerce-pos' ), ), 'gateway_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway ID', 'woocommerce-pos' ), ), 'gateway_title' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Gateway Title', 'woocommerce-pos' ), ), 'processing_mode' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Processing Mode', 'woocommerce-pos' ), ), 'lines' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Lines', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'name' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Product Name', 'woocommerce-pos' ), ), 'sku' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'SKU', 'woocommerce-pos' ), ), 'qty' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Quantity', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Total (excl tax)', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Line Taxes', 'woocommerce-pos' ), ), ), ), 'fees' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fees', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fee Label', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Total (excl tax)', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Fee Taxes', 'woocommerce-pos' ), ), ), ), 'shipping' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Label', 'woocommerce-pos' ), ), 'method_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Shipping Method ID', 'woocommerce-pos' ), ), 'total' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total', 'woocommerce-pos' ), ), 'total_incl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (incl tax)', 'woocommerce-pos' ), ), 'total_excl' => array( 'type' => 'money', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Total (excl tax)', 'woocommerce-pos' ), ), 'taxes' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Refund Shipping Taxes', 'woocommerce-pos' ), ), ), ), ), ), 'fiscal' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Fiscal', 'woocommerce-pos' ), 'fields' => array( 'immutable_id' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Immutable ID', 'woocommerce-pos' ), ), 'receipt_number' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Receipt Number', 'woocommerce-pos' ), ), 'sequence' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Sequence', 'woocommerce-pos' ), ), 'hash' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Hash', 'woocommerce-pos' ), ), 'qr_payload' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'QR Payload', 'woocommerce-pos' ), ), 'tax_agency_code' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Tax Agency Code', 'woocommerce-pos' ), ), 'signed_at' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signed At', 'woocommerce-pos' ), ), 'signature_excerpt' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Signature Excerpt', 'woocommerce-pos' ), ), 'document_label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Document Label', 'woocommerce-pos' ), ), 'is_reprint' => array( 'type' => 'boolean', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Is Reprint', 'woocommerce-pos' ), ), 'reprint_count' => array( 'type' => 'number', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Reprint Count', 'woocommerce-pos' ), ), 'extra_fields' => array( 'type' => 'array', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Extra Fields', 'woocommerce-pos' ), 'is_array' => true, 'fields' => array( 'label' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Label', 'woocommerce-pos' ), ), 'value' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Value', 'woocommerce-pos' ), ), ), ), ), ), 'i18n' => array( 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Labels (i18n)', 'woocommerce-pos' ), 'fields' => self::get_i18n_field_tree_fields(), ), ); } /** * Get field metadata for every canonical i18n receipt label. * * @return array */ private static function get_i18n_field_tree_fields(): array { $fields = array(); foreach ( Receipt_I18n_Labels::get_labels() as $key => $label ) { $fields[ $key ] = array( 'type' => 'string', 'label' => $label, ); } return $fields; } /** * Get field metadata for a semantic date section. * * @return array */ private static function get_date_field_tree_fields(): array { return array( 'datetime' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date & Time', 'woocommerce-pos' ), ), 'date' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Date', 'woocommerce-pos' ), ), 'time' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Time', 'woocommerce-pos' ), ), 'datetime_short' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date & Time', 'woocommerce-pos' ), ), 'datetime_long' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date & Time', 'woocommerce-pos' ), ), 'datetime_full' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date & Time', 'woocommerce-pos' ), ), 'date_short' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Short Date', 'woocommerce-pos' ), ), 'date_long' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Long Date', 'woocommerce-pos' ), ), 'date_full' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Full Date', 'woocommerce-pos' ), ), 'date_ymd' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'YYYY-MM-DD', 'woocommerce-pos' ), ), 'date_dmy' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'DD/MM/YYYY', 'woocommerce-pos' ), ), 'date_mdy' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'MM/DD/YYYY', 'woocommerce-pos' ), ), 'weekday_short' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Short', 'woocommerce-pos' ), ), 'weekday_long' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Weekday Long', 'woocommerce-pos' ), ), 'day' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Day', 'woocommerce-pos' ), ), 'month' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Number', 'woocommerce-pos' ), ), 'month_short' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Short', 'woocommerce-pos' ), ), 'month_long' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Month Long', 'woocommerce-pos' ), ), 'year' => array( 'type' => 'string', 'label' => /* translators: Label for a receipt data field in the template editor. */ __( 'Year', 'woocommerce-pos' ), ), ); } /** * Get the JSON Schema for canonical receipt_data payloads. * * PHP remains the source of truth in this repository. This export is used by * generated TypeScript artifacts and downstream renderer/studio checks. * * @return array JSON-Schema-compatible receipt data schema. */ public static function get_json_schema(): array { $schema = array( '$schema' => 'https://json-schema.org/draft/2020-12/schema', '$id' => 'https://wcpos.com/schemas/receipt-data.schema.json', 'x-schema-version' => self::SCHEMA_VERSION, 'title' => 'ReceiptData', 'type' => 'object', 'additionalProperties' => true, 'required' => self::REQUIRED_KEYS, 'properties' => array(), ); foreach ( self::REQUIRED_KEYS as $key ) { $schema['properties'][ $key ] = self::get_default_section_schema( $key ); } foreach ( self::get_field_tree() as $path => $section ) { self::merge_field_tree_section_schema( $schema, $path, $section ); } unset( $schema['properties']['presentation_hints']['properties'] ); return $schema; } /** * Get a default schema for a top-level receipt section. * * @param string $key Top-level receipt data key. * * @return array */ private static function get_default_section_schema( string $key ): array { if ( 'has_tax_summary' === $key ) { return array( 'type' => 'boolean', 'description' => 'Whether tax_summary contains at least one row.', ); } if ( \in_array( $key, array( 'lines', 'fees', 'shipping', 'discounts', 'tax_summary', 'payments', 'refunds' ), true ) ) { return array( 'type' => 'array', 'items' => array( 'type' => 'object', 'additionalProperties' => true, 'properties' => array(), ), 'additionalProperties' => true, ); } return array( 'type' => 'object', 'additionalProperties' => true, 'properties' => array(), ); } /** * Merge a template editor field-tree section into the JSON Schema. * * @param array $schema Full schema, passed by reference. * @param string $path Dot path for the field-tree section. * @param array $section Field-tree section metadata. * * @return void */ private static function merge_field_tree_section_schema( array &$schema, string $path, array $section ): void { $segments = explode( '.', $path ); $top_key = array_shift( $segments ); if ( ! \is_string( $top_key ) || '' === $top_key ) { return; } if ( ! isset( $schema['properties'][ $top_key ] ) ) { $schema['properties'][ $top_key ] = self::get_default_section_schema( $top_key ); } $target =& $schema['properties'][ $top_key ]; $target_is_collection_item = false; if ( 'array' === ( $target['type'] ?? null ) ) { $target =& $target['items']; $target_is_collection_item = true; } foreach ( $segments as $segment ) { if ( ! isset( $target['properties'][ $segment ] ) ) { $target['properties'][ $segment ] = array( 'type' => 'object', 'additionalProperties' => true, 'properties' => array(), ); } $target =& $target['properties'][ $segment ]; } $target['description'] = isset( $section['label'] ) ? (string) $section['label'] : $path; if ( isset( $section['type'] ) && empty( $section['fields'] ) && empty( $segments ) ) { $target = self::field_metadata_to_json_schema( $section ); return; } if ( ! empty( $section['is_array'] ) && ! $target_is_collection_item ) { $target['type'] = 'array'; $target['items'] = $target['items'] ?? array( 'type' => 'object', 'additionalProperties' => true, 'properties' => array(), ); // Strip object-only keywords that may have been seeded by the // segment-walk above; they are invalid on an array schema. unset( $target['additionalProperties'], $target['properties'] ); } else { $target['type'] = 'object'; $target['additionalProperties'] = true; $target['properties'] = $target['properties'] ?? array(); } $field_target =& $target; if ( 'array' === ( $target['type'] ?? null ) ) { $field_target =& $target['items']; } foreach ( $section['fields'] ?? array() as $field_name => $field ) { $field_target['properties'][ $field_name ] = self::field_metadata_to_json_schema( $field ); } } /** * Convert one field-tree field to a JSON Schema property. * * @param array $field Field metadata. * * @return array */ private static function field_metadata_to_json_schema( array $field ): array { $type = isset( $field['type'] ) ? (string) $field['type'] : 'string'; $schema_type = self::field_type_to_json_type( $type ); // Honor a nullable flag from the field-tree metadata: producers may // legitimately emit null for a field whose JSON Schema type would // otherwise reject it (e.g. refunds[].refunded_by_id when no user). if ( ! empty( $field['nullable'] ) ) { if ( \is_array( $schema_type ) ) { if ( ! \in_array( 'null', $schema_type, true ) ) { $schema_type[] = 'null'; } } else { $schema_type = array( $schema_type, 'null' ); } } $schema = array( 'type' => $schema_type, 'description' => isset( $field['label'] ) ? (string) $field['label'] : '', ); if ( \in_array( $type, array( 'string[]', 'array' ), true ) ) { $schema['items'] = array( 'type' => 'string[]' === $type ? 'string' : array( 'string', 'number', 'boolean', 'object', 'array', 'null' ) ); } if ( ! empty( $field['is_array'] ) && ! empty( $field['fields'] ) ) { $schema['type'] = 'array'; $schema['items'] = array( 'type' => 'object', 'additionalProperties' => true, 'properties' => array(), ); foreach ( $field['fields'] as $child_name => $child_field ) { $schema['items']['properties'][ $child_name ] = self::field_metadata_to_json_schema( $child_field ); } } return $schema; } /** * Map template field-tree scalar types to JSON Schema types. * * @param string $type Field-tree type. * * @return string|array */ private static function field_type_to_json_type( string $type ) { switch ( $type ) { case 'number': return 'number'; case 'boolean': return 'boolean'; case 'money': return array( 'number', 'string' ); case 'object': return 'object'; case 'array': case 'string[]': return 'array'; case 'string': default: return 'string'; } } /** * Get mock receipt data for template preview. * * Returns a representative receipt payload with realistic values * for use in the template editor preview and tests. * * @return array Mock receipt data. */ public static function get_mock_receipt_data(): array { $created = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:30:00 UTC' ) ); $paid = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:35:00 UTC' ) ); $completed = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:42:00 UTC' ) ); $printed = Receipt_Date_Formatter::from_timestamp( strtotime( '2024-01-15 10:45:00 UTC' ) ); return array( 'has_tax_summary' => true, 'order' => array( 'id' => 1001, 'number' => '1001', 'currency' => 'USD', 'customer_note' => '', 'wc_status' => 'completed', 'status_label' => 'Completed', 'created_via' => 'woocommerce-pos', 'created' => $created, 'paid' => $paid, 'completed' => $completed, 'printed' => $printed, 'needs_payment' => false, 'payment_url' => '', ), 'store' => array( 'id' => 1, 'name' => 'My Store', 'address' => array( 'address_1' => '123 Main St', 'address_2' => '', 'city' => 'Anytown', 'state' => 'CA', 'postcode' => '90210', 'country' => 'US', ), 'address_lines' => array( '123 Main St', 'Anytown, CA 90210' ), 'tax_ids' => array( array( 'type' => 'us_ein', 'value' => '12-3456789', 'country' => 'US', 'label' => 'EIN', ), ), 'phone' => '+1 (555) 123-4567', 'email' => 'hello@mystore.com', 'logo' => 'https://example.com/logo.png', '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", '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", '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", 'opening_hours_notes' => 'Closed on public holidays', 'personal_notes' => '', 'policies_and_conditions' => '', 'footer_imprint' => '', ), 'cashier' => array( 'id' => 1, 'name' => 'Admin', ), ); } }