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 / Services / CustomerService.php

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

235 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php //phpcs:ignore
2
3 /**
4 * CustomerService — composes customer queries into service-level results.
5 *
6 * @package Disco
7 * @subpackage Disco\App\Analytics\Services
8 * @since 1.3.37
9 */
10
11 namespace Disco\App\Analytics\Services;
12
13 use Disco\App\Analytics\Queries\CustomerQuery;
14
15 /**
16 * Provides customer analytics at the service layer.
17 *
18 * Used by the REST controller for the customers list and customer-orders endpoints.
19 * No SQL lives here — all DB work is delegated to CustomerQuery / OrderQuery.
20 */
21 class CustomerService extends BaseService {
22
23 /**
24 * Returns a paginated customer table with current/compare periods and inline campaigns.
25 *
26 * Only completed orders with disco_campaign meta are included.
27 * Default sort: total_spent ASC.
28 *
29 * @param array $args date_from, date_to, campaign_id, order_id, page, limit.
30 * @return array { current_period, compare_period, pagination, data }
31 */
32 public static function get_customers_table( array $args ): array {
33 $limit = min( absint( $args['limit'] ?? 10 ), 100 );
34 $page = max( 1, absint( $args['page'] ?? 1 ) );
35
36 $date_to = $args['date_to'] ?? '';
37 $date_from = $args['date_from'] ?? '';
38
39 $query = new CustomerQuery;
40 $result = $query->get_customers_for_table( self::build_table_query_args( $args, $limit, $page ) );
41
42 $data = array();
43
44 foreach ( $result['rows'] as $row ) {
45 $data[] = self::format_customer_row( $row );
46 }
47
48 return array(
49 'current_period' => array(
50 'from' => $date_from,
51 'to' => $date_to,
52 ),
53 'compare_period' => self::resolve_compare_period( $date_from, $date_to ),
54 'pagination' => self::build_pagination( $result, $page, $limit ),
55 'data' => $data,
56 );
57 }
58
59 /**
60 * Returns detail for a single customer including aggregated metrics and inline campaigns.
61 *
62 * @param int $customer_id WP User ID.
63 * @param array $args date_from, date_to.
64 * @return array { current_period, compare_period, data }
65 */
66 public static function get_customer( int $customer_id, array $args ): array {
67 $date_from = $args['date_from'] ?? '';
68 $date_to = $args['date_to'] ?? '';
69
70 $query = new CustomerQuery;
71 $result = $query->get_customers_for_table(
72 array(
73 'date_from' => $date_from,
74 'date_to' => $date_to,
75 'user_id' => $customer_id,
76 'per_page' => 1,
77 'page' => 1,
78 'status' => 'wc-completed',
79 )
80 );
81
82 $data = null;
83
84 if ( ! empty( $result['rows'] ) ) {
85 $data = self::format_customer_row( $result['rows'][0] );
86 }
87
88 return array(
89 'current_period' => array(
90 'from' => $date_from,
91 'to' => $date_to,
92 ),
93 'compare_period' => self::resolve_compare_period( $date_from, $date_to ),
94 'data' => $data,
95 );
96 }
97
98 /**
99 * Maps service-level $args onto the argument array CustomerQuery expects.
100 *
101 * Whitelists orderby, normalizes sort direction, casts filter IDs.
102 *
103 * @param array $args Raw service args.
104 * @param int $limit Sanitized per-page limit.
105 * @param int $page Sanitized page number.
106 * @return array Query args for CustomerQuery::get_customers_for_table().
107 */
108 private static function build_table_query_args( array $args, int $limit, int $page ): array {
109 $orderby_map = array(
110 'total_spent' => 'total_spent',
111 'orders' => 'orders_count',
112 );
113 $orderby = $orderby_map[ $args['orderby'] ?? 'total_spent' ] ?? 'total_spent';
114 $order = 'ASC';
115
116 if ( strtolower( $args['order'] ?? 'asc' ) === 'desc' ) {
117 $order = 'DESC';
118 }
119
120 return array(
121 'date_from' => $args['date_from'] ?? '',
122 'date_to' => $args['date_to'] ?? '',
123 'search' => $args['search'] ?? '',
124 'campaign_id' => ! empty( $args['campaign_id'] ) ? (int) $args['campaign_id'] : 0,
125 'order_id' => ! empty( $args['order_id'] ) ? (int) $args['order_id'] : 0,
126 'orderby' => $orderby,
127 'order' => $order,
128 'per_page' => $limit,
129 'page' => $page,
130 'status' => 'wc-completed',
131 );
132 }
133
134 /**
135 * Formats one raw query row into the API response shape.
136 *
137 * Parses inline campaigns, resolves the avatar, casts and rounds metrics.
138 *
139 * @param object $row Raw row from CustomerQuery::get_customers_for_table().
140 * @return array Formatted customer row.
141 * @phpstan-param object{customer_id: int|string, customer_name: string|null, customer_email: string|null, user_login: string|null, billing_state: string|null, campaigns_raw: string|null, orders_count: int|string, total_spent: string|null} $row
142 */
143 private static function format_customer_row( object $row ): array {
144 $customer_id = (int) $row->customer_id;
145 $email = $row->customer_email ?? '';
146
147 return array(
148 'id' => $customer_id,
149 'name' => $row->customer_name ?? '',
150 'email' => $email,
151 'avatar' => self::resolve_avatar( $customer_id, $email ),
152 'user_name' => $row->user_login ?? '',
153 'state' => $row->billing_state ?? '',
154 'campaigns' => self::parse_campaigns( (string) ( $row->campaigns_raw ?? '' ) ),
155 'orders' => (int) $row->orders_count,
156 'total_spent' => round( (float) $row->total_spent, 2 ),
157 );
158 }
159
160 /**
161 * Parses the "id::name::intent||..." campaigns_raw string into structured campaigns.
162 *
163 * @param string $raw Raw concatenated string from the query layer.
164 * @return array<int, array{id: int, name: string, intent: string}>
165 */
166 private static function parse_campaigns( string $raw ): array {
167 if ( '' === $raw ) {
168 return array();
169 }
170
171 $campaigns = array();
172
173 foreach ( explode( '||', $raw ) as $entry ) {
174 [ $id_str, $name, $intent ] = explode( '::', $entry, 3 );
175
176 $campaigns[] = array(
177 'id' => (int) $id_str,
178 'name' => $name,
179 'intent' => $intent,
180 );
181 }
182
183 return $campaigns;
184 }
185
186 /**
187 * Returns the Gravatar URL only when the user has a real avatar set.
188 *
189 * Probes Gravatar with d=404 so the request 404s when no custom avatar exists.
190 * Result is transient-cached per email hash for 24 hours to avoid repeat HTTP calls.
191 *
192 * @param int $customer_id WP user ID (0 for guests).
193 * @param string $email Billing/account email used for Gravatar hash.
194 * @return string Avatar URL, or empty string if no avatar is set.
195 */
196 private static function resolve_avatar( int $customer_id, string $email ): string {
197 if ( $customer_id > 0 ) {
198 $user = get_userdata( $customer_id );
199
200 if ( $user ) {
201 $email = $user->user_email;
202 }
203 }
204
205 if ( ! $email ) {
206 return '';
207 }
208
209 $hash = md5( strtolower( trim( $email ) ) );
210 $cache_key = 'disco_av_' . $hash;
211 $cached = get_transient( $cache_key );
212
213 if ( false !== $cached ) {
214 if ( is_string( $cached ) ) {
215 return $cached;
216 }
217
218 return '';
219 }
220
221 $response = wp_remote_head( "https://www.gravatar.com/avatar/{$hash}?d=404&r=pg", array( 'timeout' => 2 ) );
222
223 $url = '';
224
225 if ( ! is_wp_error( $response ) && 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
226 $url = "https://www.gravatar.com/avatar/{$hash}?s=80&r=pg";
227 }
228
229 set_transient( $cache_key, $url, DAY_IN_SECONDS );
230
231 return $url;
232 }
233
234 }
235