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( '';
}
$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;
}
}