PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.14
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.14
1.4.14 1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 1.3.46 1.3.45 All 177 releases
disco / app / Analytics / Queries / CustomerQuery.php

CustomerQuery.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.14, at app/Analytics/Queries/CustomerQuery.php

276 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * CustomerQuery — focused queries for customer analytics 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 customers.
15 *
16 * Each method does exactly ONE job.
17 * No business logic — raw DB results only.
18 */
19 class CustomerQuery extends BaseQuery {
20
21 /**
22 * Returns a paginated list of customers for the analytics table view.
23 *
24 * Filters to completed orders with disco_campaign meta.
25 * Campaigns are returned inline via GROUP_CONCAT (id::name::intent).
26 *
27 * @param array $args date_from, date_to, campaign_id, order_id, per_page, page.
28 * @return array { total: int, pages: int, rows: array }
29 */
30 public function get_customers_for_table( array $args ): array {
31 $context = $this->build_list_context( $args );
32 $pagination = $this->resolve_pagination( $args );
33 $sort = $this->resolve_sort( $args, array( 'total_spent', 'orders_count' ), 'total_spent' );
34
35 $total = $this->run_count( $this->build_count_sql( $context ), $context['params'] );
36
37 if ( 0 === $total ) {
38 return $this->format_list_result( 0, $pagination['per_page'], array() );
39 }
40
41 $rows = $this->fetch_customer_rows( $context, $sort, $pagination );
42 $rows = $this->resolve_customer_identities( $rows, $context['tables'] );
43 $rows = $this->resolve_campaign_names( $rows, $context['tables'] );
44
45 return $this->format_list_result( $total, $pagination['per_page'], $rows );
46 }
47
48 /**
49 * Builds WHERE conditions and params for the customer list filters.
50 *
51 * Numeric search matches the user ID; text search matches account
52 * name/email and billing name/email. The users / order_addresses JOINs are
53 * only emitted (via search_joins) when a search term needs them — identity
54 * display fields are resolved separately in resolve_customer_identities().
55 *
56 * Also exposes customer_key: registered customers group by user ID,
57 * guests group by billing email.
58 *
59 * @param array $args date_from, date_to, campaign_id, order_id, user_id, search.
60 * @return array { tables, clauses, id_col, where, params, customer_key, search_joins }
61 */
62 private function build_list_context( array $args ): array {
63 global $wpdb;
64
65 $tables = $this->get_tables();
66 $clauses = $this->get_order_clauses( $tables );
67 $common = $this->build_common_conditions( $args, $clauses, $tables );
68 $conditions = array_merge(
69 array( 'order_meta.meta_key = %s', $this->get_campaign_dedup_condition( $tables ) ),
70 $common['conditions']
71 );
72 $params = array_merge( array( 'disco_campaign' ), $common['params'] );
73
74 $search = $args['search'] ?? '';
75 $order_id_column = $tables['order_id_col'];
76 $users_join = "LEFT JOIN {$tables['users']} u ON u.ID = {$clauses['customer_expr']}";
77 $address_join = "LEFT JOIN {$tables['order_addresses']} billing_address ON billing_address.order_id = order_meta.{$order_id_column} AND billing_address.address_type = 'billing'";
78 $search_joins = '';
79
80 if ( is_numeric( $search ) && '' !== $search ) {
81 $search_joins = $users_join;
82 $conditions[] = 'u.ID = %d';
83 $params[] = (int) $search;
84 } elseif ( ! empty( $search ) ) {
85 $search_joins = $users_join . ' ' . $address_join;
86 $like = '%' . $wpdb->esc_like( $search ) . '%';
87 $conditions[] = '( LOWER(u.display_name) LIKE LOWER(%s) OR LOWER(u.user_email) LIKE LOWER(%s) OR LOWER(CONCAT(billing_address.first_name, \' \', billing_address.last_name)) LIKE LOWER(%s) OR LOWER(billing_address.email) LIKE LOWER(%s) )';
88 $params[] = $like;
89 $params[] = $like;
90 $params[] = $like;
91 $params[] = $like;
92 }
93
94 return array(
95 'tables' => $tables,
96 'clauses' => $clauses,
97 'id_col' => $order_id_column,
98 'where' => 'WHERE ' . implode( ' AND ', $conditions ),
99 'params' => $params,
100 'customer_key' => "CASE WHEN {$clauses['customer_expr']} != 0 THEN CAST({$clauses['customer_expr']} AS CHAR) ELSE o.billing_email END",
101 'search_joins' => $search_joins,
102 );
103 }
104
105 /**
106 * Builds the COUNT(DISTINCT customer) SQL for the current filters.
107 *
108 * @param array $context From build_list_context().
109 * @return string COUNT SQL with placeholders matching context params.
110 */
111 private function build_count_sql( array $context ): string {
112 $tables = $context['tables'];
113 $clauses = $context['clauses'];
114
115 return "SELECT COUNT(DISTINCT {$context['customer_key']})
116 FROM {$tables['order_meta']} order_meta
117 JOIN {$tables['orders']} o ON o.ID = order_meta.{$context['id_col']} AND {$clauses['status_where']}
118 {$clauses['customer_join']}
119 {$context['search_joins']}
120 {$context['where']}";
121 }
122
123 /**
124 * Fetches the paginated per-customer aggregate rows.
125 *
126 * Pure aggregates only — identity display fields (name, email, login, state)
127 * are resolved afterwards in resolve_customer_identities(), so the users and
128 * order_addresses tables are joined only when a search term filters on them.
129 * sample_order_id carries one order per customer for the address lookup.
130 *
131 * Campaign IDs are concatenated raw; names resolved later in PHP.
132 *
133 * @param array $context From build_list_context().
134 * @param array $sort From resolve_sort().
135 * @param array $pagination From resolve_pagination().
136 * @return array Customer row objects.
137 */
138 private function fetch_customer_rows( array $context, array $sort, array $pagination ): array {
139 $tables = $context['tables'];
140 $clauses = $context['clauses'];
141 $order_id_column = $context['id_col'];
142
143 $rows_sql = "SELECT
144 {$clauses['customer_expr']} AS customer_id,
145 MAX(order_meta.{$order_id_column}) AS sample_order_id,
146 COUNT(DISTINCT order_meta.{$order_id_column}) AS orders_count,
147 SUM({$clauses['total_expr']}) AS total_spent,
148 GROUP_CONCAT(
149 DISTINCT CAST(order_meta.meta_value AS UNSIGNED)
150 ORDER BY CAST(order_meta.meta_value AS UNSIGNED)
151 SEPARATOR '||'
152 ) AS campaign_ids_raw
153 FROM {$tables['order_meta']} order_meta
154 JOIN {$tables['orders']} o ON o.ID = order_meta.{$order_id_column} AND {$clauses['status_where']}
155 {$clauses['total_join']}
156 {$clauses['customer_join']}
157 {$context['search_joins']}
158 {$context['where']}
159 GROUP BY {$context['customer_key']}
160 ORDER BY {$sort['orderby']} {$sort['direction']}
161 LIMIT %d OFFSET %d";
162
163 $params = array_merge( $context['params'], array( $pagination['per_page'], $pagination['offset'] ) );
164
165 return $this->run_rows( $rows_sql, $params );
166 }
167
168 /**
169 * Resolves name / email / login / state for each row via two batch lookups.
170 *
171 * Replaces the unconditional users and order_addresses LEFT JOINs that were
172 * previously inline in the SQL. Mirrors the old COALESCE semantics: account
173 * fields win, billing address (from the customer's sampled order) is the
174 * fallback; CONCAT of first/last name is null when either part is null.
175 *
176 * @param array $rows Raw query result rows (stdClass objects).
177 * @param array $tables Table name map from get_tables().
178 * @return array Rows with customer_name, customer_email, user_login, billing_state set.
179 */
180 private function resolve_customer_identities( array $rows, array $tables ): array {
181 $user_ids = array();
182 $order_ids = array();
183
184 foreach ( $rows as $row ) {
185 if ( (int) $row->customer_id > 0 ) {
186 $user_ids[ (int) $row->customer_id ] = true;
187 }
188
189 $order_ids[ (int) $row->sample_order_id ] = true;
190 }
191
192 $user_by_id = $this->fetch_users( array_keys( $user_ids ), $tables );
193 $address_by_oid = $this->fetch_billing_addresses( array_keys( $order_ids ), $tables );
194
195 foreach ( $rows as &$row ) {
196 $user = $user_by_id[ (int) $row->customer_id ] ?? null;
197 $address = $address_by_oid[ (int) $row->sample_order_id ] ?? null;
198
199 $billing_name = null;
200
201 if ( $address && null !== $address->first_name && null !== $address->last_name ) {
202 $billing_name = $address->first_name . ' ' . $address->last_name;
203 }
204
205 $row->customer_name = $user->display_name ?? $billing_name;
206 $row->customer_email = $user->user_email ?? ( $address->email ?? null );
207 $row->user_login = $user->user_login ?? null;
208 $row->billing_state = $address->state ?? null;
209 }
210
211 unset( $row );
212
213 return $rows;
214 }
215
216
217 /**
218 * Resolves campaigns_raw for each row via a single batch_campaign_meta() call.
219 *
220 * Replaces LEFT JOIN campaigns + JSON_EXTRACT inside GROUP_CONCAT which executed
221 * once per order row during the GROUP BY scan. Rebuilds "id::name::intent||..." format
222 * expected by CustomerService.
223 *
224 * @param array $rows Raw query result rows (stdClass objects with campaign_ids_raw).
225 * @param array $tables Table name map from get_tables().
226 * @return array Rows with campaigns_raw set on every item.
227 */
228 // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
229 private function resolve_campaign_names( array $rows, array $tables ): array {
230 $all_ids = array();
231
232 foreach ( $rows as $row ) {
233 if ( empty( $row->campaign_ids_raw ) ) {
234 continue;
235 }
236
237 foreach ( explode( '||', $row->campaign_ids_raw ) as $id ) {
238 $id = (int) $id;
239
240 if ( $id <= 0 ) {
241 continue;
242 }
243
244 $all_ids[ $id ] = true;
245 }
246 }
247
248 $campaign_map = $this->batch_campaign_meta( array_keys( $all_ids ), $tables );
249
250 foreach ( $rows as &$row ) {
251 if ( empty( $row->campaign_ids_raw ) ) {
252 $row->campaigns_raw = '';
253
254 continue;
255 }
256
257 $parts = array();
258
259 foreach ( explode( '||', $row->campaign_ids_raw ) as $id ) {
260 $id = (int) $id;
261 $name = $campaign_map[ $id ]['name'] ?? 'Unknown';
262 $intent = $campaign_map[ $id ]['intent'] ?? 'Unknown';
263 $parts[] = $id . '::' . $name . '::' . $intent;
264 }
265
266 // @phpstan-ignore-next-line
267 $row->campaigns_raw = implode( '||', $parts );
268 }
269
270 unset( $row );
271
272 return $rows;
273 }
274
275 }
276