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>
320 lines
6.9 KiB
PHP
320 lines
6.9 KiB
PHP
<?php
|
|
/**
|
|
* Seasonal pricing management.
|
|
*
|
|
* Handles seasonal pricing periods with date ranges and price modifiers.
|
|
*
|
|
* @package Magdev\WpBnb\Pricing
|
|
*/
|
|
|
|
declare( strict_types=1 );
|
|
|
|
namespace Magdev\WpBnb\Pricing;
|
|
|
|
/**
|
|
* Season class for managing seasonal pricing.
|
|
*/
|
|
final class Season {
|
|
|
|
/**
|
|
* Option name for storing seasons.
|
|
*
|
|
* @var string
|
|
*/
|
|
private const OPTION_NAME = 'wp_bnb_seasons';
|
|
|
|
/**
|
|
* Season ID.
|
|
*
|
|
* @var string
|
|
*/
|
|
public string $id;
|
|
|
|
/**
|
|
* Season name.
|
|
*
|
|
* @var string
|
|
*/
|
|
public string $name;
|
|
|
|
/**
|
|
* Start date (format: MM-DD).
|
|
*
|
|
* @var string
|
|
*/
|
|
public string $start_date;
|
|
|
|
/**
|
|
* End date (format: MM-DD).
|
|
*
|
|
* @var string
|
|
*/
|
|
public string $end_date;
|
|
|
|
/**
|
|
* Price modifier (multiplier). 1.0 = normal, 1.2 = 20% increase, 0.8 = 20% decrease.
|
|
*
|
|
* @var float
|
|
*/
|
|
public float $modifier;
|
|
|
|
/**
|
|
* Priority for overlapping seasons. Higher value = higher priority.
|
|
*
|
|
* @var int
|
|
*/
|
|
public int $priority;
|
|
|
|
/**
|
|
* Whether this season is active.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public bool $active;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $data Season data.
|
|
*/
|
|
public function __construct( array $data = array() ) {
|
|
$this->id = $data['id'] ?? wp_generate_uuid4();
|
|
$this->name = $data['name'] ?? '';
|
|
$this->start_date = $data['start_date'] ?? '';
|
|
$this->end_date = $data['end_date'] ?? '';
|
|
$this->modifier = isset( $data['modifier'] ) ? (float) $data['modifier'] : 1.0;
|
|
$this->priority = isset( $data['priority'] ) ? (int) $data['priority'] : 0;
|
|
$this->active = isset( $data['active'] ) ? (bool) $data['active'] : true;
|
|
}
|
|
|
|
/**
|
|
* Convert to array.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function toArray(): array {
|
|
return array(
|
|
'id' => $this->id,
|
|
'name' => $this->name,
|
|
'start_date' => $this->start_date,
|
|
'end_date' => $this->end_date,
|
|
'modifier' => $this->modifier,
|
|
'priority' => $this->priority,
|
|
'active' => $this->active,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a date falls within this season.
|
|
*
|
|
* @param \DateTimeInterface $date Date to check.
|
|
* @param int|null $year Year context (for spanning seasons like winter).
|
|
* @return bool
|
|
*/
|
|
public function containsDate( \DateTimeInterface $date, ?int $year = null ): bool {
|
|
if ( ! $this->active ) {
|
|
return false;
|
|
}
|
|
|
|
$year = $year ?? (int) $date->format( 'Y' );
|
|
|
|
// Parse start and end as month-day.
|
|
$start_parts = explode( '-', $this->start_date );
|
|
$end_parts = explode( '-', $this->end_date );
|
|
|
|
if ( count( $start_parts ) !== 2 || count( $end_parts ) !== 2 ) {
|
|
return false;
|
|
}
|
|
|
|
$start_month = (int) $start_parts[0];
|
|
$start_day = (int) $start_parts[1];
|
|
$end_month = (int) $end_parts[0];
|
|
$end_day = (int) $end_parts[1];
|
|
|
|
$check_month = (int) $date->format( 'm' );
|
|
$check_day = (int) $date->format( 'd' );
|
|
|
|
// Create comparable values (month * 100 + day).
|
|
$check_value = $check_month * 100 + $check_day;
|
|
$start_value = $start_month * 100 + $start_day;
|
|
$end_value = $end_month * 100 + $end_day;
|
|
|
|
// Handle year-spanning seasons (e.g., December to February).
|
|
if ( $start_value > $end_value ) {
|
|
// Season spans year boundary.
|
|
return $check_value >= $start_value || $check_value <= $end_value;
|
|
}
|
|
|
|
// Normal season within same year.
|
|
return $check_value >= $start_value && $check_value <= $end_value;
|
|
}
|
|
|
|
/**
|
|
* Get the display label for the modifier.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getModifierLabel(): string {
|
|
if ( $this->modifier === 1.0 ) {
|
|
return __( 'Normal', 'wp-bnb' );
|
|
}
|
|
|
|
$percentage = ( $this->modifier - 1 ) * 100;
|
|
if ( $percentage > 0 ) {
|
|
/* translators: %s: percentage increase */
|
|
return sprintf( __( '+%s%%', 'wp-bnb' ), number_format( $percentage, 0 ) );
|
|
}
|
|
|
|
/* translators: %s: percentage decrease */
|
|
return sprintf( __( '%s%%', 'wp-bnb' ), number_format( $percentage, 0 ) );
|
|
}
|
|
|
|
/**
|
|
* Get all seasons.
|
|
*
|
|
* @return array<self>
|
|
*/
|
|
public static function all(): array {
|
|
$data = get_option( self::OPTION_NAME, array() );
|
|
$seasons = array();
|
|
|
|
foreach ( $data as $season_data ) {
|
|
$seasons[] = new self( $season_data );
|
|
}
|
|
|
|
// Sort by priority (descending).
|
|
usort( $seasons, fn( Season $a, Season $b ) => $b->priority <=> $a->priority );
|
|
|
|
return $seasons;
|
|
}
|
|
|
|
/**
|
|
* Get all active seasons.
|
|
*
|
|
* @return array<self>
|
|
*/
|
|
public static function allActive(): array {
|
|
return array_filter( self::all(), fn( Season $s ) => $s->active );
|
|
}
|
|
|
|
/**
|
|
* Find a season by ID.
|
|
*
|
|
* @param string $id Season ID.
|
|
* @return self|null
|
|
*/
|
|
public static function find( string $id ): ?self {
|
|
foreach ( self::all() as $season ) {
|
|
if ( $season->id === $id ) {
|
|
return $season;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the applicable season for a date.
|
|
*
|
|
* Returns the highest priority active season that contains the date.
|
|
*
|
|
* @param \DateTimeInterface $date Date to check.
|
|
* @return self|null
|
|
*/
|
|
public static function forDate( \DateTimeInterface $date ): ?self {
|
|
$seasons = self::allActive();
|
|
|
|
foreach ( $seasons as $season ) {
|
|
if ( $season->containsDate( $date ) ) {
|
|
return $season;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Save a season.
|
|
*
|
|
* @param self $season Season to save.
|
|
* @return bool
|
|
*/
|
|
public static function save( self $season ): bool {
|
|
$data = get_option( self::OPTION_NAME, array() );
|
|
$exists = false;
|
|
|
|
foreach ( $data as $index => $existing ) {
|
|
if ( $existing['id'] === $season->id ) {
|
|
$data[ $index ] = $season->toArray();
|
|
$exists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! $exists ) {
|
|
$data[] = $season->toArray();
|
|
}
|
|
|
|
return update_option( self::OPTION_NAME, $data );
|
|
}
|
|
|
|
/**
|
|
* Delete a season.
|
|
*
|
|
* @param string $id Season ID to delete.
|
|
* @return bool
|
|
*/
|
|
public static function delete( string $id ): bool {
|
|
$data = get_option( self::OPTION_NAME, array() );
|
|
$data = array_filter( $data, fn( $s ) => $s['id'] !== $id );
|
|
return update_option( self::OPTION_NAME, array_values( $data ) );
|
|
}
|
|
|
|
/**
|
|
* Create default seasons.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function createDefaults(): void {
|
|
$existing = get_option( self::OPTION_NAME, array() );
|
|
if ( ! empty( $existing ) ) {
|
|
return;
|
|
}
|
|
|
|
$defaults = array(
|
|
new self(
|
|
array(
|
|
'name' => __( 'High Season', 'wp-bnb' ),
|
|
'start_date' => '06-15',
|
|
'end_date' => '09-15',
|
|
'modifier' => 1.25,
|
|
'priority' => 10,
|
|
'active' => true,
|
|
)
|
|
),
|
|
new self(
|
|
array(
|
|
'name' => __( 'Winter Holidays', 'wp-bnb' ),
|
|
'start_date' => '12-20',
|
|
'end_date' => '01-06',
|
|
'modifier' => 1.30,
|
|
'priority' => 20,
|
|
'active' => true,
|
|
)
|
|
),
|
|
new self(
|
|
array(
|
|
'name' => __( 'Low Season', 'wp-bnb' ),
|
|
'start_date' => '11-01',
|
|
'end_date' => '03-31',
|
|
'modifier' => 0.85,
|
|
'priority' => 5,
|
|
'active' => true,
|
|
)
|
|
),
|
|
);
|
|
|
|
$data = array_map( fn( Season $s ) => $s->toArray(), $defaults );
|
|
update_option( self::OPTION_NAME, $data );
|
|
}
|
|
}
|