settings = Settings::get_instance(); $this->meta_name = '_' . $this->prefix . 'order_metadata'; $this->letterbox_meta_name = '_' . $this->prefix . 'letterbox'; $this->set_template_file(); $this->set_primary_field_name(); $this->init_hooks(); } /** * Need to set the primary field name; */ abstract public function set_primary_field_name(); /** * Need to set the template file name; */ abstract public function set_template_file(); /** * List of frontend fields. */ abstract public function get_fields(); /** * Check if this feature is enabled from the settings. * * @return bool */ abstract public function is_enabled(); /** * Collection of hooks when initiation. */ public function init_hooks() { add_filter( 'woocommerce_checkout_posted_data', array( $this, 'validate_posted_data' ) ); add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_data' ), 10, 2 ); add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_letterbox_data' ), 13, 2 ); add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_letterbox_type' ), 14, 2 ); add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'save_default_data' ), 15, 2 ); add_filter( 'postnl_frontend_checkout_tab', array( $this, 'add_checkout_tab' ), 10, 2 ); add_action( 'postnl_checkout_content', array( $this, 'display_content' ), 10, 2 ); add_filter( 'woocommerce_email_order_meta_fields', array( $this, 'add_pickup_points_fields_to_email' ), 10, 3 ); } /** * Adding a tab in the frontend checkout. * * @param array $tabs List of displayed tabs. * @param array $response Response from PostNL Checkout Rest API. * * @return array */ abstract public function add_checkout_tab( $tabs, $response ); /** * Adding a content in the frontend checkout. * * @param array $response Response from PostNL Checkout Rest API. * @param array $post_data Post data on checkout page. */ abstract public function get_content_data( $response, $post_data ); /** * Add pickup points info to email templates. * * @param array $fields Current fields. * @param bool $sent_to_admin If should sent to admin. * @param \WC_Order $order Order instance. * * @return array */ public function add_pickup_points_fields_to_email( $fields, $sent_to_admin, $order ) { $data = $this->get_data( $order->get_id() ); if ( ! empty( $data['frontend'] ) ) { $value = $this->generate_pickup_points_email_html( $data['frontend'] ); $fields['Pickup address'] = array( 'label' => __( 'Pick up at PostNL-point', 'postnl-for-woocommerce' ), 'value' => $value, ); } return $fields; } /** * Generate the dropoff points html information. * * @param Array $infos Dropoff points informations. */ public function generate_pickup_points_email_html( $infos ) { $filtered_infos = Utils::get_filtered_pickup_points_infos( $infos ); if ( empty( $filtered_infos ) ) { return ''; } $value = "
"; foreach ( $filtered_infos as $info_idx => $info_val ) { switch ( $info_idx ) { case 'dropoff_points_date': $additional_text = esc_html__( 'Date:', 'postnl-for-woocommerce' ); break; case 'dropoff_points_time': $additional_text = esc_html__( 'Time:', 'postnl-for-woocommerce' ); break; default: $additional_text = ''; break; } $value .= '
' . esc_html( $additional_text . ' ' . $info_val ) . '
'; } $value .= '

