Files
wp-bnb/src/Integration/WooCommerce/CartHandler.php

546 lines
17 KiB
PHP
Raw Normal View History

<?php
/**
* WooCommerce Cart Handler.
*
* Handles cart item data, availability validation, and dynamic pricing.
*
* @package Magdev\WpBnb\Integration\WooCommerce
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Integration\WooCommerce;
use Magdev\WpBnb\Booking\Availability;
use Magdev\WpBnb\PostTypes\Room;
use Magdev\WpBnb\PostTypes\Service;
use Magdev\WpBnb\Pricing\Calculator;
/**
* Cart Handler class.
*
* Manages booking data in the WooCommerce cart, validates availability,
* and calculates dynamic pricing based on dates and services.
*/
final class CartHandler {
/**
* Initialize cart handler.
*
* @return void
*/
public static function init(): void {
// Add booking data to cart item.
add_filter( 'woocommerce_add_cart_item_data', array( self::class, 'add_cart_item_data' ), 10, 3 );
// Restore booking data from session.
add_filter( 'woocommerce_get_cart_item_from_session', array( self::class, 'get_cart_item_from_session' ), 10, 2 );
// Validate before adding to cart.
add_filter( 'woocommerce_add_to_cart_validation', array( self::class, 'validate_add_to_cart' ), 10, 5 );
// Re-validate availability on cart load.
add_action( 'woocommerce_cart_loaded_from_session', array( self::class, 'validate_cart_items' ) );
// Display booking info in cart.
add_filter( 'woocommerce_get_item_data', array( self::class, 'display_cart_item_data' ), 10, 2 );
// Calculate dynamic price.
add_action( 'woocommerce_before_calculate_totals', array( self::class, 'calculate_cart_item_prices' ) );
// Prevent quantity changes for room products.
add_filter( 'woocommerce_quantity_input_args', array( self::class, 'lock_quantity' ), 10, 2 );
// Add booking data to order item meta.
add_action( 'woocommerce_checkout_create_order_line_item', array( self::class, 'add_order_item_meta' ), 10, 4 );
}
/**
* Add a room to cart with booking data.
*
* @param int $room_id Room post ID.
* @param string $check_in Check-in date (Y-m-d).
* @param string $check_out Check-out date (Y-m-d).
* @param int $guests Number of guests.
* @param array $services Array of service selections.
* @return string|bool Cart item key or false on failure.
*/
public static function add_room_to_cart(
int $room_id,
string $check_in,
string $check_out,
int $guests = 1,
array $services = array()
) {
// Get product ID for room.
$product_id = ProductSync::get_product_for_room( $room_id );
if ( ! $product_id ) {
// Try to sync the room first.
$product_id = ProductSync::sync_room_to_product( $room_id );
if ( ! $product_id ) {
wc_add_notice( __( 'Unable to add room to cart. Product not found.', 'wp-bnb' ), 'error' );
return false;
}
}
// Store booking data in session temporarily for the filter.
WC()->session->set(
'bnb_pending_booking',
array(
'room_id' => $room_id,
'check_in' => $check_in,
'check_out' => $check_out,
'guests' => $guests,
'services' => $services,
)
);
// Add to cart.
$cart_item_key = WC()->cart->add_to_cart( $product_id, 1 );
// Clean up session.
WC()->session->set( 'bnb_pending_booking', null );
return $cart_item_key;
}
/**
* Add booking data to cart item when adding to cart.
*
* @param array $cart_item_data Cart item data.
* @param int $product_id Product ID.
* @param int $variation_id Variation ID.
* @return array Modified cart item data.
*/
public static function add_cart_item_data( array $cart_item_data, int $product_id, int $variation_id ): array {
// Check if this is a room product.
$room_id = ProductSync::get_room_for_product( $product_id );
if ( ! $room_id ) {
return $cart_item_data;
}
// Get booking data from session or POST.
$booking_data = WC()->session->get( 'bnb_pending_booking' );
if ( ! $booking_data ) {
// Try to get from POST (for direct form submissions).
// phpcs:disable WordPress.Security.NonceVerification.Missing
$booking_data = array(
'room_id' => isset( $_POST['bnb_room_id'] ) ? absint( $_POST['bnb_room_id'] ) : $room_id,
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
'services' => isset( $_POST['bnb_services'] ) ? self::sanitize_services( $_POST['bnb_services'] ) : array(),
);
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
// Validate required fields.
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
return $cart_item_data;
}
// Calculate nights.
$check_in = new \DateTime( $booking_data['check_in'] );
$check_out = new \DateTime( $booking_data['check_out'] );
$nights = (int) $check_in->diff( $check_out )->days;
if ( $nights < 1 ) {
return $cart_item_data;
}
// Calculate price breakdown.
$calculator = new Calculator( $room_id, $booking_data['check_in'], $booking_data['check_out'] );
$price_breakdown = $calculator->calculate();
$room_total = $price_breakdown['total'];
// Calculate services total.
$services_total = 0;
$services_data = array();
foreach ( $booking_data['services'] as $service_selection ) {
$service_id = $service_selection['service_id'] ?? 0;
$quantity = $service_selection['quantity'] ?? 1;
if ( ! $service_id ) {
continue;
}
$service_price = Service::calculate_service_price( $service_id, $quantity, $nights );
$service_data = Service::get_service_data( $service_id );
$services_data[] = array(
'service_id' => $service_id,
'quantity' => $quantity,
'price' => $service_price,
'pricing_type' => $service_data['pricing_type'] ?? 'per_booking',
'name' => $service_data['name'] ?? '',
);
$services_total += $service_price;
}
// Store booking data.
$cart_item_data[ Manager::CART_ITEM_KEY ] = array(
'room_id' => $room_id,
'check_in' => $booking_data['check_in'],
'check_out' => $booking_data['check_out'],
'guests' => $booking_data['guests'],
'nights' => $nights,
'services' => $services_data,
'price_breakdown' => array(
'room_total' => $room_total,
'services_total' => $services_total,
'grand_total' => $room_total + $services_total,
'full_breakdown' => $price_breakdown,
),
);
// Generate unique key based on booking data to allow multiple bookings.
$cart_item_data['unique_key'] = md5(
$room_id . $booking_data['check_in'] . $booking_data['check_out'] . microtime()
);
return $cart_item_data;
}
/**
* Restore booking data from session.
*
* @param array $cart_item Cart item data.
* @param array $values Session values.
* @return array Modified cart item data.
*/
public static function get_cart_item_from_session( array $cart_item, array $values ): array {
if ( isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
$cart_item[ Manager::CART_ITEM_KEY ] = $values[ Manager::CART_ITEM_KEY ];
}
return $cart_item;
}
/**
* Validate room availability before adding to cart.
*
* @param bool $passed Whether validation passed.
* @param int $product_id Product ID.
* @param int $quantity Quantity.
* @param int $variation_id Variation ID.
* @param array $variations Variations.
* @return bool
*/
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity, int $variation_id = 0, array $variations = array() ): bool {
// Check if this is a room product.
$room_id = ProductSync::get_room_for_product( $product_id );
if ( ! $room_id ) {
return $passed;
}
// Get booking data.
$booking_data = WC()->session->get( 'bnb_pending_booking' );
if ( ! $booking_data ) {
// phpcs:disable WordPress.Security.NonceVerification.Missing
$booking_data = array(
'check_in' => isset( $_POST['bnb_check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_in'] ) ) : '',
'check_out' => isset( $_POST['bnb_check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['bnb_check_out'] ) ) : '',
'guests' => isset( $_POST['bnb_guests'] ) ? absint( $_POST['bnb_guests'] ) : 1,
);
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
// Validate dates provided.
if ( empty( $booking_data['check_in'] ) || empty( $booking_data['check_out'] ) ) {
wc_add_notice( __( 'Please select check-in and check-out dates.', 'wp-bnb' ), 'error' );
return false;
}
// Validate date format.
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
if ( ! $check_in || ! $check_out ) {
wc_add_notice( __( 'Invalid date format. Please use the date picker.', 'wp-bnb' ), 'error' );
return false;
}
// Validate check-out after check-in.
if ( $check_out <= $check_in ) {
wc_add_notice( __( 'Check-out date must be after check-in date.', 'wp-bnb' ), 'error' );
return false;
}
// Validate not in past.
$today = new \DateTime( 'today' );
if ( $check_in < $today ) {
wc_add_notice( __( 'Check-in date cannot be in the past.', 'wp-bnb' ), 'error' );
return false;
}
// Check availability.
$is_available = Availability::check_availability(
$room_id,
$booking_data['check_in'],
$booking_data['check_out']
);
if ( ! $is_available ) {
wc_add_notice( __( 'Sorry, this room is not available for the selected dates.', 'wp-bnb' ), 'error' );
return false;
}
// Check capacity.
$capacity = get_post_meta( $room_id, '_bnb_room_capacity', true );
if ( $capacity && $booking_data['guests'] > (int) $capacity ) {
wc_add_notice(
sprintf(
/* translators: %d: Room capacity */
__( 'This room has a maximum capacity of %d guests.', 'wp-bnb' ),
$capacity
),
'error'
);
return false;
}
// Check if same room with same dates already in cart.
foreach ( WC()->cart->get_cart() as $cart_item ) {
if ( isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
$existing = $cart_item[ Manager::CART_ITEM_KEY ];
if ( $existing['room_id'] === $room_id
&& $existing['check_in'] === $booking_data['check_in']
&& $existing['check_out'] === $booking_data['check_out']
) {
wc_add_notice( __( 'This room with the same dates is already in your cart.', 'wp-bnb' ), 'error' );
return false;
}
}
}
return $passed;
}
/**
* Validate cart items on cart load.
*
* @param \WC_Cart $cart Cart object.
* @return void
*/
public static function validate_cart_items( \WC_Cart $cart ): void {
$items_to_remove = array();
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
continue;
}
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
// Check if dates are still valid.
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
$today = new \DateTime( 'today' );
if ( $check_in < $today ) {
$items_to_remove[] = array(
'key' => $cart_item_key,
'message' => __( 'A room booking was removed because the check-in date has passed.', 'wp-bnb' ),
);
continue;
}
// Check if room still available.
$is_available = Availability::check_availability(
$booking_data['room_id'],
$booking_data['check_in'],
$booking_data['check_out']
);
if ( ! $is_available ) {
$items_to_remove[] = array(
'key' => $cart_item_key,
'message' => __( 'A room booking was removed because the room is no longer available for those dates.', 'wp-bnb' ),
);
}
}
// Remove invalid items.
foreach ( $items_to_remove as $item ) {
$cart->remove_cart_item( $item['key'] );
wc_add_notice( $item['message'], 'error' );
}
}
/**
* Display booking info in cart.
*
* @param array $item_data Item data for display.
* @param array $cart_item Cart item.
* @return array Modified item data.
*/
public static function display_cart_item_data( array $item_data, array $cart_item ): array {
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
return $item_data;
}
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
// Format dates.
$date_format = get_option( 'date_format' );
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
$item_data[] = array(
'key' => __( 'Check-in', 'wp-bnb' ),
'value' => $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'],
);
$item_data[] = array(
'key' => __( 'Check-out', 'wp-bnb' ),
'value' => $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'],
);
$item_data[] = array(
'key' => __( 'Nights', 'wp-bnb' ),
'value' => $booking_data['nights'],
);
$item_data[] = array(
'key' => __( 'Guests', 'wp-bnb' ),
'value' => $booking_data['guests'],
);
// Display services.
if ( ! empty( $booking_data['services'] ) ) {
$services_list = array();
foreach ( $booking_data['services'] as $service ) {
$services_list[] = $service['name'] . ' &times; ' . $service['quantity'];
}
$item_data[] = array(
'key' => __( 'Services', 'wp-bnb' ),
'value' => implode( ', ', $services_list ),
);
}
return $item_data;
}
/**
* Calculate dynamic prices for cart items.
*
* @param \WC_Cart $cart Cart object.
* @return void
*/
public static function calculate_cart_item_prices( \WC_Cart $cart ): void {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
foreach ( $cart->get_cart() as $cart_item ) {
if ( ! isset( $cart_item[ Manager::CART_ITEM_KEY ] ) ) {
continue;
}
$booking_data = $cart_item[ Manager::CART_ITEM_KEY ];
$grand_total = $booking_data['price_breakdown']['grand_total'] ?? 0;
/**
* Filter the cart item price for a booking.
*
* @param float $price The calculated price.
* @param array $cart_item The cart item.
*/
$grand_total = apply_filters( 'wp_bnb_wc_cart_item_price', $grand_total, $cart_item );
// Set the price.
$cart_item['data']->set_price( $grand_total );
}
}
/**
* Lock quantity to 1 for room products.
*
* @param array $args Input arguments.
* @param \WC_Product $product Product object.
* @return array Modified arguments.
*/
public static function lock_quantity( array $args, \WC_Product $product ): array {
if ( ProductSync::is_room_product( $product ) ) {
$args['min_value'] = 1;
$args['max_value'] = 1;
$args['readonly'] = true;
}
return $args;
}
/**
* Add booking data to order item meta.
*
* @param \WC_Order_Item_Product $item Order item.
* @param string $cart_item_key Cart item key.
* @param array $values Cart item values.
* @param \WC_Order $order Order object.
* @return void
*/
public static function add_order_item_meta( \WC_Order_Item_Product $item, string $cart_item_key, array $values, \WC_Order $order ): void {
if ( ! isset( $values[ Manager::CART_ITEM_KEY ] ) ) {
return;
}
$booking_data = $values[ Manager::CART_ITEM_KEY ];
// Store booking data in order item meta.
$item->add_meta_data( '_bnb_room_id', $booking_data['room_id'] );
$item->add_meta_data( '_bnb_check_in', $booking_data['check_in'] );
$item->add_meta_data( '_bnb_check_out', $booking_data['check_out'] );
$item->add_meta_data( '_bnb_guests', $booking_data['guests'] );
$item->add_meta_data( '_bnb_nights', $booking_data['nights'] );
$item->add_meta_data( '_bnb_services', wp_json_encode( $booking_data['services'] ) );
$item->add_meta_data( '_bnb_price_breakdown', wp_json_encode( $booking_data['price_breakdown'] ) );
// Add visible meta for admin display.
$date_format = get_option( 'date_format' );
$check_in = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_in'] );
$check_out = \DateTime::createFromFormat( 'Y-m-d', $booking_data['check_out'] );
$item->add_meta_data( __( 'Check-in', 'wp-bnb' ), $check_in ? $check_in->format( $date_format ) : $booking_data['check_in'] );
$item->add_meta_data( __( 'Check-out', 'wp-bnb' ), $check_out ? $check_out->format( $date_format ) : $booking_data['check_out'] );
$item->add_meta_data( __( 'Nights', 'wp-bnb' ), $booking_data['nights'] );
$item->add_meta_data( __( 'Guests', 'wp-bnb' ), $booking_data['guests'] );
}
/**
* Sanitize services array from POST data.
*
* @param mixed $services Raw services data.
* @return array Sanitized services array.
*/
private static function sanitize_services( $services ): array {
if ( ! is_array( $services ) ) {
return array();
}
$sanitized = array();
foreach ( $services as $service ) {
if ( ! is_array( $service ) ) {
continue;
}
$service_id = isset( $service['service_id'] ) ? absint( $service['service_id'] ) : 0;
$quantity = isset( $service['quantity'] ) ? absint( $service['quantity'] ) : 1;
if ( $service_id > 0 ) {
$sanitized[] = array(
'service_id' => $service_id,
'quantity' => max( 1, $quantity ),
);
}
}
return $sanitized;
}
}