get_date_created() : null; $context = array( 'total' => (float) $order->get_total(), 'currency' => is_callable( array( $order, 'get_currency' ) ) ? strtoupper( (string) $order->get_currency() ) : '', 'order_id' => is_callable( array( $order, 'get_id' ) ) ? (int) $order->get_id() : 0, 'created_timestamp' => ( is_object( $created ) && is_callable( array( $created, 'getTimestamp' ) ) ) ? (int) $created->getTimestamp() : null, 'item_count' => 0, 'line_count' => 0, 'product_count' => 0, 'attribute_tokens' => array(), 'backorder_flags' => array(), 'virtual_flags' => array(), 'downloadable_flags' => array(), 'shipping_method_ids' => array(), 'shipping_instance_ids' => array(), 'roles' => array(), 'is_guest' => ! $order->get_user_id(), 'user_id' => (int) $order->get_user_id(), 'billing_email' => (string) $order->get_billing_email(), 'payment_method' => (string) $order->get_payment_method(), 'product_ids' => array(), 'line_product_ids' => array(), 'line_cats' => array(), 'line_tags' => array(), 'line_brands' => array(), 'line_types' => array(), 'status' => 'wc-' . $order->get_status(), 'coupon_codes' => array_map( 'strtolower', (array) $order->get_coupon_codes() ), 'billing_country' => (string) $order->get_billing_country(), 'billing_state' => (string) $order->get_billing_state(), 'billing_postcode' => (string) $order->get_billing_postcode(), 'shipping_country' => (string) $order->get_shipping_country(), 'shipping_state' => (string) $order->get_shipping_state(), 'shipping_postcode' => (string) $order->get_shipping_postcode(), ); $user = $order->get_user(); if ( $user ) { $context['roles'] = (array) $user->roles; } foreach ( $order->get_shipping_methods() as $item ) { $method_id = (string) $item->get_method_id(); if ( '' !== $method_id ) { $context['shipping_method_ids'][] = $method_id; } $instance_id = method_exists( $item, 'get_instance_id' ) ? (string) $item->get_instance_id() : ''; if ( '' !== $method_id && '' !== $instance_id && '0' !== $instance_id ) { $context['shipping_instance_ids'][] = $method_id . ':' . $instance_id; } } $distinct_products = array(); foreach ( $order->get_items() as $item ) { $product = $item->get_product(); if ( ! $product ) { continue; } $product_id = (int) $product->get_id(); $parent_id = (int) $product->get_parent_id(); $line_ids = array(); foreach ( array( $product_id, $parent_id ) as $id ) { if ( $id ) { $context['product_ids'][] = $id; $line_ids[] = $id; } } if ( is_callable( array( $item, 'get_product_id' ) ) ) { $line_product_id = (int) $item->get_product_id(); if ( $line_product_id ) { $context['product_ids'][] = $line_product_id; $line_ids[] = $line_product_id; } } if ( is_callable( array( $item, 'get_variation_id' ) ) ) { $line_variation_id = (int) $item->get_variation_id(); if ( $line_variation_id ) { $context['product_ids'][] = $line_variation_id; $line_ids[] = $line_variation_id; } } $line_types = array( $product->get_type() ); if ( $parent_id ) { $parent_product = wc_get_product( $parent_id ); if ( $parent_product ) { $line_types[] = $parent_product->get_type(); } } $catalog_id = $parent_id ? $parent_id : $product_id; $context['virtual_flags'][] = (bool) $product->is_virtual(); $context['downloadable_flags'][] = (bool) $product->is_downloadable(); $context['line_product_ids'][] = array_values( array_unique( $line_ids ) ); $context['line_types'][] = $line_types; $context['line_cats'][] = wc_get_product_term_ids( $catalog_id, 'product_cat' ); $context['line_tags'][] = wc_get_product_term_ids( $catalog_id, 'product_tag' ); $context['line_brands'][] = wc_get_product_term_ids( $catalog_id, 'product_brand' ); $quantity = 1.0; if ( is_callable( array( $item, 'get_quantity' ) ) ) { $quantity = (float) $item->get_quantity(); } if ( $quantity < 0 ) { $quantity = 0; } $context['item_count'] += $quantity; $context['line_count']++; $context['backorder_flags'][] = hawo_line_is_backordered( $product, $item, $quantity ); if ( $catalog_id ) { $distinct_products[ $catalog_id ] = true; } if ( $with_attributes ) { $context['attribute_tokens'] = array_merge( $context['attribute_tokens'], hawo_collect_attribute_tokens( $product, $catalog_id ) ); } } $context['product_count'] = count( $distinct_products ); $context['attribute_tokens'] = array_values( array_unique( $context['attribute_tokens'] ) ); $context['order'] = $order; $context['trigger'] = 'thankyou'; return $context; } /** * Include/exclude list. Empty values do not restrict the order. * * @param mixed $setting Saved list. * @param array $haystack Values from the order. * @param bool $skip_if_haystack_empty Used by product rules in "any" mode. * @param bool $case_insensitive Compare as lowercase text. * @return bool */ function hawo_mode_list_allows( $setting, array $haystack, $skip_if_haystack_empty = false, $case_insensitive = false ) { if ( ! is_array( $setting ) ) { return true; } $values = hawo_list_values( $setting['value'] ?? array() ); if ( empty( $values ) ) { return true; } if ( $skip_if_haystack_empty && empty( $haystack ) ) { return true; } if ( $case_insensitive ) { $values = array_map( 'strtolower', array_map( 'strval', $values ) ); $haystack = array_map( 'strtolower', array_map( 'strval', $haystack ) ); } $matched = array_intersect( $haystack, $values ); $mode = ( isset( $setting['mode'] ) && 'exclude' === $setting['mode'] ) ? 'exclude' : 'include'; if ( 'include' === $mode && empty( $matched ) ) { return false; } if ( 'exclude' === $mode && ! empty( $matched ) ) { return false; } return true; } /** * Product, category, tag, brand, and type checks. * * Missing product_match means "any product", which is today's behavior. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_products_allow( $rule, $context ) { $match = hawo_sanitize_product_match( $rule['product_match'] ?? 'any' ); $map = array( 'products' => array( 'flat' => $context['product_ids'] ?? array(), 'lines' => $context['line_product_ids'] ?? array(), ), 'categories' => array( 'flat' => hawo_flatten_lines( $context['line_cats'] ?? array() ), 'lines' => $context['line_cats'] ?? array(), ), 'tags' => array( 'flat' => hawo_flatten_lines( $context['line_tags'] ?? array() ), 'lines' => $context['line_tags'] ?? array(), ), 'brands' => array( 'flat' => hawo_flatten_lines( $context['line_brands'] ?? array() ), 'lines' => $context['line_brands'] ?? array(), ), 'types' => array( 'flat' => hawo_flatten_lines( $context['line_types'] ?? array() ), 'lines' => $context['line_types'] ?? array(), ), ); foreach ( $map as $key => $data ) { if ( ! isset( $rule[ $key ] ) || ! is_array( $rule[ $key ] ) ) { continue; } $values = hawo_list_values( $rule[ $key ]['value'] ?? array() ); if ( empty( $values ) ) { continue; } if ( 'any' === $match ) { if ( ! hawo_mode_list_allows( $rule[ $key ], $data['flat'], true ) ) { return false; } continue; } $ok = hawo_lines_match( $data['lines'], $values, $match ); $mode = ( isset( $rule[ $key ]['mode'] ) && 'exclude' === $rule[ $key ]['mode'] ) ? 'exclude' : 'include'; if ( 'include' === $mode && ! $ok ) { return false; } if ( 'exclude' === $mode && $ok ) { return false; } } return true; } /** * Whether every line matches, and for "only" whether each chosen value is present. * * @param array $lines One list of values per order line. * @param array $values Chosen values. * @param string $match every|only. * @return bool */ function hawo_lines_match( array $lines, array $values, $match ) { if ( empty( $lines ) ) { return false; } $union = array(); foreach ( $lines as $line ) { $line = (array) $line; $union = array_merge( $union, $line ); if ( empty( array_intersect( $line, $values ) ) ) { return false; } } if ( 'only' !== $match ) { return true; } foreach ( $values as $value ) { if ( empty( array_intersect( $union, array( $value ) ) ) ) { return false; } } return true; } /** * Customers, emails, and guests. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_customer_allows( $rule, $context ) { $setting = $rule['customers'] ?? null; if ( ! is_array( $setting ) ) { return true; } $values = hawo_list_values( $setting['value'] ?? array() ); $guests = ! empty( $setting['guests'] ); if ( ! $guests && empty( $values ) ) { return true; } $is_match = false; if ( $guests && ! empty( $context['is_guest'] ) ) { $is_match = true; } $user_id = isset( $context['user_id'] ) ? (string) $context['user_id'] : ''; $email = strtolower( (string) ( $context['billing_email'] ?? '' ) ); foreach ( $values as $value ) { $value_text = strtolower( (string) $value ); if ( 'guest' === $value_text && ! empty( $context['is_guest'] ) ) { $is_match = true; } if ( '' !== $user_id && (string) $value === $user_id ) { $is_match = true; } if ( '' !== $email && $value_text === $email ) { $is_match = true; } } $mode = ( isset( $setting['mode'] ) && 'exclude' === $setting['mode'] ) ? 'exclude' : 'include'; if ( 'include' === $mode && ! $is_match ) { return false; } if ( 'exclude' === $mode && $is_match ) { return false; } return true; } /** * Billing or shipping country, state, and postcode. * * @param array $rule Rule. * @param array $context Order facts. * @param string $which billing|shipping. * @return bool */ function hawo_location_allows( $rule, $context, $which ) { $country = (string) ( $context[ $which . '_country' ] ?? '' ); $state = (string) ( $context[ $which . '_state' ] ?? '' ); $postcode = (string) ( $context[ $which . '_postcode' ] ?? '' ); if ( ! hawo_mode_list_allows( $rule[ $which . '_countries' ] ?? null, array( $country ), false, true ) ) { return false; } if ( ! hawo_mode_list_allows( $rule[ $which . '_states' ] ?? null, array( $state ), false, true ) ) { return false; } if ( ! hawo_mode_list_allows( $rule[ $which . '_postcodes' ] ?? null, array( $postcode ), false, true ) ) { return false; } return true; } /** * A non-primary rule with no limits would complete every remaining order. * Those rules are skipped until the store fills something in. * * @param array $rule Rule. * @return bool */ function hawo_rule_has_conditions( $rule ) { if ( ! is_array( $rule ) ) { return false; } $order_type = isset( $rule['order_type'] ) ? (string) $rule['order_type'] : ''; if ( '' !== $order_type && 'all_orders' !== $order_type ) { return true; } if ( hawo_order_total_has_bound( $rule['order_total'] ?? null ) ) { return true; } if ( ! empty( $rule['customers']['guests'] ) || ! empty( hawo_list_values( $rule['customers']['value'] ?? array() ) ) ) { return true; } $list_keys = array( 'shipping_methods', 'shipping_instances', 'payment_methods', 'coupons', 'products', 'categories', 'tags', 'brands', 'types', 'user_roles', 'order_status', 'billing_countries', 'billing_states', 'billing_postcodes', 'shipping_countries', 'shipping_states', 'shipping_postcodes', ); foreach ( $list_keys as $key ) { if ( ! empty( hawo_list_values( $rule[ $key ]['value'] ?? array() ) ) ) { return true; } } return hawo_phase3_has_conditions( $rule ) || hawo_phase5_has_conditions( $rule ); } /** * Extra rules built from product and category checkboxes. * * A checked product and a checked category are separate rules, so either one can match. * They are added after rule 1, so existing orders still follow rule 1 first. * * @return array */ function hawo_checked_item_rules() { if ( ! function_exists( 'get_posts' ) || ! function_exists( 'get_terms' ) ) { return array(); } $product_ids = get_posts( array( 'post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_key' => '_hawo_autocomplete', 'meta_value' => 'yes', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ) ); $term_ids = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'fields' => 'ids', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_hawo_autocomplete', 'value' => 'yes', ), ), ) ); if ( is_wp_error( $term_ids ) || ! is_array( $term_ids ) ) { $term_ids = array(); } $product_ids = array_map( 'absint', (array) $product_ids ); $term_ids = array_map( 'absint', (array) $term_ids ); $product_ids = array_values( array_filter( $product_ids ) ); $term_ids = array_values( array_filter( $term_ids ) ); $rules = array(); $base = array( 'name' => __( 'Checked products and categories', 'advanced-autocomplete-orders-for-woocommerce' ), 'enabled' => 1, 'is_primary' => 0, 'product_match' => 'any', ); if ( ! empty( $product_ids ) ) { $product_rule = $base; $product_rule['products'] = array( 'mode' => 'include', 'value' => $product_ids, ); $rules[] = $product_rule; } if ( ! empty( $term_ids ) ) { $category_rule = $base; $category_rule['categories'] = array( 'mode' => 'include', 'value' => $term_ids, ); $rules[] = $category_rule; } return $rules; } /** * @param mixed $value Posted match mode. * @return string */ function hawo_sanitize_product_match( $value ) { $value = is_string( $value ) ? $value : ''; if ( in_array( $value, array( 'any', 'every', 'only' ), true ) ) { return $value; } return 'any'; } /** * @param mixed $value Posted customers. * @return array */ function hawo_sanitize_customers( $value ) { $mode = ( is_array( $value ) && isset( $value['mode'] ) && 'exclude' === $value['mode'] ) ? 'exclude' : 'include'; $guests = ( is_array( $value ) && ! empty( $value['guests'] ) ) ? 1 : 0; $raw = is_array( $value ) ? ( $value['value'] ?? array() ) : array(); if ( is_string( $raw ) ) { $parts = preg_split( '/[\s,]+/', $raw ); } else { $parts = (array) $raw; } $items = array(); foreach ( $parts as $part ) { if ( is_array( $part ) ) { continue; } $part = sanitize_text_field( (string) $part ); if ( '' === $part ) { continue; } if ( 'guest' === strtolower( $part ) ) { $guests = 1; continue; } $items[] = $part; } return array( 'mode' => $mode, 'guests' => $guests, 'value' => array_values( array_unique( $items ) ), ); } /** * Comma-separated text saved as an include/exclude list. * * @param mixed $value Posted setting. * @return array */ function hawo_sanitize_text_list_setting( $value ) { if ( is_array( $value ) && isset( $value['value'] ) && is_string( $value['value'] ) ) { $value['value'] = preg_split( '/[\s,]+/', $value['value'] ); } return hawo_sanitize_mode_list( $value, 'text' ); } /** * @param mixed $rules Posted extra rules. * @return array */ function hawo_sanitize_extra_rules( $rules ) { if ( ! is_array( $rules ) ) { return array(); } $clean = array(); foreach ( $rules as $rule ) { if ( ! is_array( $rule ) ) { continue; } $clean_rule = hawo_sanitize_one_rule( $rule ); $named = isset( $clean_rule['name'] ) && '' !== $clean_rule['name']; if ( $named || hawo_rule_has_conditions( $clean_rule ) ) { $clean[] = $clean_rule; } } return $clean; } /** * @param array $rule Posted rule. * @return array */ function hawo_sanitize_one_rule( $rule ) { $clean = array( 'name' => sanitize_text_field( (string) ( $rule['name'] ?? '' ) ), 'enabled' => empty( $rule['enabled'] ) ? 0 : 1, 'product_match' => hawo_sanitize_product_match( $rule['product_match'] ?? 'any' ), 'is_primary' => 0, ); $order_type = is_string( $rule['order_type'] ?? null ) ? $rule['order_type'] : ''; $allowed = array( 'all_orders', 'only_virtual_all', 'has_virtual', 'has_downloadable', 'only_downloadable_all', 'mixed_cart' ); if ( in_array( $order_type, $allowed, true ) ) { $clean['order_type'] = $order_type; } if ( isset( $rule['order_total'] ) ) { $clean['order_total'] = hawo_sanitize_order_total( $rule['order_total'] ); } if ( isset( $rule['customers'] ) ) { $clean['customers'] = hawo_sanitize_customers( $rule['customers'] ); } foreach ( array( 'shipping_methods', 'shipping_instances', 'payment_methods', 'coupons', 'billing_countries', 'shipping_countries', 'types', 'user_roles', 'order_status' ) as $key ) { if ( isset( $rule[ $key ] ) ) { $clean[ $key ] = hawo_sanitize_mode_list( $rule[ $key ], 'text' ); } } foreach ( array( 'billing_states', 'billing_postcodes', 'shipping_states', 'shipping_postcodes' ) as $key ) { if ( isset( $rule[ $key ] ) ) { $clean[ $key ] = hawo_sanitize_text_list_setting( $rule[ $key ] ); } } foreach ( array( 'products', 'categories', 'tags', 'brands' ) as $key ) { if ( isset( $rule[ $key ] ) ) { $posted = $rule[ $key ]; if ( is_array( $posted ) && isset( $posted['value'] ) && is_string( $posted['value'] ) ) { $posted['value'] = preg_split( '/[\s,]+/', $posted['value'] ); } $clean[ $key ] = hawo_sanitize_mode_list( $posted, 'id' ); } } return array_merge( $clean, hawo_sanitize_phase3_input( $rule ), hawo_sanitize_phase4_input( $rule ), hawo_sanitize_phase5_input( $rule ) ); } /** * @param mixed $values List values. * @return array */ function hawo_list_values( $values ) { $clean = array(); foreach ( (array) $values as $value ) { if ( is_array( $value ) || null === $value || '' === $value ) { continue; } $clean[] = $value; } return $clean; } /** * @param array $lines Lists of line values. * @return array */ function hawo_flatten_lines( array $lines ) { $flat = array(); foreach ( $lines as $line ) { foreach ( (array) $line as $value ) { $flat[] = $value; } } return $flat; } /** * Zone shipping methods, such as flat_rate:12. * * @return array */ function hawo_shipping_instance_choices() { if ( ! class_exists( 'WC_Shipping_Zones' ) ) { return array(); } $zones = WC_Shipping_Zones::get_zones(); $zone0 = new WC_Shipping_Zone( 0 ); $zones[] = array( 'zone_name' => $zone0->get_zone_name(), 'shipping_methods' => $zone0->get_shipping_methods(), ); $choices = array(); foreach ( $zones as $zone ) { foreach ( $zone['shipping_methods'] ?? array() as $method ) { if ( ! is_object( $method ) || ! method_exists( $method, 'get_rate_id' ) ) { continue; } $choices[ $method->get_rate_id() ] = $zone['zone_name'] . ' — ' . $method->get_title(); } } return $choices; } /** * Whether any rule asks for size, color, or another product group. * * @param array $settings Saved settings. * @return bool */ function hawo_rules_use_attributes( $settings ) { $rules = array( $settings ); if ( ! empty( $settings['extra_rules'] ) && is_array( $settings['extra_rules'] ) ) { $rules = array_merge( $rules, $settings['extra_rules'] ); } foreach ( $rules as $rule ) { if ( is_array( $rule ) && ! empty( hawo_list_values( $rule['attributes']['value'] ?? array() ) ) ) { return true; } } return false; } /** * The single order total, unless this currency has its own amount. * * A currency row replaces the single amount only for that currency. * Other currencies keep the amount from Phase 1. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_totals_allow( $rule, $context ) { $currency_total = hawo_currency_total_setting( $rule, $context['currency'] ?? '' ); if ( is_array( $currency_total ) ) { return hawo_order_total_allows( $currency_total, $context['total'] ?? 0 ); } if ( isset( $rule['order_total'] ) && ! hawo_order_total_allows( $rule['order_total'], $context['total'] ?? 0 ) ) { return false; } return true; } /** * Include range for one currency, when that row has an amount. * * @param array $rule Rule. * @param string $currency Order currency. * @return array|null */ function hawo_currency_total_setting( $rule, $currency ) { $currency = strtoupper( (string) $currency ); if ( '' === $currency || empty( $rule['currency_totals'] ) || ! is_array( $rule['currency_totals'] ) ) { return null; } foreach ( $rule['currency_totals'] as $row ) { if ( ! is_array( $row ) || strtoupper( (string) ( $row['currency'] ?? '' ) ) !== $currency ) { continue; } $setting = array( 'mode' => 'include', 'value' => array( 'min' => $row['min'] ?? '', 'max' => $row['max'] ?? '', ), ); if ( hawo_order_total_has_bound( $setting ) ) { return $setting; } } return null; } /** * Item quantity, line count, and number of different products. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_quantities_allow( $rule, $context ) { $checks = array( 'item_count' => $context['item_count'] ?? 0, 'line_count' => $context['line_count'] ?? 0, 'product_count' => $context['product_count'] ?? 0, ); foreach ( $checks as $key => $number ) { if ( ! hawo_range_allows( $rule[ $key ] ?? null, $number ) ) { return false; } } return true; } /** * Empty or zero bounds do not restrict the order. * * @param mixed $setting Min and max. * @param mixed $number Number from the order. * @return bool */ function hawo_range_allows( $setting, $number ) { if ( ! is_array( $setting ) ) { return true; } $min = hawo_total_bound_number( $setting['min'] ?? '' ); $max = hawo_total_bound_number( $setting['max'] ?? '' ); if ( $min <= 0 && $max <= 0 ) { return true; } $number = (float) $number; if ( $min > 0 && $number < $min ) { return false; } if ( $max > 0 && $number > $max ) { return false; } return true; } /** * Backorder choice. Empty means the store did not ask. * * @param array $rule Rule. * @param array $flags One true/false per line. * @return bool */ function hawo_backorder_allows( $rule, $flags ) { $mode = isset( $rule['backorder'] ) ? (string) $rule['backorder'] : ''; if ( ! in_array( $mode, array( 'any', 'all', 'none' ), true ) ) { return true; } $flags = array_map( 'boolval', (array) $flags ); $has = in_array( true, $flags, true ); $all = ! empty( $flags ) && ! in_array( false, $flags, true ); if ( 'any' === $mode ) { return $has; } if ( 'all' === $mode ) { return $all; } return ! $has; } /** * First-time buyers, returning buyers, past orders, and money spent. * * Past orders do not include the order being completed. * The lookup runs only when the rule asks for it. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_history_allows( $rule, $context ) { $buyer = isset( $rule['buyer_type'] ) ? (string) $rule['buyer_type'] : ''; $needs_past = hawo_range_has_bound( $rule['past_orders'] ?? null ); $needs_spent = hawo_range_has_bound( $rule['amount_spent'] ?? null ); if ( ! in_array( $buyer, array( 'first', 'returning' ), true ) && ! $needs_past && ! $needs_spent ) { return true; } $history = hawo_customer_history( $context ); if ( 'first' === $buyer && $history['past_orders'] > 0 ) { return false; } if ( 'returning' === $buyer && $history['past_orders'] < 1 ) { return false; } if ( ! hawo_range_allows( $rule['past_orders'] ?? null, $history['past_orders'] ) ) { return false; } if ( ! hawo_range_allows( $rule['amount_spent'] ?? null, $history['amount_spent'] ) ) { return false; } return true; } /** * Whether a min or max is set above zero. * * @param mixed $setting Range. * @return bool */ function hawo_range_has_bound( $setting ) { if ( ! is_array( $setting ) ) { return false; } return hawo_total_bound_number( $setting['min'] ?? '' ) > 0 || hawo_total_bound_number( $setting['max'] ?? '' ) > 0; } /** * Orders this customer already placed, and the money on those orders. * * @param array $context Order facts. * @return array */ function hawo_customer_history( $context ) { static $cache = array(); $key = (int) ( $context['order_id'] ?? 0 ) . '|' . (int) ( $context['user_id'] ?? 0 ) . '|' . strtolower( (string) ( $context['billing_email'] ?? '' ) ); if ( isset( $cache[ $key ] ) ) { return $cache[ $key ]; } $history = array( 'past_orders' => 0, 'amount_spent' => 0.0, ); if ( ! function_exists( 'wc_get_orders' ) ) { $cache[ $key ] = $history; return $history; } $args = array( 'limit' => -1, 'return' => 'ids', 'status' => array( 'processing', 'completed', 'on-hold' ), 'exclude' => array( (int) ( $context['order_id'] ?? 0 ) ), ); if ( ! empty( $context['user_id'] ) ) { $args['customer_id'] = (int) $context['user_id']; } elseif ( ! empty( $context['billing_email'] ) ) { $args['billing_email'] = (string) $context['billing_email']; } else { $cache[ $key ] = $history; return $history; } $ids = wc_get_orders( $args ); if ( ! is_array( $ids ) ) { $ids = array(); } $history['past_orders'] = count( $ids ); foreach ( $ids as $order_id ) { if ( ! function_exists( 'wc_get_order' ) ) { continue; } $past = wc_get_order( $order_id ); if ( is_object( $past ) && is_callable( array( $past, 'get_total' ) ) ) { $history['amount_spent'] += (float) $past->get_total(); } } $cache[ $key ] = $history; return $history; } /** * Start date, end date, days of the week, and hours. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_schedule_allows( $rule, $context ) { $dates = is_array( $rule['order_dates'] ?? null ) ? $rule['order_dates'] : array(); $days = hawo_sanitize_days( $rule['days_of_week'] ?? array() ); $hours = is_array( $rule['hours'] ?? null ) ? $rule['hours'] : array(); $start = isset( $dates['start'] ) ? (string) $dates['start'] : ''; $end = isset( $dates['end'] ) ? (string) $dates['end'] : ''; $hour_start = $hours['start'] ?? ''; $hour_end = $hours['end'] ?? ''; $needs_time = ( '' !== $start || '' !== $end || ! empty( $days ) || hawo_hour_is_set( $hour_start ) || hawo_hour_is_set( $hour_end ) ); if ( ! $needs_time ) { return true; } $timestamp = $context['created_timestamp'] ?? null; if ( ! is_numeric( $timestamp ) ) { return false; } $timezone = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ); $created = ( new DateTimeImmutable( '@' . (int) $timestamp ) )->setTimezone( $timezone ); if ( '' !== $start ) { $start_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $start . ' 00:00:00', $timezone ); if ( $start_at instanceof DateTimeImmutable && $created < $start_at ) { return false; } } if ( '' !== $end ) { $end_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $end . ' 23:59:59', $timezone ); if ( $end_at instanceof DateTimeImmutable && $created > $end_at ) { return false; } } if ( ! empty( $days ) && ! in_array( (string) $created->format( 'w' ), $days, true ) ) { return false; } if ( hawo_hour_is_set( $hour_start ) || hawo_hour_is_set( $hour_end ) ) { $hour = (int) $created->format( 'G' ); $from = hawo_hour_is_set( $hour_start ) ? (int) $hour_start : 0; $to = hawo_hour_is_set( $hour_end ) ? (int) $hour_end : 23; $inside = ( $from <= $to ) ? ( $hour >= $from && $hour <= $to ) : ( $hour >= $from || $hour <= $to ); if ( ! $inside ) { return false; } } return true; } /** * @param mixed $hour Hour value. * @return bool */ function hawo_hour_is_set( $hour ) { return is_scalar( $hour ) && '' !== (string) $hour && is_numeric( $hour ); } /** * Whether a line was sold on backorder. * * @param object $product Product. * @param object $item Order line. * @param float $quantity Quantity. * @return bool */ function hawo_line_is_backordered( $product, $item, $quantity ) { $quantity = max( 1, (int) round( (float) $quantity ) ); if ( is_callable( array( $product, 'is_on_backorder' ) ) && $product->is_on_backorder( $quantity ) ) { return true; } if ( is_callable( array( $product, 'get_stock_status' ) ) && 'onbackorder' === $product->get_stock_status() ) { return true; } if ( ! is_callable( array( $item, 'get_meta_data' ) ) ) { return false; } foreach ( (array) $item->get_meta_data() as $meta ) { if ( ! is_object( $meta ) ) { continue; } $key = strtolower( (string) ( $meta->key ?? '' ) ); $val = (string) ( $meta->value ?? '' ); if ( false !== strpos( $key, 'backorder' ) && '' !== $val && '0' !== $val ) { return true; } } return false; } /** * Size, color, and other groups on one line, as taxonomy:value. * * @param object $product Product. * @param int $catalog_id Parent or simple product ID. * @return array */ function hawo_collect_attribute_tokens( $product, $catalog_id ) { $tokens = array(); if ( is_callable( array( $product, 'get_variation_attributes' ) ) ) { foreach ( (array) $product->get_variation_attributes() as $key => $value ) { $token = hawo_attribute_token( $key, $value ); if ( '' !== $token ) { $tokens[] = $token; } } } if ( is_callable( array( $product, 'get_attributes' ) ) ) { foreach ( (array) $product->get_attributes() as $attribute ) { if ( ! is_object( $attribute ) || ! is_callable( array( $attribute, 'get_name' ) ) ) { continue; } $name = (string) $attribute->get_name(); $options = is_callable( array( $attribute, 'get_options' ) ) ? (array) $attribute->get_options() : array(); $is_tax = is_callable( array( $attribute, 'is_taxonomy' ) ) && $attribute->is_taxonomy(); foreach ( $options as $option ) { if ( $is_tax && is_numeric( $option ) && function_exists( 'get_term' ) ) { $term = get_term( (int) $option ); if ( is_object( $term ) && ! is_wp_error( $term ) && isset( $term->slug ) ) { $option = $term->slug; } } $token = hawo_attribute_token( $name, $option ); if ( '' !== $token ) { $tokens[] = $token; } } } } if ( $catalog_id && function_exists( 'get_object_taxonomies' ) && function_exists( 'get_the_terms' ) ) { $skip = array( 'product_cat', 'product_tag', 'product_brand', 'product_type', 'product_shipping_class', 'product_visibility' ); foreach ( (array) get_object_taxonomies( 'product' ) as $taxonomy ) { if ( in_array( $taxonomy, $skip, true ) ) { continue; } $terms = get_the_terms( $catalog_id, $taxonomy ); if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { continue; } foreach ( $terms as $term ) { if ( is_object( $term ) && isset( $term->slug ) ) { $token = hawo_attribute_token( $taxonomy, $term->slug ); if ( '' !== $token ) { $tokens[] = $token; } } } } } return array_values( array_unique( $tokens ) ); } /** * @param mixed $taxonomy Attribute or group name. * @param mixed $value Slug or text. * @return string */ function hawo_attribute_token( $taxonomy, $value ) { $taxonomy = strtolower( (string) $taxonomy ); if ( 0 === strpos( $taxonomy, 'attribute_' ) ) { $taxonomy = substr( $taxonomy, 10 ); } $value = strtolower( trim( (string) $value ) ); $value = str_replace( ' ', '-', $value ); $taxonomy = preg_replace( '/[^a-z0-9_-]/', '', $taxonomy ); $value = preg_replace( '/[^a-z0-9_-]/', '', $value ); if ( '' === $taxonomy || '' === $value ) { return ''; } return $taxonomy . ':' . $value; } /** * Choices for size, color, and other product groups. * * @return array */ function hawo_attribute_choices() { if ( ! function_exists( 'get_terms' ) ) { return array(); } $taxonomies = array(); if ( function_exists( 'wc_get_attribute_taxonomies' ) ) { foreach ( (array) wc_get_attribute_taxonomies() as $attribute ) { $name = is_object( $attribute ) ? (string) ( $attribute->attribute_name ?? '' ) : ''; if ( '' === $name ) { continue; } $taxonomies[ 'pa_' . $name ] = (string) ( $attribute->attribute_label ?? $name ); } } if ( function_exists( 'get_object_taxonomies' ) ) { $skip = array( 'product_cat', 'product_tag', 'product_brand', 'product_type', 'product_shipping_class', 'product_visibility' ); foreach ( (array) get_object_taxonomies( 'product', 'objects' ) as $taxonomy ) { if ( ! is_object( $taxonomy ) || in_array( $taxonomy->name, $skip, true ) || isset( $taxonomies[ $taxonomy->name ] ) ) { continue; } if ( empty( $taxonomy->public ) && 0 !== strpos( (string) $taxonomy->name, 'pa_' ) ) { continue; } $label = $taxonomy->labels->singular_name ?? $taxonomy->label ?? $taxonomy->name; $taxonomies[ $taxonomy->name ] = (string) $label; } } $choices = array(); foreach ( $taxonomies as $taxonomy => $label ) { $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, ) ); if ( is_wp_error( $terms ) || ! is_array( $terms ) ) { continue; } foreach ( $terms as $term ) { if ( ! is_object( $term ) || empty( $term->slug ) ) { continue; } $token = hawo_attribute_token( $taxonomy, $term->slug ); $choices[ $token ] = $label . ': ' . $term->name; } } return $choices; } /** * Phase 3 filters that are filled in. * * @param array $rule Rule. * @return bool */ function hawo_phase3_has_conditions( $rule ) { if ( ! is_array( $rule ) ) { return false; } if ( ! empty( hawo_list_values( $rule['attributes']['value'] ?? array() ) ) ) { return true; } foreach ( array( 'item_count', 'line_count', 'product_count', 'past_orders', 'amount_spent' ) as $key ) { if ( hawo_range_has_bound( $rule[ $key ] ?? null ) ) { return true; } } if ( in_array( $rule['backorder'] ?? '', array( 'any', 'all', 'none' ), true ) ) { return true; } if ( in_array( $rule['buyer_type'] ?? '', array( 'first', 'returning' ), true ) ) { return true; } $dates = is_array( $rule['order_dates'] ?? null ) ? $rule['order_dates'] : array(); if ( ! empty( $dates['start'] ) || ! empty( $dates['end'] ) ) { return true; } if ( ! empty( hawo_sanitize_days( $rule['days_of_week'] ?? array() ) ) ) { return true; } $hours = is_array( $rule['hours'] ?? null ) ? $rule['hours'] : array(); if ( hawo_hour_is_set( $hours['start'] ?? '' ) || hawo_hour_is_set( $hours['end'] ?? '' ) ) { return true; } return hawo_currency_totals_have_bound( $rule ); } /** * @param array $rule Rule. * @return bool */ function hawo_currency_totals_have_bound( $rule ) { foreach ( (array) ( $rule['currency_totals'] ?? array() ) as $row ) { if ( ! is_array( $row ) || '' === (string) ( $row['currency'] ?? '' ) ) { continue; } $setting = array( 'mode' => 'include', 'value' => array( 'min' => $row['min'] ?? '', 'max' => $row['max'] ?? '', ), ); if ( hawo_order_total_has_bound( $setting ) ) { return true; } } return false; } /** * Clean Phase 3 fields that were posted. Missing fields are left alone. * * @param array $input Posted rule or settings. * @return array */ function hawo_sanitize_phase3_input( $input ) { if ( ! is_array( $input ) ) { return array(); } $clean = array(); if ( array_key_exists( 'attributes', $input ) ) { $clean['attributes'] = hawo_sanitize_attributes( $input['attributes'] ); } foreach ( array( 'item_count', 'line_count', 'product_count', 'past_orders', 'amount_spent' ) as $key ) { if ( array_key_exists( $key, $input ) ) { $clean[ $key ] = hawo_sanitize_count_range( $input[ $key ] ); } } if ( array_key_exists( 'backorder', $input ) ) { $clean['backorder'] = hawo_sanitize_backorder( $input['backorder'] ); } if ( array_key_exists( 'buyer_type', $input ) ) { $clean['buyer_type'] = hawo_sanitize_buyer_type( $input['buyer_type'] ); } if ( array_key_exists( 'order_dates', $input ) ) { $clean['order_dates'] = hawo_sanitize_order_dates( $input['order_dates'] ); } if ( ! empty( $input['days_of_week_form'] ) || array_key_exists( 'days_of_week', $input ) ) { $clean['days_of_week'] = hawo_sanitize_days( $input['days_of_week'] ?? array() ); } if ( array_key_exists( 'hours', $input ) ) { $clean['hours'] = hawo_sanitize_hours( $input['hours'] ); } if ( array_key_exists( 'currency_totals', $input ) || ! empty( $input['currency_totals_form'] ) ) { $clean['currency_totals'] = hawo_sanitize_currency_totals( $input['currency_totals'] ?? array() ); } return $clean; } /** * @param mixed $value Posted attributes. * @return array */ function hawo_sanitize_attributes( $value ) { $list = hawo_sanitize_text_list_setting( $value ); $items = array(); foreach ( $list['value'] as $item ) { $parts = explode( ':', strtolower( (string) $item ), 2 ); if ( count( $parts ) < 2 ) { continue; } $token = hawo_attribute_token( $parts[0], $parts[1] ); if ( '' !== $token ) { $items[] = $token; } } $list['value'] = array_values( array_unique( $items ) ); return $list; } /** * @param mixed $value Posted min and max. * @return array */ function hawo_sanitize_count_range( $value ) { $value = is_array( $value ) ? $value : array(); return array( 'min' => hawo_sanitize_amount( $value['min'] ?? '' ), 'max' => hawo_sanitize_amount( $value['max'] ?? '' ), ); } /** * @param mixed $value Posted backorder choice. * @return string */ function hawo_sanitize_backorder( $value ) { $value = is_string( $value ) ? $value : ''; return in_array( $value, array( 'any', 'all', 'none' ), true ) ? $value : ''; } /** * @param mixed $value Posted buyer type. * @return string */ function hawo_sanitize_buyer_type( $value ) { $value = is_string( $value ) ? $value : ''; return in_array( $value, array( 'first', 'returning' ), true ) ? $value : ''; } /** * @param mixed $value Posted dates. * @return array */ function hawo_sanitize_order_dates( $value ) { $value = is_array( $value ) ? $value : array(); return array( 'start' => hawo_sanitize_date( $value['start'] ?? '' ), 'end' => hawo_sanitize_date( $value['end'] ?? '' ), ); } /** * @param mixed $value Posted date. * @return string */ function hawo_sanitize_date( $value ) { $value = is_string( $value ) ? trim( $value ) : ''; $date = DateTimeImmutable::createFromFormat( '!Y-m-d', $value ); if ( ! $date || $date->format( 'Y-m-d' ) !== $value ) { return ''; } return $value; } /** * @param mixed $value Posted weekdays. 0 is Sunday. * @return array */ function hawo_sanitize_days( $value ) { if ( is_array( $value ) && isset( $value['value'] ) ) { $value = $value['value']; } if ( is_string( $value ) ) { $value = preg_split( '/[\s,]+/', $value ); } $days = array(); foreach ( (array) $value as $day ) { if ( is_array( $day ) || ! is_numeric( $day ) ) { continue; } $day = (int) $day; if ( $day >= 0 && $day <= 6 ) { $days[] = (string) $day; } } return array_values( array_unique( $days ) ); } /** * @param mixed $value Posted hours. * @return array */ function hawo_sanitize_hours( $value ) { $value = is_array( $value ) ? $value : array(); return array( 'start' => hawo_sanitize_hour( $value['start'] ?? '' ), 'end' => hawo_sanitize_hour( $value['end'] ?? '' ), ); } /** * @param mixed $value Posted hour. * @return string */ function hawo_sanitize_hour( $value ) { if ( ! is_scalar( $value ) || '' === (string) $value || ! is_numeric( $value ) ) { return ''; } $hour = (int) $value; if ( $hour < 0 || $hour > 23 ) { return ''; } return (string) $hour; } /** * @param mixed $value Posted currency totals. * @return array */ function hawo_sanitize_currency_totals( $value ) { if ( ! is_array( $value ) ) { return array(); } $clean = array(); foreach ( $value as $row ) { if ( ! is_array( $row ) ) { continue; } $currency = strtoupper( sanitize_text_field( (string) ( $row['currency'] ?? '' ) ) ); if ( ! preg_match( '/^[A-Z]{3}$/', $currency ) ) { continue; } $min = hawo_sanitize_amount( $row['min'] ?? '' ); $max = hawo_sanitize_amount( $row['max'] ?? '' ); if ( '' === $min && '' === $max ) { continue; } $clean[] = array( 'currency' => $currency, 'min' => $min, 'max' => $max, ); } return $clean; } /** * Note phrases for filters the store actually filled in. * * @param array $settings Rule or settings. * @return array */ function hawo_phase3_note_reasons( $settings ) { if ( ! is_array( $settings ) ) { return array(); } $reasons = array(); if ( hawo_setting_has_values( $settings, 'attributes' ) ) { $reasons[] = __( 'Product detail matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } foreach ( array( 'item_count' => __( 'Item quantity matched.', 'advanced-autocomplete-orders-for-woocommerce' ), 'line_count' => __( 'Number of lines matched.', 'advanced-autocomplete-orders-for-woocommerce' ), 'product_count' => __( 'Number of products matched.', 'advanced-autocomplete-orders-for-woocommerce' ), 'past_orders' => __( 'Past orders matched.', 'advanced-autocomplete-orders-for-woocommerce' ), 'amount_spent' => __( 'Money spent matched.', 'advanced-autocomplete-orders-for-woocommerce' ), ) as $key => $label ) { if ( hawo_range_has_bound( $settings[ $key ] ?? null ) ) { $reasons[] = $label; } } if ( in_array( $settings['backorder'] ?? '', array( 'any', 'all', 'none' ), true ) ) { $reasons[] = __( 'Backorder matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } if ( 'first' === ( $settings['buyer_type'] ?? '' ) ) { $reasons[] = __( 'First-time buyer.', 'advanced-autocomplete-orders-for-woocommerce' ); } elseif ( 'returning' === ( $settings['buyer_type'] ?? '' ) ) { $reasons[] = __( 'Returning buyer.', 'advanced-autocomplete-orders-for-woocommerce' ); } $dates = is_array( $settings['order_dates'] ?? null ) ? $settings['order_dates'] : array(); if ( ! empty( $dates['start'] ) || ! empty( $dates['end'] ) ) { $reasons[] = __( 'Order date matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } if ( ! empty( hawo_sanitize_days( $settings['days_of_week'] ?? array() ) ) ) { $reasons[] = __( 'Day of the week matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } $hours = is_array( $settings['hours'] ?? null ) ? $settings['hours'] : array(); if ( hawo_hour_is_set( $hours['start'] ?? '' ) || hawo_hour_is_set( $hours['end'] ?? '' ) ) { $reasons[] = __( 'Time of day matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } if ( hawo_currency_totals_have_bound( $settings ) ) { $reasons[] = __( 'Currency total matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } return $reasons; } /** * How long to wait before changing the status. Zero means change it now. * * @param array $rule Rule. * @return int */ function hawo_delay_seconds( $rule ) { $amount = hawo_total_bound_number( $rule['complete_after_amount'] ?? '' ); if ( $amount <= 0 ) { return 0; } $minute = defined( 'MINUTE_IN_SECONDS' ) ? MINUTE_IN_SECONDS : 60; $hour = defined( 'HOUR_IN_SECONDS' ) ? HOUR_IN_SECONDS : 3600; $day = defined( 'DAY_IN_SECONDS' ) ? DAY_IN_SECONDS : 86400; $unit = isset( $rule['complete_after_unit'] ) ? (string) $rule['complete_after_unit'] : 'minutes'; if ( 'days' === $unit ) { return (int) round( $amount * $day ); } if ( 'hours' === $unit ) { return (int) round( $amount * $hour ); } return (int) round( $amount * $minute ); } /** * Status to set. Empty means Completed. * * @param array $rule Rule. * @return string */ function hawo_target_status( $rule ) { $status = isset( $rule['complete_status'] ) ? strtolower( (string) $rule['complete_status'] ) : ''; $status = preg_replace( '/^wc-/', '', $status ); $status = preg_replace( '/[^a-z0-9_-]/', '', (string) $status ); if ( '' === $status || 'completed' === $status ) { return 'completed'; } if ( function_exists( 'wc_get_order_statuses' ) ) { $allowed = array(); foreach ( array_keys( wc_get_order_statuses() ) as $key ) { $allowed[] = preg_replace( '/^wc-/', '', (string) $key ); } if ( ! in_array( $status, $allowed, true ) ) { return 'completed'; } } return $status; } /** * Email choice. Empty means the normal completed email. * * @param array $rule Rule. * @return string */ function hawo_email_mode( $rule ) { $mode = isset( $rule['email_mode'] ) ? (string) $rule['email_mode'] : ''; return in_array( $mode, array( 'skip', 'manager', 'custom' ), true ) ? $mode : ''; } /** * Name used in the test tool and the log. * * @param array $rule Rule. * @param int $index Position in line. * @return string */ function hawo_rule_label( $rule, $index ) { if ( ! empty( $rule['is_primary'] ) ) { return __( 'Rule 1', 'advanced-autocomplete-orders-for-woocommerce' ); } $name = isset( $rule['name'] ) ? trim( (string) $rule['name'] ) : ''; if ( '' === $name ) { return sprintf( /* translators: %d: rule number */ __( 'Rule %d', 'advanced-autocomplete-orders-for-woocommerce' ), (int) $index + 1 ); } return $name; } /** * Which rule matched, and which checks failed. * * @param array $settings Saved settings. * @param WC_Order $order Order. * @return array */ function hawo_explain_order( $settings, $order, $trigger = 'thankyou' ) { $lines = array(); $matched = null; if ( ! is_array( $settings ) || ! is_object( $order ) ) { $lines[] = __( 'The order could not be read.', 'advanced-autocomplete-orders-for-woocommerce' ); } else { $context = hawo_build_order_context( $order, hawo_rules_use_attributes( $settings ) ); $context['order'] = $order; $context['trigger'] = hawo_normalize_trigger( $trigger ); foreach ( hawo_rules_to_try( $settings ) as $index => $rule ) { $name = hawo_rule_label( $rule, (int) $index ); $failures = hawo_rule_failures( $rule, $context ); if ( empty( $failures ) ) { $lines[] = sprintf( /* translators: %s: rule name */ __( '%s matched.', 'advanced-autocomplete-orders-for-woocommerce' ), $name ); $matched = $rule; break; } $lines[] = sprintf( /* translators: 1: rule name, 2: failed checks */ __( '%1$s did not match. %2$s', 'advanced-autocomplete-orders-for-woocommerce' ), $name, implode( ' ', $failures ) ); } } if ( ! $matched ) { $lines[] = __( 'No rule matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } return array( 'matched' => $matched, 'lines' => $lines, 'summary' => implode( ' ', $lines ), ); } /** * Change the order, or wait, using the rule that matched. * * No wait, Completed, and the normal email stay in place when those fields are empty. * * @param WC_Order $order Order. * @param array $rule Matching rule. * @return void */ function hawo_finish_matched_order( $order, $rule ) { if ( ! is_object( $order ) || ! is_array( $rule ) || ! is_callable( array( $order, 'update_status' ) ) ) { return; } $order_id = is_callable( array( $order, 'get_id' ) ) ? (int) $order->get_id() : 0; $delay = hawo_delay_seconds( $rule ); if ( $delay > 0 ) { $already = is_callable( array( $order, 'get_meta' ) ) ? (string) $order->get_meta( '_hawo_delay_scheduled' ) : ''; if ( '' !== $already ) { return; } hawo_schedule_completion( $order_id, $delay ); if ( is_callable( array( $order, 'update_meta_data' ) ) && is_callable( array( $order, 'save' ) ) ) { $order->update_meta_data( '_hawo_delay_scheduled', (string) ( time() + $delay ) ); $order->save(); } hawo_add_log( $order_id, 'waiting', sprintf( /* translators: %s: wait, such as 2 hours */ __( 'Waiting %s before completing.', 'advanced-autocomplete-orders-for-woocommerce' ), hawo_delay_label( $rule ) ) ); return; } hawo_apply_status_now( $order, $rule ); } /** * Set the status now. Used for no-wait rules and when a wait ends. * * @param WC_Order $order Order. * @param array $rule Matching rule. * @return void */ function hawo_apply_status_now( $order, $rule ) { $status = hawo_target_status( $rule ); if ( is_callable( array( $order, 'has_status' ) ) && $order->has_status( $status ) ) { return; } $order_id = is_callable( array( $order, 'get_id' ) ) ? (int) $order->get_id() : 0; if ( 'skip' === hawo_email_mode( $rule ) && is_callable( array( $order, 'update_meta_data' ) ) && is_callable( array( $order, 'save' ) ) ) { $order->update_meta_data( '_hawo_skip_completed_email', 'yes' ); $order->save(); } $order->update_status( $status, hawo_status_note( $rule ) ); if ( 'skip' === hawo_email_mode( $rule ) && is_callable( array( $order, 'delete_meta_data' ) ) && is_callable( array( $order, 'save' ) ) ) { $order->delete_meta_data( '_hawo_skip_completed_email' ); $order->save(); } if ( in_array( hawo_email_mode( $rule ), array( 'manager', 'custom' ), true ) ) { hawo_send_extra_email( $order, $rule ); } if ( is_callable( array( $order, 'get_meta' ) ) && '' !== (string) $order->get_meta( '_hawo_delay_scheduled' ) && is_callable( array( $order, 'delete_meta_data' ) ) && is_callable( array( $order, 'save' ) ) ) { $order->delete_meta_data( '_hawo_delay_scheduled' ); $order->save(); } hawo_add_log( $order_id, 'completed', hawo_status_note( $rule ) ); hawo_notify_webhook( $order, $rule ); } /** * Run a delayed completion. The wait is not applied again. * * @param mixed $order_id Order ID, or an args array from Action Scheduler. * @return void */ function hawo_run_delayed_completion( $order_id ) { if ( is_array( $order_id ) && isset( $order_id['order_id'] ) ) { $order_id = $order_id['order_id']; } $order_id = (int) $order_id; if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) { return; } $order = wc_get_order( $order_id ); if ( ! is_object( $order ) ) { return; } $settings = get_option( 'hawo_settings', array() ); if ( empty( $settings['enable'] ) ) { hawo_add_log( $order_id, 'skipped', __( 'Autocomplete is turned off.', 'advanced-autocomplete-orders-for-woocommerce' ) ); return; } $matched = hawo_first_matching_rule( $settings, $order ); if ( ! is_array( $matched ) ) { hawo_add_log( $order_id, 'skipped', hawo_explain_order( $settings, $order )['summary'] ); return; } $matched['complete_after_amount'] = ''; hawo_apply_status_now( $order, $matched ); } /** * Schedule one completion. Does nothing when WordPress has no scheduler. * * @param int $order_id Order ID. * @param int $delay Seconds. * @return void */ function hawo_schedule_completion( $order_id, $delay ) { $order_id = (int) $order_id; $timestamp = time() + (int) $delay; if ( $order_id <= 0 || $delay <= 0 ) { return; } if ( function_exists( 'as_schedule_single_action' ) ) { $args = array( 'order_id' => $order_id ); if ( function_exists( 'as_next_scheduled_action' ) && as_next_scheduled_action( 'hawo_complete_delayed_order', $args, 'hawo' ) ) { return; } as_schedule_single_action( $timestamp, 'hawo_complete_delayed_order', $args, 'hawo' ); return; } if ( function_exists( 'wp_next_scheduled' ) && wp_next_scheduled( 'hawo_complete_delayed_order', array( $order_id ) ) ) { return; } if ( function_exists( 'wp_schedule_single_event' ) ) { wp_schedule_single_event( $timestamp, 'hawo_complete_delayed_order', array( $order_id ) ); } } /** * Hide the completed email for one status change. * * @param bool $enabled Whether WooCommerce will send the email. * @param WC_Order $order Order. * @return bool */ function hawo_filter_completed_email( $enabled, $order ) { if ( ! $enabled || ! is_object( $order ) || ! is_callable( array( $order, 'get_meta' ) ) ) { return $enabled; } if ( 'yes' === (string) $order->get_meta( '_hawo_skip_completed_email' ) ) { return false; } return $enabled; } /** * Shop manager or custom email, in addition to the normal completed email. * * @param WC_Order $order Order. * @param array $rule Rule. * @return void */ function hawo_send_extra_email( $order, $rule ) { $mode = hawo_email_mode( $rule ); $to = ''; if ( 'manager' === $mode ) { $to = (string) get_option( 'admin_email', '' ); } elseif ( 'custom' === $mode ) { $to = hawo_sanitize_email_address( $rule['custom_email'] ?? '' ); } if ( '' === $to || ( function_exists( 'is_email' ) && ! is_email( $to ) ) ) { return; } $number = is_callable( array( $order, 'get_order_number' ) ) ? (string) $order->get_order_number() : (string) $order->get_id(); $subject = isset( $rule['custom_email_subject'] ) ? trim( (string) $rule['custom_email_subject'] ) : ''; if ( '' === $subject ) { $subject = sprintf( /* translators: %s: order number */ __( 'Order %s was auto-completed', 'advanced-autocomplete-orders-for-woocommerce' ), $number ); } $message = isset( $rule['custom_email_message'] ) ? trim( (string) $rule['custom_email_message'] ) : ''; if ( '' === $message ) { $message = sprintf( /* translators: %s: order number */ __( 'Order %s matched an autocomplete rule.', 'advanced-autocomplete-orders-for-woocommerce' ), $number ); } if ( function_exists( 'wp_mail' ) ) { wp_mail( $to, $subject, $message ); } } /** * Remember a completion, a skip, or a wait. Keeps the latest 100. * * @param int $order_id Order ID. * @param string $result completed, skipped, or waiting. * @param string $reason Plain reason. * @return void */ function hawo_add_log( $order_id, $result, $reason ) { if ( ! in_array( $result, array( 'completed', 'skipped', 'waiting' ), true ) ) { return; } $log = get_option( 'hawo_log', array() ); if ( ! is_array( $log ) ) { $log = array(); } array_unshift( $log, array( 'time' => time(), 'order_id' => (int) $order_id, 'result' => $result, 'reason' => sanitize_text_field( (string) $reason ), ) ); $log = array_slice( $log, 0, 100 ); if ( false === get_option( 'hawo_log', false ) ) { add_option( 'hawo_log', $log, '', false ); return; } update_option( 'hawo_log', $log ); } /** * Orders in a list that match a current rule. Does not change them. * * @param array $orders Orders. * @param array $settings Settings. * @return array */ function hawo_matching_order_ids( $orders, $settings ) { $ids = array(); foreach ( (array) $orders as $order ) { if ( ! is_object( $order ) || ! is_callable( array( $order, 'get_id' ) ) ) { continue; } if ( hawo_first_matching_rule( $settings, $order ) ) { $ids[] = (int) $order->get_id(); } } return $ids; } /** * @param array $rule Rule. * @return string */ function hawo_delay_label( $rule ) { $amount = hawo_total_bound_number( $rule['complete_after_amount'] ?? '' ); $unit = isset( $rule['complete_after_unit'] ) ? (string) $rule['complete_after_unit'] : 'minutes'; if ( 'days' === $unit ) { return sprintf( /* translators: %s: number of days */ __( '%s days', 'advanced-autocomplete-orders-for-woocommerce' ), $amount ); } if ( 'hours' === $unit ) { return sprintf( /* translators: %s: number of hours */ __( '%s hours', 'advanced-autocomplete-orders-for-woocommerce' ), $amount ); } return sprintf( /* translators: %s: number of minutes */ __( '%s minutes', 'advanced-autocomplete-orders-for-woocommerce' ), $amount ); } /** * Note written on the order. Completed keeps the original sentence. * * @param array $rule Rule. * @return string */ function hawo_status_note( $rule ) { $note = hawo_note_for_rule( $rule ); if ( 'completed' === hawo_target_status( $rule ) ) { return $note; } $status = hawo_target_status( $rule ); $label = function_exists( 'wc_get_order_status_name' ) ? wc_get_order_status_name( $status ) : $status; $extra = trim( str_replace( __( 'Order auto-completed.', 'advanced-autocomplete-orders-for-woocommerce' ), '', $note ) ); $sentence = sprintf( /* translators: %s: order status name */ __( 'Order status set to %s.', 'advanced-autocomplete-orders-for-woocommerce' ), $label ); return '' === $extra ? $sentence : $sentence . ' ' . $extra; } /** * Clean the actions that run after a rule matches. * * @param array $input Posted rule or settings. * @return array */ function hawo_sanitize_phase4_input( $input ) { if ( ! is_array( $input ) ) { return array(); } $clean = array(); if ( array_key_exists( 'complete_after_amount', $input ) || array_key_exists( 'complete_after_unit', $input ) ) { $clean['complete_after_amount'] = hawo_sanitize_amount( $input['complete_after_amount'] ?? '' ); $unit = isset( $input['complete_after_unit'] ) ? (string) $input['complete_after_unit'] : 'minutes'; $clean['complete_after_unit'] = in_array( $unit, array( 'minutes', 'hours', 'days' ), true ) ? $unit : 'minutes'; } if ( array_key_exists( 'complete_status', $input ) ) { $clean['complete_status'] = hawo_sanitize_complete_status( $input['complete_status'] ); } if ( array_key_exists( 'email_mode', $input ) ) { $clean['email_mode'] = hawo_sanitize_email_mode( $input['email_mode'] ); } if ( array_key_exists( 'custom_email', $input ) ) { $clean['custom_email'] = hawo_sanitize_email_address( $input['custom_email'] ); } if ( array_key_exists( 'custom_email_subject', $input ) ) { $clean['custom_email_subject'] = sanitize_text_field( (string) $input['custom_email_subject'] ); } if ( array_key_exists( 'custom_email_message', $input ) ) { $message = (string) $input['custom_email_message']; $clean['custom_email_message'] = function_exists( 'sanitize_textarea_field' ) ? sanitize_textarea_field( $message ) : sanitize_text_field( $message ); } return $clean; } /** * @param mixed $value Posted status. * @return string */ function hawo_sanitize_complete_status( $value ) { $status = is_string( $value ) ? strtolower( $value ) : ''; $status = preg_replace( '/^wc-/', '', $status ); $status = preg_replace( '/[^a-z0-9_-]/', '', (string) $status ); if ( '' === $status || 'completed' === $status ) { return ''; } if ( function_exists( 'wc_get_order_statuses' ) ) { $allowed = array(); foreach ( array_keys( wc_get_order_statuses() ) as $key ) { $allowed[] = preg_replace( '/^wc-/', '', (string) $key ); } if ( ! in_array( $status, $allowed, true ) ) { return ''; } } return $status; } /** * @param mixed $value Posted email choice. * @return string */ function hawo_sanitize_email_mode( $value ) { $value = is_string( $value ) ? $value : ''; return in_array( $value, array( 'skip', 'manager', 'custom' ), true ) ? $value : ''; } /** * @param mixed $value Posted email address. * @return string */ function hawo_sanitize_email_address( $value ) { $email = sanitize_text_field( (string) $value ); if ( function_exists( 'sanitize_email' ) ) { $email = sanitize_email( $email ); } return $email; } /** * Checkout stays "thankyou". Admin and in-person sales are separate. * * @param mixed $trigger Trigger name. * @return string */ function hawo_normalize_trigger( $trigger ) { $trigger = is_string( $trigger ) ? $trigger : 'thankyou'; return in_array( $trigger, array( 'admin', 'pos', 'thankyou' ), true ) ? $trigger : 'thankyou'; } /** * @param WC_Order $order Order. * @return string */ function hawo_trigger_for_order( $order ) { $via = ( is_object( $order ) && is_callable( array( $order, 'get_created_via' ) ) ) ? (string) $order->get_created_via() : ''; if ( 'admin' === $via ) { return 'admin'; } if ( in_array( $via, array( 'pos', 'woocommerce_pos', 'woocommerce-pos', 'point-of-sale' ), true ) ) { return 'pos'; } return 'thankyou'; } /** * @param array $rule Rule. * @return array */ function hawo_also_complete_list( $rule ) { $values = $rule['also_complete'] ?? array(); if ( is_array( $values ) && isset( $values['value'] ) ) { $values = $values['value']; } $clean = array(); foreach ( (array) $values as $value ) { $value = is_string( $value ) ? $value : ''; if ( in_array( $value, array( 'admin', 'pos' ), true ) ) { $clean[] = $value; } } return array_values( array_unique( $clean ) ); } /** * Admin and in-person orders stay out until a rule turns them on. * Checkout is unchanged. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_source_allows( $rule, $context ) { $trigger = hawo_normalize_trigger( $context['trigger'] ?? 'thankyou' ); $chosen = hawo_also_complete_list( $rule ); if ( in_array( $trigger, array( 'admin', 'pos' ), true ) ) { return in_array( $trigger, $chosen, true ); } if ( empty( $rule['is_primary'] ) && ! empty( $chosen ) && ! hawo_rule_has_other_conditions( $rule ) ) { return false; } return true; } /** * Conditions other than "also complete admin or in-person orders". * * @param array $rule Rule. * @return bool */ function hawo_rule_has_other_conditions( $rule ) { $copy = $rule; unset( $copy['also_complete'] ); return hawo_rule_has_conditions( $copy ); } /** * Whether any rule asked to complete this kind of order. * * @param array $settings Settings. * @param string $source admin|pos. * @return bool */ function hawo_rules_allow_source( $settings, $source ) { foreach ( hawo_rules_to_try( $settings ) as $rule ) { if ( in_array( $source, hawo_also_complete_list( $rule ), true ) ) { return true; } } return false; } /** * Renewals, bookings, pre-orders, and part payments. Empty means no check. * * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_special_orders_allow( $rule, $context ) { $setting = $rule['special_orders'] ?? null; if ( ! is_array( $setting ) || empty( hawo_list_values( $setting['value'] ?? array() ) ) ) { return true; } if ( isset( $context['special_tokens'] ) && is_array( $context['special_tokens'] ) ) { $tokens = $context['special_tokens']; } else { $tokens = hawo_detect_special_tokens( $context['order'] ?? null ); } return hawo_mode_list_allows( $setting, $tokens ); } /** * @param object|null $order Order. * @return array */ function hawo_detect_special_tokens( $order ) { if ( ! is_object( $order ) || ! is_callable( array( $order, 'get_meta' ) ) ) { return array(); } $tokens = array(); if ( ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order ) ) || $order->get_meta( '_subscription_renewal' ) ) { $tokens[] = 'renewal'; } if ( $order->get_meta( '_booking_id' ) || $order->get_meta( '_wc_bookings_gcal_event_id' ) || ( function_exists( 'wc_bookings_get_ids_from_order_id' ) && $order->get_id() && wc_bookings_get_ids_from_order_id( $order->get_id() ) ) ) { $tokens[] = 'booking'; } if ( $order->get_meta( '_wc_pre_orders_status' ) || $order->get_meta( '_wc_pre_order_item' ) || ( function_exists( 'wc_pre_orders_is_pre_order' ) && wc_pre_orders_is_pre_order( $order ) ) ) { $tokens[] = 'preorder'; } if ( $order->get_meta( '_wc_deposits_order_has_deposit' ) || $order->get_meta( '_awcdp_deposits_order_has_deposit' ) || $order->get_meta( '_deposit_full_amount_ex_tax' ) ) { $tokens[] = 'deposit'; } return $tokens; } /** * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_fraud_allows( $rule, $context ) { $mode = isset( $rule['fraud_hold'] ) ? (string) $rule['fraud_hold'] : ''; if ( ! in_array( $mode, array( 'skip', 'only' ), true ) ) { return true; } $fraud = array_key_exists( 'fraud_hold', $context ) ? (bool) $context['fraud_hold'] : hawo_detect_fraud_hold( $context['order'] ?? null ); if ( 'skip' === $mode ) { return ! $fraud; } return $fraud; } /** * @param object|null $order Order. * @return bool */ function hawo_detect_fraud_hold( $order ) { if ( ! is_object( $order ) ) { return false; } if ( is_callable( array( $order, 'has_status' ) ) && $order->has_status( 'fraud' ) ) { return true; } if ( is_callable( array( $order, 'get_meta' ) ) ) { $hold = strtolower( (string) $order->get_meta( '_fraud_hold' ) ); if ( in_array( $hold, array( 'yes', '1', 'true' ), true ) ) { return true; } $advice = strtolower( (string) $order->get_meta( '_wc_af_recommended_status' ) ); if ( in_array( $advice, array( 'on-hold', 'cancelled', 'failed', 'fraud' ), true ) ) { return true; } } if ( function_exists( 'apply_filters' ) ) { return (bool) apply_filters( 'hawo_order_is_fraud_hold', false, $order ); } return false; } /** * @param array $rule Rule. * @param array $context Order facts. * @return bool */ function hawo_tracking_allows( $rule, $context ) { $mode = isset( $rule['shipment_tracking'] ) ? (string) $rule['shipment_tracking'] : ''; if ( ! in_array( $mode, array( 'shipped', 'not_shipped' ), true ) ) { return true; } $shipped = array_key_exists( 'has_tracking', $context ) ? (bool) $context['has_tracking'] : hawo_detect_tracking( $context['order'] ?? null ); if ( 'shipped' === $mode ) { return $shipped; } return ! $shipped; } /** * @param object|null $order Order. * @return bool */ function hawo_detect_tracking( $order ) { if ( ! is_object( $order ) || ! is_callable( array( $order, 'get_meta' ) ) ) { return false; } $items = $order->get_meta( '_wc_shipment_tracking_items' ); if ( is_array( $items ) && ! empty( $items ) ) { return true; } foreach ( array( '_wc_shipment_tracking_number', 'ywot_tracking_code', '_tracking_number' ) as $key ) { if ( '' !== (string) $order->get_meta( $key ) ) { return true; } } if ( function_exists( 'apply_filters' ) ) { return (bool) apply_filters( 'hawo_order_has_tracking', false, $order ); } return false; } /** * @param array $rule Rule. * @return bool */ function hawo_phase5_has_conditions( $rule ) { if ( ! is_array( $rule ) ) { return false; } if ( ! empty( hawo_list_values( $rule['special_orders']['value'] ?? array() ) ) ) { return true; } if ( in_array( $rule['fraud_hold'] ?? '', array( 'skip', 'only' ), true ) ) { return true; } if ( in_array( $rule['shipment_tracking'] ?? '', array( 'shipped', 'not_shipped' ), true ) ) { return true; } return ! empty( hawo_also_complete_list( $rule ) ); } /** * @param array $input Posted rule or settings. * @return array */ function hawo_sanitize_phase5_input( $input ) { if ( ! is_array( $input ) ) { return array(); } $clean = array(); if ( array_key_exists( 'special_orders', $input ) ) { $list = hawo_sanitize_mode_list( $input['special_orders'], 'text' ); $allowed = array( 'renewal', 'booking', 'preorder', 'deposit' ); $list['value'] = array_values( array_intersect( $list['value'], $allowed ) ); $clean['special_orders'] = $list; } if ( array_key_exists( 'fraud_hold', $input ) ) { $clean['fraud_hold'] = in_array( $input['fraud_hold'], array( 'skip', 'only' ), true ) ? $input['fraud_hold'] : ''; } if ( array_key_exists( 'shipment_tracking', $input ) ) { $clean['shipment_tracking'] = in_array( $input['shipment_tracking'], array( 'shipped', 'not_shipped' ), true ) ? $input['shipment_tracking'] : ''; } if ( ! empty( $input['also_complete_form'] ) || array_key_exists( 'also_complete', $input ) ) { $clean['also_complete'] = hawo_also_complete_list( array( 'also_complete' => $input['also_complete'] ?? array() ) ); } if ( array_key_exists( 'webhook_url', $input ) ) { $clean['webhook_url'] = hawo_sanitize_webhook_url( $input['webhook_url'] ); } return $clean; } /** * @param mixed $value Posted URL. * @return string */ function hawo_sanitize_webhook_url( $value ) { $url = trim( sanitize_text_field( (string) $value ) ); if ( function_exists( 'esc_url_raw' ) ) { $url = (string) esc_url_raw( $url ); } if ( ! preg_match( '#^https?://#i', $url ) ) { return ''; } return $url; } /** * Tell another app after the status changes. An empty URL does nothing. * * @param WC_Order $order Order. * @param array $rule Rule. * @return void */ function hawo_notify_webhook( $order, $rule ) { $url = hawo_sanitize_webhook_url( $rule['webhook_url'] ?? '' ); if ( '' === $url || ! function_exists( 'wp_remote_post' ) ) { return; } $payload = array( 'order_id' => is_callable( array( $order, 'get_id' ) ) ? (int) $order->get_id() : 0, 'order_number' => is_callable( array( $order, 'get_order_number' ) ) ? (string) $order->get_order_number() : '', 'status' => hawo_target_status( $rule ), 'rule' => hawo_rule_label( $rule, 0 ), ); $body = function_exists( 'wp_json_encode' ) ? wp_json_encode( $payload ) : json_encode( $payload ); wp_remote_post( $url, array( 'timeout' => 2, 'blocking' => false, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => $body, ) ); } /** * Same order list the settings screen looks at. * * @return array */ function hawo_old_order_query_args() { $statuses = array( 'pending', 'processing', 'on-hold', 'failed' ); if ( function_exists( 'wc_get_order_statuses' ) ) { $statuses = array(); foreach ( array_keys( wc_get_order_statuses() ) as $key ) { if ( 'wc-completed' === $key ) { continue; } $statuses[] = $key; } } return array( 'limit' => 20, 'orderby' => 'date', 'order' => 'DESC', 'status' => $statuses, 'return' => 'objects', ); } /** * Matching old orders. Does not change them. * * @param array $settings Settings. * @return array */ function hawo_preview_old_order_ids( $settings ) { if ( ! function_exists( 'wc_get_orders' ) ) { return array(); } $orders = wc_get_orders( hawo_old_order_query_args() ); return hawo_matching_order_ids( is_array( $orders ) ? $orders : array(), is_array( $settings ) ? $settings : array() ); } /** * Complete orders the store already chose. Does nothing when the list is empty. * * @param array $ids Order IDs. * @param array $settings Settings. * @return array */ function hawo_complete_listed_orders( $ids, $settings ) { $done = 0; $skipped = 0; foreach ( (array) $ids as $order_id ) { $order = function_exists( 'wc_get_order' ) ? wc_get_order( (int) $order_id ) : null; if ( ! is_object( $order ) ) { $skipped++; continue; } $rule = hawo_first_matching_rule( is_array( $settings ) ? $settings : array(), $order ); if ( ! is_array( $rule ) ) { hawo_add_log( (int) $order_id, 'skipped', hawo_explain_order( $settings, $order )['summary'] ); $skipped++; continue; } hawo_finish_matched_order( $order, $rule ); $done++; } return array( 'done' => $done, 'skipped' => $skipped, ); } /** * Note for the rule that completed the order. * * Rule 1 keeps the same note as before unless a new filter was used. * * @param array $rule Matching rule. * @return string */ function hawo_note_for_rule( $rule ) { $note = hawo_autocomplete_note( $rule ); if ( empty( $rule['is_primary'] ) && ! empty( $rule['name'] ) ) { $note .= ' ' . sprintf( /* translators: %s: rule name */ __( 'Rule: %s.', 'advanced-autocomplete-orders-for-woocommerce' ), $rule['name'] ); } $match = $rule['product_match'] ?? 'any'; if ( 'every' === $match ) { $note .= ' ' . __( 'Every product in the order matched.', 'advanced-autocomplete-orders-for-woocommerce' ); } elseif ( 'only' === $match ) { $note .= ' ' . __( 'The order had only the chosen products.', 'advanced-autocomplete-orders-for-woocommerce' ); } return $note; }