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();
?>
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
);
?>