* @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Endpoints; use Care_API\Services\Database\Appointment_Service; use Care_API\Services\Auth_Service; use Care_API\Utils\Input_Validator; use Care_API\Utils\Error_Handler; use WP_REST_Request; use WP_REST_Response; use WP_Error; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Appointment_Endpoints * * REST API endpoints for appointment management * * @since 1.0.0 */ class Appointment_Endpoints { /** * API namespace * * @var string */ private const NAMESPACE = 'kivicare/v1'; /** * Register all appointment endpoints * * @since 1.0.0 */ public static function register_routes() { // Get appointments (list with filters) register_rest_route( self::NAMESPACE, '/appointments', array( 'methods' => 'GET', 'callback' => array( self::class, 'get_appointments' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => self::get_appointments_args() ) ); // Create appointment register_rest_route( self::NAMESPACE, '/appointments', array( 'methods' => 'POST', 'callback' => array( self::class, 'create_appointment' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => self::get_create_appointment_args() ) ); // Get single appointment register_rest_route( self::NAMESPACE, '/appointments/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( self::class, 'get_appointment' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => array( 'id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ) ) ) ); // Update appointment register_rest_route( self::NAMESPACE, '/appointments/(?P\d+)', array( 'methods' => 'PUT', 'callback' => array( self::class, 'update_appointment' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => self::get_update_appointment_args() ) ); // Cancel appointment register_rest_route( self::NAMESPACE, '/appointments/(?P\d+)/cancel', array( 'methods' => 'POST', 'callback' => array( self::class, 'cancel_appointment' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => array( 'id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'reason' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_textarea_field' ) ) ) ); // Complete appointment register_rest_route( self::NAMESPACE, '/appointments/(?P\d+)/complete', array( 'methods' => 'POST', 'callback' => array( self::class, 'complete_appointment' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => array( 'id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'notes' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_textarea_field' ) ) ) ); // Get doctor availability register_rest_route( self::NAMESPACE, '/appointments/availability/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( self::class, 'get_doctor_availability' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => array( 'doctor_id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'start_date' => array( 'required' => true, 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'end_date' => array( 'required' => true, 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ) ) ) ); // Search appointments register_rest_route( self::NAMESPACE, '/appointments/search', array( 'methods' => 'GET', 'callback' => array( self::class, 'search_appointments' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => self::get_search_args() ) ); // Bulk operations register_rest_route( self::NAMESPACE, '/appointments/bulk', array( 'methods' => 'POST', 'callback' => array( self::class, 'bulk_operations' ), 'permission_callback' => array( Auth_Service::class, 'check_authentication' ), 'args' => self::get_bulk_operation_args() ) ); } /** * Get appointments list * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function get_appointments( WP_REST_Request $request ) { try { $params = $request->get_params(); // Build filters array $filters = array( 'limit' => $params['per_page'] ?? 20, 'offset' => ( ( $params['page'] ?? 1 ) - 1 ) * ( $params['per_page'] ?? 20 ) ); // Add filters based on parameters if ( ! empty( $params['start_date'] ) ) { $filters['start_date'] = sanitize_text_field( $params['start_date'] ); } if ( ! empty( $params['end_date'] ) ) { $filters['end_date'] = sanitize_text_field( $params['end_date'] ); } if ( ! empty( $params['doctor_id'] ) ) { $filters['doctor_id'] = absint( $params['doctor_id'] ); } if ( ! empty( $params['patient_id'] ) ) { $filters['patient_id'] = absint( $params['patient_id'] ); } if ( ! empty( $params['clinic_id'] ) ) { $filters['clinic_id'] = absint( $params['clinic_id'] ); } if ( isset( $params['status'] ) ) { $status = $params['status']; if ( is_array( $status ) ) { $filters['status'] = array_map( 'absint', $status ); } else { $filters['status'] = absint( $status ); } } if ( ! empty( $params['search'] ) ) { $filters['search'] = sanitize_text_field( $params['search'] ); } $result = Appointment_Service::search_appointments( $filters ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'data' => $result['appointments'], 'pagination' => array( 'total' => $result['total'], 'page' => $params['page'] ?? 1, 'per_page' => $params['per_page'] ?? 20, 'has_more' => $result['has_more'] ) ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Create a new appointment * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function create_appointment( WP_REST_Request $request ) { try { $data = $request->get_json_params(); // Validate required fields $validation = Input_Validator::validate_appointment_data( $data, 'create' ); if ( is_wp_error( $validation ) ) { return $validation; } // Sanitize input data $appointment_data = self::sanitize_appointment_data( $data ); $result = Appointment_Service::create_appointment( $appointment_data ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'message' => 'Appointment created successfully', 'data' => $result ), 201 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Get single appointment * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function get_appointment( WP_REST_Request $request ) { try { $appointment_id = $request['id']; $result = Appointment_Service::get_appointment_with_metadata( $appointment_id ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'data' => $result ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Update appointment * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function update_appointment( WP_REST_Request $request ) { try { $appointment_id = $request['id']; $data = $request->get_json_params(); // Validate input data $validation = Input_Validator::validate_appointment_data( $data, 'update' ); if ( is_wp_error( $validation ) ) { return $validation; } // Sanitize input data $appointment_data = self::sanitize_appointment_data( $data ); $result = Appointment_Service::update_appointment( $appointment_id, $appointment_data ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'message' => 'Appointment updated successfully', 'data' => $result ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Cancel appointment * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function cancel_appointment( WP_REST_Request $request ) { try { $appointment_id = $request['id']; $data = $request->get_json_params(); $reason = sanitize_textarea_field( $data['reason'] ?? '' ); $result = Appointment_Service::cancel_appointment( $appointment_id, $reason ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'message' => 'Appointment cancelled successfully', 'data' => $result ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Complete appointment * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function complete_appointment( WP_REST_Request $request ) { try { $appointment_id = $request['id']; $data = $request->get_json_params(); $completion_data = array(); if ( ! empty( $data['notes'] ) ) { $completion_data['completion_notes'] = sanitize_textarea_field( $data['notes'] ); } $result = Appointment_Service::complete_appointment( $appointment_id, $completion_data ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'message' => 'Appointment completed successfully', 'data' => $result ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Get doctor availability * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function get_doctor_availability( WP_REST_Request $request ) { try { $doctor_id = $request['doctor_id']; $start_date = $request['start_date']; $end_date = $request['end_date']; $result = Appointment_Service::get_doctor_availability( $doctor_id, $start_date, $end_date ); return new WP_REST_Response( array( 'success' => true, 'data' => $result ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Search appointments * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function search_appointments( WP_REST_Request $request ) { try { $params = $request->get_params(); // Build filters array $filters = array(); if ( ! empty( $params['q'] ) ) { $filters['search'] = sanitize_text_field( $params['q'] ); } if ( ! empty( $params['start_date'] ) ) { $filters['start_date'] = sanitize_text_field( $params['start_date'] ); } if ( ! empty( $params['end_date'] ) ) { $filters['end_date'] = sanitize_text_field( $params['end_date'] ); } if ( ! empty( $params['doctor_id'] ) ) { $filters['doctor_id'] = absint( $params['doctor_id'] ); } if ( ! empty( $params['patient_id'] ) ) { $filters['patient_id'] = absint( $params['patient_id'] ); } if ( ! empty( $params['clinic_id'] ) ) { $filters['clinic_id'] = absint( $params['clinic_id'] ); } if ( isset( $params['status'] ) ) { $filters['status'] = absint( $params['status'] ); } $filters['limit'] = $params['per_page'] ?? 20; $filters['offset'] = ( ( $params['page'] ?? 1 ) - 1 ) * ( $params['per_page'] ?? 20 ); $result = Appointment_Service::search_appointments( $filters ); if ( is_wp_error( $result ) ) { return Error_Handler::handle_service_error( $result ); } return new WP_REST_Response( array( 'success' => true, 'data' => $result['appointments'], 'pagination' => array( 'total' => $result['total'], 'page' => $params['page'] ?? 1, 'per_page' => $params['per_page'] ?? 20, 'has_more' => $result['has_more'] ) ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Bulk operations on appointments * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error * @since 1.0.0 */ public static function bulk_operations( WP_REST_Request $request ) { try { $data = $request->get_json_params(); $action = sanitize_text_field( $data['action'] ?? '' ); $appointment_ids = array_map( 'absint', $data['appointment_ids'] ?? array() ); if ( empty( $action ) || empty( $appointment_ids ) ) { return new WP_Error( 'invalid_bulk_data', 'Action and appointment IDs are required', array( 'status' => 400 ) ); } $results = array(); $errors = array(); switch ( $action ) { case 'cancel': $reason = sanitize_textarea_field( $data['reason'] ?? 'Bulk cancellation' ); foreach ( $appointment_ids as $appointment_id ) { $result = Appointment_Service::cancel_appointment( $appointment_id, $reason ); if ( is_wp_error( $result ) ) { $errors[] = array( 'id' => $appointment_id, 'error' => $result->get_error_message() ); } else { $results[] = array( 'id' => $appointment_id, 'status' => 'cancelled' ); } } break; case 'complete': $completion_data = array(); if ( ! empty( $data['notes'] ) ) { $completion_data['completion_notes'] = sanitize_textarea_field( $data['notes'] ); } foreach ( $appointment_ids as $appointment_id ) { $result = Appointment_Service::complete_appointment( $appointment_id, $completion_data ); if ( is_wp_error( $result ) ) { $errors[] = array( 'id' => $appointment_id, 'error' => $result->get_error_message() ); } else { $results[] = array( 'id' => $appointment_id, 'status' => 'completed' ); } } break; default: return new WP_Error( 'invalid_bulk_action', 'Invalid bulk action', array( 'status' => 400 ) ); } return new WP_REST_Response( array( 'success' => true, 'message' => 'Bulk operation completed', 'results' => $results, 'errors' => $errors ), 200 ); } catch ( Exception $e ) { return Error_Handler::handle_exception( $e ); } } /** * Sanitize appointment data * * @param array $data Raw data * @return array Sanitized data * @since 1.0.0 */ private static function sanitize_appointment_data( $data ) { $sanitized = array(); if ( isset( $data['patient_id'] ) ) { $sanitized['patient_id'] = absint( $data['patient_id'] ); } if ( isset( $data['doctor_id'] ) ) { $sanitized['doctor_id'] = absint( $data['doctor_id'] ); } if ( isset( $data['clinic_id'] ) ) { $sanitized['clinic_id'] = absint( $data['clinic_id'] ); } if ( isset( $data['service_id'] ) ) { $sanitized['service_id'] = absint( $data['service_id'] ); } $text_fields = array( 'appointment_start_date', 'appointment_start_time', 'appointment_end_time', 'description' ); foreach ( $text_fields as $field ) { if ( isset( $data[$field] ) ) { $sanitized[$field] = sanitize_text_field( $data[$field] ); } } if ( isset( $data['duration'] ) ) { $sanitized['duration'] = absint( $data['duration'] ); } if ( isset( $data['status'] ) ) { $sanitized['status'] = absint( $data['status'] ); } return $sanitized; } /** * Validate date format * * @param string $date Date string * @return bool Valid or not * @since 1.0.0 */ private static function validate_date( $date ) { $d = \DateTime::createFromFormat( 'Y-m-d', $date ); return $d && $d->format( 'Y-m-d' ) === $date; } /** * Get arguments for appointments list endpoint * * @return array * @since 1.0.0 */ private static function get_appointments_args() { return array( 'page' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint', 'default' => 1 ), 'per_page' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0 && $param <= 100; }, 'sanitize_callback' => 'absint', 'default' => 20 ), 'start_date' => array( 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'end_date' => array( 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'doctor_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'patient_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'clinic_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'status' => array( 'validate_callback' => function( $param ) { if ( is_array( $param ) ) { return ! empty( $param ); } return is_numeric( $param ); }, 'sanitize_callback' => function( $param ) { if ( is_array( $param ) ) { return array_map( 'absint', $param ); } return absint( $param ); } ), 'search' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_text_field' ) ); } /** * Get arguments for create appointment endpoint * * @return array * @since 1.0.0 */ private static function get_create_appointment_args() { return array( 'patient_id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint' ), 'doctor_id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint' ), 'clinic_id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint' ), 'appointment_start_date' => array( 'required' => true, 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'appointment_start_time' => array( 'required' => true, 'validate_callback' => function( $param ) { return preg_match( '/^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/', $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'appointment_end_time' => array( 'validate_callback' => function( $param ) { return preg_match( '/^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/', $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'duration' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint' ), 'service_id' => array( 'validate_callback' => function( $param ) { return empty( $param ) || is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'description' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_textarea_field' ) ); } /** * Get arguments for update appointment endpoint * * @return array * @since 1.0.0 */ private static function get_update_appointment_args() { $args = self::get_create_appointment_args(); // Make all fields optional for update foreach ( $args as &$arg ) { $arg['required'] = false; } return $args; } /** * Get arguments for search endpoint * * @return array * @since 1.0.0 */ private static function get_search_args() { return array( 'q' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_text_field' ), 'start_date' => array( 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'end_date' => array( 'validate_callback' => function( $param ) { return self::validate_date( $param ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'doctor_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'patient_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'clinic_id' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'status' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint' ), 'page' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, 'sanitize_callback' => 'absint', 'default' => 1 ), 'per_page' => array( 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0 && $param <= 100; }, 'sanitize_callback' => 'absint', 'default' => 20 ) ); } /** * Get arguments for bulk operations endpoint * * @return array * @since 1.0.0 */ private static function get_bulk_operation_args() { return array( 'action' => array( 'required' => true, 'validate_callback' => function( $param ) { return in_array( $param, array( 'cancel', 'complete' ) ); }, 'sanitize_callback' => 'sanitize_text_field' ), 'appointment_ids' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_array( $param ) && ! empty( $param ); }, 'sanitize_callback' => function( $param ) { return array_map( 'absint', $param ); } ), 'reason' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_textarea_field' ), 'notes' => array( 'validate_callback' => 'rest_validate_request_arg', 'sanitize_callback' => 'sanitize_textarea_field' ) ); } }