Files
wp-bnb/src/Integration/WooCommerce/OrderHandler.php
magdev 2865956c56
All checks were successful
Create Release Package / build-release (push) Successful in 1m11s
Add WooCommerce integration for payments, invoices, and order management (v0.11.0)
- Product sync: Virtual WC products for rooms with bidirectional linking
- Cart/Checkout: Booking data in cart items, availability validation, dynamic pricing
- Orders: Automatic booking creation on payment, status mapping, guest record creation
- Invoices: PDF generation via mPDF, auto-attach to emails, configurable numbering
- Refunds: Full refund cancels booking, partial refund records amount only
- Admin: Cross-linked columns and row actions between bookings and orders
- Settings: WooCommerce tab with subtabs (General, Products, Orders, Invoices)
- HPOS compatibility declared for High-Performance Order Storage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 22:40:36 +01:00

585 lines
18 KiB
PHP

<?php
/**
* WooCommerce Order Handler.
*
* Handles order-to-booking creation and status synchronization.
*
* @package Magdev\WpBnb\Integration\WooCommerce
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Integration\WooCommerce;
use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\PostTypes\Booking;
use Magdev\WpBnb\PostTypes\Guest;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\Pricing\Calculator;
/**
* Order Handler class.
*
* Creates bookings from WooCommerce orders on payment completion
* and synchronizes order/booking statuses.
*/
final class OrderHandler {
/**
* Flag to prevent recursive status updates.
*
* @var bool
*/
private static bool $updating_status = false;
/**
* Initialize order handler.
*
* @return void
*/
public static function init(): void {
// Create booking on payment completion.
add_action( 'woocommerce_payment_complete', array( self::class, 'on_payment_complete' ) );
// Also handle manual order status changes to processing/completed.
add_action( 'woocommerce_order_status_processing', array( self::class, 'on_order_processing' ) );
add_action( 'woocommerce_order_status_completed', array( self::class, 'on_order_completed' ) );
// Sync order status changes to booking.
add_action( 'woocommerce_order_status_changed', array( self::class, 'sync_order_status_to_booking' ), 10, 4 );
// Display booking info in admin order page.
add_action( 'woocommerce_admin_order_data_after_billing_address', array( self::class, 'display_booking_info_admin' ) );
// Display booking info on thank you page.
add_action( 'woocommerce_thankyou', array( self::class, 'display_booking_info_thankyou' ) );
// Display booking info in order details.
add_action( 'woocommerce_order_details_after_order_table', array( self::class, 'display_booking_info_order_details' ) );
}
/**
* Handle payment completion.
*
* @param int $order_id Order ID.
* @return void
*/
public static function on_payment_complete( int $order_id ): void {
self::maybe_create_booking( $order_id );
}
/**
* Handle order status change to processing.
*
* @param int $order_id Order ID.
* @return void
*/
public static function on_order_processing( int $order_id ): void {
self::maybe_create_booking( $order_id );
}
/**
* Handle order status change to completed.
*
* @param int $order_id Order ID.
* @return void
*/
public static function on_order_completed( int $order_id ): void {
self::maybe_create_booking( $order_id );
}
/**
* Create booking from order if not already created.
*
* @param int $order_id Order ID.
* @return void
*/
private static function maybe_create_booking( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order ) {
return;
}
// Check if booking already created.
$existing_booking = Manager::get_booking_for_order( $order );
if ( $existing_booking ) {
return;
}
// Check if order has room bookings.
$has_bookings = false;
foreach ( $order->get_items() as $item ) {
$room_id = $item->get_meta( '_bnb_room_id', true );
if ( $room_id ) {
$has_bookings = true;
break;
}
}
if ( ! $has_bookings ) {
return;
}
// Create booking.
self::create_booking_from_order( $order );
}
/**
* Create booking(s) from WooCommerce order.
*
* @param \WC_Order $order WooCommerce order.
* @return int|null Booking ID or null on failure.
*/
public static function create_booking_from_order( \WC_Order $order ): ?int {
/**
* Fires before creating a booking from an order.
*
* @param \WC_Order $order WooCommerce order.
*/
do_action( 'wp_bnb_wc_before_booking_from_order', $order );
// Find or create guest.
$guest_id = self::find_or_create_guest( $order );
// Get booking data from order items.
$booking_id = null;
foreach ( $order->get_items() as $item ) {
$room_id = $item->get_meta( '_bnb_room_id', true );
if ( ! $room_id ) {
continue;
}
$check_in = $item->get_meta( '_bnb_check_in', true );
$check_out = $item->get_meta( '_bnb_check_out', true );
$guests = $item->get_meta( '_bnb_guests', true );
$nights = $item->get_meta( '_bnb_nights', true );
$services = $item->get_meta( '_bnb_services', true );
$breakdown = $item->get_meta( '_bnb_price_breakdown', true );
// Decode JSON if necessary.
if ( is_string( $services ) ) {
$services = json_decode( $services, true ) ?: array();
}
if ( is_string( $breakdown ) ) {
$breakdown = json_decode( $breakdown, true ) ?: array();
}
// Determine initial status.
$status = Manager::is_auto_confirm_enabled() ? 'confirmed' : 'pending';
// Get guest notes.
$guest_notes = $order->get_meta( '_bnb_guest_notes', true );
// Create booking post.
$booking_data = array(
'post_type' => Booking::POST_TYPE,
'post_status' => 'publish',
'post_title' => self::generate_booking_title( $guest_id, $check_in, $check_out ),
);
/**
* Filter the booking data before creation.
*
* @param array $booking_data Booking post data.
* @param \WC_Order $order WooCommerce order.
*/
$booking_data = apply_filters( 'wp_bnb_wc_booking_from_order_data', $booking_data, $order );
$booking_id = wp_insert_post( $booking_data );
if ( ! $booking_id || is_wp_error( $booking_id ) ) {
continue;
}
// Store booking meta.
update_post_meta( $booking_id, '_bnb_booking_room_id', $room_id );
update_post_meta( $booking_id, '_bnb_booking_check_in', $check_in );
update_post_meta( $booking_id, '_bnb_booking_check_out', $check_out );
update_post_meta( $booking_id, '_bnb_booking_status', $status );
update_post_meta( $booking_id, '_bnb_booking_adults', max( 1, (int) $guests ) );
update_post_meta( $booking_id, '_bnb_booking_children', 0 );
update_post_meta( $booking_id, '_bnb_booking_guest_notes', $guest_notes );
update_post_meta( $booking_id, '_bnb_booking_source', 'woocommerce_order_' . $order->get_id() );
// Store guest info.
if ( $guest_id ) {
update_post_meta( $booking_id, '_bnb_booking_guest_id', $guest_id );
update_post_meta( $booking_id, '_bnb_booking_guest_name', Guest::get_full_name( $guest_id ) );
update_post_meta( $booking_id, '_bnb_booking_guest_email', get_post_meta( $guest_id, '_bnb_guest_email', true ) );
update_post_meta( $booking_id, '_bnb_booking_guest_phone', get_post_meta( $guest_id, '_bnb_guest_phone', true ) );
} else {
// Use order billing info.
$guest_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
update_post_meta( $booking_id, '_bnb_booking_guest_name', $guest_name );
update_post_meta( $booking_id, '_bnb_booking_guest_email', $order->get_billing_email() );
update_post_meta( $booking_id, '_bnb_booking_guest_phone', $order->get_billing_phone() );
}
// Store pricing.
$total = $item->get_total();
update_post_meta( $booking_id, '_bnb_booking_calculated_price', $total );
if ( ! empty( $breakdown ) ) {
update_post_meta( $booking_id, '_bnb_booking_price_breakdown', $breakdown );
}
// Store services.
if ( ! empty( $services ) ) {
update_post_meta( $booking_id, Booking::SERVICES_META_KEY, $services );
}
// Generate booking reference.
$reference = self::generate_booking_reference( $booking_id );
update_post_meta( $booking_id, '_bnb_booking_reference', $reference );
// Store confirmed timestamp if auto-confirmed.
if ( 'confirmed' === $status ) {
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
}
// Link booking to order.
Manager::link_booking_to_order( $booking_id, $order );
// Also store room ID in order meta for quick access.
$order->update_meta_data( Manager::ORDER_ROOM_META, $room_id );
// Store check-in/out in order meta.
$order->update_meta_data( '_bnb_check_in', $check_in );
$order->update_meta_data( '_bnb_check_out', $check_out );
$order->save();
// Trigger status change action for email notifications.
if ( 'confirmed' === $status ) {
/**
* Fires when booking status changes (for email notifications).
*
* @param int $booking_id Booking post ID.
* @param string $old_status Old status.
* @param string $new_status New status.
*/
do_action( 'wp_bnb_booking_status_changed', $booking_id, 'pending', 'confirmed' );
}
}
/**
* Fires after creating a booking from an order.
*
* @param int|null $booking_id Last booking ID created.
* @param \WC_Order $order WooCommerce order.
*/
do_action( 'wp_bnb_wc_after_booking_from_order', $booking_id, $order );
return $booking_id;
}
/**
* Sync order status changes to booking status.
*
* @param int $order_id Order ID.
* @param string $old_status Old status.
* @param string $new_status New status.
* @param \WC_Order $order Order object.
* @return void
*/
public static function sync_order_status_to_booking( int $order_id, string $old_status, string $new_status, \WC_Order $order ): void {
// Prevent recursive updates.
if ( self::$updating_status ) {
return;
}
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
// Map WC status to booking status.
$booking_status = Manager::map_wc_status_to_booking( $new_status );
// Get current booking status.
$current_booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
// Don't update if status is the same.
if ( $current_booking_status === $booking_status ) {
return;
}
// Don't downgrade from checked_in/checked_out.
if ( in_array( $current_booking_status, array( 'checked_in', 'checked_out' ), true ) ) {
return;
}
self::$updating_status = true;
// Update booking status.
update_post_meta( $booking_id, '_bnb_booking_status', $booking_status );
// Update confirmed timestamp if confirming.
if ( 'confirmed' === $booking_status && 'pending' === $current_booking_status ) {
update_post_meta( $booking_id, '_bnb_booking_confirmed_at', current_time( 'mysql' ) );
}
// Trigger booking status changed action.
do_action( 'wp_bnb_booking_status_changed', $booking_id, $current_booking_status, $booking_status );
self::$updating_status = false;
}
/**
* Find or create guest from order data.
*
* @param \WC_Order $order WooCommerce order.
* @return int|null Guest ID or null.
*/
private static function find_or_create_guest( \WC_Order $order ): ?int {
$email = $order->get_billing_email();
if ( ! $email ) {
return null;
}
// Try to find existing guest by email.
$existing_guests = get_posts(
array(
'post_type' => Guest::POST_TYPE,
'posts_per_page' => 1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => '_bnb_guest_email',
'value' => $email,
),
),
)
);
if ( ! empty( $existing_guests ) ) {
return $existing_guests[0]->ID;
}
// Create new guest.
$first_name = $order->get_billing_first_name();
$last_name = $order->get_billing_last_name();
$full_name = trim( $first_name . ' ' . $last_name );
$guest_data = array(
'post_type' => Guest::POST_TYPE,
'post_status' => 'publish',
'post_title' => $full_name ?: $email,
);
$guest_id = wp_insert_post( $guest_data );
if ( ! $guest_id || is_wp_error( $guest_id ) ) {
return null;
}
// Store guest meta.
update_post_meta( $guest_id, '_bnb_guest_first_name', $first_name );
update_post_meta( $guest_id, '_bnb_guest_last_name', $last_name );
update_post_meta( $guest_id, '_bnb_guest_email', $email );
update_post_meta( $guest_id, '_bnb_guest_phone', $order->get_billing_phone() );
update_post_meta( $guest_id, '_bnb_guest_address', $order->get_billing_address_1() );
update_post_meta( $guest_id, '_bnb_guest_city', $order->get_billing_city() );
update_post_meta( $guest_id, '_bnb_guest_postal_code', $order->get_billing_postcode() );
update_post_meta( $guest_id, '_bnb_guest_country', $order->get_billing_country() );
update_post_meta( $guest_id, '_bnb_guest_status', 'active' );
update_post_meta( $guest_id, '_bnb_guest_source', 'woocommerce' );
return $guest_id;
}
/**
* Generate booking title.
*
* @param int|null $guest_id Guest ID.
* @param string $check_in Check-in date.
* @param string $check_out Check-out date.
* @return string Booking title.
*/
private static function generate_booking_title( ?int $guest_id, string $check_in, string $check_out ): string {
$guest_name = $guest_id ? Guest::get_full_name( $guest_id ) : __( 'Guest', 'wp-bnb' );
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
if ( ! $check_in_date || ! $check_out_date ) {
return $guest_name;
}
// Format: "Guest Name (DD.MM - DD.MM.YYYY)" or span years.
if ( $check_in_date->format( 'Y' ) === $check_out_date->format( 'Y' ) ) {
return sprintf(
'%s (%s - %s)',
$guest_name,
$check_in_date->format( 'd.m' ),
$check_out_date->format( 'd.m.Y' )
);
}
return sprintf(
'%s (%s - %s)',
$guest_name,
$check_in_date->format( 'd.m.Y' ),
$check_out_date->format( 'd.m.Y' )
);
}
/**
* Generate booking reference.
*
* @param int $booking_id Booking ID.
* @return string Booking reference.
*/
private static function generate_booking_reference( int $booking_id ): string {
return sprintf(
'BNB-%s-%05d',
gmdate( 'Y' ),
$booking_id
);
}
/**
* Display booking info in admin order page.
*
* @param \WC_Order $order WooCommerce order.
* @return void
*/
public static function display_booking_info_admin( \WC_Order $order ): void {
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
$booking = get_post( $booking_id );
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
$room = $room_id ? get_post( $room_id ) : null;
?>
<div class="order_data_column bnb-order-booking-info">
<h3><?php esc_html_e( 'Booking Information', 'wp-bnb' ); ?></h3>
<p>
<strong><?php esc_html_e( 'Booking:', 'wp-bnb' ); ?></strong>
<a href="<?php echo esc_url( get_edit_post_link( $booking_id ) ); ?>">
<?php echo esc_html( $booking ? $booking->post_title : "#{$booking_id}" ); ?>
</a>
</p>
<?php if ( $room ) : ?>
<p>
<strong><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></strong>
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
<?php echo esc_html( $room->post_title ); ?>
</a>
</p>
<?php endif; ?>
<p>
<strong><?php esc_html_e( 'Status:', 'wp-bnb' ); ?></strong>
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
</span>
</p>
</div>
<?php
}
/**
* Display booking info on thank you page.
*
* @param int $order_id Order ID.
* @return void
*/
public static function display_booking_info_thankyou( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
$room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true );
$room = $room_id ? get_post( $room_id ) : null;
$building = $room_id ? Room::get_building( $room_id ) : null;
$date_format = get_option( 'date_format' );
$check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in );
$check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out );
?>
<section class="woocommerce-booking-confirmation">
<h2><?php esc_html_e( 'Booking Confirmed', 'wp-bnb' ); ?></h2>
<table class="woocommerce-table woocommerce-table--booking-details">
<tbody>
<tr>
<th><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></th>
<td><strong><?php echo esc_html( $reference ); ?></strong></td>
</tr>
<?php if ( $room ) : ?>
<tr>
<th><?php esc_html_e( 'Room:', 'wp-bnb' ); ?></th>
<td>
<?php echo esc_html( $room->post_title ); ?>
<?php if ( $building ) : ?>
<br><small><?php echo esc_html( $building->post_title ); ?></small>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
<tr>
<th><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></th>
<td><?php echo esc_html( $check_in_date ? $check_in_date->format( $date_format ) : $check_in ); ?></td>
</tr>
<tr>
<th><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></th>
<td><?php echo esc_html( $check_out_date ? $check_out_date->format( $date_format ) : $check_out ); ?></td>
</tr>
</tbody>
</table>
<p class="woocommerce-notice woocommerce-notice--success">
<?php esc_html_e( 'Your booking has been confirmed. A confirmation email has been sent to your email address.', 'wp-bnb' ); ?>
</p>
</section>
<?php
}
/**
* Display booking info in order details (customer account).
*
* @param \WC_Order $order WooCommerce order.
* @return void
*/
public static function display_booking_info_order_details( \WC_Order $order ): void {
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
$reference = get_post_meta( $booking_id, '_bnb_booking_reference', true );
$status = get_post_meta( $booking_id, '_bnb_booking_status', true );
if ( $reference ) {
?>
<p class="woocommerce-booking-reference">
<strong><?php esc_html_e( 'Booking Reference:', 'wp-bnb' ); ?></strong>
<?php echo esc_html( $reference ); ?>
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $status ); ?>">
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $status ) ) ); ?>
</span>
</p>
<?php
}
}
}