PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 / Services / EnquiryService.php

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

526 lines 18.4 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. The repository throws when the INSERT is rejected;
196 // this endpoint is public, so turn that into the same friendly failure
197 // every other rejection returns instead of a 500 that echoes the raw
198 // database error back to the visitor.
199 try {
200 $enquiryId = $this->enquiryRepository->create($data);
201 } catch (\Throwable $e) {
202 error_log('Yatra: failed to create enquiry - ' . $e->getMessage());
203
204 return ['success' => false, 'message' => __('Failed to submit enquiry.', 'yatra')];
205 }
206
207 if (!$enquiryId) {
208 return ['success' => false, 'message' => __('Failed to submit enquiry.', 'yatra')];
209 }
210
211 // Load with trip JOIN so listeners (e.g. Pro Email Automation's onEnquiryCreated
212 // → buildEnquiryVariables) receive trip_title / trip_slug. Without these, Pro
213 // resolves {{trip_name}} to the literal string "General Enquiry".
214 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId)
215 ?: $this->enquiryRepository->find($enquiryId);
216
217 /**
218 * Action: Enquiry created
219 * Fires after a new enquiry is successfully created
220 *
221 * @param object $enquiry The enquiry object (joined with trip when available)
222 * @since 3.0.0
223 */
224 do_action('yatra_enquiry_created', $enquiry);
225
226 // Core plaintext (wp_mail). Pro Email Automation sends HTML when templates exist — see
227 // yatra_send_enquiry_created_* filters in yatra-pro EmailAutomationHooks.
228 if (apply_filters('yatra_send_enquiry_created_admin_email', true, $enquiry)) {
229 $this->sendAdminNotification($enquiryId);
230 }
231
232 if (apply_filters('yatra_send_enquiry_created_customer_email', true, $enquiry)) {
233 $this->sendCustomerConfirmation($enquiryId);
234 }
235
236 return [
237 'success' => true,
238 'enquiry_id' => $enquiryId,
239 'message' => __('Your enquiry has been submitted successfully. We will get back to you soon.', 'yatra'),
240 ];
241 }
242
243 /**
244 * Update an enquiry
245 *
246 * @param int $id Enquiry ID
247 * @param array $data Enquiry data
248 * @return array {success: bool, message: string}
249 */
250 public function updateEnquiry(int $id, array $data): array
251 {
252 $enquiry = $this->enquiryRepository->find($id);
253
254 if (!$enquiry) {
255 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
256 }
257
258 $updated = $this->enquiryRepository->update($id, $data);
259
260 if (!$updated) {
261 return ['success' => false, 'message' => __('Failed to update enquiry.', 'yatra')];
262 }
263
264 return [
265 'success' => true,
266 'message' => __('Enquiry updated successfully.', 'yatra'),
267 ];
268 }
269
270 /**
271 * Respond to an enquiry
272 *
273 * @param int $id Enquiry ID
274 * @param string $response Response message
275 * @return array {success: bool, message: string}
276 */
277 public function respondToEnquiry(int $id, string $response): array
278 {
279 $enquiry = $this->enquiryRepository->findWithTrip($id);
280
281 if (!$enquiry) {
282 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
283 }
284
285 if (empty(trim($response))) {
286 return ['success' => false, 'message' => __('Response message is required.', 'yatra')];
287 }
288
289 $updated = $this->enquiryRepository->addResponse($id, $response, get_current_user_id());
290
291 if (!$updated) {
292 return ['success' => false, 'message' => __('Failed to save response.', 'yatra')];
293 }
294
295 /**
296 * Action: Enquiry responded
297 * Fires after admin responds to an enquiry
298 *
299 * @param object $enquiry The enquiry object
300 * @param string $response The response message
301 * @since 3.0.0
302 */
303 do_action('yatra_enquiry_responded', $enquiry, $response);
304
305 if (apply_filters('yatra_send_enquiry_response_core_email', true, $enquiry, $response)) {
306 $this->sendResponseEmail($enquiry, $response);
307 }
308
309 return [
310 'success' => true,
311 'message' => __('Response sent successfully.', 'yatra'),
312 ];
313 }
314
315 /**
316 * Delete an enquiry
317 *
318 * @param int $id Enquiry ID
319 * @return array {success: bool, message: string}
320 */
321 public function deleteEnquiry(int $id): array
322 {
323 $enquiry = $this->enquiryRepository->find($id);
324
325 if (!$enquiry) {
326 return ['success' => false, 'message' => __('Enquiry not found.', 'yatra')];
327 }
328
329 $deleted = $this->enquiryRepository->delete($id);
330
331 if (!$deleted) {
332 return ['success' => false, 'message' => __('Failed to delete enquiry.', 'yatra')];
333 }
334
335 return [
336 'success' => true,
337 'message' => __('Enquiry deleted successfully.', 'yatra'),
338 ];
339 }
340
341 /**
342 * Bulk update status
343 *
344 * @param array $ids Enquiry IDs
345 * @param string $status New status
346 * @return array {success: bool, affected: int, message: string}
347 */
348 public function bulkUpdateStatus(array $ids, string $status): array
349 {
350 // Allowed statuses for bulk updates. This list is mirrored in the admin UI.
351 // 'completed' marks enquiries that have been fully handled, distinct from
352 // open/in-progress ones. 'closed' is the "no further action" end state the
353 // enquiry edit screen has always offered; it is accepted here too so the
354 // list's quick status actions and bulk actions can set it without the
355 // operator having to open each enquiry.
356 $validStatuses = ['pending', 'read', 'responded', 'completed', 'closed', 'archived', 'spam', 'trash'];
357
358 if (!in_array($status, $validStatuses, true)) {
359 return ['success' => false, 'affected' => 0, 'message' => __('Invalid status.', 'yatra')];
360 }
361
362 $affected = $this->enquiryRepository->bulkUpdateStatus($ids, $status);
363
364 return [
365 'success' => true,
366 'affected' => $affected,
367 'message' => sprintf(
368 /* translators: %d: number of enquiries updated. */
369 __('%d enquiries updated.', 'yatra'),
370 $affected
371 ),
372 ];
373 }
374
375 /**
376 * Bulk delete enquiries
377 *
378 * @param array $ids Enquiry IDs
379 * @return array {success: bool, affected: int, message: string}
380 */
381 public function bulkDelete(array $ids): array
382 {
383 $affected = $this->enquiryRepository->bulkDelete($ids);
384
385 return [
386 'success' => true,
387 'affected' => $affected,
388 'message' => sprintf(
389 /* translators: %d: number of enquiries deleted. */
390 __('%d enquiries deleted.', 'yatra'),
391 $affected
392 ),
393 ];
394 }
395
396 /**
397 * Get enquiry statistics
398 *
399 * @return array
400 */
401 public function getStats(): array
402 {
403 return $this->enquiryRepository->getStats();
404 }
405
406 /**
407 * Format enquiry for API response
408 *
409 * @param object $enquiry Raw enquiry data
410 * @return array
411 */
412 private function formatEnquiry(object $enquiry): array
413 {
414 $metadata = [];
415 if (!empty($enquiry->metadata)) {
416 $decoded = json_decode($enquiry->metadata, true);
417 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
418 $metadata = $decoded;
419 }
420 }
421
422 $travelersCount = null;
423 if (!empty($metadata['classifications']) && is_array($metadata['classifications'])) {
424 $travelersCount = array_reduce($metadata['classifications'], function ($carry, $item) {
425 $count = isset($item['count']) ? (int) $item['count'] : 0;
426 return $carry + $count;
427 }, 0);
428 if ($travelersCount === 0) {
429 $travelersCount = null;
430 }
431 } elseif (isset($enquiry->travelers_count)) {
432 $travelersCount = $enquiry->travelers_count ? (int) $enquiry->travelers_count : null;
433 } elseif (isset($enquiry->adults) || isset($enquiry->children)) {
434 // Backward compatibility fallback
435 $travelersCount = ((int) ($enquiry->adults ?? 0) + (int) ($enquiry->children ?? 0)) ?: null;
436 }
437
438 return [
439 'id' => (int) $enquiry->id,
440 'trip_id' => $enquiry->trip_id ? (int) $enquiry->trip_id : null,
441 'trip_title' => $enquiry->trip_title ?? null,
442 'trip_slug' => $enquiry->trip_slug ?? null,
443 'name' => $enquiry->name,
444 'email' => $enquiry->email,
445 'phone' => $enquiry->phone,
446 'subject' => $enquiry->subject ?? null,
447 'message' => $enquiry->message,
448 'travel_date' => $enquiry->travel_date ?? null,
449 'travelers_count' => $travelersCount,
450 'metadata' => $metadata ?: null,
451 'status' => $enquiry->status,
452 'response' => $enquiry->response_notes ?? ($enquiry->response ?? null),
453 'response_notes' => $enquiry->response_notes ?? null,
454 'responded_by' => $enquiry->responded_by ? (int) $enquiry->responded_by : null,
455 'responded_at' => $enquiry->responded_at ?? null,
456 'created_at' => $enquiry->created_at,
457 'updated_at' => $enquiry->updated_at,
458 ];
459 }
460
461 /**
462 * Send admin notification for new enquiry
463 *
464 * @param int $enquiryId Enquiry ID
465 */
466 private function sendAdminNotification(int $enquiryId): void
467 {
468 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
469
470 if (!$enquiry) {
471 return;
472 }
473
474 $adminEmail = sanitize_email((string) SettingsService::getString('admin_email', (string) get_option('admin_email', '')));
475 if ($adminEmail === '' || !is_email($adminEmail)) {
476 return;
477 }
478
479 TransactionalEmailTemplateService::sendIfEnabled(
480 TransactionalEmailTemplateService::TYPE_ENQUIRY_ADMIN,
481 $adminEmail,
482 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
483 );
484 }
485
486 /**
487 * Send customer confirmation email
488 *
489 * @param int $enquiryId Enquiry ID
490 */
491 private function sendCustomerConfirmation(int $enquiryId): void
492 {
493 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
494
495 if (!$enquiry || empty($enquiry->email)) {
496 return;
497 }
498
499 TransactionalEmailTemplateService::sendIfEnabled(
500 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RECEIVED,
501 (string) $enquiry->email,
502 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
503 );
504 }
505
506 /**
507 * Send response email to customer
508 *
509 * @param object $enquiry Enquiry data
510 * @param string $response Response message
511 */
512 private function sendResponseEmail(object $enquiry, string $response): void
513 {
514 if (empty($enquiry->email)) {
515 return;
516 }
517
518 TransactionalEmailTemplateService::sendIfEnabled(
519 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RESPONSE,
520 (string) $enquiry->email,
521 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, $response)
522 );
523 }
524 }
525
526