namespace, $this->base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_customer_permission', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_customer_permission', ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_items' ), 'permission_callback' => array( $this, 'delete_customer_permission', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/profile', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_profile' ), 'permission_callback' => array( $this, 'customer_profile_permission', ), ), ) ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_customer_permission', ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_customer_permission', ), ), array( 'method' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_customer_permission', ), ), ) ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)/connect', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'connect_customer' ), 'permission_callback' => array( $this, 'update_customer_permission', ), ), ) ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)/disconnect', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'disconnect_customer' ), 'permission_callback' => array( $this, 'update_customer_permission', ), ), ) ); } /** * Check if a given request has access to create items. * * @param $request Full data about the request. */ public function create_customer_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Prepare and validate customer data * * @param $request * * @return array|bool|WP_Error */ protected function prepare_customer_data( $request ) { $input_data = json_decode( $request->get_body(), true ); $validate = booktics_validate( $input_data, array( 'name' => array( 'required', ), 'email' => array( 'required', 'unique:' . booktics_get_table_name( 'booktics_guests' ) . ',email', ), ) ); if ( is_wp_error( $validate ) ) { return $validate; } return array( 'name' => $input_data['name'], 'email' => $input_data['email'], 'user_login' => User_Model::generate_username( $input_data['email'] ), 'phone' => $input_data['phone'], ); } /** * Store new customer data * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function create_item( $request ) { $prepared_data = $this->prepare_customer_data( $request ); if ( is_wp_error( $prepared_data ) ) { return $this->error( $prepared_data->get_error_message(), 422, 'Validation', $prepared_data->errors ); } $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->create( $prepared_data ); $image_url = $request->get_param( 'image' ); if ( $image_url ) { $customer->update( array( 'image' => esc_url_raw( $image_url ) ) ); } if ( ! $customer ) { return $this->error( __( 'Customer create error', 'booktics' ), 500 ); } do_action( 'booktics_customer_created', $customer, $request ); return $this->response( $customer, __( 'Successfully created customer', 'booktics' ), 201 ); } /** * Check if a given request has access to get all customer * * @param $request * * @return bool */ public function get_customer_permission( $request ) { if ( current_user_can( 'manage_options' ) ) { return true; } if ( is_user_logged_in() ) { $current_user = wp_get_current_user(); if ( in_array( 'booktics_team_member', (array) $current_user->roles, true ) ) { return true; } $requested_customer_id = isset( $request['id'] ) ? intval( $request['id'] ) : 0; if ( $requested_customer_id > 0 ) { $db = new Booktics_Database(); $requested_customer = ( new Guest_Model( $db ) )->find( array( 'id' => $requested_customer_id ) ); if ( $requested_customer && intval( $requested_customer->wp_user_id ) === get_current_user_id() ) { return true; } } } return new WP_Error( 'booktics_forbidden', __( 'You are not allowed to view this customer data.', 'booktics' ), array( 'status' => 403 ) ); } /** * Get paginated list items for customer * * @param $request * * @return WP_Error|WP_HTTP_Response|WP_REST_Response */ public function get_items( $request ) { $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20; $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1; $name = ! empty( $request['name'] ) ? sanitize_text_field( $request['name'] ) : ''; $email = ! empty( $request['email'] ) ? sanitize_email( $request['email'] ) : ''; $phone = ! empty( $request['phone'] ) ? sanitize_text_field( $request['phone'] ) : ''; $user_name = ! empty( $request['user_name'] ) ? sanitize_text_field( $request['user_name'] ) : ''; $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : ''; $db = new Booktics_Database(); $customers = ( new Guest_Model( $db ) )->query(); $args = array( 'name' => array( 'LIKE', $name ), 'email' => array( 'LIKE', $email ), 'phone' => array( 'LIKE', $phone ), 'user_login' => array( 'LIKE', $user_name ), ); foreach ( $args as $key => $value ) { if ( $value[1] ) { $customers->where( $key, $value[0], $value[1] ); } } if ( $search ) { $customers->where( 'name', 'LIKE', $search ) ->or_where( 'email', 'LIKE', $search ) ->or_where( 'phone', 'LIKE', $search ); } $customers = $customers->paginate( $paged, $per_page ); foreach ( $customers['items'] as &$customer ) { $customer = ( new Guest_Model( $db ) )->with_virtual_attributes( $customer ); // Mask wp_user_id and user_login from non-admins (use empty string instead of unset for API consistency) if ( ! current_user_can( 'manage_options' ) ) { $customer->wp_user_id = ''; $customer->user_login = ''; } } return $this->response( $customers, __( 'Successfully fetched customer', 'booktics' ) ); } /** * Checks if a given request has access to delete a team member. * * @param $request * * @return bool */ public function delete_customer_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Delete a customer * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function delete_item( $request ) { $id = intval( $request['id'] ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $orders = $order_model->get( array( 'customer_id' => $id ) ); foreach ( $orders['items'] as $order ) { $appointment_model = new Appointment_Model(); $appointments = $appointment_model->where( 'order_id', $order->id ); foreach ( $appointments as $appointment ) { $appointment->delete(); } $payment_model = new Payment_Model( $db ); $payments = $payment_model->get( array( 'order_id' => $order->id ) ); foreach ( $payments['items'] as $payment ) { ( new Payment_Model( $db ) )->delete( $payment->id ); } $order_model->delete( $order->id ); } $guest_model = ( new Guest_Model( $db ) ); $guest = $guest_model->find( array( 'id' => $id ) ); if ( $guest->wp_user_id ) { $customer = new Customer_Model( $guest->wp_user_id ); $customer->delete(); } $deleted = $guest_model->delete( $id ); if ( ! $deleted ) { return $this->error( __( 'Customer delete error', 'booktics' ), 500 ); } do_action( 'booktics_customers_deleted', $request ); return $this->response( array(), __( 'Successfully deleted customer', 'booktics' ) ); } /** * Delete multiple customer * * @param $request * * @return array|WP_Error|WP_HTTP_Response */ public function delete_items( $request ) { $ids = ! empty( $request['ids'] ) ? $request['ids'] : array(); if ( ! $ids ) { return $this->error( __( 'Customer ids cannot be empty', 'booktics' ), 500 ); } $count = 0; $db = new Booktics_Database(); foreach ( $ids as $id ) { // Delete all orders (and their related appointments and payments) for this customer $order_model = new Order_Model( $db ); $orders = $order_model->get( array( 'customer_id' => $id ) ); foreach ( $orders['items'] as $order ) { // Delete related appointments $appointment_model = new Appointment_Model(); $appointments = $appointment_model->where( 'order_id', $order->id ); foreach ( $appointments as $appointment ) { $appointment->delete(); } // Delete related payments $payment_model = new Payment_Model( $db ); $payments = $payment_model->get( array( 'order_id' => $order->id ) ); foreach ( $payments['items'] as $payment ) { ( new Payment_Model( $db ) )->delete( $payment->id ); } // Delete the order $order_model->delete( $order->id ); } $guest_model = ( new Guest_Model( $db ) ); $guest = $guest_model->find( array( 'id' => $id ) ); if ( $guest->wp_user_id ) { $customer = new Customer_Model( $guest->wp_user_id ); $customer->delete(); } if ( $guest_model->delete( $id ) ) { ++$count; } } if ( 0 == $count ) { return $this->error( __( 'Customer delete error', 'booktics' ), 500 ); } do_action( 'booktics_customers_deleted', $request ); return $this->response( array(), __( 'Successfully deleted customer', 'booktics' ) ); } /** * Get single customer * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function get_item( $request ) { $id = intval( $request['id'] ); $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $id ) ); if ( ! $customer ) { return $this->error( __( 'Customer not found', 'booktics' ), 404 ); } $orders = ( new Order_Model( $db ) )->get( array( 'customer_id' => $id ) ); $customer->orders = array_map( function ( $order ) { $booking_count = ( new Appointment_Model() ) ->get_appointments_count_by_order_id( $order->id ); return array( 'date' => $order->created_at ? gmdate( 'j F, Y', strtotime( $order->created_at ) ) : '', 'total_booking' => $booking_count, 'total_amount' => $order->total, ); }, $orders['items'] ); // Mask wp_user_id and user_login from non-admins (use empty string instead of unset for API consistency) if ( ! current_user_can( 'manage_options' ) ) { $customer->wp_user_id = ''; $customer->user_login = ''; } return $this->response( $customer, __( 'Customer found successfully', 'booktics' ) ); } /** * Checks if a given request has access to update a customer. * * @param $request * * @return bool True if the request has access to update the item, WP_Error object otherwise. */ public function update_customer_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Prepare data for customer update * * @param $request * * @return array|bool|WP_Error */ public function prepare_customer_update( $request ) { $input_data = json_decode( $request->get_body(), true ); $validation_rules = array( 'first_name' => array( 'required', ), 'email' => array( 'required', 'unique:' . booktics_get_table_name( 'booktics_guests' ) . ',email,' . $request['id'], ), ); $update_validation_rules = array(); $validated_data = array(); if ( ! is_array( $input_data ) ) { return new WP_Error( 'invalid_input_data', 'Invalid data' ); } foreach ( $input_data as $key => $value ) { $validated_data[ $key ] = $value; } $validate = booktics_validate( $input_data, $update_validation_rules ); if ( is_wp_error( $validate ) ) { return $validate; } return $validated_data; } /** * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function update_item( $request ) { if ( ! isset( $request['id'] ) ) { return $this->error( 'Invalid customer id', 400 ); } $prepared_data = $this->prepare_customer_update( $request ); if ( is_wp_error( $prepared_data ) ) { return $this->error( __( 'Customer validation error', 'booktics' ), 422, 'Validation', $prepared_data->errors ); } $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->update( $request['id'], $prepared_data ); $image_url = $request->get_param( 'image' ); if ( $image_url ) { ( new Guest_Model( $db ) )->update( $request['id'], array( 'image' => esc_url_raw( $image_url ) ) ); ( new Guest_Model( $db ) )->update( absint( $request['id'] ), array( 'image' => esc_url_raw( $image_url ) ) ); } if ( ! $customer ) { return $this->error( __( 'Customer update error', 'booktics' ), 500 ); } do_action( 'booktics_customer_updated', $customer, $request ); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $request['id'] ) ); return $this->response( $customer, __( 'Successfully updated customer', 'booktics' ) ); } /** * Check if a given request has access to get customer profile. * * @param $request * * @return bool */ public function customer_profile_permission( $request ) { if ( is_user_logged_in() ) { return true; } return new WP_Error( 'booktics_unauthorized', __( 'You must be logged in to view your profile.', 'booktics' ), array( 'status' => 401 ) ); } /** * Get customer profile * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function get_profile( $request ) { $user_id = get_current_user_id(); $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'wp_user_id' => $user_id ) ); $args = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering customers by meta fields 'meta_query' => array( array( 'key' => 'customer_id', 'value' => $customer->id, 'compare' => '=', ), ), ); $appointments = ( new Appointment_Model() )->all( $args ); $customer->appointments = $appointments; return $this->response( $customer, __( 'Successfully fetched customer profile', 'booktics' ) ); } /** * Connect a guest to a customer * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function connect_customer( $request ) { $id = intval( $request['id'] ); $db = new Booktics_Database(); $guest = ( new Guest_Model( $db ) )->find( array( 'id' => $id ) ); if ( ! $guest ) { return $this->error( __( 'Customer not found', 'booktics' ), 404 ); } if ( $guest->wp_user_id ) { return $this->error( __( 'Customer already connected', 'booktics' ), 400 ); } $customer_model = ( new Customer_Model() ); $customer_id = $customer_model->save( array( 'user_login' => $guest->user_login, 'user_email' => $guest->email, 'first_name' => $guest->first_name ?? '', 'last_name' => $guest->last_name ?? '', 'display_name' => $guest->display_name ?? '', 'phone' => $guest->phone ?? '', ) ); if ( is_wp_error( $customer_id ) ) { return $this->error( __( 'Customer create error', 'booktics' ), 500 ); } ( new Customer_Event_Handler() )->on_customer_create( $customer_id ); ( new Guest_Model( $db ) )->update( $id, array( 'wp_user_id' => $customer_id ) ); $guest = ( new Guest_Model( $db ) )->find( array( 'id' => $id ) ); return $this->response( $guest, __( 'Successfully connected customer', 'booktics' ) ); } /** * Disconnect a guest from a customer * * @param $request * * @return array|WP_Error|WP_HTTP_Response|WP_REST_Response */ public function disconnect_customer( $request ) { $id = intval( $request['id'] ); $db = new Booktics_Database(); $guest = ( new Guest_Model( $db ) )->find( array( 'id' => $id ) ); if ( ! $guest ) { return $this->error( __( 'Customer not found', 'booktics' ), 404 ); } if ( $guest->wp_user_id ) { $customer = new Customer_Model( $guest->wp_user_id ); $customer->delete(); } ( new Guest_Model( $db ) )->update( $id, array( 'wp_user_id' => null ) ); $guest = ( new Guest_Model( $db ) )->find( array( 'id' => $id ) ); return $this->response( $guest, __( 'Successfully disconnected customer', 'booktics' ) ); } }