| 1 |
<?php |
| 2 |
|
| 3 |
declare(strict_types=1); |
| 4 |
|
| 5 |
namespace Yatra\Services; |
| 6 |
|
| 7 |
use Yatra\Repositories\AvailabilityRepository; |
| 8 |
use Yatra\Repositories\RecurringAvailabilityRepository; |
| 9 |
use Yatra\Repositories\TripRepository; |
| 10 |
|
| 11 |
class CapacityService |
| 12 |
{ |
| 13 |
private AvailabilityRepository $availabilityRepository; |
| 14 |
private RecurringAvailabilityRepository $recurringAvailabilityRepository; |
| 15 |
private TripRepository $tripRepository; |
| 16 |
|
| 17 |
public function __construct( |
| 18 |
?AvailabilityRepository $availabilityRepository = null, |
| 19 |
?RecurringAvailabilityRepository $recurringAvailabilityRepository = null, |
| 20 |
?TripRepository $tripRepository = null |
| 21 |
) { |
| 22 |
$this->availabilityRepository = $availabilityRepository ?? new AvailabilityRepository(); |
| 23 |
$this->recurringAvailabilityRepository = $recurringAvailabilityRepository ?? new RecurringAvailabilityRepository(); |
| 24 |
$this->tripRepository = $tripRepository ?? new TripRepository(); |
| 25 |
} |
| 26 |
|
| 27 |
/** |
| 28 |
* Get capacity for a specific trip and date based on priority |
| 29 |
* |
| 30 |
* @param int $tripId Trip ID |
| 31 |
* @param string $date Date in YYYY-MM-DD format |
| 32 |
* @param string|null $time Departure time (HH:MM or HH:MM:SS). When the matching |
| 33 |
* rule defines time slots, this selects the slot whose |
| 34 |
* seat count applies. Optional for backward compatibility. |
| 35 |
* @return int Maximum capacity |
| 36 |
*/ |
| 37 |
public function getCapacityForDate(int $tripId, string $date, ?string $time = null): int |
| 38 |
{ |
| 39 |
// Normalize to Y-m-d. The availability-date and recurring-rule columns are |
| 40 |
// DATE type, so a datetime input (e.g. "2026-07-18 12:30:00") breaks the |
| 41 |
// rule's `end_date >= %s` boundary match (the DATE end_date is treated as |
| 42 |
// midnight) and silently falls through to the trip default. Strip any time |
| 43 |
// component so matching is date-only. |
| 44 |
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $date, $m)) { |
| 45 |
$date = $m[1]; |
| 46 |
} |
| 47 |
|
| 48 |
// 1. Check Availability Date first (specific date overrides) |
| 49 |
$availability = $this->availabilityRepository->findByTripIdAndDate($tripId, $date); |
| 50 |
if ($availability && isset($availability->seats_total) && $availability->seats_total > 0) { |
| 51 |
return (int) $availability->seats_total; |
| 52 |
} |
| 53 |
|
| 54 |
// 2. Check Recurring Availability Rules |
| 55 |
$recurringRules = $this->recurringAvailabilityRepository->findActiveRulesForDate($tripId, $date); |
| 56 |
if (!empty($recurringRules)) { |
| 57 |
// Sort by priority (if applicable) and get the first matching rule |
| 58 |
$matchingRule = reset($recurringRules); |
| 59 |
|
| 60 |
// A rule may define its capacity per time slot rather than on the rule |
| 61 |
// itself (e.g. a private tour whose vehicle seats 9 but sells one group |
| 62 |
// booking per departure). The public availability path already honours |
| 63 |
// the slot seats, so resolve them here too or departures/capacity would |
| 64 |
// fall through to the trip default and report the wrong number. |
| 65 |
$slotSeats = $this->resolveSlotSeats($matchingRule, $time); |
| 66 |
if ($slotSeats > 0) { |
| 67 |
return $slotSeats; |
| 68 |
} |
| 69 |
|
| 70 |
$seats = (int) ($matchingRule->seats_total ?? 0); |
| 71 |
if ($seats <= 0 && !empty($matchingRule->capacity_value)) { |
| 72 |
$capType = $matchingRule->capacity_type ?? 'fixed'; |
| 73 |
if ($capType === 'fixed') { |
| 74 |
$seats = (int) $matchingRule->capacity_value; |
| 75 |
} |
| 76 |
} |
| 77 |
if ($seats > 0) { |
| 78 |
return $seats; |
| 79 |
} |
| 80 |
} |
| 81 |
|
| 82 |
// 3. Fall back to trip's default capacity (column is max_travelers; max_travellers kept for legacy rows) |
| 83 |
$trip = $this->tripRepository->find($tripId); |
| 84 |
if (!$trip) { |
| 85 |
return 0; |
| 86 |
} |
| 87 |
|
| 88 |
$cap = (int) ($trip->max_travelers ?? $trip->max_travellers ?? 0); |
| 89 |
|
| 90 |
return $cap > 0 ? $cap : 0; |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* Resolve the seat count a rule defines on its time slots. |
| 95 |
* |
| 96 |
* Returns 0 when the rule has no slots, or when no slot carries a positive |
| 97 |
* seat count, so the caller falls back to the existing rule/trip resolution |
| 98 |
* exactly as before. |
| 99 |
* |
| 100 |
* @param object $rule Recurring availability rule |
| 101 |
* @param string|null $time Departure time to match against (HH:MM or HH:MM:SS) |
| 102 |
* @return int |
| 103 |
*/ |
| 104 |
private function resolveSlotSeats(object $rule, ?string $time): int |
| 105 |
{ |
| 106 |
$slots = $rule->time_slots ?? null; |
| 107 |
|
| 108 |
// Most finders hydrate this to an array, but some return the raw column. |
| 109 |
if (is_string($slots)) { |
| 110 |
$slots = json_decode($slots, true); |
| 111 |
} |
| 112 |
|
| 113 |
if (empty($slots) || !is_array($slots)) { |
| 114 |
return 0; |
| 115 |
} |
| 116 |
|
| 117 |
// With a known departure time, only the matching slot's seats apply. |
| 118 |
if ($time !== null && $time !== '') { |
| 119 |
$wanted = $this->normalizeTime($time); |
| 120 |
foreach ($slots as $slot) { |
| 121 |
if (!is_array($slot) || !isset($slot['departure_time'])) { |
| 122 |
continue; |
| 123 |
} |
| 124 |
if ($this->normalizeTime((string) $slot['departure_time']) === $wanted) { |
| 125 |
return max(0, (int) ($slot['seats'] ?? 0)); |
| 126 |
} |
| 127 |
} |
| 128 |
|
| 129 |
return 0; |
| 130 |
} |
| 131 |
|
| 132 |
// Without a time, a single slot is unambiguous; multiple slots make up the |
| 133 |
// day's total capacity. |
| 134 |
$total = 0; |
| 135 |
foreach ($slots as $slot) { |
| 136 |
if (is_array($slot)) { |
| 137 |
$total += max(0, (int) ($slot['seats'] ?? 0)); |
| 138 |
} |
| 139 |
} |
| 140 |
|
| 141 |
return $total; |
| 142 |
} |
| 143 |
|
| 144 |
/** |
| 145 |
* Normalize a time value to HH:MM for comparison, so "8:00", "08:00" and |
| 146 |
* "08:00:00" all match. |
| 147 |
* |
| 148 |
* @param string $time |
| 149 |
* @return string |
| 150 |
*/ |
| 151 |
private function normalizeTime(string $time): string |
| 152 |
{ |
| 153 |
$parts = explode(':', trim($time)); |
| 154 |
$hour = isset($parts[0]) ? (int) $parts[0] : 0; |
| 155 |
$minute = isset($parts[1]) ? (int) $parts[1] : 0; |
| 156 |
|
| 157 |
return sprintf('%02d:%02d', $hour, $minute); |
| 158 |
} |
| 159 |
} |
| 160 |
|