PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
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 / Repositories / CustomerRepository.php

CustomerRepository.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.8, at app/Repositories/CustomerRepository.php

763 lines 24.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\Repositories;
6
7 use Yatra\Database\Tables\CustomersTable;
8 use Yatra\Database\Tables\BookingPaymentsTable;
9 use Yatra\Database\Tables\TripsTable;
10
11 /**
12 * Customer Repository
13 *
14 * Manages customer data in the yatra_customers table.
15 * Customers are separate from WordPress users - a customer may or may not have a WP account.
16 */
17 class CustomerRepository extends BaseRepository
18 {
19 /**
20 * Get full table name with prefix
21 */
22 protected function getTableName(): string
23 {
24 return CustomersTable::getTableName();
25 }
26
27 /**
28 * Find or create customer by email
29 *
30 * @param array $data Customer data
31 * @return int Customer ID
32 */
33 public function findOrCreate(array $data): int
34 {
35 $email = sanitize_email($data['email'] ?? '');
36
37 if (empty($email)) {
38 throw new \Exception('Customer email is required');
39 }
40
41 // Check if customer exists
42 $existing = $this->findByEmail($email);
43
44 if ($existing) {
45 // Update existing customer with new data
46 $this->updateFromBooking((int) $existing->id, $data);
47 return (int) $existing->id;
48 }
49
50 // Create new customer
51 return $this->createFromBooking($data);
52 }
53
54 /**
55 * Find customer by email
56 *
57 * @param string $email
58 * @return object|null
59 */
60 public function findByEmail(string $email): ?object
61 {
62 global $wpdb;
63
64 $table = $this->getTableName();
65 $email = sanitize_email($email);
66
67 $result = $wpdb->get_row($wpdb->prepare(
68 "SELECT * FROM {$table} WHERE email = %s",
69 $email
70 ));
71
72 return $result ?: null;
73 }
74
75 /**
76 * Find customer by WordPress user ID
77 *
78 * @param int $userId
79 * @return object|null
80 */
81 public function findByUserId(int $userId): ?object
82 {
83 global $wpdb;
84
85 $table = $this->getTableName();
86
87 $result = $wpdb->get_row($wpdb->prepare(
88 "SELECT * FROM {$table} WHERE user_id = %d",
89 $userId
90 ));
91
92 return $result ?: null;
93 }
94
95 /**
96 * Find customer by gateway customer ID
97 *
98 * @param string $gateway 'stripe', 'paypal', 'razorpay'
99 * @param string $gatewayCustomerId
100 * @return object|null
101 */
102 public function findByGatewayCustomerId(string $gateway, string $gatewayCustomerId): ?object
103 {
104 global $wpdb;
105
106 $table = $this->getTableName();
107 $column = $gateway . '_customer_id';
108
109 // Validate column name to prevent SQL injection
110 $allowedColumns = ['stripe_customer_id', 'paypal_customer_id', 'razorpay_customer_id'];
111 if (!in_array($column, $allowedColumns, true)) {
112 return null;
113 }
114
115 $result = $wpdb->get_row($wpdb->prepare(
116 "SELECT * FROM {$table} WHERE {$column} = %s",
117 $gatewayCustomerId
118 ));
119
120 return $result ?: null;
121 }
122
123 /**
124 * Create customer from booking data
125 *
126 * @param array $data
127 * @return int Customer ID
128 */
129 public function createFromBooking(array $data): int
130 {
131 global $wpdb;
132
133 $table = $this->getTableName();
134
135 $insertData = [
136 'user_id' => !empty($data['user_id']) ? (int) $data['user_id'] : null,
137 'first_name' => sanitize_text_field($data['first_name'] ?? ''),
138 'last_name' => sanitize_text_field($data['last_name'] ?? ''),
139 'email' => sanitize_email($data['email'] ?? ''),
140 'phone' => sanitize_text_field($data['phone'] ?? ''),
141 'address' => sanitize_text_field($data['address'] ?? ''),
142 'city' => sanitize_text_field($data['city'] ?? ''),
143 'country' => sanitize_text_field($data['country'] ?? ''),
144 'nationality' => sanitize_text_field($data['nationality'] ?? ''),
145 'newsletter_optin' => !empty($data['newsletter_optin']) ? 1 : 0,
146 'source' => sanitize_text_field($data['source'] ?? 'booking'),
147 'total_bookings' => 1,
148 'total_spent' => (float) ($data['total_spent'] ?? 0),
149 'last_booking_date' => current_time('mysql'),
150 'created_at' => current_time('mysql'),
151 'updated_at' => current_time('mysql'),
152 ];
153
154 // Handle emergency contact if provided
155 if (!empty($data['emergency_name'])) {
156 $insertData['emergency_name'] = sanitize_text_field($data['emergency_name']);
157 $insertData['emergency_phone'] = sanitize_text_field($data['emergency_phone'] ?? '');
158 $insertData['emergency_relationship'] = sanitize_text_field($data['emergency_relationship'] ?? '');
159 }
160
161 $result = $wpdb->insert($table, $insertData);
162
163 if ($result === false) {
164 throw new \Exception('Failed to create customer: ' . $wpdb->last_error);
165 }
166
167 return (int) $wpdb->insert_id;
168 }
169
170 /**
171 * Update customer from booking data
172 *
173 * @param int $customerId
174 * @param array $data
175 * @return bool
176 */
177 public function updateFromBooking(int $customerId, array $data): bool
178 {
179 global $wpdb;
180
181 $table = $this->getTableName();
182
183 // Get current customer data
184 $current = $this->find($customerId);
185 if (!$current) {
186 return false;
187 }
188
189 $updateData = [
190 'updated_at' => current_time('mysql'),
191 'last_booking_date' => current_time('mysql'),
192 'total_bookings' => ((int) $current->total_bookings) + 1,
193 'total_spent' => ((float) $current->total_spent) + ((float) ($data['total_spent'] ?? 0)),
194 ];
195
196 // Update phone if provided and not set
197 if (!empty($data['phone']) && empty($current->phone)) {
198 $updateData['phone'] = sanitize_text_field($data['phone']);
199 }
200
201 // Update address if provided and not set
202 if (!empty($data['address']) && empty($current->address)) {
203 $updateData['address'] = sanitize_text_field($data['address']);
204 }
205
206 // Update city if provided and not set
207 if (!empty($data['city']) && empty($current->city)) {
208 $updateData['city'] = sanitize_text_field($data['city']);
209 }
210
211 // Update country if provided and not set
212 if (!empty($data['country']) && empty($current->country)) {
213 $updateData['country'] = sanitize_text_field($data['country']);
214 }
215
216 // Update nationality if provided and not set
217 if (!empty($data['nationality']) && empty($current->nationality)) {
218 $updateData['nationality'] = sanitize_text_field($data['nationality']);
219 }
220
221 // Link WordPress user if not linked and provided
222 if (!empty($data['user_id']) && empty($current->user_id)) {
223 $updateData['user_id'] = (int) $data['user_id'];
224 }
225
226 // Update emergency contact if provided
227 if (!empty($data['emergency_name'])) {
228 $updateData['emergency_name'] = sanitize_text_field($data['emergency_name']);
229 $updateData['emergency_phone'] = sanitize_text_field($data['emergency_phone'] ?? '');
230 $updateData['emergency_relationship'] = sanitize_text_field($data['emergency_relationship'] ?? '');
231 }
232
233 // Update newsletter preference if opted in
234 if (!empty($data['newsletter_optin'])) {
235 $updateData['newsletter_optin'] = 1;
236 }
237
238 $result = $wpdb->update(
239 $table,
240 $updateData,
241 ['id' => $customerId],
242 null,
243 ['%d']
244 );
245
246 return $result !== false;
247 }
248
249 /**
250 * Update customer from admin form
251 *
252 * This is used by the CustomerService::updateCustomer method when saving
253 * changes from the admin Edit Customer screen.
254 *
255 * @param int $customerId Customer ID
256 * @param array $data Data to update
257 * @return bool
258 */
259 public function updateCustomer(int $customerId, array $data): bool
260 {
261 global $wpdb;
262
263 $table = $this->getTableName();
264
265 $updateData = [
266 'updated_at' => current_time('mysql'),
267 ];
268
269 // Simple text fields
270 if (array_key_exists('first_name', $data)) {
271 $updateData['first_name'] = sanitize_text_field($data['first_name']);
272 }
273 if (array_key_exists('last_name', $data)) {
274 $updateData['last_name'] = sanitize_text_field($data['last_name']);
275 }
276 if (array_key_exists('email', $data)) {
277 $updateData['email'] = sanitize_email($data['email']);
278 }
279 if (array_key_exists('phone', $data)) {
280 $updateData['phone'] = sanitize_text_field($data['phone']);
281 }
282 if (array_key_exists('secondary_phone', $data)) {
283 $updateData['secondary_phone'] = sanitize_text_field($data['secondary_phone']);
284 }
285 if (array_key_exists('country', $data)) {
286 $updateData['country'] = sanitize_text_field($data['country']);
287 }
288 if (array_key_exists('city', $data)) {
289 $updateData['city'] = sanitize_text_field($data['city']);
290 }
291 if (array_key_exists('state', $data)) {
292 $updateData['state'] = sanitize_text_field($data['state']);
293 }
294 if (array_key_exists('address', $data)) {
295 $updateData['address'] = sanitize_text_field($data['address']);
296 }
297 if (array_key_exists('postal_code', $data)) {
298 $updateData['postal_code'] = sanitize_text_field($data['postal_code']);
299 }
300 if (array_key_exists('nationality', $data)) {
301 $updateData['nationality'] = sanitize_text_field($data['nationality']);
302 }
303 if (array_key_exists('date_of_birth', $data)) {
304 $updateData['date_of_birth'] = sanitize_text_field($data['date_of_birth']);
305 }
306 if (array_key_exists('gender', $data)) {
307 $updateData['gender'] = sanitize_text_field($data['gender']);
308 }
309
310 // Emergency contact
311 if (array_key_exists('emergency_name', $data)) {
312 $updateData['emergency_name'] = sanitize_text_field($data['emergency_name']);
313 }
314 if (array_key_exists('emergency_phone', $data)) {
315 $updateData['emergency_phone'] = sanitize_text_field($data['emergency_phone']);
316 }
317 if (array_key_exists('emergency_relationship', $data)) {
318 $updateData['emergency_relationship'] = sanitize_text_field($data['emergency_relationship']);
319 }
320
321 // Special requirements & notes
322 if (array_key_exists('dietary_requirements', $data)) {
323 $updateData['dietary_requirements'] = sanitize_text_field($data['dietary_requirements']);
324 }
325 if (array_key_exists('medical_conditions', $data)) {
326 $updateData['medical_conditions'] = sanitize_textarea_field($data['medical_conditions']);
327 }
328 if (array_key_exists('special_needs', $data)) {
329 $updateData['special_needs'] = sanitize_textarea_field($data['special_needs']);
330 }
331 if (array_key_exists('notes', $data)) {
332 $updateData['notes'] = sanitize_textarea_field($data['notes']);
333 }
334
335 // Status & loyalty
336 if (array_key_exists('status', $data)) {
337 $updateData['status'] = sanitize_text_field($data['status']);
338 }
339 if (array_key_exists('loyalty_tier', $data)) {
340 $updateData['loyalty_tier'] = sanitize_text_field($data['loyalty_tier']);
341 }
342 if (array_key_exists('loyalty_points', $data)) {
343 $updateData['loyalty_points'] = (int) $data['loyalty_points'];
344 }
345
346 // Preferences
347 if (array_key_exists('newsletter_optin', $data)) {
348 $updateData['newsletter_optin'] = !empty($data['newsletter_optin']) ? 1 : 0;
349 }
350 if (array_key_exists('marketing_optin', $data)) {
351 $updateData['marketing_optin'] = !empty($data['marketing_optin']) ? 1 : 0;
352 }
353
354 // Nothing to update
355 if (count($updateData) === 1) {
356 return true;
357 }
358
359 $result = $wpdb->update(
360 $table,
361 $updateData,
362 ['id' => $customerId],
363 null,
364 ['%d']
365 );
366
367 return $result !== false;
368 }
369
370 /**
371 * Update gateway customer ID
372 *
373 * @param int $customerId
374 * @param string $gateway 'stripe', 'paypal', 'razorpay'
375 * @param string $gatewayCustomerId
376 * @return bool
377 */
378 public function updateGatewayCustomerId(int $customerId, string $gateway, string $gatewayCustomerId): bool
379 {
380 global $wpdb;
381
382 $table = $this->getTableName();
383 $column = $gateway . '_customer_id';
384
385 // Validate column name to prevent SQL injection
386 $allowedColumns = ['stripe_customer_id', 'paypal_customer_id', 'razorpay_customer_id'];
387 if (!in_array($column, $allowedColumns, true)) {
388 return false;
389 }
390
391 $result = $wpdb->update(
392 $table,
393 [
394 $column => sanitize_text_field($gatewayCustomerId),
395 'updated_at' => current_time('mysql'),
396 ],
397 ['id' => $customerId]
398 );
399
400 return $result !== false;
401 }
402
403 /**
404 * Get gateway customer ID for a customer
405 *
406 * @param int $customerId
407 * @param string $gateway
408 * @return string|null
409 */
410 public function getGatewayCustomerId(int $customerId, string $gateway): ?string
411 {
412 $customer = $this->find($customerId);
413 if (!$customer) {
414 return null;
415 }
416
417 $column = $gateway . '_customer_id';
418 return !empty($customer->$column) ? $customer->$column : null;
419 }
420
421 /**
422 * Update customer stats (called after booking changes)
423 *
424 * @param int $customerId
425 * @return bool
426 */
427 public function recalculateStats(int $customerId): bool
428 {
429 global $wpdb;
430
431 $bookingRepository = new \Yatra\Repositories\BookingRepository();
432 $bookingsTable = $bookingRepository->getTableName();
433 $customersTable = $this->getTableName();
434
435 // Get stats from bookings
436 $stats = $wpdb->get_row($wpdb->prepare(
437 "SELECT
438 COUNT(*) as total_bookings,
439 COALESCE(SUM(total_amount), 0) as total_spent,
440 COALESCE(SUM(travelers_count), 0) as total_travelers,
441 MAX(created_at) as last_booking_date,
442 MAX(travel_date) as last_travel_date
443 FROM {$bookingsTable}
444 WHERE customer_id = %d AND status NOT IN ('cancelled', 'refunded', 'failed')",
445 $customerId
446 ));
447
448 if (!$stats) {
449 return false;
450 }
451
452 // Update customer
453 $result = $wpdb->update(
454 $customersTable,
455 [
456 'total_bookings' => (int) $stats->total_bookings,
457 'total_spent' => (float) $stats->total_spent,
458 'total_travelers' => (int) $stats->total_travelers,
459 'last_booking_date' => $stats->last_booking_date,
460 'last_travel_date' => $stats->last_travel_date,
461 'updated_at' => current_time('mysql'),
462 ],
463 ['id' => $customerId]
464 );
465
466 // Update loyalty tier based on total spent
467 $this->updateLoyaltyTier($customerId, (float) $stats->total_spent);
468
469 return $result !== false;
470 }
471
472 /**
473 * Update loyalty tier based on total spent
474 *
475 * @param int $customerId
476 * @param float $totalSpent
477 * @return void
478 */
479 private function updateLoyaltyTier(int $customerId, float $totalSpent): void
480 {
481 global $wpdb;
482
483 $table = $this->getTableName();
484
485 // Define tier thresholds (can be made configurable via settings)
486 $tier = 'bronze';
487 if ($totalSpent >= 10000) {
488 $tier = 'platinum';
489 } elseif ($totalSpent >= 5000) {
490 $tier = 'gold';
491 } elseif ($totalSpent >= 2000) {
492 $tier = 'silver';
493 }
494
495 // Tier expires 1 year from last update
496 $expiry = date('Y-m-d', strtotime('+1 year'));
497
498 $wpdb->update(
499 $table,
500 [
501 'loyalty_tier' => $tier,
502 'loyalty_tier_expiry' => $expiry,
503 ],
504 ['id' => $customerId]
505 );
506 }
507
508 /**
509 * Search customers
510 *
511 * @param string $search
512 * @param int $limit
513 * @param int $offset
514 * @return array
515 */
516 public function search(string $search, int $limit = 20, int $offset = 0): array
517 {
518 global $wpdb;
519
520 $table = $this->getTableName();
521 $search = '%' . $wpdb->esc_like($search) . '%';
522
523 return $wpdb->get_results($wpdb->prepare(
524 "SELECT * FROM {$table}
525 WHERE first_name LIKE %s
526 OR last_name LIKE %s
527 OR email LIKE %s
528 OR phone LIKE %s
529 ORDER BY created_at DESC
530 LIMIT %d OFFSET %d",
531 $search,
532 $search,
533 $search,
534 $search,
535 $limit,
536 $offset
537 )) ?: [];
538 }
539
540 /**
541 * Get customers with pagination
542 *
543 * @param array $args
544 * @return array ['data' => [], 'total' => int, 'pages' => int]
545 */
546 public function paginate(array $args = []): array
547 {
548 global $wpdb;
549
550 $table = $this->getTableName();
551
552 $page = max(1, (int) ($args['page'] ?? 1));
553 $perPage = max(1, min(100, (int) ($args['per_page'] ?? 20)));
554 $offset = ($page - 1) * $perPage;
555
556 $where = ['1=1'];
557 $params = [];
558
559 // Filter by status
560 if (!empty($args['status'])) {
561 $where[] = 'status = %s';
562 $params[] = sanitize_text_field($args['status']);
563 }
564
565 // Filter by loyalty tier
566 if (!empty($args['loyalty_tier'])) {
567 $where[] = 'loyalty_tier = %s';
568 $params[] = sanitize_text_field($args['loyalty_tier']);
569 }
570
571 // Filter by country
572 if (!empty($args['country'])) {
573 $where[] = 'country = %s';
574 $params[] = sanitize_text_field($args['country']);
575 }
576
577 // Search
578 if (!empty($args['search'])) {
579 $search = '%' . $wpdb->esc_like($args['search']) . '%';
580 $where[] = '(first_name LIKE %s OR last_name LIKE %s OR email LIKE %s OR phone LIKE %s)';
581 $params[] = $search;
582 $params[] = $search;
583 $params[] = $search;
584 $params[] = $search;
585 }
586
587 $whereClause = implode(' AND ', $where);
588
589 // Count total
590 $countQuery = "SELECT COUNT(*) FROM {$table} WHERE {$whereClause}";
591 $total = (int) $wpdb->get_var($wpdb->prepare($countQuery, $params));
592
593 // Get data
594 $orderBy = sanitize_sql_orderby($args['orderby'] ?? 'created_at') ?: 'created_at';
595 $order = strtoupper($args['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
596
597 $dataQuery = "SELECT * FROM {$table} WHERE {$whereClause} ORDER BY {$orderBy} {$order} LIMIT %d OFFSET %d";
598 $params[] = $perPage;
599 $params[] = $offset;
600
601 $data = $wpdb->get_results($wpdb->prepare($dataQuery, $params)) ?: [];
602
603 return [
604 'data' => $data,
605 'total' => $total,
606 'pages' => (int) ceil($total / $perPage),
607 'page' => $page,
608 'per_page' => $perPage,
609 ];
610 }
611
612 /**
613 * Get customer bookings
614 *
615 * @param int $customerId
616 * @return array
617 */
618 public function getBookings(int $customerId): array
619 {
620 return $this->getCustomerBookings($customerId, 1000);
621 }
622
623 /**
624 * Get customer bookings with limit
625 *
626 * @param int $customerId Customer ID
627 * @param int $limit Limit results
628 * @return array
629 */
630 public function getCustomerBookings(int $customerId, int $limit = 10): array
631 {
632 global $wpdb;
633
634 $bookingRepository = new \Yatra\Repositories\BookingRepository();
635 $tripRepository = new \Yatra\Repositories\TripRepository();
636 $bookingsTable = $bookingRepository->getTableName();
637 $tripsTable = $tripRepository->getTableName();
638
639 return $wpdb->get_results($wpdb->prepare(
640 "SELECT b.*, t.title as trip_title, t.slug as trip_slug, t.featured_image as trip_image
641 FROM {$bookingsTable} b
642 LEFT JOIN {$tripsTable} t ON b.trip_id = t.id
643 WHERE b.customer_id = %d
644 ORDER BY b.created_at DESC
645 LIMIT %d",
646 $customerId,
647 $limit
648 )) ?: [];
649 }
650
651 /**
652 * Merge duplicate customers (by email)
653 *
654 * @param int $keepId Customer ID to keep
655 * @param int $mergeId Customer ID to merge into keepId
656 * @return bool
657 */
658 public function mergeCustomers(int $keepId, int $mergeId): bool
659 {
660 global $wpdb;
661
662 $bookingRepository = new \Yatra\Repositories\BookingRepository();
663 $bookingsTable = $bookingRepository->getTableName();
664 $customersTable = $this->getTableName();
665
666 // Update all bookings from mergeId to keepId
667 $wpdb->update(
668 $bookingsTable,
669 ['customer_id' => $keepId],
670 ['customer_id' => $mergeId],
671 ['%d'],
672 ['%d']
673 );
674
675 // Delete the merged customer
676 $wpdb->delete($customersTable, ['id' => $mergeId], ['%d']);
677
678 // Recalculate stats
679 $this->recalculateStats($keepId);
680
681 return true;
682 }
683
684 /**
685 * Get customer statistics
686 *
687 * @return array
688 */
689 public function getStats(): array
690 {
691 $table = $this->getTableName();
692
693 // Total customers by status
694 $statusStatsRaw = $this->wpdb->get_results(
695 "SELECT status, COUNT(*) as count FROM {$table} GROUP BY status",
696 OBJECT_K
697 );
698
699 // Normalize by_status with integer counts and default buckets
700 $byStatus = [
701 'active' => (object) ['status' => 'active', 'count' => 0],
702 'inactive' => (object) ['status' => 'inactive', 'count' => 0],
703 'blocked' => (object) ['status' => 'blocked', 'count' => 0],
704 ];
705
706 foreach ((array)$statusStatsRaw as $status => $row) {
707 $count = isset($row->count) ? (int)$row->count : 0;
708 if (isset($byStatus[$status])) {
709 $byStatus[$status]->count = $count;
710 } else {
711 // keep unexpected statuses too
712 $byStatus[$status] = (object) ['status' => $status, 'count' => $count];
713 }
714 }
715 // Normalize counts for UI expectations
716 return [
717 'all' => array_sum(array_column((array)$byStatus, 'count')),
718 'active' => $byStatus['active']->count ?? 0,
719 'inactive' => $byStatus['inactive']->count ?? 0,
720 'blocked' => $byStatus['blocked']->count ?? 0,
721 ];
722 }
723
724 /**
725 * Get payments for specific booking IDs with related booking and trip information
726 *
727 * @param array $bookingIds Array of booking IDs
728 * @param int $limit Maximum number of payments to return
729 * @return array Array of payment objects with booking and trip details
730 */
731 public function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array
732 {
733 global $wpdb;
734 $bookingRepository = new BookingRepository();
735 $bookings_table = $bookingRepository->getBookingsTableName();
736 $trips_table = TripsTable::getTableName();
737
738 // Use BookingPaymentsTable for payments
739 $payments_table = BookingPaymentsTable::getTableName();
740
741 $placeholders = implode(',', array_fill(0, count($bookingIds), '%d'));
742
743 $query = $wpdb->prepare(
744 "SELECT p.*,
745 b.reference as booking_reference,
746 b.amount_due as booking_amount_due,
747 b.amount_paid as booking_amount_paid,
748 b.total_amount as booking_total_amount,
749 t.title as trip_title
750 FROM {$payments_table} p
751 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
752 LEFT JOIN {$trips_table} t ON b.trip_id = t.id
753 WHERE p.booking_id IN ($placeholders)
754 ORDER BY p.created_at DESC
755 LIMIT %d",
756 array_merge($bookingIds, [$limit])
757 );
758
759 return $wpdb->get_results($query) ?: [];
760 }
761 }
762
763