PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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.2.9, at app/Services/CustomerService.php

867 lines 31.1 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 'travelers_count' => (int) ($booking->travelers_count ?? 0),
426 'total_amount' => (float) ($booking->total_amount ?? 0),
427 'amount_paid' => (float) ($booking->amount_paid ?? 0),
428 'amount_due' => (float) ($booking->amount_due ?? 0),
429 'currency' => $booking->currency ?? null,
430 'payment_status' => $booking->payment_status ?? null,
431 'status' => $booking->status ?? null,
432 'payment_gateway' => $booking->payment_gateway ?? null,
433 'contact_first_name' => $booking->contact_first_name ?? null,
434 'contact_last_name' => $booking->contact_last_name ?? null,
435 'contact_email' => $booking->contact_email ?? null,
436 'contact_phone' => $booking->contact_phone ?? null,
437 'contact_country' => $booking->contact_country ?? null,
438 'special_requests' => $booking->special_requests ?? null,
439 'emergency_contact' => $emergencyContact,
440 'contact_data' => isset($booking->contact_data) ? maybe_unserialize($booking->contact_data) : null,
441 'travelers' => isset($booking->travelers) ? maybe_unserialize($booking->travelers) : null,
442 'payments' => [],
443 ];
444
445 return apply_filters('yatra_customer_booking_details', $details, $booking, $userId);
446 }
447
448 /**
449 * Get customer's payments
450 *
451 * @param int $customerId Customer ID
452 * @param int $limit Limit results
453 * @return array
454 */
455 public function getCustomerPayments(int $customerId, int $limit = 50): array
456 {
457 $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000);
458 $bookingIds = array_map(static function($booking) {
459 if (is_object($booking)) {
460 return (int) ($booking->id ?? 0);
461 }
462 if (is_array($booking)) {
463 return (int) ($booking['id'] ?? 0);
464 }
465 return 0;
466 }, $bookings);
467
468 // Include bookings linked via user ID or email (older bookings may not have customer_id)
469 $customer = $this->customerRepository->find($customerId);
470 if ($customer) {
471 if (!empty($customer->user_id)) {
472 $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000);
473 $bookingIds = array_merge($bookingIds, array_map(static function($booking) {
474 if (is_object($booking)) {
475 return (int) ($booking->id ?? 0);
476 }
477 if (is_array($booking)) {
478 return (int) ($booking['id'] ?? 0);
479 }
480 return 0;
481 }, $userBookings));
482 }
483
484 if (!empty($customer->email)) {
485 $emailBookings = $this->bookingRepository->findByContactEmail($customer->email, 1000);
486 $bookingIds = array_merge($bookingIds, array_map(static function($booking) {
487 if (is_object($booking)) {
488 return (int) ($booking->id ?? 0);
489 }
490 if (is_array($booking)) {
491 return (int) ($booking['id'] ?? 0);
492 }
493 return 0;
494 }, $emailBookings));
495 }
496 }
497
498 $bookingIds = array_values(array_unique(array_filter($bookingIds)));
499
500 return $this->getPaymentsForBookingIds($bookingIds, $limit);
501 }
502
503 public function getPaymentsByUserId(int $userId, int $limit = 50): array
504 {
505 $bookings = $this->bookingRepository->findByUserId($userId, 1000);
506
507 $user = get_userdata($userId);
508 if ($user && !empty($user->user_email)) {
509 $bookingsByEmail = $this->bookingRepository->findByContactEmail($user->user_email, 1000);
510 $bookings = array_merge($bookings, $bookingsByEmail);
511 }
512
513 $bookingIds = array_map(static function($booking) {
514 if (is_object($booking)) {
515 return (int) ($booking->id ?? 0);
516 }
517 if (is_array($booking)) {
518 return (int) ($booking['id'] ?? 0);
519 }
520 return 0;
521 }, $bookings);
522
523 return $this->getPaymentsForBookingIds($bookingIds, $limit);
524 }
525
526 private function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array
527 {
528 $bookingIds = array_values(array_filter(array_map('intval', $bookingIds))); // ensure ints
529
530 if (empty($bookingIds)) {
531 return [];
532 }
533
534 $customerRepository = new \Yatra\Repositories\CustomerRepository();
535 $payments = $customerRepository->getPaymentsForBookingIds($bookingIds, $limit);
536
537 return array_map(static function($payment) {
538 return [
539 'id' => (int) $payment->id,
540 'booking_id' => (int) $payment->booking_id,
541 'booking_reference' => $payment->booking_reference,
542 'amount' => (float) $payment->amount,
543 'currency' => $payment->currency,
544 'status' => $payment->status,
545 'payment_method' => $payment->payment_method,
546 'gateway' => $payment->gateway,
547 'transaction_id' => $payment->transaction_id,
548 'created_at' => $payment->created_at,
549 'updated_at' => $payment->updated_at,
550 'trip_title' => $payment->trip_title,
551 'booking_amount_due' => (float) $payment->booking_amount_due,
552 'booking_amount_paid' => (float) $payment->booking_amount_paid,
553 'booking_total_amount' => (float) $payment->booking_total_amount,
554 ];
555 }, $payments);
556 }
557
558 public function getDocumentsForBookings(array $bookings, int $customerId = 0): array
559 {
560 $documents = [];
561
562 // Process each booking individually for vouchers and itineraries
563 // but group by trip for downloads
564 $tripsWithBookings = [];
565
566 foreach ($bookings as $booking) {
567 $bookingId = is_object($booking) ? (int) ($booking->id ?? 0) : (int) ($booking['id'] ?? $booking['booking_id'] ?? 0);
568 $tripId = is_object($booking) ? (int) ($booking->trip_id ?? 0) : (int) ($booking['trip_id'] ?? 0);
569 $tripTitle = is_object($booking) ? (string) ($booking->trip_title ?? '') : (string) ($booking['trip_title'] ?? '');
570 $reference = is_object($booking) ? ($booking->reference ?? null) : ($booking['reference'] ?? null);
571 $status = is_object($booking) ? (string) ($booking->status ?? '') : (string) ($booking['status'] ?? '');
572 $createdAt = is_object($booking) ? (string) ($booking->created_at ?? '') : (string) ($booking['created_at'] ?? '');
573
574 if ($bookingId <= 0) {
575 continue;
576 }
577
578 // Store trip info for downloads (grouped by trip)
579 if ($tripId > 0 && !isset($tripsWithBookings[$tripId])) {
580 $tripsWithBookings[$tripId] = [
581 'booking_id' => $bookingId,
582 'trip_title' => $tripTitle,
583 'reference' => $reference,
584 'status' => $status,
585 'created_at' => $createdAt,
586 ];
587 }
588
589 // Get payments for this booking (invoices per payment)
590 $payments = $this->paymentRepository->findByBookingId($bookingId);
591 foreach ($payments as $payment) {
592 $paymentId = (int) ($payment->id ?? 0);
593 if ($paymentId <= 0) {
594 continue;
595 }
596
597 $paymentStatus = (string) ($payment->status ?? '');
598 if (!in_array($paymentStatus, ['paid', 'completed', 'success'], true)) {
599 continue;
600 }
601
602 $docRef = $reference ?: $bookingId;
603
604 // Invoice per payment
605 $invoiceUrl = rest_url('yatra/v1/payment/' . $paymentId . '/invoice');
606 $invoiceUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $invoiceUrl);
607
608 $documents[] = [
609 'id' => 'invoice-payment-' . $paymentId,
610 'name' => sprintf(__('Invoice #%s.pdf', 'yatra'), $docRef),
611 'trip_title' => $tripTitle,
612 'category' => 'invoice',
613 'updated_at' => $payment->created_at ?? $createdAt ?: date('Y-m-d H:i:s'),
614 'url' => $invoiceUrl,
615 'booking_id' => $bookingId,
616 'payment_id' => $paymentId,
617 ];
618 }
619
620 // Voucher per booking
621 if ($status === 'confirmed') {
622 $docRef = $reference ?: $bookingId;
623
624 $voucherUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/voucher');
625 $voucherUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $voucherUrl);
626
627 $documents[] = [
628 'id' => 'voucher-' . $bookingId, // Booking-based ID
629 'name' => sprintf(__('Travel Voucher #%s.pdf', 'yatra'), $docRef),
630 'trip_title' => $tripTitle,
631 'category' => 'voucher',
632 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'),
633 'url' => $voucherUrl,
634 'booking_id' => $bookingId,
635 ];
636
637 // Itinerary per booking
638 $itineraryUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/itinerary');
639 $itineraryUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $itineraryUrl);
640
641 $documents[] = [
642 'id' => 'itinerary-' . $bookingId, // Booking-based ID
643 'name' => sprintf(__('Travel Itinerary #%s.pdf', 'yatra'), $docRef),
644 'trip_title' => $tripTitle,
645 'category' => 'itinerary',
646 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'),
647 'url' => $itineraryUrl,
648 'booking_id' => $bookingId,
649 ];
650 }
651 }
652
653 usort($documents, function ($a, $b) {
654 return strtotime($b['updated_at']) - strtotime($a['updated_at']);
655 });
656
657 // Apply downloads filter (which groups by trip)
658 $documents = apply_filters('yatra_customer_documents', $documents, $bookings, $customerId);
659
660 return is_array($documents) ? $documents : [];
661 }
662
663 /**
664 * Get customer's documents (invoices, vouchers, itineraries)
665 *
666 * @param int $customerId Customer ID
667 * @return array
668 */
669 public function getCustomerDocuments(int $customerId): array
670 {
671 // Get customer's bookings
672 $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000);
673
674 // Also include bookings linked via user_id/email (older bookings may not have customer_id)
675 $customer = $this->customerRepository->find($customerId);
676 if ($customer) {
677 if (!empty($customer->user_id)) {
678 $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000);
679 $bookings = array_merge($bookings, $userBookings);
680 }
681
682 if (!empty($customer->email)) {
683 $emailBookings = $this->bookingRepository->findByContactEmail((string) $customer->email, 1000);
684 $bookings = array_merge($bookings, $emailBookings);
685 }
686 }
687
688 // Deduplicate by booking id
689 $seen = [];
690 $unique = [];
691 foreach ($bookings as $b) {
692 $id = is_object($b) ? ($b->id ?? null) : ($b['id'] ?? $b['booking_id'] ?? null);
693 if ($id && !isset($seen[$id])) {
694 $seen[$id] = true;
695 $unique[] = $b;
696 }
697 }
698
699 return $this->getDocumentsForBookings($unique, $customerId);
700 }
701
702 /**
703 * Get customer's support tickets
704 *
705 * @param int $customerId Customer ID
706 * @return array
707 */
708 public function getCustomerSupportTickets(int $customerId): array
709 {
710 // For now, return empty array as support tickets system may not be implemented yet
711 // This can be extended when support ticket system is added
712 return [];
713 }
714
715 /**
716 * Merge two customer records
717 *
718 * @param int $sourceId Source customer ID (will be deleted)
719 * @param int $targetId Target customer ID (will be kept)
720 * @return array {success: bool, message: string}
721 */
722 public function mergeCustomers(int $sourceId, int $targetId): array
723 {
724 if ($sourceId === $targetId) {
725 return ['success' => false, 'message' => __('Cannot merge customer with itself.', 'yatra')];
726 }
727
728 $source = $this->customerRepository->find($sourceId);
729 $target = $this->customerRepository->find($targetId);
730
731 if (!$source || !$target) {
732 return ['success' => false, 'message' => __('One or both customers not found.', 'yatra')];
733 }
734
735 // Update all bookings to point to target customer
736 $this->bookingRepository->updateCustomerBookings($sourceId, $targetId);
737
738 // Update target customer stats
739 $this->customerRepository->updateCustomer($targetId, [
740 'total_bookings' => (int) $target->total_bookings + (int) $source->total_bookings,
741 'total_spent' => (float) $target->total_spent + (float) $source->total_spent,
742 ]);
743
744 // Delete source customer
745 $this->customerRepository->deleteCustomer($sourceId);
746
747 return [
748 'success' => true,
749 'message' => __('Customers merged successfully.', 'yatra'),
750 ];
751 }
752
753 /**
754 * Format customer for API response
755 *
756 * @param object $customer Raw customer data
757 * @return array
758 */
759 private function formatCustomer(object $customer): array
760 {
761 $name = trim((string) ($customer->first_name ?? '') . ' ' . (string) ($customer->last_name ?? ''));
762 if ($name === '') {
763 $uid = (int) ($customer->user_id ?? 0);
764 if ($uid > 0) {
765 $u = get_userdata($uid);
766 if ($u instanceof \WP_User) {
767 $name = trim((string) $u->display_name);
768 if ($name === '') {
769 $name = trim($u->first_name . ' ' . $u->last_name);
770 }
771 if ($name === '') {
772 $name = (string) $u->user_login;
773 }
774 }
775 }
776 }
777 if ($name === '' && !empty($customer->email)) {
778 $local = explode('@', (string) $customer->email)[0] ?? '';
779 $name = $local !== '' ? $local : $name;
780 }
781
782 $created = $customer->created_at ?? '';
783
784 return [
785 'id' => (int) $customer->id,
786 'user_id' => $customer->user_id ? (int) $customer->user_id : null,
787 'name' => $name,
788 'first_name' => $customer->first_name ?? '',
789 'last_name' => $customer->last_name ?? '',
790 'email' => $customer->email,
791 'phone' => $customer->phone ?? '',
792 'country' => $customer->country ?? '',
793 'city' => $customer->city ?? '',
794 'status' => $customer->status ?? 'active',
795 'total_bookings' => (int) ($customer->total_bookings ?? 0),
796 'total_spent' => (float) ($customer->total_spent ?? 0),
797 'loyalty_tier' => $customer->loyalty_tier ?? 'bronze',
798 'created_at' => $created,
799 'registered_at' => $created,
800 'last_booking_date' => $customer->last_booking_date ?? null,
801 ];
802 }
803
804 /**
805 * Format customer with all details
806 *
807 * @param object $customer Raw customer data
808 * @return array
809 */
810 private function formatCustomerWithDetails(object $customer): array
811 {
812 $formatted = $this->formatCustomer($customer);
813
814 // Add additional fields
815 $formatted['secondary_phone'] = $customer->secondary_phone ?? null;
816 $formatted['address'] = $customer->address ?? null;
817 $formatted['state'] = $customer->state ?? null;
818 $formatted['postal_code'] = $customer->postal_code ?? null;
819 $formatted['date_of_birth'] = $customer->date_of_birth ?? null;
820 $formatted['gender'] = $customer->gender ?? null;
821 $formatted['nationality'] = $customer->nationality ?? null;
822
823 // Emergency contact
824 $formatted['emergency_contact'] = [
825 'name' => $customer->emergency_name ?? null,
826 'phone' => $customer->emergency_phone ?? null,
827 'relationship' => $customer->emergency_relationship ?? null,
828 ];
829
830 // Preferences
831 $formatted['dietary_requirements'] = $customer->dietary_requirements ?? null;
832 $formatted['medical_conditions'] = $customer->medical_conditions ?? null;
833 $formatted['special_needs'] = $customer->special_needs ?? null;
834 $formatted['preferred_language'] = $customer->preferred_language ?? 'en';
835 $formatted['preferred_currency'] = $customer->preferred_currency ?? 'USD';
836
837 // Marketing
838 $formatted['newsletter_optin'] = (bool) ($customer->newsletter_optin ?? false);
839 $formatted['marketing_optin'] = (bool) ($customer->marketing_optin ?? false);
840 $formatted['source'] = $customer->source ?? null;
841
842 // Stats
843 $formatted['total_travelers'] = (int) ($customer->total_travelers ?? 0);
844 $formatted['last_travel_date'] = $customer->last_travel_date ?? null;
845 $formatted['loyalty_points'] = (int) ($customer->loyalty_points ?? 0);
846
847 // Gateway IDs
848 $formatted['stripe_customer_id'] = $customer->stripe_customer_id ?? null;
849 $formatted['paypal_customer_id'] = $customer->paypal_customer_id ?? null;
850 $formatted['razorpay_customer_id'] = $customer->razorpay_customer_id ?? null;
851
852 // Notes
853 $formatted['notes'] = $customer->notes ?? null;
854
855 // Recent bookings
856 $formatted['recent_bookings'] = $customer->recent_bookings ?? [];
857
858 // Timestamps
859 $formatted['updated_at'] = $customer->updated_at ?? null;
860 $formatted['last_login_at'] = $customer->last_login_at ?? null;
861 $formatted['verified_at'] = $customer->verified_at ?? null;
862
863 return $formatted;
864 }
865 }
866
867