true ) ); // Room selector. wpcf7_add_form_tag( array( 'bnb_room_select', 'bnb_room_select*' ), array( self::class, 'render_room_select_tag' ), array( 'name-attr' => true ) ); // Check-in date. wpcf7_add_form_tag( array( 'bnb_date_checkin', 'bnb_date_checkin*' ), array( self::class, 'render_date_checkin_tag' ), array( 'name-attr' => true ) ); // Check-out date. wpcf7_add_form_tag( array( 'bnb_date_checkout', 'bnb_date_checkout*' ), array( self::class, 'render_date_checkout_tag' ), array( 'name-attr' => true ) ); // Guests count. wpcf7_add_form_tag( array( 'bnb_guests', 'bnb_guests*' ), array( self::class, 'render_guests_tag' ), array( 'name-attr' => true ) ); } /** * Render building select tag. * * @param \WPCF7_FormTag $tag Form tag object. * @return string HTML output. */ public static function render_building_select_tag( $tag ): string { if ( empty( $tag->name ) ) { return ''; } $validation_error = wpcf7_get_validation_error( $tag->name ); $class = wpcf7_form_controls_class( $tag->type ); if ( $validation_error ) { $class .= ' wpcf7-not-valid'; } $atts = array( 'class' => trim( $class . ' wp-bnb-building-select' ), 'id' => $tag->get_id_option(), 'name' => $tag->name, 'aria-required' => $tag->is_required() ? 'true' : 'false', 'aria-invalid' => $validation_error ? 'true' : 'false', 'data-bnb-building-select' => 'true', ); // Get first_as_label option. $first_label = __( '-- Select Building --', 'wp-bnb' ); foreach ( $tag->options as $option ) { if ( strpos( $option, 'first_as_label:' ) === 0 ) { $first_label = str_replace( array( 'first_as_label:', '"', "'" ), '', $option ); break; } } // Get buildings that have rooms. $buildings = get_posts( array( 'post_type' => Building::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', ) ); // Build options. $options = sprintf( '', esc_html( $first_label ) ); foreach ( $buildings as $building ) { $room_count = count( Room::get_rooms_for_building( $building->ID ) ); if ( $room_count > 0 ) { $options .= sprintf( '', esc_attr( $building->ID ), esc_html( $building->post_title ) ); } } $atts_html = wpcf7_format_atts( $atts ); $html = sprintf( '', esc_attr( $tag->name ) ); $html .= sprintf( '', $atts_html, $options ); $html .= $validation_error; $html .= ''; return $html; } /** * Render room select tag. * * @param \WPCF7_FormTag $tag Form tag object. * @return string HTML output. */ public static function render_room_select_tag( $tag ): string { if ( empty( $tag->name ) ) { return ''; } $validation_error = wpcf7_get_validation_error( $tag->name ); $class = wpcf7_form_controls_class( $tag->type ); if ( $validation_error ) { $class .= ' wpcf7-not-valid'; } // Parse options. $building_field = ''; $include_price = false; foreach ( $tag->options as $option ) { if ( strpos( $option, 'building_field:' ) === 0 ) { $building_field = str_replace( array( 'building_field:', '"', "'" ), '', $option ); } if ( 'include_price:true' === $option || 'include_price' === $option ) { $include_price = true; } } $atts = array( 'class' => trim( $class . ' wp-bnb-room-select' ), 'id' => $tag->get_id_option(), 'name' => $tag->name, 'aria-required' => $tag->is_required() ? 'true' : 'false', 'aria-invalid' => $validation_error ? 'true' : 'false', 'data-bnb-room-select' => 'true', ); if ( $building_field ) { $atts['data-building-field'] = $building_field; } if ( $tag->is_required() ) { $atts['required'] = 'required'; } // Get all rooms grouped by building. $rooms = get_posts( array( 'post_type' => Room::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', ) ); $rooms_by_building = array(); foreach ( $rooms as $room ) { $building = Room::get_building( $room->ID ); $building_id = $building ? $building->ID : 0; if ( ! isset( $rooms_by_building[ $building_id ] ) ) { $rooms_by_building[ $building_id ] = array( 'name' => $building ? $building->post_title : __( 'No Building', 'wp-bnb' ), 'rooms' => array(), ); } $rooms_by_building[ $building_id ]['rooms'][] = $room; } // Build options. $options = sprintf( '', esc_html__( '-- Select Room --', 'wp-bnb' ) ); $currency = get_option( 'wp_bnb_currency', 'CHF' ); foreach ( $rooms_by_building as $building_id => $data ) { $options .= sprintf( '', esc_attr( $data['name'] ) ); foreach ( $data['rooms'] as $room ) { $capacity = (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ) ?: 2; $nightly_rate = (float) get_post_meta( $room->ID, '_bnb_room_price_short_term', true ); $room_status = get_post_meta( $room->ID, '_bnb_room_status', true ) ?: 'available'; // Skip rooms that are not available. if ( 'available' !== $room_status ) { continue; } $label = $room->post_title; $label .= sprintf( ' (%d %s)', $capacity, _n( 'guest', 'guests', $capacity, 'wp-bnb' ) ); if ( $include_price && $nightly_rate > 0 ) { $label .= sprintf( ' - %s %s/%s', $currency, number_format( $nightly_rate, 2 ), __( 'night', 'wp-bnb' ) ); } $options .= sprintf( '', esc_attr( $room->ID ), esc_attr( $capacity ), esc_attr( $building_id ), esc_attr( $nightly_rate ), esc_html( $label ) ); } $options .= ''; } $atts_html = wpcf7_format_atts( $atts ); $html = sprintf( '', esc_attr( $tag->name ) ); $html .= sprintf( '', $atts_html, $options ); $html .= $validation_error; $html .= ''; return $html; } /** * Render check-in date tag. * * @param \WPCF7_FormTag $tag Form tag object. * @return string HTML output. */ public static function render_date_checkin_tag( $tag ): string { if ( empty( $tag->name ) ) { return ''; } $validation_error = wpcf7_get_validation_error( $tag->name ); $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-date' ); if ( $validation_error ) { $class .= ' wpcf7-not-valid'; } // Parse options. $min_advance = 0; $max_advance = 365; foreach ( $tag->options as $option ) { if ( strpos( $option, 'min_advance:' ) === 0 ) { $min_advance = (int) str_replace( 'min_advance:', '', $option ); } if ( strpos( $option, 'max_advance:' ) === 0 ) { $max_advance = (int) str_replace( 'max_advance:', '', $option ); } } $min_date = gmdate( 'Y-m-d', strtotime( "+{$min_advance} days" ) ); $max_date = gmdate( 'Y-m-d', strtotime( "+{$max_advance} days" ) ); $atts = array( 'type' => 'date', 'class' => trim( $class . ' wp-bnb-date-checkin' ), 'id' => $tag->get_id_option(), 'name' => $tag->name, 'min' => $min_date, 'max' => $max_date, 'aria-required' => $tag->is_required() ? 'true' : 'false', 'aria-invalid' => $validation_error ? 'true' : 'false', 'data-bnb-checkin' => 'true', ); if ( $tag->is_required() ) { $atts['required'] = 'required'; } // Handle default value from POST. if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $atts['value'] = sanitize_text_field( wp_unslash( $_POST[ $tag->name ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } $atts_html = wpcf7_format_atts( $atts ); $html = sprintf( '', esc_attr( $tag->name ) ); $html .= sprintf( '', $atts_html ); $html .= $validation_error; $html .= ''; return $html; } /** * Render check-out date tag. * * @param \WPCF7_FormTag $tag Form tag object. * @return string HTML output. */ public static function render_date_checkout_tag( $tag ): string { if ( empty( $tag->name ) ) { return ''; } $validation_error = wpcf7_get_validation_error( $tag->name ); $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-date' ); if ( $validation_error ) { $class .= ' wpcf7-not-valid'; } // Parse options. $checkin_field = 'check_in'; $min_nights = 1; $max_nights = 365; foreach ( $tag->options as $option ) { if ( strpos( $option, 'checkin_field:' ) === 0 ) { $checkin_field = str_replace( array( 'checkin_field:', '"', "'" ), '', $option ); } if ( strpos( $option, 'min_nights:' ) === 0 ) { $min_nights = (int) str_replace( 'min_nights:', '', $option ); } if ( strpos( $option, 'max_nights:' ) === 0 ) { $max_nights = (int) str_replace( 'max_nights:', '', $option ); } } // Default min is tomorrow. $min_date = gmdate( 'Y-m-d', strtotime( '+1 day' ) ); $max_date = gmdate( 'Y-m-d', strtotime( '+366 days' ) ); $atts = array( 'type' => 'date', 'class' => trim( $class . ' wp-bnb-date-checkout' ), 'id' => $tag->get_id_option(), 'name' => $tag->name, 'min' => $min_date, 'max' => $max_date, 'aria-required' => $tag->is_required() ? 'true' : 'false', 'aria-invalid' => $validation_error ? 'true' : 'false', 'data-bnb-checkout' => 'true', 'data-checkin-field' => $checkin_field, 'data-min-nights' => $min_nights, 'data-max-nights' => $max_nights, ); if ( $tag->is_required() ) { $atts['required'] = 'required'; } // Handle default value from POST. if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $atts['value'] = sanitize_text_field( wp_unslash( $_POST[ $tag->name ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } $atts_html = wpcf7_format_atts( $atts ); $html = sprintf( '', esc_attr( $tag->name ) ); $html .= sprintf( '', $atts_html ); $html .= $validation_error; $html .= ''; return $html; } /** * Render guests count tag. * * @param \WPCF7_FormTag $tag Form tag object. * @return string HTML output. */ public static function render_guests_tag( $tag ): string { if ( empty( $tag->name ) ) { return ''; } $validation_error = wpcf7_get_validation_error( $tag->name ); $class = wpcf7_form_controls_class( $tag->type, 'wpcf7-number' ); if ( $validation_error ) { $class .= ' wpcf7-not-valid'; } // Parse options. $min = 1; $max = 10; $default = 1; $room_field = ''; foreach ( $tag->options as $option ) { if ( strpos( $option, 'min:' ) === 0 ) { $min = (int) str_replace( 'min:', '', $option ); } if ( strpos( $option, 'max:' ) === 0 ) { $max = (int) str_replace( 'max:', '', $option ); } if ( strpos( $option, 'default:' ) === 0 ) { $default = (int) str_replace( 'default:', '', $option ); } if ( strpos( $option, 'room_field:' ) === 0 ) { $room_field = str_replace( array( 'room_field:', '"', "'" ), '', $option ); } } $atts = array( 'type' => 'number', 'class' => trim( $class . ' wp-bnb-guests' ), 'id' => $tag->get_id_option(), 'name' => $tag->name, 'min' => $min, 'max' => $max, 'value' => $default, 'aria-required' => $tag->is_required() ? 'true' : 'false', 'aria-invalid' => $validation_error ? 'true' : 'false', 'data-bnb-guests' => 'true', ); if ( $room_field ) { $atts['data-room-field'] = $room_field; } if ( $tag->is_required() ) { $atts['required'] = 'required'; } // Handle default value from POST. if ( isset( $_POST[ $tag->name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $atts['value'] = absint( $_POST[ $tag->name ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } $atts_html = wpcf7_format_atts( $atts ); $html = sprintf( '', esc_attr( $tag->name ) ); $html .= sprintf( '', $atts_html ); $html .= $validation_error; $html .= ''; return $html; } /** * Validate room select field. * * @param \WPCF7_Validation $result Validation result. * @param \WPCF7_FormTag $tag Form tag. * @return \WPCF7_Validation */ public static function validate_room_select( $result, $tag ) { $name = $tag->name; $room_id = isset( $_POST[ $name ] ) ? absint( $_POST[ $name ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $tag->is_required() && ! $room_id ) { $result->invalidate( $tag, __( 'Please select a room.', 'wp-bnb' ) ); return $result; } if ( $room_id ) { $room = get_post( $room_id ); if ( ! $room || Room::POST_TYPE !== $room->post_type ) { $result->invalidate( $tag, __( 'Invalid room selected.', 'wp-bnb' ) ); } } return $result; } /** * Validate check-in date field. * * @param \WPCF7_Validation $result Validation result. * @param \WPCF7_FormTag $tag Form tag. * @return \WPCF7_Validation */ public static function validate_date_checkin( $result, $tag ) { $name = $tag->name; $date = isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $tag->is_required() && empty( $date ) ) { $result->invalidate( $tag, __( 'Please select a check-in date.', 'wp-bnb' ) ); return $result; } if ( $date ) { // Validate format. if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { $result->invalidate( $tag, __( 'Invalid date format.', 'wp-bnb' ) ); return $result; } // Validate not in past. $check_in = strtotime( $date ); $today = strtotime( 'today' ); if ( $check_in < $today ) { $result->invalidate( $tag, __( 'Check-in date cannot be in the past.', 'wp-bnb' ) ); } } return $result; } /** * Validate check-out date field. * * @param \WPCF7_Validation $result Validation result. * @param \WPCF7_FormTag $tag Form tag. * @return \WPCF7_Validation */ public static function validate_date_checkout( $result, $tag ) { $name = $tag->name; $check_out = isset( $_POST[ $name ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing // Try common check-in field names. $check_in = ''; $checkin_fields = array( 'check_in', 'checkin', 'check-in', 'arrival' ); foreach ( $checkin_fields as $field ) { if ( isset( $_POST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $check_in = sanitize_text_field( wp_unslash( $_POST[ $field ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing break; } } if ( $tag->is_required() && empty( $check_out ) ) { $result->invalidate( $tag, __( 'Please select a check-out date.', 'wp-bnb' ) ); return $result; } if ( $check_out ) { // Validate format. if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $check_out ) ) { $result->invalidate( $tag, __( 'Invalid date format.', 'wp-bnb' ) ); return $result; } // Validate check-out is after check-in. if ( $check_in && strtotime( $check_out ) <= strtotime( $check_in ) ) { $result->invalidate( $tag, __( 'Check-out must be after check-in.', 'wp-bnb' ) ); } } return $result; } /** * Validate guests field. * * @param \WPCF7_Validation $result Validation result. * @param \WPCF7_FormTag $tag Form tag. * @return \WPCF7_Validation */ public static function validate_guests( $result, $tag ) { $name = $tag->name; $guests = isset( $_POST[ $name ] ) ? absint( $_POST[ $name ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $tag->is_required() && $guests < 1 ) { $result->invalidate( $tag, __( 'Please enter number of guests.', 'wp-bnb' ) ); return $result; } // Try to get room_id for capacity validation. $room_id = 0; $room_fields = array( 'room', 'room_id', 'room-id' ); foreach ( $room_fields as $field ) { if ( isset( $_POST[ $field ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $room_id = absint( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing break; } } // Validate against room capacity if room is selected. if ( $room_id && $guests > 0 ) { $capacity = (int) get_post_meta( $room_id, '_bnb_room_capacity', true ); if ( $capacity > 0 && $guests > $capacity ) { $result->invalidate( $tag, sprintf( /* translators: %d: Maximum guest capacity */ __( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ), $capacity ) ); } } return $result; } /** * Validate availability before sending mail. * * @param \WPCF7_ContactForm $contact_form Contact form object. * @param bool $abort Whether to abort. * @param \WPCF7_Submission $submission Submission object. * @return void */ public static function validate_availability_before_mail( $contact_form, &$abort, $submission ): void { if ( ! self::is_booking_form( $contact_form ) ) { return; } $posted_data = $submission->get_posted_data(); // Get room and dates. $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' ); $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) ); $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) ); if ( ! $room_id || ! $check_in || ! $check_out ) { return; } // Check availability. if ( ! Availability::is_available( $room_id, $check_in, $check_out ) ) { $abort = true; $submission->set_status( 'validation_failed' ); $submission->set_response( __( 'Sorry, this room is not available for the selected dates. Please choose different dates.', 'wp-bnb' ) ); } } /** * Handle mail sent event - create booking. * * @param \WPCF7_ContactForm $contact_form Contact form object. * @return void */ public static function on_mail_sent( $contact_form ): void { if ( ! self::is_booking_form( $contact_form ) ) { return; } $submission = \WPCF7_Submission::get_instance(); if ( ! $submission ) { return; } $posted_data = $submission->get_posted_data(); // Extract booking data. $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' ); $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) ); $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) ); if ( ! $room_id || ! $check_in || ! $check_out ) { return; } // Guest data. $guest_name = self::get_field_value( $posted_data, array( 'your-name', 'name', 'guest_name', 'guest-name' ) ); $guest_email = self::get_field_value( $posted_data, array( 'your-email', 'email', 'guest_email', 'guest-email' ) ); $guest_phone = self::get_field_value( $posted_data, array( 'your-phone', 'phone', 'tel', 'guest_phone', 'guest-phone' ) ); // Guest counts. $guests = self::get_field_value( $posted_data, array( 'guests', 'guest_count', 'guest-count' ), 'int' ); $adults = self::get_field_value( $posted_data, array( 'adults', 'adult_count' ), 'int' ); $children = self::get_field_value( $posted_data, array( 'children', 'child_count' ), 'int' ); // If single guests field, use it as adults. if ( $guests && ! $adults ) { $adults = $guests; } if ( ! $adults ) { $adults = 1; } if ( ! $children ) { $children = 0; } // Notes from message field. $notes = self::get_field_value( $posted_data, array( 'your-message', 'message', 'notes', 'special_requests', 'special-requests' ) ); // Create the booking. $booking_id = self::create_booking( array( 'room_id' => $room_id, 'check_in' => $check_in, 'check_out' => $check_out, 'guest_name' => $guest_name, 'guest_email' => $guest_email, 'guest_phone' => $guest_phone, 'adults' => $adults, 'children' => $children, 'notes' => $notes, 'source' => 'cf7_form_' . $contact_form->id(), ) ); if ( $booking_id ) { // Store booking ID for potential use in mail tags. $submission->add_extra_var( 'bnb_booking_id', (string) $booking_id ); $submission->add_extra_var( 'bnb_booking_reference', get_the_title( $booking_id ) ); /** * Fires after a booking is created from a CF7 form. * * @param int $booking_id Created booking post ID. * @param \WPCF7_ContactForm $contact_form CF7 form object. * @param array $posted_data Form submission data. */ do_action( 'wp_bnb_cf7_booking_created', $booking_id, $contact_form, $posted_data ); } } /** * Create a booking from form data. * * @param array $data Booking data. * @return int|false Booking post ID or false on failure. */ private static function create_booking( array $data ) { // Find or create guest. $guest_id = null; if ( ! empty( $data['guest_name'] ) ) { $guest_id = self::find_or_create_guest( $data['guest_name'], $data['guest_email'] ?? '', $data['guest_phone'] ?? '' ); } // Calculate price. $price = 0; $breakdown = array(); if ( $data['room_id'] && $data['check_in'] && $data['check_out'] ) { try { $calculator = new Calculator( $data['room_id'], $data['check_in'], $data['check_out'] ); $price = $calculator->calculate(); $breakdown = $calculator->getBreakdown(); } catch ( \Exception $e ) { // Price calculation failed, continue without price. } } // Generate booking reference. $reference = Booking::generate_reference(); // Create booking post. $booking_id = wp_insert_post( array( 'post_type' => Booking::POST_TYPE, 'post_status' => 'publish', 'post_title' => $reference, ) ); if ( is_wp_error( $booking_id ) || ! $booking_id ) { return false; } // Save booking meta. update_post_meta( $booking_id, self::META_PREFIX . 'room_id', $data['room_id'] ); update_post_meta( $booking_id, self::META_PREFIX . 'check_in', $data['check_in'] ); update_post_meta( $booking_id, self::META_PREFIX . 'check_out', $data['check_out'] ); update_post_meta( $booking_id, self::META_PREFIX . 'status', 'pending' ); update_post_meta( $booking_id, self::META_PREFIX . 'guest_name', $data['guest_name'] ?? '' ); update_post_meta( $booking_id, self::META_PREFIX . 'guest_email', $data['guest_email'] ?? '' ); update_post_meta( $booking_id, self::META_PREFIX . 'guest_phone', $data['guest_phone'] ?? '' ); update_post_meta( $booking_id, self::META_PREFIX . 'adults', $data['adults'] ?? 1 ); update_post_meta( $booking_id, self::META_PREFIX . 'children', $data['children'] ?? 0 ); update_post_meta( $booking_id, self::META_PREFIX . 'guest_notes', $data['notes'] ?? '' ); update_post_meta( $booking_id, self::META_PREFIX . 'calculated_price', $price ); update_post_meta( $booking_id, self::META_PREFIX . 'price_breakdown', $breakdown ); update_post_meta( $booking_id, self::META_PREFIX . 'source', $data['source'] ?? 'cf7' ); if ( $guest_id ) { update_post_meta( $booking_id, self::META_PREFIX . 'guest_id', $guest_id ); } // Generate comprehensive title (Guest Name (dates)). self::update_booking_title( $booking_id, $data ); // Trigger the booking status changed action (for email notifications). do_action( 'wp_bnb_booking_status_changed', $booking_id, '', 'pending' ); return $booking_id; } /** * Update booking title with guest name and dates. * * @param int $booking_id Booking post ID. * @param array $data Booking data. * @return void */ private static function update_booking_title( int $booking_id, array $data ): void { $guest_name = $data['guest_name'] ?? __( 'Unknown Guest', 'wp-bnb' ); // Format dates. $date_part = ''; if ( ! empty( $data['check_in'] ) && ! empty( $data['check_out'] ) ) { $check_in_date = \DateTime::createFromFormat( 'Y-m-d', $data['check_in'] ); $check_out_date = \DateTime::createFromFormat( 'Y-m-d', $data['check_out'] ); if ( $check_in_date && $check_out_date ) { if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) { $date_part = sprintf( '%s - %s', $check_in_date->format( 'd.m' ), $check_out_date->format( 'd.m.Y' ) ); } else { $date_part = sprintf( '%s - %s', $check_in_date->format( 'd.m.Y' ), $check_out_date->format( 'd.m.Y' ) ); } } } $title = $guest_name; if ( $date_part ) { $title .= sprintf( ' (%s)', $date_part ); } // Update the post title directly. global $wpdb; $wpdb->update( $wpdb->posts, array( 'post_title' => $title ), array( 'ID' => $booking_id ), array( '%s' ), array( '%d' ) ); clean_post_cache( $booking_id ); } /** * Find an existing guest by email or create a new one. * * @param string $name Guest full name. * @param string $email Guest email. * @param string $phone Guest phone (optional). * @return int|null Guest post ID or null on failure. */ private static function find_or_create_guest( string $name, string $email, string $phone = '' ): ?int { if ( empty( $name ) ) { return null; } // Try to find existing guest by email. if ( ! empty( $email ) ) { $existing_guest = Guest::get_by_email( $email ); if ( $existing_guest ) { return $existing_guest->ID; } } // Parse name into first/last name. $name_parts = explode( ' ', trim( $name ), 2 ); $first_name = $name_parts[0] ?? ''; $last_name = $name_parts[1] ?? ''; // Create new guest post. $guest_id = wp_insert_post( array( 'post_type' => Guest::POST_TYPE, 'post_status' => 'publish', 'post_title' => $name, ) ); if ( is_wp_error( $guest_id ) || ! $guest_id ) { return null; } // Save guest meta. update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name ); update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name ); if ( ! empty( $email ) ) { update_post_meta( $guest_id, '_bnb_guest_email', $email ); } if ( ! empty( $phone ) ) { update_post_meta( $guest_id, '_bnb_guest_phone', $phone ); } // Set default status. update_post_meta( $guest_id, '_bnb_guest_status', 'active' ); return $guest_id; } /** * Check if a contact form is a booking form. * * @param \WPCF7_ContactForm $contact_form Contact form object. * @return bool */ private static function is_booking_form( $contact_form ): bool { // Check for CSS class. $additional_settings = $contact_form->additional_setting( 'class', false ); if ( is_array( $additional_settings ) ) { foreach ( $additional_settings as $setting ) { if ( strpos( $setting, self::BOOKING_FORM_CLASS ) !== false ) { return true; } } } // Auto-detect by checking for required booking fields. $form_content = $contact_form->prop( 'form' ); $has_room = strpos( $form_content, '[bnb_room_select' ) !== false; $has_checkin = strpos( $form_content, '[bnb_date_checkin' ) !== false; $has_checkout = strpos( $form_content, '[bnb_date_checkout' ) !== false; return $has_room && $has_checkin && $has_checkout; } /** * Get field value from posted data. * * @param array $posted_data Posted form data. * @param array $field_names Possible field names. * @param string $type Type cast ('int', 'string'). * @return mixed */ private static function get_field_value( array $posted_data, array $field_names, string $type = 'string' ) { foreach ( $field_names as $field ) { if ( isset( $posted_data[ $field ] ) && '' !== $posted_data[ $field ] ) { $value = is_array( $posted_data[ $field ] ) ? $posted_data[ $field ][0] : $posted_data[ $field ]; if ( 'int' === $type ) { return absint( $value ); } return sanitize_text_field( $value ); } } return 'int' === $type ? 0 : ''; } /** * Handle custom mail tags. * * @param string|null $output Output value. * @param string $name Tag name. * @param bool $html Whether HTML is allowed. * @param array $mail_tag Mail tag data. * @return string|null */ public static function custom_mail_tags( $output, $name, $html, $mail_tag ) { $submission = \WPCF7_Submission::get_instance(); if ( ! $submission ) { return $output; } switch ( $name ) { case '_bnb_booking_reference': return $submission->get_extra_var( 'bnb_booking_reference' ) ?: ''; case '_bnb_booking_id': return $submission->get_extra_var( 'bnb_booking_id' ) ?: ''; case '_bnb_room_name': $posted_data = $submission->get_posted_data(); $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' ); if ( $room_id ) { $room = get_post( $room_id ); return $room ? $room->post_title : ''; } return ''; case '_bnb_calculated_price': $posted_data = $submission->get_posted_data(); $room_id = self::get_field_value( $posted_data, array( 'room', 'room_id', 'room-id' ), 'int' ); $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) ); $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) ); if ( $room_id && $check_in && $check_out ) { try { $calculator = new Calculator( $room_id, $check_in, $check_out ); $price = $calculator->calculate(); return Calculator::formatPrice( $price ); } catch ( \Exception $e ) { return ''; } } return ''; case '_bnb_nights': $posted_data = $submission->get_posted_data(); $check_in = self::get_field_value( $posted_data, array( 'check_in', 'checkin', 'check-in', 'arrival' ) ); $check_out = self::get_field_value( $posted_data, array( 'check_out', 'checkout', 'check-out', 'departure' ) ); if ( $check_in && $check_out ) { $nights = Booking::calculate_nights( $check_in, $check_out ); return (string) $nights; } return ''; } return $output; } }