PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.11
Yatra – Travel Booking & Tour Operator Software v3.0.11
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 / Controllers / CustomerController.php

CustomerController.php in Yatra – Travel Booking & Tour Operator Software 3.0.11, at app/Controllers/CustomerController.php

720 lines 24.7 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\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\CustomerService;
11 use Yatra\Validators\CustomerValidator;
12 use Yatra\Exceptions\ValidationException;
13 use Yatra\Utils\Logger;
14
15 /**
16 * Customer REST API Controller
17 *
18 * Handles HTTP requests only - delegates business logic to CustomerService.
19 *
20 * NO DATABASE QUERIES OR BUSINESS LOGIC IN THIS FILE.
21 *
22 * @package Yatra\Controllers
23 */
24 class CustomerController extends BaseController
25 {
26 /**
27 * Customer service instance
28 */
29 private CustomerService $customerService;
30
31 /**
32 * Constructor
33 */
34 public function __construct()
35 {
36 $this->customerService = new CustomerService();
37 }
38
39 /**
40 * Register routes
41 */
42 public function register_routes(): void
43 {
44 $namespace = 'yatra/v1';
45 $base = 'customers';
46
47 // =====================
48 // CUSTOMER (FRONTEND) ROUTES
49 // =====================
50
51 // Get current customer profile
52 register_rest_route($namespace, '/' . $base . '/me', [
53 [
54 'methods' => \WP_REST_Server::READABLE,
55 'callback' => [$this, 'getMe'],
56 'permission_callback' => [$this, 'checkCustomerPermission'],
57 ],
58 [
59 'methods' => \WP_REST_Server::EDITABLE,
60 'callback' => [$this, 'updateMe'],
61 'permission_callback' => [$this, 'checkCustomerPermission'],
62 ],
63 ]);
64
65 // Change current customer's account password
66 register_rest_route($namespace, '/' . $base . '/me/password', [
67 [
68 'methods' => \WP_REST_Server::EDITABLE,
69 'callback' => [$this, 'updateMyPassword'],
70 'permission_callback' => [$this, 'checkCustomerPermission'],
71 ],
72 ]);
73
74 // Request an account email change (WordPress pending-change pattern:
75 // a confirmation link is emailed to the new address; nothing changes yet).
76 // The confirmation link itself is a normal front-end URL handled by
77 // AccountPageHandler — NOT a REST route — because a browser GET to REST
78 // carries no nonce and would be treated as anonymous.
79 register_rest_route($namespace, '/' . $base . '/me/email', [
80 [
81 'methods' => \WP_REST_Server::CREATABLE,
82 'callback' => [$this, 'updateMyEmail'],
83 'permission_callback' => [$this, 'checkCustomerPermission'],
84 ],
85 ]);
86
87 // Re-send the confirmation email for an already-pending email change.
88 register_rest_route($namespace, '/' . $base . '/me/email/resend', [
89 [
90 'methods' => \WP_REST_Server::CREATABLE,
91 'callback' => [$this, 'resendMyEmailConfirmation'],
92 'permission_callback' => [$this, 'checkCustomerPermission'],
93 ],
94 ]);
95
96 // Cancel (dismiss) a pending email change — deletes the stored token.
97 register_rest_route($namespace, '/' . $base . '/me/email', [
98 [
99 'methods' => \WP_REST_Server::DELETABLE,
100 'callback' => [$this, 'cancelMyEmailChange'],
101 'permission_callback' => [$this, 'checkCustomerPermission'],
102 ],
103 ]);
104
105 // Current customer's bookings
106 register_rest_route($namespace, '/' . $base . '/my-bookings', [
107 [
108 'methods' => \WP_REST_Server::READABLE,
109 'callback' => [$this, 'getMyBookings'],
110 'permission_callback' => [$this, 'checkCustomerPermission'],
111 ],
112 ]);
113
114 register_rest_route($namespace, '/' . $base . '/my-bookings/(?P<id>\d+)', [
115 [
116 'methods' => \WP_REST_Server::READABLE,
117 'callback' => [$this, 'getMyBooking'],
118 'permission_callback' => [$this, 'checkCustomerPermission'],
119 'args' => [
120 'id' => [
121 'required' => true,
122 'type' => 'integer',
123 'sanitize_callback' => 'absint',
124 ],
125 ],
126 ],
127 ]);
128
129 // Current customer's payments
130 register_rest_route($namespace, '/' . $base . '/my-payments', [
131 [
132 'methods' => \WP_REST_Server::READABLE,
133 'callback' => [$this, 'getMyPayments'],
134 'permission_callback' => [$this, 'checkCustomerPermission'],
135 ],
136 ]);
137
138 // Current customer's documents
139 register_rest_route($namespace, '/' . $base . '/my-documents', [
140 [
141 'methods' => \WP_REST_Server::READABLE,
142 'callback' => [$this, 'getMyDocuments'],
143 'permission_callback' => [$this, 'checkCustomerPermission'],
144 ],
145 ]);
146
147 // Current customer's support tickets
148 register_rest_route($namespace, '/' . $base . '/my-support-tickets', [
149 [
150 'methods' => \WP_REST_Server::READABLE,
151 'callback' => [$this, 'getMySupportTickets'],
152 'permission_callback' => [$this, 'checkCustomerPermission'],
153 ],
154 ]);
155
156 // =====================
157 // ADMIN ROUTES
158 // =====================
159
160 // List + create customers. List gates on view, create gates
161 // on edit (creating a customer is a write).
162 register_rest_route($namespace, '/' . $base, [
163 [
164 'methods' => \WP_REST_Server::READABLE,
165 'callback' => [$this, 'getCustomers'],
166 'permission_callback' => [$this, 'checkCanView'],
167 ],
168 [
169 'methods' => \WP_REST_Server::CREATABLE,
170 'callback' => [$this, 'createCustomer'],
171 'permission_callback' => [$this, 'checkCanEdit'],
172 ],
173 ]);
174
175 // Single customer — read / update / delete with distinct caps.
176 register_rest_route($namespace, '/' . $base . '/(?P<id>\d+)', [
177 [
178 'methods' => \WP_REST_Server::READABLE,
179 'callback' => [$this, 'getCustomer'],
180 'permission_callback' => [$this, 'checkCanView'],
181 ],
182 [
183 'methods' => \WP_REST_Server::EDITABLE,
184 'callback' => [$this, 'updateCustomer'],
185 'permission_callback' => [$this, 'checkCanEdit'],
186 ],
187 [
188 'methods' => \WP_REST_Server::DELETABLE,
189 'callback' => [$this, 'deleteCustomer'],
190 'permission_callback' => [$this, 'checkCanEdit'],
191 ],
192 ]);
193
194 // Customer bookings list — view cap.
195 register_rest_route($namespace, '/' . $base . '/(?P<id>\d+)/bookings', [
196 [
197 'methods' => \WP_REST_Server::READABLE,
198 'callback' => [$this, 'getCustomerBookings'],
199 'permission_callback' => [$this, 'checkCanView'],
200 ],
201 ]);
202
203 // Merge customers — destructive write. Edit cap.
204 register_rest_route($namespace, '/' . $base . '/merge', [
205 [
206 'methods' => \WP_REST_Server::CREATABLE,
207 'callback' => [$this, 'mergeCustomers'],
208 'permission_callback' => [$this, 'checkCanEdit'],
209 ],
210 ]);
211
212 // Customer statistics — view cap.
213 register_rest_route($namespace, '/' . $base . '/stats', [
214 [
215 'methods' => \WP_REST_Server::READABLE,
216 'callback' => [$this, 'getCustomerStats'],
217 'permission_callback' => [$this, 'checkCanView'],
218 ],
219 ]);
220 }
221
222 /**
223 * Check if user is logged in (for /me, /my-bookings etc. — these
224 * are customer-facing endpoints that work for any logged-in WP
225 * user, not just team members).
226 */
227 public function checkCustomerPermission(): bool
228 {
229 return is_user_logged_in();
230 }
231
232 /**
233 * Granular admin-side permission checks. WP administrators pass
234 * every cap via the Team module's admin-fallback filter, so an
235 * explicit `manage_options` check isn't needed here.
236 */
237 public function checkCanView(): bool
238 {
239 return current_user_can('yatra_view_customers');
240 }
241
242 public function checkCanEdit(): bool
243 {
244 return current_user_can('yatra_edit_customers');
245 }
246
247 /**
248 * @deprecated Kept for any external code referencing the old
249 * method name. The previous implementation OR-ed
250 * `yatra_manage_customers` which was never registered anywhere
251 * — that arm has been removed because it was always false in
252 * practice. Routes to view-only — admin users still pass via
253 * the admin-fallback layer.
254 */
255 public function checkAdminPermission(): bool
256 {
257 return $this->checkCanView();
258 }
259
260 // =========================================================================
261 // FRONTEND ENDPOINTS (Current User)
262 // =========================================================================
263
264 /**
265 * GET /customers/me - Get current customer profile
266 */
267 public function getMe(WP_REST_Request $request): WP_REST_Response
268 {
269 $userId = get_current_user_id();
270
271 $profile = $this->customerService->getAccountProfileForUser($userId);
272
273 if ($profile === null) {
274 return new WP_REST_Response([
275 'success' => false,
276 'message' => __('Customer profile not found.', 'yatra'),
277 ], 404);
278 }
279
280 return new WP_REST_Response([
281 'success' => true,
282 'data' => $profile,
283 ]);
284 }
285
286 /**
287 * PUT /customers/me - Update current customer profile
288 */
289 public function updateMe(WP_REST_Request $request)
290 {
291 try {
292 $userId = get_current_user_id();
293 $data = $request->get_json_params();
294
295 // Email is the account login and is intentionally NOT editable from
296 // the account profile. Strip it server-side so it can never be
297 // changed via this endpoint, regardless of what the client sends.
298 if (is_array($data)) {
299 unset($data['email'], $data['user_email']);
300 }
301
302 Logger::apiRequest('/customers/me', 'PUT', $data);
303
304 $customer = $this->customerService->getCustomerByUserId($userId);
305
306 if (!$customer) {
307 // No customer record yet (e.g. registered but never booked).
308 // Create one linked to this user so the profile persists instead
309 // of failing. Email stays the account login (never client-set).
310 $wpUser = get_user_by('id', $userId);
311 if (!$wpUser) {
312 return $this->not_found(__('Customer profile not found', 'yatra'));
313 }
314
315 $createData = CustomerValidator::sanitize($data);
316 $createData['user_id'] = $userId;
317 $createData['email'] = $wpUser->user_email;
318 if (empty($createData['first_name'])) {
319 $createData['first_name'] = $wpUser->first_name !== ''
320 ? $wpUser->first_name
321 : ($wpUser->display_name !== '' ? $wpUser->display_name : $wpUser->user_login);
322 }
323 if (empty($createData['last_name'])) {
324 $createData['last_name'] = (string) $wpUser->last_name;
325 }
326
327 $createResult = $this->customerService->createCustomer($createData);
328 if (empty($createResult['success'])) {
329 Logger::warning('Could not create customer profile on update', ['user_id' => $userId, 'result' => $createResult]);
330 return $this->error_response($createResult['message'] ?? __('Failed to save profile.', 'yatra'), 400);
331 }
332
333 return $this->success_response($this->customerService->getAccountProfileForUser($userId));
334 }
335
336 $customerId = (int) $customer['id'];
337
338 // Validate and sanitize input data
339 CustomerValidator::validateUpdate($data, $customerId);
340 $data = CustomerValidator::sanitize($data);
341
342 $result = $this->customerService->updateCustomer($customerId, $data);
343
344 if (!$result['success']) {
345 Logger::warning("Customer profile update failed", ['customer_id' => $customerId, 'user_id' => $userId, 'result' => $result]);
346 return $this->error_response($result['message'] ?? 'Failed to update customer profile', 400);
347 }
348
349 Logger::info("Customer profile updated successfully", ['customer_id' => $customerId, 'user_id' => $userId]);
350
351 // updateCustomer() returns success/message only — return the fresh
352 // profile so the response carries the updated values (and avoids an
353 // "undefined key data" warning).
354 return $this->success_response(
355 $this->customerService->getAccountProfileForUser($userId)
356 );
357
358 } catch (\Exception $e) {
359 Logger::error("Failed to update customer profile", ['user_id' => $userId ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
360 return $this->handle_exception($e);
361 }
362 }
363
364 /**
365 * PUT /customers/me/password - Change the current customer's account password.
366 *
367 * Requires the correct current password, then sets the new one and refreshes
368 * the auth cookie so the customer stays logged in (wp_set_password otherwise
369 * invalidates the current session).
370 */
371 public function updateMyPassword(WP_REST_Request $request)
372 {
373 $userId = get_current_user_id();
374 if ($userId <= 0) {
375 return $this->error_response(__('Authentication required.', 'yatra'), 401);
376 }
377
378 $data = $request->get_json_params();
379 $current = isset($data['current_password']) ? (string) $data['current_password'] : '';
380 $newPassword = isset($data['new_password']) ? (string) $data['new_password'] : '';
381
382 if ($current === '' || $newPassword === '') {
383 return $this->error_response(__('Current and new password are required.', 'yatra'), 400);
384 }
385
386 $user = get_user_by('id', $userId);
387 if (!$user || !wp_check_password($current, $user->user_pass, $userId)) {
388 return $this->error_response(__('Your current password is incorrect.', 'yatra'), 400);
389 }
390
391 wp_set_password($newPassword, $userId);
392
393 // wp_set_password() invalidates the session token / logs the user out.
394 // Re-establish the current session so the account page stays authenticated.
395 wp_set_current_user($userId);
396 wp_set_auth_cookie($userId, true);
397
398 Logger::info('Customer changed account password', ['user_id' => $userId]);
399
400 return $this->success_response(['updated' => true]);
401 }
402
403 /**
404 * POST /customers/me/email - Request a change to the account login email.
405 *
406 * Follows WordPress core's pending-change pattern: the email is NOT changed
407 * here. A confirmation link is emailed to the NEW address; the change only
408 * applies once the customer clicks it (confirmed by AccountPageHandler).
409 */
410 public function updateMyEmail(WP_REST_Request $request)
411 {
412 $userId = get_current_user_id();
413 if ($userId <= 0) {
414 return $this->error_response(__('Authentication required.', 'yatra'), 401);
415 }
416
417 $data = $request->get_json_params();
418 $newEmail = '';
419 if (is_array($data)) {
420 $newEmail = (string) ($data['email'] ?? $data['new_email'] ?? '');
421 }
422
423 $result = $this->customerService->requestEmailChange($userId, $newEmail);
424
425 return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
426 }
427
428 /**
429 * POST /customers/me/email/resend - Re-send the confirmation link for a
430 * pending email change (reuses the existing token; nothing else changes).
431 */
432 public function resendMyEmailConfirmation(WP_REST_Request $request)
433 {
434 $userId = get_current_user_id();
435 if ($userId <= 0) {
436 return $this->error_response(__('Authentication required.', 'yatra'), 401);
437 }
438
439 $result = $this->customerService->resendEmailChangeConfirmation($userId);
440
441 return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
442 }
443
444 /**
445 * DELETE /customers/me/email - Cancel (dismiss) a pending email change,
446 * discarding the stored token so the emailed link stops working.
447 */
448 public function cancelMyEmailChange(WP_REST_Request $request)
449 {
450 $userId = get_current_user_id();
451 if ($userId <= 0) {
452 return $this->error_response(__('Authentication required.', 'yatra'), 401);
453 }
454
455 $result = $this->customerService->cancelEmailChange($userId);
456
457 return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
458 }
459
460 /**
461 * GET /customers/my-bookings - Get current customer's bookings
462 */
463 public function getMyBookings(WP_REST_Request $request): WP_REST_Response
464 {
465 $userId = get_current_user_id();
466
467 // Get bookings by user ID (checks both customer_id and user_id)
468 $bookings = $this->customerService->getBookingsByUserId($userId);
469
470 return new WP_REST_Response([
471 'success' => true,
472 'data' => $bookings,
473 ]);
474 }
475
476 public function getMyBooking(WP_REST_Request $request): WP_REST_Response
477 {
478 $userId = get_current_user_id();
479 $bookingId = (int) $request->get_param('id');
480
481 $booking = $this->customerService->getBookingDetailsForUser($userId, $bookingId);
482
483 if (!$booking) {
484 return new WP_REST_Response([
485 'success' => false,
486 'message' => __('Booking not found.', 'yatra'),
487 ], 404);
488 }
489
490 return new WP_REST_Response([
491 'success' => true,
492 'data' => $booking,
493 ]);
494 }
495
496 /**
497 * GET /customers/my-payments - Get current customer's payments
498 */
499 public function getMyPayments(WP_REST_Request $request): WP_REST_Response
500 {
501 $userId = get_current_user_id();
502
503 $customer = $this->customerService->getCustomerByUserId($userId);
504
505 if ($customer) {
506 $payments = $this->customerService->getCustomerPayments((int) $customer['id']);
507 } else {
508 $payments = $this->customerService->getPaymentsByUserId($userId);
509 }
510
511 return new WP_REST_Response([
512 'success' => true,
513 'data' => $payments,
514 ]);
515 }
516
517 /**
518 * GET /customers/my-documents - Get current customer's documents
519 */
520 public function getMyDocuments(WP_REST_Request $request): WP_REST_Response
521 {
522 $userId = get_current_user_id();
523
524 $customer = $this->customerService->getCustomerByUserId($userId);
525
526 if (!$customer) {
527 $bookings = $this->customerService->getBookingsByUserId($userId, 1000);
528 $documents = $this->customerService->getDocumentsForBookings($bookings, 0);
529
530 return new WP_REST_Response([
531 'success' => true,
532 'data' => is_array($documents) ? $documents : [],
533 ]);
534 }
535
536 $documents = $this->customerService->getCustomerDocuments((int) $customer['id']);
537
538 return new WP_REST_Response([
539 'success' => true,
540 'data' => $documents,
541 ]);
542 }
543
544 /**
545 * GET /customers/my-support-tickets - Get current customer's support tickets
546 */
547 public function getMySupportTickets(WP_REST_Request $request): WP_REST_Response
548 {
549 $userId = get_current_user_id();
550
551 $customer = $this->customerService->getCustomerByUserId($userId);
552
553 if (!$customer) {
554 return new WP_REST_Response([
555 'success' => true,
556 'data' => [],
557 ]);
558 }
559
560 $tickets = $this->customerService->getCustomerSupportTickets((int) $customer['id']);
561
562 return new WP_REST_Response([
563 'success' => true,
564 'data' => $tickets,
565 ]);
566 }
567
568 // =========================================================================
569 // ADMIN ENDPOINTS
570 // =========================================================================
571
572 /**
573 * GET /customers - List all customers
574 */
575 public function getCustomers(WP_REST_Request $request): WP_REST_Response
576 {
577 $filters = [
578 'page' => (int) ($request->get_param('page') ?: 1),
579 'per_page' => (int) ($request->get_param('per_page') ?: 20),
580 'status' => $request->get_param('status') ?: '',
581 'search' => $request->get_param('search') ?: '',
582 'orderby' => $request->get_param('orderby') ?: 'created_at',
583 'order' => $request->get_param('order') ?: 'desc',
584 ];
585
586 $result = $this->customerService->getCustomers($filters);
587
588 return new WP_REST_Response([
589 'success' => true,
590 'data' => $result['data'],
591 'total' => $result['total'],
592 'page' => $result['page'],
593 'per_page' => $result['per_page'],
594 'pages' => $result['pages'] ?? $result['total_pages'] ?? 1,
595 ]);
596 }
597
598 /**
599 * GET /customers/{id} - Get single customer
600 */
601 public function getCustomer(WP_REST_Request $request): WP_REST_Response
602 {
603 $id = (int) $request->get_param('id');
604
605 $customer = $this->customerService->getCustomer($id);
606
607 if (!$customer) {
608 return new WP_REST_Response([
609 'success' => false,
610 'message' => __('Customer not found.', 'yatra'),
611 ], 404);
612 }
613
614 // Return both legacy flat fields and wrapped data for UI compatibility
615 return new WP_REST_Response(array_merge([
616 'success' => true,
617 'data' => $customer,
618 ], is_array($customer) ? $customer : []));
619 }
620
621 /**
622 * POST /customers - Create customer
623 */
624 public function createCustomer(WP_REST_Request $request): WP_REST_Response
625 {
626 $data = $request->get_json_params();
627
628 $result = $this->customerService->createCustomer($data);
629
630 if (!$result['success']) {
631 return new WP_REST_Response($result, 400);
632 }
633
634 return new WP_REST_Response($result, 201);
635 }
636
637 /**
638 * PUT /customers/{id} - Update customer
639 */
640 public function updateCustomer(WP_REST_Request $request): WP_REST_Response
641 {
642 $id = (int) $request->get_param('id');
643 $data = $request->get_json_params();
644
645 $result = $this->customerService->updateCustomer($id, $data);
646
647 if (!$result['success']) {
648 return new WP_REST_Response($result, 400);
649 }
650
651 return new WP_REST_Response($result);
652 }
653
654 /**
655 * DELETE /customers/{id} - Delete customer
656 */
657 public function deleteCustomer(WP_REST_Request $request): WP_REST_Response
658 {
659 $id = (int) $request->get_param('id');
660
661 $result = $this->customerService->deleteCustomer($id);
662
663 if (!$result['success']) {
664 return new WP_REST_Response($result, 400);
665 }
666
667 return new WP_REST_Response($result);
668 }
669
670 /**
671 * GET /customers/{id}/bookings - Get customer's bookings (admin)
672 */
673 public function getCustomerBookings(WP_REST_Request $request): WP_REST_Response
674 {
675 $id = (int) $request->get_param('id');
676 $limit = (int) ($request->get_param('limit') ?: 10);
677
678 $bookings = $this->customerService->getCustomerBookings($id, $limit);
679
680 return new WP_REST_Response([
681 'success' => true,
682 'data' => $bookings,
683 ]);
684 }
685
686 /**
687 * POST /customers/merge - Merge two customers
688 */
689 public function mergeCustomers(WP_REST_Request $request): WP_REST_Response
690 {
691 $data = $request->get_json_params();
692 $sourceId = (int) ($data['source_id'] ?? 0);
693 $targetId = (int) ($data['target_id'] ?? 0);
694
695 if (!$sourceId || !$targetId) {
696 return new WP_REST_Response([
697 'success' => false,
698 'message' => __('Both source and target customer IDs are required.', 'yatra'),
699 ], 400);
700 }
701
702 $result = $this->customerService->mergeCustomers($sourceId, $targetId);
703
704 if (!$result['success']) {
705 return new WP_REST_Response($result, 400);
706 }
707
708 return new WP_REST_Response($result);
709 }
710
711 /**
712 * GET /customers/stats - Get customer statistics
713 */
714 public function getCustomerStats(WP_REST_Request $request): WP_REST_Response
715 {
716 $stats = $this->customerService->getStats();
717 return new WP_REST_Response($stats ?? []);
718 }
719 }
720