PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / ReportsController.php

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

669 lines 26.4 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
10 class ReportsController extends BaseController
11 {
12 public function register_routes(): void
13 {
14 $namespace = 'yatra/v1';
15
16 register_rest_route($namespace, '/reports', [
17 [
18 'methods' => \WP_REST_Server::READABLE,
19 'callback' => [$this, 'get_reports'],
20 'permission_callback' => [$this, 'check_permission'],
21 ],
22 ]);
23 }
24
25 public function check_permission(?WP_REST_Request $request = null): bool
26 {
27 return current_user_can('manage_options');
28 }
29
30 /**
31 * GET /reports
32 * Central reporting endpoint used by the admin Reports page.
33 */
34 public function get_reports(WP_REST_Request $request): WP_REST_Response
35 {
36 $dateFrom = $request->get_param('date_from');
37 $dateTo = $request->get_param('date_to');
38
39 // Basic defaults: last 30 days if not provided
40 if (!$dateFrom || !$dateTo) {
41 $today = new \DateTimeImmutable('today');
42 $start = $today->sub(new \DateInterval('P30D'));
43 $dateFrom = $start->format('Y-m-d');
44 $dateTo = $today->format('Y-m-d');
45 }
46
47 $params = [
48 'date_from' => $dateFrom,
49 'date_to' => $dateTo,
50 ];
51
52 $bookingsList = $this->request('GET', '/yatra/v1/bookings', $params);
53 $paymentsList = $this->request('GET', '/yatra/v1/payments', $params);
54 $departuresList = $this->request('GET', '/yatra/v1/departures', [
55 'date_from' => $dateFrom,
56 'date_to' => $dateTo,
57 'include_past' => 'false',
58 ]);
59
60 $bookings = isset($bookingsList['data']) && is_array($bookingsList['data'])
61 ? $bookingsList['data']
62 : (is_array($bookingsList) ? $bookingsList : []);
63 $payments = is_array($paymentsList) ? $paymentsList : [];
64 $departures = isset($departuresList['data']) && is_array($departuresList['data'])
65 ? $departuresList['data']
66 : (is_array($departuresList) ? $departuresList : []);
67
68 // ------------------------------------------------------------------
69 // Normalize and strictly filter bookings to the requested date range
70 // using created_at (or travel_date) so that "Today" and other
71 // filters only reflect bookings actually in that window.
72 // ------------------------------------------------------------------
73 $fromTs = strtotime($dateFrom . ' 00:00:00');
74 $toTs = strtotime($dateTo . ' 23:59:59');
75
76 if ($fromTs === false || $toTs === false) {
77 $fromTs = null;
78 $toTs = null;
79 }
80
81 $filteredBookings = [];
82 foreach ($bookings as $b) {
83 $createdAt = $b['created_at'] ?? ($b['travel_date'] ?? null);
84 if (!$createdAt) {
85 continue;
86 }
87 $ts = strtotime((string) $createdAt);
88 if ($ts === false) {
89 continue;
90 }
91 if ($fromTs !== null && ($ts < $fromTs || $ts > $toTs)) {
92 continue;
93 }
94 $filteredBookings[] = $b;
95 }
96
97 $bookings = $filteredBookings;
98
99 // ------------------------------------------------------------------
100 // Revenue stats (derived from filtered bookings only)
101 // ------------------------------------------------------------------
102 $totalRevenue = 0.0;
103 $totalBookings = count($bookings);
104
105 foreach ($bookings as $b) {
106 if (isset($b['total_amount'])) {
107 $totalRevenue += (float) $b['total_amount'];
108 }
109 }
110
111 $averageBooking = $totalBookings > 0 ? $totalRevenue / $totalBookings : 0.0;
112
113 $revenueStats = [
114 'total' => $totalRevenue,
115 'bookings' => $totalBookings,
116 'average' => $averageBooking,
117 'previous' => 0.0,
118 'change' => 0.0,
119 ];
120
121 // ------------------------------------------------------------------
122 // Booking stats & trends
123 // ------------------------------------------------------------------
124 $statusCounts = [
125 'confirmed' => 0,
126 'pending' => 0,
127 'cancelled' => 0,
128 'completed' => 0,
129 ];
130
131 // Aggregate by DAY so the trend charts can show one point per day in
132 // the selected range (e.g. 1..7 when filtering 7 days).
133 $byDayCount = [];
134 $byDayRevenue = [];
135
136 foreach ($bookings as $b) {
137 $status = strtolower((string) ($b['status'] ?? 'pending'));
138 if (isset($statusCounts[$status])) {
139 $statusCounts[$status]++;
140 }
141
142 $createdAt = $b['created_at'] ?? ($b['travel_date'] ?? null);
143 if (!$createdAt) {
144 continue;
145 }
146 $ts = strtotime((string) $createdAt);
147 if ($ts === false) {
148 continue;
149 }
150 $dayKey = gmdate('Y-m-d', $ts);
151 $amount = isset($b['total_amount']) ? (float) $b['total_amount'] : 0.0;
152
153 if (!isset($byDayCount[$dayKey])) {
154 $byDayCount[$dayKey] = 0;
155 $byDayRevenue[$dayKey] = 0.0;
156 }
157 $byDayCount[$dayKey]++;
158 $byDayRevenue[$dayKey] += $amount;
159 }
160
161 $totalCount = array_sum($statusCounts);
162 $cancelled = $statusCounts['cancelled'];
163 $cancellationRate = $totalCount > 0 ? ($cancelled / $totalCount) * 100.0 : 0.0;
164
165 // Build a continuous list of DAYS across the selected range so that
166 // the charts always reflect the full date window (including days
167 // with zero bookings), rather than only the days that have data.
168 $bookingTrend = [];
169 $revenueTrend = [];
170
171 if ($fromTs !== null && $toTs !== null && $fromTs <= $toTs) {
172 $day = $fromTs;
173
174 while ($day <= $toTs) {
175 $key = gmdate('Y-m-d', $day);
176 $count = $byDayCount[$key] ?? 0;
177 $revenue = $byDayRevenue[$key] ?? 0.0;
178
179 $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $key);
180 if ($dt) {
181 // Label as "1 Nov", "2 Nov" etc. You can tweak format if needed.
182 $label = $dt->format('j M');
183 $bookingTrend[] = [
184 'label' => $label,
185 'value' => $count,
186 ];
187 $revenueTrend[] = [
188 'label' => $label,
189 'value' => $revenue,
190 ];
191 }
192
193 // increment by one day
194 $day = strtotime('+1 day', $day);
195 }
196 }
197
198 $bookingStats = [
199 'total' => $totalCount,
200 'confirmed' => $statusCounts['confirmed'],
201 'pending' => $statusCounts['pending'],
202 'cancelled' => $statusCounts['cancelled'],
203 'completed' => $statusCounts['completed'],
204 'cancellationRate' => $cancellationRate,
205 'conversionRate' => 0.0,
206 'averageBookingValue' => $averageBooking,
207 'trend' => $bookingTrend,
208 ];
209
210 // ------------------------------------------------------------------
211 // Trip performance (group by trip title)
212 // ------------------------------------------------------------------
213 $trips = [];
214 foreach ($bookings as $b) {
215 $title = $b['trip_title'] ?? ($b['trip']['title'] ?? __('(Untitled Trip)', 'yatra'));
216 $amount = isset($b['total_amount']) ? (float) $b['total_amount'] : 0.0;
217 if (!isset($trips[$title])) {
218 $trips[$title] = ['count' => 0, 'revenue' => 0.0];
219 }
220 $trips[$title]['count']++;
221 $trips[$title]['revenue'] += $amount;
222 }
223
224 arsort($trips);
225 $palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'];
226 $tripPerformance = [];
227 $i = 0;
228 foreach ($trips as $label => $stats) {
229 if ($i >= 6) break;
230 $tripPerformance[] = [
231 'label' => $label,
232 'value' => $stats['count'],
233 'revenue' => $stats['revenue'],
234 'occupancy' => 0,
235 'color' => $palette[$i % count($palette)],
236 ];
237 $i++;
238 }
239
240 // ------------------------------------------------------------------
241 // Payment status breakdown
242 // ------------------------------------------------------------------
243 $byStatus = [];
244 foreach ($payments as $p) {
245 $status = strtolower((string) ($p['status'] ?? 'pending'));
246 $amount = isset($p['amount']) ? (float) $p['amount'] : (isset($p['total_amount']) ? (float) $p['total_amount'] : 0.0);
247 if (!isset($byStatus[$status])) {
248 $byStatus[$status] = ['count' => 0, 'amount' => 0.0];
249 }
250 $byStatus[$status]['count']++;
251 $byStatus[$status]['amount'] += $amount;
252 }
253
254 $statusOrder = [
255 'paid' => ['label' => __('Paid', 'yatra'), 'color' => '#10b981'],
256 'pending' => ['label' => __('Pending', 'yatra'), 'color' => '#f59e0b'],
257 'refunded'=> ['label' => __('Refunded', 'yatra'), 'color' => '#ef4444'],
258 'partial' => ['label' => __('Partial', 'yatra'), 'color' => '#8b5cf6'],
259 ];
260
261 $paymentStatus = [];
262 foreach ($statusOrder as $key => $meta) {
263 if (!isset($byStatus[$key])) continue;
264 $paymentStatus[] = [
265 'label' => $meta['label'],
266 'value' => $byStatus[$key]['count'],
267 'amount' => $byStatus[$key]['amount'],
268 'color' => $meta['color'],
269 ];
270 }
271
272 // ------------------------------------------------------------------
273 // Operational stats from departures
274 // ------------------------------------------------------------------
275 $upcomingDepartures = 0;
276 $totalCapacity = 0;
277 $bookedCapacity = 0;
278 $upcomingTrips = [];
279
280 $todayTs = strtotime('today');
281
282 foreach ($departures as $d) {
283 $dateStr = $d['start_date'] ?? ($d['date'] ?? null);
284 $depTs = $dateStr ? strtotime((string) $dateStr) : false;
285 if ($depTs !== false && $depTs >= $todayTs) {
286 $upcomingDepartures++;
287 $upcomingTrips[] = [
288 'trip' => $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra')),
289 'date' => $dateStr,
290 'booked' => (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0),
291 'capacity' => (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0),
292 ];
293 }
294
295 $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0);
296 $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0);
297 $totalCapacity += $capacity;
298 $bookedCapacity += $booked;
299 }
300
301 $occupancyRate = $totalCapacity > 0 ? round(($bookedCapacity / $totalCapacity) * 100.0, 1) : 0.0;
302 $averageGroupSize = $upcomingDepartures > 0 ? round($bookedCapacity / $upcomingDepartures, 1) : 0.0;
303
304 $operationalStats = [
305 'upcomingDepartures' => $upcomingDepartures,
306 'totalCapacity' => $totalCapacity,
307 'bookedCapacity' => $bookedCapacity,
308 'occupancyRate' => $occupancyRate,
309 'averageGroupSize' => $averageGroupSize,
310 'upcomingTrips' => $upcomingTrips,
311 ];
312
313 // ------------------------------------------------------------------
314 // Customer analytics (group by email)
315 // ------------------------------------------------------------------
316 $customers = [];
317 foreach ($bookings as $b) {
318 $email = strtolower(trim((string) ($b['contact_email'] ?? $b['customer_email'] ?? '')));
319 if ($email === '') {
320 $email = __('Unknown', 'yatra');
321 }
322 $name = trim((string) (($b['contact_first_name'] ?? $b['customer_first_name'] ?? '') . ' ' . ($b['contact_last_name'] ?? $b['customer_last_name'] ?? '')));
323 if ($name === '') {
324 $name = $email;
325 }
326 $amount = isset($b['total_amount']) ? (float) $b['total_amount'] : 0.0;
327
328 if (!isset($customers[$email])) {
329 $customers[$email] = [
330 'name' => $name,
331 'email' => $email,
332 'bookings' => 0,
333 'revenue' => 0.0,
334 ];
335 }
336 $customers[$email]['bookings']++;
337 $customers[$email]['revenue'] += $amount;
338 }
339
340 $customerList = array_values($customers);
341 $totalCustomers = count($customerList);
342 $newCustomers = 0;
343 $returningCustomers = 0;
344 $firstTime = 0;
345 $returning23 = 0;
346 $loyal4 = 0;
347 $totalCustomerRevenue = 0.0;
348
349 foreach ($customerList as $c) {
350 $totalCustomerRevenue += $c['revenue'];
351 if ($c['bookings'] === 1) {
352 $newCustomers++;
353 $firstTime++;
354 } elseif ($c['bookings'] <= 3) {
355 $returningCustomers++;
356 $returning23++;
357 } else {
358 $returningCustomers++;
359 $loyal4++;
360 }
361 }
362
363 $repeatBookingRate = $totalCustomers > 0 ? ($returningCustomers / $totalCustomers) * 100.0 : 0.0;
364 $customerLifetimeValue = $totalCustomers > 0 ? $totalCustomerRevenue / $totalCustomers : 0.0;
365
366 usort($customerList, function ($a, $b) {
367 return $b['revenue'] <=> $a['revenue'];
368 });
369 $topCustomers = array_slice($customerList, 0, 5);
370
371 $customerSegments = [
372 ['label' => __('First-time', 'yatra'), 'value' => $firstTime, 'color' => '#3b82f6'],
373 ['label' => __('Returning (2-3)', 'yatra'), 'value' => $returning23, 'color' => '#10b981'],
374 ['label' => __('Loyal (4+)', 'yatra'), 'value' => $loyal4, 'color' => '#f59e0b'],
375 ];
376
377 $customerAnalytics = [
378 'newCustomers' => $newCustomers,
379 'returningCustomers' => $returningCustomers,
380 'totalCustomers' => $totalCustomers,
381 'customerLifetimeValue' => $customerLifetimeValue,
382 'repeatBookingRate' => $repeatBookingRate,
383 'customerRetentionRate' => $repeatBookingRate,
384 'topCustomers' => $topCustomers,
385 'customerSegments' => $customerSegments,
386 ];
387
388 // --------------------------------------------------------------
389 // Extended datasets for detailed reports UI
390 // --------------------------------------------------------------
391
392 // Revenue broken down by trip
393 $revenueByTrip = [];
394 foreach ($bookings as $b) {
395 $tripTitle = $b['trip_title'] ?? ($b['trip']['title'] ?? __('(Untitled Trip)', 'yatra'));
396 $amount = isset($b['total_amount']) ? (float) $b['total_amount'] : 0.0;
397 $status = strtolower((string) ($b['payment_status'] ?? $b['status'] ?? 'pending'));
398
399 if (!isset($revenueByTrip[$tripTitle])) {
400 $revenueByTrip[$tripTitle] = [
401 'trip' => $tripTitle,
402 'totalRevenue' => 0.0,
403 'bookings' => 0,
404 'paidTotal' => 0.0,
405 'pendingTotal' => 0.0,
406 'refundedTotal' => 0.0,
407 ];
408 }
409
410 $revenueByTrip[$tripTitle]['totalRevenue'] += $amount;
411 $revenueByTrip[$tripTitle]['bookings']++;
412
413 if ($status === 'paid' || $status === 'completed') {
414 $revenueByTrip[$tripTitle]['paidTotal'] += $amount;
415 } elseif ($status === 'pending') {
416 $revenueByTrip[$tripTitle]['pendingTotal'] += $amount;
417 } elseif ($status === 'refunded' || $status === 'cancelled') {
418 $revenueByTrip[$tripTitle]['refundedTotal'] += $amount;
419 }
420 }
421
422 foreach ($revenueByTrip as &$tripRow) {
423 $count = $tripRow['bookings'] > 0 ? $tripRow['bookings'] : 1;
424 $tripRow['avgRevenuePerBooking'] = $tripRow['totalRevenue'] / $count;
425 }
426 unset($tripRow);
427 $revenueByTripRows = array_values($revenueByTrip);
428
429 // Flat bookings table used by detailed booking and cancellation views
430 $bookingsTable = [];
431 foreach ($bookings as $b) {
432 $travelerCount = 0;
433 $travelerCount += isset($b['adult_count']) ? (int) $b['adult_count'] : 0;
434 $travelerCount += isset($b['child_count']) ? (int) $b['child_count'] : 0;
435 $travelerCount += isset($b['senior_count']) ? (int) $b['senior_count'] : 0;
436 $travelerCount += isset($b['student_count']) ? (int) $b['student_count'] : 0;
437
438 if ($travelerCount === 0 && isset($b['travelers_count'])) {
439 $travelerCount = (int) $b['travelers_count'];
440 }
441
442 $bookingsTable[] = [
443 'id' => $b['id'] ?? null,
444 'bookingNumber' => $b['booking_number'] ?? ($b['id'] ?? null),
445 'trip' => $b['trip_title'] ?? ($b['trip']['title'] ?? __('(Untitled Trip)', 'yatra')),
446 'departureDate' => $b['travel_date'] ?? null,
447 'travelerCount' => $travelerCount,
448 'price' => isset($b['total_amount']) ? (float) $b['total_amount'] : 0.0,
449 'paymentMethod' => $b['payment_method'] ?? ($b['gateway'] ?? null),
450 'status' => strtolower((string) ($b['status'] ?? 'pending')),
451 'cancellationReason' => $b['cancellation_reason'] ?? null,
452 'refundAmount' => isset($b['refund_amount']) ? (float) $b['refund_amount'] : 0.0,
453 ];
454 }
455
456 // Traveler segments (adult / child / senior / student) and trend
457 $travelerBuckets = [
458 'adult' => 0,
459 'child' => 0,
460 'senior' => 0,
461 'student' => 0,
462 ];
463 $byDayTravelers = [];
464
465 foreach ($bookings as $b) {
466 $adult = isset($b['adult_count']) ? (int) $b['adult_count'] : 0;
467 $child = isset($b['child_count']) ? (int) $b['child_count'] : 0;
468 $senior = isset($b['senior_count']) ? (int) $b['senior_count'] : 0;
469 $student = isset($b['student_count']) ? (int) $b['student_count'] : 0;
470
471 $travelerBuckets['adult'] += $adult;
472 $travelerBuckets['child'] += $child;
473 $travelerBuckets['senior'] += $senior;
474 $travelerBuckets['student'] += $student;
475
476 $createdAt = $b['created_at'] ?? ($b['travel_date'] ?? null);
477 if (!$createdAt) {
478 continue;
479 }
480 $ts = strtotime((string) $createdAt);
481 if ($ts === false) {
482 continue;
483 }
484 $dayKey = gmdate('Y-m-d', $ts);
485 $totalTravelers = $adult + $child + $senior + $student;
486 if (!isset($byDayTravelers[$dayKey])) {
487 $byDayTravelers[$dayKey] = 0;
488 }
489 $byDayTravelers[$dayKey] += $totalTravelers;
490 }
491
492 $travelersTrend = [];
493 if ($fromTs !== null && $toTs !== null && $fromTs <= $toTs) {
494 $day = $fromTs;
495 while ($day <= $toTs) {
496 $key = gmdate('Y-m-d', $day);
497 $count = $byDayTravelers[$key] ?? 0;
498 $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $key);
499 if ($dt) {
500 $travelersTrend[] = [
501 'label' => $dt->format('j M'),
502 'value' => $count,
503 ];
504 }
505 $day = strtotime('+1 day', $day);
506 }
507 }
508
509 $totalTravelersAll = array_sum($travelerBuckets);
510 $avgTravelersPerBooking = $totalBookings > 0 ? $totalTravelersAll / $totalBookings : 0.0;
511 $topTravelerCategory = null;
512 if ($totalTravelersAll > 0) {
513 $maxVal = -1;
514 foreach ($travelerBuckets as $key => $val) {
515 if ($val > $maxVal) {
516 $maxVal = $val;
517 $topTravelerCategory = $key;
518 }
519 }
520 }
521
522 $travelerSegments = [
523 'segments' => [
524 ['label' => __('Adult', 'yatra'), 'key' => 'adult', 'value' => $travelerBuckets['adult']],
525 ['label' => __('Child', 'yatra'), 'key' => 'child', 'value' => $travelerBuckets['child']],
526 ['label' => __('Senior', 'yatra'), 'key' => 'senior', 'value' => $travelerBuckets['senior']],
527 ['label' => __('Student', 'yatra'), 'key' => 'student', 'value' => $travelerBuckets['student']],
528 ],
529 'totalTravelers' => $totalTravelersAll,
530 'avgTravelersPerBooking' => $avgTravelersPerBooking,
531 'topCategory' => $topTravelerCategory,
532 'trend' => $travelersTrend,
533 ];
534
535 // Departures table and occupancy datasets
536 $departuresTable = [];
537 $occupancyByDay = [];
538 $capacityByDay = [];
539 $seatUtilizationByTrip = [];
540
541 foreach ($departures as $d) {
542 $dateStr = $d['start_date'] ?? ($d['date'] ?? null);
543 $tripTitle = $d['trip']['title'] ?? ($d['trip_title'] ?? __('Unknown Trip', 'yatra'));
544 $capacity = (int) ($d['max_capacity'] ?? $d['total_spots'] ?? 0);
545 $booked = (int) ($d['booked_count'] ?? $d['travelers_count'] ?? 0);
546 $left = $capacity > 0 ? max(0, $capacity - $booked) : 0;
547 $status = strtolower((string) ($d['status'] ?? 'upcoming'));
548
549 $departuresTable[] = [
550 'date' => $dateStr,
551 'trip' => $tripTitle,
552 'maxSeats' => $capacity,
553 'bookedSeats' => $booked,
554 'leftSeats' => $left,
555 'status' => $status,
556 ];
557
558 if ($dateStr) {
559 $dayKey = substr((string) $dateStr, 0, 10);
560 if (!isset($occupancyByDay[$dayKey])) {
561 $occupancyByDay[$dayKey] = 0;
562 $capacityByDay[$dayKey] = 0;
563 }
564 $occupancyByDay[$dayKey] += $booked;
565 $capacityByDay[$dayKey] += $capacity;
566 }
567
568 if (!isset($seatUtilizationByTrip[$tripTitle])) {
569 $seatUtilizationByTrip[$tripTitle] = ['trip' => $tripTitle, 'booked' => 0, 'capacity' => 0];
570 }
571 $seatUtilizationByTrip[$tripTitle]['booked'] += $booked;
572 $seatUtilizationByTrip[$tripTitle]['capacity'] += $capacity;
573 }
574
575 $occupancyTrend = [];
576 foreach ($occupancyByDay as $dayKey => $bookedSum) {
577 $capSum = $capacityByDay[$dayKey] ?? 0;
578 if ($capSum <= 0) {
579 continue;
580 }
581 $dt = \DateTimeImmutable::createFromFormat('Y-m-d', $dayKey);
582 if ($dt) {
583 $occupancyTrend[] = [
584 'label' => $dt->format('j M'),
585 'value' => round(($bookedSum / $capSum) * 100.0, 1),
586 ];
587 }
588 }
589
590 $seatUtilization = [];
591 foreach ($seatUtilizationByTrip as $row) {
592 $cap = $row['capacity'] > 0 ? $row['capacity'] : 1;
593 $seatUtilization[] = [
594 'trip' => $row['trip'],
595 'utilization' => round(($row['booked'] / $cap) * 100.0, 1),
596 ];
597 }
598
599 // Cancellations summary
600 $totalCancellations = 0;
601 $revenueLost = 0.0;
602 foreach ($bookingsTable as $row) {
603 if ($row['status'] === 'cancelled') {
604 $totalCancellations++;
605 $revenueLost += $row['refundAmount'] > 0 ? $row['refundAmount'] : $row['price'];
606 }
607 }
608
609 $cancellationRatePercent = $totalCount > 0 ? ($totalCancellations / $totalCount) * 100.0 : 0.0;
610 $cancellationsSummary = [
611 'totalCancellations' => $totalCancellations,
612 'cancellationRate' => $cancellationRatePercent,
613 'revenueLost' => $revenueLost,
614 ];
615
616 // Profitability placeholders (phase 2)
617 $profitabilityPlaceholders = [
618 'profitPerTrip' => [],
619 'costVsRevenue' => [],
620 ];
621
622 return new WP_REST_Response([
623 'success' => true,
624 'data' => [
625 'revenue_stats' => $revenueStats,
626 'revenue_trend' => $revenueTrend,
627 'booking_stats' => $bookingStats,
628 'booking_trend' => $bookingTrend,
629 'trip_performance' => $tripPerformance,
630 'payment_status' => $paymentStatus,
631 'operational_stats' => $operationalStats,
632 'customer_analytics' => $customerAnalytics,
633 // Extended datasets
634 'revenue_by_trip' => $revenueByTripRows,
635 'bookings_table' => $bookingsTable,
636 'traveler_segments' => $travelerSegments,
637 'departures_table' => $departuresTable,
638 'occupancy_trend' => $occupancyTrend,
639 'seat_utilization' => $seatUtilization,
640 'cancellations' => $cancellationsSummary,
641 'profitability' => $profitabilityPlaceholders,
642 ],
643 ]);
644 }
645
646 /**
647 * Helper to call an internal REST endpoint and return decoded data
648 * in array form. This keeps all reporting logic in one place while
649 * reusing existing controllers.
650 *
651 * @param string $method
652 * @param string $route
653 * @param array<string,mixed> $params
654 * @return mixed
655 */
656 private function request(string $method, string $route, array $params = [])
657 {
658 $req = new \WP_REST_Request($method, $route);
659 foreach ($params as $key => $value) {
660 $req->set_param($key, $value);
661 }
662 $response = rest_do_request($req);
663 if ($response instanceof \WP_REST_Response) {
664 return $response->get_data();
665 }
666 return null;
667 }
668 }
669