PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 trunk, at app/Repositories/CustomerRepository.php

814 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\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 * Link a WordPress user to a customer, ONLY when the customer has none yet.
251 *
252 * Deliberately narrow: the WHERE clause requires the current user_id to be
253 * NULL/0, so this can add a login link but can never reassign or overwrite an
254 * existing one — a customer's login is never silently switched to a different
255 * account. Returns true only when a row was actually linked.
256 */
257 public function linkUserIfUnlinked(int $customerId, int $userId): bool
258 {
259 global $wpdb;
260
261 if ($customerId <= 0 || $userId <= 0) {
262 return false;
263 }
264
265 $table = $this->getTableName();
266
267 $result = $wpdb->query($wpdb->prepare(
268 "UPDATE `{$table}`
269 SET user_id = %d, updated_at = %s
270 WHERE id = %d AND (user_id IS NULL OR user_id = 0)",
271 $userId,
272 current_time('mysql'),
273 $customerId
274 ));
275
276 return $result > 0;
277 }
278
279 /**
280 * Update customer from admin form
281 *
282 * This is used by the CustomerService::updateCustomer method when saving
283 * changes from the admin Edit Customer screen.
284 *
285 * @param int $customerId Customer ID
286 * @param array $data Data to update
287 * @return bool
288 */
289 public function updateCustomer(int $customerId, array $data): bool
290 {
291 global $wpdb;
292
293 $table = $this->getTableName();
294
295 $updateData = [
296 'updated_at' => current_time('mysql'),
297 ];
298
299 // Simple text fields
300 if (array_key_exists('first_name', $data)) {
301 $updateData['first_name'] = sanitize_text_field($data['first_name']);
302 }
303 if (array_key_exists('last_name', $data)) {
304 $updateData['last_name'] = sanitize_text_field($data['last_name']);
305 }
306 if (array_key_exists('email', $data)) {
307 $updateData['email'] = sanitize_email($data['email']);
308 }
309 if (array_key_exists('phone', $data)) {
310 $updateData['phone'] = sanitize_text_field($data['phone']);
311 }
312 if (array_key_exists('secondary_phone', $data)) {
313 $updateData['secondary_phone'] = sanitize_text_field($data['secondary_phone']);
314 }
315 if (array_key_exists('country', $data)) {
316 $updateData['country'] = sanitize_text_field($data['country']);
317 }
318 if (array_key_exists('city', $data)) {
319 $updateData['city'] = sanitize_text_field($data['city']);
320 }
321 if (array_key_exists('state', $data)) {
322 $updateData['state'] = sanitize_text_field($data['state']);
323 }
324 if (array_key_exists('address', $data)) {
325 $updateData['address'] = sanitize_text_field($data['address']);
326 }
327 if (array_key_exists('postal_code', $data)) {
328 $updateData['postal_code'] = sanitize_text_field($data['postal_code']);
329 }
330 if (array_key_exists('nationality', $data)) {
331 $updateData['nationality'] = sanitize_text_field($data['nationality']);
332 }
333 if (array_key_exists('date_of_birth', $data)) {
334 $updateData['date_of_birth'] = sanitize_text_field($data['date_of_birth']);
335 }
336 if (array_key_exists('gender', $data)) {
337 $updateData['gender'] = sanitize_text_field($data['gender']);
338 }
339
340 // Emergency contact
341 if (array_key_exists('emergency_name', $data)) {
342 $updateData['emergency_name'] = sanitize_text_field($data['emergency_name']);
343 }
344 if (array_key_exists('emergency_phone', $data)) {
345 $updateData['emergency_phone'] = sanitize_text_field($data['emergency_phone']);
346 }
347 if (array_key_exists('emergency_relationship', $data)) {
348 $updateData['emergency_relationship'] = sanitize_text_field($data['emergency_relationship']);
349 }
350
351 // Special requirements & notes
352 if (array_key_exists('dietary_requirements', $data)) {
353 $updateData['dietary_requirements'] = sanitize_text_field($data['dietary_requirements']);
354 }
355 if (array_key_exists('medical_conditions', $data)) {
356 $updateData['medical_conditions'] = sanitize_textarea_field($data['medical_conditions']);
357 }
358 if (array_key_exists('special_needs', $data)) {
359 $updateData['special_needs'] = sanitize_textarea_field($data['special_needs']);
360 }
361 if (array_key_exists('notes', $data)) {
362 $updateData['notes'] = sanitize_textarea_field($data['notes']);
363 }
364
365 // Status & loyalty
366 if (array_key_exists('status', $data)) {
367 $updateData['status'] = sanitize_text_field($data['status']);
368 }
369 if (array_key_exists('loyalty_tier', $data)) {
370 $updateData['loyalty_tier'] = sanitize_text_field($data['loyalty_tier']);
371 }
372 if (array_key_exists('loyalty_points', $data)) {
373 $updateData['loyalty_points'] = (int) $data['loyalty_points'];
374 }
375
376 // Preferences
377 if (array_key_exists('newsletter_optin', $data)) {
378 $updateData['newsletter_optin'] = !empty($data['newsletter_optin']) ? 1 : 0;
379 }
380 if (array_key_exists('marketing_optin', $data)) {
381 $updateData['marketing_optin'] = !empty($data['marketing_optin']) ? 1 : 0;
382 }
383
384 // Nothing to update
385 if (count($updateData) === 1) {
386 return true;
387 }
388
389 $result = $wpdb->update(
390 $table,
391 $updateData,
392 ['id' => $customerId],
393 null,
394 ['%d']
395 );
396
397 return $result !== false;
398 }
399
400 /**
401 * Update gateway customer ID
402 *
403 * @param int $customerId
404 * @param string $gateway 'stripe', 'paypal', 'razorpay'
405 * @param string $gatewayCustomerId
406 * @return bool
407 */
408 public function updateGatewayCustomerId(int $customerId, string $gateway, string $gatewayCustomerId): bool
409 {
410 global $wpdb;
411
412 $table = $this->getTableName();
413 $column = $gateway . '_customer_id';
414
415 // Validate column name to prevent SQL injection
416 $allowedColumns = ['stripe_customer_id', 'paypal_customer_id', 'razorpay_customer_id'];
417 if (!in_array($column, $allowedColumns, true)) {
418 return false;
419 }
420
421 $result = $wpdb->update(
422 $table,
423 [
424 $column => sanitize_text_field($gatewayCustomerId),
425 'updated_at' => current_time('mysql'),
426 ],
427 ['id' => $customerId]
428 );
429
430 return $result !== false;
431 }
432
433 /**
434 * Delete a customer record
435 *
436 * CustomerService calls this method when deleting customers via the REST API.
437 * BaseRepository already implements delete(int $id), so this is a thin wrapper
438 * for backward/semantic compatibility.
439 *
440 * @param int $customerId
441 * @return bool
442 */
443 public function deleteCustomer(int $customerId): bool
444 {
445 return $this->delete($customerId);
446 }
447
448 /**
449 * Get gateway customer ID for a customer
450 *
451 * @param int $customerId
452 * @param string $gateway
453 * @return string|null
454 */
455 public function getGatewayCustomerId(int $customerId, string $gateway): ?string
456 {
457 $customer = $this->find($customerId);
458 if (!$customer) {
459 return null;
460 }
461
462 $column = $gateway . '_customer_id';
463 return !empty($customer->$column) ? $customer->$column : null;
464 }
465
466 /**
467 * Update customer stats (called after booking changes)
468 *
469 * @param int $customerId
470 * @return bool
471 */
472 public function recalculateStats(int $customerId): bool
473 {
474 global $wpdb;
475
476 $bookingRepository = new \Yatra\Repositories\BookingRepository();
477 $bookingsTable = $bookingRepository->getTableName();
478 $customersTable = $this->getTableName();
479
480 // Get stats from bookings
481 $stats = $wpdb->get_row($wpdb->prepare(
482 "SELECT
483 COUNT(*) as total_bookings,
484 COALESCE(SUM(total_amount), 0) as total_spent,
485 COALESCE(SUM(travelers_count), 0) as total_travelers,
486 MAX(created_at) as last_booking_date,
487 MAX(travel_date) as last_travel_date
488 FROM {$bookingsTable}
489 WHERE customer_id = %d AND status NOT IN ('cancelled', 'refunded', 'failed')",
490 $customerId
491 ));
492
493 if (!$stats) {
494 return false;
495 }
496
497 // Update customer
498 $result = $wpdb->update(
499 $customersTable,
500 [
501 'total_bookings' => (int) $stats->total_bookings,
502 'total_spent' => (float) $stats->total_spent,
503 'total_travelers' => (int) $stats->total_travelers,
504 'last_booking_date' => $stats->last_booking_date,
505 'last_travel_date' => $stats->last_travel_date,
506 'updated_at' => current_time('mysql'),
507 ],
508 ['id' => $customerId]
509 );
510
511 // Update loyalty tier based on total spent
512 $this->updateLoyaltyTier($customerId, (float) $stats->total_spent);
513
514 return $result !== false;
515 }
516
517 /**
518 * Update loyalty tier based on total spent
519 *
520 * @param int $customerId
521 * @param float $totalSpent
522 * @return void
523 */
524 private function updateLoyaltyTier(int $customerId, float $totalSpent): void
525 {
526 global $wpdb;
527
528 $table = $this->getTableName();
529
530 // Define tier thresholds (can be made configurable via settings)
531 $tier = 'bronze';
532 if ($totalSpent >= 10000) {
533 $tier = 'platinum';
534 } elseif ($totalSpent >= 5000) {
535 $tier = 'gold';
536 } elseif ($totalSpent >= 2000) {
537 $tier = 'silver';
538 }
539
540 // Tier expires 1 year from last update
541 $expiry = date('Y-m-d', strtotime('+1 year'));
542
543 $wpdb->update(
544 $table,
545 [
546 'loyalty_tier' => $tier,
547 'loyalty_tier_expiry' => $expiry,
548 ],
549 ['id' => $customerId]
550 );
551 }
552
553 /**
554 * Search customers
555 *
556 * @param string $search
557 * @param int $limit
558 * @param int $offset
559 * @return array
560 */
561 public function search(string $search, int $limit = 20, int $offset = 0): array
562 {
563 global $wpdb;
564
565 $table = $this->getTableName();
566 $search = '%' . $wpdb->esc_like($search) . '%';
567
568 return $wpdb->get_results($wpdb->prepare(
569 "SELECT * FROM {$table}
570 WHERE first_name LIKE %s
571 OR last_name LIKE %s
572 OR email LIKE %s
573 OR phone LIKE %s
574 ORDER BY created_at DESC
575 LIMIT %d OFFSET %d",
576 $search,
577 $search,
578 $search,
579 $search,
580 $limit,
581 $offset
582 )) ?: [];
583 }
584
585 /**
586 * Get customers with pagination
587 *
588 * @param array $args
589 * @return array ['data' => [], 'total' => int, 'pages' => int]
590 */
591 public function paginate(array $args = []): array
592 {
593 global $wpdb;
594
595 $table = $this->getTableName();
596
597 $page = max(1, (int) ($args['page'] ?? 1));
598 $perPage = max(1, min(100, (int) ($args['per_page'] ?? 20)));
599 $offset = ($page - 1) * $perPage;
600
601 $where = ['1=1'];
602 $params = [];
603
604 // Filter by status
605 if (!empty($args['status'])) {
606 $where[] = 'status = %s';
607 $params[] = sanitize_text_field($args['status']);
608 }
609
610 // Filter by loyalty tier
611 if (!empty($args['loyalty_tier'])) {
612 $where[] = 'loyalty_tier = %s';
613 $params[] = sanitize_text_field($args['loyalty_tier']);
614 }
615
616 // Filter by country
617 if (!empty($args['country'])) {
618 $where[] = 'country = %s';
619 $params[] = sanitize_text_field($args['country']);
620 }
621
622 // Search
623 if (!empty($args['search'])) {
624 $search = '%' . $wpdb->esc_like($args['search']) . '%';
625 $where[] = '(first_name LIKE %s OR last_name LIKE %s OR email LIKE %s OR phone LIKE %s)';
626 $params[] = $search;
627 $params[] = $search;
628 $params[] = $search;
629 $params[] = $search;
630 }
631
632 $whereClause = implode(' AND ', $where);
633
634 // Count total. With no status/search filters the WHERE clause is all
635 // literals, so there is nothing to bind — and prepare() on a
636 // placeholder-free query is exactly what WordPress warns about. The data
637 // query below always binds its LIMIT/OFFSET, so only this one needs the
638 // guard.
639 $countQuery = "SELECT COUNT(*) FROM {$table} WHERE {$whereClause}";
640 $total = (int) (empty($params)
641 ? $wpdb->get_var($countQuery)
642 : $wpdb->get_var($wpdb->prepare($countQuery, $params)));
643
644 // Get data
645 $orderBy = sanitize_sql_orderby($args['orderby'] ?? 'created_at') ?: 'created_at';
646 $order = strtoupper($args['order'] ?? 'DESC') === 'ASC' ? 'ASC' : 'DESC';
647
648 $dataQuery = "SELECT * FROM {$table} WHERE {$whereClause} ORDER BY {$orderBy} {$order} LIMIT %d OFFSET %d";
649 $params[] = $perPage;
650 $params[] = $offset;
651
652 $data = $wpdb->get_results($wpdb->prepare($dataQuery, $params)) ?: [];
653
654 return [
655 'data' => $data,
656 'total' => $total,
657 'pages' => (int) ceil($total / $perPage),
658 'page' => $page,
659 'per_page' => $perPage,
660 ];
661 }
662
663 /**
664 * Get customer bookings
665 *
666 * @param int $customerId
667 * @return array
668 */
669 public function getBookings(int $customerId): array
670 {
671 return $this->getCustomerBookings($customerId, 1000);
672 }
673
674 /**
675 * Get customer bookings with limit
676 *
677 * @param int $customerId Customer ID
678 * @param int $limit Limit results
679 * @return array
680 */
681 public function getCustomerBookings(int $customerId, int $limit = 10): array
682 {
683 global $wpdb;
684
685 $bookingRepository = new \Yatra\Repositories\BookingRepository();
686 $tripRepository = new \Yatra\Repositories\TripRepository();
687 $bookingsTable = $bookingRepository->getTableName();
688 $tripsTable = $tripRepository->getTableName();
689
690 return $wpdb->get_results($wpdb->prepare(
691 "SELECT b.*, t.title as trip_title, t.slug as trip_slug, t.featured_image as trip_image
692 FROM {$bookingsTable} b
693 LEFT JOIN {$tripsTable} t ON b.trip_id = t.id
694 WHERE b.customer_id = %d
695 ORDER BY b.created_at DESC
696 LIMIT %d",
697 $customerId,
698 $limit
699 )) ?: [];
700 }
701
702 /**
703 * Merge duplicate customers (by email)
704 *
705 * @param int $keepId Customer ID to keep
706 * @param int $mergeId Customer ID to merge into keepId
707 * @return bool
708 */
709 public function mergeCustomers(int $keepId, int $mergeId): bool
710 {
711 global $wpdb;
712
713 $bookingRepository = new \Yatra\Repositories\BookingRepository();
714 $bookingsTable = $bookingRepository->getTableName();
715 $customersTable = $this->getTableName();
716
717 // Update all bookings from mergeId to keepId
718 $wpdb->update(
719 $bookingsTable,
720 ['customer_id' => $keepId],
721 ['customer_id' => $mergeId],
722 ['%d'],
723 ['%d']
724 );
725
726 // Delete the merged customer
727 $wpdb->delete($customersTable, ['id' => $mergeId], ['%d']);
728
729 // Recalculate stats
730 $this->recalculateStats($keepId);
731
732 return true;
733 }
734
735 /**
736 * Get customer statistics
737 *
738 * @return array
739 */
740 public function getStats(): array
741 {
742 $table = $this->getTableName();
743
744 // Total customers by status
745 $statusStatsRaw = $this->wpdb->get_results(
746 "SELECT status, COUNT(*) as count FROM {$table} GROUP BY status",
747 OBJECT_K
748 );
749
750 // Normalize by_status with integer counts and default buckets
751 $byStatus = [
752 'active' => (object) ['status' => 'active', 'count' => 0],
753 'inactive' => (object) ['status' => 'inactive', 'count' => 0],
754 'blocked' => (object) ['status' => 'blocked', 'count' => 0],
755 ];
756
757 foreach ((array)$statusStatsRaw as $status => $row) {
758 $count = isset($row->count) ? (int)$row->count : 0;
759 if (isset($byStatus[$status])) {
760 $byStatus[$status]->count = $count;
761 } else {
762 // keep unexpected statuses too
763 $byStatus[$status] = (object) ['status' => $status, 'count' => $count];
764 }
765 }
766 // Normalize counts for UI expectations
767 return [
768 'all' => array_sum(array_column((array)$byStatus, 'count')),
769 'active' => $byStatus['active']->count ?? 0,
770 'inactive' => $byStatus['inactive']->count ?? 0,
771 'blocked' => $byStatus['blocked']->count ?? 0,
772 ];
773 }
774
775 /**
776 * Get payments for specific booking IDs with related booking and trip information
777 *
778 * @param array $bookingIds Array of booking IDs
779 * @param int $limit Maximum number of payments to return
780 * @return array Array of payment objects with booking and trip details
781 */
782 public function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array
783 {
784 global $wpdb;
785 $bookingRepository = new BookingRepository();
786 $bookings_table = $bookingRepository->getBookingsTableName();
787 $trips_table = TripsTable::getTableName();
788
789 // Use BookingPaymentsTable for payments
790 $payments_table = BookingPaymentsTable::getTableName();
791
792 $placeholders = implode(',', array_fill(0, count($bookingIds), '%d'));
793
794 $query = $wpdb->prepare(
795 "SELECT p.*,
796 b.reference as booking_reference,
797 b.amount_due as booking_amount_due,
798 b.amount_paid as booking_amount_paid,
799 b.total_amount as booking_total_amount,
800 t.title as trip_title
801 FROM {$payments_table} p
802 LEFT JOIN {$bookings_table} b ON p.booking_id = b.id
803 LEFT JOIN {$trips_table} t ON b.trip_id = t.id
804 WHERE p.booking_id IN ($placeholders)
805 ORDER BY p.created_at DESC
806 LIMIT %d",
807 array_merge($bookingIds, [$limit])
808 );
809
810 return $wpdb->get_results($query) ?: [];
811 }
812 }
813
814