PluginProbe
PostNL for WooCommerce / 5.9.12
PostNL for WooCommerce v5.9.12
5.9.12 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 All 72 releases
woo-postnl / src / Rest_API / Legacy / Shipping / Item_Info.php

Item_Info.php in PostNL for WooCommerce 5.9.12, at src/Rest_API/Legacy/Shipping/Item_Info.php

1,201 lines 36.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Rest_API\Shipping\Item_Info file.
4 *
5 * @package PostNLWooCommerce\Rest_API\Legacy\Shipping
6 */
7
8 namespace PostNLWooCommerce\Rest_API\Legacy\Shipping;
9
10 use PostNLWooCommerce\Address_Utils;
11 use PostNLWooCommerce\Rest_API\Base_Info;
12 use PostNLWooCommerce\Shipping_Method\Settings;
13 use PostNLWooCommerce\Utils;
14 use PostNLWooCommerce\Helper\Mapping;
15 use PostNLWooCommerce\Product\Single;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Class Item_Info
23 *
24 * @package PostNLWooCommerce\Rest_API\Legacy\Shipping
25 */
26 class Item_Info extends Base_Info {
27 /**
28 * API args.
29 *
30 * @var api_args
31 */
32 protected $api_args;
33
34 /**
35 * Settings class instance.
36 *
37 * @var Settings
38 */
39 protected $settings;
40
41 /**
42 * Shipment of the item info.
43 *
44 * @var shipment
45 */
46 public $shipment;
47
48 /**
49 * Customer info data of the item info.
50 *
51 * @var customer
52 */
53 public $customer;
54
55 /**
56 * Order item data of the item info.
57 *
58 * @var contents
59 */
60 public $contents = array();
61
62 /**
63 * Shipper data of the item info.
64 *
65 * @var shipper
66 */
67 public $shipper;
68
69 /**
70 * Receiver data of the item info.
71 *
72 * @var receiver
73 */
74 public $receiver;
75
76 /**
77 * Pickup points data of the item info.
78 *
79 * @var pickup_points
80 */
81 public $pickup_points;
82
83 /**
84 * Delivery day data of the item info.
85 *
86 * @var delivery_day
87 */
88 public $delivery_day;
89
90 /**
91 * Saved data from database.
92 *
93 * @var backend_data
94 */
95 public $backend_data;
96
97 /**
98 * Weight UOM that set in WooCommerce settings.
99 *
100 * @var weight_uom
101 */
102 public $weight_uom;
103
104 /**
105 * Parses the arguments and sets the instance's properties.
106 *
107 * @throws Exception If some data in $args did not pass validation.
108 */
109 protected function parse_args() {
110 $this->weight_uom = Utils::get_uom();
111
112 $customer_info = $this->api_args['settings'] + $this->api_args['store_address'];
113 $shipment = $this->api_args['billing_address'] + $this->api_args['order_details'] + $this->api_args['settings'];
114
115 $this->shipment = Utils::parse_args( $shipment, $this->get_shipment_info_schema() );
116 $this->receiver = Utils::parse_args( $this->api_args['shipping_address'], $this->get_receiver_info_schema() );
117 $this->customer = Utils::parse_args( $customer_info, $this->get_customer_info_schema() );
118 $this->shipper = Utils::parse_args( $this->api_args['store_address'], $this->get_store_info_schema() );
119 $this->pickup_points = Utils::parse_args( $this->api_args['frontend_data']['pickup_points'], $this->get_pickup_points_info_schema() );
120 $this->delivery_day = Utils::parse_args( $this->api_args['frontend_data']['delivery_day'], $this->get_delivery_day_info_schema() );
121 $this->backend_data = Utils::parse_args( $this->api_args['backend_data'], $this->get_backend_data_info_schema() );
122
123 if ( ! empty( $this->api_args['order_details']['contents'] ) ) {
124 foreach ( $this->api_args['order_details']['contents'] as $item_info ) {
125 $this->contents[] = Utils::parse_args( $item_info, $this->get_content_item_info_schema() );
126 }
127 }
128 }
129
130 /**
131 * Method to convert the post data to API args.
132 *
133 * @param Array $post_data Data from post variable in checkout page.
134 *
135 * @throws \Exception When order ID doesnt exists.
136 */
137 public function convert_data_to_args( $post_data ) {
138 if ( ! is_a( $post_data['order'], 'WC_Order' ) ) {
139 throw new \Exception(
140 __( 'Order ID does not exist!', 'postnl-for-woocommerce' )
141 );
142 }
143
144 $order = $post_data['order'];
145 $saved_data = $post_data['saved_data'];
146 $order_weight = $this->calculate_order_weight( $order );
147 $shipping_country = $order->get_shipping_country();
148 $shipping_state = $order->get_shipping_state();
149
150 $this->api_args['billing_address'] = array(
151 'first_name' => $order->get_billing_first_name(),
152 'last_name' => $order->get_billing_last_name(),
153 'company' => $order->get_billing_company(),
154 'email' => $order->get_billing_email(),
155 'phone' => $order->get_billing_phone(),
156 'address_1' => $order->get_billing_address_1(),
157 'address_2' => $order->get_billing_address_2(),
158 'city' => $order->get_billing_city(),
159 'state' => $order->get_billing_state(),
160 'country' => $order->get_billing_country(),
161 'postcode' => $order->get_billing_postcode(),
162 );
163
164 $shipping_address = array(
165 'first_name' => $order->get_shipping_first_name(),
166 'last_name' => $order->get_shipping_last_name(),
167 'company' => $order->get_shipping_company(),
168 'address_1' => $order->get_shipping_address_1(),
169 'address_2' => $order->get_shipping_address_2(),
170 'city' => $order->get_shipping_city(),
171 'state' => $shipping_state,
172 'country' => Utils::is_canary_island( $shipping_state, $shipping_country ) ? 'IC' : $shipping_country,
173 'postcode' => $order->get_shipping_postcode(),
174 'house_number' => $order->get_meta( '_shipping_house_number' ),
175 );
176
177 // Check the house number.
178 $this->api_args['shipping_address'] = Address_Utils::split_address( $shipping_address );
179
180 $this->api_args['backend_data'] = array(
181 'delivery_type' => $saved_data['backend']['delivery_type'] ?? '',
182 'insured_shipping' => $saved_data['backend']['insured_shipping'] ?? '',
183 'return_no_answer' => $saved_data['backend']['return_no_answer'] ?? '',
184 'signature_on_delivery' => $saved_data['backend']['signature_on_delivery'] ?? '',
185 'only_home_address' => $saved_data['backend']['only_home_address'] ?? '',
186 'num_labels' => $saved_data['backend']['num_labels'] ?? '',
187 'create_return_label' => $saved_data['backend']['create_return_label'] ?? '',
188 'create_shipment_return_label' => $saved_data['backend']['create_shipment_return_label'] ?? '',
189 'letterbox' => $saved_data['backend']['letterbox'] ?? '',
190 'id_check' => $saved_data['backend']['id_check'] ?? '',
191 'packets' => $saved_data['backend']['packets'] ?? '',
192 'mailboxpacket' => $saved_data['backend']['mailboxpacket'] ?? '',
193 'track_and_trace' => $saved_data['backend']['track_and_trace'] ?? '',
194 'insured_plus' => $saved_data['backend']['insured_plus'] ?? '',
195 'delivery_code_at_door' => $saved_data['backend']['delivery_code_at_door'] ?? '',
196 );
197
198 // Check mailbox weight limit
199 $this->check_mailbox_weight_limit( $this->api_args['backend_data'], $order_weight );
200
201 $this->api_args['frontend_data'] = array(
202 'delivery_day' => array(
203 'value' => $saved_data['frontend']['delivery_day'] ?? '',
204 'date' => $saved_data['frontend']['delivery_day_date'] ?? '',
205 'from' => $saved_data['frontend']['delivery_day_from'] ?? '',
206 'to' => $saved_data['frontend']['delivery_day_to'] ?? '',
207 'price' => $saved_data['frontend']['delivery_day_price'] ?? '',
208 'type' => $saved_data['frontend']['delivery_day_type'] ?? '',
209 ),
210 'pickup_points' => array(
211 'value' => $saved_data['frontend']['dropoff_points'] ?? '',
212 'company' => $saved_data['frontend']['dropoff_points_address_company'] ?? '',
213 'distance' => $saved_data['frontend']['dropoff_points_distance'] ?? '',
214 'address_1' => $saved_data['frontend']['dropoff_points_address_address_1'] ?? '',
215 'address_2' => $saved_data['frontend']['dropoff_points_address_address_2'] ?? '',
216 'city' => $saved_data['frontend']['dropoff_points_address_city'] ?? '',
217 'postcode' => $saved_data['frontend']['dropoff_points_address_postcode'] ?? '',
218 'country' => $saved_data['frontend']['dropoff_points_address_country'] ?? '',
219 'partner_id' => $saved_data['frontend']['dropoff_points_parther_id'] ?? '',
220 'date' => $saved_data['frontend']['dropoff_points_date'] ?? '',
221 'time' => $saved_data['frontend']['dropoff_points_time'] ?? '',
222 ),
223 );
224
225 $this->api_args['order_details'] = array(
226 'order_id' => $order->get_id(),
227 'order_number' => $order->get_order_number(),
228 'main_barcode' => $post_data['main_barcode'],
229 'barcodes' => $post_data['barcodes'],
230 'return_barcode' => isset( $post_data['return_barcode'] ) ? $post_data['return_barcode'] : '',
231 'shipping_return_barcode' => isset( $post_data['shipping_return_barcode'] ) ? $post_data['shipping_return_barcode'] : '',
232 'is_return_activated' => isset( $post_data['is_return_activated'] ) && $post_data['is_return_activated'] ? 'yes' : 'no',
233 'currency' => $order->get_currency(),
234 'total_weight' => $order_weight,
235 'subtotal' => $order->get_subtotal(),
236 );
237
238 // Check mailbox weight limit.
239 $this->check_insurance_amount_limit( $this->api_args['backend_data'], $order->get_subtotal() );
240
241 foreach ( $order->get_items() as $item_id => $item ) {
242 $product = $item->get_product();
243
244 if ( ! is_a( $product, 'WC_Product' ) || $product->is_virtual() ) {
245 continue;
246 }
247
248 $is_adult = Utils::is_adults_only_product( $product );
249 $hs_code = ! empty( $product->get_meta( Single::HS_CODE_FIELD ) ) ? $product->get_meta( Single::HS_CODE_FIELD ) : $this->settings->get_hs_tariff_code();
250 $origin = ! empty( $product->get_meta( Single::ORIGIN_FIELD ) ) ? $product->get_meta( Single::ORIGIN_FIELD ) : $this->settings->get_country_origin();
251
252 $content = array(
253 'product_id' => $product->get_id(),
254 'qty' => $item->get_quantity(),
255 'sku' => $product->get_sku(),
256 'item_value' => 0 !== (int) $item->get_subtotal() ? $item->get_subtotal() : $product->get_regular_price(),
257 'item_description' => $product->get_name(),
258 'item_weight' => $product->get_weight(),
259 'hs_code' => $hs_code,
260 'origin' => $origin,
261 'is_adult' => $is_adult,
262 );
263
264 $this->api_args['order_details']['contents'][] = $content;
265 }
266 }
267
268 /**
269 * Is pickup point being selected or not.
270 *
271 * @return Boolean
272 */
273 public function is_pickup_points() {
274 return ! empty( $this->api_args['frontend_data']['pickup_points']['value'] );
275 }
276
277 /**
278 * Is order has an adult item.
279 *
280 * @return Boolean
281 */
282 public function is_adult_order(): bool {
283 foreach ( $this->api_args['order_details']['contents'] as $item ) {
284 if ( ! empty( $item['is_adult'] ) ) {
285 return true;
286 }
287 }
288 return false;
289 }
290
291 /**
292 * Is pickup point being selected or not.
293 *
294 * @return Boolean
295 */
296 public function is_delivery_day() {
297 return ! empty( $this->api_args['frontend_data']['delivery_day']['value'] );
298 }
299
300 /**
301 * Set extra API args.
302 */
303 public function set_extra_data_to_api_args() {
304 $this->set_order_shipping_product();
305 // $this->set_rest_of_world_args();
306 }
307
308 /**
309 * Set product code in the order details.
310 */
311 public function set_order_shipping_product() {
312 $this->api_args['order_details']['shipping_product'] = $this->get_shipping_product();
313 $this->api_args['order_details']['product_options'] = $this->get_product_options();
314 $this->api_args['order_details']['return_options'] = $this->get_return_options();
315 }
316
317 /**
318 * Change or set the args value for rest of the world.
319 */
320 public function set_rest_of_world_args() {
321 if ( ! $this->is_rest_of_world() ) {
322 return;
323 }
324
325 $this->api_args['settings']['customer_code'] = $this->settings->get_globalpack_customer_code();
326 }
327
328 /**
329 * Check if the current order is for Rest of the world.
330 *
331 * @return Boolean.
332 */
333 public function is_rest_of_world() {
334 $to_country = $this->api_args['shipping_address']['country'];
335 $to_state = $this->api_args['shipping_address']['state'];
336 $destination = Utils::get_shipping_zone( $to_country, $to_state );
337
338 return ( 'ROW' === $destination );
339 }
340
341 /**
342 * Retrieves the args scheme to use with for parsing customer info.
343 *
344 * @return array
345 */
346 protected function get_customer_info_schema() {
347 // Closures in PHP 5.3 do not inherit class context
348 // So we need to copy $this into a lexical variable and pass it to closures manually.
349 $self = $this;
350
351 return array(
352 'location_code' => array(
353 'error' => __( 'Location Code is empty!', 'postnl-for-woocommerce' ),
354 ),
355 'customer_code' => array(
356 'error' => __( 'Customer Code is empty!', 'postnl-for-woocommerce' ),
357 ),
358 'customer_num' => array(
359 'error' => __( 'Customer Number is empty!', 'postnl-for-woocommerce' ),
360 ),
361 'company' => array(
362 'default' => '',
363 ),
364 'email' => array(
365 'validate' => function ( $value ) {
366 if ( empty( $value ) ) {
367 throw new \Exception(
368 __( 'Store email is empty!', 'postnl-for-woocommerce' )
369 );
370 }
371
372 if ( ! is_email( $value ) ) {
373 throw new \Exception(
374 __( 'Wrong format for store email!', 'postnl-for-woocommerce' )
375 );
376 }
377 },
378 ),
379 'return_company' => array(
380 'default' => '',
381 ),
382 'return_address_1' => array(
383 'default' => '',
384 'sanitize' => function ( $value ) use ( $self ) {
385 if ( 'NL' === $self->api_args['store_address']['country'] ) {
386 return ( $self->api_args['settings']['is_return_to_home_enabled'] || '2928' === $this->get_product_code() || '2948' === $this->get_product_code() ) ? $self->api_args['settings']['return_address_street'] : 'Antwoordnummer';
387 }
388
389 return $self->string_length_sanitization( $value, 95 );
390 },
391 ),
392 'return_address_2' => array(
393 'default' => '',
394 'sanitize' => function ( $value ) use ( $self ) {
395 if ( 'NL' === $self->api_args['store_address']['country'] ) {
396 $value = ( $self->api_args['settings']['is_return_to_home_enabled'] || '2928' === $this->get_product_code() || '2948' === $this->get_product_code() ) ? $self->api_args['settings']['return_address_house_no'] : $self->api_args['settings']['return_replynumber'];
397 }
398
399 return $self->string_length_sanitization( $value, 35 );
400 },
401 ),
402 'return_address_house_noext' => array(
403 'default' => '',
404 ),
405 'return_address_city' => array(
406 'default' => '',
407 ),
408 'return_address_zip' => array(
409 'default' => '',
410 ),
411 'return_customer_code' => array(
412 'default' => '',
413 ),
414 );
415 }
416
417 /**
418 * Retrieves the args scheme to use with for parsing store address info.
419 *
420 * @return array
421 */
422 protected function get_shipment_info_schema() {
423 // Closures in PHP 5.3 do not inherit class context
424 // So we need to copy $this into a lexical variable and pass it to closures manually.
425 $self = $this;
426
427 return array(
428 'order_id' => array(
429 'error' => __( 'Order ID is empty!', 'postnl-for-woocommerce' ),
430 ),
431 'order_number' => array(
432 'error' => __( 'Order number is empty!', 'postnl-for-woocommerce' ),
433 ),
434 'main_barcode' => array(
435 'error' => __( 'Barcode is empty!', 'postnl-for-woocommerce' ),
436 ),
437 'barcodes' => array(
438 'default' => array(),
439 ),
440 'return_barcode' => array(
441 'default' => '',
442 ),
443 'shipping_return_barcode' => array(
444 'default' => '',
445 ),
446 'shipping_product' => array(
447 'error' => __( 'Product code is empty!', 'postnl-for-woocommerce' ),
448 'validate' => function ( $value ) {
449 if ( empty( $value ) || ! is_numeric( $value['code'] ) && 4 !== strlen( $value['code'] ) ) {
450 throw new \Exception(
451 __( 'Wrong format for product code!', 'postnl-for-woocommerce' )
452 );
453 }
454 },
455 ),
456 'product_options' => array(
457 'default' => array(
458 'characteristic' => '',
459 'option' => '',
460 ),
461 'sanitize' => function ( $value ) use ( $self ) {
462 return array(
463 'characteristic' => ! empty( $value['characteristic'] ) ? $self->string_length_sanitization( $value['characteristic'], 3 ) : '',
464 'option' => ! empty( $value['option'] ) ? $self->string_length_sanitization( $value['option'], 3 ) : '',
465 );
466 },
467 ),
468 'return_options' => array(
469 'default' => array(),
470 ),
471 'merchant_code' => array(
472 'default' => '',
473 'sanitize' => function ( $value ) use ( $self ) {
474 // Get merchant code for destination country if it's non-EU
475 $destination_country = $self->api_args['shipping_address']['country'];
476 return Utils::get_merchant_code_for_country( $destination_country );
477 },
478 ),
479 'printer_type' => array(
480 'default' => '4909' === $this->get_product_code() ? 'GraphicFile|PDF' : $this->settings->get_printer_type(),
481 'sanitize' => function ( $value ) use ( $self ) {
482 if ( in_array( $this->get_product_code(), array( '4909', '2928', '2948' ), true ) ) {
483 return 'GraphicFile|PDF';
484 }
485
486 return $this->settings->get_printer_type();
487 },
488 ),
489 'shipment_return_label' => array(
490 'default' => $this->settings->get_return_shipment_and_labels_all(),
491 'sanitize' => function ( $value ) use ( $self ) {
492 return sanitize_text_field( $value );
493 },
494 ),
495 'total_weight' => array(
496 'error' => __( 'Total weight is empty!', 'postnl-for-woocommerce' ),
497 'sanitize' => function ( $value ) use ( $self ) {
498 return $self->float_round_sanitization( $value, 2 );
499 },
500 ),
501 'subtotal' => array(
502 'default' => 0,
503 'sanitize' => function ( $value ) use ( $self ) {
504 return $self->float_round_sanitization( $value, 2 );
505 },
506 ),
507 'email' => array(
508 'validate' => function ( $value ) {
509 if ( ! is_email( $value ) ) {
510 throw new \Exception(
511 __( 'Customer email is not valid!', 'postnl-for-woocommerce' )
512 );
513 }
514 },
515 ),
516 'currency' => array(
517 'default' => '',
518 ),
519 'phone' => array(
520 'default' => '',
521 ),
522 );
523 }
524
525 /**
526 * Retrieves the args scheme to use with for parsing pickup points info.
527 *
528 * @return array
529 */
530 protected function get_delivery_day_info_schema() {
531 // Closures in PHP 5.3 do not inherit class context
532 // So we need to copy $this into a lexical variable and pass it to closures manually.
533 $self = $this;
534
535 return array(
536 'date' => array(
537 'default' => '',
538 'validate' => function ( $date ) use ( $self ) {
539 if ( empty( $date ) && $self->is_delivery_day() ) {
540 throw new \Exception(
541 __( 'Delivery day "Date" is empty!', 'postnl-for-woocommerce' )
542 );
543 }
544 },
545 'sanitize' => function ( $value ) {
546 $timestamp = strtotime( $value );
547 $date = gmdate( 'd-m-Y', $timestamp );
548
549 return $date;
550 },
551 ),
552 'from' => array(
553 'default' => '',
554 'validate' => function ( $hour ) use ( $self ) {
555 if ( empty( $hour ) && $self->is_delivery_day() ) {
556 throw new \Exception(
557 __( 'Delivery day "From" is empty!', 'postnl-for-woocommerce' )
558 );
559 }
560 },
561 'sanitize' => function ( $value ) {
562 return $value . ':00';
563 },
564 ),
565 'to' => array(
566 'default' => '',
567 'validate' => function ( $hour ) use ( $self ) {
568 if ( empty( $hour ) && $self->is_delivery_day() ) {
569 throw new \Exception(
570 __( 'Delivery day "To" is empty!', 'postnl-for-woocommerce' )
571 );
572 }
573 },
574 'sanitize' => function ( $value ) {
575 return $value . ':00';
576 },
577 ),
578 'price' => array(
579 'default' => '',
580 ),
581 'type' => array(
582 'default' => '',
583 'validate' => function ( $type ) use ( $self ) {
584 if ( empty( $type ) && $self->is_delivery_day() ) {
585 throw new \Exception(
586 __( 'Delivery day "Type" is empty!', 'postnl-for-woocommerce' )
587 );
588 }
589 },
590 ),
591 );
592 }
593
594 /**
595 * Retrieves the args scheme to use with for parsing pickup points info.
596 *
597 * @return array
598 */
599 protected function get_pickup_points_info_schema() {
600 // Closures in PHP 5.3 do not inherit class context
601 // So we need to copy $this into a lexical variable and pass it to closures manually.
602 $self = $this;
603
604 return array(
605 'date' => array(
606 'default' => '',
607 'validate' => function ( $date ) use ( $self ) {
608 if ( empty( $date ) && $self->is_pickup_points() ) {
609 throw new \Exception(
610 __( 'Pickup "Date" is empty!', 'postnl-for-woocommerce' )
611 );
612 }
613 },
614 'sanitize' => function ( $value ) {
615 $timestamp = strtotime( $value );
616 $date = gmdate( 'd-m-Y', $timestamp );
617
618 return $date;
619 },
620 ),
621 'time' => array(
622 'default' => '',
623 'validate' => function ( $hour ) use ( $self ) {
624 if ( empty( $hour ) && $self->is_pickup_points() ) {
625 throw new \Exception(
626 __( 'Pickup "Time" is empty!', 'postnl-for-woocommerce' )
627 );
628 }
629 },
630 'sanitize' => function ( $value ) {
631 return $value . ':00';
632 },
633 ),
634 'company' => array(
635 'validate' => function ( $company ) use ( $self ) {
636 if ( empty( $company ) && $self->is_pickup_points() ) {
637 throw new \Exception(
638 __( 'Pickup "Company name" is empty!', 'postnl-for-woocommerce' )
639 );
640 }
641 },
642 ),
643 'address_1' => array(
644 'validate' => function ( $address ) use ( $self ) {
645 if ( empty( $address ) && $self->is_pickup_points() ) {
646 throw new \Exception(
647 __( 'Pickup "Address 1" is empty!', 'postnl-for-woocommerce' )
648 );
649 }
650 },
651 'sanitize' => function ( $address ) use ( $self ) {
652 return $self->string_length_sanitization( $address, 50 );
653 },
654 ),
655 'address_2' => array(
656 'default' => '',
657 ),
658 'city' => array(
659 'validate' => function ( $city ) use ( $self ) {
660 if ( empty( $city ) && $self->is_pickup_points() ) {
661 throw new \Exception(
662 __( 'Pickup Point "City" is empty!', 'postnl-for-woocommerce' )
663 );
664 }
665 },
666 ),
667 'postcode' => array(
668 'validate' => function ( $postcode ) use ( $self ) {
669 if ( empty( $postcode ) && $self->is_pickup_points() ) {
670 throw new \Exception(
671 __( 'Pickup Point "Postcode" is empty!', 'postnl-for-woocommerce' )
672 );
673 }
674 },
675 ),
676 'state' => array(
677 'default' => '',
678 ),
679 'country' => array(
680 'validate' => function ( $country ) use ( $self ) {
681 if ( empty( $country ) && $self->is_pickup_points() ) {
682 throw new \Exception(
683 __( 'Pickup Point "Country" is empty!', 'postnl-for-woocommerce' )
684 );
685 }
686 },
687 ),
688 );
689 }
690
691 /**
692 * Retrieves the args scheme to use with for parsing data from database.
693 *
694 * @return array
695 */
696 protected function get_backend_data_info_schema() {
697 // Closures in PHP 5.3 do not inherit class context
698 // So we need to copy $this into a lexical variable and pass it to closures manually.
699 $self = $this;
700
701 return array(
702 'delivery_type' => array(
703 'default' => 'Standard',
704 'sanitize' => function ( $type ) {
705 return ( 'Evening' === $type ) ? 'Evening' : 'Standard';
706 },
707 ),
708 'insured_shipping' => array(
709 'default' => false,
710 'sanitize' => function ( $picked ) {
711 return ( 'yes' === $picked );
712 },
713 ),
714 'return_no_answer' => array(
715 'default' => false,
716 'sanitize' => function ( $picked ) {
717 return ( 'yes' === $picked );
718 },
719 ),
720 'signature_on_delivery' => array(
721 'default' => false,
722 'sanitize' => function ( $picked ) {
723 return ( 'yes' === $picked );
724 },
725 ),
726 'only_home_address' => array(
727 'default' => false,
728 'sanitize' => function ( $picked ) {
729 return ( 'yes' === $picked );
730 },
731 ),
732 'num_labels' => array(
733 'default' => '1',
734 'sanitize' => function ( $num ) use ( $self ) {
735 $abs_number = abs( intval( $num ) );
736 if ( empty( $abs_number ) ) {
737 return 1;
738 }
739
740 return ( 10 >= $abs_number ) ? $abs_number : 10;
741 },
742 ),
743 'create_return_label' => array(
744 'default' => false,
745 'sanitize' => function ( $picked ) {
746 return ( 'yes' === $picked );
747 },
748 ),
749 'letterbox' => array(
750 'default' => false,
751 'sanitize' => function ( $picked ) {
752 return ( 'yes' === $picked );
753 },
754 ),
755 'id_check' => array(
756 'default' => false,
757 'sanitize' => function ( $picked ) {
758 return ( 'yes' === $picked );
759 },
760 ),
761 'packets' => array(
762 'default' => false,
763 'sanitize' => function ( $picked ) {
764 return ( 'yes' === $picked );
765 },
766 ),
767 'mailboxpacket' => array(
768 'default' => false,
769 'sanitize' => function ( $picked ) {
770 return ( 'yes' === $picked );
771 },
772 ),
773 'track_and_trace' => array(
774 'default' => false,
775 'sanitize' => function ( $picked ) {
776 return ( 'yes' === $picked );
777 },
778 ),
779 'insured_plus' => array(
780 'default' => false,
781 'sanitize' => function ( $picked ) {
782 return ( 'yes' === $picked );
783 },
784 ),
785 'delivery_code_at_door' => array(
786 'default' => false,
787 'sanitize' => function ( $picked ) {
788 return ( 'yes' === $picked );
789 },
790 ),
791 );
792 }
793
794 /**
795 * Retrieves the args scheme to use with parser for parsing order content item info.
796 *
797 * @return array
798 * @since [*next-version*]
799 */
800 protected function get_content_item_info_schema() {
801 // Closures in PHP 5.3 do not inherit class context.
802 // So we need to copy $this into a lexical variable and pass it to closures manually.
803 $self = $this;
804
805 return array(
806 'hs_code' => array(
807 'default' => '',
808 'validate' => function ( $hs_code ) {
809 $length = is_string( $hs_code ) ? strlen( $hs_code ) : 0;
810
811 if ( empty( $length ) ) {
812 return;
813 }
814
815 if ( $length < 6 || $length > 20 ) {
816 throw new \Exception(
817 __( 'Item HS Code must be between 6 and 20 characters long', 'postnl-for-woocommerce' )
818 );
819 }
820 },
821 ),
822 'item_description' => array(
823 'rename' => 'description',
824 'default' => '',
825 'sanitize' => function ( $description ) use ( $self ) {
826 return $self->string_length_sanitization( $description, 35 );
827 },
828 ),
829 'product_id' => array(
830 'error' => __( 'Item "Product ID" is empty!', 'postnl-for-woocommerce' ),
831 ),
832 'sku' => array(
833 'error' => __( 'Item "Product SKU" is empty!', 'postnl-for-woocommerce' ),
834 ),
835 'item_value' => array(
836 'rename' => 'value',
837 'default' => 0,
838 'sanitize' => function ( $value ) use ( $self ) {
839 return $self->float_round_sanitization( $value, 2 );
840 },
841 ),
842 'origin' => array(
843 'default' => Utils::get_base_country(),
844 ),
845 'qty' => array(
846 'validate' => function ( $qty ) {
847
848 if ( ! is_numeric( $qty ) || $qty < 1 ) {
849 throw new \Exception(
850 __( 'Item quantity must be more than 1', 'postnl-for-woocommerce' )
851 );
852 }
853 },
854 ),
855 'item_weight' => array(
856 'rename' => 'weight',
857 'sanitize' => function ( $weight ) use ( $self ) {
858
859 $weight = $self->maybe_convert_to_grams( $weight, $self->weight_uom );
860 $weight = ( $weight > 1 ) ? $weight : 1;
861
862 return $weight;
863 },
864 ),
865 );
866 }
867
868 /**
869 * Calculate total weight in one order.
870 *
871 * @param \WC_Order $order Order object.
872 *
873 * @return Float Total weight.
874 */
875 protected function calculate_order_weight( $order ) {
876 $total_weight = 0;
877
878 if ( ! is_a( $order, 'WC_Order' ) ) {
879 return apply_filters( 'postnl_order_weight', $total_weight, $order );
880 }
881
882 $ordered_items = $order->get_items();
883
884 if ( empty( $ordered_items ) || ! is_array( $ordered_items ) ) {
885 return apply_filters( 'postnl_order_weight', $total_weight, $order );
886 }
887
888 foreach ( $ordered_items as $key => $item ) {
889 $product = $item->get_product();
890
891 if ( ! is_a( $product, 'WC_Product' ) || $product->is_virtual() ) {
892 continue;
893 }
894
895 $product_weight = $product->get_weight();
896 $quantity = $item->get_quantity();
897
898 if ( $product_weight ) {
899 $total_weight += ( $quantity * $product_weight );
900 }
901 }
902
903 $total_weight = Utils::maybe_convert_weight( $total_weight );
904
905 return apply_filters( 'postnl_order_weight', $total_weight, $order );
906 }
907
908 /**
909 * Get selected shipping features.
910 *
911 * @return String
912 */
913 public function get_selected_shipping_features() {
914 foreach ( $this->api_args['frontend_data'] as $parent_name => $parent_data ) {
915 foreach ( $parent_data as $data_key => $data_value ) {
916 if ( ! empty( $data_value ) ) {
917 return $parent_name;
918 }
919 }
920 }
921
922 return array_key_first( $this->api_args['frontend_data'] );
923 }
924
925 /**
926 * Get product from api args.
927 *
928 * @return array.
929 * @throws \Exception
930 */
931 public function get_shipping_product() {
932 $checked_features = Utils::get_selected_label_features( $this->api_args['backend_data'] );
933 $shipping_feature = $this->get_selected_shipping_features();
934 $from_country = $this->api_args['store_address']['country'];
935 $to_country = $this->api_args['shipping_address']['country'];
936 $to_state = $this->api_args['shipping_address']['state'];
937
938 // Check if letterbox is selected and determine which letterbox type to use.
939 if ( 'yes' === ( $checked_features['letterbox'] ?? '' ) ) {
940 $letterbox_type = $this->get_letterbox_type();
941 // Remove generic 'letterbox' and add specific type.
942 unset( $checked_features['letterbox'] );
943
944 if ( 'letterbox_48' === $letterbox_type ) {
945 $checked_features['letterbox_48'] = 'yes';
946 } else {
947 // Default to letterbox (24 hours).
948 $checked_features['letterbox'] = 'yes';
949 }
950 }
951
952 $features = array_keys( $checked_features );
953 $code_map = Mapping::products_data();
954
955 $selected_product = array();
956 $destination = Utils::get_shipping_zone( $to_country, $to_state );
957 if ( empty( $code_map[ $from_country ][ $destination ][ $shipping_feature ] ) ) {
958 return $selected_product;
959 }
960 foreach ( $code_map[ $from_country ][ $destination ][ $shipping_feature ] as $product ) {
961 if ( empty( $product['combination'] ) && empty( $selected_product ) ) {
962 $selected_product = $product;
963 continue;
964 }
965 $is_this_it = true;
966 foreach ( $features as $feature ) {
967 if ( ! in_array( $feature, $product['combination'] ) ) {
968 $is_this_it = false;
969 }
970 }
971 foreach ( $product['combination'] as $combination ) {
972 if ( ! in_array( $combination, $features ) ) {
973 $is_this_it = false;
974 }
975 }
976
977 if ( $is_this_it ) {
978 $selected_product = $product;
979 }
980 }
981
982 return $selected_product;
983 }
984
985 /**
986 * Resolve the letterbox product variant for this shipment.
987 *
988 * Note the asymmetric token values: 'letterbox' denotes the 24h variant
989 * (product 2928) and 'letterbox_48' denotes the 48h variant (product 2948).
990 *
991 * Resolution order:
992 * 1. The customer's recorded choice on the order (_postnl_letterbox_type).
993 * 2. The merchant default setting (24h or 48h).
994 * 3. For 'customer_decide' with no recorded choice — e.g. an order placed
995 * before this feature shipped — fall back to 24h (2928), which matches
996 * the behaviour that was in effect when those legacy orders were
997 * originally placed. The assigned variant is surfaced to the merchant
998 * in the Shipping Options order-overview column so they can correct it
999 * on the rare exception.
1000 *
1001 * @since 5.9.6
1002 *
1003 * @return string 'letterbox' (24h / 2928) or 'letterbox_48' (48h / 2948).
1004 */
1005 protected function get_letterbox_type() {
1006 $order = ! empty( $this->api_args['order_details']['order_id'] )
1007 ? wc_get_order( $this->api_args['order_details']['order_id'] )
1008 : null;
1009
1010 if ( $order instanceof \WC_Order ) {
1011 $stored_type = $order->get_meta( '_postnl_letterbox_type' );
1012 if ( in_array( $stored_type, array( 'letterbox', 'letterbox_48' ), true ) ) {
1013 return $stored_type;
1014 }
1015 }
1016
1017 $default_letterbox_type = $this->settings->get_default_automatic_letterboxparcel_product();
1018
1019 if ( 'letterbox_48' === $default_letterbox_type ) {
1020 return 'letterbox_48';
1021 }
1022
1023 // 'customer_decide' with no recorded choice: backward-compatibility gap
1024 // for orders placed before this feature. Default to 24h and log it.
1025 if ( 'customer_decide' === $default_letterbox_type && $order instanceof \WC_Order ) {
1026 \PostNLWooCommerce\Main::get_logger()->write(
1027 sprintf(
1028 'PostNL letterbox: order #%d has no recorded 24h/48h choice under "customer decide"; defaulting to Letterbox 24h (2928).',
1029 $order->get_id()
1030 )
1031 );
1032 }
1033
1034 return 'letterbox';
1035 }
1036
1037 /**
1038 * Get product code from api args.
1039 *
1040 * @return string.
1041 * @throws \Exception
1042 */
1043 public function get_product_code() {
1044 $product = $this->get_shipping_product();
1045
1046 return $product['code'] ?? '';
1047 }
1048
1049 /**
1050 * Get product options from api args.
1051 *
1052 * @return array.
1053 */
1054 public function get_product_options() {
1055 $option_map = Mapping::additional_product_options();
1056 $from_country = $this->api_args['store_address']['country'];
1057 $to_country = $this->api_args['shipping_address']['country'];
1058 $to_state = $this->api_args['shipping_address']['state'];
1059 $destination = Utils::get_shipping_zone( $to_country, $to_state );
1060
1061 foreach ( $this->api_args as $arg_keys => $arg_data ) {
1062 if ( empty( $option_map[ $from_country ][ $destination ][ $arg_keys ] ) ) {
1063 continue;
1064 }
1065
1066 foreach ( $arg_data as $index => $data ) {
1067 if ( empty( $option_map[ $from_country ][ $destination ][ $arg_keys ][ $index ] ) ) {
1068 continue;
1069 }
1070
1071 foreach ( $data as $idx => $val ) {
1072 if ( ! empty( $option_map[ $from_country ][ $destination ][ $arg_keys ][ $index ][ $idx ][ $val ] ) ) {
1073 return $option_map[ $from_country ][ $destination ][ $arg_keys ][ $index ][ $idx ][ $val ];
1074 }
1075 }
1076 }
1077 }
1078
1079 return array();
1080 }
1081
1082 /**
1083 * Check mailbox weight limit.
1084 *
1085 * @param $backend_data .
1086 * @param $order_weight .
1087 *
1088 * @return void.
1089 * @throws \Exception if the order weight exceeds 2000 grams.
1090 */
1091 protected function check_mailbox_weight_limit( $backend_data, $order_weight ) {
1092 $is_mailbox = 'yes' === $backend_data['mailboxpacket'] || 'yes' === $backend_data['letterbox'];
1093
1094 if ( $is_mailbox && 2000 < $order_weight ) {
1095 throw new \Exception(
1096 esc_html__( 'Max weight for Mailbox Packet is 2kg!', 'postnl-for-woocommerce' )
1097 );
1098 }
1099 }
1100
1101 /**
1102 * Check Insurance amount limit.
1103 *
1104 * @param $backend_data .
1105 * @param $order_total .
1106 *
1107 * @return void.
1108 * @throws \Exception if the order weight exceeds € 5000.
1109 */
1110 protected function check_insurance_amount_limit( $backend_data, $order_total ) {
1111 if ( $this->is_rest_of_world() ) {
1112 // Only for EU.
1113 return;
1114 }
1115
1116 if ( 'yes' === $backend_data['insured_shipping'] && $order_total > 5000 ) {
1117 throw new \Exception(
1118 // Translators: %s is the order total.
1119 sprintf( esc_html__( 'Insurance amount for EU shipments cannot exceed €5000. Your total is: %d', 'postnl-for-woocommerce' ), $order_total )
1120 );
1121 }
1122 }
1123
1124 /**
1125 * Get Shipping and Return options.
1126 *
1127 * @return array
1128 */
1129 public function get_return_options() {
1130 $shipment_return_type = $this->settings->get_return_shipment_and_labels();
1131 if ( 'none' === $shipment_return_type ) {
1132 return array();
1133 }
1134
1135 // Extract data from API args.
1136 $from_country = $this->api_args['store_address']['country'];
1137 $to_country = $this->api_args['shipping_address']['country'];
1138 $to_state = $this->api_args['shipping_address']['state'];
1139 $destination = Utils::get_shipping_zone( $to_country, $to_state );
1140
1141 $is_letterbox = 'yes' === $this->api_args['backend_data']['letterbox'];
1142 $is_return_activated = 'yes' === $this->api_args['order_details']['is_return_activated'];
1143 $create_return_label = 'yes' === ( $this->api_args['backend_data']['create_return_label'] ?? 'no' );
1144 $is_BE_destination = ( 'BE' === $destination );
1145 $is_adult_order = $this->is_adult_order();
1146 $return_all_labels = 'yes' === $this->settings->get_return_shipment_and_labels_all();
1147
1148 // Return empty array for letterbox without return label.
1149 if ( $is_letterbox && ! $create_return_label ) {
1150 return array();
1151 }
1152
1153 // Determine the appropriate shipment return type.
1154 if ( $this->requires_in_box_return( $is_letterbox, $is_BE_destination, $is_adult_order, $create_return_label ) ) {
1155 $shipment_return_type = 'in_box';
1156 } elseif ( ! $is_return_activated &&
1157 ! $is_letterbox &&
1158 ! $return_all_labels &&
1159 'shipping_return' === $shipment_return_type &&
1160 ! $is_BE_destination ) {
1161 $shipment_return_type = 'return_all_labels_not_active';
1162 }
1163
1164 // Get available options from mapping.
1165 $return_label_options = Mapping::shipping_return_labels_options();
1166 if ( ! isset( $return_label_options[ $from_country ][ $destination ][ $shipment_return_type ] ) ) {
1167 return array();
1168 }
1169
1170 // Check if current shipping product is allowed.
1171 $option_data = $return_label_options[ $from_country ][ $destination ][ $shipment_return_type ];
1172 $allowed_products = $option_data['products'];
1173 $current_product = $this->api_args['order_details']['shipping_product']['code'];
1174
1175 // Return options if product is allowed or if there are no product restrictions.
1176 if ( empty( $allowed_products ) || in_array( $current_product, $allowed_products ) ) {
1177 return $option_data['options'];
1178 }
1179
1180 return array();
1181 }
1182
1183 /**
1184 * Determine if return type should be forced to 'in_box' based on business logic.
1185 *
1186 * @param bool $is_letterbox
1187 * @param bool $is_BE_destination
1188 * @param bool $is_adult_order
1189 * @param bool $create_return_label
1190 *
1191 * @return bool
1192 */
1193 private function requires_in_box_return( bool $is_letterbox, bool $is_BE_destination, bool $is_adult_order, bool $create_return_label ): bool {
1194 return (
1195 ( $is_letterbox && $create_return_label )
1196 || $is_BE_destination
1197 || ( $is_adult_order && $create_return_label )
1198 );
1199 }
1200 }
1201