PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.20
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.20
1.10.20 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 All 164 releases
← All changes | includes/Services/Receipt_Data_Builder.php +184 -349 1.9.15 → 1.10.20 View file →
@@ -18,17 +18,14 @@
18 18 /**
19 19 * Build a canonical receipt payload.
20 20 *
21 21 * @param WC_Abstract_Order $order Receipt order.
22 - * @param string $mode Reserved for caller compatibility; the receipt mode is
23 - * carried in the request, not the payload.
22 + * @param string $mode Receipt mode ('live', 'fiscal', 'preview'), passed unchanged to the receipt data filter.
24 23 * @param object|null $pos_store POS store object. Falls back to order meta or default.
25 24 *
26 25 * @return array
27 26 */
28 27 public function build( WC_Abstract_Order $order, string $mode = 'live', $pos_store = null ): array {
29 - unset( $mode );
30 -
31 28 $wc_status = method_exists( $order, 'get_status' ) ? (string) $order->get_status() : '';
32 29 $status_label = '';
33 30 if ( '' !== $wc_status && function_exists( 'wc_get_order_status_name' ) ) {
34 31 $status_label = (string) wc_get_order_status_name( $wc_status );
@@ -85,78 +82,22 @@
85 82 get_option( 'woocommerce_tax_display_cart', 'excl' )
86 83 );
87 84 $presentation_hints = $store_resolver->build_presentation_hints( (string) $order->get_currency() );
88 85 $tax = $store_resolver->build_tax_section();
89 - $store_id = (int) $store_resolver->get_store_value( 'get_id', 0 );
90 - $store_name = (string) $store_resolver->get_store_value( 'get_name', '' );
86 +
87 + // $missing_order_store_id > 0 only ever happens alongside the bare \stdClass
88 + // assigned above, so no getter resolves and every fallback below is taken.
89 + // That is what keeps a deleted store's receipt showing the recorded store ID
90 + // rather than silently borrowing the current store's name and address.
91 + $store_fallbacks = array();
91 92 if ( $missing_order_store_id > 0 ) {
92 - $store_id = $missing_order_store_id;
93 + $store_fallbacks['id'] = $missing_order_store_id;
93 94 // translators: %d: Historical POS store ID that no longer exists.
94 - $store_name = sprintf( __( 'Store #%d', 'woocommerce-pos' ), $missing_order_store_id );
95 + $store_fallbacks['name'] = sprintf( __( 'Store #%d', 'woocommerce-pos' ), $missing_order_store_id );
95 96 }
96 - $store_address = (string) $store_resolver->get_store_value( 'get_store_address', '' );
97 - $store_address_2 = (string) $store_resolver->get_store_value( 'get_store_address_2', '' );
98 - $store_city = (string) $store_resolver->get_store_value( 'get_store_city', '' );
99 - $store_postcode = (string) $store_resolver->get_store_value( 'get_store_postcode', '' );
100 - $store_country = (string) $store_resolver->get_store_value( 'get_store_country', '' );
101 - $store_state = (string) $store_resolver->get_store_value( 'get_store_state', '' );
102 - $store_phone = (string) $store_resolver->get_store_value( 'get_phone', '' );
103 - $store_email = (string) $store_resolver->get_store_value( 'get_email', '' );
104 97
105 - $store_tax_ids = $store_resolver->get_store_value( 'get_tax_ids', array() );
106 - if ( ! is_array( $store_tax_ids ) ) {
107 - $store_tax_ids = array();
108 - }
109 - $store_tax_ids = Receipt_Store_Resolver::with_store_tax_id_labels( $store_tax_ids, $presentation_hints['locale'] ?? '' );
98 + $store = $store_resolver->build_store_section( $store_fallbacks );
110 99
111 - $store_address_parts = array(
112 - 'address_1' => $store_address,
113 - 'address_2' => $store_address_2,
114 - 'city' => $store_city,
115 - 'state' => $store_state,
116 - 'postcode' => $store_postcode,
117 - 'country' => $store_country,
118 - );
119 -
120 - $store = array(
121 - 'id' => $store_id,
122 - 'name' => '' !== $store_name ? $store_name : get_bloginfo( 'name' ),
123 - // Structured address parts mirror customer.billing_address — templates that
124 - // want country-specific layouts compose from these. address_lines[] is the
125 - // pre-formatted default for templates that just iterate, composed via
126 - // WC_Countries::get_formatted_address() so per-country layouts are honoured.
127 - 'address' => $store_address_parts,
128 - 'address_lines' => Receipt_Store_Resolver::compose_address_lines( $store_address_parts ),
129 - 'tax_ids' => $store_tax_ids,
130 - 'phone' => $store_phone,
131 - 'email' => $store_email,
132 - );
133 -
134 - $opening_hours_raw = $store_resolver->get_store_value( 'get_opening_hours', array() );
135 - $personal_notes = (string) $store_resolver->get_store_value( 'get_personal_notes', '' );
136 - $policies_and_conditions = (string) $store_resolver->get_store_value( 'get_policies_and_conditions', '' );
137 - $footer_imprint = (string) $store_resolver->get_store_value( 'get_footer_imprint', '' );
138 -
139 - $store['logo'] = Store_Logo_Resolver::resolve( $pos_store );
140 - if ( ! empty( $opening_hours_raw ) && \is_array( $opening_hours_raw ) ) {
141 - $store['opening_hours'] = Opening_Hours_Formatter::format_compact( $opening_hours_raw );
142 - $store['opening_hours_vertical'] = Opening_Hours_Formatter::format_vertical( $opening_hours_raw );
143 - $store['opening_hours_inline'] = Opening_Hours_Formatter::format_inline( $opening_hours_raw );
144 - } elseif ( \is_string( $opening_hours_raw ) && '' !== trim( $opening_hours_raw ) ) {
145 - $store['opening_hours'] = $opening_hours_raw;
146 - $store['opening_hours_vertical'] = null;
147 - $store['opening_hours_inline'] = null;
148 - } else {
149 - $store['opening_hours'] = null;
150 - $store['opening_hours_vertical'] = null;
151 - $store['opening_hours_inline'] = null;
152 - }
153 - $opening_hours_notes = (string) $store_resolver->get_store_value( 'get_opening_hours_notes', '' );
154 - $store['opening_hours_notes'] = '' !== $opening_hours_notes ? $opening_hours_notes : null;
155 - $store['personal_notes'] = $personal_notes ? $personal_notes : null;
156 - $store['policies_and_conditions'] = $policies_and_conditions ? $policies_and_conditions : null;
157 - $store['footer_imprint'] = $footer_imprint ? $footer_imprint : null;
158 -
159 100 $cashier = array(
160 101 'id' => (int) $order->get_meta( '_pos_user' ),
161 102 'name' => '',
162 103 );
@@ -174,9 +115,9 @@
174 115 $customer_name = /* translators: Short WCPOS UI label; keep concise. */ __( 'Guest', 'woocommerce-pos' );
175 116 }
176 117
177 118 $tax_ids = ( new Tax_Id_Reader() )->read_for_order( $order );
178 - $tax_ids = self::with_customer_tax_id_labels( $tax_ids, $presentation_hints['locale'] ?? '' );
119 + $tax_ids = Receipt_Sections::label_tax_ids( $tax_ids, 'customer', $presentation_hints['locale'] ?? '' );
179 120
180 121 $customer = array(
181 122 'id' => $customer_id ? $customer_id : null,
182 123 'name' => $customer_name,
@@ -221,9 +162,8 @@
221 162 : 0.0;
222 163 $price_convenience = $this->get_line_price_convenience_fields(
223 164 $item,
224 165 $order,
225 - $display_incl,
226 166 $qty,
227 167 $unit_subtotal_incl,
228 168 $unit_subtotal_excl,
229 169 $line_subtotal_incl,
@@ -237,29 +177,34 @@
237 177 'sku' => $item->get_product() ? $item->get_product()->get_sku() : '',
238 178 'name' => $item->get_name(),
239 179 'qty' => $qty,
240 180 'qty_refunded' => $qty_refunded,
241 - 'unit_subtotal' => $display_incl ? $unit_subtotal_incl : $unit_subtotal_excl,
242 - 'unit_subtotal_incl' => $unit_subtotal_incl,
243 - 'unit_subtotal_excl' => $unit_subtotal_excl,
244 - 'unit_price' => $display_incl ? $unit_price_incl : $unit_price_excl,
245 - 'unit_price_incl' => $unit_price_incl,
246 - 'unit_price_excl' => $unit_price_excl,
247 - 'line_subtotal' => $display_incl ? $line_subtotal_incl : $line_subtotal_excl,
248 - 'line_subtotal_incl' => $line_subtotal_incl,
249 - 'line_subtotal_excl' => $line_subtotal_excl,
250 - 'discounts' => $display_incl ? $discounts_incl : $discounts_excl,
251 - 'discounts_incl' => $discounts_incl,
252 - 'discounts_excl' => $discounts_excl,
253 - 'line_total' => $display_incl ? $line_total_incl : $line_total_excl,
254 - 'line_total_incl' => $line_total_incl,
255 - 'line_total_excl' => $line_total_excl,
181 + 'unit_subtotal' => array(
182 + 'incl' => $unit_subtotal_incl,
183 + 'excl' => $unit_subtotal_excl,
184 + ),
185 + 'unit_price' => array(
186 + 'incl' => $unit_price_incl,
187 + 'excl' => $unit_price_excl,
188 + ),
189 + 'line_subtotal' => array(
190 + 'incl' => $line_subtotal_incl,
191 + 'excl' => $line_subtotal_excl,
192 + ),
193 + 'discounts' => array(
194 + 'incl' => $discounts_incl,
195 + 'excl' => $discounts_excl,
196 + ),
197 + 'line_total' => array(
198 + 'incl' => $line_total_incl,
199 + 'excl' => $line_total_excl,
200 + ),
256 201 'total_refunded' => $total_refunded,
257 202 'taxes' => $this->get_line_taxes( $item ),
258 203 'meta' => $this->get_item_meta_pairs( $item ),
259 204 'attributes' => $this->get_product_attribute_pairs( $item ),
260 205 );
261 - $lines[] = array_merge( $line, $price_convenience );
206 + $lines[] = Receipt_Sections::line( array_merge( $line, $price_convenience ), $display_incl );
262 207 }
263 208
264 209 $shipping = array();
265 210 foreach ( $order->get_items( 'shipping' ) as $shipping_item ) {
@@ -268,16 +213,18 @@
268 213 }
269 214 $ship_total_excl = (float) $shipping_item->get_total();
270 215 $ship_total_tax = (float) $shipping_item->get_total_tax();
271 216 $ship_total_incl = $ship_total_excl + $ship_total_tax;
272 - $shipping[] = array(
273 - 'label' => $shipping_item->get_name(),
274 - 'method_id' => (string) $shipping_item->get_method_id(),
275 - 'total' => $display_incl ? $ship_total_incl : $ship_total_excl,
276 - 'total_incl' => $ship_total_incl,
277 - 'total_excl' => $ship_total_excl,
278 - 'taxes' => $this->get_item_taxes( $shipping_item ),
279 - 'meta' => $this->get_item_meta_pairs( $shipping_item ),
217 + $shipping[] = Receipt_Sections::shipping(
218 + $shipping_item->get_name(),
219 + (string) $shipping_item->get_method_id(),
220 + array(
221 + 'incl' => $ship_total_incl,
222 + 'excl' => $ship_total_excl,
223 + ),
224 + $this->get_item_taxes( $shipping_item ),
225 + $this->get_item_meta_pairs( $shipping_item ),
226 + $display_incl
280 227 );
281 228 }
282 229
283 230 $fees = array();
@@ -284,15 +231,17 @@
284 231 foreach ( $order->get_fees() as $fee ) {
285 232 $fee_total_excl = (float) $fee->get_total();
286 233 $fee_total_tax = (float) $fee->get_total_tax();
287 234 $fee_total_incl = $fee_total_excl + $fee_total_tax;
288 - $fees[] = array(
289 - 'label' => $fee->get_name(),
290 - 'total' => $display_incl ? $fee_total_incl : $fee_total_excl,
291 - 'total_incl' => $fee_total_incl,
292 - 'total_excl' => $fee_total_excl,
293 - 'taxes' => $this->get_item_taxes( $fee ),
294 - 'meta' => $this->get_item_meta_pairs( $fee ),
235 + $fees[] = Receipt_Sections::fee(
236 + $fee->get_name(),
237 + array(
238 + 'incl' => $fee_total_incl,
239 + 'excl' => $fee_total_excl,
240 + ),
241 + $this->get_item_taxes( $fee ),
242 + $this->get_item_meta_pairs( $fee ),
243 + $display_incl
295 244 );
296 245 }
297 246
298 247 $discounts = array();
@@ -299,17 +248,21 @@
299 248 foreach ( $order->get_items( 'coupon' ) as $coupon_item ) {
300 249 if ( ! $coupon_item instanceof \WC_Order_Item_Coupon ) {
301 250 continue;
302 251 }
252 + $coupon = $this->get_order_coupon( $coupon_item );
303 253 $coupon_excl = (float) $coupon_item->get_discount();
304 254 $coupon_tax = (float) $coupon_item->get_discount_tax();
305 255 $coupon_incl = $coupon_excl + $coupon_tax;
306 - $discounts[] = array(
307 - 'label' => $this->get_coupon_label( $coupon_item ),
308 - 'code' => $coupon_item->get_code(),
309 - 'total' => $display_incl ? $coupon_incl : $coupon_excl,
310 - 'total_incl' => $coupon_incl,
311 - 'total_excl' => $coupon_excl,
256 + $discounts[] = Receipt_Sections::discount(
257 + $this->get_coupon_label( $coupon_item, $coupon ),
258 + $coupon_item->get_code(),
259 + $coupon ? (string) $coupon->get_discount_type() : '',
260 + array(
261 + 'incl' => $coupon_incl,
262 + 'excl' => $coupon_excl,
263 + ),
264 + $display_incl
312 265 );
313 266 }
314 267
315 268 $discount_total_excl = (float) $order->get_discount_total();
@@ -315,155 +268,101 @@
315 268 $discount_total_excl = (float) $order->get_discount_total();
316 269 $discount_total_tax = (float) $order->get_discount_tax();
317 270 $discount_total_incl = $discount_total_excl + $discount_total_tax;
318 271
319 - // Legacy POS lines already include regular-to-selling savings in WooCommerce's
320 - // discount total. Add only current-shape savings to total_saved to avoid overlap.
321 - $sale_savings_totals = array(
322 - 'incl' => 0.0,
323 - 'excl' => 0.0,
324 - );
325 - $additional_savings = array(
326 - 'incl' => 0.0,
327 - 'excl' => 0.0,
328 - );
329 - $savings_complete = array(
330 - 'incl' => true,
331 - 'excl' => true,
332 - );
333 - $price_precision = wc_get_price_decimals();
334 - foreach ( $lines as $line ) {
335 - foreach ( array( 'incl', 'excl' ) as $basis ) {
336 - $key = 'line_savings_' . $basis;
337 - if ( ! isset( $line[ $key ] ) || ! is_numeric( $line[ $key ] ) ) {
338 - $savings_complete[ $basis ] = false;
339 - continue;
340 - }
341 -
342 - $line_savings = (float) $line[ $key ];
343 - $sale_savings_totals[ $basis ] += $line_savings;
344 - if ( empty( $line['savings_in_discounts'] ) ) {
345 - $subtotal_key = 'line_subtotal_' . $basis;
346 - $selling_key = 'line_selling_total_' . $basis;
347 - if (
348 - $line_savings > 0.0
349 - && (
350 - ! isset( $line[ $subtotal_key ], $line[ $selling_key ] )
351 - || round( abs( (float) $line[ $subtotal_key ] - (float) $line[ $selling_key ] ), $price_precision ) > 0.0
352 - )
353 - ) {
354 - $savings_complete[ $basis ] = false;
355 - continue;
356 - }
357 - $additional_savings[ $basis ] += $line_savings;
358 - }
359 - }
360 - }
361 -
362 - $total_saved = array(
363 - 'incl' => $savings_complete['incl'] ? $discount_total_incl + $additional_savings['incl'] : null,
364 - 'excl' => $savings_complete['excl'] ? $discount_total_excl + $additional_savings['excl'] : null,
365 - );
366 - foreach ( array( 'incl', 'excl' ) as $basis ) {
367 - if ( ! $savings_complete[ $basis ] ) {
368 - $sale_savings_totals[ $basis ] = null;
369 - }
370 - }
371 - $display_basis = $display_incl ? 'incl' : 'excl';
372 -
373 - $subtotal_excl = array_sum( array_column( $lines, 'line_subtotal_excl' ) );
374 - $subtotal_incl = array_sum( array_column( $lines, 'line_subtotal_incl' ) );
375 -
376 - // Item count summaries — useful for packing slips and kitchen tickets
377 - // where Mustache can't sum/count an array at render time.
378 - $total_qty = (float) array_sum( array_column( $lines, 'qty' ) );
379 - $line_count = \count( $lines );
380 -
381 272 $tax_total = (float) $order->get_total_tax();
382 - $total = (float) $order->get_total();
383 -
384 - $total_excl = $total - $tax_total;
385 - $refund_total = method_exists( $order, 'get_total_refunded' )
273 + $total = (float) $order->get_total();
274 + $refund_total = method_exists( $order, 'get_total_refunded' )
386 275 ? abs( (float) $order->get_total_refunded() )
387 276 : 0.0;
388 - // Templates render the customer-facing balance after a partial refund.
389 - // Stays at 0 when nothing was refunded so detailed-receipt's section
390 - // guard `{{#totals.net_total}}…{{/totals.net_total}}` collapses.
391 - $net_total = $refund_total > 0 ? max( 0.0, $total - $refund_total ) : 0.0;
392 -
393 - $totals = array(
394 - 'subtotal' => $display_incl ? $subtotal_incl : $subtotal_excl,
395 - 'subtotal_incl' => $subtotal_incl,
396 - 'subtotal_excl' => $subtotal_excl,
397 - 'discount_total' => $display_incl ? $discount_total_incl : $discount_total_excl,
398 - 'discount_total_incl' => $discount_total_incl,
399 - 'discount_total_excl' => $discount_total_excl,
400 - 'sale_savings_total' => $sale_savings_totals[ $display_basis ],
401 - 'sale_savings_total_incl' => $sale_savings_totals['incl'],
402 - 'sale_savings_total_excl' => $sale_savings_totals['excl'],
403 - 'total_saved' => $total_saved[ $display_basis ],
404 - 'total_saved_incl' => $total_saved['incl'],
405 - 'total_saved_excl' => $total_saved['excl'],
406 - 'total_saved_complete' => $savings_complete[ $display_basis ],
407 - 'tax_total' => $tax_total,
408 - 'total' => $display_incl ? $total : $total_excl,
409 - 'total_incl' => $total,
410 - 'total_excl' => $total_excl,
411 - 'paid_total' => $total,
412 - 'change_total' => (float) $order->get_meta( '_pos_cash_change' ),
413 - 'refund_total' => $refund_total,
414 - 'net_total' => $net_total,
415 - 'total_qty' => $total_qty,
416 - 'line_count' => $line_count,
277 + $totals = Receipt_Sections::totals(
278 + $lines,
279 + array(
280 + 'discount_total' => array(
281 + 'incl' => $discount_total_incl,
282 + 'excl' => $discount_total_excl,
283 + ),
284 + 'tax_total' => $tax_total,
285 + 'total' => $total,
286 + 'paid_total' => $total,
287 + 'change_total' => (float) $order->get_meta( '_pos_cash_change' ),
288 + 'refund_total' => $refund_total,
289 + ),
290 + $display_incl
417 291 );
418 292
419 293 $payments = array(
420 - array(
421 - 'method_id' => $order->get_payment_method(),
422 - 'method_title' => $order->get_payment_method_title(),
423 - 'amount' => $total,
424 - 'transaction_id' => (string) $order->get_transaction_id(),
425 - 'tendered' => (float) $order->get_meta( '_pos_cash_amount_tendered' ),
426 - 'change' => (float) $order->get_meta( '_pos_cash_change' ),
294 + Receipt_Sections::payment(
295 + $order->get_payment_method(),
296 + $order->get_payment_method_title(),
297 + $total,
298 + (string) $order->get_transaction_id(),
299 + (float) $order->get_meta( '_pos_cash_amount_tendered' ),
300 + (float) $order->get_meta( '_pos_cash_change' )
427 301 ),
428 302 );
429 303
430 304 $tax_summary = $this->get_tax_summary( $order );
431 305
432 - $fiscal = array(
433 - 'immutable_id' => '',
434 - 'receipt_number' => '',
435 - 'sequence' => null,
436 - 'hash' => '',
437 - 'qr_payload' => '',
438 - 'tax_agency_code' => '',
439 - 'signed_at' => '',
440 - 'signature_excerpt' => '',
441 - 'document_label' => '',
442 - 'is_reprint' => false,
443 - 'reprint_count' => 0,
444 - 'extra_fields' => array(),
306 + $fiscal = Receipt_Payload_Assembler::fiscal(
307 + array(
308 + 'immutable_id' => '',
309 + 'receipt_number' => '',
310 + 'sequence' => null,
311 + 'hash' => '',
312 + 'qr_payload' => '',
313 + 'tax_agency_code' => '',
314 + 'signed_at' => '',
315 + 'signature_excerpt' => '',
316 + 'document_label' => '',
317 + 'is_reprint' => false,
318 + 'reprint_count' => 0,
319 + 'extra_fields' => array(),
320 + )
445 321 );
446 322
447 - return array(
448 - 'order' => $order_data,
449 - 'store' => $store,
450 - 'cashier' => $cashier,
451 - 'customer' => $customer,
452 - 'lines' => $lines,
453 - 'fees' => $fees,
454 - 'shipping' => $shipping,
455 - 'discounts' => $discounts,
456 - 'totals' => $totals,
457 - 'tax' => $tax,
458 - 'tax_summary' => $tax_summary,
459 - 'has_tax_summary' => ! empty( $tax_summary ),
460 - 'payments' => $payments,
461 - 'refunds' => $this->get_refunds( $order, $display_incl, $date_timezone, $date_locale ),
462 - 'fiscal' => $fiscal,
463 - 'presentation_hints' => $presentation_hints,
464 - 'i18n' => Receipt_I18n_Labels::get_labels( $presentation_hints['locale'] ?? '' ),
323 + $data = Receipt_Payload_Assembler::assemble(
324 + array(
325 + 'order' => $order_data,
326 + 'store' => $store,
327 + 'cashier' => $cashier,
328 + 'customer' => $customer,
329 + 'lines' => $lines,
330 + 'fees' => $fees,
331 + 'shipping' => $shipping,
332 + 'discounts' => $discounts,
333 + 'totals' => $totals,
334 + 'tax' => $tax,
335 + 'tax_summary' => $tax_summary,
336 + 'payments' => $payments,
337 + 'refunds' => $this->get_refunds( $order, $display_incl, $date_timezone, $date_locale ),
338 + 'fiscal' => $fiscal,
339 + 'presentation_hints' => $presentation_hints,
340 + )
465 341 );
342 +
343 + /**
344 + * Filters the canonical receipt data before it is rendered or snapshotted.
345 + *
346 + * Runs for every receipt this builder produces: live receipts, fiscal
347 + * snapshots captured at payment time, PDF downloads and legacy PHP
348 + * templates. Extensions can add their own keys to any section (for
349 + * example a flag on a `discounts[]` row) or adjust labels. Keys defined
350 + * by Receipt_Data_Schema should keep their documented types.
351 + *
352 + * Sample previews for the template editor and gallery also run through this filter,
353 + * with mode `preview` and an unsaved order whose id is 0; a plugin that needs a
354 + * persisted order should return `$data` unchanged when `$order->get_id()` is 0.
355 + *
356 + * @param array $data Receipt data (see Receipt_Data_Schema).
357 + * @param WC_Abstract_Order $order Order the receipt is for.
358 + * @param string $mode Receipt mode: 'live', 'fiscal' or 'preview', passed unchanged.
359 + *
360 + * @since 1.10.8
361 + *
362 + * @hook woocommerce_pos_receipt_data
363 + */
364 + return (array) apply_filters( 'woocommerce_pos_receipt_data', $data, $order, $mode );
466 365 }
467 366
468 367
469 368 /**
@@ -473,17 +372,12 @@
473 372 *
474 373 * @return array{price:float,regular_price:float,tax_status:string}|null
475 374 */
476 375 private function get_pos_price_data( \WC_Order_Item_Product $item ): ?array {
477 - $raw = $item->get_meta( '_woocommerce_pos_data', true );
478 - if ( ! \is_string( $raw ) || '' === $raw ) {
479 - return null;
480 - }
481 -
482 - $data = json_decode( $raw, true );
376 + $raw = $item->get_meta( '_woocommerce_pos_data', true );
377 + $data = \WCPOS\WooCommercePOS\Sync\Meta_Normalizer::decode_to_array( $raw );
483 378 if (
484 - JSON_ERROR_NONE !== json_last_error()
485 - || ! \is_array( $data )
379 + ! \is_array( $data )
486 380 || ! isset( $data['price'], $data['regular_price'], $data['tax_status'] )
487 381 || ! is_numeric( $data['price'] )
488 382 || ! is_numeric( $data['regular_price'] )
489 383 || ! \in_array( $data['tax_status'], array( 'none', 'taxable', 'shipping' ), true )
@@ -540,9 +434,8 @@
540 434 * Derive recorded prices and savings without changing WooCommerce discounts.
541 435 *
542 436 * @param \WC_Order_Item_Product $item Order item.
543 437 * @param WC_Abstract_Order $order Receipt order.
544 - * @param bool $display_incl Whether generic values include tax.
545 438 * @param float $qty Item quantity.
546 439 * @param float $unit_subtotal_incl Stored unit subtotal including tax.
547 440 * @param float $unit_subtotal_excl Stored unit subtotal excluding tax.
548 441 * @param float $subtotal_incl Stored line subtotal including tax.
@@ -549,14 +442,13 @@
549 442 * @param float $subtotal_excl Stored line subtotal excluding tax.
550 443 * @param float $total_incl Stored line total including tax.
551 444 * @param float $total_excl Stored line total excluding tax.
552 445 *
553 - * @return array<string,float|bool|null>
446 + * @return array<string,array|bool>
554 447 */
555 448 private function get_line_price_convenience_fields(
556 449 \WC_Order_Item_Product $item,
557 450 WC_Abstract_Order $order,
558 - bool $display_incl,
559 451 float $qty,
560 452 float $unit_subtotal_incl,
561 453 float $unit_subtotal_excl,
562 454 float $subtotal_incl,
@@ -648,58 +540,58 @@
648 540 && $stored_discount >= $recorded_savings;
649 541 break;
650 542 }
651 543
652 - $select = static function ( array $values ) use ( $display_incl ): ?float {
653 - return $display_incl ? $values['incl'] : $values['excl'];
654 - };
655 -
656 544 return array(
657 - 'regular_price' => $select( $regular ),
658 - 'regular_price_incl' => $regular['incl'],
659 - 'regular_price_excl' => $regular['excl'],
660 - 'selling_price' => $select( $selling ),
661 - 'selling_price_incl' => $selling['incl'],
662 - 'selling_price_excl' => $selling['excl'],
663 - 'unit_savings' => $select( $unit_savings ),
664 - 'unit_savings_incl' => $unit_savings['incl'],
665 - 'unit_savings_excl' => $unit_savings['excl'],
666 - 'line_regular_total' => $select( $line_regular ),
667 - 'line_regular_total_incl' => $line_regular['incl'],
668 - 'line_regular_total_excl' => $line_regular['excl'],
669 - 'line_selling_total' => $select( $line_selling ),
670 - 'line_selling_total_incl' => $line_selling['incl'],
671 - 'line_selling_total_excl' => $line_selling['excl'],
672 - 'line_savings' => $select( $line_savings ),
673 - 'line_savings_incl' => $line_savings['incl'],
674 - 'line_savings_excl' => $line_savings['excl'],
675 - 'savings_in_discounts' => $savings_in_discounts,
545 + 'regular_price' => $regular,
546 + 'selling_price' => $selling,
547 + 'unit_savings' => $unit_savings,
548 + 'line_regular_total' => $line_regular,
549 + 'line_selling_total' => $line_selling,
550 + 'line_savings' => $line_savings,
551 + 'savings_in_discounts' => $savings_in_discounts,
676 552 );
677 553 }
678 554
679 555 /**
680 - * Resolve an optional human-facing coupon label for a receipt discount row.
556 + * Load the WooCommerce coupon behind an order coupon line, if it still exists.
681 557 *
682 - * Coupons are identified by code; the code is already exposed as
683 - * `discounts[].code`. Prefer a distinct, user-authored description, but
684 - * fall back to the code so templates that render `label` always have text.
558 + * Order coupon lines only store the code; the coupon post may have been
559 + * deleted since the order was placed, in which case templates fall back to
560 + * the code alone.
685 561 *
686 562 * @param \WC_Order_Item_Coupon $coupon_item Coupon order item.
687 - * @return string
563 + * @return \WC_Coupon|null
688 564 */
689 - private function get_coupon_label( \WC_Order_Item_Coupon $coupon_item ): string {
565 + private function get_order_coupon( \WC_Order_Item_Coupon $coupon_item ): ?\WC_Coupon {
690 566 $code = (string) $coupon_item->get_code();
691 567 if ( '' === $code ) {
692 - return '';
568 + return null;
693 569 }
694 570
695 571 try {
696 572 $coupon = new \WC_Coupon( $code );
697 573 } catch ( \Exception $exception ) {
698 - return $code;
574 + return null;
699 575 }
700 576
701 - if ( ! $coupon->get_id() ) {
577 + return $coupon->get_id() ? $coupon : null;
578 + }
579 +
580 + /**
581 + * Resolve an optional human-facing coupon label for a receipt discount row.
582 + *
583 + * Coupons are identified by code; the code is already exposed as
584 + * `discounts[].code`. Prefer a distinct, user-authored description, but
585 + * fall back to the code so templates that render `label` always have text.
586 + *
587 + * @param \WC_Order_Item_Coupon $coupon_item Coupon order item.
588 + * @param \WC_Coupon|null $coupon Coupon behind the line, when it still exists.
589 + * @return string
590 + */
591 + private function get_coupon_label( \WC_Order_Item_Coupon $coupon_item, ?\WC_Coupon $coupon ): string {
592 + $code = (string) $coupon_item->get_code();
593 + if ( null === $coupon ) {
702 594 return $code;
703 595 }
704 596
705 597 $label = trim( wp_strip_all_tags( (string) $coupon->get_description() ) );
@@ -723,18 +615,16 @@
723 615 $tax_amount = (float) $tax_item->get_tax_total() + (float) $tax_item->get_shipping_tax_total();
724 616 $rate = (float) $tax_item->get_rate_percent();
725 617 $rate_id = (string) $tax_item->get_rate_id();
726 618 $taxable_excl = $taxable_bases[ $rate_id ] ?? null;
727 - $taxable_incl = null !== $taxable_excl ? $taxable_excl + $tax_amount : null;
728 619
729 - $summary[] = array(
730 - 'code' => $rate_id,
731 - 'rate' => $rate > 0 ? $rate : null,
732 - 'label' => $tax_item->get_label( $order ),
733 - 'compound' => method_exists( $tax_item, 'is_compound' ) ? (bool) $tax_item->is_compound() : false,
734 - 'taxable_amount_excl' => $taxable_excl,
735 - 'tax_amount' => $tax_amount,
736 - 'taxable_amount_incl' => $taxable_incl,
620 + $summary[] = Receipt_Sections::tax_summary_entry(
621 + $rate_id,
622 + $rate,
623 + $tax_item->get_label( $order ),
624 + method_exists( $tax_item, 'is_compound' ) ? (bool) $tax_item->is_compound() : false,
625 + $taxable_excl,
626 + $tax_amount
737 627 );
738 628 }
739 629
740 630 return $summary;
@@ -923,22 +813,9 @@
923 813 return $pairs;
924 814 }
925 815
926 816 if ( $product instanceof \WC_Product_Variation ) {
927 - foreach ( $product->get_variation_attributes() as $attribute_key => $attribute_value ) {
928 - if ( '' === (string) $attribute_value ) {
929 - continue;
930 - }
931 -
932 - $taxonomy = preg_replace( '/^attribute_/', '', (string) $attribute_key );
933 - $value = $product->get_attribute( $taxonomy );
934 - $pairs[] = array(
935 - 'key' => wp_strip_all_tags( wc_attribute_label( $taxonomy, $product ) ),
936 - 'value' => wp_strip_all_tags( '' !== $value ? $value : (string) $attribute_value ),
937 - );
938 - }
939 -
940 - return $pairs;
817 + return Receipt_Sections::variation_attribute_pairs( $product );
941 818 }
942 819
943 820 foreach ( $product->get_attributes() as $attribute ) {
944 821 if ( ! $attribute instanceof \WC_Product_Attribute || ! $attribute->get_visible() ) {
@@ -1103,48 +980,6 @@
1103 980 );
1104 981 }
1105 982
1106 983 return $refunds;
1107 - }
1108 -
1109 -
1110 - /**
1111 - * Ensure customer tax IDs include display labels for logicless templates.
1112 - *
1113 - * @param array<int,array<string,mixed>> $tax_ids Customer tax IDs.
1114 - * @param string $locale Receipt locale.
1115 - * @return array<int,array<string,mixed>>
1116 - */
1117 - private static function with_customer_tax_id_labels( array $tax_ids, string $locale = '' ): array {
1118 - return self::with_tax_id_labels( $tax_ids, 'customer', $locale );
1119 - }
1120 -
1121 - /**
1122 - * Resolve a display label for each tax-ID entry. Precedence: explicit
1123 - * `label` → `<scope>_tax_id_label_<type>` i18n key → scope-specific
1124 - * `_other` fallback.
1125 - *
1126 - * @param array<int,array<string,mixed>> $tax_ids Tax IDs.
1127 - * @param string $scope "store" or "customer".
1128 - * @param string $locale Receipt locale.
1129 - * @return array<int,array<string,mixed>>
1130 - */
1131 - private static function with_tax_id_labels( array $tax_ids, string $scope, string $locale = '' ): array {
1132 - $labels = Receipt_I18n_Labels::get_labels( $locale );
1133 - $prefix = $scope . '_tax_id_label_';
1134 -
1135 - return array_map(
1136 - static function ( array $tax_id ) use ( $labels, $prefix ): array {
1137 - if ( ! empty( $tax_id['label'] ) ) {
1138 - return $tax_id;
1139 - }
1140 -
1141 - $type = isset( $tax_id['type'] ) ? (string) $tax_id['type'] : 'other';
1142 - $key = $prefix . $type;
1143 - $tax_id['label'] = $labels[ $key ] ?? $labels[ $prefix . 'other' ];
1144 -
1145 - return $tax_id;
1146 - },
1147 - $tax_ids
1148 - );
1149 984 }
1150 985 }