PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
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 2.0.10 All 83 releases
yatra / app / Services / EnquiryService.php

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

512 lines 17.5 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 $validStatuses = ['pending', 'read', 'responded', 'archived', 'spam', 'trash'];
343
344 if (!in_array($status, $validStatuses, true)) {
345 return ['success' => false, 'affected' => 0, 'message' => __('Invalid status.', 'yatra')];
346 }
347
348 $affected = $this->enquiryRepository->bulkUpdateStatus($ids, $status);
349
350 return [
351 'success' => true,
352 'affected' => $affected,
353 'message' => sprintf(
354 /* translators: %d: number of enquiries updated. */
355 __('%d enquiries updated.', 'yatra'),
356 $affected
357 ),
358 ];
359 }
360
361 /**
362 * Bulk delete enquiries
363 *
364 * @param array $ids Enquiry IDs
365 * @return array {success: bool, affected: int, message: string}
366 */
367 public function bulkDelete(array $ids): array
368 {
369 $affected = $this->enquiryRepository->bulkDelete($ids);
370
371 return [
372 'success' => true,
373 'affected' => $affected,
374 'message' => sprintf(
375 /* translators: %d: number of enquiries deleted. */
376 __('%d enquiries deleted.', 'yatra'),
377 $affected
378 ),
379 ];
380 }
381
382 /**
383 * Get enquiry statistics
384 *
385 * @return array
386 */
387 public function getStats(): array
388 {
389 return $this->enquiryRepository->getStats();
390 }
391
392 /**
393 * Format enquiry for API response
394 *
395 * @param object $enquiry Raw enquiry data
396 * @return array
397 */
398 private function formatEnquiry(object $enquiry): array
399 {
400 $metadata = [];
401 if (!empty($enquiry->metadata)) {
402 $decoded = json_decode($enquiry->metadata, true);
403 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
404 $metadata = $decoded;
405 }
406 }
407
408 $travelersCount = null;
409 if (!empty($metadata['classifications']) && is_array($metadata['classifications'])) {
410 $travelersCount = array_reduce($metadata['classifications'], function ($carry, $item) {
411 $count = isset($item['count']) ? (int) $item['count'] : 0;
412 return $carry + $count;
413 }, 0);
414 if ($travelersCount === 0) {
415 $travelersCount = null;
416 }
417 } elseif (isset($enquiry->travelers_count)) {
418 $travelersCount = $enquiry->travelers_count ? (int) $enquiry->travelers_count : null;
419 } elseif (isset($enquiry->adults) || isset($enquiry->children)) {
420 // Backward compatibility fallback
421 $travelersCount = ((int) ($enquiry->adults ?? 0) + (int) ($enquiry->children ?? 0)) ?: null;
422 }
423
424 return [
425 'id' => (int) $enquiry->id,
426 'trip_id' => $enquiry->trip_id ? (int) $enquiry->trip_id : null,
427 'trip_title' => $enquiry->trip_title ?? null,
428 'trip_slug' => $enquiry->trip_slug ?? null,
429 'name' => $enquiry->name,
430 'email' => $enquiry->email,
431 'phone' => $enquiry->phone,
432 'subject' => $enquiry->subject ?? null,
433 'message' => $enquiry->message,
434 'travel_date' => $enquiry->travel_date ?? null,
435 'travelers_count' => $travelersCount,
436 'metadata' => $metadata ?: null,
437 'status' => $enquiry->status,
438 'response' => $enquiry->response_notes ?? ($enquiry->response ?? null),
439 'response_notes' => $enquiry->response_notes ?? null,
440 'responded_by' => $enquiry->responded_by ? (int) $enquiry->responded_by : null,
441 'responded_at' => $enquiry->responded_at ?? null,
442 'created_at' => $enquiry->created_at,
443 'updated_at' => $enquiry->updated_at,
444 ];
445 }
446
447 /**
448 * Send admin notification for new enquiry
449 *
450 * @param int $enquiryId Enquiry ID
451 */
452 private function sendAdminNotification(int $enquiryId): void
453 {
454 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
455
456 if (!$enquiry) {
457 return;
458 }
459
460 $adminEmail = sanitize_email((string) SettingsService::getString('admin_email', (string) get_option('admin_email', '')));
461 if ($adminEmail === '' || !is_email($adminEmail)) {
462 return;
463 }
464
465 TransactionalEmailTemplateService::sendIfEnabled(
466 TransactionalEmailTemplateService::TYPE_ENQUIRY_ADMIN,
467 $adminEmail,
468 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
469 );
470 }
471
472 /**
473 * Send customer confirmation email
474 *
475 * @param int $enquiryId Enquiry ID
476 */
477 private function sendCustomerConfirmation(int $enquiryId): void
478 {
479 $enquiry = $this->enquiryRepository->findWithTrip($enquiryId);
480
481 if (!$enquiry || empty($enquiry->email)) {
482 return;
483 }
484
485 TransactionalEmailTemplateService::sendIfEnabled(
486 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RECEIVED,
487 (string) $enquiry->email,
488 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, '')
489 );
490 }
491
492 /**
493 * Send response email to customer
494 *
495 * @param object $enquiry Enquiry data
496 * @param string $response Response message
497 */
498 private function sendResponseEmail(object $enquiry, string $response): void
499 {
500 if (empty($enquiry->email)) {
501 return;
502 }
503
504 TransactionalEmailTemplateService::sendIfEnabled(
505 TransactionalEmailTemplateService::TYPE_ENQUIRY_CUSTOMER_RESPONSE,
506 (string) $enquiry->email,
507 TransactionalEmailTemplateService::variablesFromEnquiry($enquiry, $response)
508 );
509 }
510 }
511
512