PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
← All changes | app/Services/CustomerService.php +253 -3 3.0.10 → 3.0.16 View file →
@@ -6,8 +6,9 @@
6 6
7 7 use Yatra\Repositories\CustomerRepository;
8 8 use Yatra\Repositories\BookingRepository;
9 9 use Yatra\Repositories\PaymentRepository;
10 +use Yatra\Utils\Logger;
10 11
11 12 /**
12 13 * Customer Service
13 14 *
@@ -450,8 +451,31 @@
450 451 'existing_id' => (int) $existingCustomer->id,
451 452 ];
452 453 }
453 454
455 + // Optional: also give the customer a WordPress login account. This is
456 + // opt-in (the operator ticks "Create a login account"); left off, the
457 + // customer stays a CRM-only record with user_id = NULL exactly as before.
458 + // Resolved before the row is inserted so the customer is stored already
459 + // linked to its user in a single write.
460 + $accountResult = null;
461 + if (!empty($data['create_account'])) {
462 + $accountResult = $this->createOrLinkLoginAccount($data, !empty($data['confirm_link_existing']));
463 + // The email belongs to an existing account — return the confirmation
464 + // request WITHOUT writing anything, so no customer is created until the
465 + // operator agrees to link (or changes the email).
466 + if (!empty($accountResult['needs_link_confirmation'])) {
467 + return $accountResult;
468 + }
469 + if (empty($accountResult['success'])) {
470 + return [
471 + 'success' => false,
472 + 'message' => $accountResult['message'] ?? __('Failed to create the login account.', 'yatra'),
473 + ];
474 + }
475 + $data['user_id'] = (int) $accountResult['user_id'];
476 + }
477 +
454 478 // Create customer
455 479 $customerId = $this->customerRepository->findOrCreate($data);
456 480
457 481 if (!$customerId) {
@@ -457,16 +481,120 @@
457 481 if (!$customerId) {
458 482 return ['success' => false, 'message' => __('Failed to create customer.', 'yatra')];
459 483 }
460 484
485 + // A newly created account may already have guest bookings under the same
486 + // email — link them so they show in My Account (mirrors the user_register
487 + // reconciliation used for self-registrations).
488 + if ($accountResult !== null && !empty($accountResult['user_id'])) {
489 + $this->linkGuestBookingsToUser((int) $accountResult['user_id']);
490 + }
491 +
492 + $message = __('Customer created successfully.', 'yatra');
493 + if ($accountResult !== null) {
494 + $message = !empty($accountResult['linked'])
495 + ? __('Customer created and linked to the existing login account.', 'yatra')
496 + : __('Customer created and a login account was set up. They will receive an email to choose a password.', 'yatra');
497 + }
498 +
461 499 return [
462 500 'success' => true,
463 501 'customer_id' => $customerId,
464 - 'message' => __('Customer created successfully.', 'yatra'),
502 + 'user_id' => $accountResult['user_id'] ?? null,
503 + 'account_created' => $accountResult !== null && empty($accountResult['linked']),
504 + 'account_linked' => $accountResult !== null && !empty($accountResult['linked']),
505 + 'message' => $message,
465 506 ];
466 507 }
467 508
468 509 /**
510 + * Create a WordPress login account for a customer being added in the admin,
511 + * or link an existing account when one already uses that email.
512 + *
513 + * Mirrors the account creation used by self-registration and guest checkout
514 + * (yatra_customer role + billing meta), so an admin-created login behaves
515 + * identically to one the customer made themselves. The password is random and
516 + * never shown; WordPress emails the customer a set-your-password link.
517 + *
518 + * @param array $data Customer data (email required; name/phone optional)
519 + * @return array{success:bool, user_id?:int, linked?:bool, message?:string}
520 + */
521 + private function createOrLinkLoginAccount(array $data, bool $confirmLink = false): array
522 + {
523 + $email = sanitize_email((string) ($data['email'] ?? ''));
524 + if ($email === '' || !is_email($email)) {
525 + return ['success' => false, 'message' => __('A valid email is required to create a login account.', 'yatra')];
526 + }
527 +
528 + $firstName = sanitize_text_field((string) ($data['first_name'] ?? ''));
529 + $lastName = sanitize_text_field((string) ($data['last_name'] ?? ''));
530 + $phone = sanitize_text_field((string) ($data['phone'] ?? ''));
531 +
532 + // Email already has an account. Linking connects this customer — and any
533 + // past bookings made with that email — to a real, possibly unrelated
534 + // account, so it must be confirmed first (guards a mistyped address). Once
535 + // the operator confirms, link rather than create a duplicate.
536 + $existingUser = get_user_by('email', $email);
537 + if ($existingUser instanceof \WP_User) {
538 + if (!$confirmLink) {
539 + return [
540 + 'success' => false,
541 + 'needs_link_confirmation' => true,
542 + 'existing_user_login' => $existingUser->user_login,
543 + 'message' => sprintf(
544 + /* translators: 1: email address, 2: existing account username. */
545 + __('A login account already exists for %1$s (username: %2$s). Linking will connect this customer — and any past bookings made with that email — to that account. Confirm to link, or use a different email.', 'yatra'),
546 + $email,
547 + $existingUser->user_login
548 + ),
549 + ];
550 + }
551 + return ['success' => true, 'user_id' => (int) $existingUser->ID, 'linked' => true];
552 + }
553 +
554 + // Derive a unique username from the email local-part (same as registration).
555 + $baseUsername = sanitize_user(current(explode('@', $email)), true);
556 + if ($baseUsername === '') {
557 + $baseUsername = 'customer';
558 + }
559 + $username = $baseUsername;
560 + $counter = 1;
561 + while (username_exists($username)) {
562 + $username = $baseUsername . $counter;
563 + $counter++;
564 + }
565 +
566 + $userId = wp_insert_user([
567 + 'user_login' => $username,
568 + 'user_email' => $email,
569 + 'user_pass' => wp_generate_password(24, true),
570 + 'first_name' => $firstName,
571 + 'last_name' => $lastName,
572 + 'display_name' => trim($firstName . ' ' . $lastName) !== '' ? trim($firstName . ' ' . $lastName) : $username,
573 + 'role' => 'yatra_customer',
574 + ]);
575 +
576 + if (is_wp_error($userId)) {
577 + return ['success' => false, 'message' => wp_strip_all_tags($userId->get_error_message())];
578 + }
579 +
580 + if ($phone !== '') {
581 + update_user_meta($userId, 'billing_phone', $phone);
582 + update_user_meta($userId, 'phone', $phone);
583 + }
584 +
585 + // Admin-created accounts are trusted (the operator vouches for the email),
586 + // so they are pre-verified — unlike self-registration, which starts at '0'.
587 + update_user_meta($userId, 'yatra_email_verified', '1');
588 +
589 + // WordPress emails the customer a "set your password" link so they choose
590 + // their own password; the random one above is never disclosed.
591 + wp_new_user_notification($userId, null, 'user');
592 +
593 + return ['success' => true, 'user_id' => (int) $userId, 'linked' => false];
594 + }
595 +
596 + /**
469 597 * Update a customer
470 598 *
471 599 * @param int $id Customer ID
472 600 * @param array $data Customer data
@@ -479,16 +607,97 @@
479 607 if (!$customer) {
480 608 return ['success' => false, 'message' => __('Customer not found.', 'yatra')];
481 609 }
482 610
611 + $previousEmail = (string) $customer->email;
612 + $linkedUserId = (int) ($customer->user_id ?? 0);
613 +
483 614 // Check email uniqueness if changing
615 + $emailChanged = false;
616 + $newEmail = '';
484 617 if (!empty($data['email']) && $data['email'] !== $customer->email) {
485 - $existingCustomer = $this->customerRepository->findByEmail($data['email']);
618 + $newEmail = sanitize_email((string) $data['email']);
619 +
620 + if (!is_email($newEmail)) {
621 + return ['success' => false, 'message' => __('Please enter a valid email address.', 'yatra')];
622 + }
623 +
624 + $existingCustomer = $this->customerRepository->findByEmail($newEmail);
486 625 if ($existingCustomer && (int) $existingCustomer->id !== $id) {
487 626 return ['success' => false, 'message' => __('Email is already in use by another customer.', 'yatra')];
488 627 }
628 +
629 + $data['email'] = $newEmail;
630 + $emailChanged = true;
489 631 }
490 632
633 + // Decide up-front whether this customer signs in, so a rejection happens
634 + // BEFORE anything is written.
635 + //
636 + // An account is only recognised when this customer row unambiguously
637 + // represents it — that is, the account currently carries this very same
638 + // address. Several customer rows can legitimately share one user_id (an
639 + // operator booking on behalf of guests while logged in links every row to
640 + // their own account), and acting on the account from one of those rows
641 + // would touch the WRONG person's login — including an administrator's.
642 + $accountUserId = 0;
643 + if ($emailChanged && $linkedUserId > 0) {
644 + $linkedUser = get_userdata($linkedUserId);
645 +
646 + if ($linkedUser && strtolower((string) $linkedUser->user_email) === strtolower($previousEmail)) {
647 + $ownerId = email_exists($data['email']);
648 + if ($ownerId && (int) $ownerId !== $linkedUserId) {
649 + return [
650 + 'success' => false,
651 + 'message' => __('That email address already belongs to another user account.', 'yatra'),
652 + ];
653 + }
654 +
655 + $accountUserId = $linkedUserId;
656 + }
657 + }
658 +
659 + // A customer who can sign in keeps ownership of their own login address:
660 + // the new address must confirm the change before it takes effect, exactly
661 + // as it does when the customer edits it themselves. Nothing is written
662 + // here — the stored email stays put until that link is clicked.
663 + //
664 + // A customer WITHOUT an account has no login and no inbox to confirm
665 + // from, so their record is corrected immediately.
666 + $pendingEmail = '';
667 + if ($accountUserId > 0) {
668 + unset($data['email']);
669 + }
670 +
671 + // Add a login account to a customer that doesn't have one yet, when the
672 + // operator ticked "Create a login account" on the edit form. This ONLY
673 + // ADDS an account — it never removes one: a customer who already signs in
674 + // never reaches here ($linkedUserId > 0), and the form hides the option
675 + // for them, so unchecking is always a no-op. Runs before the write so the
676 + // new user_id is persisted in the same update; the repository only accepts
677 + // user_id when the row has none, a second guard against reassignment.
678 + $accountAdded = false;
679 + if (!empty($data['create_account']) && $linkedUserId <= 0) {
680 + $accountResult = $this->createOrLinkLoginAccount([
681 + 'email' => $data['email'] ?? $customer->email,
682 + 'first_name' => $data['first_name'] ?? $customer->first_name,
683 + 'last_name' => $data['last_name'] ?? $customer->last_name,
684 + 'phone' => $data['phone'] ?? $customer->phone,
685 + ], !empty($data['confirm_link_existing']));
686 + // Existing account for this email → ask before linking; return here so
687 + // the edit is not written until the operator confirms.
688 + if (!empty($accountResult['needs_link_confirmation'])) {
689 + return $accountResult;
690 + }
691 + if (empty($accountResult['success'])) {
692 + return [
693 + 'success' => false,
694 + 'message' => $accountResult['message'] ?? __('Failed to create the login account.', 'yatra'),
695 + ];
696 + }
697 + $accountAdded = true;
698 + }
699 +
491 700 $updated = $this->customerRepository->updateCustomer($id, $data);
492 701
493 702 if (!$updated) {
494 703 return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')];
@@ -493,11 +702,52 @@
493 702 if (!$updated) {
494 703 return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')];
495 704 }
496 705
706 + // Persist the link and reconcile prior guest bookings. Uses the dedicated
707 + // linkUserIfUnlinked (updateCustomer does not write user_id), which only
708 + // sets it when the row has none — so it adds, never reassigns.
709 + if ($accountAdded && !empty($accountResult['user_id'])) {
710 + $newUserId = (int) $accountResult['user_id'];
711 + $this->customerRepository->linkUserIfUnlinked($id, $newUserId);
712 + $this->linkGuestBookingsToUser($newUserId);
713 + }
714 +
715 + if ($accountUserId > 0) {
716 + $requested = $this->requestEmailChange($accountUserId, $newEmail);
717 +
718 + if (empty($requested['success'])) {
719 + Logger::warning('Admin-requested customer email change could not be sent', [
720 + 'customer_id' => $id,
721 + 'user_id' => $accountUserId,
722 + 'reason' => $requested['message'] ?? '',
723 + ]);
724 +
725 + return [
726 + 'success' => false,
727 + 'message' => $requested['message'] ?? __('The email change could not be requested.', 'yatra'),
728 + ];
729 + }
730 +
731 + $pendingEmail = (string) ($requested['pending_email'] ?? $newEmail);
732 +
733 + return [
734 + 'success' => true,
735 + 'message' => sprintf(
736 + /* translators: %s: the new email address awaiting confirmation. */
737 + __('Customer updated. A confirmation link was sent to %s — their email address changes once it is confirmed there.', 'yatra'),
738 + $pendingEmail
739 + ),
740 + 'pending_email' => $pendingEmail,
741 + ];
742 + }
743 +
497 744 return [
498 745 'success' => true,
499 - 'message' => __('Customer updated successfully.', 'yatra'),
746 + 'account_created' => $accountAdded,
747 + 'message' => $accountAdded
748 + ? __('Customer updated and a login account was set up. They will receive an email to choose a password.', 'yatra')
749 + : __('Customer updated successfully.', 'yatra'),
500 750 ];
501 751 }
502 752
503 753 /**