Files
wp-bnb/src/Integration/WooCommerce/ProductSync.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

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 );
}
}