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;
}
}