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
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:
394
src/Integration/WooCommerce/RefundHandler.php
Normal file
394
src/Integration/WooCommerce/RefundHandler.php
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user