9 ? substr( $d, -9 ) : $d; } // International digits for wa.me links. $cc = calling code of the billing country ("+212" or "212"). function codc_wa_number( $raw, $cc ) { $d = preg_replace( '/\D+/', '', (string) $raw ); $cc = preg_replace( '/\D+/', '', (string) $cc ); if ( strpos( $d, '00' ) === 0 ) { return substr( $d, 2 ); } if ( $cc && strpos( (string) $raw, '+' ) !== 0 && strpos( $d, $cc ) !== 0 ) { return $cc . ltrim( $d, '0' ); } return $d; } function codc_fill( $template, array $vars ) { return strtr( $template, $vars ); } /* ---------- Settings ---------- */ function codc_opt( $key ) { $defaults = array( 'threshold' => 2, // refused/cancelled COD orders before COD is blocked for that phone. 0 = never block. 'message' => "Hello {name}, thank you for your order #{order}:\n{items}\nTotal: {total} (cash on delivery).\nPlease reply YES to confirm delivery.", ); $o = get_option( 'codc', array() ); return isset( $o[ $key ] ) ? $o[ $key ] : $defaults[ $key ]; } /* ---------- WooCommerce compatibility ---------- */ add_action( 'before_woocommerce_init', function () { if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); } } ); /* ---------- Order statuses ---------- */ add_filter( 'woocommerce_register_shop_order_post_statuses', function ( $s ) { $base = array( 'public' => false, 'exclude_from_search' => false, 'show_in_admin_all_list' => true, 'show_in_admin_status_list' => true ); /* translators: %s: number of orders */ $s[ 'wc-' . CODC_PENDING ] = $base + array( 'label' => _x( 'Awaiting confirmation', 'Order status', 'yaceen-cod' ), 'label_count' => _n_noop( 'Awaiting confirmation (%s)', 'Awaiting confirmation (%s)', 'yaceen-cod' ) ); /* translators: %s: number of orders */ $s[ 'wc-' . CODC_REFUSED ] = $base + array( 'label' => _x( 'Refused at delivery', 'Order status', 'yaceen-cod' ), 'label_count' => _n_noop( 'Refused at delivery (%s)', 'Refused at delivery (%s)', 'yaceen-cod' ) ); return $s; } ); add_filter( 'wc_order_statuses', function ( $s ) { $s[ 'wc-' . CODC_PENDING ] = _x( 'Awaiting confirmation', 'Order status', 'yaceen-cod' ); $s[ 'wc-' . CODC_REFUSED ] = _x( 'Refused at delivery', 'Order status', 'yaceen-cod' ); return $s; } ); // New COD orders wait for confirmation instead of going straight to Processing. add_filter( 'woocommerce_cod_process_payment_order_status', function () { return CODC_PENDING; } ); // Remember the normalized phone so history lookups are one indexed meta query. // ponytail: orders from before activation have no key and don't count toward risk; add a backfill when someone asks. function codc_tag( $order_id ) { $order = wc_get_order( $order_id ); if ( $order && ! $order->get_meta( '_codc_phone' ) && $order->get_billing_phone() ) { $order->update_meta_data( '_codc_phone', codc_phone_key( $order->get_billing_phone() ) ); $order->save(); } } add_action( 'woocommerce_order_status_' . CODC_PENDING, function ( $order_id, $order ) { codc_tag( $order_id ); wc_maybe_reduce_stock_levels( $order_id ); // reserve stock while we call $mails = WC()->mailer()->get_emails(); if ( isset( $mails['WC_Email_New_Order'] ) ) { $mails['WC_Email_New_Order']->trigger( $order_id, $order ); // core only sends it on pending→processing/on-hold } }, 10, 2 ); add_action( 'woocommerce_order_status_cancelled', 'codc_tag' ); add_action( 'woocommerce_order_status_' . CODC_REFUSED, function ( $order_id ) { codc_tag( $order_id ); wc_maybe_increase_stock_levels( $order_id ); // parcel comes back } ); /* ---------- Risk + blocking ---------- */ function codc_risk( $phone ) { $key = codc_phone_key( $phone ); if ( strlen( $key ) < 6 ) { return 0; } $args = array( 'limit' => -1, 'return' => 'ids', 'status' => array( 'cancelled', CODC_REFUSED ), 'codc_phone' => $key, // legacy posts storage: mapped to meta_query by the filter below ); if ( \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { $args['meta_query'] = array( array( 'key' => '_codc_phone', 'value' => $key ) ); // phpcs:ignore WordPress.DB.SlowDBQuery } return count( wc_get_orders( $args ) ); } // The legacy (non-HPOS) order store ignores meta_query in wc_get_orders(); inject it into the WP_Query instead. add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', function ( $query, $vars ) { if ( ! empty( $vars['codc_phone'] ) ) { $query['meta_query'][] = array( 'key' => '_codc_phone', 'value' => $vars['codc_phone'] ); // phpcs:ignore WordPress.DB.SlowDBQuery } return $query; }, 10, 2 ); function codc_blocked( $phone ) { $t = (int) codc_opt( 'threshold' ); return $t > 0 && codc_risk( $phone ) >= $t; } function codc_block_msg() { return __( 'Cash on delivery is not available for this phone number. Please choose another payment method.', 'yaceen-cod' ); } // Classic checkout. add_action( 'woocommerce_after_checkout_validation', function ( $data, $errors ) { if ( isset( $data['payment_method'] ) && 'cod' === $data['payment_method'] && codc_blocked( isset( $data['billing_phone'] ) ? $data['billing_phone'] : '' ) ) { $errors->add( 'codc_blocked', codc_block_msg() ); } }, 10, 2 ); // Block checkout (Store API). add_action( 'woocommerce_store_api_checkout_update_order_from_request', function ( $order, $request ) { if ( 'cod' === $request['payment_method'] && codc_blocked( $order->get_billing_phone() ) ) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'codc_blocked', esc_html( codc_block_msg() ), 400 ); } }, 10, 2 ); /* ---------- Admin: confirmation queue ---------- */ add_action( 'admin_menu', function () { $n = count( wc_get_orders( array( 'status' => CODC_PENDING, 'limit' => -1, 'return' => 'ids' ) ) ); $badge = $n ? ' ' . (int) $n . '' : ''; add_submenu_page( 'woocommerce', esc_html__( 'Yaceen COD', 'yaceen-cod' ), esc_html__( 'Yaceen COD', 'yaceen-cod' ) . $badge, 'edit_shop_orders', 'codc', 'codc_page' ); } ); add_action( 'admin_post_codc', function () { check_admin_referer( 'codc' ); $redirect = admin_url( 'admin.php?page=codc' ); if ( isset( $_POST['codc_settings'] ) ) { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'yaceen-cod' ), 403 ); } update_option( 'codc', array( 'threshold' => absint( wp_unslash( $_POST['threshold'] ?? 0 ) ), 'message' => sanitize_textarea_field( wp_unslash( $_POST['message'] ?? '' ) ), ) ); wp_safe_redirect( $redirect ); exit; } if ( ! current_user_can( 'edit_shop_orders' ) ) { wp_die( esc_html__( 'Not allowed.', 'yaceen-cod' ), 403 ); } $order = wc_get_order( absint( $_POST['order'] ?? 0 ) ); $do = sanitize_key( wp_unslash( $_POST['do'] ?? '' ) ); $agent = wp_get_current_user()->display_name; if ( $order && $order->has_status( CODC_PENDING ) ) { if ( 'confirm' === $do ) { /* translators: %s: agent name */ $order->update_status( 'processing', sprintf( __( 'COD confirmed by %s.', 'yaceen-cod' ), $agent ) ); $mails = WC()->mailer()->get_emails(); if ( isset( $mails['WC_Email_Customer_Processing_Order'] ) ) { $mails['WC_Email_Customer_Processing_Order']->trigger( $order->get_id(), $order ); // core skips it for custom→processing } } elseif ( 'cancel' === $do ) { /* translators: %s: agent name */ $order->update_status( 'cancelled', sprintf( __( 'Cancelled at confirmation by %s.', 'yaceen-cod' ), $agent ) ); } elseif ( 'noanswer' === $do ) { $n = (int) $order->get_meta( '_codc_attempts' ) + 1; $order->update_meta_data( '_codc_attempts', $n ); /* translators: 1: attempt number, 2: agent name */ $order->add_order_note( sprintf( __( 'No answer (attempt %1$d) — %2$s.', 'yaceen-cod' ), $n, $agent ) ); $order->save(); } } wp_safe_redirect( $redirect ); exit; } ); function codc_button( $order_id, $do, $label, $class = '' ) { ?>
' . ( time() - 30 * DAY_IN_SECONDS ); $count = function ( $status ) use ( $since ) { return count( wc_get_orders( array( 'status' => $status, 'payment_method' => 'cod', 'date_created' => $since, 'limit' => -1, 'return' => 'ids' ) ) ); }; $confirmed = $count( array( 'processing', 'completed', CODC_REFUSED ) ); $cancelled = $count( 'cancelled' ); $delivered = $count( 'completed' ); $refused = $count( CODC_REFUSED ); $pct = function ( $a, $b ) { return $b ? round( 100 * $a / $b ) . '%' : '—'; }; // ponytail: 200 oldest + one risk query per row; paginate if a store has more waiting at once. $orders = wc_get_orders( array( 'status' => CODC_PENDING, 'limit' => 200, 'orderby' => 'date', 'order' => 'ASC' ) ); ?>

