PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.1
Yatra – Travel Booking & Tour Operator Software v3.0.14.1
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
yatra / app / Services / EnquiryService.php

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

514 lines 17.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\Services;
6
7 use Yatra\Repositories\EnquiryRepository;
8 use Yatra\Repositories\TripRepository;
9
10 /**
11 * Enquiry Service
12 *
13 * Contains business logic for customer enquiries.
14 *
15 * @package Yatra\Services
16 */
17 class EnquiryService
18 {
19 private EnquiryRepository $enquiryRepository;
20 private TripRepository $tripRepository;
21
22 public function __construct()
23 {
24 $this->enquiryRepository = new EnquiryRepository();
25 $this->tripRepository = new TripRepository();
26 }
27
28 /**
29 * Get paginated enquiries
30 *
31 * @param array $filters Filters
32 * @return array
33 */
34 public function getEnquiries(array $filters = []): array
35 {
36 $result = $this->enquiryRepository->paginate($filters);
37
38 $result['data'] = array_map([$this, 'formatEnquiry'], $result['data']);
39
40 return $result;
41 }
42
43 /**
44 * Get single enquiry
45 *
46 * @param int $id Enquiry ID
47 * @return array|null
48 */
49 public function getEnquiry(int $id): ?array
50 {
51 $enquiry = $this->enquiryRepository->findWithTrip($id);
52
53 if (!$enquiry) {
54 return null;
55 }
56
57 return $this->formatEnquiry($enquiry);
58 }
59
60 /**
61 * Create a new enquiry
62 *
63 * @param array $data Enquiry data
64 * @return array {success: bool, enquiry_id?: int, message: string}
65 */
66 public function createEnquiry(array $data): array
67 {
68 // Normalize common client-side key variants (REST/JS often uses camelCase).
69 if ((!isset($data['trip_id']) || $data['trip_id'] === '' || $data['trip_id'] === null)
70 && isset($data['tripId'])
71 && $data['tripId'] !== ''
72 && $data['tripId'] !== null
73 ) {
74 $data['trip_id'] = $data['tripId'];
75 }
76
77 // If trip_id is missing, attempt to derive it from trip slug or the referring URL.
78 // This makes enquiry emails resilient even if a client drops hidden fields.
79 if (empty($data['trip_id']) || (string) $data['trip_id'] === '0') {
80 $candidateSlug = '';
81 if (!empty($data['trip_slug'])) {
82 $candidateSlug = sanitize_title((string) $data['trip_slug']);
83 } elseif (!empty($data['tripSlug'])) {
84 $candidateSlug = sanitize_title((string) $data['tripSlug']);
85 } else {
86 $ref = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '';
87 if ($ref !== '') {
88 $parts = wp_parse_url($ref);
89 $path = isset($parts['path']) ? trim((string) $parts['path'], '/') : '';
90 if ($path !== '') {
91 $segments = array_values(array_filter(explode('/', $path), static fn ($s) => $s !== ''));
92 $tripBase = trim((string) SettingsService::getTripBase(), '/');
93 if ($tripBase !== '' && !empty($segments)) {
94 $baseIndex = array_search($tripBase, $segments, true);
95 if ($baseIndex !== false && isset($segments[$baseIndex + 1])) {
96 $candidateSlug = sanitize_title((string) $segments[$baseIndex + 1]);
97 }
98 }
99 }
100 }
101 }
102
103 if ($candidateSlug !== '') {
104 try {
105 $trip = $this->tripRepository->findBySlug($candidateSlug);
106 if ($trip && !empty($trip->id)) {
107 $data['trip_id'] = (int) $trip->id;
108 }
109 } catch (\Throwable $e) {
110 // Ignore; will proceed as general enquiry.
111 }
112 }
113 }
114
115 // Validate required fields
116 if (empty($data['name']) || empty($data['email']) || empty($data['message'])) {
117 return ['success' => false, 'message' => __('Name, email, and message are required.', 'yatra')];
118 }
119
120 // Validate email
121 if (!is_email($data['email'])) {
122 return ['success' => false, 'message' => __('Please provide a valid email address.', 'yatra')];
123 }
124
125 // Validate trip if provided
126 if (!empty($data['trip_id'])) {
127 $trip = $this->tripRepository->find((int) $data['trip_id']);
128 if (!$trip) {
129 return ['success' => false, 'message' => __('Trip not found.', 'yatra')];
130 }
131 }
132
133 // Build metadata for traveler classifications (flat array, no wrapper)
134 $classifications = [];
135 if (!empty($data['metadata'])) {
136 $metaValue = $data['metadata'];
137 if (is_string($metaValue)) {
138 $decoded = json_decode($metaValue, true);
139 if (json_last_error() === JSON_ERROR_NONE) {
140 $metaValue = $decoded;
141 }
142 }
143 if (is_array($metaValue)) {
144 // Accept both wrapped {classifications: [...]} and direct array [...]
145 if (isset($metaValue['classifications']) && is_array($metaValue['classifications'])) {
146 $classifications = $metaValue['classifications'];
147 } elseif (array_is_list($metaValue)) {
148 $classifications = $metaValue;
149 }
150 }
151 }
152
153 // Backward compatibility: derive from adults/children if still not provided
154 if (empty($classifications) && (isset($data['adults']) || isset($data['children']))) {
155 $adults = isset($data['adults']) ? (int) $data['adults'] : null;
156 $children = isset($data['children']) ? (int) $data['children'] : null;
157 if ($adults !== null) {
158 $classifications[] = ['id' => 'adult', 'title' => 'Adult', 'count' => $adults];
159 }
160 if ($children !== null) {
161 $classifications[] = ['id' => 'child', 'title' => 'Child', 'count' => $children];
162 }
163 }
164
165 // Compute travelers_count from classifications
166 $travelersCount = null;
167 if (!empty($classifications)) {
168 $travelersCount = array_reduce($classifications, function ($carry, $item) {
169 $count = isset($item['count']) ? (int) $item['count'] : 0;
170 return $carry + $count;
171 }, 0);
172 if ($travelersCount === 0) {
173 $travelersCount = null;
174 }
175 } elseif (isset($data['adults']) || isset($data['children'])) {
176 $travelersCount = ((int) ($data['adults'] ?? 0) + (int) ($data['children'] ?? 0)) ?: null;
177 }
178
179 $data['metadata'] = !empty($classifications) ? wp_json_encode($classifications) : null;
180 $data['travelers_count'] = $travelersCount;
181
182 // Capture IP and user agent if not provided
183 if (empty($data['ip_address'])) {
184 $data['ip_address'] = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
185 }
186 if (empty($data['user_agent'])) {
187 $data['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? null;
188 }
189
190 // Set default status
191 if (empty($data['status'])) {
192 $data['status'] = 'pending';
193 }
194
195 // Create enquiry
196 $enquiryId = $this->enquiryRepository->create($data);
197
198 if (!$enquiryId) {
199 return ['success' => false, 'message' => __('Failed to submit enquiry.', 'yatra')];
200 }
201
202 // Load with trip JOIN so listeners (e.g. Pro Email Automation's onEnquiryCreated
203 // → buildEnquiryVariables) receive trip_title / trip_slug. Without these, Pro
204 // resolves {{trip_name}} to the literal string "General Enquiry".
205 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId)
206 ?: $this->enquiryRepository->find($enquiryId);
207
208 /**
209 * Action: Enquiry created
210 * Fires after a new enquiry is successfully created
211 *
212 * @param object $enquiry The enquiry object (joined with trip when available)
213 * @since 3.0.0
214 */
215 do_action('yatra_enquiry_created', $enquiry);
216
217 // Core plaintext (wp_mail). Pro Email Automation sends HTML when templates exist — see
218 // yatra_send_enquiry_created_* filters in yatra-pro EmailAutomationHooks.
219 if (apply_filters('yatra_send_enquiry_created_admin_email', true, $enquiry)) {
220 $this->sendAdminNotification($enquiryId);
221 }
222
223 if (apply_filters('yatra_send_enquiry_created_customer_email', true, $enquiry)) {
224 $this->sendCustomerConfirmation($enquiryId);
225 }
226
227 return [
228 'success' => true,
229 'enquiry_id' => $enquiryId,
230 'message' => __('Your enquiry has been submitted successfully. We will get back to you soon.', 'yatra'),
231 ];
232 }
233
234 /**
235 * Update an enquiry
236 *
237 * @param int $id Enquiry ID
238 * @param array $data Enquiry data
239 * @return array {success: bool, message: string}
240 */
241 public function updateEnquiry(int $id, array $data): array
242 {
243 $enquiry = $this->enquiryRepository->find($id);
244
245 if (!$enquiry) {
246 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
247 }
248
249 $updated = $this->enquiryRepository->update($id, $data);
250
251 if (!$updated) {
252 return ['success' => false, 'message' => __('Failed to update enquiry.', 'yatra')];
253 }
254
255 return [
256 'success' => true,
257 'message' => __('Enquiry updated successfully.', 'yatra'),
258 ];
259 }
260
261 /**
262 * Respond to an enquiry
263 *
264 * @param int $id Enquiry ID
265 * @param string $response Response message
266 * @return array {success: bool, message: string}
267 */
268 public function respondToEnquiry(int $id, string $response): array
269 {
270 $enquiry = $this->enquiryRepository->findWithTrip($id);
271
272 if (!$enquiry) {
273 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
274 }
275
276 if (empty(trim($response))) {
277 return ['success' => false, 'message' => __('Response message is required.', 'yatra')];
278 }
279
280 $updated = $this->enquiryRepository->addResponse($id, $response, get_current_user_id());
281
282 if (!$updated) {
283 return ['success' => false, 'message' => __('Failed to save response.', 'yatra')];
284 }
285
286 /**
287 * Action: Enquiry responded
288 * Fires after admin responds to an enquiry
289 *
290 * @param object $enquiry The enquiry object
291 * @param string $response The response message
292 * @since 3.0.0
293 */
294 do_action('yatra_enquiry_responded', $enquiry, $response);
295
296 if (apply_filters('yatra_send_enquiry_response_core_email', true, $enquiry, $response)) {
297 $this->sendResponseEmail($enquiry, $response);
298 }
299
300 return [
301 'success' => true,
302 'message' => __('Response sent successfully.', 'yatra'),
303 ];
304 }
305
306 /**
307 * Delete an enquiry
308 *
309 * @param int $id Enquiry ID
310 * @return array {success: bool, message: string}
311 */
312 public function deleteEnquiry(int $id): array
313 {
314 $enquiry = $this->enquiryRepository->find($id);
315
316 if (!$enquiry) {
317 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
318 }
319
320 $deleted = $this->enquiryRepository->delete($id);
321
322 if (!$deleted) {
323 return ['success' => false, 'message' => __('Failed to delete enquiry.', 'yatra')];
324 }
325
326 return [
327 'success' => true,
328 'message' => __('Enquiry deleted successfully.', 'yatra'),
329 ];
330 }
331
332 /**
333 * Bulk update status
334 *
335 * @param array $ids Enquiry IDs
336 * @param string $status New status
337 * @return array {success: bool, affected: int, message: string}
338 */
339 public function bulkUpdateStatus(array $ids, string $status): array
340 {
341 // Allowed statuses for bulk updates. This list is mirrored in the admin UI.
342 // 'completed' marks enquiries that have been fully handled, distinct from
343 // open/in-progress ones.
344 $validStatuses = ['pending', 'read', 'responded', 'completed', 'archived', 'spam', 'trash'];
345
346 if (!in_array($status, $validStatuses, true)) {
347 return ['success' => false, 'affected' => 0, 'message' => __('Invalid status.', 'yatra')];
348 }
349
350 $affected = $this->enquiryRepository->bulkUpdateStatus($ids, $status);
351
352 return [
353 'success' => true,
354 'affected' => $affected,
355 'message' => sprintf(
356 /* translators: %d: number of enquiries updated. */
357 __('%d enquiries updated.', 'yatra'),
358 $affected
359 ),
360 ];
361 }
362
363 /**
364 * Bulk delete enquiries
365 *
366 * @param array $ids Enquiry IDs
367 * @return array {success: bool, affected: int, message: string}
368 */
369 public function bulkDelete(array $ids): array
370 {
371 $affected = $this->enquiryRepository->bulkDelete($ids);
372
373 return [
374 'success' => true,
375 'affected' => $affected,
376 'message' => sprintf(
377 /* translators: %d: number of enquiries deleted. */
378 __('%d enquiries deleted.', 'yatra'),
379 $affected
380 ),
381 ];
382 }
383
384 /**
385 * Get enquiry statistics
386 *
387 * @return array
388 */
389 public function getStats(): array
390 {
391 return $this->enquiryRepository->getStats();
392 }
393
394 /**
395 * Format enquiry for API response
396 *
397 * @param object $enquiry Raw enquiry data
398 * @return array
399 */
400 private function formatEnquiry(object $enquiry): array
401 {
402 $metadata = [];
403 if (!empty($enquiry->metadata)) {
404 $decoded = json_decode($enquiry->metadata, true);
405 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
406 $metadata = $decoded;
407 }
408 }
409
410 $travelersCount = null;
411 if (!empty($metadata['classifications']) && is_array($metadata['classifications'])) {
412 $travelersCount = array_reduce($metadata['classifications'], function ($carry, $item) {
413 $count = isset($item['count']) ? (int) $item['count'] : 0;
414 return $carry + $count;
415 }, 0);
416 if ($travelersCount === 0) {
417 $travelersCount = null;
418 }
419 } elseif (isset($enquiry->travelers_count)) {
420 $travelersCount = $enquiry->travelers_count ? (int) $enquiry->travelers_count : null;
421 } elseif (isset($enquiry->adults) || isset($enquiry->children)) {
422 // Backward compatibility fallback
423 $travelersCount = ((int) ($enquiry->adults ?? 0) + (int) ($enquiry->children ?? 0)) ?: null;
424 }
425
426 return [
427 'id' => (int) $enquiry->id,
428 'trip_id' => $enquiry->trip_id ? (int) $enquiry->trip_id : null,
429 'trip_title' => $enquiry->trip_title ?? null,
430 'trip_slug' => $enquiry->trip_slug ?? null,
431 'name' => $enquiry->name,
432 'email' => $enquiry->email,
433 'phone' => $enquiry->phone,
434 'subject' => $enquiry->subject ?? null,
435 'message' => $enquiry->message,
436 'travel_date' => $enquiry->travel_date ?? null,
437 'travelers_count' => $travelersCount,
438 'metadata' => $metadata ?: null,
439 'status' => $enquiry->status,
440 'response' => $enquiry->response_notes ?? ($enquiry->response ?? null),
441 'response_notes' => $enquiry->response_notes ?? null,
442 'responded_by' => $enquiry->responded_by ? (int) $enquiry->responded_by : null,
443 'responded_at' => $enquiry->responded_at ?? null,
444 'created_at' => $enquiry->created_at,
445 'updated_at' => $enquiry->updated_at,
446 ];
447 }
448
449 /**
450 * Send admin notification for new enquiry
451 *
452 * @param int $enquiryId Enquiry ID
453 */
454 private function sendAdminNotification(int $enquiryId): void
455 {
456 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
457
458 if (!$enquiry) {
459 return;
460 }
461
462 $adminEmail = sanitize_email((string) SettingsService::getString('admin_email', (string) get_option('admin_email', '')));
463 if ($adminEmail === '' || !is_email($adminEmail)) {
464 return;
465 }
466
467 TransactionalEmailTemplateService::sendIfEnabled(
468 TransactionalEmailTemplateService::TYPE_ENQUIRY_ADMIN,
469 $adminEmail,
470 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
471 );
472 }
473
474 /**
475 * Send customer confirmation email
476 *
477 * @param int $enquiryId Enquiry ID
478 */
479 private function sendCustomerConfirmation(int $enquiryId): void
480 {
481 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
482
483 if (!$enquiry || empty($enquiry->email)) {
484 return;
485 }
486
487 TransactionalEmailTemplateService::sendIfEnabled(
488 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RECEIVED,
489 (string) $enquiry->email,
490 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
491 );
492 }
493
494 /**
495 * Send response email to customer
496 *
497 * @param object $enquiry Enquiry data
498 * @param string $response Response message
499 */
500 private function sendResponseEmail(object $enquiry, string $response): void
501 {
502 if (empty($enquiry->email)) {
503 return;
504 }
505
506 TransactionalEmailTemplateService::sendIfEnabled(
507 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RESPONSE,
508 (string) $enquiry->email,
509 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, $response)
510 );
511 }
512 }
513
514