Add WooCommerce integration for payments, invoices, and order management (v0.11.0)
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>
This commit is contained in:
2026-02-03 22:40:36 +01:00
parent 965060cc03
commit 2865956c56
15 changed files with 5036 additions and 9 deletions

View File

@@ -0,0 +1,394 @@
<?php
/**
* WooCommerce Refund Handler.
*
* Handles refund processing and booking cancellation.
*
* @package Magdev\WpBnb\Integration\WooCommerce
*/
declare( strict_types=1 );
namespace Magdev\WpBnb\Integration\WooCommerce;
use Magdev\WpBnb\Booking\EmailNotifier;
use Magdev\WpBnb\PostTypes\Booking;
/**
* Refund Handler class.
*
* Processes refunds and updates booking status accordingly.
*/
final class RefundHandler {
/**
* Booking meta key for refund amount.
*/
public const REFUND_AMOUNT_META = '_bnb_booking_refund_amount';
/**
* Booking meta key for refund reason.
*/
public const REFUND_REASON_META = '_bnb_booking_refund_reason';
/**
* Booking meta key for refund date.
*/
public const REFUND_DATE_META = '_bnb_booking_refund_date';
/**
* Initialize refund handler.
*
* @return void
*/
public static function init(): void {
// Handle refund creation.
add_action( 'woocommerce_refund_created', array( self::class, 'on_refund_created' ), 10, 2 );
// Handle order fully refunded.
add_action( 'woocommerce_order_fully_refunded', array( self::class, 'on_order_fully_refunded' ), 10, 2 );
// Add refund notice in admin order page.
add_action( 'woocommerce_admin_order_totals_after_refunded', array( self::class, 'add_booking_refund_notice' ) );
}
/**
* Handle refund creation.
*
* @param int $refund_id Refund ID.
* @param array $args Refund arguments.
* @return void
*/
public static function on_refund_created( int $refund_id, array $args ): void {
$order_id = $args['order_id'] ?? 0;
if ( ! $order_id ) {
return;
}
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order ) {
return;
}
// Check if order has a linked booking.
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
$refund_amount = abs( floatval( $args['amount'] ?? 0 ) );
$refund_reason = $args['reason'] ?? '';
/**
* Fires before processing a refund for a booking.
*
* @param \WC_Order $order WooCommerce order.
* @param float $refund_amount Refund amount.
*/
do_action( 'wp_bnb_wc_before_refund_process', $order, $refund_amount );
// Check if this is a full or partial refund.
$is_full_refund = self::is_full_refund( $order );
if ( $is_full_refund ) {
// Full refund - cancel the booking.
self::cancel_booking_on_refund( $booking_id, $refund_amount, $refund_reason );
} else {
// Partial refund - store refund info but don't cancel.
self::record_partial_refund( $booking_id, $refund_amount, $refund_reason );
}
/**
* Fires after processing a refund for a booking.
*
* @param \WC_Order $order WooCommerce order.
* @param int $booking_id Booking ID.
* @param bool $cancelled Whether booking was cancelled.
*/
do_action( 'wp_bnb_wc_after_refund_process', $order, $booking_id, $is_full_refund );
}
/**
* Handle order fully refunded.
*
* @param int $order_id Order ID.
* @param int $refund_id Refund ID.
* @return void
*/
public static function on_order_fully_refunded( int $order_id, int $refund_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order ) {
return;
}
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
// Get current booking status.
$current_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
// Don't cancel if already cancelled.
if ( 'cancelled' === $current_status ) {
return;
}
// Cancel the booking.
$total_refunded = $order->get_total_refunded();
self::cancel_booking_on_refund( $booking_id, $total_refunded, __( 'Order fully refunded', 'wp-bnb' ) );
}
/**
* Check if the order is fully refunded.
*
* @param \WC_Order $order WooCommerce order.
* @return bool
*/
public static function is_full_refund( \WC_Order $order ): bool {
$order_total = floatval( $order->get_total() );
$total_refunded = floatval( $order->get_total_refunded() );
// Consider it full refund if refunded amount >= order total.
return $total_refunded >= $order_total;
}
/**
* Cancel booking on full refund.
*
* @param int $booking_id Booking ID.
* @param float $refund_amount Refund amount.
* @param string $reason Refund reason.
* @return void
*/
public static function cancel_booking_on_refund( int $booking_id, float $refund_amount, string $reason ): void {
// Get current status.
$old_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
// Don't cancel if already cancelled.
if ( 'cancelled' === $old_status ) {
// Just update refund info.
self::record_refund_meta( $booking_id, $refund_amount, $reason );
return;
}
/**
* Filter whether to cancel booking on refund.
*
* @param bool $cancel Whether to cancel.
* @param \WC_Order $order WooCommerce order.
* @param float $refund_amount Refund amount.
*/
$should_cancel = apply_filters( 'wp_bnb_wc_should_cancel_on_refund', true, $booking_id, $refund_amount );
if ( ! $should_cancel ) {
self::record_partial_refund( $booking_id, $refund_amount, $reason );
return;
}
// Update booking status to cancelled.
update_post_meta( $booking_id, '_bnb_booking_status', 'cancelled' );
// Store refund information.
self::record_refund_meta( $booking_id, $refund_amount, $reason );
// Add cancellation note.
$note = sprintf(
/* translators: %s: Refund amount */
__( 'Booking cancelled due to WooCommerce refund (%s)', 'wp-bnb' ),
wc_price( $refund_amount )
);
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
/**
* Fires when booking status changes (triggers email notifications).
*
* @param int $booking_id Booking ID.
* @param string $old_status Old status.
* @param string $new_status New status.
*/
do_action( 'wp_bnb_booking_status_changed', $booking_id, $old_status, 'cancelled' );
}
/**
* Record partial refund without cancelling.
*
* @param int $booking_id Booking ID.
* @param float $refund_amount Refund amount.
* @param string $reason Refund reason.
* @return void
*/
private static function record_partial_refund( int $booking_id, float $refund_amount, string $reason ): void {
// Get existing refund amount and add to it.
$existing_refund = floatval( get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true ) );
$total_refund = $existing_refund + $refund_amount;
// Update refund meta.
self::record_refund_meta( $booking_id, $total_refund, $reason );
// Add note about partial refund.
$note = sprintf(
/* translators: %s: Refund amount */
__( 'Partial refund processed: %s', 'wp-bnb' ),
wc_price( $refund_amount )
);
$existing_notes = get_post_meta( $booking_id, '_bnb_booking_notes', true );
$new_notes = $existing_notes ? $existing_notes . "\n\n" . $note : $note;
update_post_meta( $booking_id, '_bnb_booking_notes', $new_notes );
}
/**
* Record refund metadata.
*
* @param int $booking_id Booking ID.
* @param float $refund_amount Total refund amount.
* @param string $reason Refund reason.
* @return void
*/
private static function record_refund_meta( int $booking_id, float $refund_amount, string $reason ): void {
update_post_meta( $booking_id, self::REFUND_AMOUNT_META, $refund_amount );
update_post_meta( $booking_id, self::REFUND_DATE_META, current_time( 'mysql' ) );
if ( $reason ) {
update_post_meta( $booking_id, self::REFUND_REASON_META, $reason );
}
}
/**
* Calculate refund amount for a booking.
*
* @param int $booking_id Booking ID.
* @param string $type Refund type: 'full' or 'nights_remaining'.
* @return float Refund amount.
*/
public static function calculate_refund_amount( int $booking_id, string $type = 'full' ): float {
$calculated_price = floatval( get_post_meta( $booking_id, '_bnb_booking_calculated_price', true ) );
if ( 'full' === $type ) {
return $calculated_price;
}
// Calculate pro-rata based on nights remaining.
$check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true );
$check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true );
if ( ! $check_in || ! $check_out ) {
return $calculated_price;
}
$today = new \DateTime( 'today' );
$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 $calculated_price;
}
// If check-in hasn't happened, full refund.
if ( $today < $check_in_date ) {
return $calculated_price;
}
// If check-out has passed, no refund.
if ( $today >= $check_out_date ) {
return 0.0;
}
// Calculate remaining nights.
$total_nights = $check_in_date->diff( $check_out_date )->days;
$nights_used = $check_in_date->diff( $today )->days;
$nights_remaining = $total_nights - $nights_used;
if ( $total_nights <= 0 ) {
return 0.0;
}
// Pro-rata refund.
$nightly_rate = $calculated_price / $total_nights;
return $nightly_rate * $nights_remaining;
}
/**
* Get refund info for a booking.
*
* @param int $booking_id Booking ID.
* @return array|null Refund info or null.
*/
public static function get_booking_refund_info( int $booking_id ): ?array {
$amount = get_post_meta( $booking_id, self::REFUND_AMOUNT_META, true );
if ( ! $amount ) {
return null;
}
return array(
'amount' => floatval( $amount ),
'reason' => get_post_meta( $booking_id, self::REFUND_REASON_META, true ),
'date' => get_post_meta( $booking_id, self::REFUND_DATE_META, true ),
);
}
/**
* Add booking refund notice in admin order page.
*
* @param int $order_id Order ID.
* @return void
*/
public static function add_booking_refund_notice( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order instanceof \WC_Order ) {
return;
}
$booking_id = Manager::get_booking_for_order( $order );
if ( ! $booking_id ) {
return;
}
$refund_info = self::get_booking_refund_info( $booking_id );
if ( ! $refund_info ) {
return;
}
$booking_status = get_post_meta( $booking_id, '_bnb_booking_status', true );
?>
<tr>
<td class="label"><?php esc_html_e( 'Booking Status', 'wp-bnb' ); ?>:</td>
<td width="1%"></td>
<td class="total">
<span class="bnb-status-badge bnb-status-<?php echo esc_attr( $booking_status ); ?>">
<?php echo esc_html( ucfirst( str_replace( '_', ' ', $booking_status ) ) ); ?>
</span>
</td>
</tr>
<?php if ( 'cancelled' === $booking_status ) : ?>
<tr>
<td class="label" colspan="2">
<small class="description">
<?php
printf(
/* translators: %s: Booking edit link */
esc_html__( 'Booking was cancelled due to refund. %s', 'wp-bnb' ),
'<a href="' . esc_url( get_edit_post_link( $booking_id ) ) . '">' . esc_html__( 'View booking', 'wp-bnb' ) . '</a>'
);
?>
</small>
</td>
</tr>
<?php endif; ?>
<?php
}
}