:

get_billing_phone(); $risk = codc_risk( $phone ); $items = array(); foreach ( $o->get_items() as $item ) { $items[] = '- ' . $item->get_name() . ' x' . $item->get_quantity(); } $total = html_entity_decode( wp_strip_all_tags( wc_price( $o->get_total(), array( 'currency' => $o->get_currency() ) ) ) ); $msg = codc_fill( codc_opt( 'message' ), array( '{name}' => $o->get_billing_first_name(), '{order}' => $o->get_order_number(), '{total}' => $total, '{items}' => implode( "\n", $items ), ) ); $wa = 'https://wa.me/' . codc_wa_number( $phone, WC()->countries->get_country_calling_code( $o->get_billing_country() ) ); ?>
#get_order_number() ); ?>
get_date_created() ? $o->get_date_created()->date_i18n( 'd/m H:i' ) : '' ); ?>
get_formatted_billing_full_name() ); ?>
get_billing_city() ); ?>

WhatsApp
get_formatted_order_total() ); ?> get_meta( '_codc_attempts' ); ?> ⚠ OK get_id(), 'confirm', __( 'Confirm', 'yaceen-cod' ), 'button-primary' ); codc_button( $o->get_id(), 'noanswer', __( 'No answer', 'yaceen-cod' ) ); codc_button( $o->get_id(), 'cancel', __( 'Cancel', 'yaceen-cod' ) ); ?>