getInputs(); $mid = (int) ( $inputs['meeting_id'] ?? 0 ); $name = $mid > 0 ? get_the_title( $mid ) : ''; $meeting = '' !== $name ? "\"{$name}\" (ID: {$mid})" : "#{$mid}"; $who = trim( (string) ( $inputs['customer_first_name'] ?? '' ) . ' ' . (string) ( $inputs['customer_last_name'] ?? '' ) ); $who = '' !== $who ? $who : (string) ( $inputs['customer_email'] ?? 'a customer' ); $when = trim( (string) ( $inputs['start_date'] ?? '' ) . ' ' . (string) ( $inputs['start_time'] ?? '' ) ); return "Book {$who} into meeting {$meeting} on {$when}"; } public function get_approval_inputs(): array { $inputs = $this->getInputs(); if ( isset( $inputs['meeting_id'] ) ) { $mid = (int) $inputs['meeting_id']; $title = get_the_title( $mid ); $inputs['meeting_id'] = '' !== $title ? "{$title} (ID: {$mid})" : (string) $mid; } return $inputs; } public function __invoke( int $meeting_id, string $customer_email, string $customer_first_name, string $start_date, string $start_time, ?string $customer_last_name = null, ?string $customer_phone = null, ?int $staff_id = null, ?string $timezone = null, ?string $location_type = null, ?string $payment_method = null, ?string $status = null ): array { $check = $this->guard_admin(); if ( $check ) { return $check; } if ( $meeting_id <= 0 || ! class_exists( '\Timetics\Core\Appointments\Appointment' ) ) { return [ 'error' => 'A valid meeting_id is required.' ]; } $meeting = new \Timetics\Core\Appointments\Appointment( $meeting_id ); if ( ! $meeting->is_appointment() ) { return [ 'error' => "Meeting #{$meeting_id} was not found." ]; } if ( ! is_email( $customer_email ) ) { return [ 'error' => 'A valid customer_email is required.' ]; } if ( '' === trim( $customer_first_name ) ) { return [ 'error' => 'customer_first_name cannot be empty.' ]; } if ( '' === trim( $start_date ) ) { return [ 'error' => 'start_date is required (YYYY-MM-DD).' ]; } // Resolve the staff member. $staff_ids = array_map( 'intval', $meeting->get_staff_ids() ); if ( empty( $staff_ids ) ) { return [ 'error' => "Meeting #{$meeting_id} has no staff assigned and cannot be booked. Assign staff to it in Timetics first." ]; } if ( null !== $staff_id && $staff_id > 0 ) { if ( ! in_array( $staff_id, $staff_ids, true ) ) { return [ 'error' => "Staff #{$staff_id} is not assigned to meeting #{$meeting_id}. Assigned staff: " . implode( ', ', $staff_ids ) . '.' ]; } } elseif ( 1 === count( $staff_ids ) ) { $staff_id = (int) $staff_ids[0]; } else { return [ 'error' => "Meeting #{$meeting_id} has multiple staff; specify staff_id. Options: " . implode( ', ', $staff_ids ) . '.' ]; } // Resolve the location from the meeting's configured locations. $location = $this->resolve_location( (array) $meeting->get_locations(), $location_type ); if ( isset( $location['error'] ) ) { return $location; } $timezone = $this->resolve_timezone( ( null !== $timezone && '' !== trim( $timezone ) ) ? $timezone : $meeting->get_timezone() ); $normalized = $this->normalize_slot_time( $start_time ); $start_date = sanitize_text_field( $start_date ); $payment_method = ( null !== $payment_method && '' !== trim( $payment_method ) ) ? sanitize_text_field( $payment_method ) : 'offline'; if ( '' === $normalized ) { return [ 'error' => "Could not parse start_time '{$start_time}'. Use a time like \"9:00am\"." ]; } if ( null !== $status && ! in_array( $status, self::STATUSES, true ) ) { return [ 'error' => "Invalid status '{$status}'. Accepted values: " . implode( ', ', self::STATUSES ) . '.' ]; } // Reject a start time the controller would reject, with a helpful hint. $slot_error = $this->validate_booking_slot( $meeting_id, $staff_id, $start_date, $normalized, $timezone ); if ( null !== $slot_error ) { return $slot_error; } try { $body = [ 'first_name' => sanitize_text_field( $customer_first_name ), 'last_name' => null !== $customer_last_name ? sanitize_text_field( $customer_last_name ) : '', 'email' => sanitize_email( $customer_email ), 'phone' => null !== $customer_phone ? sanitize_text_field( $customer_phone ) : '', 'appointment' => $meeting_id, 'staff' => $staff_id, 'start_date' => $start_date, 'end_date' => $start_date, 'start_time' => $normalized, 'end_time' => $this->compute_end_time( $normalized, $meeting_id ), 'timezone' => $timezone, 'location' => $location['location'], 'location_type' => $location['location_type'], 'payment_method' => $payment_method, ]; if ( null !== $status ) { $body['status'] = $status; } $request = $this->build_request( 'POST', '/timetics/v1/bookings', [], $body ); if ( is_array( $request ) ) { return $request; } $controller = \Timetics\Core\Bookings\Api_Booking::instance(); $data = $this->extract_response( $controller->create_item( $request ), 'Failed to create booking.' ); if ( isset( $data['error'] ) ) { return $data; } return [ 'success' => true, 'booking_id' => $data['id'] ?? null, 'booking' => $this->shape_booking( $data ), 'message' => 'Booking created successfully.', ]; } catch ( \Throwable $e ) { Log::error( 'Timetics_Create_Booking_Tool error', [ 'error' => $e->getMessage() ] ); return [ 'error' => 'Failed to create booking: ' . $e->getMessage() ]; } } /** * Pick a booking location out of a meeting's configured locations. * * @param array $locations Meeting locations (each { location_type, location }). * @param string|null $location_type Requested location type, or null for the first. * @return array { location_type, location } or [ 'error' => .. ]. */ private function resolve_location( array $locations, ?string $location_type ): array { $locations = array_values( array_filter( $locations, 'is_array' ) ); if ( empty( $locations ) ) { return [ 'error' => 'This meeting has no configured location, so it cannot be booked.' ]; } if ( null !== $location_type && '' !== trim( $location_type ) ) { foreach ( $locations as $loc ) { if ( ( $loc['location_type'] ?? '' ) === $location_type ) { return [ 'location_type' => (string) $location_type, 'location' => (string) ( $loc['location'] ?? '' ), ]; } } $types = array_map( fn( $l ) => $l['location_type'] ?? '', $locations ); return [ 'error' => "Location type '{$location_type}' is not configured on this meeting. Options: " . implode( ', ', array_filter( $types ) ) . '.' ]; } $first = $locations[0]; return [ 'location_type' => (string) ( $first['location_type'] ?? '' ), 'location' => (string) ( $first['location'] ?? '' ), ]; } }