PluginProbe
PostNL for WooCommerce / trunk
PostNL for WooCommerce vtrunk
5.9.11 5.9.10 5.9.9 5.9.8 5.9.7 5.9.6 trunk 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.3.2 4.3.3 4.4.0 4.4.1 4.4.2 All 71 releases
woo-postnl / src / Order / Base.php

Base.php in PostNL for WooCommerce trunk, at src/Order/Base.php

1,763 lines 51.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Order\Base file.
4 *
5 * @package PostNLWooCommerce\Order
6 */
7
8 namespace PostNLWooCommerce\Order;
9
10 use PostNLWooCommerce\Utils;
11 use PostNLWooCommerce\Rest_API\Service_Factory;
12 use PostNLWooCommerce\Rest_API\Shipping;
13 use PostNLWooCommerce\Rest_API\Return_Label;
14 use PostNLWooCommerce\Rest_API\Letterbox;
15 use PostNLWooCommerce\Shipping_Method\Settings;
16 use PostNLWooCommerce\Helper\Mapping;
17 use PostNLWooCommerce\Library\CustomizedPDFMerger;
18 use Imagick;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Class Base
26 *
27 * @package PostNLWooCommerce\Order
28 */
29 abstract class Base {
30 /**
31 * Settings class instance.
32 *
33 * @var PostNLWooCommerce\Shipping_Method\Settings
34 */
35 protected $settings;
36
37 /**
38 * Lazy-initialised Service_Factory instance.
39 *
40 * @var Service_Factory|null
41 */
42 private $service_factory_instance = null;
43
44 /**
45 * Nonce key for ajax call.
46 *
47 * @var string nonce_key.
48 */
49 protected $nonce_key = 'create-postnl-label';
50
51 /**
52 * Current service.
53 *
54 * @var string service.
55 */
56 protected $service = POSTNL_SERVICE_NAME;
57
58 /**
59 * Prefix for meta box fields.
60 *
61 * @var string prefix.
62 */
63 protected $prefix = POSTNL_SETTINGS_ID . '_';
64
65 /**
66 * Meta name for saved fields.
67 *
68 * @var string.
69 */
70 protected $meta_name;
71
72 /**
73 * Is return activated flag meta name.
74 *
75 * @var string.
76 */
77 protected $is_return_activated_meta;
78
79 /**
80 * Init and hook in the integration.
81 */
82 public function __construct() {
83 $this->settings = Settings::get_instance();
84 $this->meta_name = '_' . $this->prefix . 'order_metadata';
85 $this->is_return_activated_meta = '_' . $this->prefix . 'return_activated';
86 $this->init_hooks();
87 }
88
89 /**
90 * Abstract function for collection of hooks when initiation.
91 */
92 abstract public function init_hooks();
93
94 /**
95 * Return a lazy-initialised Service_Factory instance.
96 *
97 * @return Service_Factory
98 */
99 protected function service_factory(): Service_Factory {
100 if ( null === $this->service_factory_instance ) {
101 $this->service_factory_instance = new Service_Factory( $this->settings );
102 }
103 return $this->service_factory_instance;
104 }
105
106 /**
107 * Get nonce field.
108 *
109 * @return array
110 */
111 public function get_nonce_fields() {
112 // Resolve from the 'save' field set: the nonce is a structural field the save/verify
113 // callers always need, so it must never be subject to the bulk-modal display trim.
114 return array_filter(
115 $this->meta_box_fields( false, 'save' ),
116 function ( $field ) {
117 return ( ! empty( $field['nonce'] ) && true === $field['nonce'] );
118 }
119 );
120 }
121
122 /**
123 * Get shipping options from the PostNL meta, if those no-exists then form the plugin settings.
124 *
125 * @param \WC_Order $order
126 *
127 * @return array
128 *
129 * @internal
130 */
131 public function get_shipping_options( $order ) {
132
133 if ( ! is_a( $order, 'WC_Order' ) ) {
134 return array();
135 }
136
137 $default_options = $this->resolve_default_shipping_options( $order );
138
139 // The default options only carry the generic 'letterbox' feature; surface the
140 // resolved 24h/48h variant so the matching option is pre-selected and a re-save
141 // preserves the existing choice instead of silently reverting it to 24h.
142 return $this->apply_letterbox_display_variant( $default_options, $order );
143 }
144
145 /**
146 * Swap the generic 'letterbox' feature for the resolved 24h/48h display variant.
147 *
148 * The stored backend selection and the plugin defaults only ever carry the
149 * generic 'letterbox' feature; the concrete 24h/48h variant is recorded
150 * separately in the _postnl_letterbox_type meta. Surfacing the resolved variant
151 * keeps the two mutually-exclusive admin checkboxes in step with the label
152 * engine so exactly one of them is pre-selected, instead of the generic 24h
153 * feature re-checking the 24h box on top of the pre-selected 48h box.
154 *
155 * @since 5.9.8
156 *
157 * @param array $options Option map ( feature => 'yes' ).
158 * @param \WC_Order $order Order object.
159 *
160 * @return array
161 */
162 protected function apply_letterbox_display_variant( $options, $order ) {
163 if ( ! is_array( $options ) ) {
164 return $options;
165 }
166
167 if ( 'yes' === ( $options['letterbox'] ?? '' ) && 'letterbox_48' === $this->resolve_letterbox_variant( $order ) ) {
168 unset( $options['letterbox'] );
169 $options['letterbox_48'] = 'yes';
170 }
171
172 return $options;
173 }
174
175 /**
176 * Resolve the pre-selected shipping options from the order meta or the plugin settings.
177 *
178 * @since 5.9.8
179 *
180 * @param \WC_Order $order Order object.
181 *
182 * @return array
183 */
184 protected function resolve_default_shipping_options( $order ) {
185 // Return shipping options already selected by the user.
186 $default_options = $this->get_backend_data( $order->get_id() );
187 if ( ! empty( $default_options ) ) {
188 return $default_options;
189 }
190
191 if ( Utils::is_adults_only_order( $order ) ) {
192 return array( 'id_check' => 'yes' );
193 }
194
195 // Get from the plugin settings.
196 $shipping_zone = Utils::get_shipping_zone( $order->get_shipping_country(), $order->get_shipping_state() );
197 $frontend_data = $this->get_frontend_data( $order->get_id() );
198
199 if ( ! empty( $frontend_data['dropoff_points'] ) ) {
200 $shipping_zone = 'PICKUP';
201 }
202
203 if ( 'NL' === $shipping_zone && Utils::is_order_eligible_auto_letterbox( $order ) ) {
204 return array( 'letterbox' => 'yes' );
205 }
206
207 return $this->settings->get_default_shipping_options( $shipping_zone );
208 }
209
210 /**
211 * Resolve the letterbox variant for display, mirroring the label engine.
212 *
213 * Kept in sync with Rest_API\Shipping\Item_Info::get_letterbox_type():
214 * the recorded choice on the order wins, otherwise the merchant default
215 * setting, otherwise the 24h variant.
216 *
217 * @since 5.9.8
218 *
219 * @param \WC_Order $order Order object.
220 *
221 * @return string 'letterbox' (24h) or 'letterbox_48' (48h).
222 */
223 protected function resolve_letterbox_variant( $order ) {
224 $stored_type = $order->get_meta( '_postnl_letterbox_type' );
225 if ( in_array( $stored_type, array( 'letterbox', 'letterbox_48' ), true ) ) {
226 return $stored_type;
227 }
228
229 return ( 'letterbox_48' === $this->settings->get_default_automatic_letterboxparcel_product() ) ? 'letterbox_48' : 'letterbox';
230 }
231
232 /**
233 * List of meta box fields.
234 *
235 * @param \WC_Order $order WooCommerce order ID.
236 * @param string $context 'display' (default) when rendering the admin UI, 'save' when persisting.
237 */
238 public function meta_box_fields( $order = false, $context = 'display' ) {
239
240 $default_options = $this->get_shipping_options( $order );
241 $fields = array(
242 array(
243 'id' => $this->prefix . 'id_check',
244 'type' => 'checkbox',
245 'label' => __( 'ID Check (18+): ', 'postnl-for-woocommerce' ),
246 'placeholder' => '',
247 'description' => '',
248 'value' => $default_options['id_check'] ?? '',
249 'show_in_bulk' => false,
250 'standard_feat' => false,
251 'const_field' => false,
252 'container' => true,
253 ),
254 array(
255 'id' => $this->prefix . 'insured_shipping',
256 'type' => 'checkbox',
257 'label' => __( 'Insured Shipping: ', 'postnl-for-woocommerce' ),
258 'placeholder' => '',
259 'description' => '',
260 'value' => $default_options['insured_shipping'] ?? '',
261 'show_in_bulk' => true,
262 'standard_feat' => false,
263 'const_field' => false,
264 'container' => true,
265 ),
266 array(
267 'id' => $this->prefix . 'insured_plus',
268 'type' => 'checkbox',
269 'label' => __( 'Insured Plus: ', 'postnl-for-woocommerce' ),
270 'placeholder' => '',
271 'description' => '',
272 'value' => '',
273 'show_in_bulk' => true,
274 'standard_feat' => false,
275 'const_field' => false,
276 'container' => true,
277 ),
278 array(
279 'id' => $this->prefix . 'return_no_answer',
280 'type' => 'checkbox',
281 'label' => __( 'Return if no answer: ', 'postnl-for-woocommerce' ),
282 'placeholder' => '',
283 'description' => '',
284 'value' => $default_options['return_no_answer'] ?? '',
285 'show_in_bulk' => true,
286 'standard_feat' => false,
287 'const_field' => false,
288 'container' => true,
289 ),
290 array(
291 'id' => $this->prefix . 'signature_on_delivery',
292 'type' => 'checkbox',
293 'label' => __( 'Signature on Delivery: ', 'postnl-for-woocommerce' ),
294 'placeholder' => '',
295 'description' => '',
296 'value' => $default_options['signature_on_delivery'] ?? '',
297 'show_in_bulk' => true,
298 'standard_feat' => false,
299 'const_field' => false,
300 'container' => true,
301 ),
302 array(
303 'id' => $this->prefix . 'only_home_address',
304 'type' => 'checkbox',
305 'label' => __( 'Only Home Address: ', 'postnl-for-woocommerce' ),
306 'placeholder' => '',
307 'description' => '',
308 'value' => $default_options['only_home_address'] ?? '',
309 'show_in_bulk' => true,
310 'standard_feat' => false,
311 'const_field' => false,
312 'container' => true,
313 ),
314 array(
315 'id' => $this->prefix . 'letterbox',
316 'type' => 'checkbox',
317 'label' => __( 'Letterboxparcel Standard (24 hours)', 'postnl-for-woocommerce' ) . ': ',
318 'placeholder' => '',
319 'description' => '',
320 'value' => $default_options['letterbox'] ?? '',
321 'show_in_bulk' => true,
322 'standard_feat' => false,
323 'const_field' => false,
324 'container' => true,
325 ),
326 array(
327 'id' => $this->prefix . 'letterbox_48',
328 'type' => 'checkbox',
329 'label' => __( 'Letterboxparcel 48 hours', 'postnl-for-woocommerce' ) . ': ',
330 'placeholder' => '',
331 'description' => '',
332 'value' => $default_options['letterbox_48'] ?? '',
333 'show_in_bulk' => false,
334 'standard_feat' => false,
335 'const_field' => false,
336 'container' => true,
337 ),
338 array(
339 'id' => $this->prefix . 'packets',
340 'type' => 'checkbox',
341 'label' => __( 'Packets: ', 'postnl-for-woocommerce' ),
342 'placeholder' => '',
343 'description' => '',
344 'value' => '',
345 'show_in_bulk' => true,
346 'standard_feat' => false,
347 'const_field' => false,
348 'container' => true,
349 ),
350 array(
351 'id' => $this->prefix . 'mailboxpacket',
352 'type' => 'checkbox',
353 'label' => __( 'Mailbox Packet (International): ', 'postnl-for-woocommerce' ),
354 'placeholder' => '',
355 'description' => '',
356 'value' => '',
357 'show_in_bulk' => true,
358 'standard_feat' => false,
359 'const_field' => false,
360 'container' => true,
361 ),
362 array(
363 'id' => $this->prefix . 'track_and_trace',
364 'type' => 'checkbox',
365 'label' => __( 'Track & Trace: ', 'postnl-for-woocommerce' ),
366 'placeholder' => '',
367 'description' => '',
368 'value' => '',
369 'show_in_bulk' => true,
370 'standard_feat' => false,
371 'const_field' => false,
372 'container' => true,
373 ),
374 array(
375 'id' => $this->prefix . 'delivery_code_at_door',
376 'type' => 'checkbox',
377 'label' => esc_html__( 'Delivery code at the door: ', 'postnl-for-woocommerce' ),
378 'placeholder' => '',
379 'description' => '',
380 'value' => $default_options['delivery_code_at_door'] ?? '',
381 'show_in_bulk' => true,
382 'standard_feat' => false,
383 'const_field' => false,
384 'container' => true,
385 ),
386 array(
387 'id' => $this->prefix . 'break_2',
388 'standard_feat' => false,
389 'const_field' => true,
390 'type' => 'break',
391 ),
392 array(
393 'id' => $this->prefix . 'num_labels',
394 'type' => 'number',
395 'label' => __( 'Number of Labels: ', 'postnl-for-woocommerce' ),
396 'placeholder' => '',
397 'description' => '',
398 'class' => 'short',
399 'value' => '',
400 'custom_attributes' =>
401 array(
402 'step' => 'any',
403 'min' => '0',
404 ),
405 'show_in_bulk' => true,
406 'standard_feat' => true,
407 'const_field' => false,
408 'container' => true,
409 ),
410 array(
411 'id' => $this->prefix . 'label_nonce',
412 'type' => 'hidden',
413 'nonce' => true,
414 'value' => wp_create_nonce( $this->nonce_key ),
415 'show_in_bulk' => true,
416 'standard_feat' => false,
417 'const_field' => true,
418 'container' => true,
419 ),
420 );
421
422 if ( 'in_box' === $this->settings->get_return_shipment_and_labels() ) {
423 $fields[] = array(
424 'id' => $this->prefix . 'create_return_label',
425 'type' => 'checkbox',
426 'label' => __( 'Create Return Label: ', 'postnl-for-woocommerce' ),
427 'placeholder' => '',
428 'description' => '',
429 'value' => 'yes',
430 'show_in_bulk' => true,
431 'standard_feat' => true,
432 'const_field' => false,
433 'container' => true,
434 );
435 }
436
437 if ( 'A6' !== $this->settings->get_label_format() ) {
438 $fields[] = array(
439 'id' => $this->prefix . 'position_printing_labels',
440 'type' => 'select',
441 'label' => __( 'Start position printing label: ', 'postnl-for-woocommerce' ),
442 'placeholder' => '',
443 'description' => '',
444 'options' => array(
445 'top-left' => __( 'Top Left', 'postnl-for-woocommerce' ),
446 'top-right' => __( 'Top Right', 'postnl-for-woocommerce' ),
447 'bottom-left' => __( 'Bottom Left', 'postnl-for-woocommerce' ),
448 'bottom-right' => __( 'Bottom Right', 'postnl-for-woocommerce' ),
449 ),
450 'value' => '',
451 'show_in_bulk' => true,
452 'standard_feat' => false,
453 'const_field' => true,
454 'container' => true,
455 );
456 }
457
458 return apply_filters(
459 'postnl_order_meta_box_fields',
460 $fields,
461 $context
462 );
463 }
464
465 /**
466 * Get available option based on the countries and chosen option in the frontend checkout.
467 *
468 * @param WC_Order $order Order object.
469 *
470 * @return array.
471 */
472 public function get_available_options( $order ) {
473 if ( ! is_a( $order, 'WC_Order' ) ) {
474 return array();
475 }
476
477 $product_map = Mapping::products_data();
478 $from_country = Utils::get_base_country();
479 $to_country = Utils::get_shipping_zone( $order->get_shipping_country(), $order->get_shipping_state() );
480 $saved_data = $this->get_data( $order->get_id() );
481
482 if ( empty( $saved_data['frontend'] ) ) {
483 $saved_data['frontend'] = array();
484 }
485
486 $selected_option = 'delivery_day';
487 $available_options = array();
488
489 foreach ( $saved_data['frontend'] as $key => $value ) {
490 $converted_key = Utils::convert_data_key( $key );
491
492 if ( ! empty( $product_map[ $from_country ][ $to_country ][ $converted_key ] ) ) {
493 $selected_option = $converted_key;
494 break;
495 }
496 }
497
498 foreach ( $product_map[ $from_country ][ $to_country ][ $selected_option ] as $product ) {
499 $available_options = array_merge( $available_options, $product['combination'] );
500 }
501
502 return $available_options;
503 }
504
505 /**
506 * Get saved data from Order object.
507 *
508 * @param int $order_id ID of the order.
509 *
510 * @return array.
511 */
512 public function get_data( $order_id ) {
513 $order = wc_get_order( $order_id );
514
515 if ( ! is_a( $order, 'WC_Order' ) ) {
516 return array();
517 }
518
519 $data = $order->get_meta( $this->meta_name );
520
521 return ! empty( $data ) && is_array( $data ) ? $data : array();
522 }
523
524 /**
525 * Init order object for meta box.
526 *
527 * @param WP_POST|WC_Order $metabox_object Either WP_Post or WC_Order object.
528 */
529 public function init_order_object( $metabox_object ) {
530 if ( is_a( $metabox_object, 'WP_Post' ) ) {
531 return wc_get_order( $metabox_object->ID );
532 }
533
534 if ( is_a( $metabox_object, 'WC_Order' ) ) {
535 return $metabox_object;
536 }
537
538 return false;
539 }
540
541 /**
542 * Check if the current order is using PostNL shipping method.
543 *
544 * @param WC_Order $order Order object.
545 */
546 public function is_postnl_shipping_method( $order ) {
547
548 if ( ! is_a( $order, 'WC_Order' ) ) {
549 return false;
550 }
551
552 $shipping_methods = $order->get_shipping_methods();
553
554 if ( empty( $shipping_methods ) ) {
555 return false;
556 }
557
558 foreach ( $shipping_methods as $shipping_item ) {
559 if ( in_array( $shipping_item->get_method_id(), $this->settings->get_supported_shipping_methods() ) ) {
560 return true;
561 }
562 }
563
564 return false;
565 }
566
567 /**
568 * Saving meta box in order admin page.
569 *
570 * @param int $order_id Order post ID.
571 * @param array $meta_values PostNL meta values.
572 *
573 * @throws \Exception When the order is invalid, or the V4 label response carries no barcode.
574 */
575 public function save_meta_value( $order_id, $meta_values ) {
576
577 $order = wc_get_order( $order_id );
578
579 if ( ! is_a( $order, 'WC_Order' ) ) {
580 throw new \Exception( esc_html__( 'Order does not exist!', 'postnl-for-woocommerce' ) );
581 }
582
583 $saved_data = $this->get_data( $order_id );
584
585 // Check if label is already created.
586 if ( isset( $saved_data['labels']['label'] ) ) {
587 return array(
588 'saved_data' => $saved_data,
589 'labels' => $saved_data['labels'],
590 );
591 }
592
593 // Get array of nonce fields.
594 $nonce_fields = array_values( $this->get_nonce_fields() );
595
596 // Loop through inputs within id 'shipment-postnl-label-form'.
597 // Use the 'save' context so the bulk-modal display trim cannot drop a persisted
598 // field (e.g. Letterbox 48 / ID Check) when generating a label from the legacy
599 // orders list bulk action.
600 foreach ( $this->meta_box_fields( $order_id, 'save' ) as $field ) {
601 // Don't save nonce field.
602 if ( $nonce_fields[0]['id'] === $field['id'] ) {
603 continue;
604 }
605
606 $post_value = ! empty( $meta_values[ $field['id'] ] ) ? sanitize_text_field( wp_unslash( $meta_values[ $field['id'] ] ) ) : '';
607 $post_field = Utils::remove_prefix_field( $this->prefix, $field['id'] );
608
609 $saved_data['backend'][ $post_field ] = $post_value;
610 }
611
612 // Collapse an explicit 24h/48h letterbox choice onto the generic 'letterbox'
613 // feature and record the variant as the authoritative merchant choice, which
614 // Item_Info::get_letterbox_type() reads to pick product 2928 vs 2948. The meta
615 // must be persisted here: Item_Info (constructed below) re-reads the order via
616 // wc_get_order(), which returns a fresh instance that would not see an unsaved value.
617 $letterbox_selection = Utils::normalize_letterbox_options( $saved_data['backend'] );
618 $saved_data['backend'] = $letterbox_selection['options'];
619 if ( '' !== $letterbox_selection['type'] ) {
620 $order->update_meta_data( '_postnl_letterbox_type', $letterbox_selection['type'] );
621 $order->save_meta_data();
622 }
623
624 $label_post_data = array(
625 'order' => $order,
626 'saved_data' => $saved_data,
627 );
628
629 if ( $this->service_factory()->barcode_from_label() ) {
630 // V4: the label call issues the barcode(s); there is no standalone barcode
631 // request. Generate the label first, then harvest the barcode(s) out of the
632 // label response into the same barcodes[] shape the prefetch path produces.
633 $label_post_data['is_return_activated'] = $this->is_return_function_activated( $order );
634
635 $labels = $this->create_label( $label_post_data );
636 $barcodes = $this->harvest_barcodes_or_fail( $labels );
637 } else {
638 $barcodes = $this->maybe_create_multi_barcodes( $label_post_data );
639 $label_post_data['main_barcode'] = $barcodes[0]; // for MainBarcode.
640 $label_post_data['barcodes'] = $barcodes;
641
642 // Need to be refactored.
643 $shipping_item_info = new Shipping\Item_Info( $label_post_data );
644 $label_post_data['return_barcode'] = $this->maybe_create_return_barcode( $label_post_data, $shipping_item_info );
645 $label_post_data['shipping_return_barcode'] = $this->maybe_create_shipping_return_barcode( $label_post_data, $shipping_item_info );
646 $label_post_data['is_return_activated'] = $this->is_return_function_activated( $order );
647
648 $labels = $this->create_label( $label_post_data );
649 }
650
651 /*
652 Temporarily commented.
653 $return_post_data = $label_post_data;
654 $return_post_data['barcode'] = $this->create_barcode( $order );
655 $return_labels = $this->maybe_create_return_label( $label_post_data );
656
657 $saved_data['labels'] = array_merge( $labels, $return_labels );
658 */
659
660 $saved_data['barcodes'] = array_map(
661 function ( $barc ) {
662 return array(
663 'value' => $barc,
664 'created_at' => current_time( 'timestamp' ),
665 );
666 },
667 $barcodes
668 );
669
670 $saved_data['labels'] = $labels;
671
672 if ( $this->settings->is_auto_complete_order_enabled() ) {
673 // Updating the order status to completed.
674 $order->update_status( 'completed' );
675 }
676
677 $order->update_meta_data( $this->meta_name, $saved_data );
678 $order->save();
679
680 // Need to add labels in array to remove the merged files later.
681 return array(
682 'saved_data' => $saved_data,
683 'labels' => $labels,
684 );
685 }
686
687 /**
688 * Get frontend data from Order object.
689 *
690 * @param int $order_id ID of the order.
691 *
692 * @return array.
693 */
694 public function get_frontend_data( $order_id ) {
695 $saved_data = $this->get_data( $order_id );
696
697 return ! empty( $saved_data['frontend'] ) ? $saved_data['frontend'] : array();
698 }
699
700 /**
701 * Get backend data from Order object.
702 *
703 * @param int $order_id ID of the order.
704 *
705 * @return array.
706 */
707 public function get_backend_data( $order_id ) {
708 $saved_data = $this->get_data( $order_id );
709
710 return ! empty( $saved_data['backend'] ) ? $saved_data['backend'] : array();
711 }
712
713 /**
714 * Get order information from frontend data.
715 *
716 * @param WC_Order $order Order object.
717 * @param String $needle String that will be used to search the frontend value.
718 *
719 * @return array.
720 */
721 public function get_order_frontend_info( $order, $needle ) {
722 if ( ! is_a( $order, 'WC_Order' ) ) {
723 return array();
724 }
725
726 $order_data = $order->get_meta( $this->meta_name );
727
728 if ( ! empty( $order_data['frontend'] ) ) {
729 $info_value = array();
730
731 foreach ( $order_data['frontend'] as $key => $value ) {
732 if ( false !== strpos( $key, $needle ) ) {
733 $info_value[ $key ] = $value;
734 }
735 }
736
737 return $info_value;
738 }
739
740 return array();
741 }
742
743 /**
744 * Get delivery type string.
745 *
746 * @param WC_Order $order Order object.
747 *
748 * @return String.
749 */
750 public function get_delivery_type( $order ) {
751 // A letterbox order has no delivery-day or pickup type, so the frontend
752 // mapping below would fall through to the generic "Standard Shipment"
753 // label. Surface the resolved 24h/48h variant instead, mirroring the
754 // selected shipping option so the summary matches the checkbox.
755 $shipping_options = $this->get_shipping_options( $order );
756 if ( 'yes' === ( $shipping_options['letterbox_48'] ?? '' ) ) {
757 return Utils::get_letterbox_label_48h();
758 }
759 if ( 'yes' === ( $shipping_options['letterbox'] ?? '' ) ) {
760 return Utils::get_letterbox_label_24h();
761 }
762
763 $from_country = Utils::get_base_country();
764 $to_country = $order->get_shipping_country();
765 $to_state = $order->get_shipping_state();
766 $delivery_type_map = Mapping::delivery_type();
767 $filtered_frontend = $this->get_order_frontend_info( $order, '_type' );
768 $destination = Utils::get_shipping_zone( $to_country, $to_state );
769
770 if ( ! is_array( $delivery_type_map[ $from_country ][ $destination ] ) ) {
771 return ! empty( $delivery_type_map[ $from_country ][ $destination ] ) ? $delivery_type_map[ $from_country ][ $destination ] : '';
772 }
773
774 if ( empty( $filtered_frontend ) ) {
775 return '';
776 }
777
778 foreach ( $filtered_frontend as $frontend_key => $frontend_value ) {
779 if ( ! empty( $delivery_type_map[ $from_country ][ $destination ][ $frontend_key ][ $frontend_value ] ) ) {
780 return $delivery_type_map[ $from_country ][ $destination ][ $frontend_key ][ $frontend_value ];
781 }
782 }
783
784 return '';
785 }
786
787 /**
788 * Delete meta data in order admin page.
789 *
790 * @param int $order_id Order post ID.
791 *
792 * @throws \Exception Throw error for invalid order.
793 */
794 public function delete_meta_value( $order_id ) {
795 $order = wc_get_order( $order_id );
796
797 if ( ! is_a( $order, 'WC_Order' ) ) {
798 throw new \Exception( esc_html__( 'Order does not exist!', 'postnl-for-woocommerce' ) );
799 }
800
801 $saved_data = $this->get_data( $order_id );
802
803 // Delete label file.
804 $this->delete_label( $saved_data );
805 unset( $saved_data['backend'] );
806 unset( $saved_data['labels'] );
807 unset( $saved_data['barcodes'] );
808
809 $order->update_meta_data( $this->meta_name, $saved_data );
810 $order->save();
811
812 return $saved_data;
813 }
814
815 /**
816 * Put the label content into PDF files.
817 *
818 * @param array $response Response from PostNL API.
819 * @param WC_Order $order Order object.
820 * @param String $parent_barcode Generated barcode string.
821 * @param String $parent_label_type Type of label.
822 *
823 * @return array
824 */
825 public function put_label_content( $response, $order, $parent_barcode, $parent_label_type ) {
826 $message_types = Utils::get_label_response_type();
827 $labels = array();
828
829 foreach ( $message_types as $type => $content_type ) {
830 if ( empty( $response[ $type ] ) ) {
831 continue;
832 }
833
834 foreach ( $response[ $type ] as $shipment_idx => $shipment_contents ) {
835
836 if ( empty( $shipment_contents['Labels'] ) ) {
837 continue 2;
838 }
839
840 foreach ( $shipment_contents['Labels'] as $label_idx => $label_contents ) {
841 if ( empty( $label_contents['Content'] ) ) {
842 continue 3;
843 }
844
845 $label_type = ! empty( $label_contents['Labeltype'] ) ? sanitize_title( $label_contents['Labeltype'] ) : 'unknown-type';
846 $label_extension = ! empty( $label_contents['OutputType'] ) ? sanitize_title( $label_contents['OutputType'] ) : 'pdf';
847 $barcode = $response[ $type ][ $shipment_idx ][ $content_type['barcode_key'] ];
848 $barcode = is_array( $barcode ) ? array_shift( $barcode ) : $barcode;
849 $filename = Utils::generate_label_name( $order->get_id(), $label_type, $barcode, 'A6', $label_extension );
850 $filepath = trailingslashit( POSTNL_UPLOADS_DIR ) . $filename;
851
852 if ( wp_mkdir_p( POSTNL_UPLOADS_DIR ) && ! file_exists( $filepath ) ) {
853 $content = base64_decode( $label_contents['Content'] );
854 $file_ret = file_put_contents( $filepath, $content );
855 }
856
857 $labels[] = array(
858 'type' => $label_type,
859 'barcode' => $barcode,
860 'created_at' => current_time( 'timestamp' ),
861 'filepath' => $filepath,
862 );
863 }
864 }
865 }
866
867 // Legacy passes the prefetched barcode through untouched; V4 has none, so fall
868 // back to the barcode the response already carried into the raw records.
869 $parent_barcode = self::resolve_parent_barcode( $labels, (string) $parent_barcode );
870
871 // if ( 'PDF' === $label_contents['OutputType'] ) {
872 $labels = $this->maybe_merge_labels( $labels, $order, $parent_barcode, $parent_label_type );
873
874 // }
875 return $labels;
876 }
877
878 /**
879 * Create PostNL barcode for current order
880 *
881 * @param array $label_post_data .
882 *
883 * @return array
884 *
885 * @throws \Exception Error when response does not have Barcode value.
886 */
887 public function create_barcode( $label_post_data ) {
888 $data = array(
889 'order' => $label_post_data['order'],
890 'saved_data' => $label_post_data['saved_data'],
891 );
892
893 $response = $this->service_factory()->barcode_service()->generate( $data );
894
895 if ( empty( $response['Barcode'] ) ) {
896 throw new \Exception(
897 esc_html__( 'Cannot create the barcode.', 'postnl-for-woocommerce' )
898 );
899 }
900
901 return $response['Barcode'];
902 }
903
904 /**
905 * Get multi barcodes from cacne or create new barcodes for current order.
906 *
907 * @param array $post_data Order post data.
908 *
909 * @return array
910 *
911 * @throws \Exception Error when response has an error.
912 */
913 public function maybe_create_multi_barcodes( $post_data ) {
914 // Minimum number of labels is 1 so it will create at least 1 barcode.
915 $num_labels = 1;
916 $barcodes = array();
917 $saved_data = $post_data['saved_data'];
918
919 if ( isset( $saved_data['backend']['num_labels'] ) && 1 < intval( $saved_data['backend']['num_labels'] ) ) {
920 $num_labels = intval( $saved_data['backend']['num_labels'] );
921 }
922
923 for ( $i = 0; $i < $num_labels; $i++ ) {
924 // Check if barcode has been created on the last 7 days before creating a new one.
925 if ( ! empty( $saved_data['barcodes'][ $i ]['created_at'] ) && ! empty( $saved_data['barcodes'][ $i ]['value'] ) ) {
926 $time_deviation = current_time( 'timestamp' ) - intval( $saved_data['barcodes'][ $i ]['created_at'] );
927
928 if ( $time_deviation <= 7 * DAY_IN_SECONDS ) {
929 $barcodes[ $i ] = $saved_data['barcodes'][ $i ]['value'];
930 continue;
931 }
932 }
933
934 $barcodes[ $i ] = $this->create_barcode( $post_data );
935 }
936
937 return $barcodes;
938 }
939
940 /**
941 * Harvest the barcode(s) out of a label-generation response.
942 *
943 * On V4 the label call issues the barcode, so it arrives under the same 'barcode'
944 * key put_label_content() already writes. Returned in the indexed shape
945 * maybe_create_multi_barcodes() produces.
946 *
947 * Multi-collo parity is deferred to the V4 multi-collo label work: the merge
948 * collapses N collo into one record, so this returns a single barcode where the
949 * Legacy prefetch records N.
950 *
951 * @param mixed $labels Label records returned by create_label().
952 *
953 * @return array List of barcode strings.
954 *
955 * @since 6.0.0
956 */
957 protected static function get_barcodes_from_labels( $labels ) {
958 $barcodes = array();
959
960 if ( ! is_array( $labels ) ) {
961 return $barcodes;
962 }
963
964 foreach ( $labels as $label ) {
965 if ( ! empty( $label['barcode'] ) && ! in_array( $label['barcode'], $barcodes, true ) ) {
966 $barcodes[] = $label['barcode'];
967 }
968 }
969
970 return $barcodes;
971 }
972
973 /**
974 * Harvest the barcodes from a fresh label response, discarding the files on failure.
975 *
976 * On the V4 path the label call has already written PDFs to disk before the
977 * harvest runs. Throwing while keeping them would orphan those files behind a
978 * retry that generates a fresh label, so they are removed before failing.
979 *
980 * @param array $labels Label records returned by create_label().
981 *
982 * @return array List of barcode strings.
983 *
984 * @throws \Exception When the label response carries no barcode.
985 *
986 * @since 6.0.0
987 */
988 protected function harvest_barcodes_or_fail( $labels ) {
989 $barcodes = self::get_barcodes_from_labels( $labels );
990
991 if ( empty( $barcodes ) ) {
992 $this->delete_label( array( 'labels' => $labels ) );
993
994 throw new \Exception(
995 esc_html__( 'PostNL returned a label without a barcode. Please try generating the label again.', 'postnl-for-woocommerce' )
996 );
997 }
998
999 return $barcodes;
1000 }
1001
1002 /**
1003 * Resolve the parent barcode used for the merged label record.
1004 *
1005 * Legacy prefetches it, so the passed-in value wins and the merge is unchanged.
1006 * V4 has no prefetch: without a fallback maybe_merge_labels() would stamp an
1007 * empty barcode onto the merged record for every non-A6 or multi-collo order,
1008 * leaving the harvest empty and the tracking URL blank.
1009 *
1010 * @param mixed $labels Raw label records built from the API response.
1011 * @param string $parent_barcode Prefetched parent barcode, empty on the V4 path.
1012 *
1013 * @return string
1014 *
1015 * @since 6.0.0
1016 */
1017 protected static function resolve_parent_barcode( $labels, string $parent_barcode ) {
1018 if ( '' !== $parent_barcode ) {
1019 return $parent_barcode;
1020 }
1021
1022 if ( ! is_array( $labels ) ) {
1023 return '';
1024 }
1025
1026 foreach ( $labels as $label ) {
1027 if ( ! empty( $label['barcode'] ) ) {
1028 return (string) $label['barcode'];
1029 }
1030 }
1031
1032 return '';
1033 }
1034
1035 /**
1036 * Create PostNL return barcode for current order
1037 *
1038 * @param array $post_data Order post data.
1039 *
1040 * @return array|Boolean
1041 *
1042 * @throws \Exception Error when response has an error.
1043 */
1044 public function maybe_create_shipping_return_barcode( $label_post_data, $shipping_item_info ) {
1045 $shipment_return_type = $this->settings->get_return_shipment_and_labels();
1046
1047 if ( 'none' === $shipment_return_type ) {
1048 return '';
1049 }
1050
1051 if ( 'shipping_return' !== $shipment_return_type ) {
1052 return '';
1053 }
1054
1055 if ( 'NL' !== $shipping_item_info->receiver['country'] ) {
1056 return '';
1057 }
1058
1059 if ( '2928' === $shipping_item_info->shipment['shipping_product']['code'] ||
1060 '2948' === $shipping_item_info->shipment['shipping_product']['code'] ) {
1061 return '';
1062 }
1063
1064 return $label_post_data['main_barcode'];
1065 }
1066
1067 /**
1068 * Create PostNL return barcode for current order
1069 *
1070 * @param array $post_data Order post data.
1071 *
1072 * @return array|Boolean
1073 *
1074 * @throws \Exception Error when response has an error.
1075 */
1076 public function maybe_create_return_barcode( $post_data, $shipping_item_info ) {
1077 $shipment_return_type = $this->settings->get_return_shipment_and_labels();
1078 if ( 'none' === $shipment_return_type ) {
1079 return '';
1080 }
1081
1082 if ( ! in_array( $shipping_item_info->receiver['country'], array( 'BE', 'NL' ) ) ) {
1083 return '';
1084 }
1085
1086 if ( 'shipping_return' === $shipment_return_type &&
1087 'BE' !== $shipping_item_info->receiver['country'] ) {
1088 return '';
1089 }
1090
1091 if ( 'in_box' === $shipment_return_type &&
1092 ( ! isset( $post_data['saved_data']['backend']['create_return_label'] ) ||
1093 'yes' !== $post_data['saved_data']['backend']['create_return_label'] )
1094 ) {
1095 return '';
1096 }
1097
1098 $not_allowed = array( '6440', '6972', '6405', '6350', '6906' );
1099 if ( 'BE' === $shipping_item_info->receiver['country'] && in_array( $shipping_item_info->shipment['shipping_product']['code'], $not_allowed ) ) {
1100 return '';
1101 }
1102
1103 $return_code = $this->settings->get_return_customer_code();
1104
1105 $data = array(
1106 'order' => $post_data['order'],
1107 'customer_code' => $return_code,
1108 );
1109
1110 $response = $this->service_factory()->barcode_service()->generate( $data );
1111
1112 if ( empty( $response['Barcode'] ) ) {
1113 throw new \Exception(
1114 esc_html__( 'Cannot create return barcode.', 'postnl-for-woocommerce' )
1115 );
1116 }
1117
1118 return $response['Barcode'];
1119 }
1120
1121 /**
1122 * Merging the label.
1123 *
1124 * @param Array $labels List of labels.
1125 * @param WC_Order $order Order object.
1126 * @param String $barcode Generated barcode string.
1127 * @param String $label_type Type of label.
1128 *
1129 * @return Array.
1130 */
1131 public function maybe_merge_labels( $labels, $order, $barcode, $label_type ) {
1132 $label_format = $this->settings->get_label_format();
1133 $merged_labels = array();
1134
1135 if ( ! is_array( $labels ) ) {
1136 return $merged_labels;
1137 }
1138
1139 if ( 1 === count( $labels ) && 'A6' === $label_format ) {
1140 return array(
1141 $label_type => array_shift( $labels ),
1142 );
1143 }
1144
1145 $from_country = Utils::get_base_country();
1146 $to_country = $order->get_shipping_country();
1147 $to_state = $order->get_shipping_state();
1148 $destination = Utils::get_shipping_zone( $to_country, $to_state );
1149 $label_type_list = Mapping::label_type_list();
1150
1151 $available_type = ( ! empty( $label_type_list[ $from_country ][ $destination ] ) ) ? $label_type_list[ $from_country ][ $destination ] : array( 'label' );
1152
1153 $file_paths = array();
1154 foreach ( $labels as $label ) {
1155 if ( ! in_array( $label['type'], $available_type, true ) ) {
1156 continue;
1157 }
1158
1159 $file_paths[] = $label['filepath'];
1160 }
1161 $extension = pathinfo( $file_paths[0], PATHINFO_EXTENSION );
1162
1163 $filename = Utils::generate_label_name( $order->get_id(), $label_type, $barcode, $label_format . '-merged', $extension );
1164 $merged_info = $this->merge_labels( $file_paths, $filename );
1165
1166 $merged_labels[ $label_type ] = array(
1167 'type' => $label_type,
1168 'barcode' => $barcode,
1169 'created_at' => current_time( 'timestamp' ),
1170 'filepath' => $merged_info['filepath'],
1171 'merged_files' => $file_paths,
1172 );
1173
1174 return $merged_labels;
1175 }
1176
1177 /**
1178 * Merge given files into the single one.
1179 *
1180 * @param array $label_paths Array of files to be merged.
1181 * @param string $merge_filename The final/merged filename with extension.
1182 * @param string $start_position Start position for the pdf file only.
1183 *
1184 * @return array
1185 */
1186 protected function merge_labels( $label_paths, $merge_filename, $start_position = 'top-left' ) {
1187 $extension = pathinfo( $label_paths[0], PATHINFO_EXTENSION );
1188
1189 try {
1190 switch ( $extension ) {
1191 case 'pdf':
1192 return $this->merge_pdf_labels( $label_paths, $merge_filename, $start_position );
1193 case 'jpg':
1194 return $this->merge_jpg_files( $label_paths, $merge_filename, 'horizontal' );
1195 case 'gif':
1196 return $this->merge_graphic_labels( $label_paths, $merge_filename );
1197 // Two spellings of the same Zebra label stream: Legacy names the file
1198 // from the V1 response's OutputType, the V4 path from the SDK's
1199 // LabelOutputType enum, whose Zebra case is the bare string 'zpl'.
1200 // Both are plain text, so both concatenate.
1201 case 'zpl':
1202 case 'zpl_rle':
1203 return $this->merge_text_files( $label_paths, $merge_filename );
1204 }
1205 } catch ( \Exception $e ) {
1206 }
1207
1208 return array();
1209 }
1210
1211 /**
1212 * Merge PDF Labels.
1213 *
1214 * @param array $label_paths List of label path.
1215 * @param String $merge_filename Name of the file after the merge process.
1216 *
1217 * @return array List of filepath that has been merged.
1218 */
1219 protected function merge_pdf_labels( $label_paths, $merge_filename, $start_position = 'top-left' ) {
1220 $pdf = new CustomizedPDFMerger();
1221 $merged_paths = array();
1222
1223 foreach ( $label_paths as $path ) {
1224 $pdf->addPDF( $path, 'all' );
1225 $merged_paths[] = $path;
1226 }
1227
1228 $filepath = trailingslashit( POSTNL_UPLOADS_DIR ) . $merge_filename;
1229
1230 if ( isset( $_POST['postnl_position_printing_labels'] ) ) {
1231 $start_position = sanitize_text_field( $_POST['postnl_position_printing_labels'] );
1232 }
1233
1234 $pdf->merge( 'file', $filepath, 'A', $start_position );
1235
1236 return array(
1237 'merged_filepaths' => $merged_paths,
1238 'filepath' => $filepath,
1239 );
1240 }
1241
1242 /**
1243 * Merge JPG Labels.
1244 *
1245 * @param array $image_paths List of label path.
1246 * @param String $merge_filename Name of the file after the merge process.
1247 *
1248 * @return array List of filepath that has been merged.
1249 */
1250 protected function merge_jpg_files( $image_paths, $merge_filename, $direction = 'vertical' ) {
1251 $images = array();
1252 $width = 0;
1253 $height = 0;
1254
1255 // Load images and calculate dimensions
1256 foreach ( $image_paths as $path ) {
1257 $img = imagecreatefromjpeg( $path );
1258 $images[] = $img;
1259 $width = max( $width, imagesx( $img ) );
1260 $height += imagesy( $img );
1261 }
1262
1263 // Create a blank canvas for the merged image
1264 if ( $direction == 'horizontal' ) {
1265 $canvas = imagecreatetruecolor( $width * count( $images ), $height );
1266 } else {
1267 $canvas = imagecreatetruecolor( $width, $height );
1268 }
1269
1270 // Set white background
1271 $white = imagecolorallocate( $canvas, 255, 255, 255 );
1272 imagefill( $canvas, 0, 0, $white );
1273
1274 // Copy each image onto the canvas
1275 $offset = 0;
1276 foreach ( $images as $img ) {
1277 if ( $direction == 'horizontal' ) {
1278 imagecopy( $canvas, $img, $offset, 0, 0, 0, imagesx( $img ), imagesy( $img ) );
1279 $offset += imagesx( $img );
1280 } else {
1281 imagecopy( $canvas, $img, 0, $offset, 0, 0, imagesx( $img ), imagesy( $img ) );
1282 $offset += imagesy( $img );
1283 }
1284 imagedestroy( $img );
1285 }
1286
1287 // Set the output file path
1288 $filepath = trailingslashit( POSTNL_UPLOADS_DIR ) . $merge_filename;
1289
1290 // Save the merged image
1291 imagejpeg( $canvas, $filepath );
1292 imagedestroy( $canvas );
1293
1294 return array(
1295 'merged_filepaths' => $image_paths,
1296 'filepath' => $filepath,
1297 );
1298 }
1299
1300 /**
1301 * Merge graphic labels.
1302 *
1303 * @param Array $label_paths List of label path.
1304 * @param String $merge_filename Name of the file after the merge process.
1305 *
1306 * @return array
1307 * @throws \ImagickException
1308 */
1309 protected function merge_graphic_labels( $label_paths, $merge_filename ) {
1310
1311 if ( empty( $label_paths ) ) {
1312 throw new Exception( __( 'There are no files to merge.', 'postnl-for-woocommerce' ) );
1313 }
1314
1315 if ( ! class_exists( 'Imagick' ) ) {
1316 throw new Exception( __( '"Imagick" must be installed on the server to merge png files.', 'postnl-for-woocommerce' ) );
1317 }
1318 $merged_paths = array();
1319 $final_label_path = trailingslashit( POSTNL_UPLOADS_DIR ) . $merge_filename;
1320 $final_label = new \Imagick();
1321 foreach ( $label_paths as $path ) {
1322 $final_label->addImage( new \Imagick( $path ) );
1323 $merged_paths[] = $path;
1324 }
1325
1326 /* Append the images into one */
1327 $final_label->resetIterator();
1328 $combined = $final_label->appendImages( true );
1329 $combined->setImageFormat( pathinfo( $final_label_path, PATHINFO_EXTENSION ) );
1330 $combined->writeimage( $final_label_path );
1331
1332 return array(
1333 'merged_filepaths' => $merged_paths,
1334 'filepath' => $final_label_path,
1335 );
1336 }
1337
1338 /**
1339 * Merge text files, for the ZEBRA printer.
1340 *
1341 * @param array $label_paths List of label path.
1342 * @param String $merge_filename Name of the file after the merge process.
1343 *
1344 * @return array
1345 */
1346 protected function merge_text_files( $label_paths, $merge_filename ) {
1347 $merged_paths = array();
1348 $filepath = trailingslashit( POSTNL_UPLOADS_DIR ) . $merge_filename;
1349
1350 $output = fopen( $filepath, 'w' );
1351
1352 foreach ( $label_paths as $path ) {
1353 if ( ! file_exists( $path ) ) {
1354 continue; // Skip if the file does not exist
1355 }
1356
1357 $input = fopen( $path, 'r' );
1358 if ( ! $input ) {
1359 continue; // Skip if unable to open the file
1360 }
1361
1362 // Read each line and write it to the output file
1363 while ( ( $line = fgets( $input ) ) !== false ) {
1364 fwrite( $output, $line );
1365 }
1366
1367 fclose( $input ); // Close each input file after reading
1368 $merged_paths[] = $path;
1369 }
1370
1371 fclose( $output ); // Close the output file after writing
1372
1373 return array(
1374 'merged_filepaths' => $merged_paths,
1375 'filepath' => $filepath,
1376 );
1377 }
1378
1379 /**
1380 * Create PostNL label for current order
1381 *
1382 * @param array $post_data Order post data.
1383 *
1384 * @return array
1385 *
1386 * @throws \Exception Error when response has an error.
1387 */
1388 public function create_label( $post_data ) {
1389 return $this->service_factory()->label_service()->create( $post_data );
1390 }
1391
1392 /**
1393 * Legacy pipeline for outbound shipping labels.
1394 *
1395 * Called exclusively by Legacy\Label_Service::create() to avoid recursion
1396 * (Label_Service extends Order_Base and inherits create_label(), so
1397 * Label_Service cannot safely call create_label() once it routes through
1398 * the factory).
1399 *
1400 * @param array $post_data Order post data.
1401 *
1402 * @return array
1403 *
1404 * @throws \Exception Error when response has an error.
1405 */
1406 protected function create_label_pipeline( $post_data ) {
1407 $order = $post_data['order'];
1408 $shipping_item_info = new Shipping\Item_Info( $post_data );
1409 $shipping = new Shipping\Client( $shipping_item_info );
1410 $response = $shipping->send_request();
1411
1412 // Check any errors.
1413 $this->check_label_and_barcode( $response );
1414
1415 $labels = $this->put_label_content( $response, $order, $post_data['main_barcode'], 'label' );
1416
1417 if ( empty( $labels ) ) {
1418 throw new \Exception(
1419 esc_html__( 'Cannot create the label. Label content is missing', 'postnl-for-woocommerce' )
1420 );
1421 }
1422
1423 return $labels;
1424 }
1425
1426 /**
1427 * Create PostNL return label for current order
1428 *
1429 * @param array $post_data Order post data.
1430 *
1431 * @return array
1432 *
1433 * @throws \Exception Error when response has an error.
1434 */
1435 public function maybe_create_return_label( $post_data ) {
1436 return $this->service_factory()->return_label_service()->create( $post_data );
1437 }
1438
1439 /**
1440 * Legacy pipeline for return labels.
1441 *
1442 * Called exclusively by Legacy\Return_Label_Service::create() to avoid
1443 * recursion (Return_Label_Service extends Order_Base).
1444 *
1445 * @param array $post_data Order post data.
1446 *
1447 * @return array
1448 *
1449 * @throws \Exception Error when response has an error.
1450 */
1451 protected function maybe_create_return_label_pipeline( $post_data ) {
1452 if ( 'yes' !== $post_data['saved_data']['backend']['create_return_label'] ) {
1453 return array();
1454 }
1455
1456 $order = $post_data['order'];
1457
1458 $item_info = new Return_Label\Item_Info( $post_data );
1459 $return_label = new Return_Label\Client( $item_info );
1460 $response = $return_label->send_request();
1461
1462 $labels = $this->put_label_content( $response, $order, $post_data['main_barcode'], 'return-label' );
1463
1464 if ( empty( $labels ) ) {
1465 throw new \Exception(
1466 esc_html__( 'Cannot create the return label. Label content is missing', 'postnl-for-woocommerce' )
1467 );
1468 }
1469
1470 return $labels;
1471 }
1472
1473 /**
1474 * Create PostNL letterbox label for current order
1475 *
1476 * @param array $post_data Order post data.
1477 *
1478 * @return array
1479 *
1480 * @throws \Exception Error when response has an error.
1481 */
1482 public function maybe_create_letterbox( $post_data ) {
1483 return $this->service_factory()->letterbox_service()->create( $post_data );
1484 }
1485
1486 /**
1487 * Legacy pipeline for letterbox labels.
1488 *
1489 * Called exclusively by Legacy\Letterbox_Service::create() to avoid
1490 * recursion (Letterbox_Service extends Order_Base).
1491 *
1492 * @param array $post_data Order post data.
1493 *
1494 * @return array
1495 *
1496 * @throws \Exception Error when response has an error.
1497 */
1498 protected function maybe_create_letterbox_pipeline( $post_data ) {
1499 if ( 'yes' !== $post_data['saved_data']['backend']['letterbox'] ) {
1500 return array();
1501 }
1502
1503 $order = $post_data['order'];
1504
1505 $item_info = new Letterbox\Item_Info( $post_data );
1506 $return_label = new Letterbox\Client( $item_info );
1507 $response = $return_label->send_request();
1508
1509 $labels = $this->put_label_content( $response, $order, $post_data['main_barcode'], 'letterbox' );
1510
1511 if ( empty( $labels ) ) {
1512 throw new \Exception(
1513 esc_html__( 'Cannot create the letterbox. Label content is missing', 'postnl-for-woocommerce' )
1514 );
1515 }
1516
1517 return $labels;
1518 }
1519
1520 /**
1521 * Make sure the barcode and label content is exists before printing.
1522 *
1523 * @param Array $response Response from API Call.
1524 *
1525 * @throws \Exception Error when barcode or label content is missing.
1526 */
1527 public function check_label_and_barcode( $response ) {
1528 $message_types = Utils::get_label_response_type();
1529
1530 $has_barcode = false;
1531 $has_content = false;
1532 foreach ( $message_types as $type => $content_type ) {
1533 if ( ! empty( $response[ $type ][0]['Barcode'] ) ) {
1534 $has_barcode = true;
1535 }
1536
1537 if ( ! empty( $response[ $type ][0]['Labels'][0]['Content'] ) ) {
1538 $has_content = true;
1539 }
1540 }
1541
1542 if ( ! $has_barcode ) {
1543 throw new \Exception(
1544 esc_html__( 'Cannot create the label. Barcode data is missing', 'postnl-for-woocommerce' )
1545 );
1546 }
1547
1548 if ( ! $has_content ) {
1549 throw new \Exception(
1550 esc_html__( 'Cannot create the label. Label content is missing', 'postnl-for-woocommerce' )
1551 );
1552 }
1553 }
1554
1555 /**
1556 * Delete PostNL label for current order
1557 *
1558 * @param array $saved_data Order saved meta data.
1559 *
1560 * @return bool
1561 */
1562 public function delete_label( $saved_data ) {
1563 if ( empty( $saved_data['labels']['label']['filepath'] ) ) {
1564 return false;
1565 }
1566
1567 if ( isset( $saved_data['labels']['label']['merged_files'] ) ) {
1568 foreach ( $saved_data['labels']['label']['merged_files'] as $label_path ) {
1569 unlink( $label_path );
1570 }
1571 }
1572
1573 return unlink( $saved_data['labels']['label']['filepath'] );
1574 }
1575
1576 /**
1577 * Generate download label url
1578 *
1579 * @param int $order_id ID of the order post.
1580 * @param String $label_type Type of the label. Possible options : 'label', 'return-label'.
1581 *
1582 * @return String.
1583 */
1584 public function get_download_label_url( $order_id, $label_type = 'label' ) {
1585 $download_url = add_query_arg(
1586 array(
1587 'postnl_label_order_id' => $order_id,
1588 'label_type' => $label_type,
1589 'postnl_label_nonce' => wp_create_nonce( 'postnl_download_label_nonce' ),
1590 ),
1591 home_url()
1592 );
1593
1594 return $download_url;
1595 }
1596
1597 /**
1598 * Get label file.
1599 *
1600 * @return void
1601 * @since 1.0.0
1602 */
1603 public function get_label_file() {
1604 if ( empty( $_GET['postnl_label_nonce'] ) ) {
1605 return;
1606 }
1607
1608 if ( empty( $_GET['label_type'] ) ) {
1609 return;
1610 }
1611
1612 // Check nonce before proceed.
1613 $nonce_result = check_ajax_referer( 'postnl_download_label_nonce', sanitize_text_field( wp_unslash( $_GET['postnl_label_nonce'] ) ), false );
1614
1615 if ( empty( $_GET['postnl_label_order_id'] ) ) {
1616 return;
1617 }
1618
1619 $order_id = sanitize_text_field( wp_unslash( $_GET['postnl_label_order_id'] ) );
1620 $label_type = sanitize_text_field( wp_unslash( $_GET['label_type'] ) );
1621
1622 if ( ! ( current_user_can( 'manage_woocommerce' ) || current_user_can( 'view_order', $order_id ) ) ) {
1623 return;
1624 }
1625
1626 $saved_data = $this->get_data( $order_id );
1627
1628 if ( empty( $saved_data['labels'][ $label_type ]['filepath'] ) ) {
1629 return;
1630 }
1631
1632 $this->download_label( $saved_data['labels'][ $label_type ]['filepath'] );
1633 }
1634
1635 /**
1636 * Downloads the generated label file
1637 *
1638 * @param string $file_path File path to the label.
1639 *
1640 * @return boolean|void
1641 */
1642 protected function download_label( $file_path ) {
1643 if ( ! empty( $file_path ) && is_string( $file_path ) && file_exists( $file_path ) ) {
1644 // Check if buffer exists, then flush any buffered output to prevent it from being included in the file's content.
1645 if ( ob_get_contents() ) {
1646 ob_clean();
1647 }
1648
1649 $filename = basename( $file_path );
1650
1651 header( 'Content-Description: File Transfer' );
1652 header( 'Content-Type: application/octet-stream' );
1653 header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
1654 header( 'Expires: 0' );
1655 header( 'Cache-Control: must-revalidate' );
1656 header( 'Pragma: public' );
1657 header( 'Content-Length: ' . filesize( $file_path ) );
1658
1659 readfile( $file_path );
1660 exit;
1661 } else {
1662 return false;
1663 }
1664 }
1665
1666 /**
1667 * Get tracking note for the order.
1668 *
1669 * @param Int $order_id ID of the order object.
1670 *
1671 * @return String
1672 */
1673 protected function get_tracking_note( $order_id ) {
1674
1675 if ( ! empty( $this->settings->get_woocommerce_email_text() ) ) {
1676 $tracking_note = $this->settings->get_woocommerce_email_text();
1677 } else {
1678 // translators: %s the current service.
1679 $tracking_note = sprintf( __( '%s Tracking Number: {tracking-link}', 'postnl-for-woocommerce' ), $this->service );
1680 }
1681
1682 $tracking_link = $this->get_tracking_link( $order_id );
1683
1684 if ( empty( $tracking_link ) ) {
1685 return '';
1686 }
1687
1688 $tracking_note_new = str_replace( '{tracking-link}', $tracking_link, $tracking_note, $count );
1689
1690 if ( 0 === $count ) {
1691 $tracking_note_new = $tracking_note . ' ' . $tracking_link;
1692 }
1693
1694 return $tracking_note_new;
1695 }
1696
1697 /**
1698 * Get tracking url for the order.
1699 *
1700 * @param Int $order_id ID of the order object.
1701 */
1702 protected function get_tracking_link( $order_id ) {
1703 $saved_data = $this->get_data( $order_id );
1704 $order = wc_get_order( $order_id );
1705
1706 if ( empty( $saved_data['labels']['label']['barcode'] ) || ! is_a( $order, 'WC_Order' ) ) {
1707 return '';
1708 }
1709
1710 $tracking_url = Utils::generate_tracking_url( $saved_data['labels']['label']['barcode'], $order->get_shipping_country(), $order->get_shipping_postcode() );
1711
1712 return sprintf( '<a href="%1$s" target="_blank" class="postnl-tracking-link">%2$s</a>', esc_url( $tracking_url ), $saved_data['labels']['label']['barcode'] );
1713 }
1714
1715 /**
1716 * Check if the order have the label data.
1717 *
1718 * @param WC_Order $order current order object.
1719 * @param String $field Backend field name.
1720 *
1721 * @return boolean
1722 */
1723 public function have_backend_data( $order, $field = '' ) {
1724 $order_data = $order->get_meta( $this->meta_name );
1725
1726 if ( ! empty( $field ) ) {
1727 return ! empty( $order_data['backend'][ $field ] );
1728 }
1729
1730 return ! empty( $order_data['backend'] );
1731 }
1732
1733 /**
1734 * Check if the order have the label file.
1735 *
1736 * @param \WC_Order $order current order object.
1737 *
1738 * @return boolean.
1739 */
1740 public function have_label_file( $order ) {
1741 $order_data = $order->get_meta( $this->meta_name );
1742
1743 return ! empty( $order_data['labels']['label']['filepath'] );
1744 }
1745
1746 /**
1747 * Check if the return function is activated for the order.
1748 *
1749 * @param int|\WC_Order $order .
1750 *
1751 * @return bool
1752 */
1753 public function is_return_function_activated( $order ) {
1754 $order = ( $order instanceof \WC_Order ) ? $order : wc_get_order( $order );
1755
1756 if ( ! $order instanceof \WC_Order ) {
1757 return false;
1758 }
1759
1760 return 'yes' === $order->get_meta( $this->is_return_activated_meta );
1761 }
1762 }
1763