namespace, $this->base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_order_permission', ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permission', ), 'args' => array( 'page' => array( 'default' => 1 ), 'per_page' => array( 'default' => 10 ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'bulk_delete_items', ), 'permission_callback' => array( $this, 'bulk_delete_items_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_item_permission', ), 'args' => array( 'id' => array( 'required' => true ), 'status' => array( 'type' => 'string' ), ), ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permission', ), 'args' => array( 'id' => array( 'required' => true ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permission', ), 'args' => array( 'id' => array( 'required' => true ), ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)/payment', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_payment_status', ), 'permission_callback' => array( $this, 'update_payment_status_permission', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)/refund', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'refund_payment' ), 'permission_callback' => array( $this, 'update_payment_status_permission', ), ), ) ); } /** * Check if the current user has permission to create orders. * * @param WP_REST_Request $request * * @return bool */ public function create_order_permission( $request ) { return true; } /** * Create a new order with the provided data * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response * @throws Exception */ public function create_item( $request ) { $input_data = json_decode( $request->get_body(), true ); // Single comprehensive validation and preparation $validation_result = $this->validate_and_prepare_order_data( $input_data ); if ( is_wp_error( $validation_result ) ) { $validation_error_msg = $validation_result->get_error_message() ?? __( 'Order validation failed', 'booktics' ); return $this->error( $validation_error_msg, 422, 'Validation' ); } $db = new Booktics_Database(); // Resolve customer ID $customer_id_result = $this->resolve_customer_id( $input_data ); if ( is_wp_error( $customer_id_result ) ) { return $this->error( $customer_id_result->get_error_message() ); } $input_data['customer_id'] = $customer_id_result; try { $db->begin_transaction(); list( $order_model, $appointments ) = $this->create_order_and_appointments( $input_data, $db ); $payment_intent = $this->handle_payment_if_needed( $input_data, $order_model, $db ); $db->commit(); } catch ( Exception $e ) { $db->roll_back(); return $this->error( $e->getMessage() ); } $this->send_notifications( $db, $order_model, $input_data['customer_id'], $appointments ); $response = $this->format_order_response( $db, $order_model, $input_data, $payment_intent ); do_action('booktics_after_order_created', $order_model, $appointments); $response_message = 'Order created successfully'; if ( isset( $input_data['pay_now']) && $input_data['pay_now'] ) { $response_message = 'Order pending, please proceed payment'; } return $this->response( $response, __( $response_message, 'booktics' ), 201 ); } /** * Comprehensive validation and preparation of order data * * @param array $input_data * * @return array|WP_Error */ private function validate_and_prepare_order_data( &$input_data ) { // Step 1: Basic validation using Order_Validator $validator = new Order_Validator(); $validation_result = $validator->validate( $input_data ); if ( is_wp_error( $validation_result ) ) { return $validation_result; } // Step 2: Preserve original timezone for appointment creation, then validate and setup working timezone $input_data['original_timezone'] = $input_data['timezone'] ?? wp_timezone_string(); if ( ! isset( $input_data['timezone'] ) || ! Time_Zone_Handler::validate_time_zone( $input_data['timezone'] ) ) { $input_data['timezone'] = 'UTC'; } // Step 3: Payment validation if pay_now is true if ( isset( $input_data['pay_now'] ) && $input_data['pay_now'] ) { $validate = booktics_validate( $input_data, array( 'payment_method' => array( 'required' ), ) ); if ( is_wp_error( $validate ) ) { return $validate; } } // Step 4: Process and validate each item if ( ! is_array( $input_data['items'] ) || count( $input_data['items'] ) == 0 ) { return new WP_Error( 'items', __( 'Items are required', 'booktics' ) ); } foreach ( $input_data['items'] as $key => $item ) { // Validate required fields for each item $item_validate = booktics_validate( $item, array( 'date' => array( 'required' ), 'start_time' => array( 'required' ), 'end_time' => array( 'required' ), 'team_member_id' => array( 'required' ), 'service_id' => array( 'required' ), ) ); if ( is_wp_error( $item_validate ) ) { return $item_validate; } // Convert times to UTC $converted_start_time = Time_Zone_Handler::convert_to_utc( $item['start_time'], $item['date'], $input_data['timezone'] ); $converted_end_time = Time_Zone_Handler::convert_to_utc( $item['end_time'], $item['date'], $input_data['timezone'] ); // Validate time range $start_time = strtotime( $converted_start_time['time'] ); $end_time = strtotime( $converted_end_time['time'] ); if ( ! $start_time || ! $end_time || $start_time >= $end_time ) { return new WP_Error( 'time_invalid', __( 'Invalid time range provided', 'booktics' ) ); } // In validate_and_prepare_order_data(), after converting times: $item_with_converted_times = array_merge($item, [ 'start_time' => $converted_start_time['time'], 'end_time' => $converted_end_time['time'], 'date' => gmdate('Y-m-d', strtotime($converted_start_time['date'])) ]); $item_timet_slot_validate = $this->validate_appointment_time_is_free($item_with_converted_times); if ( is_wp_error( $item_timet_slot_validate ) ) { return $item_timet_slot_validate; } // Sanitize and prepare item data $input_data['items'][ $key ] = array( 'start_time' => $converted_start_time['time'], 'end_time' => $converted_end_time['time'], 'date' => gmdate( 'Y-m-d', strtotime( $converted_start_time['date'] ) ), 'customer_id' => isset( $item['customer_id'] ) ? absint( $item['customer_id'] ) : null, 'team_member_id' => absint( $item['team_member_id'] ), 'service_id' => absint( $item['service_id'] ), 'additional_duration_id' => $item['additional_duration_id'] ?? '', 'price' => floatval( $item['price'] ?? 0 ), 'subtotal' => floatval( $item['subtotal'] ?? 0 ), 'quantity' => absint( $item['quantity'] ?? 1 ), 'total_attendees' => absint( $item['total_attendees'] ?? 1 ), ); $input_data['items'][$key] = apply_filters('booktics_pro_appointment_validate', $input_data['items'][$key], $item); } $input_data = apply_filters( 'booktics_pro_order_validate', $input_data ); return $input_data; } /** * Check if the requested time slot has available capacity for the given team member * Works by checking all appointments for this team member on this date and calculating remaining capacity * * @param array $item Appointment item data * @return bool|WP_Error True if time slot has capacity, WP_Error if not */ private function validate_appointment_time_is_free( $item ) { try { // Convert times to DateTime objects $requested_start = new \DateTime( $item['date'] . ' ' . $item['start_time'] ); $requested_end = new \DateTime( $item['date'] . ' ' . $item['end_time'] ); // Get the requested attendee count (default to 1 for backward compatibility) $requested_attendees = isset( $item['total_attendees'] ) ? (int) $item['total_attendees'] : 1; // Get service to check if group booking is enabled and get max capacity $service = Service_Model::find($item['service_id']); if ( ! $service ) { return new \WP_Error( 'service_not_found', __( 'Service not found', 'booktics' ) ); } $max_group_size = $this->get_service_max_group_size( $service->id ); $args = [ 'team_member_id' => $item['team_member_id'], 'date' => $item['date'], 'post_status' => booktics_get_option('appointment_statuses_for_blocked_slot', []), ]; // Get all non-cancelled appointments for this team member on this date $appointments = Appointment_Model::all( $args ); if ( ! is_array( $appointments ) || empty( $appointments ) ) { // No existing appointments, check if requested attendees fit within max capacity if ( $requested_attendees > $max_group_size ) { return new \WP_Error( 'exceeds_max_capacity', sprintf( __( 'Requested attendees (%d) exceeds maximum group size (%d) for this service.', 'booktics' ), $requested_attendees, $max_group_size ) ); } return true; } // Calculate total attendees for overlapping time slots $overlapping_attendees = 0; foreach ( $appointments as $appointment ) { // Skip if this is the same appointment (for updates) if ( isset($item['id']) && $appointment->ID == $item['id'] ) { continue; } $appt_start = new \DateTime( $appointment->date . ' ' . $appointment->start_time ); $appt_end = new \DateTime( $appointment->date . ' ' . $appointment->end_time ); // Check for time overlap if ( ! ( $requested_end <= $appt_start || $requested_start >= $appt_end ) ) { // Times overlap, add this appointment's attendees to the count $appointment_attendees = $appointment->total_attendees ?? 1; $overlapping_attendees += $appointment_attendees; } } // Check if adding the requested attendees would exceed capacity $total_after_booking = $overlapping_attendees + $requested_attendees; if ($total_after_booking > $max_group_size) { return new \WP_Error( 'insufficient_capacity', __( 'In sufficient capacity' , 'booktics' ) ); } return true; } catch ( \Throwable $e ) { return new \WP_Error( 'time_validation_error', __( 'Error validating appointment time: ', 'booktics' ) . $e->getMessage() ); } } /** * Get max group size for a specific service * * @param int $service_id Service ID * * @return int Max group size */ private function get_service_max_group_size( $service_id ) { $default_max_size = 1; $group_booking_meta = get_post_meta( $service_id, 'group_booking', true ); if ( ! $this->is_group_booking_enabled( $group_booking_meta ) ) { return $default_max_size; } return $this->extract_max_booking_size( $group_booking_meta, $default_max_size ); } /** * Checks if group booking is enabled for a services group booking meta data * * @param array $group_booking_meta Group booking meta data * * @return bool True if group booking is enabled, false otherwise */ private function is_group_booking_enabled( $group_booking_meta ) { return !empty( $group_booking_meta ) && is_array( $group_booking_meta ) && !empty( $group_booking_meta['enable_group_booking'] ); } /** * Extracts the max booking size from the group booking meta data of a service * * @param array $group_booking_meta Group booking meta data * @param int $default_size Default max booking size * * @return int Max booking size */ private function extract_max_booking_size( $group_booking_meta, $default_size ) { if ( isset( $group_booking_meta['max_booking'] ) && is_numeric( $group_booking_meta['max_booking'] ) ) { return (int) $group_booking_meta['max_booking']; } return $default_size; } /** * Resolve the customer ID from input data, creating or finding the customer as needed * * @param array $input_data * * @return int|WP_Error */ private function resolve_customer_id( &$input_data ) { if ( isset( $input_data['customer_id'] ) ) { return $input_data['customer_id']; } if ( ! isset( $input_data['customer'] ) ) { return new WP_Error( 'missing_customer', __( 'Customer information is required', 'booktics' ) ); } return $this->find_or_create_customer( $input_data['customer'] ); } /** * Create the order and its associated appointments * * @param array $input_data * @param Booktics_Database $db * * @return array [Order_Model $order_model, array $appointments] */ private function create_order_and_appointments( $input_data, $db ) { $input_data['currency'] = ! empty( $input_data['currency'] ) ? strtolower( $input_data['currency'] ) : strtolower( booktics_get_option( 'currency', 'USD' ) ); $order_model = new Order_Model( $db ); $input_timezone_string = $input_data['timezone'] ?? 'UTC'; $input_timezone = Time_Zone_Handler::get_timezone_from_string( $input_timezone_string ); $original_timezone = $input_data['original_timezone'] ?? $input_timezone_string; $order_data = array( 'customer_id' => $input_data['customer_id'], 'payment_status' => Payment_Status::FAILED, 'price_breakdown' => $input_data['price_breakdown'] ?? array(), 'status' => 'open', 'tax_total' => $input_data['tax_total'] ?? 0, 'coupon_code' => $input_data['coupon_code'] ?? '', 'coupon_discount' => $input_data['coupon_discount'] ?? 0, 'subtotal' => $input_data['subtotal'], 'timezone' => $input_timezone, 'total' => $input_data['total'], 'currency' => $input_data['currency'], 'payment_method' => $input_data['payment_method'] ?? null, ); // This is to only allow setting payment status when creating order from admin panel as admin if ( isset( $input_data['payment_status'] ) && current_user_can( 'manage_options' ) ) { $order_data['payment_status'] = $input_data['payment_status']; } $order_data = apply_filters( 'booktics_pro_order_columns', $order_data, $input_data ); $order_model = $order_model->create( $order_data ); $appointments = array(); foreach ( $input_data['items'] as $item ) { if ( isset( $item['uuid'] ) ) { ( new Cart_Manager() )->remove_cart_item( $item['uuid'] ); } $total_duration_min = $this->calculate_total_duration( $item ); $appointment_data = array( 'start_time' => wp_date('g:i A', strtotime($item['start_time']), $input_timezone) ?? '', 'end_time' => wp_date('g:i A', strtotime($item['end_time']), $input_timezone) ?? '', 'date' => wp_date( 'Y-m-d', strtotime( $item['date'] ), $input_timezone ), 'customer_id' => $input_data['customer_id'] ?? '', 'team_member_id' => $item['team_member_id'] ?? '', 'service_id' => $item['service_id'] ?? '', 'additional_duration_id' => $item['additional_duration_id'] ?? '', 'duration' => $total_duration_min, 'order_id' => $order_model->id, 'quantity' => $item['quantity'] ?? 1, 'price' => $item['price'] ?? 0, 'subtotal' => $item['subtotal'] ?? 0, 'total' => $item['total'] ?? $item['subtotal'] ?? 0, 'status' => booktics_get_option( 'appointment_status', 'pending' ), 'total_attendees' => $input_data['total_attendees'] ?? 1, 'timezone' => $original_timezone, ); $appointment_data = apply_filters( 'booktics_appointment_pro_format', $appointment_data, $item ); $appointment = Appointment_Model::create( $appointment_data ); if ( is_wp_error( $appointment ) ) { throw new Exception( $appointment->get_error_message() ); } do_action( 'booktics_after_appointment_created', $appointment ); $appointments[] = $appointment; } return array( $order_model, $appointments ); } /** * Process payment for an order if immediate payment is requested * * @param array $input_data Order input data containing payment details * @param Order_Model $order_model Order model instance * @param Booktics_Database $db Database instance * * @return array * @throws Exception */ private function handle_payment_if_needed( $input_data, $order_model, $db ) { if ( ! isset( $input_data['pay_now'] ) || ! $input_data['pay_now'] ) { return array(); } $payment_method = ( new Payment_Method_Manager() )->get_payment_method( $input_data['payment_method'] ); if ( ! $payment_method ) { throw new Exception( __( 'Payment method cannot be found!', 'booktics' ) ); } $payment_intent = $payment_method->process_payment( $order_model ); if ( $payment_intent['status'] === 'error' ) { throw new Exception( $payment_intent['message'] ); } if ( $payment_intent['status'] === 'success' ) { $this->update_payment_records( $db, $order_model, $input_data, $payment_intent ); // Update orders payment status on successfull payment completion $order_model_id = is_object( $order_model ) ? $order_model->id : $order_model['id']; ( new Order_Model($db) )->update( $order_model_id, [ 'payment_status' => 'fully_paid' ] ); } return $payment_intent; } /** * Update payment records after successful payment processing * * @param Booktics_Database $db Database instance * @param Order_Model $order_model Order model instance * @param array $input_data Order input data containing payment details * @param array $payment_intent Payment intent data from payment processor * * @return void */ private function update_payment_records( $db, $order_model, $input_data, $payment_intent ) { ( new Order_Model( $db ) )->update( $order_model->id, array( 'payment_intent_id' => $payment_intent['payment_intent'] ) ); ( new Payment_Model( $db ) )->create( array( 'order_id' => $order_model->id, 'customer_id' => $input_data['customer_id'], 'amount' => $input_data['total'] * 100, 'currency' => $input_data['currency'] ?? booktics_get_option( 'currency', 'USD' ), 'intent_id' => $payment_intent['payment_intent'], 'transaction_id' => null, 'payment_method' => $input_data['payment_method'], 'status' => $payment_intent['status'] == 'success' ? 'completed' : 'pending', 'date' => gmdate( 'Y-m-d' ), 'date_time' => gmdate( 'Y-m-d H:i:s' ), ) ); } /** * Helper: Format a single appointment for API output * * @param Appointment_Model|array $appointment * * @return array */ private function format_appointment( $appointment ): array { if ( is_object( $appointment ) && method_exists( $appointment, 'to_array' ) ) { $appointment = $appointment->to_array(); } $team_member = ( new Team_Member_Model() )->find( $appointment['team_member_id'] ); $service = ( new Service_Model() )->find( $appointment['service_id'] ); $service = $service ? $service->to_array() : array(); $additional_durations = $service['additional_durations'] ?? array(); $additional_durations = ! empty( $additional_durations ) ? array_filter( $additional_durations, function ( $duration ) use ( $appointment ) { return $duration['id'] === $appointment['additional_duration_id']; } ) : array(); $additional_duration = ! empty( $additional_durations ) ? reset( $additional_durations ) : array(); $formatted_appointment = array( 'id' => $appointment['id'] ?? 0, 'date' => $appointment['date'] ?? '', 'start_time' => $appointment['start_time'] ?? '', 'end_time' => $appointment['end_time'] ?? '', 'additional_duration_id' => $appointment['additional_duration_id'] ?? '', 'team' => array( 'id' => $team_member->id ?? null, 'display_name' => $team_member->display_name ?? '', 'image' => $team_member->image ?? '', 'user_email' => $team_member->user_email ?? '', ), 'service' => array( 'id' => $appointment['service_id'] ?? '', 'title' => $service['title'] ?? '', 'additional_duration' => $additional_duration, 'enable_google_meet' => $service['enable_google_meet'] ?? false, ), 'duration' => $appointment['duration'] ?? null, 'quantity' => $appointment['quantity'] ?? 1, 'price' => $appointment['price'] ?? 0, 'subtotal' => $appointment['subtotal'] ?? 0, 'total' => $appointment['total'] ?? 0, 'status' => $appointment['status'] ?? '', 'total_attendees' => $appointment['total_attendees'] ?? 1, 'appointment_token' => $appointment['security_token'] ?? '', 'cancel_note' => $appointment['cancel_note'] ?? '', ); $formatted_appointment = apply_filters( 'booktics_appointment_pro_format', $formatted_appointment, $appointment ); return $formatted_appointment; } /** * Helper: Format order for API output * * @param Order_Model $order_model * @param array $input_data * @param array $payment_intent * * @return array */ private function format_order_response( $db, $order_model, $input_data, $payment_intent ): array { $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $input_data['customer_id'] ) ); // Ensure $order_model is an object before accessing properties $order_id = is_object( $order_model ) ? $order_model->id : ( is_array( $order_model ) ? $order_model['id'] : null ); $appointments = ( new Appointment_Model() )->where( 'order_id', $order_id ); $appointment_details = array(); foreach ( $appointments as $appointment ) { $appointment_details[] = $this->format_appointment( $appointment ); } $order_data = array( 'id' => $order_id, 'order_no' => is_object( $order_model ) ? $order_model->order_no : ( $order_model['order_no'] ?? null ), 'customer' => array( 'id' => $customer->id ?? null, 'name' => $customer->name ?? '', 'phone' => $customer->phone ?? '', 'email' => $customer->email ?? '', ), 'status' => is_object( $order_model ) ? $order_model->status : ( $order_model['status'] ?? null ), 'subtotal' => is_object( $order_model ) ? $order_model->subtotal : ( $order_model['subtotal'] ?? null ), 'total' => is_object( $order_model ) ? $order_model->total : ( $order_model['total'] ?? null ), 'payment_status' => is_object( $order_model ) ? $order_model->payment_status : ( $order_model['payment_status'] ?? null ), 'payment_method' => is_object( $order_model ) ? $order_model->payment_method : $order_model['payment_method'], 'items' => $appointment_details, 'payment_intent_id' => $payment_intent['payment_intent'] ?? '', 'client_secret' => $payment_intent['client_secret'] ?? '', 'total_services' => $input_data['total_services'] ?? 1, ); $order_data = apply_filters( 'booktics_pro_order_format', $order_data, $order_model ); return $order_data; } /** * Helper: Send notifications for order and appointment creation * * @param Order_Model $order_model * @param int $customer_id * @param array $appointments * * @return void */ private function send_notifications( $db, $order_model, $customer_id, $appointments ): void { $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $customer_id ) ); foreach ( $appointments as $appointment ) { $formatted_appointment = $this->format_appointment( $appointment ); $appointment_time = $formatted_appointment['date'] . ' ' . $formatted_appointment['start_time']; $dt = new DateTime( $appointment_time, wp_timezone() ); $timestamp = $dt->getTimestamp(); $appointment_datetime = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp, wp_timezone() ); $notification_flow_data_for_appointment = array( 'appointment_service' => $formatted_appointment['service']['title'] ?? '', 'appointment_team_member' => $formatted_appointment['team']['display_name'] ?? '', 'start_time' => $formatted_appointment['start_time'], 'appointment_date' => $formatted_appointment['date'], 'appointment_time' => $appointment_datetime, 'attendee_count' => $formatted_appointment['attendee_count'] ?? null, 'customer_name' => $customer->name ?? '', 'user_email' => $customer->email ?? '', 'appointment_team_member_email' => $formatted_appointment['team']['user_email'] ?? '', ); $notification_flow_data_for_appointment = apply_filters( 'booktics_appointment_scheduled_notification_flow_data', $notification_flow_data_for_appointment, $formatted_appointment ); do_action( 'global_notification_hook', 'appointment_scheduled', $notification_flow_data_for_appointment ); } $appointment = $appointments[0]; // As currently only one appointment is created for each user $formatted_appointment = $this->format_appointment( $appointment ); $notification_flow_data_for_order = [ 'order_no' => $order_model->order_no ?? '', 'order_date' => $order_model->created_at ?? '', 'order_total' => $order_model->total ?? 0, 'customer_name' => $customer->name ?? '', 'user_email' => $customer->email ?? '', 'order_status' => $order_model->status ?? '', 'service_name' => $formatted_appointment['service']['title'] ?? '', 'team_name' => $formatted_appointment['team']['display_name'] ?? '', 'start_time' => $formatted_appointment['start_time'] ?? '', 'end_time' => $formatted_appointment['end_time'] ?? '', 'appointment_date' => $formatted_appointment['date'] ?? '', 'location_address' => $formatted_appointment['location']['address'] ?? '', 'appointment_status' => $formatted_appointment['status'] ?? '', 'appointment_cancel_url' => $appointment->get_cancel_link(), 'appointment_reschedule_url' => $appointment->get_reschedule_link(), ]; $notification_flow_data_for_order = apply_filters( 'booktics_order_created_notification_flow_data', $notification_flow_data_for_order, $formatted_appointment, $order_model ); do_action( 'global_notification_hook', 'order_created', $notification_flow_data_for_order ); } /** * Find existing customer by email or create new customer * * @param array $customer * * @return int|WP_Error */ private function find_or_create_customer( array $customer ) { $db = new Booktics_Database(); if ( ! isset( $customer['email'] ) ) { return new WP_Error( 'invalid_email', __( 'Customer email is required', 'booktics' ) ); } $user_login = User_Model::generate_username( $customer['email'] ); $allow_customer_as_wp_user = booktics_get_option( 'allow_customer_as_wp_user', false ); $wp_user_id = null; if ( $allow_customer_as_wp_user ) { $wp_user_id = $this->get_or_create_wp_user( $customer, $user_login ); } $guest = $this->find_and_update_guest( $db, $customer, $wp_user_id ); if ( $guest ) { return $guest->id; } $guest = $this->create_guest( $db, $customer, $user_login, $wp_user_id ); return $guest->id; } /** * Get or create a WordPress user for the customer * * @param array $customer * @param string $user_login * * @return int WP user ID */ private function get_or_create_wp_user( $customer, $user_login ) { $wp_user = get_user_by( 'email', $customer['email'] ); if ( $wp_user ) { return $wp_user->ID; } $userdata = array( 'user_login' => $user_login, 'user_email' => $customer['email'], 'first_name' => $customer['name'] ?? '', 'display_name' => $customer['name'] ?? '', 'phone' => $customer['phone'] ?? '', ); $customer_model = new Customer_Model(); $customer_id = $customer_model->save( $userdata ); ( new Customer_Event_Handler() )->on_customer_create( $customer_id ); return $customer_id; } /** * Find and update a guest record by email * * @param Booktics_Database $db * @param array $customer * * @return object|null Guest_Model instance or null */ private function find_and_update_guest( $db, $customer, $wp_user_id = null ) { $guest = ( new Guest_Model( $db ) )->find( array( 'email' => $customer['email'] ) ); if ( $guest ) { ( new Guest_Model( $db ) )->update( $guest->id, array( 'name' => $customer['name'] ?? '', 'phone' => $customer['phone'] ?? '', 'wp_user_id' => $wp_user_id, ) ); } return $guest; } /** * Create a new guest record * * @param Booktics_Database $db * @param array $customer * @param string $user_login * @param int|null $wp_user_id * * @return object Guest_Model instance */ private function create_guest( $db, $customer, $user_login, $wp_user_id ) { return ( new Guest_Model( $db ) )->create( array( 'wp_user_id' => $wp_user_id, 'email' => $customer['email'], 'name' => $customer['name'] ?? '', 'user_login' => $user_login, 'phone' => $customer['phone'] ?? '', ) ); } /** * Check if user has permission to get orders. * * @param WP_REST_Request $request * * @return bool */ public function get_items_permission( $request ) { return true; } /** * Get paginated list of orders. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function get_items( $request ) { $page = (int) $request->get_param( 'paged' ) == 0 ? 1 : $request->get_param( 'paged' ); $per_page = (int) $request->get_param( 'per_page' ) == 0 ? 10 : $request->get_param( 'per_page' ); $payment_status = $request->get_param( 'payment_status' ); $payment_method = $request->get_param( 'payment_method' ); $order_date = $request->get_param( 'order_date' ); $team_member = (int) $request->get_param( 'team_member' ); $service = (int) $request->get_param( 'service' ); $search = $request->get_param( 'search' ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $conditions = array(); if ( $payment_status ) { $conditions['payment_status'] = $payment_status; } if ( $payment_method ) { $conditions['payment_method'] = $payment_method; } if ( $order_date ) { $conditions['DATE(created_at)'] = array( '=', $order_date ); } if ( $search ) { $conditions['order_no'] = array( 'LIKE', '%' . $search . '%' ); } $conditions = apply_filters( 'booktics_order_filters', $conditions ); $orders = $order_model->get( $conditions, $page, $per_page, array( 'created_at', 'DESC', ) ); if ( $team_member || $service ) { foreach ( $orders['items'] as $key => $order ) { $appointments = ( new Appointment_Model() )->where( 'order_id', $order->id ); $keep_order = false; foreach ( $appointments as $appointment ) { if ( $team_member && $appointment->team_member_id == $team_member ) { $keep_order = true; } if ( $service && $appointment->service_id == $service ) { $keep_order = true; } } if ( ! $keep_order && ( $team_member || $service ) ) { unset( $orders['items'][ $key ] ); --$orders['total']; } } $orders['items'] = array_values( $orders['items'] ); $orders['total_pages'] = ceil( $orders['total'] / $per_page ); } foreach ( $orders['items'] as &$order ) { $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $order->customer_id ) ); $order->customer = array( 'id' => $customer ? $customer->id : null, 'email' => $customer ? $customer->email : '', 'name' => $customer ? $customer->name : '', 'phone' => $customer ? $customer->phone : '', ); $order->total_services = ( new Appointment_Model() )->get_appointments_count_by_order_id( $order->id ); } return $this->response( $orders, __( 'Orders fetched successfully', 'booktics' ) ); } /** * Check if user has permission to update order. * * @param WP_REST_Request $request * * @return bool */ public function update_item_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Update an existing order. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function update_item( $request ) { $order_id = (int) $request->get_param( 'id' ); $data = json_decode( $request->get_body(), true ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); if ( ! $order ) { return $this->error( __( 'Order not found', 'booktics' ) ); } $order_model->update( $order_id, array( 'status' => $data['status'] ?? $order->status, 'payment_status' => $data['payment_status'] ?? $order->payment_status, ) ); if ( isset( $data['customer'] ) && isset( $data['customer']['id']) ) { ( new Guest_Model( $db ) )->update( $data['customer']['id'], $data['customer'] ); } $order_model = $order_model->find( array( 'id' => $order_id ) ); $data['customer_id'] = $order_model->customer_id; $response = $this->format_order_response($db, $order_model, $data, null); return $this->response( $response, __( 'Order updated successfully', 'booktics' ) ); } /** * Check if user has permission to get order. * * @param WP_REST_Request $request * * @return bool */ public function get_item_permission( $request ) { return true; } /** * Get single order. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function get_item( $request ) { $order_id = (int) $request->get_param( 'id' ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); if ( ! $order ) { return $this->error( __( 'Order not found', 'booktics' ) ); } // Use the format_order_response helper for consistency $input_data = array( 'customer_id' => $order->customer_id ); $response = $this->format_order_response( $db, $order, $input_data, array() ); // Add extra fields specific to GET if needed $response['status'] = $order->status; $response['created_at'] = $order->created_at; $response['payment_gateway'] = $order->payment_method; return $this->response( $response, __( 'Order fetched successfully', 'booktics' ) ); } /** * Check if user has permission to update payment status. * * @param WP_REST_Request $request * * @return bool */ public function update_payment_status_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Update payment status for an order. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function update_payment_status( $request ) { $data = json_decode( $request->get_body(), true ); $order_id = ! empty( $data['order_id'] ) ? intval( $data['order_id'] ) : 0; $payment_status = ! empty( $data['payment_status'] ) ? $data['payment_status'] : 0; $payment_method = ! empty( $data['payment_method'] ) ? $data['payment_method'] : null; $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); if ( ! $order ) { return $this->error( __( 'Order not found', 'booktics' ) ); } if ( ! array_key_exists( $payment_method, ( new Payment_Method_Manager() )->get_payment_methods() ) ) { return $this->error( __( 'Unauthorized payment method', 'booktics' ) ); } try { $payment_handler = ( new Payment_Method_Manager() )->get_payment_method( $payment_method ); if ( ! $payment_handler ) { return $this->error( __( 'Payment method not found', 'booktics' ) ); } $status = $payment_handler->get_payment_status( $order ); if ( is_wp_error( $status ) ) { return $this->error( $status->get_error_message() ); } $order_model->update( $order_id, array( 'payment_status' => $status, 'transaction_id' => $data['transaction_id'] ?? null, ) ); // Payment model status and transaction id update return $this->response( array( 'payment_status' => $status, ), __( 'Payment status updated successfully', 'booktics' ) ); } catch ( \Exception $e ) { return $this->error( $e->getMessage() ); } } /** * Check if user has permission to delete order. * * @param WP_REST_Request $request * * @return bool */ public function delete_item_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Delete an order. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function delete_item( $request ) { $order_id = (int) $request->get_param( 'id' ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); if ( ! $order ) { return $this->error( __( 'Order not found', 'booktics' ) ); } try { $db->begin_transaction(); // 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 ); $db->commit(); return $this->response( array(), __( 'Order deleted successfully', 'booktics' ) ); } catch ( Exception $e ) { $db->roll_back(); return $this->error( $e->getMessage() ); } } /** * Check if user has permission to bulk delete orders. * * @param WP_REST_Request $request * * @return bool */ public function bulk_delete_items_permission( $request ) { return current_user_can( 'manage_options' ); } /** * Bulk delete orders. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function bulk_delete_items( $request ) { $order_ids = array_map( 'intval', $request->get_param( 'ids' ) ); $db = new Booktics_Database(); try { $db->begin_transaction(); foreach ( $order_ids as $order_id ) { $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); if ( ! $order ) { continue; } // 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 ); } $db->commit(); return $this->response( array(), __( 'Orders deleted successfully', 'booktics' ) ); } catch ( Exception $e ) { $db->roll_back(); return $this->error( $e->getMessage() ); } } /** * Process payment refund for an order. * * @param WP_REST_Request $request * * @return WP_Error|WP_HTTP_Response */ public function refund_payment( $request ) { $order_id = (int) $request->get_param( 'id' ); $db = new Booktics_Database(); $order_model = new Order_Model( $db ); $order = $order_model->find( array( 'id' => $order_id ) ); $payment_method = ( new Payment_Method_Manager() )->get_payment_method( $order->payment_method ); if ( ! $payment_method ) { return $this->error( __( 'Invalid payment method!', 'booktics' ) ); } $response = $payment_method->process_refund( $order ); if ( $response['status'] === 'error' ) { return $this->error( $response['message'] ); } $order_model->update( $order_id, array( 'payment_status' => Payment_Status::REFUNDED, ) ); return $this->response( $order_model->find( array( 'id' => $order_id ) ), __( 'Refund successful', 'booktics' ) ); } /** * Calculate total duration in minutes for a service appointment. * * @param array $item * * @return int */ private function calculate_total_duration( $item ) { $service_model = Service_Model::find( $item['service_id'] ); if ( ! $service_model ) { return 0; } $service_duration = array_filter( $service_model->additional_durations, function ( $duration ) use ( $item ) { return $item['additional_duration_id'] === $duration['id']; } ); if ( empty( $service_duration ) ) { return 0; } $service_duration = reset( $service_duration ); $total_duration_min = $service_duration['duration_unit'] === 'min' ? $service_duration['duration_time'] : ( $service_duration['duration_time'] * 60 ); if ( ! empty( $item['extra_services'] ) ) { $total_duration_min = apply_filters( 'booktics_extra_service_duration', $total_duration_min, $item['extra_services'] ); } $total_duration_min = apply_filters( 'booktics_calculate_total_duration', $total_duration_min, $item ); return $total_duration_min; } }