PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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 2.0.11 All 82 releases
yatra / app / Controllers / TripAvailabilityController.php

TripAvailabilityController.php in Yatra – Travel Booking & Tour Operator Software 3.0.11, at app/Controllers/TripAvailabilityController.php

931 lines 36.7 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\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\DepartureService;
11 use Yatra\Services\RecurringRuleService;
12 use Yatra\Repositories\DepartureRepository;
13 use Yatra\Repositories\RecurringRuleRepository;
14
15 /**
16 * Trip Availability Controller
17 * REST API endpoints for managing trip departures and recurring rules
18 */
19 class TripAvailabilityController extends BaseController
20 {
21 private DepartureService $departureService;
22 private RecurringRuleService $ruleService;
23
24 public function __construct()
25 {
26 $departureRepo = new DepartureRepository();
27 $ruleRepo = new RecurringRuleRepository();
28
29 $this->departureService = new DepartureService($departureRepo);
30 $this->ruleService = new RecurringRuleService($ruleRepo, $departureRepo);
31 }
32
33 /**
34 * Register routes
35 */
36 public function register_routes(): void
37 {
38 $namespace = 'yatra/v1';
39
40 // All departures endpoint (without trip ID) — view cap.
41 register_rest_route($namespace, '/departures', [
42 [
43 'methods' => \WP_REST_Server::READABLE,
44 'callback' => [$this, 'get_all_departures'],
45 'permission_callback' => [$this, 'check_view_permission'],
46 ],
47 ]);
48
49 $base = 'trips/(?P<trip_id>[\d]+)/departures';
50
51 // Departures list + create — view cap for read, manage cap
52 // for create (a departure is a scheduled trip instance, not
53 // trip content edit).
54 register_rest_route($namespace, '/' . $base, [
55 [
56 'methods' => \WP_REST_Server::READABLE,
57 'callback' => [$this, 'get_departures'],
58 'permission_callback' => [$this, 'check_view_permission'],
59 ],
60 [
61 'methods' => \WP_REST_Server::CREATABLE,
62 'callback' => [$this, 'create_departure'],
63 'permission_callback' => [$this, 'check_manage_permission'],
64 ],
65 ]);
66
67 // Single departure — view / update / delete. Update is a
68 // manage operation; DELETE is the cancellation cap because
69 // dropping a departure typically means cancelling it.
70 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
71 [
72 'methods' => \WP_REST_Server::READABLE,
73 'callback' => [$this, 'get_departure'],
74 'permission_callback' => [$this, 'check_view_permission'],
75 ],
76 [
77 'methods' => \WP_REST_Server::EDITABLE,
78 'callback' => [$this, 'update_departure'],
79 'permission_callback' => [$this, 'check_manage_permission'],
80 ],
81 [
82 'methods' => \WP_REST_Server::DELETABLE,
83 'callback' => [$this, 'delete_departure'],
84 'permission_callback' => [$this, 'check_cancel_permission'],
85 ],
86 ]);
87
88 // Past departures endpoint — view cap.
89 register_rest_route($namespace, '/' . $base . '/past', [
90 [
91 'methods' => \WP_REST_Server::READABLE,
92 'callback' => [$this, 'get_past_departures'],
93 'permission_callback' => [$this, 'check_view_permission'],
94 ],
95 ]);
96
97 // Available dates endpoint (for frontend booking widget).
98 register_rest_route($namespace, '/trips/(?P<trip_id>[\d]+)/available-dates', [
99 [
100 'methods' => \WP_REST_Server::READABLE,
101 'callback' => [$this, 'get_available_dates'],
102 'permission_callback' => '__return_true', // Public endpoint
103 ],
104 ]);
105
106 // Recurring rules — these are availability templates on the
107 // TRIP, not on individual departures. Gated on the trip-edit
108 // cap (same as the other availability controllers).
109 $rulesBase = 'trips/(?P<trip_id>[\d]+)/recurring-rules';
110 register_rest_route($namespace, '/' . $rulesBase, [
111 [
112 'methods' => \WP_REST_Server::READABLE,
113 'callback' => [$this, 'get_recurring_rules'],
114 'permission_callback' => [$this, 'check_view_permission'],
115 ],
116 [
117 'methods' => \WP_REST_Server::CREATABLE,
118 'callback' => [$this, 'create_recurring_rule'],
119 'permission_callback' => [$this, 'check_trip_edit_permission'],
120 ],
121 ]);
122
123 register_rest_route($namespace, '/' . $rulesBase . '/(?P<id>[\d]+)', [
124 [
125 'methods' => \WP_REST_Server::READABLE,
126 'callback' => [$this, 'get_recurring_rule'],
127 'permission_callback' => [$this, 'check_view_permission'],
128 ],
129 [
130 'methods' => \WP_REST_Server::EDITABLE,
131 'callback' => [$this, 'update_recurring_rule'],
132 'permission_callback' => [$this, 'check_trip_edit_permission'],
133 ],
134 [
135 'methods' => \WP_REST_Server::DELETABLE,
136 'callback' => [$this, 'delete_recurring_rule'],
137 'permission_callback' => [$this, 'check_trip_edit_permission'],
138 ],
139 ]);
140
141 // Preview recurring-rule dates — view cap.
142 register_rest_route($namespace, '/' . $rulesBase . '/(?P<id>[\d]+)/preview', [
143 [
144 'methods' => \WP_REST_Server::READABLE,
145 'callback' => [$this, 'preview_recurring_rule'],
146 'permission_callback' => [$this, 'check_view_permission'],
147 ],
148 ]);
149 }
150
151 /**
152 * Granular cap checks for every Departure endpoint. The previous
153 * implementation gated everything on `manage_options` which
154 * locked Sales Agent / Front Desk / Guide / Accountant / Auditor
155 * out of the departures REST surface despite the role bundles
156 * granting them view / manage / cancel caps. WP admins pass via
157 * the Team module's admin-fallback filter.
158 */
159 public function check_view_permission(?WP_REST_Request $request = null): bool
160 {
161 return current_user_can('yatra_view_departures');
162 }
163
164 public function check_manage_permission(?WP_REST_Request $request = null): bool
165 {
166 // Held by Owner / Manager / Guide. Used for create + update.
167 return current_user_can('yatra_manage_departures');
168 }
169
170 public function check_cancel_permission(?WP_REST_Request $request = null): bool
171 {
172 // Held by Owner / Manager only by default. Cancelling a
173 // departure is a customer-affecting action (refunds, emails)
174 // so it gets the stricter cap than ordinary management.
175 return current_user_can('yatra_cancel_departures');
176 }
177
178 public function check_trip_edit_permission(?WP_REST_Request $request = null): bool
179 {
180 // Recurring rules belong to the parent trip, not to any one
181 // departure. Their lifecycle matches the trip-edit cap.
182 return current_user_can('yatra_edit_trips');
183 }
184
185 /**
186 * @deprecated Kept for any external code referencing the old
187 * method. Routes to view — safer default than the old
188 * `manage_options` shorthand. Admin users still pass via the
189 * admin-fallback layer.
190 */
191 public function check_permission(?WP_REST_Request $request = null): bool
192 {
193 return $this->check_view_permission($request);
194 }
195
196 // =========================================================================
197 // DEPARTURES ENDPOINTS
198 // =========================================================================
199
200 /**
201 * GET /trips/{trip_id}/departures
202 */
203 public function get_departures(WP_REST_Request $request): WP_REST_Response
204 {
205 $tripId = (int) $request->get_param('trip_id');
206 $status = $request->get_param('status');
207 $source = $request->get_param('source');
208 $dateFrom = $request->get_param('date_from');
209 $dateTo = $request->get_param('date_to');
210 $includePast = $request->get_param('include_past') !== 'false';
211
212 $filters = [];
213 if ($status) $filters['status'] = $status;
214 if ($source) $filters['source'] = $source;
215 if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom;
216 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
217 $filters['include_past'] = $includePast;
218
219 try {
220 $departures = $this->departureService->getByTripId($tripId, $filters);
221
222 // Get trip information
223 $tripRepository = new \Yatra\Repositories\TripRepository();
224 $trip = $tripRepository->find($tripId);
225
226 // Get booking departure repository for booking links
227 $bookingDepartureRepo = new \Yatra\Repositories\BookingDepartureRepository();
228 $travellerRepo = new \Yatra\Repositories\TravellerRepository();
229 $bookingRepo = new \Yatra\Repositories\BookingRepository();
230
231 // Get capacity service to sync capacity from availability
232 $capacityService = new \Yatra\Services\CapacityService();
233 $departureRepo = new \Yatra\Repositories\DepartureRepository();
234
235 return new WP_REST_Response([
236 'success' => true,
237 'data' => array_map(function ($d) use ($trip, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) {
238 // Sync capacity from availability before returning
239 $date = $d->start_date ?: $d->date;
240 $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date);
241 if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) {
242 $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]);
243 $d->max_capacity = $correctCapacity;
244 }
245
246 $departureArray = $d->toArray();
247
248 // Add trip information
249 if ($trip) {
250 $departureArray['trip'] = [
251 'id' => (int) $trip->id,
252 'title' => $trip->title ?? '',
253 'slug' => $trip->slug ?? '',
254 ];
255 }
256
257 // Add booking links and get travelers
258 $bookingIds = $bookingDepartureRepo->getBookingIdsForDeparture($d->id);
259 $departureArray['booking_ids'] = $bookingIds;
260 $departureArray['bookings_count'] = count($bookingIds);
261
262 // Recalculate revenue for departures with bookings
263 if (!empty($bookingIds)) {
264 try {
265 // Call the service method to recalculate revenue
266 $totalRevenue = 0.00;
267 foreach ($bookingIds as $bookingId) {
268 $booking = $bookingRepo->find($bookingId);
269 if ($booking && !empty($booking->total_amount)) {
270 $totalRevenue += (float) $booking->total_amount;
271 }
272 }
273 // Set revenue in the array directly
274 $departureArray['total_revenue'] = $totalRevenue;
275 } catch (\Exception $e) {
276 // If any error occurs, leave the original value
277 }
278 }
279
280 // Get all travelers for this departure
281 $allTravelers = [];
282 foreach ($bookingIds as $bookingId) {
283 $travelers = $travellerRepo->getByBookingId($bookingId);
284 $booking = $bookingRepo->find($bookingId);
285 foreach ($travelers as $traveler) {
286 $fields = $traveler['fields'] ?? [];
287
288 $firstName = $fields['first_name']
289 ?? $traveler['first_name']
290 ?? ($booking->contact_first_name ?? '');
291 $lastName = $fields['last_name']
292 ?? $traveler['last_name']
293 ?? ($booking->contact_last_name ?? '');
294
295 $email = $fields['email']
296 ?? $fields['contact_email']
297 ?? $fields['primary_email']
298 ?? ($booking->contact_email ?? '');
299
300 $phone = $fields['phone']
301 ?? $fields['contact_phone']
302 ?? $fields['mobile_phone']
303 ?? $fields['whatsapp']
304 ?? ($booking->contact_phone ?? '');
305
306 $allTravelers[] = [
307 'id' => (int) $traveler['id'],
308 'booking_id' => $bookingId,
309 'booking_reference' => $booking ? ($booking->reference ?? '') : '',
310 'is_lead' => (bool) ($traveler['is_lead'] ?? false),
311 'first_name' => $firstName,
312 'last_name' => $lastName,
313 'email' => $email,
314 'phone' => $phone,
315 ];
316 }
317 }
318 $departureArray['travelers'] = $allTravelers;
319 $departureArray['travelers_count'] = count($allTravelers);
320
321 // Format time for display (remove seconds if present)
322 if (!empty($departureArray['time'])) {
323 $time = $departureArray['time'];
324 // Convert HH:MM:SS to HH:MM if needed
325 if (strlen($time) > 5 && substr_count($time, ':') === 2) {
326 $departureArray['time'] = substr($time, 0, 5);
327 }
328 }
329
330 // Debug: Log time and revenue values
331 return $departureArray;
332 }, $departures),
333 'meta' => [
334 'total' => count($departures),
335 ],
336 ]);
337 } catch (\Exception $e) {
338 return new WP_REST_Response([
339 'success' => false,
340 'message' => $e->getMessage(),
341 ], 400);
342 }
343 }
344
345 /**
346 * GET /trips/{trip_id}/departures/{id}
347 */
348 public function get_departure(WP_REST_Request $request): WP_REST_Response
349 {
350 $id = (int) $request->get_param('id');
351 $tripId = (int) $request->get_param('trip_id');
352
353 $repo = new DepartureRepository();
354 $departure = $repo->findModel($id);
355
356 if (!$departure) {
357 return new WP_REST_Response([
358 'success' => false,
359 'message' => 'Departure not found',
360 ], 404);
361 }
362
363 // Sync capacity from availability before returning
364 $capacityService = new \Yatra\Services\CapacityService();
365 $date = $departure->start_date ?: $departure->date;
366 $correctCapacity = $capacityService->getCapacityForDate($departure->trip_id, $date);
367 if ($correctCapacity > 0 && $departure->max_capacity !== $correctCapacity) {
368 $repo->update($departure->id, ['max_capacity' => $correctCapacity]);
369 $departure->max_capacity = $correctCapacity;
370 }
371
372 // Base departure array
373 $departureArray = $departure->toArray();
374
375 // Trip information
376 $tripRepository = new \Yatra\Repositories\TripRepository();
377 $trip = $tripRepository->find($tripId ?: $departure->trip_id);
378 if ($trip) {
379 // Log trip data for debugging
380 \Yatra\Utils\Logger::info("Trip data for departure {$departure->id}: " . json_encode([
381 'duration' => $trip->duration ?? 'NULL',
382 'group_type' => $trip->group_type ?? 'NULL',
383 'difficulty_level' => $trip->difficulty_level ?? 'NULL',
384 'min_travelers' => $trip->min_travelers ?? 'NULL',
385 'max_travelers' => $trip->max_travelers ?? 'NULL',
386 ]));
387
388 // Fetch difficulty level name from difficulty_levels table
389 $difficultyLevelName = '';
390 if (!empty($trip->difficulty_level) && is_numeric($trip->difficulty_level)) {
391 $difficultyRepo = new \Yatra\Repositories\DifficultyLevelRepository();
392 $difficultyLevel = $difficultyRepo->find((int) $trip->difficulty_level);
393 if ($difficultyLevel) {
394 $difficultyLevelName = $difficultyLevel->name ?? '';
395 }
396 }
397
398 // Fetch group type name from traveler_categories table
399 $groupTypeName = '';
400 if (!empty($trip->group_type) && is_numeric($trip->group_type)) {
401 $travelerCategoryRepo = new \Yatra\Repositories\TravelerCategoryRepository();
402 $travelerCategory = $travelerCategoryRepo->find((int) $trip->group_type);
403 if ($travelerCategory) {
404 $groupTypeName = $travelerCategory->name ?? '';
405 }
406 }
407
408 $departureArray['trip'] = [
409 'id' => (int) $trip->id,
410 'title' => $trip->title ?? '',
411 'slug' => $trip->slug ?? '',
412 'summary' => $trip->short_description
413 ?? $trip->excerpt
414 ?? $trip->summary
415 ?? '',
416 'starting_location' => $trip->starting_location ?? '',
417 'ending_location' => $trip->ending_location ?? '',
418 'difficulty_level' => $difficultyLevelName,
419 'group_type' => $groupTypeName,
420 'min_travelers' => $trip->min_travelers ?? null,
421 'max_travelers' => $trip->max_travelers ?? null,
422 'duration' => $trip->duration ?? null,
423 'price' => $trip->price ?? null,
424 'created_at' => $trip->created_at ?? '',
425 ];
426 }
427
428 // Related bookings and travelers (mirror get_departures logic)
429 $bookingDepartureRepo = new \Yatra\Repositories\BookingDepartureRepository();
430 $travellerRepo = new \Yatra\Repositories\TravellerRepository();
431 $bookingRepo = new \Yatra\Repositories\BookingRepository();
432
433 $bookingIds = $bookingDepartureRepo->getBookingIdsForDeparture($departure->id);
434 $departureArray['booking_ids'] = $bookingIds;
435 $departureArray['bookings_count'] = count($bookingIds);
436
437 // Calculate total revenue from bookings (simple sum of total_amount like list endpoint)
438 if (!empty($bookingIds)) {
439 try {
440 $totalRevenue = 0.00;
441 foreach ($bookingIds as $bookingId) {
442 $booking = $bookingRepo->find($bookingId);
443 if ($booking && !empty($booking->total_amount)) {
444 $totalRevenue += (float) $booking->total_amount;
445 }
446 }
447 $departureArray['total_revenue'] = $totalRevenue;
448 } catch (\Exception $e) {
449 // Leave original value on error
450 if (defined('WP_DEBUG') && WP_DEBUG) {
451 }
452 }
453 }
454
455 // Travelers linked to this departure
456 $allTravelers = [];
457 foreach ($bookingIds as $bookingId) {
458 $travelers = $travellerRepo->getByBookingId($bookingId);
459 $booking = $bookingRepo->find($bookingId);
460 foreach ($travelers as $traveler) {
461 $fields = $traveler['fields'] ?? [];
462
463 $firstName = $fields['first_name']
464 ?? $traveler['first_name']
465 ?? ($booking->contact_first_name ?? '');
466 $lastName = $fields['last_name']
467 ?? $traveler['last_name']
468 ?? ($booking->contact_last_name ?? '');
469
470 $email = $fields['email']
471 ?? $fields['contact_email']
472 ?? $fields['primary_email']
473 ?? ($booking->contact_email ?? '');
474
475 $phone = $fields['phone']
476 ?? $fields['contact_phone']
477 ?? $fields['mobile_phone']
478 ?? $fields['whatsapp']
479 ?? ($booking->contact_phone ?? '');
480
481 $allTravelers[] = [
482 'id' => (int) $traveler['id'],
483 'booking_id' => $bookingId,
484 'booking_reference' => $booking ? ($booking->reference ?? '') : '',
485 'is_lead' => (bool) ($traveler['is_lead'] ?? false),
486 'first_name' => $firstName,
487 'last_name' => $lastName,
488 'email' => $email,
489 'phone' => $phone,
490 ];
491 }
492 }
493 $departureArray['travelers'] = $allTravelers;
494 $departureArray['travelers_count'] = count($allTravelers);
495
496 // Format time (HH:MM)
497 if (!empty($departureArray['time'])) {
498 $time = $departureArray['time'];
499 if (strlen($time) > 5 && substr_count($time, ':') === 2) {
500 $departureArray['time'] = substr($time, 0, 5);
501 }
502 }
503
504 return new WP_REST_Response([
505 'success' => true,
506 'data' => $departureArray,
507 ]);
508 }
509
510 /**
511 * POST /trips/{trip_id}/departures
512 */
513 public function create_departure(WP_REST_Request $request): WP_REST_Response
514 {
515 $tripId = (int) $request->get_param('trip_id');
516 $data = $request->get_json_params();
517 $data['trip_id'] = $tripId;
518
519 try {
520 $id = $this->departureService->create($data);
521
522 $repo = new DepartureRepository();
523 $departure = $repo->findModel($id);
524
525 return new WP_REST_Response([
526 'success' => true,
527 'data' => $departure->toArray(),
528 'message' => 'Departure created successfully',
529 ], 201);
530 } catch (\Exception $e) {
531 return new WP_REST_Response([
532 'success' => false,
533 'message' => $e->getMessage(),
534 ], 400);
535 }
536 }
537
538 /**
539 * PUT /trips/{trip_id}/departures/{id}
540 */
541 public function update_departure(WP_REST_Request $request): WP_REST_Response
542 {
543 $id = (int) $request->get_param('id');
544 $data = $request->get_json_params();
545
546 try {
547 $this->departureService->update($id, $data);
548
549 $repo = new DepartureRepository();
550 $departure = $repo->findModel($id);
551
552 return new WP_REST_Response([
553 'success' => true,
554 'data' => $departure->toArray(),
555 'message' => 'Departure updated successfully',
556 ]);
557 } catch (\Exception $e) {
558 return new WP_REST_Response([
559 'success' => false,
560 'message' => $e->getMessage(),
561 ], 400);
562 }
563 }
564
565 /**
566 * DELETE /trips/{trip_id}/departures/{id}
567 */
568 public function delete_departure(WP_REST_Request $request): WP_REST_Response
569 {
570 $id = (int) $request->get_param('id');
571
572 try {
573 $this->departureService->delete($id);
574
575 return new WP_REST_Response([
576 'success' => true,
577 'message' => 'Departure deleted successfully',
578 ]);
579 } catch (\Exception $e) {
580 return new WP_REST_Response([
581 'success' => false,
582 'message' => $e->getMessage(),
583 ], 400);
584 }
585 }
586
587 /**
588 * GET /departures
589 * Get departures from all trips
590 */
591 public function get_all_departures(WP_REST_Request $request): WP_REST_Response
592 {
593 $status = $request->get_param('status');
594 $source = $request->get_param('source');
595 $dateFrom = $request->get_param('date_from');
596 $dateTo = $request->get_param('date_to');
597 $includePast = $request->get_param('include_past') !== 'false';
598
599 $filters = [];
600 if ($status) $filters['status'] = $status;
601 if ($source) $filters['source'] = $source;
602 if ($dateFrom && trim($dateFrom) !== '') $filters['date_from'] = $dateFrom;
603 if ($dateTo && trim($dateTo) !== '') $filters['date_to'] = $dateTo;
604 $filters['include_past'] = $includePast;
605
606 try {
607 // Get all departures (no trip filter)
608 $departures = $this->departureService->getAllDepartures($filters);
609
610 // Get repository for additional data
611 $bookingDepartureRepo = new \Yatra\Repositories\BookingDepartureRepository();
612 $travellerRepo = new \Yatra\Repositories\TravellerRepository();
613 $bookingRepo = new \Yatra\Repositories\BookingRepository();
614 $tripRepository = new \Yatra\Repositories\TripRepository();
615
616 // Get capacity service to sync capacity from availability
617 $capacityService = new \Yatra\Services\CapacityService();
618 $departureRepo = new \Yatra\Repositories\DepartureRepository();
619
620 // Process each departure to add related data
621 $processed = array_map(function ($d) use ($tripRepository, $bookingDepartureRepo, $travellerRepo, $bookingRepo, $capacityService, $departureRepo) {
622 // Sync capacity from availability before returning
623 $date = $d->start_date ?: $d->date;
624 $correctCapacity = $capacityService->getCapacityForDate($d->trip_id, $date);
625 if ($correctCapacity > 0 && $d->max_capacity !== $correctCapacity) {
626 $departureRepo->update($d->id, ['max_capacity' => $correctCapacity]);
627 $d->max_capacity = $correctCapacity;
628 }
629
630 $departureArray = $d->toArray();
631
632 // Add trip information
633 $trip = $tripRepository->find($d->trip_id);
634 if ($trip) {
635 $departureArray['trip'] = [
636 'id' => (int) $trip->id,
637 'title' => $trip->title ?? '',
638 'slug' => $trip->slug ?? '',
639 ];
640 }
641
642 // Add booking links and get travelers
643 $bookingIds = $bookingDepartureRepo->getBookingIdsForDeparture($d->id);
644 $departureArray['booking_ids'] = $bookingIds;
645 $departureArray['bookings_count'] = count($bookingIds);
646
647 // Recalculate revenue for departures with bookings
648 if (!empty($bookingIds)) {
649 try {
650 $totalRevenue = 0.00;
651 foreach ($bookingIds as $bookingId) {
652 $booking = $bookingRepo->find($bookingId);
653 if ($booking && !empty($booking->total_amount)) {
654 $totalRevenue += (float) $booking->total_amount;
655 }
656 }
657 $departureArray['total_revenue'] = $totalRevenue;
658 } catch (\Exception $e) {
659 }
660 }
661
662 // Get all travelers for this departure
663 $allTravelers = [];
664 foreach ($bookingIds as $bookingId) {
665 $travelers = $travellerRepo->getByBookingId($bookingId);
666 $booking = $bookingRepo->find($bookingId);
667 foreach ($travelers as $traveler) {
668 $fields = $traveler['fields'] ?? [];
669
670 $firstName = $fields['first_name']
671 ?? $traveler['first_name']
672 ?? ($booking->contact_first_name ?? '');
673 $lastName = $fields['last_name']
674 ?? $traveler['last_name']
675 ?? ($booking->contact_last_name ?? '');
676
677 $email = $fields['email']
678 ?? $fields['contact_email']
679 ?? $fields['primary_email']
680 ?? ($booking->contact_email ?? '');
681
682 $phone = $fields['phone']
683 ?? $fields['contact_phone']
684 ?? $fields['mobile_phone']
685 ?? $fields['whatsapp']
686 ?? ($booking->contact_phone ?? '');
687
688 $allTravelers[] = [
689 'id' => (int) $traveler['id'],
690 'booking_id' => $bookingId,
691 'booking_reference' => $booking ? ($booking->reference ?? '') : '',
692 'is_lead' => (bool) ($traveler['is_lead'] ?? false),
693 'first_name' => $firstName,
694 'last_name' => $lastName,
695 'email' => $email,
696 'phone' => $phone,
697 ];
698 }
699 }
700 $departureArray['travelers'] = $allTravelers;
701 $departureArray['travelers_count'] = count($allTravelers);
702
703 // Format time for display (remove seconds if present)
704 if (!empty($departureArray['time'])) {
705 $time = $departureArray['time'];
706 if (strlen($time) > 5 && substr_count($time, ':') === 2) {
707 $departureArray['time'] = substr($time, 0, 5);
708 }
709 }
710
711 return $departureArray;
712 }, $departures);
713
714 return new WP_REST_Response([
715 'success' => true,
716 'data' => $processed,
717 'meta' => [
718 'total' => count($processed),
719 ],
720 ]);
721 } catch (\Exception $e) {
722 return new WP_REST_Response([
723 'success' => false,
724 'message' => $e->getMessage(),
725 ], 400);
726 }
727 }
728
729 /**
730 * GET /trips/{trip_id}/departures/past
731 */
732 public function get_past_departures(WP_REST_Request $request): WP_REST_Response
733 {
734 $tripId = (int) $request->get_param('trip_id');
735
736 try {
737 $departures = $this->departureService->getPastByTripId($tripId);
738
739 return new WP_REST_Response([
740 'success' => true,
741 'data' => array_map(function ($d) {
742 return $d->toArray();
743 }, $departures),
744 ]);
745 } catch (\Exception $e) {
746 return new WP_REST_Response([
747 'success' => false,
748 'message' => $e->getMessage(),
749 ], 400);
750 }
751 }
752
753 /**
754 * GET /trips/{trip_id}/available-dates
755 * Public endpoint for frontend to get available dates
756 */
757 public function get_available_dates(WP_REST_Request $request): WP_REST_Response
758 {
759 $tripId = (int) $request->get_param('trip_id');
760 $fromDate = $request->get_param('from_date') ?: date('Y-m-d');
761 $toDate = $request->get_param('to_date') ?: date('Y-m-d', strtotime('+12 months'));
762
763 try {
764 $dates = $this->departureService->getAvailableDates($tripId, $fromDate, $toDate);
765
766 return new WP_REST_Response([
767 'success' => true,
768 'data' => $dates,
769 ]);
770 } catch (\Exception $e) {
771 return new WP_REST_Response([
772 'success' => false,
773 'message' => $e->getMessage(),
774 ], 400);
775 }
776 }
777
778 // =========================================================================
779 // RECURRING RULES ENDPOINTS
780 // =========================================================================
781
782 /**
783 * GET /trips/{trip_id}/recurring-rules
784 */
785 public function get_recurring_rules(WP_REST_Request $request): WP_REST_Response
786 {
787 $tripId = (int) $request->get_param('trip_id');
788 $activeOnly = $request->get_param('active_only') === 'true';
789
790 try {
791 $rules = $this->ruleService->getByTripId($tripId, $activeOnly);
792
793 return new WP_REST_Response([
794 'success' => true,
795 'data' => array_map(function ($r) {
796 return $r->toArray();
797 }, $rules),
798 ]);
799 } catch (\Exception $e) {
800 return new WP_REST_Response([
801 'success' => false,
802 'message' => $e->getMessage(),
803 ], 400);
804 }
805 }
806
807 /**
808 * GET /trips/{trip_id}/recurring-rules/{id}
809 */
810 public function get_recurring_rule(WP_REST_Request $request): WP_REST_Response
811 {
812 $id = (int) $request->get_param('id');
813
814 $repo = new RecurringRuleRepository();
815 $rule = $repo->findModel($id);
816
817 if (!$rule) {
818 return new WP_REST_Response([
819 'success' => false,
820 'message' => 'Recurring rule not found',
821 ], 404);
822 }
823
824 return new WP_REST_Response([
825 'success' => true,
826 'data' => $rule->toArray(),
827 ]);
828 }
829
830 /**
831 * POST /trips/{trip_id}/recurring-rules
832 */
833 public function create_recurring_rule(WP_REST_Request $request): WP_REST_Response
834 {
835 $tripId = (int) $request->get_param('trip_id');
836 $data = $request->get_json_params();
837 $data['trip_id'] = $tripId;
838
839 try {
840 $id = $this->ruleService->create($data);
841
842 $repo = new RecurringRuleRepository();
843 $rule = $repo->findModel($id);
844
845 return new WP_REST_Response([
846 'success' => true,
847 'data' => $rule->toArray(),
848 'message' => 'Recurring rule created successfully',
849 ], 201);
850 } catch (\Exception $e) {
851 return new WP_REST_Response([
852 'success' => false,
853 'message' => $e->getMessage(),
854 ], 400);
855 }
856 }
857
858 /**
859 * PUT /trips/{trip_id}/recurring-rules/{id}
860 */
861 public function update_recurring_rule(WP_REST_Request $request): WP_REST_Response
862 {
863 $id = (int) $request->get_param('id');
864 $data = $request->get_json_params();
865
866 try {
867 $this->ruleService->update($id, $data);
868
869 $repo = new RecurringRuleRepository();
870 $rule = $repo->findModel($id);
871
872 return new WP_REST_Response([
873 'success' => true,
874 'data' => $rule->toArray(),
875 'message' => 'Recurring rule updated successfully',
876 ]);
877 } catch (\Exception $e) {
878 return new WP_REST_Response([
879 'success' => false,
880 'message' => $e->getMessage(),
881 ], 400);
882 }
883 }
884
885 /**
886 * DELETE /trips/{trip_id}/recurring-rules/{id}
887 */
888 public function delete_recurring_rule(WP_REST_Request $request): WP_REST_Response
889 {
890 $id = (int) $request->get_param('id');
891
892 try {
893 $this->ruleService->delete($id);
894
895 return new WP_REST_Response([
896 'success' => true,
897 'message' => 'Recurring rule deleted successfully',
898 ]);
899 } catch (\Exception $e) {
900 return new WP_REST_Response([
901 'success' => false,
902 'message' => $e->getMessage(),
903 ], 400);
904 }
905 }
906
907 /**
908 * GET /trips/{trip_id}/recurring-rules/{id}/preview
909 */
910 public function preview_recurring_rule(WP_REST_Request $request): WP_REST_Response
911 {
912 $id = (int) $request->get_param('id');
913 $count = (int) ($request->get_param('count') ?: 10);
914
915 try {
916 $dates = $this->ruleService->getPreviewDates($id, $count);
917
918 return new WP_REST_Response([
919 'success' => true,
920 'data' => $dates,
921 ]);
922 } catch (\Exception $e) {
923 return new WP_REST_Response([
924 'success' => false,
925 'message' => $e->getMessage(),
926 ], 400);
927 }
928 }
929 }
930
931