All checks were successful
Create Release Package / build-release (push) Successful in 1m11s
- 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>
516 lines
13 KiB
PHP
516 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* WooCommerce Product Synchronization.
|
|
*
|
|
* Synchronizes rooms with WooCommerce products.
|
|
*
|
|
* @package Magdev\WpBnb\Integration\WooCommerce
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
namespace Magdev\WpBnb\Integration\WooCommerce;
|
|
|
|
use Magdev\WpBnb\PostTypes\Building;
|
|
use Magdev\WpBnb\PostTypes\Room;
|
|
use Magdev\WpBnb\Pricing\Calculator;
|
|
use Magdev\WpBnb\Pricing\PricingTier;
|
|
use Magdev\WpBnb\Taxonomies\Amenity;
|
|
use Magdev\WpBnb\Taxonomies\RoomType;
|
|
|
|
/**
|
|
* Product Synchronization class.
|
|
*
|
|
* Creates and maintains WooCommerce products that correspond to BnB rooms.
|
|
*/
|
|
final class ProductSync {
|
|
|
|
/**
|
|
* Initialize product synchronization.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function init(): void {
|
|
// Sync product when room is saved.
|
|
add_action( 'save_post_' . Room::POST_TYPE, array( self::class, 'on_room_save' ), 20, 2 );
|
|
|
|
// Delete product when room is deleted.
|
|
add_action( 'before_delete_post', array( self::class, 'on_room_delete' ) );
|
|
|
|
// Add linked room info to product edit screen.
|
|
add_action( 'woocommerce_product_options_general_product_data', array( self::class, 'add_product_room_info' ) );
|
|
|
|
// AJAX handler for syncing all rooms.
|
|
add_action( 'wp_ajax_wp_bnb_sync_all_rooms', array( self::class, 'ajax_sync_all_rooms' ) );
|
|
}
|
|
|
|
/**
|
|
* Handle room save - create or update product.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param \WP_Post $post Post object.
|
|
* @return void
|
|
*/
|
|
public static function on_room_save( int $post_id, \WP_Post $post ): void {
|
|
// Skip autosaves, revisions, and other non-published posts.
|
|
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( 'publish' !== $post->post_status ) {
|
|
return;
|
|
}
|
|
|
|
// Check if auto-sync is enabled.
|
|
if ( ! Manager::is_auto_sync_enabled() ) {
|
|
return;
|
|
}
|
|
|
|
// Sync the product.
|
|
self::sync_room_to_product( $post_id );
|
|
}
|
|
|
|
/**
|
|
* Handle room deletion - delete linked product.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @return void
|
|
*/
|
|
public static function on_room_delete( int $post_id ): void {
|
|
$post = get_post( $post_id );
|
|
|
|
if ( ! $post || Room::POST_TYPE !== $post->post_type ) {
|
|
return;
|
|
}
|
|
|
|
self::delete_product_for_room( $post_id );
|
|
}
|
|
|
|
/**
|
|
* Sync a room to a WooCommerce product.
|
|
*
|
|
* Creates a new product if one doesn't exist, or updates existing one.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @return int|null Product ID or null on failure.
|
|
*/
|
|
public static function sync_room_to_product( int $room_id ): ?int {
|
|
$room = get_post( $room_id );
|
|
|
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
|
return null;
|
|
}
|
|
|
|
// Check for existing product.
|
|
$product_id = self::get_product_for_room( $room_id );
|
|
|
|
if ( $product_id ) {
|
|
// Update existing product.
|
|
return self::update_product( $product_id, $room );
|
|
}
|
|
|
|
// Create new product.
|
|
return self::create_product_for_room( $room );
|
|
}
|
|
|
|
/**
|
|
* Create a WooCommerce product for a room.
|
|
*
|
|
* @param \WP_Post $room Room post object.
|
|
* @return int|null Product ID or null on failure.
|
|
*/
|
|
public static function create_product_for_room( \WP_Post $room ): ?int {
|
|
/**
|
|
* Fires before creating a WC product for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
*/
|
|
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
|
|
|
// Create a simple virtual product.
|
|
$product = new \WC_Product_Simple();
|
|
|
|
// Configure the product.
|
|
self::configure_product( $product, $room );
|
|
|
|
// Save the product.
|
|
$product_id = $product->save();
|
|
|
|
if ( ! $product_id ) {
|
|
return null;
|
|
}
|
|
|
|
// Store bidirectional links.
|
|
update_post_meta( $room->ID, Manager::ROOM_PRODUCT_META, $product_id );
|
|
update_post_meta( $product_id, Manager::PRODUCT_ROOM_META, $room->ID );
|
|
|
|
/**
|
|
* Fires after creating a WC product for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @param int $product_id WC product ID.
|
|
*/
|
|
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
|
|
|
return $product_id;
|
|
}
|
|
|
|
/**
|
|
* Update an existing WooCommerce product.
|
|
*
|
|
* @param int $product_id Product ID.
|
|
* @param \WP_Post $room Room post object.
|
|
* @return int|null Product ID or null on failure.
|
|
*/
|
|
private static function update_product( int $product_id, \WP_Post $room ): ?int {
|
|
$product = wc_get_product( $product_id );
|
|
|
|
if ( ! $product ) {
|
|
// Product was deleted, create a new one.
|
|
return self::create_product_for_room( $room );
|
|
}
|
|
|
|
/**
|
|
* Fires before updating a WC product for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
*/
|
|
do_action( 'wp_bnb_wc_before_product_sync', $room->ID );
|
|
|
|
// Configure the product.
|
|
self::configure_product( $product, $room );
|
|
|
|
// Save the product.
|
|
$product->save();
|
|
|
|
/**
|
|
* Fires after updating a WC product for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @param int $product_id WC product ID.
|
|
*/
|
|
do_action( 'wp_bnb_wc_after_product_sync', $room->ID, $product_id );
|
|
|
|
return $product_id;
|
|
}
|
|
|
|
/**
|
|
* Configure a WooCommerce product from room data.
|
|
*
|
|
* @param \WC_Product $product Product object.
|
|
* @param \WP_Post $room Room post object.
|
|
* @return void
|
|
*/
|
|
private static function configure_product( \WC_Product $product, \WP_Post $room ): void {
|
|
// Get room data.
|
|
$building = Room::get_building( $room->ID );
|
|
$building_name = $building ? $building->post_title : '';
|
|
$pricing = Calculator::getRoomPricing( $room->ID );
|
|
|
|
// Basic info.
|
|
$product->set_name( $room->post_title );
|
|
$product->set_slug( 'bnb-room-' . $room->ID );
|
|
$product->set_status( 'publish' );
|
|
|
|
// SKU.
|
|
$product->set_sku( 'bnb-room-' . $room->ID );
|
|
|
|
// Virtual product (no shipping).
|
|
$product->set_virtual( true );
|
|
|
|
// Description.
|
|
$description = $room->post_content;
|
|
if ( $building_name ) {
|
|
$description = sprintf(
|
|
/* translators: %s: Building name */
|
|
__( 'Room at %s', 'wp-bnb' ),
|
|
$building_name
|
|
) . "\n\n" . $description;
|
|
}
|
|
$product->set_description( $description );
|
|
$product->set_short_description( $room->post_excerpt ?: wp_trim_words( $room->post_content, 30 ) );
|
|
|
|
// Price (use short-term/nightly rate as base).
|
|
$base_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? 0;
|
|
if ( $base_price > 0 ) {
|
|
$product->set_regular_price( (string) $base_price );
|
|
}
|
|
|
|
// Featured image.
|
|
$thumbnail_id = get_post_thumbnail_id( $room->ID );
|
|
if ( $thumbnail_id ) {
|
|
$product->set_image_id( $thumbnail_id );
|
|
}
|
|
|
|
// Gallery images.
|
|
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
|
if ( $gallery_ids ) {
|
|
$ids = array_filter( explode( ',', $gallery_ids ), 'is_numeric' );
|
|
$product->set_gallery_image_ids( array_map( 'absint', $ids ) );
|
|
}
|
|
|
|
// Stock management (disabled - availability handled by booking system).
|
|
$product->set_manage_stock( false );
|
|
$product->set_stock_status( 'instock' );
|
|
|
|
// Catalog visibility - visible.
|
|
$product->set_catalog_visibility( 'visible' );
|
|
|
|
// Product category.
|
|
$category_id = Manager::get_product_category();
|
|
if ( $category_id ) {
|
|
$product->set_category_ids( array( $category_id ) );
|
|
}
|
|
|
|
// Store room metadata.
|
|
$capacity = get_post_meta( $room->ID, '_bnb_room_capacity', true );
|
|
$beds = get_post_meta( $room->ID, '_bnb_room_beds', true );
|
|
|
|
$product->update_meta_data( '_bnb_room_capacity', $capacity );
|
|
$product->update_meta_data( '_bnb_room_beds', $beds );
|
|
$product->update_meta_data( '_bnb_building_id', $building ? $building->ID : 0 );
|
|
$product->update_meta_data( '_bnb_building_name', $building_name );
|
|
|
|
/**
|
|
* Filter the product data before save.
|
|
*
|
|
* @param array $data Product data array.
|
|
* @param int $room_id Room post ID.
|
|
* @param \WP_Post $room Room post object.
|
|
*/
|
|
$data = apply_filters(
|
|
'wp_bnb_wc_product_data',
|
|
array(
|
|
'name' => $product->get_name(),
|
|
'price' => $product->get_regular_price(),
|
|
'description' => $product->get_description(),
|
|
),
|
|
$room->ID,
|
|
$room
|
|
);
|
|
|
|
// Apply filtered data.
|
|
if ( isset( $data['name'] ) ) {
|
|
$product->set_name( $data['name'] );
|
|
}
|
|
if ( isset( $data['price'] ) ) {
|
|
$product->set_regular_price( (string) $data['price'] );
|
|
}
|
|
if ( isset( $data['description'] ) ) {
|
|
$product->set_description( $data['description'] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the WooCommerce product for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @return bool True if deleted, false otherwise.
|
|
*/
|
|
public static function delete_product_for_room( int $room_id ): bool {
|
|
$product_id = self::get_product_for_room( $room_id );
|
|
|
|
if ( ! $product_id ) {
|
|
return false;
|
|
}
|
|
|
|
$product = wc_get_product( $product_id );
|
|
|
|
if ( ! $product ) {
|
|
return false;
|
|
}
|
|
|
|
// Delete the product (force delete, not trash).
|
|
$product->delete( true );
|
|
|
|
// Clean up room meta.
|
|
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the WooCommerce product ID for a room.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @return int|null Product ID or null.
|
|
*/
|
|
public static function get_product_for_room( int $room_id ): ?int {
|
|
$product_id = get_post_meta( $room_id, Manager::ROOM_PRODUCT_META, true );
|
|
|
|
if ( ! $product_id ) {
|
|
return null;
|
|
}
|
|
|
|
// Verify product still exists.
|
|
$product = wc_get_product( $product_id );
|
|
|
|
if ( ! $product ) {
|
|
// Clean up stale meta.
|
|
delete_post_meta( $room_id, Manager::ROOM_PRODUCT_META );
|
|
return null;
|
|
}
|
|
|
|
return absint( $product_id );
|
|
}
|
|
|
|
/**
|
|
* Get the room ID for a WooCommerce product.
|
|
*
|
|
* @param int $product_id Product ID.
|
|
* @return int|null Room ID or null.
|
|
*/
|
|
public static function get_room_for_product( int $product_id ): ?int {
|
|
$room_id = get_post_meta( $product_id, Manager::PRODUCT_ROOM_META, true );
|
|
|
|
if ( ! $room_id ) {
|
|
return null;
|
|
}
|
|
|
|
// Verify room still exists.
|
|
$room = get_post( $room_id );
|
|
|
|
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
|
// Clean up stale meta.
|
|
delete_post_meta( $product_id, Manager::PRODUCT_ROOM_META );
|
|
return null;
|
|
}
|
|
|
|
return absint( $room_id );
|
|
}
|
|
|
|
/**
|
|
* Sync all published rooms to WooCommerce products.
|
|
*
|
|
* @return array{created: int, updated: int, errors: array<string>}
|
|
*/
|
|
public static function sync_all_rooms(): array {
|
|
$result = array(
|
|
'created' => 0,
|
|
'updated' => 0,
|
|
'errors' => array(),
|
|
);
|
|
|
|
$rooms = get_posts(
|
|
array(
|
|
'post_type' => Room::POST_TYPE,
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
)
|
|
);
|
|
|
|
foreach ( $rooms as $room_id ) {
|
|
$existing_product = self::get_product_for_room( $room_id );
|
|
|
|
$product_id = self::sync_room_to_product( $room_id );
|
|
|
|
if ( $product_id ) {
|
|
if ( $existing_product ) {
|
|
++$result['updated'];
|
|
} else {
|
|
++$result['created'];
|
|
}
|
|
} else {
|
|
$room = get_post( $room_id );
|
|
$result['errors'][] = sprintf(
|
|
/* translators: %s: Room title */
|
|
__( 'Failed to sync room: %s', 'wp-bnb' ),
|
|
$room ? $room->post_title : "#{$room_id}"
|
|
);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for syncing all rooms.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function ajax_sync_all_rooms(): void {
|
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-bnb' ) ) );
|
|
}
|
|
|
|
$result = self::sync_all_rooms();
|
|
|
|
wp_send_json_success(
|
|
array(
|
|
'message' => sprintf(
|
|
/* translators: 1: Created count, 2: Updated count */
|
|
__( 'Sync complete. Created: %1$d, Updated: %2$d', 'wp-bnb' ),
|
|
$result['created'],
|
|
$result['updated']
|
|
),
|
|
'created' => $result['created'],
|
|
'updated' => $result['updated'],
|
|
'errors' => $result['errors'],
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add linked room info to WooCommerce product edit screen.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function add_product_room_info(): void {
|
|
global $post;
|
|
|
|
if ( ! $post ) {
|
|
return;
|
|
}
|
|
|
|
$room_id = self::get_room_for_product( $post->ID );
|
|
|
|
if ( ! $room_id ) {
|
|
return;
|
|
}
|
|
|
|
$room = get_post( $room_id );
|
|
|
|
if ( ! $room ) {
|
|
return;
|
|
}
|
|
|
|
?>
|
|
<div class="options_group show_if_simple">
|
|
<p class="form-field">
|
|
<label><?php esc_html_e( 'Linked BnB Room', 'wp-bnb' ); ?></label>
|
|
<span class="description">
|
|
<a href="<?php echo esc_url( get_edit_post_link( $room_id ) ); ?>">
|
|
<?php echo esc_html( $room->post_title ); ?>
|
|
</a>
|
|
<br>
|
|
<small><?php esc_html_e( 'This product is automatically synced from the linked room. Changes should be made in the room settings.', 'wp-bnb' ); ?></small>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Check if a product is a BnB room product.
|
|
*
|
|
* @param int|\WC_Product $product Product ID or object.
|
|
* @return bool
|
|
*/
|
|
public static function is_room_product( $product ): bool {
|
|
if ( is_int( $product ) ) {
|
|
$product = wc_get_product( $product );
|
|
}
|
|
|
|
if ( ! $product instanceof \WC_Product ) {
|
|
return false;
|
|
}
|
|
|
|
$room_id = $product->get_meta( Manager::PRODUCT_ROOM_META, true );
|
|
|
|
return ! empty( $room_id );
|
|
}
|
|
}
|