Add pricing system with tiers, seasons, and calculator (v0.2.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m19s
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>
This commit is contained in:
363
src/Pricing/Calculator.php
Normal file
363
src/Pricing/Calculator.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
159
src/Pricing/PricingTier.php
Normal file
159
src/Pricing/PricingTier.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
/**
|
||||
* Pricing tier enumeration.
|
||||
*
|
||||
* Defines the pricing tiers for short, mid, and long-term stays.
|
||||
*
|
||||
* @package Magdev\WpBnb\Pricing
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Pricing;
|
||||
|
||||
/**
|
||||
* Pricing tier enum.
|
||||
*/
|
||||
enum PricingTier: string {
|
||||
|
||||
/**
|
||||
* Short-term stay (per night).
|
||||
* Default: 1-6 nights.
|
||||
*/
|
||||
case SHORT_TERM = 'short_term';
|
||||
|
||||
/**
|
||||
* Mid-term stay (per week).
|
||||
* Default: 7-27 nights (1-4 weeks).
|
||||
*/
|
||||
case MID_TERM = 'mid_term';
|
||||
|
||||
/**
|
||||
* Long-term stay (per month).
|
||||
* Default: 28+ nights (1+ months).
|
||||
*/
|
||||
case LONG_TERM = 'long_term';
|
||||
|
||||
/**
|
||||
* Get the label for this tier.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function label(): string {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => __( 'Short-term (Nightly)', 'wp-bnb' ),
|
||||
self::MID_TERM => __( 'Mid-term (Weekly)', 'wp-bnb' ),
|
||||
self::LONG_TERM => __( 'Long-term (Monthly)', 'wp-bnb' ),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit label for this tier.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function unit(): string {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => __( 'per night', 'wp-bnb' ),
|
||||
self::MID_TERM => __( 'per week', 'wp-bnb' ),
|
||||
self::LONG_TERM => __( 'per month', 'wp-bnb' ),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit label for this tier (singular).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function unitSingular(): string {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => __( 'night', 'wp-bnb' ),
|
||||
self::MID_TERM => __( 'week', 'wp-bnb' ),
|
||||
self::LONG_TERM => __( 'month', 'wp-bnb' ),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unit label for this tier (plural).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function unitPlural(): string {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => __( 'nights', 'wp-bnb' ),
|
||||
self::MID_TERM => __( 'weeks', 'wp-bnb' ),
|
||||
self::LONG_TERM => __( 'months', 'wp-bnb' ),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default minimum nights for this tier.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function defaultMinNights(): int {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => 1,
|
||||
self::MID_TERM => 7,
|
||||
self::LONG_TERM => 28,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default maximum nights for this tier.
|
||||
*
|
||||
* @return int|null Null means unlimited.
|
||||
*/
|
||||
public function defaultMaxNights(): ?int {
|
||||
return match ( $this ) {
|
||||
self::SHORT_TERM => 6,
|
||||
self::MID_TERM => 27,
|
||||
self::LONG_TERM => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the pricing tier based on number of nights.
|
||||
*
|
||||
* @param int $nights Number of nights.
|
||||
* @param int|null $short_term_max Maximum nights for short-term. Default 6.
|
||||
* @param int|null $mid_term_max Maximum nights for mid-term. Default 27.
|
||||
* @return self
|
||||
*/
|
||||
public static function fromNights( int $nights, ?int $short_term_max = null, ?int $mid_term_max = null ): self {
|
||||
$short_term_max = $short_term_max ?? (int) get_option( 'wp_bnb_short_term_max_nights', 6 );
|
||||
$mid_term_max = $mid_term_max ?? (int) get_option( 'wp_bnb_mid_term_max_nights', 27 );
|
||||
|
||||
if ( $nights <= $short_term_max ) {
|
||||
return self::SHORT_TERM;
|
||||
}
|
||||
|
||||
if ( $nights <= $mid_term_max ) {
|
||||
return self::MID_TERM;
|
||||
}
|
||||
|
||||
return self::LONG_TERM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tiers.
|
||||
*
|
||||
* @return array<self>
|
||||
*/
|
||||
public static function all(): array {
|
||||
return self::cases();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tier options for select fields.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function options(): array {
|
||||
$options = array();
|
||||
foreach ( self::cases() as $tier ) {
|
||||
$options[ $tier->value ] = $tier->label();
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
319
src/Pricing/Season.php
Normal file
319
src/Pricing/Season.php
Normal file
@@ -0,0 +1,319 @@
|
||||
<?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 );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user