any` over 730 days and a // meta_query on one leg -- then hydrated every result into a WC_Order object // to read three billing fields off it. Eight hundred order objects in one // request. On the legacy post-based store that is tens of seconds or an // out-of-memory, and because it happened on a page the merchant naturally // reloads, every PHP-FPM worker ended up parked in it -- which is why the // FRONT END of the site died too, with no workers left to serve visitors. // // A page that opens must never do unbounded work. So: the queue of ORDERS // (orderguard_review_queue_orders) still renders live, because that is what the page // is for and it is bounded at 50. The history scan is now a one-off the // merchant asks for, and everything after that reads the cache. $cached = orderguard_get_transient( 'orderguard_hist_clients' ); if ( is_array( $cached ) && isset( $cached['rows'] ) && is_array( $cached['rows'] ) ) { return $cached; } if ( ! $scan ) { return array( 'rows' => array(), 'unscanned' => true ); } if ( ! function_exists( 'wc_get_orders' ) || ! orderguard_flag_enabled() ) { return array( 'rows' => array() ); } $after = time() - ( ORDERGUARD_HISTORY_CLIENTS_WINDOW_DAYS * DAY_IN_SECONDS ); $cap = ORDERGUARD_HISTORY_CLIENTS_SCAN_CAP; // IDS, NOT OBJECTS. wc_get_orders() with return => ids costs one query and no // hydration; the orders are then loaded one at a time and released, so peak // memory is one order rather than four hundred. A TIMESTAMP is passed for // date_created (not a formatted string) because that is the form // WC_Order_Query parses unambiguously on both order stores. $nc_ids = array(); $nc_truncated = false; foreach ( orderguard_order_meta_query_keys( '_orderguard_not_collected' ) as $meta_key ) { $leg = wc_get_orders( array( 'limit' => $cap, 'orderby' => 'date', 'order' => 'DESC', 'status' => 'any', 'return' => 'ids', 'date_created' => '>' . $after, 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => $meta_key, 'value' => 'yes', 'compare' => '=', ), ), ) ); $leg = is_array( $leg ) ? array_map( 'intval', $leg ) : array(); $nc_truncated = $nc_truncated || count( $leg ) >= $cap; foreach ( $leg as $order_id ) { $nc_ids[ $order_id ] = true; } } $rt_ids = wc_get_orders( array( 'limit' => $cap, 'orderby' => 'date', 'order' => 'DESC', 'status' => array( 'refunded' ), 'return' => 'ids', 'date_created' => '>' . $after, ) ); $nc_ids = array_keys( $nc_ids ); $rt_ids = is_array( $rt_ids ) ? array_map( 'intval', $rt_ids ) : array(); $truncated = ( $nc_truncated || count( $nc_ids ) > $cap || count( $rt_ids ) >= $cap ); if ( count( $nc_ids ) > $cap ) { $nc_ids = array_slice( $nc_ids, 0, $cap ); } $groups = array(); $email_group = array(); $phone_group = array(); $phone_emails = array(); $counted = array(); foreach ( array( 'not_collected' => $nc_ids, 'returned' => $rt_ids ) as $bucket => $ids ) { foreach ( $ids as $oid ) { if ( isset( $counted[ $oid ] ) ) { continue; // ONE PARCEL, ONE COUNT (refunded AND marked) } $order = wc_get_order( $oid ); if ( ! $order instanceof WC_Order ) { continue; } // RE-CHECK THE MARK IN PHP. wc_get_orders() silently DROPS an // unsupported meta_query on the legacy post-based order store, which // would degrade bucket (a) to "the most recent orders" and render a // loyal customer as "2 not collected" with a Block COD button beside // their name. orderguard_review_queue_orders() re-checks for the same reason. if ( 'not_collected' === $bucket && 'yes' !== (string) orderguard_get_order_meta( $order, '_orderguard_not_collected' ) ) { continue; } if ( 'returned' === $bucket && ! $order->has_status( 'refunded' ) ) { continue; } if ( function_exists( 'orderguard_is_excluded_order' ) && orderguard_is_excluded_order( $order ) ) { continue; } $email = strtolower( trim( (string) $order->get_billing_email() ) ); $phone = trim( (string) $order->get_billing_phone() ); if ( '' === $email && '' === $phone ) { continue; } $email_hash = ( '' !== $email ) ? orderguard_hash_value( $email ) : ''; // CANONICAL PHONE KEY -- "0888123456" and "+359888123456" are one // human and both spellings are everywhere in Bulgaria. orderguard_phone_hash() // is spelling-preserving and frozen; orderguard_phone_hash_e164() returns the // international-form hash (or '' when it already matches), so // preferring it collapses the two onto one key. Merges nothing risky: // same number by construction. $phone_key = ''; if ( '' !== $phone ) { $e164 = function_exists( 'orderguard_phone_hash_e164' ) ? orderguard_phone_hash_e164( $phone, (string) $order->get_billing_country() ) : ''; $phone_key = ( '' !== $e164 ) ? $e164 : orderguard_phone_hash( $phone ); } if ( '' !== $phone_key ) { if ( ! isset( $phone_emails[ $phone_key ] ) ) { $phone_emails[ $phone_key ] = array(); } if ( '' !== $email_hash ) { $phone_emails[ $phone_key ][ $email_hash ] = 1; } } // Key on EMAIL when present: an inbox is per-person in a way a phone // number is not (a household shares a number). $key = ( '' !== $email_hash ) ? $email_hash : $phone_key; if ( '' === $key ) { continue; } if ( ! isset( $groups[ $key ] ) ) { $groups[ $key ] = array( 'key' => $key, 'email_hash' => $email_hash, 'phone_hash' => $phone_key, 'order_id' => $oid, 'not_collected' => 0, 'returned' => 0, ); } if ( '' === $groups[ $key ]['phone_hash'] && '' !== $phone_key ) { $groups[ $key ]['phone_hash'] = $phone_key; } ++$groups[ $key ][ $bucket ]; $counted[ $oid ] = $bucket; if ( $oid > $groups[ $key ]['order_id'] ) { $groups[ $key ]['order_id'] = $oid; } if ( '' !== $email_hash ) { $email_group[ $email_hash ] = $key; } elseif ( '' !== $phone_key ) { $phone_group[ $phone_key ] = $key; } } } // MERGE PASS -- one person whose email is filled in on some orders and not // others used to appear as two rows, each carrying half the count, so someone // with two refusals could fall under the threshold on both and vanish. Merge // a phone-only group into an email group ONLY when that phone has been seen // with exactly ONE distinct email; two emails on one number is the household // case, and merging there would blame one person for another's parcels. foreach ( $phone_group as $phone_key => $pkey ) { if ( ! isset( $groups[ $pkey ] ) || ! isset( $phone_emails[ $phone_key ] ) ) { continue; } $emails = array_keys( $phone_emails[ $phone_key ] ); if ( 1 !== count( $emails ) ) { continue; } $target = isset( $email_group[ $emails[0] ] ) ? $email_group[ $emails[0] ] : ''; if ( '' === $target || $target === $pkey || ! isset( $groups[ $target ] ) ) { continue; } $groups[ $target ]['not_collected'] += $groups[ $pkey ]['not_collected']; $groups[ $target ]['returned'] += $groups[ $pkey ]['returned']; if ( $groups[ $pkey ]['order_id'] > $groups[ $target ]['order_id'] ) { $groups[ $target ]['order_id'] = $groups[ $pkey ]['order_id']; } if ( '' === $groups[ $target ]['phone_hash'] ) { $groups[ $target ]['phone_hash'] = $phone_key; } unset( $groups[ $pkey ] ); } $dismissed = orderguard_history_dismissed(); $rows = array(); foreach ( $groups as $key => $g ) { // Dismissal is checked against BOTH hashes, so a client dismissed while // the list keyed them by phone stays dismissed after the merge pass keys // them by email. if ( isset( $dismissed[ $key ] ) || ( '' !== $g['email_hash'] && isset( $dismissed[ $g['email_hash'] ] ) ) || ( '' !== $g['phone_hash'] && isset( $dismissed[ $g['phone_hash'] ] ) ) ) { continue; } if ( ! orderguard_should_flag( $g['not_collected'], $g['returned'] ) ) { continue; } if ( function_exists( 'orderguard_flag_find_by_hashes' ) && orderguard_flag_find_by_hashes( $g['email_hash'], $g['phone_hash'] ) ) { continue; // a flag or a block IS the decision } $rows[] = $g; } // Worst first. A refused parcel costs the shipping both ways and the stock // comes back late; a return at least arrived. usort( $rows, function ( $a, $b ) { $sa = ( $a['not_collected'] * 10 ) + $a['returned']; $sb = ( $b['not_collected'] * 10 ) + $b['returned']; if ( $sa === $sb ) { return $b['order_id'] - $a['order_id']; } return $sb - $sa; } ); // NO RAW IDENTITIES IN THE CACHE -- hashes and an order id only. Names, // emails and phones are re-read at render for the ten rows actually drawn, // which also keeps the exclusion list honoured as of NOW rather than as of // whenever the scan ran. $payload = array( 'rows' => $rows, 'truncated' => $truncated, 'scanned' => count( $nc_ids ) + count( $rt_ids ), ); set_transient( 'orderguard_hist_clients', $payload, 12 * HOUR_IN_SECONDS ); return $payload; } /** * How many clients still need a decision. Used by the import card's notice. */ function orderguard_history_problem_client_count() { $payload = orderguard_history_problem_clients(); return ( isset( $payload['rows'] ) && is_array( $payload['rows'] ) ) ? count( $payload['rows'] ) : 0; } /** * Mask an identity for display: "0888 ••• 412", "v•••@abv.bg". * The merchant can see these in full in his own admin, so this is habit rather * than a control -- but it does mean a screenshot of this screen is safe to send * to support. */ function orderguard_history_mask( $value, $is_email ) { $value = trim( (string) $value ); if ( '' === $value ) { return ''; } if ( $is_email ) { $at = strpos( $value, '@' ); if ( false === $at || $at < 1 ) { return '•••'; } return substr( $value, 0, 1 ) . '•••' . substr( $value, $at ); } $digits = preg_replace( '/\D+/', '', $value ); if ( strlen( $digits ) < 7 ) { return '•••'; } return substr( $digits, 0, 4 ) . ' ••• ' . substr( $digits, -3 ); } /** * The "From your order history" card. Returns '' when there is nothing to show, * so the section disappears the moment it is empty and never comes back. */ function orderguard_render_history_clients_section() { if ( ! orderguard_flag_enabled() ) { return ''; } // READ-ONLY. Never scans -- see orderguard_history_problem_clients(). $payload = orderguard_history_problem_clients(); $rows = isset( $payload['rows'] ) && is_array( $payload['rows'] ) ? $payload['rows'] : array(); $truncated = ! empty( $payload['truncated'] ); $total = count( $rows ); // NOT YET SCANNED: offer the one-click scan instead of doing it here. A page // that opens must not do unbounded work, and this card is what replaced the // version that did. if ( empty( $rows ) && ! empty( $payload['unscanned'] ) ) { $scan_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_history_scan' ), 'orderguard_histscan' ); ob_start(); ?>

= ORDERGUARD_HISTORY_CLIENTS_SHOWN ) { break; } $order = wc_get_order( (int) $row['order_id'] ); if ( ! $order instanceof WC_Order ) { continue; // deleted since the scan } if ( function_exists( 'orderguard_is_excluded_order' ) && orderguard_is_excluded_order( $order ) ) { continue; } $row['name'] = trim( (string) $order->get_formatted_billing_full_name() ); $row['email'] = strtolower( trim( (string) $order->get_billing_email() ) ); $row['phone'] = trim( (string) $order->get_billing_phone() ); $cards[] = $row; } if ( empty( $cards ) ) { return ''; } $remaining = $total - count( $cards ); ob_start(); ?>

= orderguard_flag_notcollected_threshold() ); $av_bg = $danger ? '#FBEAEA' : '#FBF1DE'; $av_fg = $danger ? '#8F4444' : '#B07D2A'; $initials = orderguard_review_initials( $row['name'] ); $oid = (int) $row['order_id']; $reason_parts = array(); if ( (int) $row['not_collected'] > 0 ) { /* translators: %d is a count of non-collected orders. */ $reason_parts[] = sprintf( __( '%d not collected', 'orderguard' ), (int) $row['not_collected'] ); } if ( (int) $row['returned'] > 0 ) { /* translators: %d is a count of returned orders. */ $reason_parts[] = sprintf( __( '%d returned', 'orderguard' ), (int) $row['returned'] ); } $ident = array(); if ( '' !== $row['phone'] ) { $ident[] = orderguard_history_mask( $row['phone'], false ); } if ( '' !== $row['email'] ) { $ident[] = orderguard_history_mask( $row['email'], true ); } $flag_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_save_flag&order_id=' . $oid . '&level=flag&reason=' . ( $danger ? 'never_collects' : 'serial_returner' ) ), 'orderguard_saveflag_' . $oid ); $block_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_save_flag&order_id=' . $oid . '&level=block_cod&reason=' . ( $danger ? 'never_collects' : 'serial_returner' ) ), 'orderguard_saveflag_' . $oid ); $fine_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_history_dismiss&orderguard_key=' . rawurlencode( $row['key'] ) ), 'orderguard_histdismiss_' . $row['key'] ); ?>
·
0 ) : ?>

get_meta only. * * TODO(assumption): "needs review" = threshold-crossing snapshot counts on the * order, not the flags table (advisory 'flag' entries still show via the same * customer's counts). On the legacy CPT order store wc_get_orders may ignore * meta_query, in which case the PHP re-check below still keeps the list * correct but only within the most recent 50 orders. */ function orderguard_review_queue_orders() { if ( ! function_exists( 'wc_get_orders' ) || ! orderguard_flag_enabled() ) { return array(); } // ONE META KEY PER QUERY, MERGED IN PHP. Never build a multi-clause // meta_query here again -- read this before "simplifying" it back. // // WHAT HAPPENED. This used to be a single wc_get_orders() with an AND group // containing a FOUR-clause OR over numeric meta plus a second OR group for // _orderguard_reviewed. On HPOS (custom order tables, which is what this store runs) // each clause that cannot share an alias becomes its own LEFT JOIN on // wc_orders_meta with no restrictive predicate at the join, so the planner // materialises something close to orders x meta^n. With a few hundred orders // and a few dozen meta rows each, that is hundreds of millions of rows: the // request never returns, never trips max_execution_time (it is I/O and sort, // not CPU), and logs nothing at all. It took two clauses to be slow-but-alive // and four to hang outright, which is why it only appeared when task 50 added // the two own-history keys. // // One single-key query per metric and available schema generation is a // trivially indexed lookup on // (meta_key, meta_value). The union is tiny and the PHP loop below already // re-verified everything, so nothing is lost by dropping the SQL-side // filtering -- including _orderguard_reviewed, which is now checked only in PHP where // it was always being re-checked anyway. $specs = array( array( '_orderguard_not_collected_count', orderguard_flag_notcollected_threshold() ), array( '_orderguard_returned_count', orderguard_flag_returned_threshold() ), // TASK 50 -- the store's OWN counts, imported history included. The two // network keys above are 0 for exactly the customers a freshly-imported // store most needs to see, because the server withholds raw counts until // the store corroborates the identity with a non-import resolved order. array( '_orderguard_own_not_collected_count', orderguard_flag_notcollected_threshold() ), array( '_orderguard_own_returned_count', orderguard_flag_returned_threshold() ), ); $ids = array(); foreach ( $specs as $spec ) { foreach ( orderguard_order_meta_query_keys( $spec[0] ) as $meta_key ) { $found = wc_get_orders( array( 'limit' => 50, 'orderby' => 'date', 'order' => 'DESC', 'return' => 'ids', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => $meta_key, 'value' => $spec[1], 'compare' => '>=', 'type' => 'NUMERIC', ), ), ) ); if ( is_array( $found ) ) { foreach ( $found as $id ) { $ids[ (int) $id ] = true; } } } } if ( empty( $ids ) ) { return array(); } // Newest first. Order ids are monotonic with creation on both order stores, // so this reproduces the old orderby=date DESC without a fifth query. $ids = array_keys( $ids ); rsort( $ids, SORT_NUMERIC ); $ids = array_slice( $ids, 0, 50 ); $check_blocks = function_exists( 'orderguard_manual_flag_enabled' ) && orderguard_manual_flag_enabled(); $queue = array(); foreach ( $ids as $id ) { $order = wc_get_order( $id ); if ( ! $order instanceof WC_Order ) { continue; } // M-7: the exclusion list wins everywhere or nowhere. The orders-list // badge (orderguard_orders_column_badge) and the order panel both stand down for // an excluded customer; a queue row that survives exclusion reads as "the // exclusion didn't work" -- and the panel it links to says "not scored". // THE SAME check as the badge, on purpose: never reimplement this as a // local hash comparison, or the two surfaces drift again. if ( orderguard_is_excluded_order( $order ) ) { continue; } if ( 'yes' === orderguard_get_order_meta( $order, '_orderguard_reviewed' ) ) { continue; } // NO STATUS FILTERING HERE, and it stays that way. I briefly excluded // cancelled / refunded / failed / trash on the reasoning that a settled // order has nothing left to decide. Giuliano, 30 July 2026: "why do you // exclude these - cancelled, refunded, failed. These are the most likely to // have issues like serial returners or cod uncollected." He is right, and // there is a second reason that is worse: // // FILTERING BY STATUS MAKES THE MERCHANT'S OWN ACTION ERASE THE ROW. // Cancelling or refunding an order is a perfectly reasonable response to a // risky customer -- and under a status filter, doing it made the order // vanish from this list. So the record of the decision disappeared at the // moment it was taken, and a mis-click could not be reviewed afterwards. // A worklist whose rows evaporate when you act on them outside it is not a // worklist. In his words: "then Reviews will be pointless." // // A row leaves this list for exactly ONE reason: the merchant said so -- // reviewed (_orderguard_reviewed) or a block on the customer. Both are deliberate. // Staleness is handled by SHOWING each order's status on the row and by the // 50-row newest-first bound, not by hiding anything. $nc = (int) orderguard_get_order_meta( $order, '_orderguard_not_collected_count' ); $rt = (int) orderguard_get_order_meta( $order, '_orderguard_returned_count' ); // Either source qualifies. orderguard_own_flag_reason() applies the SAME // thresholds to the store's own counts and re-checks exclusion itself, so // a row can enter the queue on network evidence, on its own books, or on // both. if ( ! orderguard_should_flag( $nc, $rt ) && null === orderguard_own_flag_reason( $order ) ) { continue; } // Customer already blocked (COD or hard) -> considered handled. if ( $check_blocks ) { $entry = orderguard_flag_find_for_order( $order ); if ( $entry && in_array( $entry->level, array( 'block_cod', 'hard_block' ), true ) ) { continue; } } $queue[] = $order; } return $queue; } /** * Initials for the row avatar ("Ivan Petrov" -> "IP"). Multibyte-safe, '?' when * the order has no billing name. */ function orderguard_review_initials( $name ) { $name = trim( (string) $name ); if ( '' === $name ) { return '?'; } $parts = preg_split( '/\s+/u', $name ); $initials = ''; foreach ( array_slice( $parts, 0, 2 ) as $part ) { $initials .= function_exists( 'mb_substr' ) ? mb_substr( $part, 0, 1 ) : substr( $part, 0, 1 ); } return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $initials ) : strtoupper( $initials ); } /** * --------------------------------------------------------------------------- * Approve: mark an order reviewed-OK. * --------------------------------------------------------------------------- * Writes _orderguard_reviewed = yes on the order (HPOS-safe), and -- when the manual * flag feature is enabled -- also clears an advisory 'flag'-level entry for * that customer via the existing unflag path (orderguard_flag_delete_by_id + network * signal retraction), mirroring orderguard_handle_delete_flag(). Blocks are never * touched here. */ add_action( 'admin_post_orderguard_review_approve', 'orderguard_handle_review_approve' ); function orderguard_handle_review_approve() { $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0; if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'orderguard_reviewapprove_' . $order_id ) ) { wp_die( 'Security check failed.' ); } if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( 'Insufficient permissions.' ); } $order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false; if ( $order ) { $order->update_meta_data( '_orderguard_reviewed', 'yes' ); $order->save(); // TASK 50: the sidebar bubble is cached for 5 minutes. A count that // still says 3 after the merchant just cleared a row reads as broken, // so every action that resolves a row drops the cache. if ( function_exists( 'orderguard_review_queue_invalidate' ) ) { orderguard_review_queue_invalidate(); } // Reuse the existing unflag path for an advisory flags-table entry. if ( function_exists( 'orderguard_manual_flag_enabled' ) && orderguard_manual_flag_enabled() ) { $entry = orderguard_flag_find_for_order( $order ); if ( $entry && 'flag' === $entry->level ) { orderguard_flag_delete_by_id( (int) $entry->id ); if ( 'order' === $entry->source ) { orderguard_send_flag_signal( $entry->email, $entry->phone, 'none' ); } } } set_transient( 'orderguard_flag_notice_' . get_current_user_id(), 'reviewed', 120 ); } wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=orderguard-review' ) ); exit; } /** * --------------------------------------------------------------------------- * The Review queue page (Option A "inbox list"). * --------------------------------------------------------------------------- */ function orderguard_render_review_queue_page() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $flag_on = orderguard_flag_enabled(); $manual_on = function_exists( 'orderguard_manual_flag_enabled' ) && orderguard_manual_flag_enabled(); $rows = $flag_on ? orderguard_review_queue_orders() : array(); $count = count( $rows ); // Publish the count for the sidebar bubble. This page has already paid for // the query, so the bubble costs nothing anywhere else -- see // orderguard_review_queue_count(), which is read-only for exactly this reason. $setup = get_option( 'orderguard_owned_install_status' ); if ( $flag_on && ! ( is_array( $setup ) && ( $setup['activation_pending'] ?? false ) === true ) ) { set_transient( 'orderguard_review_count', (string) $count, 5 * MINUTE_IN_SECONDS ); } $nc_threshold = orderguard_flag_notcollected_threshold(); ?>

OrderGuard ·

get_id(); $nc = (int) orderguard_get_order_meta( $order, '_orderguard_not_collected_count' ); $rt = (int) orderguard_get_order_meta( $order, '_orderguard_returned_count' ); // Danger tint when the (costlier) not-collected limit is what // tripped; warning tint for a returns-driven flag. $danger = ( $nc >= $nc_threshold ); $av_bg = $danger ? '#FBEAEA' : '#FBF1DE'; $av_fg = $danger ? '#8F4444' : '#B07D2A'; $name = trim( $order->get_formatted_billing_full_name() ); $initials = orderguard_review_initials( $name ); $edit_url = $order->get_edit_order_url(); // Real risk reasons: the crossing counts, plus the cached // network signal when one is already known (no live fetch). $reasons = array(); if ( $nc >= $nc_threshold ) { $reasons[] = sprintf( /* translators: %d is a count of non-collected orders. */ __( '%d not collected', 'orderguard' ), $nc ); } if ( $rt >= orderguard_flag_returned_threshold() ) { $reasons[] = sprintf( /* translators: %d is a count of returned orders. */ __( '%d returned', 'orderguard' ), $rt ); } // TASK 50 -- say WHICH BOOKS the claim comes from. A merchant // deciding whether to hold a parcel reacts differently to // "four other shops" than to "you, twice", and the row is // where that distinction has to survive. if ( function_exists( 'orderguard_own_reason_text' ) ) { $own_txt = orderguard_own_reason_text( orderguard_own_flag_reason( $order ) ); if ( '' !== $own_txt ) { $reasons[] = $own_txt; } } if ( $manual_on && function_exists( 'orderguard_is_activated' ) && orderguard_is_activated() ) { $idn = orderguard_flag_identity_from_order( $order ); if ( '' !== $idn['email_hash'] || '' !== $idn['phone_hash'] ) { $sig = orderguard_get_transient( 'orderguard_netsig_' . md5( $idn['email_hash'] . '|' . $idn['phone_hash'] ) ); if ( is_array( $sig ) && ! empty( $sig['suspicious'] ) ) { $reasons[] = sprintf( /* translators: %d is the number of stores that flagged this customer. */ _n( 'flagged by %d store', 'flagged by %d stores', (int) $sig['suspicious'], 'orderguard' ), (int) $sig['suspicious'] ); } } } $pay_label = orderguard_is_cod_order( $order ) ? __( 'COD', 'orderguard' ) : $order->get_payment_method_title(); // The ORDER'S STATUS, in WooCommerce's own words. Whether a // parcel is still in play is the first thing a merchant needs // here, and stores differ on what 'completed' means to them -- // so state the fact and let the merchant read it. $orderguard_status_label = function_exists( 'wc_get_order_status_name' ) ? wc_get_order_status_name( $order->get_status() ) : $order->get_status(); $total_html = function_exists( 'wc_price' ) ? wc_price( (float) $order->get_total(), array( 'currency' => $order->get_currency() ) ) : esc_html( $order->get_total() ); $approve_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_review_approve&order_id=' . $order_id ), 'orderguard_reviewapprove_' . $order_id ); // Block COD = the existing orderguard_save_flag handler at level block_cod, // with a real reason matching what tripped the flag. $block_url = wp_nonce_url( admin_url( 'admin-post.php?action=orderguard_save_flag&order_id=' . $order_id . '&level=block_cod&reason=' . ( $danger ? 'never_collects' : 'serial_returner' ) ), 'orderguard_saveflag_' . $order_id ); ?>
·
· ·