All checks were successful
Create Release Package / build-release (push) Successful in 1m19s
- Create PricingTier enum for short/mid/long-term pricing - Add Season class for seasonal pricing with date ranges - Implement Calculator for price calculations with breakdown - Add pricing meta box to Room post type - Create Seasons admin page for managing seasonal pricing - Add Pricing settings tab with tier thresholds - Support weekend surcharges and configurable weekend days - Add price column to room list admin Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
364 lines
8.3 KiB
PHP
364 lines
8.3 KiB
PHP
<?php
|
|
/**
|
|
* Price calculator.
|
|
*
|
|
* Handles price calculations for room bookings.
|
|
*
|
|
* @package Magdev\WpBnb\Pricing
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
namespace Magdev\WpBnb\Pricing;
|
|
|
|
use Magdev\WpBnb\PostTypes\Room;
|
|
|
|
/**
|
|
* Price calculator class.
|
|
*/
|
|
final class Calculator {
|
|
|
|
/**
|
|
* Meta key prefix for room pricing.
|
|
*
|
|
* @var string
|
|
*/
|
|
private const META_PREFIX = '_bnb_room_price_';
|
|
|
|
/**
|
|
* Room post ID.
|
|
*
|
|
* @var int
|
|
*/
|
|
private int $room_id;
|
|
|
|
/**
|
|
* Check-in date.
|
|
*
|
|
* @var \DateTimeImmutable
|
|
*/
|
|
private \DateTimeImmutable $check_in;
|
|
|
|
/**
|
|
* Check-out date.
|
|
*
|
|
* @var \DateTimeImmutable
|
|
*/
|
|
private \DateTimeImmutable $check_out;
|
|
|
|
/**
|
|
* Price breakdown.
|
|
*
|
|
* @var array
|
|
*/
|
|
private array $breakdown = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @param \DateTimeInterface|string $check_in Check-in date.
|
|
* @param \DateTimeInterface|string $check_out Check-out date.
|
|
*/
|
|
public function __construct( int $room_id, \DateTimeInterface|string $check_in, \DateTimeInterface|string $check_out ) {
|
|
$this->room_id = $room_id;
|
|
|
|
if ( is_string( $check_in ) ) {
|
|
$check_in = new \DateTimeImmutable( $check_in );
|
|
} elseif ( $check_in instanceof \DateTime ) {
|
|
$check_in = \DateTimeImmutable::createFromMutable( $check_in );
|
|
}
|
|
|
|
if ( is_string( $check_out ) ) {
|
|
$check_out = new \DateTimeImmutable( $check_out );
|
|
} elseif ( $check_out instanceof \DateTime ) {
|
|
$check_out = \DateTimeImmutable::createFromMutable( $check_out );
|
|
}
|
|
|
|
$this->check_in = $check_in;
|
|
$this->check_out = $check_out;
|
|
}
|
|
|
|
/**
|
|
* Get the number of nights.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getNights(): int {
|
|
$interval = $this->check_in->diff( $this->check_out );
|
|
return max( 1, $interval->days );
|
|
}
|
|
|
|
/**
|
|
* Get the pricing tier for this stay.
|
|
*
|
|
* @return PricingTier
|
|
*/
|
|
public function getTier(): PricingTier {
|
|
return PricingTier::fromNights( $this->getNights() );
|
|
}
|
|
|
|
/**
|
|
* Get the base price for a room.
|
|
*
|
|
* @param PricingTier $tier Pricing tier.
|
|
* @return float
|
|
*/
|
|
public function getBasePrice( PricingTier $tier ): float {
|
|
$meta_key = self::META_PREFIX . $tier->value;
|
|
$price = get_post_meta( $this->room_id, $meta_key, true );
|
|
return $price ? (float) $price : 0.0;
|
|
}
|
|
|
|
/**
|
|
* Get the weekend surcharge for a room.
|
|
*
|
|
* @return float Surcharge as absolute amount.
|
|
*/
|
|
public function getWeekendSurcharge(): float {
|
|
$surcharge = get_post_meta( $this->room_id, self::META_PREFIX . 'weekend_surcharge', true );
|
|
return $surcharge ? (float) $surcharge : 0.0;
|
|
}
|
|
|
|
/**
|
|
* Check if weekend surcharge is enabled for this room.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasWeekendSurcharge(): bool {
|
|
return $this->getWeekendSurcharge() > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if a date is a weekend day.
|
|
*
|
|
* @param \DateTimeInterface $date Date to check.
|
|
* @return bool
|
|
*/
|
|
public static function isWeekend( \DateTimeInterface $date ): bool {
|
|
$day_of_week = (int) $date->format( 'N' ); // 1 = Monday, 7 = Sunday.
|
|
$weekend_days = array_map(
|
|
'intval',
|
|
explode( ',', (string) get_option( 'wp_bnb_weekend_days', '5,6' ) ) // Default: Friday, Saturday.
|
|
);
|
|
return in_array( $day_of_week, $weekend_days, true );
|
|
}
|
|
|
|
/**
|
|
* Calculate the total price.
|
|
*
|
|
* @return float
|
|
*/
|
|
public function calculate(): float {
|
|
$this->breakdown = array();
|
|
|
|
$nights = $this->getNights();
|
|
$tier = $this->getTier();
|
|
|
|
$base_price = $this->getBasePrice( $tier );
|
|
$weekend_surcharge = $this->getWeekendSurcharge();
|
|
|
|
// If no base price is set, return 0.
|
|
if ( $base_price <= 0 ) {
|
|
return 0.0;
|
|
}
|
|
|
|
$total = 0.0;
|
|
|
|
// Calculate based on tier.
|
|
switch ( $tier ) {
|
|
case PricingTier::LONG_TERM:
|
|
// Monthly pricing.
|
|
$months = ceil( $nights / 30 );
|
|
$total = $base_price * $months;
|
|
$this->breakdown['months'] = $months;
|
|
$this->breakdown['monthly_rate'] = $base_price;
|
|
break;
|
|
|
|
case PricingTier::MID_TERM:
|
|
// Weekly pricing.
|
|
$weeks = ceil( $nights / 7 );
|
|
$total = $base_price * $weeks;
|
|
$this->breakdown['weeks'] = $weeks;
|
|
$this->breakdown['weekly_rate'] = $base_price;
|
|
break;
|
|
|
|
case PricingTier::SHORT_TERM:
|
|
default:
|
|
// Nightly pricing with seasonal adjustments and weekend surcharges.
|
|
$current = $this->check_in;
|
|
$breakdown_nights = array();
|
|
|
|
for ( $i = 0; $i < $nights; $i++ ) {
|
|
$night_price = $base_price;
|
|
$modifiers = array();
|
|
|
|
// Apply seasonal pricing.
|
|
$season = Season::forDate( $current );
|
|
if ( $season ) {
|
|
$night_price *= $season->modifier;
|
|
$modifiers['season'] = array(
|
|
'name' => $season->name,
|
|
'modifier' => $season->modifier,
|
|
);
|
|
}
|
|
|
|
// Apply weekend surcharge.
|
|
if ( $weekend_surcharge > 0 && self::isWeekend( $current ) ) {
|
|
$night_price += $weekend_surcharge;
|
|
$modifiers['weekend'] = $weekend_surcharge;
|
|
}
|
|
|
|
$breakdown_nights[] = array(
|
|
'date' => $current->format( 'Y-m-d' ),
|
|
'base' => $base_price,
|
|
'final' => $night_price,
|
|
'modifiers' => $modifiers,
|
|
);
|
|
|
|
$total += $night_price;
|
|
$current = $current->modify( '+1 day' );
|
|
}
|
|
|
|
$this->breakdown['nights'] = $breakdown_nights;
|
|
$this->breakdown['nightly_rate'] = $base_price;
|
|
break;
|
|
}
|
|
|
|
$this->breakdown['tier'] = $tier->value;
|
|
$this->breakdown['total'] = $total;
|
|
|
|
/**
|
|
* Filter the calculated price.
|
|
*
|
|
* @param float $total Calculated total price.
|
|
* @param int $room_id Room post ID.
|
|
* @param Calculator $calculator Calculator instance.
|
|
*/
|
|
return (float) apply_filters( 'wp_bnb_calculate_price', $total, $this->room_id, $this );
|
|
}
|
|
|
|
/**
|
|
* Get the price breakdown.
|
|
*
|
|
* Must be called after calculate().
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getBreakdown(): array {
|
|
return $this->breakdown;
|
|
}
|
|
|
|
/**
|
|
* Format a price with currency.
|
|
*
|
|
* @param float $amount Amount to format.
|
|
* @param string $currency Currency code. Default from settings.
|
|
* @return string
|
|
*/
|
|
public static function formatPrice( float $amount, string $currency = '' ): string {
|
|
if ( empty( $currency ) ) {
|
|
$currency = get_option( 'wp_bnb_currency', 'CHF' );
|
|
}
|
|
|
|
$symbols = array(
|
|
'CHF' => 'CHF',
|
|
'EUR' => "\u{20AC}",
|
|
'USD' => '$',
|
|
'GBP' => "\u{00A3}",
|
|
);
|
|
|
|
$symbol = $symbols[ $currency ] ?? $currency;
|
|
$formatted = number_format( $amount, 2, '.', "'" );
|
|
|
|
// CHF uses suffix, others use prefix.
|
|
if ( 'CHF' === $currency ) {
|
|
return $formatted . ' ' . $symbol;
|
|
}
|
|
|
|
return $symbol . ' ' . $formatted;
|
|
}
|
|
|
|
/**
|
|
* Get room pricing summary.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @return array
|
|
*/
|
|
public static function getRoomPricing( int $room_id ): array {
|
|
$pricing = array();
|
|
|
|
foreach ( PricingTier::cases() as $tier ) {
|
|
$meta_key = self::META_PREFIX . $tier->value;
|
|
$price = get_post_meta( $room_id, $meta_key, true );
|
|
|
|
$pricing[ $tier->value ] = array(
|
|
'label' => $tier->label(),
|
|
'unit' => $tier->unit(),
|
|
'price' => $price ? (float) $price : null,
|
|
);
|
|
}
|
|
|
|
$pricing['weekend_surcharge'] = array(
|
|
'label' => __( 'Weekend Surcharge', 'wp-bnb' ),
|
|
'price' => (float) get_post_meta( $room_id, self::META_PREFIX . 'weekend_surcharge', true ),
|
|
);
|
|
|
|
return $pricing;
|
|
}
|
|
|
|
/**
|
|
* Save room pricing.
|
|
*
|
|
* @param int $room_id Room post ID.
|
|
* @param array $pricing Pricing data with tier keys.
|
|
* @return void
|
|
*/
|
|
public static function saveRoomPricing( int $room_id, array $pricing ): void {
|
|
foreach ( PricingTier::cases() as $tier ) {
|
|
if ( isset( $pricing[ $tier->value ] ) ) {
|
|
update_post_meta(
|
|
$room_id,
|
|
self::META_PREFIX . $tier->value,
|
|
floatval( $pricing[ $tier->value ] )
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( isset( $pricing['weekend_surcharge'] ) ) {
|
|
update_post_meta(
|
|
$room_id,
|
|
self::META_PREFIX . 'weekend_surcharge',
|
|
floatval( $pricing['weekend_surcharge'] )
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the check-in date.
|
|
*
|
|
* @return \DateTimeImmutable
|
|
*/
|
|
public function getCheckIn(): \DateTimeImmutable {
|
|
return $this->check_in;
|
|
}
|
|
|
|
/**
|
|
* Get the check-out date.
|
|
*
|
|
* @return \DateTimeImmutable
|
|
*/
|
|
public function getCheckOut(): \DateTimeImmutable {
|
|
return $this->check_out;
|
|
}
|
|
|
|
/**
|
|
* Get the room ID.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getRoomId(): int {
|
|
return $this->room_id;
|
|
}
|
|
}
|