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

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

784 lines 25.5 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 * Delete a customer record
405 *
406 * CustomerService calls this method when deleting customers via the REST API.
407 * BaseRepository already implements delete(int $id), so this is a thin wrapper
408 * for backward/semantic compatibility.
409 *
410 * @param int $customerId
411 * @return bool
412 */
413 public function deleteCustomer(int $customerId): bool
414 {
415 return $this->delete($customerId);
416 }
417
418 /**
419 * Get gateway customer ID for a customer
420 *
421 * @param int $customerId
422 * @param string $gateway
423 * @return string|null
424 */
425 public function getGatewayCustomerId(int $customerId, string $gateway): ?string
426 {
427 $customer = $this->find($customerId);
428 if (!$customer) {
429 return null;
430 }
431
432 $column = $gateway . '_customer_id';
433 return !empty($customer->$column) ? $customer->$column : null;
434 }
435
436 /**
437 * Update customer stats (called after booking changes)
438 *
439 * @param int $customerId
440 * @return bool
441 */
442 public function recalculateStats(int $customerId): bool
443 {
444 global $wpdb;
445
446 $bookingRepository = new \Yatra\Repositories\BookingRepository();
447 $bookingsTable = $bookingRepository->getTableName();
448 $customersTable = $this->getTableName();
449
450 // Get stats from bookings
451 $stats = $wpdb->get_row($wpdb->prepare(
452 "SELECT
453 COUNT(*) as total_bookings,
454 COALESCE(SUM(total_amount), 0) as total_spent,
455 COALESCE(SUM(travelers_count), 0) as total_travelers,
456 MAX(created_at) as last_booking_date,
457 MAX(travel_date) as last_travel_date
458 FROM {$bookingsTable}
459 WHERE customer_id = %d AND status NOT IN ('cancelled', 'refunded', 'failed')",
460 $customerId
461 ));
462
463 if (!$stats) {
464 return false;
465 }
466
467 // Update customer
468 $result = $wpdb->update(
469 $customersTable,
470 [
471 'total_bookings' => (int) $stats->total_bookings,
472 'total_spent' => (float) $stats->total_spent,
473 'total_travelers' => (int) $stats->total_travelers,
474 'last_booking_date' => $stats->last_booking_date,
475 'last_travel_date' => $stats->last_travel_date,
476 'updated_at' => current_time('mysql'),
477 ],
478 ['id' => $customerId]
479 );
480
481 // Update loyalty tier based on total spent
482 $this->updateLoyaltyTier($customerId, (float) $stats->total_spent);
483
484 return $result !== false;
485 }
486
487 /**
488 * Update loyalty tier based on total spent
489 *
490 * @param int $customerId
491 * @param float $totalSpent
492 * @return void
493 */
494 private function updateLoyaltyTier(int $customerId, float $totalSpent): void
495 {
496 global $wpdb;
497
498 $table = $this->getTableName();
499
500 // Define tier thresholds (can be made configurable via settings)
501 $tier = 'bronze';
502 if ($totalSpent >= 10000) {
503 $tier = 'platinum';
504 } elseif ($totalSpent >= 5000) {
505 $tier = 'gold';
506 } elseif ($totalSpent >= 2000) {
507 $tier = 'silver';
508 }
509
510 // Tier expires 1 year from last update
511 $expiry = date('Y-m-d', strtotime('+1 year'));
512
513 $wpdb->update(
514 $table,
515 [
516 'loyalty_tier' => $tier,
517 'loyalty_tier_expiry' => $expiry,
518 ],
519 ['id' => $customerId]
520 );
521 }
522
523 /**
524 * Search customers
525 *
526 * @param string $search
527 * @param int $limit
528 * @param int $offset
529 * @return array
530 */
531 public function search(string $search, int $limit = 20, int $offset = 0): array
532 {
533 global $wpdb;
534
535 $table = $this->getTableName();
536 $search = '%' . $wpdb->esc_like($search) . '%';
537
538 return $wpdb->get_results($wpdb->prepare(
539 "SELECT * FROM {$table}
540 WHERE first_name LIKE %s
541 OR last_name LIKE %s
542 OR email LIKE %s
543 OR phone LIKE %s
544 ORDER BY created_at DESC
545 LIMIT %d OFFSET %d",
546 $search,
547 $search,
548 $search,
549 $search,
550 $limit,
551 $offset
552 )) ?: [];
553 }
554
555 /**
556 * Get customers with pagination
557 *
558 * @param array $args
559 * @return array ['data' => [], 'total' => int, 'pages' => int]
560 */
561 public function paginate(array $args = []): array
562 {
563 global $wpdb;
564
565 $table = $this->getTableName();
566
567 $page = max(1, (int) ($args['page'] ?? 1));
568 $perPage = max(1, min(100, (int) ($args['per_page'] ?? 20)));
569 $offset = ($page - 1) * $perPage;
570
571 $where = ['1=1'];
572 $params = [];
573
574 // Filter by status
575 if (!empty($args['status'])) {
576 $where[] = 'status = %s';
577 $params[] = sanitize_text_field($args['status']);
578 }
579
580 // Filter by loyalty tier
581 if (!empty($args['loyalty_tier'])) {
582 $where[] = 'loyalty_tier = %s';
583 $params[] = sanitize_text_field($args['loyalty_tier']);
584 }
585
586 // Filter by country
587 if (!empty($args['country'])) {
588 $where[] = 'country = %s';
589 $params[] = sanitize_text_field($args['country']);
590 }
591
592 // Search
593 if (!empty($args['search'])) {
594 $search = '%' . $wpdb->esc_like($args['search']) . '%';
595 $where[] = '(first_name LIKE %s OR last_name LIKE %s OR email LIKE %s OR phone LIKE %s)';
596 $params[] = $search;
597 $params[] = $search;
598 $params[] = $search;
599 $params[] = $search;
600 }
601
602 $whereClause = implode(' AND ', $where);
603
604 // Count total. With no status/search filters the WHERE clause is all
605 // literals, so there is nothing to bind — and prepare() on a
606 // placeholder-free query is exactly what WordPress warns about. The data
607 // query below always binds its LIMIT/OFFSET, so only this one needs the
608 // guard.
609 $countQuery = "SELECT COUNT(*) FROM {$table} WHERE {$whereClause}";
610 $total = (int) (empty($params)
611 ? $wpdb->get_var($countQuery)
612 : $wpdb->get_var($wpdb->prepare($countQuery, $params)));
613
614 // Get data
615 $orderBy = sanitize_sql_orderby($args['orderby'] ?? 'created_at') ?: 'created_at';
616 $order = strtoupper($args['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
617
618 $dataQuery = "SELECT * FROM {$table} WHERE {$whereClause} ORDER BY {$orderBy} {$order} LIMIT %d OFFSET %d";
619 $params[] = $perPage;
620 $params[] = $offset;
621
622 $data = $wpdb->get_results($wpdb->prepare($dataQuery, $params)) ?: [];
623
624 return [
625 'data' => $data,
626 'total' => $total,
627 'pages' => (int) ceil($total / $perPage),
628 'page' => $page,
629 'per_page' => $perPage,
630 ];
631 }
632
633 /**
634 * Get customer bookings
635 *
636 * @param int $customerId
637 * @return array
638 */
639 public function getBookings(int $customerId): array
640 {
641 return $this->getCustomerBookings($customerId, 1000);
642 }
643
644 /**
645 * Get customer bookings with limit
646 *
647 * @param int $customerId Customer ID
648 * @param int $limit Limit results
649 * @return array
650 */
651 public function getCustomerBookings(int $customerId, int $limit = 10): array
652 {
653 global $wpdb;
654
655 $bookingRepository = new \Yatra\Repositories\BookingRepository();
656 $tripRepository = new \Yatra\Repositories\TripRepository();
657 $bookingsTable = $bookingRepository->getTableName();
658 $tripsTable = $tripRepository->getTableName();
659
660 return $wpdb->get_results($wpdb->prepare(
661 "SELECT b.*, t.title as trip_title, t.slug as trip_slug, t.featured_image as trip_image
662 FROM {$bookingsTable} b
663 LEFT JOIN {$tripsTable} t ON b.trip_id = t.id
664 WHERE b.customer_id = %d
665 ORDER BY b.created_at DESC
666 LIMIT %d",
667 $customerId,
668 $limit
669 )) ?: [];
670 }
671
672 /**
673 * Merge duplicate customers (by email)
674 *
675 * @param int $keepId Customer ID to keep
676 * @param int $mergeId Customer ID to merge into keepId
677 * @return bool
678 */
679 public function mergeCustomers(int $keepId, int $mergeId): bool
680 {
681 global $wpdb;
682
683 $bookingRepository = new \Yatra\Repositories\BookingRepository();
684 $bookingsTable = $bookingRepository->getTableName();
685 $customersTable = $this->getTableName();
686
687 // Update all bookings from mergeId to keepId
688 $wpdb->update(
689 $bookingsTable,
690 ['customer_id' => $keepId],
691 ['customer_id' => $mergeId],
692 ['%d'],
693 ['%d']
694 );
695
696 // Delete the merged customer
697 $wpdb->delete($customersTable, ['id' => $mergeId], ['%d']);
698
699 // Recalculate stats
700 $this->recalculateStats($keepId);
701
702 return true;
703 }
704
705 /**
706 * Get customer statistics
707 *
708 * @return array
709 */
710 public function getStats(): array
711 {
712 $table = $this->getTableName();
713
714 // Total customers by status
715 $statusStatsRaw = $this->wpdb->get_results(
716 "SELECT status, COUNT(*) as count FROM {$table} GROUP BY status",
717 OBJECT_K
718 );
719
720 // Normalize by_status with integer counts and default buckets
721 $byStatus = [
722 'active' => (object) ['status' => 'active', 'count' => 0],
723 'inactive' => (object) ['status' => 'inactive', 'count' => 0],
724 'blocked' => (object) ['status' => 'blocked', 'count' => 0],
725 ];
726
727 foreach ((array)$statusStatsRaw as $status => $row) {
728 $count = isset($row->count) ? (int)$row->count : 0;
729 if (isset($byStatus[$status])) {
730 $byStatus[$status]->count = $count;
731 } else {
732 // keep unexpected statuses too
733 $byStatus[$status] = (object) ['status' => $status, 'count' => $count];
734 }
735 }
736 // Normalize counts for UI expectations
737 return [
738 'all' => array_sum(array_column((array)$byStatus, 'count')),
739 'active' => $byStatus['active']->count ?? 0,
740 'inactive' => $byStatus['inactive']->count ?? 0,
741 'blocked' => $byStatus['blocked']->count ?? 0,
742 ];
743 }
744
745 /**
746 * Get payments for specific booking IDs with related booking and trip information
747 *
748 * @param array $bookingIds Array of booking IDs
749 * @param int $limit Maximum number of payments to return
750 * @return array Array of payment objects with booking and trip details
751 */
752 public function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array
753 {
754 global $wpdb;
755 $bookingRepository = new BookingRepository();
756 $bookings_table = $bookingRepository->getBookingsTableName();
757 $trips_table = TripsTable::getTableName();
758
759 // Use BookingPaymentsTable for payments
760 $payments_table = BookingPaymentsTable::getTableName();
761
762 $placeholders = implode(',', array_fill(0, count($bookingIds), '%d'));
763
764 $query = $wpdb->prepare(
765 "SELECT p.*,
766 b.reference as booking_reference,
767 b.amount_due as booking_amount_due,
768 b.amount_paid as booking_amount_paid,
769 b.total_amount as booking_total_amount,
770 t.title as trip_title
771 FROM {$payments_table} p
772 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
773 LEFT JOIN {$trips_table} t ON b.trip_id = t.id
774 WHERE p.booking_id IN ($placeholders)
775 ORDER BY p.created_at DESC
776 LIMIT %d",
777 array_merge($bookingIds, [$limit])
778 );
779
780 return $wpdb->get_results($query) ?: [];
781 }
782 }
783
784