From 829d9d0d8f680b3688f25cf974f76d913f451176 Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 20 Nov 2025 22:40:59 +0700 Subject: [PATCH] feat(api): Add CustomersController with full CRUD operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend implementation for Customer module Created CustomersController.php: ✅ GET /customers - List with pagination, search, role filter ✅ GET /customers/{id} - Get single customer with full details ✅ POST /customers - Create new customer with validation ✅ PUT /customers/{id} - Update customer data ✅ DELETE /customers/{id} - Delete customer (with safety checks) ✅ GET /customers/search - Autocomplete search Features: - Full WooCommerce integration (WC_Customer) - Billing and shipping address management - Order stats (total_orders, total_spent) - Email uniqueness validation - Username auto-generation from email - Password generation if not provided - Role-based permissions (list_users, create_users, etc.) - Cannot delete current user (safety) - Optional new account email notification Data format: - List: Basic customer info (id, name, email, registered) - Detail: Full data including billing, shipping, stats - Search: Minimal data for autocomplete (id, name, email) Registered routes in Routes.php: - Added CustomersController import - Registered all customer endpoints Next: Frontend API client and CRUD pages --- includes/Api/CustomersController.php | 436 +++++++++++++++++++++++++++ includes/Api/Routes.php | 4 + 2 files changed, 440 insertions(+) create mode 100644 includes/Api/CustomersController.php diff --git a/includes/Api/CustomersController.php b/includes/Api/CustomersController.php new file mode 100644 index 0000000..91de62d --- /dev/null +++ b/includes/Api/CustomersController.php @@ -0,0 +1,436 @@ + 'GET', + 'callback' => [__CLASS__, 'list_customers'], + 'permission_callback' => function () { return current_user_can('list_users'); }, + ]); + + // Get single customer + register_rest_route('woonoow/v1', '/customers/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'get_customer'], + 'permission_callback' => function () { return current_user_can('list_users'); }, + ]); + + // Create customer + register_rest_route('woonoow/v1', '/customers', [ + 'methods' => 'POST', + 'callback' => [__CLASS__, 'create_customer'], + 'permission_callback' => function () { return current_user_can('create_users'); }, + ]); + + // Update customer + register_rest_route('woonoow/v1', '/customers/(?P\d+)', [ + 'methods' => 'PUT', + 'callback' => [__CLASS__, 'update_customer'], + 'permission_callback' => function () { return current_user_can('edit_users'); }, + ]); + + // Delete customer + register_rest_route('woonoow/v1', '/customers/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [__CLASS__, 'delete_customer'], + 'permission_callback' => function () { return current_user_can('delete_users'); }, + ]); + + // Search customers (for autocomplete) + register_rest_route('woonoow/v1', '/customers/search', [ + 'methods' => 'GET', + 'callback' => [__CLASS__, 'search_customers'], + 'permission_callback' => function () { return current_user_can('list_users'); }, + ]); + } + + /** + * GET /woonoow/v1/customers + * List all customers with pagination and filtering + */ + public static function list_customers(WP_REST_Request $request): WP_REST_Response { + $page = max(1, (int) $request->get_param('page') ?: 1); + $per_page = max(1, min(100, (int) $request->get_param('per_page') ?: 20)); + $search = sanitize_text_field($request->get_param('search') ?: ''); + $role = sanitize_text_field($request->get_param('role') ?: 'customer'); + + $args = [ + 'role' => $role, + 'number' => $per_page, + 'paged' => $page, + 'orderby' => 'registered', + 'order' => 'DESC', + ]; + + // Search by name or email + if ($search) { + $args['search'] = '*' . $search . '*'; + $args['search_columns'] = ['user_login', 'user_email', 'display_name']; + } + + $user_query = new \WP_User_Query($args); + $users = $user_query->get_results(); + $total = $user_query->get_total(); + + $customers = []; + foreach ($users as $user) { + $customers[] = self::format_customer($user); + } + + return new WP_REST_Response([ + 'data' => $customers, + 'pagination' => [ + 'total' => $total, + 'total_pages' => ceil($total / $per_page), + 'current' => $page, + 'per_page' => $per_page, + ], + ]); + } + + /** + * GET /woonoow/v1/customers/{id} + * Get single customer by ID + */ + public static function get_customer(WP_REST_Request $request): WP_REST_Response|WP_Error { + $id = (int) $request->get_param('id'); + $user = get_user_by('ID', $id); + + if (!$user) { + return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]); + } + + return new WP_REST_Response(self::format_customer($user, true)); + } + + /** + * POST /woonoow/v1/customers + * Create new customer + */ + public static function create_customer(WP_REST_Request $request): WP_REST_Response|WP_Error { + $data = $request->get_json_params(); + + // Validate required fields + if (empty($data['email'])) { + return new WP_Error('missing_email', __('Email is required', 'woonoow'), ['status' => 400]); + } + + if (empty($data['first_name'])) { + return new WP_Error('missing_first_name', __('First name is required', 'woonoow'), ['status' => 400]); + } + + if (empty($data['last_name'])) { + return new WP_Error('missing_last_name', __('Last name is required', 'woonoow'), ['status' => 400]); + } + + // Check if email already exists + if (email_exists($data['email'])) { + return new WP_Error('email_exists', __('Email already exists', 'woonoow'), ['status' => 400]); + } + + // Generate username from email if not provided + $username = !empty($data['username']) ? sanitize_user($data['username']) : sanitize_user($data['email']); + + // Check if username exists + if (username_exists($username)) { + // Append number to make it unique + $base_username = $username; + $counter = 1; + while (username_exists($username)) { + $username = $base_username . $counter; + $counter++; + } + } + + // Generate password if not provided + $password = !empty($data['password']) ? $data['password'] : wp_generate_password(12, true, true); + + // Create user + $user_id = wp_create_user($username, $password, sanitize_email($data['email'])); + + if (is_wp_error($user_id)) { + return new WP_Error('create_failed', $user_id->get_error_message(), ['status' => 500]); + } + + // Set role to customer + $user = new WP_User($user_id); + $user->set_role('customer'); + + // Update user meta + wp_update_user([ + 'ID' => $user_id, + 'first_name' => sanitize_text_field($data['first_name']), + 'last_name' => sanitize_text_field($data['last_name']), + 'display_name' => sanitize_text_field($data['first_name'] . ' ' . $data['last_name']), + ]); + + // Update WooCommerce customer data + $customer = new WC_Customer($user_id); + + // Billing address + if (!empty($data['billing'])) { + $billing = $data['billing']; + $customer->set_billing_first_name(sanitize_text_field($billing['first_name'] ?? $data['first_name'])); + $customer->set_billing_last_name(sanitize_text_field($billing['last_name'] ?? $data['last_name'])); + $customer->set_billing_company(sanitize_text_field($billing['company'] ?? '')); + $customer->set_billing_address_1(sanitize_text_field($billing['address_1'] ?? '')); + $customer->set_billing_address_2(sanitize_text_field($billing['address_2'] ?? '')); + $customer->set_billing_city(sanitize_text_field($billing['city'] ?? '')); + $customer->set_billing_state(sanitize_text_field($billing['state'] ?? '')); + $customer->set_billing_postcode(sanitize_text_field($billing['postcode'] ?? '')); + $customer->set_billing_country(sanitize_text_field($billing['country'] ?? '')); + $customer->set_billing_phone(sanitize_text_field($billing['phone'] ?? '')); + } + + // Shipping address + if (!empty($data['shipping'])) { + $shipping = $data['shipping']; + $customer->set_shipping_first_name(sanitize_text_field($shipping['first_name'] ?? $data['first_name'])); + $customer->set_shipping_last_name(sanitize_text_field($shipping['last_name'] ?? $data['last_name'])); + $customer->set_shipping_company(sanitize_text_field($shipping['company'] ?? '')); + $customer->set_shipping_address_1(sanitize_text_field($shipping['address_1'] ?? '')); + $customer->set_shipping_address_2(sanitize_text_field($shipping['address_2'] ?? '')); + $customer->set_shipping_city(sanitize_text_field($shipping['city'] ?? '')); + $customer->set_shipping_state(sanitize_text_field($shipping['state'] ?? '')); + $customer->set_shipping_postcode(sanitize_text_field($shipping['postcode'] ?? '')); + $customer->set_shipping_country(sanitize_text_field($shipping['country'] ?? '')); + } + + $customer->save(); + + // Send new account email if requested + if (!empty($data['send_email'])) { + wp_new_user_notification($user_id, null, 'both'); + } + + return new WP_REST_Response(self::format_customer(get_user_by('ID', $user_id), true), 201); + } + + /** + * PUT /woonoow/v1/customers/{id} + * Update existing customer + */ + public static function update_customer(WP_REST_Request $request): WP_REST_Response|WP_Error { + $id = (int) $request->get_param('id'); + $data = $request->get_json_params(); + + $user = get_user_by('ID', $id); + if (!$user) { + return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]); + } + + // Update user data + $user_data = ['ID' => $id]; + + if (!empty($data['email'])) { + // Check if email is changing and if new email exists + if ($data['email'] !== $user->user_email && email_exists($data['email'])) { + return new WP_Error('email_exists', __('Email already exists', 'woonoow'), ['status' => 400]); + } + $user_data['user_email'] = sanitize_email($data['email']); + } + + if (!empty($data['first_name'])) { + $user_data['first_name'] = sanitize_text_field($data['first_name']); + } + + if (!empty($data['last_name'])) { + $user_data['last_name'] = sanitize_text_field($data['last_name']); + } + + if (isset($data['first_name']) || isset($data['last_name'])) { + $first = $data['first_name'] ?? get_user_meta($id, 'first_name', true); + $last = $data['last_name'] ?? get_user_meta($id, 'last_name', true); + $user_data['display_name'] = trim($first . ' ' . $last); + } + + if (!empty($data['password'])) { + $user_data['user_pass'] = $data['password']; + } + + $result = wp_update_user($user_data); + if (is_wp_error($result)) { + return new WP_Error('update_failed', $result->get_error_message(), ['status' => 500]); + } + + // Update WooCommerce customer data + $customer = new WC_Customer($id); + + // Billing address + if (!empty($data['billing'])) { + $billing = $data['billing']; + if (isset($billing['first_name'])) $customer->set_billing_first_name(sanitize_text_field($billing['first_name'])); + if (isset($billing['last_name'])) $customer->set_billing_last_name(sanitize_text_field($billing['last_name'])); + if (isset($billing['company'])) $customer->set_billing_company(sanitize_text_field($billing['company'])); + if (isset($billing['address_1'])) $customer->set_billing_address_1(sanitize_text_field($billing['address_1'])); + if (isset($billing['address_2'])) $customer->set_billing_address_2(sanitize_text_field($billing['address_2'])); + if (isset($billing['city'])) $customer->set_billing_city(sanitize_text_field($billing['city'])); + if (isset($billing['state'])) $customer->set_billing_state(sanitize_text_field($billing['state'])); + if (isset($billing['postcode'])) $customer->set_billing_postcode(sanitize_text_field($billing['postcode'])); + if (isset($billing['country'])) $customer->set_billing_country(sanitize_text_field($billing['country'])); + if (isset($billing['phone'])) $customer->set_billing_phone(sanitize_text_field($billing['phone'])); + } + + // Shipping address + if (!empty($data['shipping'])) { + $shipping = $data['shipping']; + if (isset($shipping['first_name'])) $customer->set_shipping_first_name(sanitize_text_field($shipping['first_name'])); + if (isset($shipping['last_name'])) $customer->set_shipping_last_name(sanitize_text_field($shipping['last_name'])); + if (isset($shipping['company'])) $customer->set_shipping_company(sanitize_text_field($shipping['company'])); + if (isset($shipping['address_1'])) $customer->set_shipping_address_1(sanitize_text_field($shipping['address_1'])); + if (isset($shipping['address_2'])) $customer->set_shipping_address_2(sanitize_text_field($shipping['address_2'])); + if (isset($shipping['city'])) $customer->set_shipping_city(sanitize_text_field($shipping['city'])); + if (isset($shipping['state'])) $customer->set_shipping_state(sanitize_text_field($shipping['state'])); + if (isset($shipping['postcode'])) $customer->set_shipping_postcode(sanitize_text_field($shipping['postcode'])); + if (isset($shipping['country'])) $customer->set_shipping_country(sanitize_text_field($shipping['country'])); + } + + $customer->save(); + + return new WP_REST_Response(self::format_customer(get_user_by('ID', $id), true)); + } + + /** + * DELETE /woonoow/v1/customers/{id} + * Delete customer + */ + public static function delete_customer(WP_REST_Request $request): WP_REST_Response|WP_Error { + $id = (int) $request->get_param('id'); + + $user = get_user_by('ID', $id); + if (!$user) { + return new WP_Error('customer_not_found', __('Customer not found', 'woonoow'), ['status' => 404]); + } + + // Don't allow deleting current user + if ($id === get_current_user_id()) { + return new WP_Error('cannot_delete_self', __('You cannot delete your own account', 'woonoow'), ['status' => 403]); + } + + require_once(ABSPATH . 'wp-admin/includes/user.php'); + $result = wp_delete_user($id); + + if (!$result) { + return new WP_Error('delete_failed', __('Failed to delete customer', 'woonoow'), ['status' => 500]); + } + + return new WP_REST_Response(['success' => true]); + } + + /** + * GET /woonoow/v1/customers/search + * Search customers for autocomplete + */ + public static function search_customers(WP_REST_Request $request): WP_REST_Response { + $search = sanitize_text_field($request->get_param('q') ?: ''); + $limit = max(1, min(50, (int) $request->get_param('limit') ?: 10)); + + if (empty($search)) { + return new WP_REST_Response([]); + } + + $args = [ + 'role' => 'customer', + 'number' => $limit, + 'search' => '*' . $search . '*', + 'search_columns' => ['user_login', 'user_email', 'display_name'], + ]; + + $user_query = new \WP_User_Query($args); + $users = $user_query->get_results(); + + $results = []; + foreach ($users as $user) { + $results[] = [ + 'id' => $user->ID, + 'name' => $user->display_name, + 'email' => $user->user_email, + ]; + } + + return new WP_REST_Response($results); + } + + /** + * Format customer data for API response + */ + private static function format_customer(WP_User $user, bool $detailed = false): array { + $customer = new WC_Customer($user->ID); + + $data = [ + 'id' => $user->ID, + 'username' => $user->user_login, + 'email' => $user->user_email, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'display_name' => $user->display_name, + 'registered' => $user->user_registered, + 'role' => !empty($user->roles) ? $user->roles[0] : '', + ]; + + if ($detailed) { + $data['billing'] = [ + 'first_name' => $customer->get_billing_first_name(), + 'last_name' => $customer->get_billing_last_name(), + 'company' => $customer->get_billing_company(), + 'address_1' => $customer->get_billing_address_1(), + 'address_2' => $customer->get_billing_address_2(), + 'city' => $customer->get_billing_city(), + 'state' => $customer->get_billing_state(), + 'postcode' => $customer->get_billing_postcode(), + 'country' => $customer->get_billing_country(), + 'phone' => $customer->get_billing_phone(), + ]; + + $data['shipping'] = [ + 'first_name' => $customer->get_shipping_first_name(), + 'last_name' => $customer->get_shipping_last_name(), + 'company' => $customer->get_shipping_company(), + 'address_1' => $customer->get_shipping_address_1(), + 'address_2' => $customer->get_shipping_address_2(), + 'city' => $customer->get_shipping_city(), + 'state' => $customer->get_shipping_state(), + 'postcode' => $customer->get_shipping_postcode(), + 'country' => $customer->get_shipping_country(), + ]; + + // Get order stats + $orders = wc_get_orders([ + 'customer_id' => $user->ID, + 'limit' => -1, + 'status' => ['wc-completed', 'wc-processing'], + ]); + + $total_spent = 0; + foreach ($orders as $order) { + $total_spent += $order->get_total(); + } + + $data['stats'] = [ + 'total_orders' => count($orders), + 'total_spent' => $total_spent, + ]; + } + + return $data; + } +} diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index 01870fc..bb8f46d 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -19,6 +19,7 @@ use WooNooW\Api\NotificationsController; use WooNooW\Api\ActivityLogController; use WooNooW\Api\ProductsController; use WooNooW\Api\CouponsController; +use WooNooW\Api\CustomersController; class Routes { public static function init() { @@ -112,6 +113,9 @@ class Routes { // Coupons controller CouponsController::register_routes(); + + // Customers controller + CustomersController::register_routes(); }); } }