PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.2
Yatra – Travel Booking & Tour Operator Software v3.0.14.2
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Hooks / AvailabilityInventoryHooks.php

AvailabilityInventoryHooks.php in Yatra – Travel Booking & Tour Operator Software 3.0.14.2, at app/Hooks/AvailabilityInventoryHooks.php

314 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Hooks;
6
7 use Yatra\Database\Tables\BookingsTable;
8 use Yatra\Database\Tables\TripAvailabilityDatesTable;
9 use Yatra\Repositories\AvailabilityRepository;
10 use Yatra\Repositories\BookingDepartureRepository;
11 use Yatra\Repositories\BookingRepository;
12 use Yatra\Repositories\DepartureRepository;
13 use Yatra\Services\CapacityService;
14 use Yatra\Services\WaitlistPromotionService;
15
16 class AvailabilityInventoryHooks
17 {
18 public static function init(): void
19 {
20 add_action('yatra_booking_created', [self::class, 'onBookingCreated'], 10, 2);
21 add_action('yatra_booking_deleted', [self::class, 'onBookingDeleted'], 10, 2);
22 add_action('yatra_booking_status_changed', [self::class, 'onBookingStatusChanged'], 10, 3);
23
24 // Add hooks to ensure departure capacity sync
25 add_action('yatra_availability_updated', [self::class, 'onAvailabilityUpdate'], 10, 1);
26 add_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10, 1);
27 }
28
29 public static function onBookingCreated(int $bookingId, $booking = null): void
30 {
31 self::syncForBooking($bookingId, $booking);
32 }
33
34 public static function onBookingDeleted(int $bookingId, $booking = null): void
35 {
36 self::syncForBooking($bookingId, $booking);
37 }
38
39 public static function onBookingStatusChanged(int $bookingId, string $oldStatus, string $newStatus): void
40 {
41 self::syncForBooking($bookingId, null);
42 }
43
44 private static function syncForBooking(int $bookingId, $booking = null): void
45 {
46 try {
47 if (!is_object($booking)) {
48 $repo = new BookingRepository();
49 $booking = $repo->find($bookingId);
50 }
51
52 if (!is_object($booking)) {
53 return;
54 }
55
56 $tripId = (int) ($booking->trip_id ?? 0);
57 $availabilityId = isset($booking->availability_id) ? (int) $booking->availability_id : 0;
58 $travelDate = (string) ($booking->travel_date ?? '');
59
60 if ($tripId > 0 && $availabilityId <= 0 && $travelDate !== '') {
61 // Try to infer availability_id from resolved snapshot stored in booking meta.
62 // This is more reliable when a trip has multiple departure times on the same date.
63 $metaRaw = $booking->meta ?? null;
64 $meta = [];
65 if (is_string($metaRaw) && $metaRaw !== '') {
66 $decoded = json_decode($metaRaw, true);
67 $meta = is_array($decoded) ? $decoded : [];
68 } elseif (is_array($metaRaw)) {
69 $meta = $metaRaw;
70 }
71
72 if (!empty($meta) && isset($meta['resolved_availability']) && is_array($meta['resolved_availability'])) {
73 $ra = $meta['resolved_availability'];
74
75 $metaAvailabilityId = $ra['availability_id'] ?? null;
76 if (is_numeric($metaAvailabilityId) && (int) $metaAvailabilityId > 0) {
77 $availabilityId = (int) $metaAvailabilityId;
78 } else {
79 $metaDepartureTime = isset($ra['departure_time']) ? (string) $ra['departure_time'] : '';
80 $metaTravelDate = isset($ra['travel_date']) ? (string) $ra['travel_date'] : $travelDate;
81 if ($metaDepartureTime !== '') {
82 $row = (new AvailabilityRepository())->findByTripIdAndDateTime(
83 $tripId,
84 $metaTravelDate,
85 self::normalizeTimeForAvailabilityMatch($metaDepartureTime),
86 true
87 );
88 if ($row && !empty($row->id)) {
89 $availabilityId = (int) $row->id;
90 }
91 }
92 }
93 }
94
95 $availabilityId = self::resolveAvailabilityIdForBooking($bookingId, $tripId, $travelDate);
96 if ($availabilityId > 0) {
97 (new BookingRepository())->update($bookingId, ['availability_id' => $availabilityId]);
98 }
99 }
100
101 if ($tripId <= 0 || $availabilityId <= 0) {
102 return;
103 }
104
105 self::syncAvailabilityId($availabilityId, $tripId);
106 } catch (\Throwable $e) {
107 return;
108 }
109 }
110
111 private static function syncAvailabilityId(int $availabilityId, int $tripId): void
112 {
113 $availabilityRepo = new AvailabilityRepository();
114 $availability = $availabilityRepo->find($availabilityId);
115
116 if (!$availability) {
117 return;
118 }
119
120 $departureTime = isset($availability->departure_time) ? (string) $availability->departure_time : '';
121
122 $bookingRepo = new BookingRepository();
123 $bookedCount = $bookingRepo->getTotalTravelersByTripAndAvailability(
124 $tripId,
125 $availabilityId,
126 BookingRepository::getCapacityConsumingBookingStatuses()
127 );
128
129 $seatsTotal = (int) ($availability->seats_total ?? 0);
130 $seatsReserved = $bookedCount;
131 $seatsAvailable = max(0, $seatsTotal - $bookedCount);
132
133 $status = (string) ($availability->status ?? 'available');
134 if (!in_array($status, ['blocked', 'closed', 'cancelled'], true)) {
135 if ($seatsAvailable === 0) {
136 $status = 'sold_out';
137 } elseif ($seatsTotal > 0 && $seatsAvailable <= (int) ceil($seatsTotal * 0.2)) {
138 $status = 'limited';
139 } else {
140 $status = 'available';
141 }
142 }
143
144 // Update availability using repository
145 $availabilityRepo->update($availabilityId, [
146 'seats_available' => $seatsAvailable,
147 'seats_reserved' => $seatsReserved,
148 'status' => $status,
149 ]);
150
151 self::syncDepartureWithAvailability($tripId, $availability, $bookedCount, $seatsTotal);
152
153 WaitlistPromotionService::processAvailableSlots($tripId, $availabilityId);
154 }
155
156 private static function syncDepartureWithAvailability(int $tripId, object $availability, int $bookedCount, int $seatsTotal): void
157 {
158 $departureRepo = new DepartureRepository();
159 $date = (string) ($availability->departure_date ?? '');
160 $time = (string) ($availability->departure_time ?? '');
161 $normalizedTime = '';
162 if ($time !== '') {
163 $ts = strtotime($time);
164 if ($ts !== false) {
165 $normalizedTime = date('H:i:s', $ts);
166 }
167 }
168
169 if ($date === '') {
170 return;
171 }
172
173 // Find a matching departure by trip + date (+ time if provided)
174 $departure = null;
175 $candidateTimes = array_values(array_unique(array_filter([$normalizedTime, $time], function ($t) {
176 return $t !== '';
177 })));
178
179 foreach ($candidateTimes as $candidateTime) {
180 $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, $candidateTime);
181 if ($departure) {
182 break;
183 }
184 }
185 if (!$departure) {
186 $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, null);
187 }
188
189 if (!$departure) {
190 return;
191 }
192
193 // booked_count is sourced from the actual bookings table — it
194 // is the truth. Reflect it to the departure row WITHOUT
195 // clamping to max_capacity. Clamping previously hid oversells
196 // from operators: if direct + OTA bookings legitimately
197 // exceeded capacity (always an OTA-side acceptance — Yatra's
198 // direct checkout enforces capacity), the departure showed
199 // "full" instead of "oversold by N", and operators had no way
200 // to spot the situation needing reconciliation. Allowing the
201 // overshow makes the problem visible so it can be addressed.
202 $updated = [
203 'max_capacity' => $seatsTotal,
204 'booked_count' => max(0, $bookedCount),
205 ];
206
207 $departureRepo->update((int) $departure->id, $updated);
208 }
209
210 /**
211 * Hook to sync departure capacity when availability is updated
212 */
213 public static function onAvailabilityUpdate(int $availabilityId): void
214 {
215 $availabilityRepo = new AvailabilityRepository();
216 $availability = $availabilityRepo->find($availabilityId);
217
218 if ($availability) {
219 self::syncAvailabilityId($availabilityId, (int) $availability->trip_id);
220 }
221 }
222
223 /**
224 * Hook to ensure departure capacity always matches availability on save
225 */
226 public static function onDepartureSave(int $departureId): void
227 {
228 // Prevent infinite loops
229 static $processing = [];
230 if (isset($processing[$departureId])) {
231 return;
232 }
233 $processing[$departureId] = true;
234
235 $departureRepo = new DepartureRepository();
236 $departure = $departureRepo->find($departureId);
237
238 if ($departure) {
239 $date = $departure->start_date ?: $departure->date;
240
241 // Get capacity from availability
242 $capacityService = new CapacityService();
243 $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date, $departure->time ?? null);
244
245 if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) {
246 // Remove hook temporarily to prevent infinite loop
247 remove_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10);
248
249 $departureRepo->update($departureId, [
250 'max_capacity' => $correctCapacity
251 ]);
252
253 // Re-add hook
254 add_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10, 1);
255 }
256 }
257
258 unset($processing[$departureId]);
259 }
260
261 /**
262 * Infer yatra_trip_availability_dates.id when the booking row missed availability_id
263 * (common when checkout omits it). Uses booking→departure time to disambiguate multiple slots per day.
264 */
265 private static function resolveAvailabilityIdForBooking(int $bookingId, int $tripId, string $travelDate): int
266 {
267 $availabilityRepo = new AvailabilityRepository();
268 $bdRepo = new BookingDepartureRepository();
269 $depId = $bdRepo->getDepartureIdForBooking($bookingId);
270
271 if ($depId) {
272 $depRepo = new DepartureRepository();
273 $dep = $depRepo->find($depId);
274 if (is_object($dep)) {
275 $date = (string) ($dep->start_date ?? $dep->date ?? '');
276 if ($date === '') {
277 $date = $travelDate;
278 }
279 $timeRaw = isset($dep->time) ? trim((string) $dep->time) : '';
280 if ($timeRaw !== '' && $timeRaw !== '00:00:00') {
281 $candidates = array_values(array_unique(array_filter([
282 $timeRaw,
283 self::normalizeTimeForAvailabilityMatch($timeRaw),
284 ])));
285 foreach ($candidates as $t) {
286 $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, $t, true);
287 if ($row && !empty($row->id)) {
288 return (int) $row->id;
289 }
290 }
291 }
292 $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, null, true);
293 if ($row && !empty($row->id)) {
294 return (int) $row->id;
295 }
296 }
297 }
298
299 $records = $availabilityRepo->findByTripAndDate($tripId, $travelDate);
300 if (count($records) === 1 && !empty($records[0]->id)) {
301 return (int) $records[0]->id;
302 }
303
304 return 0;
305 }
306
307 private static function normalizeTimeForAvailabilityMatch(string $time): string
308 {
309 $ts = strtotime($time);
310
311 return $ts !== false ? date('H:i:s', $ts) : $time;
312 }
313 }
314