PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / Services / CustomerService.php

CustomerService.php in Yatra – Travel Booking & Tour Operator Software 3.0.4, at app/Services/CustomerService.php

869 lines 31.3 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\Services;
6
7 use Yatra\Repositories\CustomerRepository;
8 use Yatra\Repositories\BookingRepository;
9 use Yatra\Repositories\PaymentRepository;
10
11 /**
12 * Customer Service
13 *
14 * Contains business logic for customer management.
15 *
16 * @package Yatra\Services
17 */
18 class CustomerService
19 {
20 private CustomerRepository $customerRepository;
21 private BookingRepository $bookingRepository;
22 private PaymentRepository $paymentRepository;
23
24 public function __construct()
25 {
26 $this->customerRepository = new CustomerRepository();
27 $this->bookingRepository = new BookingRepository();
28 $this->paymentRepository = new PaymentRepository();
29 }
30
31 /**
32 * Get customer statistics
33 *
34 * @return array
35 */
36 public function getStats(): array
37 {
38 return $this->customerRepository->getStats();
39 }
40
41 /**
42 * Get paginated customers
43 *
44 * @param array $filters Filters
45 * @return array
46 */
47 public function getCustomers(array $filters = []): array
48 {
49 $result = $this->customerRepository->paginate($filters);
50
51 $result['data'] = array_map([$this, 'formatCustomer'], $result['data']);
52
53 return $result;
54 }
55
56 /**
57 * Get single customer with details
58 *
59 * @param int $id Customer ID
60 * @return array|null
61 */
62 public function getCustomer(int $id): ?array
63 {
64 $customer = $this->customerRepository->find($id);
65
66 if (!$customer) {
67 return null;
68 }
69
70 return $this->formatCustomerWithDetails($customer);
71 }
72
73 /**
74 * Get customer by email
75 *
76 * @param string $email Customer email
77 * @return array|null
78 */
79 public function getCustomerByEmail(string $email): ?array
80 {
81 $customer = $this->customerRepository->findByEmail($email);
82
83 if (!$customer) {
84 return null;
85 }
86
87 return $this->formatCustomer($customer);
88 }
89
90 /**
91 * Get customer by WordPress user ID
92 *
93 * @param int $userId WordPress user ID
94 * @return array|null
95 */
96 public function getCustomerByUserId(int $userId): ?array
97 {
98 $customer = $this->customerRepository->findByUserId($userId);
99
100 if (!$customer) {
101 return null;
102 }
103
104 return $this->formatCustomer($customer);
105 }
106
107 /**
108 * Account page /customers/me: Yatra customer when linked, otherwise WordPress user (display name, email).
109 */
110 public function getAccountProfileForUser(int $userId): ?array
111 {
112 if ($userId <= 0) {
113 return null;
114 }
115
116 $customer = $this->getCustomerByUserId($userId);
117 if ($customer !== null) {
118 return $customer;
119 }
120
121 $user = get_userdata($userId);
122 if (!$user instanceof \WP_User) {
123 return null;
124 }
125
126 return $this->buildProfileArrayFromWpUser($user);
127 }
128
129 /**
130 * @return array<string, mixed>
131 */
132 private function buildProfileArrayFromWpUser(\WP_User $user): array
133 {
134 $first = trim((string) $user->first_name);
135 $last = trim((string) $user->last_name);
136 $fromParts = trim($first . ' ' . $last);
137 $display = trim((string) $user->display_name);
138 $name = $fromParts !== '' ? $fromParts : $display;
139 if ($name === '') {
140 $name = (string) $user->user_login;
141 }
142
143 return [
144 'id' => 0,
145 'user_id' => (int) $user->ID,
146 'name' => $name,
147 'first_name' => $first,
148 'last_name' => $last,
149 'email' => (string) $user->user_email,
150 'phone' => '',
151 'country' => '',
152 'city' => '',
153 'status' => 'active',
154 'total_bookings' => 0,
155 'total_spent' => 0.0,
156 'loyalty_tier' => '',
157 'created_at' => $user->user_registered,
158 'last_booking_date' => null,
159 'registered_at' => $user->user_registered,
160 ];
161 }
162
163 /**
164 * Create a new customer
165 *
166 * @param array $data Customer data
167 * @return array {success: bool, customer_id?: int, message: string}
168 */
169 public function createCustomer(array $data): array
170 {
171 // Validate required fields
172 if (empty($data['email'])) {
173 return ['success' => false, 'message' => __('Email is required.', 'yatra')];
174 }
175
176 // Validate email format
177 if (!is_email($data['email'])) {
178 return ['success' => false, 'message' => __('Please provide a valid email address.', 'yatra')];
179 }
180
181 // Check if customer already exists
182 $existingCustomer = $this->customerRepository->findByEmail($data['email']);
183 if ($existingCustomer) {
184 return [
185 'success' => false,
186 'message' => __('A customer with this email already exists.', 'yatra'),
187 'existing_id' => (int) $existingCustomer->id,
188 ];
189 }
190
191 // Create customer
192 $customerId = $this->customerRepository->findOrCreate($data);
193
194 if (!$customerId) {
195 return ['success' => false, 'message' => __('Failed to create customer.', 'yatra')];
196 }
197
198 return [
199 'success' => true,
200 'customer_id' => $customerId,
201 'message' => __('Customer created successfully.', 'yatra'),
202 ];
203 }
204
205 /**
206 * Update a customer
207 *
208 * @param int $id Customer ID
209 * @param array $data Customer data
210 * @return array {success: bool, message: string}
211 */
212 public function updateCustomer(int $id, array $data): array
213 {
214 $customer = $this->customerRepository->find($id);
215
216 if (!$customer) {
217 return ['success' => false, 'message' => __('Customer not found.', 'yatra')];
218 }
219
220 // Check email uniqueness if changing
221 if (!empty($data['email']) && $data['email'] !== $customer->email) {
222 $existingCustomer = $this->customerRepository->findByEmail($data['email']);
223 if ($existingCustomer && (int) $existingCustomer->id !== $id) {
224 return ['success' => false, 'message' => __('Email is already in use by another customer.', 'yatra')];
225 }
226 }
227
228 $updated = $this->customerRepository->updateCustomer($id, $data);
229
230 if (!$updated) {
231 return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')];
232 }
233
234 return [
235 'success' => true,
236 'message' => __('Customer updated successfully.', 'yatra'),
237 ];
238 }
239
240 /**
241 * Update customer status
242 *
243 * @param int $id Customer ID
244 * @param string $status New status (active, inactive, blocked)
245 * @return array {success: bool, message: string}
246 */
247 public function updateStatus(int $id, string $status): array
248 {
249 $validStatuses = ['active', 'inactive', 'blocked'];
250
251 if (!in_array($status, $validStatuses, true)) {
252 return ['success' => false, 'message' => __('Invalid status.', 'yatra')];
253 }
254
255 $customer = $this->customerRepository->find($id);
256
257 if (!$customer) {
258 return ['success' => false, 'message' => __('Customer not found.', 'yatra')];
259 }
260
261 $updated = $this->customerRepository->updateCustomer($id, ['status' => $status]);
262
263 if (!$updated) {
264 return ['success' => false, 'message' => __('Failed to update status.', 'yatra')];
265 }
266
267 return [
268 'success' => true,
269 'message' => sprintf(__('Customer status updated to %s.', 'yatra'), $status),
270 ];
271 }
272
273 /**
274 * Delete a customer
275 *
276 * @param int $id Customer ID
277 * @return array {success: bool, message: string}
278 */
279 public function deleteCustomer(int $id): array
280 {
281 $customer = $this->customerRepository->find($id);
282
283 if (!$customer) {
284 return ['success' => false, 'message' => __('Customer not found.', 'yatra')];
285 }
286
287 // Check for existing bookings
288 $bookings = $this->customerRepository->getCustomerBookings($id, 1);
289 if (!empty($bookings)) {
290 return [
291 'success' => false,
292 'message' => __('Cannot delete customer with existing bookings. Consider deactivating instead.', 'yatra'),
293 ];
294 }
295
296 $deleted = $this->customerRepository->deleteCustomer($id);
297
298 if (!$deleted) {
299 return ['success' => false, 'message' => __('Failed to delete customer.', 'yatra')];
300 }
301
302 return [
303 'success' => true,
304 'message' => __('Customer deleted successfully.', 'yatra'),
305 ];
306 }
307
308 /**
309 * Get customer's bookings
310 *
311 * @param int $customerId Customer ID
312 * @param int $limit Limit results
313 * @return array
314 */
315 public function getCustomerBookings(int $customerId, int $limit = 10): array
316 {
317 return $this->customerRepository->getCustomerBookings($customerId, $limit);
318 }
319
320 /**
321 * Get bookings by WordPress user ID (checks both customer_id and user_id)
322 *
323 * @param int $userId WordPress user ID
324 * @param int $limit Limit results
325 * @return array
326 */
327 public function getBookingsByUserId(int $userId, int $limit = 10): array
328 {
329 // First, try to get customer and bookings by customer_id
330 $customer = $this->getCustomerByUserId($userId);
331 $bookings = [];
332
333 if ($customer) {
334 $bookings = $this->getCustomerBookings((int) $customer['id'], $limit);
335 }
336
337 // Also get bookings directly by user_id (in case bookings were made before customer record was created)
338 $bookingsByUserId = $this->bookingRepository->findByUserId($userId, $limit);
339
340 // And include bookings made via the same email address
341 $emailBookings = [];
342 $user = get_userdata($userId);
343 if ($user && !empty($user->user_email)) {
344 $emailBookings = $this->bookingRepository->findByContactEmail($user->user_email, $limit);
345 }
346
347 // Merge and deduplicate by booking ID
348 $bookingIds = [];
349 $allBookings = [];
350 $sources = [$bookings, $bookingsByUserId, $emailBookings];
351
352 foreach ($sources as $collection) {
353 foreach ($collection as $booking) {
354 $bookingId = is_array($booking) ? ($booking['id'] ?? $booking['booking_id'] ?? null) : ($booking->id ?? null);
355 if ($bookingId && !in_array($bookingId, $bookingIds, true)) {
356 $bookingIds[] = $bookingId;
357 $allBookings[] = $booking;
358 }
359 }
360 }
361
362 // Limit results
363 if ($limit > 0 && count($allBookings) > $limit) {
364 $allBookings = array_slice($allBookings, 0, $limit);
365 }
366
367 return $allBookings;
368 }
369
370 public function getBookingDetailsForUser(int $userId, int $bookingId): ?array
371 {
372 if ($userId <= 0 || $bookingId <= 0) {
373 return null;
374 }
375
376 $booking = $this->bookingRepository->findWithTrip($bookingId);
377 if (!$booking) {
378 return null;
379 }
380
381 $user = get_userdata($userId);
382 $userEmail = ($user && !empty($user->user_email)) ? (string) $user->user_email : '';
383
384 $customer = $this->getCustomerByUserId($userId);
385 $customerId = $customer ? (int) ($customer['id'] ?? 0) : 0;
386
387 $bookingUserId = isset($booking->user_id) ? (int) $booking->user_id : 0;
388 $bookingCustomerId = isset($booking->customer_id) ? (int) $booking->customer_id : 0;
389 $bookingEmail = isset($booking->contact_email) ? (string) $booking->contact_email : '';
390
391 $allowed = false;
392 if ($bookingUserId > 0 && $bookingUserId === $userId) {
393 $allowed = true;
394 }
395 if (!$allowed && $customerId > 0 && $bookingCustomerId > 0 && $bookingCustomerId === $customerId) {
396 $allowed = true;
397 }
398 if (!$allowed && $userEmail !== '' && $bookingEmail !== '' && strtolower($userEmail) === strtolower($bookingEmail)) {
399 $allowed = true;
400 }
401
402 if (!$allowed) {
403 return null;
404 }
405
406 $emergencyContact = isset($booking->emergency_contact) ? maybe_unserialize($booking->emergency_contact) : null;
407 if (is_string($emergencyContact)) {
408 $decoded = json_decode($emergencyContact, true);
409 if (is_array($decoded)) {
410 $emergencyContact = $decoded;
411 }
412 }
413
414 $details = [
415 'id' => (int) ($booking->id ?? 0),
416 'reference' => $booking->reference ?? null,
417 'trip_id' => (int) ($booking->trip_id ?? 0),
418 'trip_title' => $booking->trip_title ?? null,
419 'trip_slug' => $booking->trip_slug ?? null,
420 'trip_url' => function_exists('yatra_get_trip_permalink') ? yatra_get_trip_permalink((int) ($booking->trip_id ?? 0)) : '',
421 'featured_image' => $booking->featured_image ?? null,
422 'created_at' => $booking->created_at ?? null,
423 'updated_at' => $booking->updated_at ?? null,
424 'travel_date' => $booking->travel_date ?? null,
425 'start_date' => $booking->start_date ?? $booking->travel_date ?? null,
426 'end_date' => $booking->end_date ?? null,
427 'travelers_count' => (int) ($booking->travelers_count ?? 0),
428 'total_amount' => (float) ($booking->total_amount ?? 0),
429 'amount_paid' => (float) ($booking->amount_paid ?? 0),
430 'amount_due' => (float) ($booking->amount_due ?? 0),
431 'currency' => $booking->currency ?? null,
432 'payment_status' => $booking->payment_status ?? null,
433 'status' => $booking->status ?? null,
434 'payment_gateway' => $booking->payment_gateway ?? null,
435 'contact_first_name' => $booking->contact_first_name ?? null,
436 'contact_last_name' => $booking->contact_last_name ?? null,
437 'contact_email' => $booking->contact_email ?? null,
438 'contact_phone' => $booking->contact_phone ?? null,
439 'contact_country' => $booking->contact_country ?? null,
440 'special_requests' => $booking->special_requests ?? null,
441 'emergency_contact' => $emergencyContact,
442 'contact_data' => isset($booking->contact_data) ? maybe_unserialize($booking->contact_data) : null,
443 'travelers' => isset($booking->travelers) ? maybe_unserialize($booking->travelers) : null,
444 'payments' => [],
445 ];
446
447 return apply_filters('yatra_customer_booking_details', $details, $booking, $userId);
448 }
449
450 /**
451 * Get customer's payments
452 *
453 * @param int $customerId Customer ID
454 * @param int $limit Limit results
455 * @return array
456 */
457 public function getCustomerPayments(int $customerId, int $limit = 50): array
458 {
459 $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000);
460 $bookingIds = array_map(static function($booking) {
461 if (is_object($booking)) {
462 return (int) ($booking->id ?? 0);
463 }
464 if (is_array($booking)) {
465 return (int) ($booking['id'] ?? 0);
466 }
467 return 0;
468 }, $bookings);
469
470 // Include bookings linked via user ID or email (older bookings may not have customer_id)
471 $customer = $this->customerRepository->find($customerId);
472 if ($customer) {
473 if (!empty($customer->user_id)) {
474 $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000);
475 $bookingIds = array_merge($bookingIds, array_map(static function($booking) {
476 if (is_object($booking)) {
477 return (int) ($booking->id ?? 0);
478 }
479 if (is_array($booking)) {
480 return (int) ($booking['id'] ?? 0);
481 }
482 return 0;
483 }, $userBookings));
484 }
485
486 if (!empty($customer->email)) {
487 $emailBookings = $this->bookingRepository->findByContactEmail($customer->email, 1000);
488 $bookingIds = array_merge($bookingIds, array_map(static function($booking) {
489 if (is_object($booking)) {
490 return (int) ($booking->id ?? 0);
491 }
492 if (is_array($booking)) {
493 return (int) ($booking['id'] ?? 0);
494 }
495 return 0;
496 }, $emailBookings));
497 }
498 }
499
500 $bookingIds = array_values(array_unique(array_filter($bookingIds)));
501
502 return $this->getPaymentsForBookingIds($bookingIds, $limit);
503 }
504
505 public function getPaymentsByUserId(int $userId, int $limit = 50): array
506 {
507 $bookings = $this->bookingRepository->findByUserId($userId, 1000);
508
509 $user = get_userdata($userId);
510 if ($user && !empty($user->user_email)) {
511 $bookingsByEmail = $this->bookingRepository->findByContactEmail($user->user_email, 1000);
512 $bookings = array_merge($bookings, $bookingsByEmail);
513 }
514
515 $bookingIds = array_map(static function($booking) {
516 if (is_object($booking)) {
517 return (int) ($booking->id ?? 0);
518 }
519 if (is_array($booking)) {
520 return (int) ($booking['id'] ?? 0);
521 }
522 return 0;
523 }, $bookings);
524
525 return $this->getPaymentsForBookingIds($bookingIds, $limit);
526 }
527
528 private function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array
529 {
530 $bookingIds = array_values(array_filter(array_map('intval', $bookingIds))); // ensure ints
531
532 if (empty($bookingIds)) {
533 return [];
534 }
535
536 $customerRepository = new \Yatra\Repositories\CustomerRepository();
537 $payments = $customerRepository->getPaymentsForBookingIds($bookingIds, $limit);
538
539 return array_map(static function($payment) {
540 return [
541 'id' => (int) $payment->id,
542 'booking_id' => (int) $payment->booking_id,
543 'booking_reference' => $payment->booking_reference,
544 'amount' => (float) $payment->amount,
545 'currency' => $payment->currency,
546 'status' => $payment->status,
547 'payment_method' => $payment->payment_method,
548 'gateway' => $payment->gateway,
549 'transaction_id' => $payment->transaction_id,
550 'created_at' => $payment->created_at,
551 'updated_at' => $payment->updated_at,
552 'trip_title' => $payment->trip_title,
553 'booking_amount_due' => (float) $payment->booking_amount_due,
554 'booking_amount_paid' => (float) $payment->booking_amount_paid,
555 'booking_total_amount' => (float) $payment->booking_total_amount,
556 ];
557 }, $payments);
558 }
559
560 public function getDocumentsForBookings(array $bookings, int $customerId = 0): array
561 {
562 $documents = [];
563
564 // Process each booking individually for vouchers and itineraries
565 // but group by trip for downloads
566 $tripsWithBookings = [];
567
568 foreach ($bookings as $booking) {
569 $bookingId = is_object($booking) ? (int) ($booking->id ?? 0) : (int) ($booking['id'] ?? $booking['booking_id'] ?? 0);
570 $tripId = is_object($booking) ? (int) ($booking->trip_id ?? 0) : (int) ($booking['trip_id'] ?? 0);
571 $tripTitle = is_object($booking) ? (string) ($booking->trip_title ?? '') : (string) ($booking['trip_title'] ?? '');
572 $reference = is_object($booking) ? ($booking->reference ?? null) : ($booking['reference'] ?? null);
573 $status = is_object($booking) ? (string) ($booking->status ?? '') : (string) ($booking['status'] ?? '');
574 $createdAt = is_object($booking) ? (string) ($booking->created_at ?? '') : (string) ($booking['created_at'] ?? '');
575
576 if ($bookingId <= 0) {
577 continue;
578 }
579
580 // Store trip info for downloads (grouped by trip)
581 if ($tripId > 0 && !isset($tripsWithBookings[$tripId])) {
582 $tripsWithBookings[$tripId] = [
583 'booking_id' => $bookingId,
584 'trip_title' => $tripTitle,
585 'reference' => $reference,
586 'status' => $status,
587 'created_at' => $createdAt,
588 ];
589 }
590
591 // Get payments for this booking (invoices per payment)
592 $payments = $this->paymentRepository->findByBookingId($bookingId);
593 foreach ($payments as $payment) {
594 $paymentId = (int) ($payment->id ?? 0);
595 if ($paymentId <= 0) {
596 continue;
597 }
598
599 $paymentStatus = (string) ($payment->status ?? '');
600 if (!in_array($paymentStatus, ['paid', 'completed', 'success'], true)) {
601 continue;
602 }
603
604 $docRef = $reference ?: $bookingId;
605
606 // Invoice per payment
607 $invoiceUrl = rest_url('yatra/v1/payment/' . $paymentId . '/invoice');
608 $invoiceUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $invoiceUrl);
609
610 $documents[] = [
611 'id' => 'invoice-payment-' . $paymentId,
612 'name' => sprintf(__('Invoice #%s.pdf', 'yatra'), $docRef),
613 'trip_title' => $tripTitle,
614 'category' => 'invoice',
615 'updated_at' => $payment->created_at ?? $createdAt ?: date('Y-m-d H:i:s'),
616 'url' => $invoiceUrl,
617 'booking_id' => $bookingId,
618 'payment_id' => $paymentId,
619 ];
620 }
621
622 // Voucher per booking
623 if ($status === 'confirmed') {
624 $docRef = $reference ?: $bookingId;
625
626 $voucherUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/voucher');
627 $voucherUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $voucherUrl);
628
629 $documents[] = [
630 'id' => 'voucher-' . $bookingId, // Booking-based ID
631 'name' => sprintf(__('Travel Voucher #%s.pdf', 'yatra'), $docRef),
632 'trip_title' => $tripTitle,
633 'category' => 'voucher',
634 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'),
635 'url' => $voucherUrl,
636 'booking_id' => $bookingId,
637 ];
638
639 // Itinerary per booking
640 $itineraryUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/itinerary');
641 $itineraryUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $itineraryUrl);
642
643 $documents[] = [
644 'id' => 'itinerary-' . $bookingId, // Booking-based ID
645 'name' => sprintf(__('Travel Itinerary #%s.pdf', 'yatra'), $docRef),
646 'trip_title' => $tripTitle,
647 'category' => 'itinerary',
648 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'),
649 'url' => $itineraryUrl,
650 'booking_id' => $bookingId,
651 ];
652 }
653 }
654
655 usort($documents, function ($a, $b) {
656 return strtotime($b['updated_at']) - strtotime($a['updated_at']);
657 });
658
659 // Apply downloads filter (which groups by trip)
660 $documents = apply_filters('yatra_customer_documents', $documents, $bookings, $customerId);
661
662 return is_array($documents) ? $documents : [];
663 }
664
665 /**
666 * Get customer's documents (invoices, vouchers, itineraries)
667 *
668 * @param int $customerId Customer ID
669 * @return array
670 */
671 public function getCustomerDocuments(int $customerId): array
672 {
673 // Get customer's bookings
674 $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000);
675
676 // Also include bookings linked via user_id/email (older bookings may not have customer_id)
677 $customer = $this->customerRepository->find($customerId);
678 if ($customer) {
679 if (!empty($customer->user_id)) {
680 $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000);
681 $bookings = array_merge($bookings, $userBookings);
682 }
683
684 if (!empty($customer->email)) {
685 $emailBookings = $this->bookingRepository->findByContactEmail((string) $customer->email, 1000);
686 $bookings = array_merge($bookings, $emailBookings);
687 }
688 }
689
690 // Deduplicate by booking id
691 $seen = [];
692 $unique = [];
693 foreach ($bookings as $b) {
694 $id = is_object($b) ? ($b->id ?? null) : ($b['id'] ?? $b['booking_id'] ?? null);
695 if ($id && !isset($seen[$id])) {
696 $seen[$id] = true;
697 $unique[] = $b;
698 }
699 }
700
701 return $this->getDocumentsForBookings($unique, $customerId);
702 }
703
704 /**
705 * Get customer's support tickets
706 *
707 * @param int $customerId Customer ID
708 * @return array
709 */
710 public function getCustomerSupportTickets(int $customerId): array
711 {
712 // For now, return empty array as support tickets system may not be implemented yet
713 // This can be extended when support ticket system is added
714 return [];
715 }
716
717 /**
718 * Merge two customer records
719 *
720 * @param int $sourceId Source customer ID (will be deleted)
721 * @param int $targetId Target customer ID (will be kept)
722 * @return array {success: bool, message: string}
723 */
724 public function mergeCustomers(int $sourceId, int $targetId): array
725 {
726 if ($sourceId === $targetId) {
727 return ['success' => false, 'message' => __('Cannot merge customer with itself.', 'yatra')];
728 }
729
730 $source = $this->customerRepository->find($sourceId);
731 $target = $this->customerRepository->find($targetId);
732
733 if (!$source || !$target) {
734 return ['success' => false, 'message' => __('One or both customers not found.', 'yatra')];
735 }
736
737 // Update all bookings to point to target customer
738 $this->bookingRepository->updateCustomerBookings($sourceId, $targetId);
739
740 // Update target customer stats
741 $this->customerRepository->updateCustomer($targetId, [
742 'total_bookings' => (int) $target->total_bookings + (int) $source->total_bookings,
743 'total_spent' => (float) $target->total_spent + (float) $source->total_spent,
744 ]);
745
746 // Delete source customer
747 $this->customerRepository->deleteCustomer($sourceId);
748
749 return [
750 'success' => true,
751 'message' => __('Customers merged successfully.', 'yatra'),
752 ];
753 }
754
755 /**
756 * Format customer for API response
757 *
758 * @param object $customer Raw customer data
759 * @return array
760 */
761 private function formatCustomer(object $customer): array
762 {
763 $name = trim((string) ($customer->first_name ?? '') . ' ' . (string) ($customer->last_name ?? ''));
764 if ($name === '') {
765 $uid = (int) ($customer->user_id ?? 0);
766 if ($uid > 0) {
767 $u = get_userdata($uid);
768 if ($u instanceof \WP_User) {
769 $name = trim((string) $u->display_name);
770 if ($name === '') {
771 $name = trim($u->first_name . ' ' . $u->last_name);
772 }
773 if ($name === '') {
774 $name = (string) $u->user_login;
775 }
776 }
777 }
778 }
779 if ($name === '' && !empty($customer->email)) {
780 $local = explode('@', (string) $customer->email)[0] ?? '';
781 $name = $local !== '' ? $local : $name;
782 }
783
784 $created = $customer->created_at ?? '';
785
786 return [
787 'id' => (int) $customer->id,
788 'user_id' => $customer->user_id ? (int) $customer->user_id : null,
789 'name' => $name,
790 'first_name' => $customer->first_name ?? '',
791 'last_name' => $customer->last_name ?? '',
792 'email' => $customer->email,
793 'phone' => $customer->phone ?? '',
794 'country' => $customer->country ?? '',
795 'city' => $customer->city ?? '',
796 'status' => $customer->status ?? 'active',
797 'total_bookings' => (int) ($customer->total_bookings ?? 0),
798 'total_spent' => (float) ($customer->total_spent ?? 0),
799 'loyalty_tier' => $customer->loyalty_tier ?? 'bronze',
800 'created_at' => $created,
801 'registered_at' => $created,
802 'last_booking_date' => $customer->last_booking_date ?? null,
803 ];
804 }
805
806 /**
807 * Format customer with all details
808 *
809 * @param object $customer Raw customer data
810 * @return array
811 */
812 private function formatCustomerWithDetails(object $customer): array
813 {
814 $formatted = $this->formatCustomer($customer);
815
816 // Add additional fields
817 $formatted['secondary_phone'] = $customer->secondary_phone ?? null;
818 $formatted['address'] = $customer->address ?? null;
819 $formatted['state'] = $customer->state ?? null;
820 $formatted['postal_code'] = $customer->postal_code ?? null;
821 $formatted['date_of_birth'] = $customer->date_of_birth ?? null;
822 $formatted['gender'] = $customer->gender ?? null;
823 $formatted['nationality'] = $customer->nationality ?? null;
824
825 // Emergency contact
826 $formatted['emergency_contact'] = [
827 'name' => $customer->emergency_name ?? null,
828 'phone' => $customer->emergency_phone ?? null,
829 'relationship' => $customer->emergency_relationship ?? null,
830 ];
831
832 // Preferences
833 $formatted['dietary_requirements'] = $customer->dietary_requirements ?? null;
834 $formatted['medical_conditions'] = $customer->medical_conditions ?? null;
835 $formatted['special_needs'] = $customer->special_needs ?? null;
836 $formatted['preferred_language'] = $customer->preferred_language ?? 'en';
837 $formatted['preferred_currency'] = $customer->preferred_currency ?? 'USD';
838
839 // Marketing
840 $formatted['newsletter_optin'] = (bool) ($customer->newsletter_optin ?? false);
841 $formatted['marketing_optin'] = (bool) ($customer->marketing_optin ?? false);
842 $formatted['source'] = $customer->source ?? null;
843
844 // Stats
845 $formatted['total_travelers'] = (int) ($customer->total_travelers ?? 0);
846 $formatted['last_travel_date'] = $customer->last_travel_date ?? null;
847 $formatted['loyalty_points'] = (int) ($customer->loyalty_points ?? 0);
848
849 // Gateway IDs
850 $formatted['stripe_customer_id'] = $customer->stripe_customer_id ?? null;
851 $formatted['paypal_customer_id'] = $customer->paypal_customer_id ?? null;
852 $formatted['razorpay_customer_id'] = $customer->razorpay_customer_id ?? null;
853
854 // Notes
855 $formatted['notes'] = $customer->notes ?? null;
856
857 // Recent bookings
858 $formatted['recent_bookings'] = $customer->recent_bookings ?? [];
859
860 // Timestamps
861 $formatted['updated_at'] = $customer->updated_at ?? null;
862 $formatted['last_login_at'] = $customer->last_login_at ?? null;
863 $formatted['verified_at'] = $customer->verified_at ?? null;
864
865 return $formatted;
866 }
867 }
868
869