'; return $value; } /** * Adding a content in the frontend checkout. * * @param array $response Response from PostNL Checkout Rest API. * @param array $post_data Post data on checkout page. */ public function display_content( $response, $post_data ) { $template_args = array( 'data' => $this->get_content_data( $response, $post_data ), ); wc_get_template( $this->template_file, $template_args, '', POSTNL_WC_PLUGIN_DIR_PATH . '/templates/' ); } /** * Add value to the fields. * * @return array */ public function get_fields_with_value() { $post_data = array(); if ( isset( $_REQUEST['post_data'] ) ) { parse_str( sanitize_text_field( wp_unslash( $_REQUEST['post_data'] ) ), $post_data ); } $field_w_val = array_map( function ( $field ) use ( $post_data ) { $field['value'] = array_key_exists( $field['id'], $post_data ) ? $post_data[ $field['id'] ] : ''; return $field; }, $this->get_fields() ); return $field_w_val; } /** * Validate posted data. * * @param array $data Array of posted data. */ public function validate_posted_data( $data ) { $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // phpcs:ignore $expiry_message = sprintf( /* translators: %s: shop cart url */ __( 'Sorry, your session has expired. Return to shop', 'postnl-for-woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ); if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) { return $data; } if ( ! $this->check_selected_option( $_POST ) ) { return $data; } $data = $this->validate_fields( $data, $_POST ); $data = $this->add_default_value_to_data( $data, $_POST ); return $data; } /** * Add default value to data. * * @param array $data Array of posted data. * @param array $posted_data Array of global _POST data. * * @return array. */ public function add_default_value_to_data( $data, $posted_data ) { foreach ( $posted_data as $input_id => $input_val ) { if ( false === strpos( $input_id, 'postnl_default' ) ) { continue; } $data[ $input_id ] = $input_val; } return $data; } /** * Validate delivery type fields. * * @param array $data Array of posted data. * @param array $posted_data Array of global _POST data. * * @return array */ abstract public function validate_fields( $data, $posted_data ); /** * Check the selected options. * * @param array $posted_data Array of global _POST data. * * @return boolean */ public function check_selected_option( $posted_data ) { if ( empty( $posted_data['postnl_option'] ) ) { return false; } return ( $posted_data['postnl_option'] === $this->primary_field ); } /** * Get primary field value. * * @param array $post_data Array of global _POST data. * * @return mixed */ public function get_primary_field_value( $post_data ) { $fields = $this->get_fields(); if ( empty( $post_data['postnl_option'] ) ) { return ''; } if ( $this->primary_field !== $post_data['postnl_option'] ) { return ''; } return ( ! empty( $post_data[ $fields['0']['id'] ] ) ) ? $post_data[ $fields['0']['id'] ] : ''; } /** * Get content data initiation. * * @param array $post_data Array of global _POST data. * * @return array */ public function get_init_content_data( $post_data ) { $fields = $this->get_fields(); $value = $this->get_primary_field_value( $post_data ); return array( 'field_name' => $fields['0']['id'], 'value' => $value, ); } /** * Get frontend data from Order object. * * @param int $order_id ID of the order. * * @return array. */ public function get_data( $order_id ) { $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return array(); } $data = $order->get_meta( $this->meta_name ); return ! empty( $data ) && is_array( $data ) ? $data : array(); } /** * Save default value to data if dropoff points is not picked. * * @param array $order_id ID of order post. * @param array $posted_data Array of global _POST data. * * @return array. */ public function save_default_data( $order_id, $posted_data ) { $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return; } $data = $this->get_data( $order->get_id() ); if ( ! empty( $data['frontend'] ) ) { return; } foreach ( $posted_data as $input_id => $input_val ) { if ( false === strpos( $input_id, 'postnl_default' ) || empty( $input_val ) ) { continue; } $replaced_id = str_replace( 'postnl_default', 'postnl_delivery_day', $input_id ); $field_name = Utils::remove_prefix_field( $this->prefix, $replaced_id ); $data['frontend'][ $field_name ] = $input_val; } $order->update_meta_data( $this->meta_name, $data ); $order->save(); } /** * Save frontend field value to meta. * * @param int $order_id ID of the order. * @param array $posted_data Posted values. */ public function save_data( $order_id, $posted_data ) { $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return; } $data = $this->get_data( $order->get_id() ); foreach ( $this->get_fields() as $field ) { if ( array_key_exists( $field['id'], $posted_data ) && ! empty( $posted_data[ $field['id'] ] ) ) { $field_name = Utils::remove_prefix_field( $this->prefix, $field['id'] ); $data['frontend'][ $field_name ] = sanitize_text_field( wp_unslash( $posted_data[ $field['id'] ] ) ); } } $order->update_meta_data( $this->meta_name, $data ); $order->save(); } /** * Check if order is eligible for the letterbox and save it as order meta. * * @param int $order_id ID of the order. * @param array $posted_data Posted values. */ public function save_letterbox_data( $order_id, $posted_data ) { $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return; } $eligible_for_letterbox = Utils::is_order_eligible_auto_letterbox( $order ); if ( $eligible_for_letterbox ) { $order->update_meta_data( $this->letterbox_meta_name, 1 ); $order->save(); } } /** * Save the letterbox type from the selected shipping method. * * @since 5.9.6 * * @param int $order_id ID of the order. * @param array $posted_data Posted values. */ public function save_letterbox_type( $order_id, $posted_data ) { $order = wc_get_order( $order_id ); if ( ! is_a( $order, 'WC_Order' ) ) { return; } // Get the shipping methods from the order. $shipping_methods = $order->get_shipping_methods(); if ( empty( $shipping_methods ) ) { return; } foreach ( $shipping_methods as $shipping_item ) { // Check if this is a PostNL shipping method. if ( ! in_array( $shipping_item->get_method_id(), $this->settings->get_supported_shipping_methods(), true ) ) { continue; } // Check if the rate has letterbox type in meta data. $meta_data = $shipping_item->get_meta_data(); foreach ( $meta_data as $meta ) { if ( 'letterbox_type' === $meta->key && in_array( $meta->value, array( 'letterbox', 'letterbox_48' ), true ) ) { $order->update_meta_data( '_postnl_letterbox_type', $meta->value ); // Persist the meta explicitly. Core saves the order BEFORE this // hook fires and passes only the order ID, so the in-memory change // above would otherwise be discarded. save_meta_data() writes just // the meta row and does not fire the full woocommerce_update_order // save cycle. $order->save_meta_data(); return; } } } } /** * Get evening fee data. * * @return array */ public static function evening_fee_data() { $settings = Settings::get_instance(); $evening_fee = $settings->get_evening_delivery_fee(); return array( 'fee_name' => esc_html__( 'PostNL Evening Fee', 'postnl-for-woocommerce' ), 'fee_price' => floatval( $evening_fee ), 'condition' => array( 'key' => 'delivery_day_type', 'value' => 'Evening', ), ); } /** * Get morning fee data. * * @return array */ public static function morning_fee_data() { $settings = Settings::get_instance(); $morning_fee = $settings->get_morning_delivery_fee(); return array( 'fee_name' => esc_html__( 'PostNL Morning Fee', 'postnl-for-woocommerce' ), 'fee_price' => floatval( $morning_fee ), 'condition' => array( 'key' => 'delivery_day_type', 'value' => '08:00-12:00', ), ); } /** * Get available nonstandard delivery time fees data. * * @return array */ public static function non_standard_fees_data() { return array( '08:00-12:00' => self::morning_fee_data(), 'Evening' => self::evening_fee_data(), ); } /** * Check whether the chosen PostNL shipping rate has cost = 0. * * Returns true when the currently selected shipping method is a supported * PostNL method and its rate cost is zero — meaning the store's * minimum_for_free_shipping threshold has been reached. * * @return bool */ protected function is_postnl_rate_free(): bool { if ( ! WC()->session ) { return false; } $chosen = WC()->session->get( 'chosen_shipping_methods', array() ); $packages = WC()->shipping()->get_packages(); $supported = $this->settings->get_supported_shipping_methods(); foreach ( $packages as $i => $package ) { $method_key = $chosen[ $i ] ?? ''; if ( ! isset( $package['rates'][ $method_key ] ) ) { continue; } $rate = $package['rates'][ $method_key ]; if ( in_array( $rate->get_method_id(), $supported, true ) && 0.0 === (float) $rate->cost ) { return true; } } return false; } }