| 1 |
<?php //phpcs:disable |
| 2 |
|
| 3 |
/** |
| 4 |
* OrderQuery — focused queries for order data. |
| 5 |
* |
| 6 |
* @package Disco |
| 7 |
* @subpackage Disco\App\Analytics\Queries |
| 8 |
* @since 1.3.37 |
| 9 |
*/ |
| 10 |
|
| 11 |
namespace Disco\App\Analytics\Queries; |
| 12 |
|
| 13 |
/** |
| 14 |
* Handles all single-purpose SQL queries related to WooCommerce orders. |
| 15 |
* |
| 16 |
* Each method does exactly ONE job. |
| 17 |
* No business logic — raw DB results only. |
| 18 |
*/ |
| 19 |
class OrderQuery extends BaseQuery { |
| 20 |
|
| 21 |
// ========================================================================= |
| 22 |
// List / paginated queries (used by OrderService for REST list endpoints) |
| 23 |
// ========================================================================= |
| 24 |
|
| 25 |
/** |
| 26 |
* Returns a paginated list of campaign-linked orders. |
| 27 |
* |
| 28 |
* Builds its own WHERE clauses from $args. |
| 29 |
* Max per_page = 20 per spec. |
| 30 |
* |
| 31 |
* @param array $args campaign_id, product_id, customer_id, date_from, date_to, |
| 32 |
* orderby, order, page, per_page. |
| 33 |
* @return array { total: int, pages: int, rows: array } |
| 34 |
*/ |
| 35 |
public function get_order_list( array $args ): array { |
| 36 |
$context = $this->build_list_context( $args ); |
| 37 |
$pagination = $this->resolve_pagination( $args ); |
| 38 |
$sort = $this->resolve_order_sort( $args, $context ); |
| 39 |
|
| 40 |
$total = $this->run_count( $this->build_count_sql( $context ), $context['params'] ); |
| 41 |
|
| 42 |
if ( 0 === $total ) { |
| 43 |
return $this->format_list_result( 0, $pagination['per_page'], array() ); |
| 44 |
} |
| 45 |
|
| 46 |
$rows = $this->fetch_order_rows( $context, $sort, $pagination ); |
| 47 |
$rows = $this->resolve_order_amounts( $rows, $context['tables'] ); |
| 48 |
$rows = $this->resolve_order_customers( $rows, $context['tables'] ); |
| 49 |
|
| 50 |
return $this->format_list_result( $total, $pagination['per_page'], $rows ); |
| 51 |
} |
| 52 |
|
| 53 |
// ========================================================================= |
| 54 |
// Single-order queries |
| 55 |
// ========================================================================= |
| 56 |
|
| 57 |
/** |
| 58 |
* Returns a single order header row (no line items). |
| 59 |
* |
| 60 |
* @param int $order_id WooCommerce order ID. |
| 61 |
* @return object|null |
| 62 |
*/ |
| 63 |
public function get_order( int $order_id ) { |
| 64 |
$tables = $this->get_tables(); |
| 65 |
$clauses = $this->get_order_clauses( $tables, 'o', 'o' ); |
| 66 |
$discount_join = "LEFT JOIN {$tables['order_meta']} discount_meta ON discount_meta.order_id = o.ID AND discount_meta.meta_key = '_discount_total'"; |
| 67 |
|
| 68 |
$sql = "SELECT |
| 69 |
o.ID AS order_id, |
| 70 |
{$clauses['date_col']} AS order_date, |
| 71 |
{$clauses['status_col']} AS order_status, |
| 72 |
{$clauses['customer_expr']} AS customer_id, |
| 73 |
COALESCE(u.display_name, CONCAT(billing_address.first_name, ' ', billing_address.last_name)) AS customer_name, |
| 74 |
COALESCE(u.user_email, billing_address.email) AS customer_email, |
| 75 |
{$clauses['total_expr']} AS order_total, |
| 76 |
COALESCE(discount_meta.meta_value, 0) AS discount_amount |
| 77 |
FROM {$tables['orders']} o |
| 78 |
{$discount_join} |
| 79 |
LEFT JOIN {$tables['users']} u ON u.ID = {$clauses['customer_expr']} |
| 80 |
LEFT JOIN {$tables['order_addresses']} billing_address ON billing_address.order_id = o.ID AND billing_address.address_type = 'billing' |
| 81 |
WHERE o.ID = %d AND {$clauses['status_where']}"; |
| 82 |
|
| 83 |
return $this->run_row( $sql, array( $order_id ) ); |
| 84 |
} |
| 85 |
|
| 86 |
/** |
| 87 |
* Returns line items for a single order. |
| 88 |
* |
| 89 |
* @param int $order_id WooCommerce order ID. |
| 90 |
*/ |
| 91 |
public function get_line_items( int $order_id ): array { |
| 92 |
$tables = $this->get_tables(); |
| 93 |
|
| 94 |
$sql = "SELECT |
| 95 |
order_item.order_item_id AS item_id, |
| 96 |
item_product_meta.meta_value AS product_id, |
| 97 |
product.post_title AS product_name, |
| 98 |
item_qty_meta.meta_value AS qty, |
| 99 |
product_price_meta.meta_value AS unit_price, |
| 100 |
item_line_total_meta.meta_value AS line_total, |
| 101 |
item_subtotal_meta.meta_value AS line_subtotal |
| 102 |
FROM {$tables['order_items']} order_item |
| 103 |
JOIN {$tables['order_itemmeta']} item_product_meta ON item_product_meta.order_item_id = order_item.order_item_id AND item_product_meta.meta_key = '_product_id' |
| 104 |
JOIN {$tables['order_itemmeta']} item_qty_meta ON item_qty_meta.order_item_id = order_item.order_item_id AND item_qty_meta.meta_key = '_qty' |
| 105 |
JOIN {$tables['order_itemmeta']} item_line_total_meta ON item_line_total_meta.order_item_id = order_item.order_item_id AND item_line_total_meta.meta_key = '_line_total' |
| 106 |
JOIN {$tables['order_itemmeta']} item_subtotal_meta ON item_subtotal_meta.order_item_id = order_item.order_item_id AND item_subtotal_meta.meta_key = '_line_subtotal' |
| 107 |
LEFT JOIN {$tables['posts']} product ON product.ID = item_product_meta.meta_value |
| 108 |
LEFT JOIN {$tables['postmeta']} product_price_meta ON product_price_meta.post_id = product.ID AND product_price_meta.meta_key = '_regular_price' |
| 109 |
WHERE order_item.order_id = %d AND order_item.order_item_type = 'line_item'"; |
| 110 |
|
| 111 |
return $this->run_rows( $sql, array( $order_id ) ); |
| 112 |
} |
| 113 |
|
| 114 |
/** |
| 115 |
* Returns campaigns attached to a single order. |
| 116 |
* |
| 117 |
* @param int $order_id WooCommerce order ID. |
| 118 |
*/ |
| 119 |
public function get_campaigns_for_order( int $order_id ): array { |
| 120 |
$tables = $this->get_tables(); |
| 121 |
$order_id_column = $tables['order_id_col']; |
| 122 |
|
| 123 |
$dedup = $this->get_campaign_dedup_condition( $tables ); |
| 124 |
|
| 125 |
$sql = "SELECT |
| 126 |
CAST(order_meta.meta_value AS UNSIGNED) AS campaign_id, |
| 127 |
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(campaign.data, '$.name')), 'Unknown') AS campaign_name, |
| 128 |
COALESCE(campaign.intent, '') AS campaign_intent, |
| 129 |
CASE WHEN campaign.id IS NULL THEN 1 ELSE 0 END AS is_deleted |
| 130 |
FROM {$tables['order_meta']} order_meta |
| 131 |
LEFT JOIN {$tables['campaigns']} campaign ON campaign.id = CAST(order_meta.meta_value AS UNSIGNED) |
| 132 |
WHERE order_meta.{$order_id_column} = %d AND order_meta.meta_key = %s |
| 133 |
AND {$dedup}"; |
| 134 |
|
| 135 |
$rows = $this->run_rows( $sql, array( $order_id, 'disco_campaign' ) ); |
| 136 |
|
| 137 |
return array_map( |
| 138 |
function ( $row ) { |
| 139 |
return array( |
| 140 |
'campaign_id' => (int) $row->campaign_id, |
| 141 |
'campaign_name' => $row->campaign_name, |
| 142 |
'campaign_intent' => $row->campaign_intent, |
| 143 |
'is_deleted' => (bool) $row->is_deleted, |
| 144 |
); |
| 145 |
}, |
| 146 |
$rows |
| 147 |
); |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* Builds WHERE conditions, params, and filter JOINs for the order list. |
| 152 |
* |
| 153 |
* The product_id filter adds line-item JOINs; numeric search matches the |
| 154 |
* order ID, text search matches customer name/email via an extra users JOIN. |
| 155 |
* |
| 156 |
* @param array $args campaign_id, product_id, customer_id, date_from, date_to, search. |
| 157 |
* @return array { tables, clauses, id_col, where, params, joins } |
| 158 |
*/ |
| 159 |
private function build_list_context( array $args ): array { |
| 160 |
global $wpdb; |
| 161 |
|
| 162 |
$tables = $this->get_tables(); |
| 163 |
$clauses = $this->get_order_clauses( $tables ); |
| 164 |
$order_id_column = $tables['order_id_col']; |
| 165 |
$common = $this->build_common_conditions( $args, $clauses, $tables ); |
| 166 |
$conditions = array_merge( |
| 167 |
array( 'order_meta.meta_key = %s', $this->get_campaign_dedup_condition( $tables ) ), |
| 168 |
$common['conditions'] |
| 169 |
); |
| 170 |
|
| 171 |
// JOIN placeholders precede WHERE placeholders in the final SQL, so their |
| 172 |
// params must be collected separately and merged join-params-first. |
| 173 |
$where_params = array_merge( array( 'disco_campaign' ), $common['params'] ); |
| 174 |
$join_params = array(); |
| 175 |
$extra_joins = array(); |
| 176 |
|
| 177 |
if ( ! empty( $args['product_id'] ) ) { |
| 178 |
$extra_joins[] = "JOIN {$tables['order_items']} filter_order_item ON filter_order_item.order_id = order_meta.{$order_id_column} AND filter_order_item.order_item_type = 'line_item'"; |
| 179 |
$extra_joins[] = "JOIN {$tables['order_itemmeta']} filter_product_meta ON filter_product_meta.order_item_id = filter_order_item.order_item_id AND filter_product_meta.meta_key = '_product_id' AND filter_product_meta.meta_value = %d"; |
| 180 |
$join_params[] = (int) $args['product_id']; |
| 181 |
} |
| 182 |
|
| 183 |
$search = $args['search'] ?? ''; |
| 184 |
|
| 185 |
if ( is_numeric( $search ) && '' !== $search ) { |
| 186 |
$conditions[] = "order_meta.{$order_id_column} = %d"; |
| 187 |
$where_params[] = (int) $search; |
| 188 |
} elseif ( ! empty( $search ) ) { |
| 189 |
$extra_joins[] = "LEFT JOIN {$tables['users']} search_user ON search_user.ID = {$clauses['customer_expr']}"; |
| 190 |
$like = '%' . $wpdb->esc_like( $search ) . '%'; |
| 191 |
$conditions[] = '( LOWER(search_user.display_name) LIKE LOWER(%s) OR LOWER(search_user.user_email) LIKE LOWER(%s) )'; |
| 192 |
array_push( $where_params, $like, $like ); |
| 193 |
} |
| 194 |
|
| 195 |
return array( |
| 196 |
'tables' => $tables, |
| 197 |
'clauses' => $clauses, |
| 198 |
'id_col' => $order_id_column, |
| 199 |
'where' => 'WHERE ' . implode( ' AND ', $conditions ), |
| 200 |
'params' => array_merge( $join_params, $where_params ), |
| 201 |
'joins' => implode( ' ', $extra_joins ), |
| 202 |
); |
| 203 |
} |
| 204 |
|
| 205 |
/** |
| 206 |
* Resolves the SQL sort column expression, direction, and the JOINs the |
| 207 |
* sort expression needs. |
| 208 |
* |
| 209 |
* Whitelist: order_date, order_total, discount_amount, quantity. Totals/discounts |
| 210 |
* are cast to DECIMAL so sorting is numeric, not lexicographic. Quantity sums the |
| 211 |
* order's line-item quantities from wc_order_product_lookup. The order_stats, |
| 212 |
* discount-meta, and product-lookup JOINs are emitted only when the chosen sort |
| 213 |
* actually references them — display amounts are resolved separately in PHP. |
| 214 |
* |
| 215 |
* The product-lookup JOIN is safe against SUM inflation because the WHERE clause |
| 216 |
* already restricts order_meta to a single disco_campaign row per order via |
| 217 |
* get_campaign_dedup_condition(). |
| 218 |
* |
| 219 |
* @param array $args orderby, order. |
| 220 |
* @param array $context From build_list_context(). |
| 221 |
* @return array { column: string, direction: 'ASC'|'DESC', joins: string } |
| 222 |
*/ |
| 223 |
private function resolve_order_sort( array $args, array $context ): array { |
| 224 |
global $wpdb; |
| 225 |
|
| 226 |
$tables = $context['tables']; |
| 227 |
$clauses = $context['clauses']; |
| 228 |
$id_col = $context['id_col']; |
| 229 |
$sort = $this->resolve_sort( $args, array( 'order_date', 'order_total', 'discount_amount', 'quantity' ), 'order_date' ); |
| 230 |
|
| 231 |
$orderby_column_map = array( |
| 232 |
'order_date' => $clauses['date_col'], |
| 233 |
'order_total' => "CAST(({$clauses['total_expr']} - COALESCE(order_stats.shipping_total, 0)) AS DECIMAL(10,2))", |
| 234 |
'discount_amount' => "CAST({$clauses['discount_expr']} AS DECIMAL(10,2))", |
| 235 |
'quantity' => 'COALESCE(SUM(sort_qty_lookup.product_qty), 0)', |
| 236 |
); |
| 237 |
|
| 238 |
$sort_join_map = array( |
| 239 |
'order_date' => '', |
| 240 |
'order_total' => "LEFT JOIN {$wpdb->prefix}wc_order_stats order_stats ON order_stats.order_id = order_meta.{$id_col}", |
| 241 |
'discount_amount' => $clauses['discount_join'], |
| 242 |
'quantity' => "LEFT JOIN {$tables['product_lookup']} sort_qty_lookup ON sort_qty_lookup.order_id = order_meta.{$id_col}", |
| 243 |
); |
| 244 |
|
| 245 |
return array( |
| 246 |
'column' => $orderby_column_map[ $sort['orderby'] ], |
| 247 |
'direction' => $sort['direction'], |
| 248 |
'joins' => $sort_join_map[ $sort['orderby'] ], |
| 249 |
); |
| 250 |
} |
| 251 |
|
| 252 |
/** |
| 253 |
* Builds the COUNT(DISTINCT order) SQL for the current filters. |
| 254 |
* |
| 255 |
* @param array $context From build_list_context(). |
| 256 |
* @return string COUNT SQL with placeholders matching context params. |
| 257 |
*/ |
| 258 |
private function build_count_sql( array $context ): string { |
| 259 |
$tables = $context['tables']; |
| 260 |
$clauses = $context['clauses']; |
| 261 |
|
| 262 |
return "SELECT COUNT(DISTINCT order_meta.{$context['id_col']}) FROM {$tables['order_meta']} order_meta JOIN {$tables['orders']} o ON o.ID = order_meta.{$context['id_col']} AND {$clauses['status_where']} {$clauses['customer_join']} {$context['joins']} {$context['where']}"; |
| 263 |
} |
| 264 |
|
| 265 |
/** |
| 266 |
* Fetches the paginated order rows — order scalars only. |
| 267 |
* |
| 268 |
* Customer identity, totals, discount, and item counts are resolved |
| 269 |
* afterwards in resolve_order_amounts() / resolve_order_customers(), so |
| 270 |
* the only JOINs left are the anchor, the orders table, any filter JOINs, |
| 271 |
* and the JOIN the active sort expression needs. |
| 272 |
* |
| 273 |
* @param array $context From build_list_context(). |
| 274 |
* @param array $sort From resolve_order_sort(). |
| 275 |
* @param array $pagination From resolve_pagination(). |
| 276 |
* @return array Order row objects. |
| 277 |
*/ |
| 278 |
private function fetch_order_rows( array $context, array $sort, array $pagination ): array { |
| 279 |
$tables = $context['tables']; |
| 280 |
$clauses = $context['clauses']; |
| 281 |
$order_id_column = $context['id_col']; |
| 282 |
|
| 283 |
$rows_sql = "SELECT |
| 284 |
order_meta.{$order_id_column} AS order_id, |
| 285 |
{$clauses['date_col']} AS order_date, |
| 286 |
{$clauses['status_col']} AS order_status, |
| 287 |
{$clauses['customer_expr']} AS customer_id, |
| 288 |
{$clauses['total_expr']} AS total_amount |
| 289 |
FROM {$tables['order_meta']} order_meta |
| 290 |
JOIN {$tables['orders']} o ON o.ID = order_meta.{$order_id_column} AND {$clauses['status_where']} |
| 291 |
{$clauses['total_join']} |
| 292 |
{$clauses['customer_join']} |
| 293 |
{$sort['joins']} |
| 294 |
{$context['joins']} |
| 295 |
{$context['where']} |
| 296 |
GROUP BY order_meta.{$order_id_column} |
| 297 |
ORDER BY {$sort['column']} {$sort['direction']} |
| 298 |
LIMIT %d OFFSET %d"; |
| 299 |
|
| 300 |
$params = array_merge( $context['params'], array( $pagination['per_page'], $pagination['offset'] ) ); |
| 301 |
|
| 302 |
return $this->run_rows( $rows_sql, $params ); |
| 303 |
} |
| 304 |
|
| 305 |
/** |
| 306 |
* Resolves order_total, discount_amount, and items_count via three batch lookups. |
| 307 |
* |
| 308 |
* Replaces the order_stats, discount-meta, and order_items LEFT JOINs that |
| 309 |
* were previously inline in the SQL: order_total = total_amount minus the |
| 310 |
* order's shipping_total, discount defaults to 0, items_count counts the |
| 311 |
* order's line items. |
| 312 |
* |
| 313 |
* @param array $rows Raw query result rows (stdClass objects). |
| 314 |
* @param array $tables Table name map from get_tables(). |
| 315 |
* @return array Rows with order_total, discount_amount, items_count set. |
| 316 |
*/ |
| 317 |
private function resolve_order_amounts( array $rows, array $tables ): array { |
| 318 |
$order_ids = array_map( |
| 319 |
function ( $row ) { |
| 320 |
return (int) $row->order_id; |
| 321 |
}, |
| 322 |
$rows |
| 323 |
); |
| 324 |
|
| 325 |
if ( empty( $order_ids ) ) { |
| 326 |
return $rows; |
| 327 |
} |
| 328 |
|
| 329 |
$shipping_by_oid = $this->fetch_shipping_totals( $order_ids, $tables ); |
| 330 |
$discount_by_oid = $this->fetch_discount_totals( $order_ids, $tables ); |
| 331 |
$items_by_oid = $this->fetch_item_counts( $order_ids, $tables ); |
| 332 |
|
| 333 |
foreach ( $rows as &$row ) { |
| 334 |
$order_id = (int) $row->order_id; |
| 335 |
|
| 336 |
$row->order_total = (float) $row->total_amount - ( $shipping_by_oid[ $order_id ] ?? 0.0 ); |
| 337 |
$row->discount_amount = $discount_by_oid[ $order_id ] ?? 0; |
| 338 |
$row->items_count = $items_by_oid[ $order_id ] ?? 0; |
| 339 |
} |
| 340 |
|
| 341 |
unset( $row ); |
| 342 |
|
| 343 |
return $rows; |
| 344 |
} |
| 345 |
|
| 346 |
/** |
| 347 |
* Fetches shipping_total from wc_order_stats for a set of order IDs in one query. |
| 348 |
* |
| 349 |
* @param array<int> $order_ids Order IDs. |
| 350 |
* @param array $tables Table name map from get_tables(). |
| 351 |
* @return array Map of order_id => shipping total (float). |
| 352 |
*/ |
| 353 |
private function fetch_shipping_totals( array $order_ids, array $tables ): array { |
| 354 |
global $wpdb; |
| 355 |
|
| 356 |
$placeholders = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) ); |
| 357 |
|
| 358 |
$stat_rows = $this->run_rows( |
| 359 |
"SELECT order_id, shipping_total FROM {$wpdb->prefix}wc_order_stats WHERE order_id IN ({$placeholders})", |
| 360 |
$order_ids |
| 361 |
); |
| 362 |
|
| 363 |
$shipping_by_oid = array(); |
| 364 |
|
| 365 |
foreach ( $stat_rows as $stat_row ) { |
| 366 |
$shipping_by_oid[ (int) $stat_row->order_id ] = (float) $stat_row->shipping_total; |
| 367 |
} |
| 368 |
|
| 369 |
return $shipping_by_oid; |
| 370 |
} |
| 371 |
|
| 372 |
/** |
| 373 |
* Fetches _discount_total order meta for a set of order IDs in one query. |
| 374 |
* |
| 375 |
* @param array<int> $order_ids Order IDs. |
| 376 |
* @param array $tables Table name map from get_tables(). |
| 377 |
* @return array Map of order_id => discount meta value (string). |
| 378 |
*/ |
| 379 |
private function fetch_discount_totals( array $order_ids, array $tables ): array { |
| 380 |
$order_id_column = $tables['order_id_col']; |
| 381 |
$placeholders = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) ); |
| 382 |
|
| 383 |
$discount_rows = $this->run_rows( |
| 384 |
"SELECT {$order_id_column} AS order_id, meta_value FROM {$tables['order_meta']} WHERE {$order_id_column} IN ({$placeholders}) AND meta_key = '_discount_total'", |
| 385 |
$order_ids |
| 386 |
); |
| 387 |
|
| 388 |
$discount_by_oid = array(); |
| 389 |
|
| 390 |
foreach ( $discount_rows as $discount_row ) { |
| 391 |
$discount_by_oid[ (int) $discount_row->order_id ] = $discount_row->meta_value; |
| 392 |
} |
| 393 |
|
| 394 |
return $discount_by_oid; |
| 395 |
} |
| 396 |
|
| 397 |
/** |
| 398 |
* Fetches line-item counts for a set of order IDs in one query. |
| 399 |
* |
| 400 |
* @param array<int> $order_ids Order IDs. |
| 401 |
* @param array $tables Table name map from get_tables(). |
| 402 |
* @return array Map of order_id => line item count (int). |
| 403 |
*/ |
| 404 |
private function fetch_item_counts( array $order_ids, array $tables ): array { |
| 405 |
$placeholders = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) ); |
| 406 |
|
| 407 |
$item_rows = $this->run_rows( |
| 408 |
"SELECT order_id, COUNT(*) AS items_count FROM {$tables['order_items']} WHERE order_id IN ({$placeholders}) AND order_item_type = 'line_item' GROUP BY order_id", |
| 409 |
$order_ids |
| 410 |
); |
| 411 |
|
| 412 |
$items_by_oid = array(); |
| 413 |
|
| 414 |
foreach ( $item_rows as $item_row ) { |
| 415 |
$items_by_oid[ (int) $item_row->order_id ] = (int) $item_row->items_count; |
| 416 |
} |
| 417 |
|
| 418 |
return $items_by_oid; |
| 419 |
} |
| 420 |
|
| 421 |
/** |
| 422 |
* Resolves customer_name / customer_email via two batch lookups. |
| 423 |
* |
| 424 |
* Replaces the users and order_addresses LEFT JOINs that were previously |
| 425 |
* inline in the SQL. Mirrors the old COALESCE semantics: account fields |
| 426 |
* win, the order's own billing address is the fallback; CONCAT of |
| 427 |
* first/last name is null when either part is null. |
| 428 |
* |
| 429 |
* @param array $rows Raw query result rows (stdClass objects). |
| 430 |
* @param array $tables Table name map from get_tables(). |
| 431 |
* @return array Rows with customer_name and customer_email set. |
| 432 |
*/ |
| 433 |
private function resolve_order_customers( array $rows, array $tables ): array { |
| 434 |
$user_ids = array(); |
| 435 |
$order_ids = array(); |
| 436 |
|
| 437 |
foreach ( $rows as $row ) { |
| 438 |
if ( (int) $row->customer_id > 0 ) { |
| 439 |
$user_ids[ (int) $row->customer_id ] = true; |
| 440 |
} |
| 441 |
|
| 442 |
$order_ids[ (int) $row->order_id ] = true; |
| 443 |
} |
| 444 |
|
| 445 |
$user_by_id = $this->fetch_users( array_keys( $user_ids ), $tables ); |
| 446 |
$address_by_oid = $this->fetch_billing_addresses( array_keys( $order_ids ), $tables ); |
| 447 |
|
| 448 |
foreach ( $rows as &$row ) { |
| 449 |
$user = $user_by_id[ (int) $row->customer_id ] ?? null; |
| 450 |
$address = $address_by_oid[ (int) $row->order_id ] ?? null; |
| 451 |
|
| 452 |
$billing_name = null; |
| 453 |
|
| 454 |
if ( $address && null !== $address->first_name && null !== $address->last_name ) { |
| 455 |
$billing_name = $address->first_name . ' ' . $address->last_name; |
| 456 |
} |
| 457 |
|
| 458 |
$row->customer_name = $user->display_name ?? $billing_name; |
| 459 |
$row->customer_email = $user->user_email ?? ( $address->email ?? null ); |
| 460 |
} |
| 461 |
|
| 462 |
unset( $row ); |
| 463 |
|
| 464 |
return $rows; |
| 465 |
} |
| 466 |
|
| 467 |
} |
| 468 |
|