PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.0
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.0
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / third-party / ecommerce / woocommerce / class-avcf-wc-customers.php
atarim-visual-collaboration / third-party / ecommerce / woocommerce Last commit date
class-avcf-wc-abilities.php 1 month ago class-avcf-wc-attributes.php 1 month ago class-avcf-wc-catalog.php 1 month ago class-avcf-wc-checkout-email.php 1 month ago class-avcf-wc-coupons.php 1 month ago class-avcf-wc-customers.php 1 month ago class-avcf-wc-detector.php 1 month ago class-avcf-wc-orders.php 1 month ago class-avcf-wc-product-relations.php 1 month ago class-avcf-wc-products.php 1 month ago class-avcf-wc-shipping.php 1 month ago class-avcf-wc-tax.php 1 month ago class-avcf-wc-variations.php 1 month ago
class-avcf-wc-customers.php
538 lines
1 <?php
2 /**
3 * WooCommerce — Customers MCP abilities.
4 *
5 * Read access to registered customers (users with the customer role) and
6 * guest customers (unique billing emails across orders). For mutation,
7 * exposes default billing and shipping address writes — the most common
8 * customer-level edit. General user CRUD is in the users cluster
9 * (atarim/create-user / update-user / delete-user / change-user-role);
10 * a "customer" in WooCommerce is just a user with the customer role.
11 *
12 * Exposed abilities:
13 * atarim/list-customers Registered customers with stats; optionally include guest emails from orders.
14 * atarim/get-customer Single customer by ID or email.
15 * atarim/update-customer-billing Update default billing address fields.
16 * atarim/update-customer-shipping Update default shipping address fields.
17 *
18 * @package atarim-visual-collaboration
19 */
20
21 if ( ! defined('ABSPATH') ) {
22 exit;
23 }
24
25 class AVCF_WC_Customers extends AVCF_Abilities_Base {
26
27 /**
28 * @var AVCF_WC_Detector
29 */
30 private $detector;
31
32 /**
33 * Writeable billing fields (WooCommerce convention).
34 */
35 private $billing_fields = [
36 'first_name', 'last_name', 'company',
37 'address_1', 'address_2', 'city', 'state', 'postcode', 'country',
38 'email', 'phone',
39 ];
40
41 /**
42 * Writeable shipping fields (no email — that's customer-level).
43 */
44 private $shipping_fields = [
45 'first_name', 'last_name', 'company',
46 'address_1', 'address_2', 'city', 'state', 'postcode', 'country',
47 'phone',
48 ];
49
50 public function __construct() {
51 $this->detector = new AVCF_WC_Detector();
52 }
53
54 public function register() {
55 if ( ! $this->detector->avcf_wc_is_available() ) {
56 return;
57 }
58
59 // ---- list-customers ----
60 wp_register_ability( 'atarim/list-customers', [
61 'label' => 'List Customers',
62 'description' => 'Returns registered customers (users with the customer role) with per-customer stats: order count, total spent, last order date. Use include_guests: true to also surface unique billing emails from orders that have no associated user account (guest checkouts). Filters: search across email/name, registered date range, total-spent range, has-orders flag.',
63 'category' => 'atarim',
64 'input_schema' => [
65 'type' => 'object',
66 'properties' => [
67 'search' => [ 'type' => 'string', 'description' => 'Match login / email / display name.' ],
68 'registered_after' => [ 'type' => 'string', 'description' => 'Only customers registered on or after this date.' ],
69 'registered_before' => [ 'type' => 'string' ],
70 'min_total_spent' => [ 'type' => 'number', 'description' => 'Minimum lifetime spend.' ],
71 'has_orders' => [ 'type' => 'boolean', 'description' => 'Only customers with at least one order.' ],
72 'orderby' => [ 'type' => 'string', 'enum' => [ 'ID', 'registered', 'total_spent', 'order_count' ], 'default' => 'registered' ],
73 'order' => [ 'type' => 'string', 'enum' => [ 'ASC', 'DESC' ], 'default' => 'DESC' ],
74 'limit' => [ 'type' => 'integer', 'minimum' => -1, 'default' => 50 ],
75 'offset' => [ 'type' => 'integer', 'minimum' => 0, 'default' => 0 ],
76 'include_guests' => [
77 'type' => 'boolean',
78 'description' => 'In addition to registered customers, return unique billing emails from orders that don\'t belong to a registered user (guest checkouts). Default false. Guests are returned with id=0 and a "guest" flag.',
79 'default' => false,
80 ],
81 ],
82 'additionalProperties' => false,
83 ],
84 'output_schema' => [
85 'type' => 'object',
86 'properties' => [
87 'total' => [ 'type' => 'integer' ],
88 'returned' => [ 'type' => 'integer' ],
89 'customers' => [ 'type' => 'array' ],
90 'guest_count' => [ 'type' => 'integer' ],
91 ],
92 'required' => [ 'total', 'returned', 'customers' ],
93 ],
94 'execute_callback' => function( $input = [] ) {
95 $limit = isset( $input['limit'] ) ? (int) $input['limit'] : 50;
96 $offset = isset( $input['offset'] ) ? max( 0, (int) $input['offset'] ) : 0;
97 $orderby_in = isset( $input['orderby'] ) ? sanitize_key( $input['orderby'] ) : 'registered';
98 $order = ( isset( $input['order'] ) && strtoupper( $input['order'] ) === 'ASC' ) ? 'ASC' : 'DESC';
99
100 // For ID/registered we can use WP_User_Query directly. For total_spent/order_count
101 // we have to post-process because those are computed from order data.
102 $needs_post_sort = in_array( $orderby_in, [ 'total_spent', 'order_count' ], true );
103
104 $args = [
105 'role' => 'customer',
106 'number' => $needs_post_sort ? -1 : ( $limit > 0 ? $limit : -1 ),
107 'offset' => $needs_post_sort ? 0 : $offset,
108 'orderby' => $needs_post_sort ? 'registered' : ( $orderby_in === 'registered' ? 'user_registered' : $orderby_in ),
109 'order' => $order,
110 ];
111 if ( ! empty( $input['search'] ) ) {
112 $args['search'] = '*' . esc_attr( (string) $input['search'] ) . '*';
113 $args['search_columns'] = [ 'user_login', 'user_email', 'display_name' ];
114 }
115 if ( ! empty( $input['registered_after'] ) || ! empty( $input['registered_before'] ) ) {
116 $dq = [ 'inclusive' => true, 'column' => 'user_registered' ];
117 if ( ! empty( $input['registered_after'] ) ) {
118 list( $after, , $err ) = $this->avcf_normalize_post_date( (string) $input['registered_after'] );
119 if ( $err === null ) $dq['after'] = $after;
120 }
121 if ( ! empty( $input['registered_before'] ) ) {
122 list( $before, , $err ) = $this->avcf_normalize_post_date( (string) $input['registered_before'] );
123 if ( $err === null ) $dq['before'] = $before;
124 }
125 if ( count( $dq ) > 2 ) {
126 $args['date_query'] = [ $dq ];
127 }
128 }
129
130 $query = new \WP_User_Query( $args );
131 $users = $query->get_results();
132
133 $rows = [];
134 foreach ( $users as $user ) {
135 $oc = (int) wc_get_customer_order_count( $user->ID );
136 $ts = (float) wc_get_customer_total_spent( $user->ID );
137
138 if ( ! empty( $input['has_orders'] ) && $oc === 0 ) continue;
139 if ( isset( $input['min_total_spent'] ) && $ts < (float) $input['min_total_spent'] ) continue;
140
141 $last_order_id = (int) wc_get_customer_last_order( $user->ID ) ?: 0;
142 $last_order_date = '';
143 if ( $last_order_id > 0 ) {
144 $lo = wc_get_order( $last_order_id );
145 if ( $lo && $lo->get_date_created() ) {
146 $last_order_date = $lo->get_date_created()->date( 'Y-m-d H:i:s' );
147 }
148 }
149
150 $rows[] = [
151 'id' => (int) $user->ID,
152 'email' => (string) $user->user_email,
153 'login' => (string) $user->user_login,
154 'display_name' => (string) $user->display_name,
155 'first_name' => (string) get_user_meta( $user->ID, 'first_name', true ),
156 'last_name' => (string) get_user_meta( $user->ID, 'last_name', true ),
157 'registered' => $user->user_registered,
158 'order_count' => $oc,
159 'total_spent' => round( $ts, 2 ),
160 'last_order_id' => $last_order_id,
161 'last_order_date'=> $last_order_date,
162 'guest' => false,
163 ];
164 }
165
166 // Sort post-fetch if needed.
167 if ( $needs_post_sort ) {
168 usort( $rows, function( $a, $b ) use ( $orderby_in, $order ) {
169 $av = $orderby_in === 'total_spent' ? $a['total_spent'] : $a['order_count'];
170 $bv = $orderby_in === 'total_spent' ? $b['total_spent'] : $b['order_count'];
171 $cmp = ( $av <=> $bv );
172 return $order === 'ASC' ? $cmp : -$cmp;
173 } );
174 $total_count = count( $rows );
175 if ( $offset > 0 ) {
176 $rows = array_slice( $rows, $offset );
177 }
178 if ( $limit > 0 ) {
179 $rows = array_slice( $rows, 0, $limit );
180 }
181 } else {
182 $total_count = (int) $query->get_total();
183 }
184
185 $guest_count = 0;
186 if ( ! empty( $input['include_guests'] ) ) {
187 global $wpdb;
188 $hpos = $this->detector->avcf_wc_is_hpos_enabled();
189 if ( $hpos ) {
190 // HPOS path: query wc_orders for distinct billing_email where customer_id = 0
191 $table_orders = $wpdb->prefix . 'wc_orders';
192 $emails = $wpdb->get_col( $wpdb->prepare(
193 "SELECT DISTINCT billing_email FROM {$table_orders} WHERE customer_id = 0 AND billing_email != '' LIMIT %d",
194 500
195 ) );
196 } else {
197 // Legacy path: pull billing_email post_meta for shop_order posts with author = 0
198 $emails = $wpdb->get_col( $wpdb->prepare(
199 "SELECT DISTINCT pm.meta_value
200 FROM {$wpdb->postmeta} pm
201 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
202 WHERE p.post_type = 'shop_order'
203 AND p.post_author = 0
204 AND pm.meta_key = '_billing_email'
205 AND pm.meta_value != ''
206 LIMIT %d",
207 500
208 ) );
209 }
210 $emails = is_array( $emails ) ? array_values( array_unique( $emails ) ) : [];
211 foreach ( $emails as $email ) {
212 if ( $email === '' ) continue;
213 // Skip if a registered user has this email (already in the list).
214 if ( get_user_by( 'email', $email ) ) continue;
215 $rows[] = [
216 'id' => 0,
217 'email' => (string) $email,
218 'login' => '',
219 'display_name' => '',
220 'first_name' => '',
221 'last_name' => '',
222 'registered' => '',
223 'order_count' => 0,
224 'total_spent' => 0.0,
225 'last_order_id' => 0,
226 'last_order_date'=> '',
227 'guest' => true,
228 ];
229 $guest_count++;
230 }
231 }
232
233 return [
234 'total' => $total_count + $guest_count,
235 'returned' => count( $rows ),
236 'customers' => $rows,
237 'guest_count' => $guest_count,
238 ];
239 },
240 'permission_callback' => function() {
241 return current_user_can( 'list_users' ) || current_user_can( 'manage_woocommerce' );
242 },
243 'meta' => [
244 'mcp' => [ 'public' => true, 'type' => 'tool' ],
245 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
246 ],
247 ] );
248
249 // ---- get-customer ----
250 wp_register_ability( 'atarim/get-customer', [
251 'label' => 'Get Customer',
252 'description' => 'Full detail for one customer by user ID or email. Includes profile info, lifetime stats, default billing and shipping addresses. Works for both registered customers (full detail) and guests (limited detail — only what\'s on their orders).',
253 'category' => 'atarim',
254 'input_schema' => [
255 'type' => 'object',
256 'properties' => [
257 'id' => [ 'type' => 'integer', 'minimum' => 1 ],
258 'email' => [ 'type' => 'string', 'description' => 'Customer email. Works for both registered users and guests.' ],
259 ],
260 'additionalProperties' => false,
261 ],
262 'output_schema' => [
263 'type' => 'object',
264 'properties' => [
265 'success' => [ 'type' => 'boolean' ],
266 'customer' => [ 'type' => 'object' ],
267 'message' => [ 'type' => 'string' ],
268 ],
269 'required' => [ 'success', 'message' ],
270 ],
271 'execute_callback' => function( $input = [] ) {
272 $id = isset( $input['id'] ) ? (int) $input['id'] : 0;
273 $email = isset( $input['email'] ) ? sanitize_email( (string) $input['email'] ) : '';
274
275 if ( $id <= 0 && $email === '' ) {
276 return [ 'success' => false, 'message' => 'Pass id or email.' ];
277 }
278 if ( $id <= 0 && $email !== '' ) {
279 $u = get_user_by( 'email', $email );
280 if ( $u ) $id = (int) $u->ID;
281 }
282
283 if ( $id <= 0 ) {
284 // Guest path — query orders by billing_email and return aggregate-only detail.
285 if ( $email === '' ) {
286 return [ 'success' => false, 'message' => 'Customer not found.' ];
287 }
288 $orders = wc_get_orders( [
289 'limit' => -1,
290 'billing_email' => $email,
291 'return' => 'objects',
292 ] );
293 if ( empty( $orders ) ) {
294 return [ 'success' => false, 'message' => sprintf( 'No customer or orders found for email "%s".', $email ) ];
295 }
296 $first_order = $orders[ count( $orders ) - 1 ];
297 $last_order = $orders[0];
298 $total = 0.0;
299 foreach ( $orders as $o ) {
300 if ( in_array( $o->get_status(), [ 'completed', 'processing', 'on-hold' ], true ) ) {
301 $total += (float) $o->get_total();
302 }
303 }
304 return [
305 'success' => true,
306 'customer' => [
307 'id' => 0,
308 'guest' => true,
309 'email' => $email,
310 'first_name' => (string) $first_order->get_billing_first_name(),
311 'last_name' => (string) $first_order->get_billing_last_name(),
312 'order_count' => count( $orders ),
313 'total_spent' => round( $total, 2 ),
314 'last_order_id' => (int) $last_order->get_id(),
315 'last_order_date'=> $last_order->get_date_created() ? $last_order->get_date_created()->date( 'Y-m-d H:i:s' ) : '',
316 'billing' => $first_order->get_address( 'billing' ),
317 'shipping' => $first_order->get_address( 'shipping' ),
318 ],
319 'message' => sprintf( 'Guest customer with %d order(s).', count( $orders ) ),
320 ];
321 }
322
323 $user = get_userdata( $id );
324 if ( ! $user ) {
325 return [ 'success' => false, 'message' => sprintf( 'User %d not found.', $id ) ];
326 }
327
328 $oc = (int) wc_get_customer_order_count( $id );
329 $ts = (float) wc_get_customer_total_spent( $id );
330 $last_order_id = (int) wc_get_customer_last_order( $id ) ?: 0;
331 $last_order_date = '';
332 if ( $last_order_id > 0 ) {
333 $lo = wc_get_order( $last_order_id );
334 if ( $lo && $lo->get_date_created() ) {
335 $last_order_date = $lo->get_date_created()->date( 'Y-m-d H:i:s' );
336 }
337 }
338
339 $billing = [];
340 $shipping = [];
341 foreach ( $this->billing_fields as $f ) {
342 $billing[ $f ] = (string) get_user_meta( $id, 'billing_' . $f, true );
343 }
344 foreach ( $this->shipping_fields as $f ) {
345 $shipping[ $f ] = (string) get_user_meta( $id, 'shipping_' . $f, true );
346 }
347
348 return [
349 'success' => true,
350 'customer' => [
351 'id' => $id,
352 'guest' => false,
353 'email' => (string) $user->user_email,
354 'login' => (string) $user->user_login,
355 'display_name' => (string) $user->display_name,
356 'first_name' => (string) get_user_meta( $id, 'first_name', true ),
357 'last_name' => (string) get_user_meta( $id, 'last_name', true ),
358 'registered' => $user->user_registered,
359 'order_count' => $oc,
360 'total_spent' => round( $ts, 2 ),
361 'last_order_id' => $last_order_id,
362 'last_order_date'=> $last_order_date,
363 'billing' => $billing,
364 'shipping' => $shipping,
365 ],
366 'message' => 'OK.',
367 ];
368 },
369 'permission_callback' => function() {
370 return current_user_can( 'list_users' ) || current_user_can( 'manage_woocommerce' );
371 },
372 'meta' => [
373 'mcp' => [ 'public' => true, 'type' => 'tool' ],
374 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
375 ],
376 ] );
377
378 // ---- update-customer-billing ----
379 wp_register_ability( 'atarim/update-customer-billing', [
380 'label' => 'Update Customer Billing Address',
381 'description' => 'Update a registered customer\'s default billing address. Partial — pass only fields you want to change. country is validated against WooCommerce\'s known country codes; state is validated when the country has a known state list (US, CA, IN, etc.) and free-form otherwise. Does NOT apply to past orders — those have their own billing snapshots. Affects future checkouts where the customer doesn\'t override the address.',
382 'category' => 'atarim',
383 'input_schema' => [
384 'type' => 'object',
385 'properties' => [
386 'customer_id' => [ 'type' => 'integer', 'minimum' => 1 ],
387 'first_name' => [ 'type' => 'string' ],
388 'last_name' => [ 'type' => 'string' ],
389 'company' => [ 'type' => 'string' ],
390 'address_1' => [ 'type' => 'string' ],
391 'address_2' => [ 'type' => 'string' ],
392 'city' => [ 'type' => 'string' ],
393 'state' => [ 'type' => 'string', 'description' => 'State / region code (e.g. "CA"). Free-form if the country has no known state list.' ],
394 'postcode' => [ 'type' => 'string' ],
395 'country' => [ 'type' => 'string', 'description' => 'ISO-3166 alpha-2 country code (e.g. "US", "GB").' ],
396 'email' => [ 'type' => 'string', 'description' => 'Billing-specific email. May differ from the customer\'s account email.' ],
397 'phone' => [ 'type' => 'string' ],
398 ],
399 'required' => [ 'customer_id' ],
400 'additionalProperties' => false,
401 ],
402 'output_schema' => [
403 'type' => 'object',
404 'properties' => [
405 'success' => [ 'type' => 'boolean' ],
406 'customer_id' => [ 'type' => 'integer' ],
407 'updated' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
408 'message' => [ 'type' => 'string' ],
409 ],
410 'required' => [ 'success', 'message' ],
411 ],
412 'execute_callback' => function( $input = [] ) {
413 return $this->avcf_update_address( $input, 'billing', $this->billing_fields );
414 },
415 'permission_callback' => function() {
416 return current_user_can( 'edit_users' ) || current_user_can( 'manage_woocommerce' );
417 },
418 'meta' => [
419 'mcp' => [ 'public' => true, 'type' => 'tool' ],
420 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
421 ],
422 ] );
423
424 // ---- update-customer-shipping ----
425 wp_register_ability( 'atarim/update-customer-shipping', [
426 'label' => 'Update Customer Shipping Address',
427 'description' => 'Update a registered customer\'s default shipping address. Partial — pass only fields you want to change. Same country/state validation as billing. No email field (shipping addresses don\'t have one — billing carries the customer email).',
428 'category' => 'atarim',
429 'input_schema' => [
430 'type' => 'object',
431 'properties' => [
432 'customer_id' => [ 'type' => 'integer', 'minimum' => 1 ],
433 'first_name' => [ 'type' => 'string' ],
434 'last_name' => [ 'type' => 'string' ],
435 'company' => [ 'type' => 'string' ],
436 'address_1' => [ 'type' => 'string' ],
437 'address_2' => [ 'type' => 'string' ],
438 'city' => [ 'type' => 'string' ],
439 'state' => [ 'type' => 'string' ],
440 'postcode' => [ 'type' => 'string' ],
441 'country' => [ 'type' => 'string' ],
442 'phone' => [ 'type' => 'string' ],
443 ],
444 'required' => [ 'customer_id' ],
445 'additionalProperties' => false,
446 ],
447 'output_schema' => [
448 'type' => 'object',
449 'properties' => [
450 'success' => [ 'type' => 'boolean' ],
451 'customer_id' => [ 'type' => 'integer' ],
452 'updated' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
453 'message' => [ 'type' => 'string' ],
454 ],
455 'required' => [ 'success', 'message' ],
456 ],
457 'execute_callback' => function( $input = [] ) {
458 return $this->avcf_update_address( $input, 'shipping', $this->shipping_fields );
459 },
460 'permission_callback' => function() {
461 return current_user_can( 'edit_users' ) || current_user_can( 'manage_woocommerce' );
462 },
463 'meta' => [
464 'mcp' => [ 'public' => true, 'type' => 'tool' ],
465 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
466 ],
467 ] );
468 }
469
470 /**
471 * Shared helper for billing and shipping address updates.
472 *
473 * @param array $input Input arguments from the ability call.
474 * @param string $prefix 'billing' or 'shipping'.
475 * @param array $allowed_fields Whitelist of field names for this prefix.
476 */
477 private function avcf_update_address( $input, $prefix, $allowed_fields ) {
478 $customer_id = isset( $input['customer_id'] ) ? (int) $input['customer_id'] : 0;
479 if ( $customer_id <= 0 ) {
480 return [ 'success' => false, 'message' => 'customer_id is required.' ];
481 }
482 $user = get_userdata( $customer_id );
483 if ( ! $user ) {
484 return [ 'success' => false, 'message' => sprintf( 'User %d not found.', $customer_id ) ];
485 }
486
487 // Country/state validation.
488 if ( array_key_exists( 'country', $input ) && $input['country'] !== '' ) {
489 $country = strtoupper( wc_clean( (string) $input['country'] ) );
490 $countries = WC()->countries->get_countries();
491 if ( ! isset( $countries[ $country ] ) ) {
492 return [ 'success' => false, 'customer_id' => $customer_id, 'message' => sprintf( 'Country code "%s" is not recognised by WooCommerce.', $country ) ];
493 }
494 $input['country'] = $country;
495
496 if ( array_key_exists( 'state', $input ) && $input['state'] !== '' ) {
497 $states = WC()->countries->get_states( $country );
498 if ( is_array( $states ) && ! empty( $states ) ) {
499 $state = strtoupper( wc_clean( (string) $input['state'] ) );
500 if ( ! isset( $states[ $state ] ) ) {
501 return [ 'success' => false, 'customer_id' => $customer_id, 'message' => sprintf( 'State code "%s" is not valid for country "%s".', $state, $country ) ];
502 }
503 $input['state'] = $state;
504 }
505 // If country has no known states list, accept free-form.
506 }
507 }
508
509 if ( array_key_exists( 'email', $input ) && $input['email'] !== '' ) {
510 $email = sanitize_email( (string) $input['email'] );
511 if ( ! is_email( $email ) ) {
512 return [ 'success' => false, 'customer_id' => $customer_id, 'message' => sprintf( 'Email "%s" is not a valid address.', $input['email'] ) ];
513 }
514 $input['email'] = $email;
515 }
516
517 $updated = [];
518 foreach ( $allowed_fields as $field ) {
519 if ( ! array_key_exists( $field, $input ) ) continue;
520 $value = (string) $input[ $field ];
521 $meta_key = $prefix . '_' . $field;
522 update_user_meta( $customer_id, $meta_key, sanitize_text_field( $value ) );
523 $updated[] = $field;
524 }
525
526 if ( empty( $updated ) ) {
527 return [ 'success' => false, 'customer_id' => $customer_id, 'message' => 'No fields provided to update.' ];
528 }
529
530 return [
531 'success' => true,
532 'customer_id' => $customer_id,
533 'updated' => $updated,
534 'message' => sprintf( '%s address updated: %s.', ucfirst( $prefix ), implode( ', ', $updated ) ),
535 ];
536 }
537 }
538