PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.16 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 All 84 releases
yatra / app / Hooks / AvailabilityInventoryHooks.php

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

271 lines 9.6 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 $availabilityId = self::resolveAvailabilityIdForBooking($bookingId, $tripId, $travelDate);
62 if ($availabilityId > 0) {
63 (new BookingRepository())->update($bookingId, ['availability_id' => $availabilityId]);
64 }
65 }
66
67 if ($tripId <= 0 || $availabilityId <= 0) {
68 return;
69 }
70
71 self::syncAvailabilityId($availabilityId, $tripId);
72 } catch (\Throwable $e) {
73 return;
74 }
75 }
76
77 private static function syncAvailabilityId(int $availabilityId, int $tripId): void
78 {
79 $availabilityRepo = new AvailabilityRepository();
80 $availability = $availabilityRepo->find($availabilityId);
81
82 if (!$availability) {
83 return;
84 }
85
86 $departureTime = isset($availability->departure_time) ? (string) $availability->departure_time : '';
87
88 $bookingRepo = new BookingRepository();
89 $bookedCount = $bookingRepo->getTotalTravelersByTripAndAvailability(
90 $tripId,
91 $availabilityId,
92 BookingRepository::getCapacityConsumingBookingStatuses()
93 );
94
95 $seatsTotal = (int) ($availability->seats_total ?? 0);
96 $seatsReserved = $bookedCount;
97 $seatsAvailable = max(0, $seatsTotal - $bookedCount);
98
99 $status = (string) ($availability->status ?? 'available');
100 if (!in_array($status, ['blocked', 'closed', 'cancelled'], true)) {
101 if ($seatsAvailable === 0) {
102 $status = 'sold_out';
103 } elseif ($seatsTotal > 0 && $seatsAvailable <= (int) ceil($seatsTotal * 0.2)) {
104 $status = 'limited';
105 } else {
106 $status = 'available';
107 }
108 }
109
110 // Update availability using repository
111 $availabilityRepo->update($availabilityId, [
112 'seats_available' => $seatsAvailable,
113 'seats_reserved' => $seatsReserved,
114 'status' => $status,
115 ]);
116
117 self::syncDepartureWithAvailability($tripId, $availability, $bookedCount, $seatsTotal);
118
119 WaitlistPromotionService::processAvailableSlots($tripId, $availabilityId);
120 }
121
122 private static function syncDepartureWithAvailability(int $tripId, object $availability, int $bookedCount, int $seatsTotal): void
123 {
124 $departureRepo = new DepartureRepository();
125 $date = (string) ($availability->departure_date ?? '');
126 $time = (string) ($availability->departure_time ?? '');
127 $normalizedTime = '';
128 if ($time !== '') {
129 $ts = strtotime($time);
130 if ($ts !== false) {
131 $normalizedTime = date('H:i:s', $ts);
132 }
133 }
134
135 if ($date === '') {
136 return;
137 }
138
139 // Find a matching departure by trip + date (+ time if provided)
140 $departure = null;
141 $candidateTimes = array_values(array_unique(array_filter([$normalizedTime, $time], function ($t) {
142 return $t !== '';
143 })));
144
145 foreach ($candidateTimes as $candidateTime) {
146 $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, $candidateTime);
147 if ($departure) {
148 break;
149 }
150 }
151 if (!$departure) {
152 $departure = $departureRepo->findByTripIdAndStartDate($tripId, $date, null);
153 }
154
155 if (!$departure) {
156 return;
157 }
158
159 $updated = [
160 'max_capacity' => $seatsTotal,
161 'booked_count' => min($seatsTotal, max(0, $bookedCount)),
162 ];
163
164 $departureRepo->update((int) $departure->id, $updated);
165 }
166
167 /**
168 * Hook to sync departure capacity when availability is updated
169 */
170 public static function onAvailabilityUpdate(int $availabilityId): void
171 {
172 $availabilityRepo = new AvailabilityRepository();
173 $availability = $availabilityRepo->find($availabilityId);
174
175 if ($availability) {
176 self::syncAvailabilityId($availabilityId, (int) $availability->trip_id);
177 }
178 }
179
180 /**
181 * Hook to ensure departure capacity always matches availability on save
182 */
183 public static function onDepartureSave(int $departureId): void
184 {
185 // Prevent infinite loops
186 static $processing = [];
187 if (isset($processing[$departureId])) {
188 return;
189 }
190 $processing[$departureId] = true;
191
192 $departureRepo = new DepartureRepository();
193 $departure = $departureRepo->find($departureId);
194
195 if ($departure) {
196 $date = $departure->start_date ?: $departure->date;
197
198 // Get capacity from availability
199 $capacityService = new CapacityService();
200 $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date);
201
202 if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) {
203 // Remove hook temporarily to prevent infinite loop
204 remove_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10);
205
206 $departureRepo->update($departureId, [
207 'max_capacity' => $correctCapacity
208 ]);
209
210 // Re-add hook
211 add_action('yatra_departure_saved', [self::class, 'onDepartureSave'], 10, 1);
212 }
213 }
214
215 unset($processing[$departureId]);
216 }
217
218 /**
219 * Infer yatra_new_trip_availability_dates.id when the booking row missed availability_id
220 * (common when checkout omits it). Uses booking→departure time to disambiguate multiple slots per day.
221 */
222 private static function resolveAvailabilityIdForBooking(int $bookingId, int $tripId, string $travelDate): int
223 {
224 $availabilityRepo = new AvailabilityRepository();
225 $bdRepo = new BookingDepartureRepository();
226 $depId = $bdRepo->getDepartureIdForBooking($bookingId);
227
228 if ($depId) {
229 $depRepo = new DepartureRepository();
230 $dep = $depRepo->find($depId);
231 if (is_object($dep)) {
232 $date = (string) ($dep->start_date ?? $dep->date ?? '');
233 if ($date === '') {
234 $date = $travelDate;
235 }
236 $timeRaw = isset($dep->time) ? trim((string) $dep->time) : '';
237 if ($timeRaw !== '' && $timeRaw !== '00:00:00') {
238 $candidates = array_values(array_unique(array_filter([
239 $timeRaw,
240 self::normalizeTimeForAvailabilityMatch($timeRaw),
241 ])));
242 foreach ($candidates as $t) {
243 $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, $t, true);
244 if ($row && !empty($row->id)) {
245 return (int) $row->id;
246 }
247 }
248 }
249 $row = $availabilityRepo->findByTripIdAndDateTime($tripId, $date, null, true);
250 if ($row && !empty($row->id)) {
251 return (int) $row->id;
252 }
253 }
254 }
255
256 $records = $availabilityRepo->findByTripAndDate($tripId, $travelDate);
257 if (count($records) === 1 && !empty($records[0]->id)) {
258 return (int) $records[0]->id;
259 }
260
261 return 0;
262 }
263
264 private static function normalizeTimeForAvailabilityMatch(string $time): string
265 {
266 $ts = strtotime($time);
267
268 return $ts !== false ? date('H:i:s', $ts) : $time;
269 }
270 }
271