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 +170 -2 3.0.13 → 3.0.16 View file →
@@ -451,8 +451,31 @@
451 451 'existing_id' => (int) $existingCustomer->id,
452 452 ];
453 453 }
454 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 +
455 478 // Create customer
456 479 $customerId = $this->customerRepository->findOrCreate($data);
457 480
458 481 if (!$customerId) {
@@ -458,16 +481,120 @@
458 481 if (!$customerId) {
459 482 return ['success' => false, 'message' => __('Failed to create customer.', 'yatra')];
460 483 }
461 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 +
462 499 return [
463 500 'success' => true,
464 501 'customer_id' => $customerId,
465 - '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,
466 506 ];
467 507 }
468 508
469 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 + /**
470 597 * Update a customer
471 598 *
472 599 * @param int $id Customer ID
473 600 * @param array $data Customer data
@@ -540,8 +667,37 @@
540 667 if ($accountUserId > 0) {
541 668 unset($data['email']);
542 669 }
543 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 +
544 700 $updated = $this->customerRepository->updateCustomer($id, $data);
545 701
546 702 if (!$updated) {
547 703 return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')];
@@ -546,8 +702,17 @@
546 702 if (!$updated) {
547 703 return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')];
548 704 }
549 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 +
550 715 if ($accountUserId > 0) {
551 716 $requested = $this->requestEmailChange($accountUserId, $newEmail);
552 717
553 718 if (empty($requested['success'])) {
@@ -577,9 +742,12 @@
577 742 }
578 743
579 744 return [
580 745 'success' => true,
581 - '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'),
582 750 ];
583 751 }
584 752
585 753 /**