PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.7
Yatra – Travel Booking & Tour Operator Software v3.0.7
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 / ReviewService.php

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

492 lines 15.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\ReviewRepository;
8 use Yatra\Repositories\TripRepository;
9
10 /**
11 * Review Service
12 *
13 * Contains business logic for trip reviews.
14 *
15 * @package Yatra\Services
16 */
17 class ReviewService
18 {
19 private ReviewRepository $reviewRepository;
20 private TripRepository $tripRepository;
21
22 public function __construct()
23 {
24 $this->reviewRepository = new ReviewRepository();
25 $this->tripRepository = new TripRepository();
26 }
27
28 /**
29 * Get paginated reviews
30 *
31 * @param array $filters Filters
32 * @return array
33 */
34 public function getReviews(array $filters = []): array
35 {
36 $result = $this->reviewRepository->paginate($filters);
37
38 $result['data'] = array_map([$this, 'formatReview'], $result['data']);
39
40 return $result;
41 }
42
43 /**
44 * Get single review
45 *
46 * @param int $id Review ID
47 * @return array|null
48 */
49 public function getReview(int $id): ?array
50 {
51 $review = $this->reviewRepository->findWithTrip($id);
52
53 if (!$review) {
54 return null;
55 }
56
57 return $this->formatReview($review);
58 }
59
60 /**
61 * Get approved reviews for a trip
62 *
63 * @param int $tripId Trip ID
64 * @param int $limit Limit results
65 * @return array
66 */
67 public function getTripReviews(int $tripId, int $limit = 10): array
68 {
69 $reviews = $this->reviewRepository->findApprovedByTripId($tripId, $limit);
70
71 return array_map([$this, 'formatReview'], $reviews);
72 }
73
74 /**
75 * Get trip rating summary
76 *
77 * @param int $tripId Trip ID
78 * @return array
79 */
80 public function getTripRatingSummary(int $tripId): array
81 {
82 return [
83 'average_rating' => $this->reviewRepository->getAverageRating($tripId),
84 'review_count' => $this->reviewRepository->getReviewCount($tripId),
85 'distribution' => $this->reviewRepository->getRatingDistribution($tripId),
86 ];
87 }
88
89 /**
90 * Submit a review
91 *
92 * @param array $data Review data
93 * @return array {success: bool, review_id?: int, message: string}
94 */
95 public function submitReview(array $data): array
96 {
97 // Validate required fields
98 if (empty($data['trip_id']) || empty($data['rating'])) {
99 return ['success' => false, 'message' => __('Trip and rating are required.', 'yatra')];
100 }
101
102 // Validate trip exists
103 $trip = $this->tripRepository->find((int) $data['trip_id']);
104 if (!$trip) {
105 return ['success' => false, 'message' => __('Trip not found.', 'yatra')];
106 }
107
108 // Validate rating
109 $rating = (int) $data['rating'];
110 if ($rating < 1 || $rating > 5) {
111 return ['success' => false, 'message' => __('Rating must be between 1 and 5.', 'yatra')];
112 }
113
114 // Check if user already reviewed this trip
115 $userId = $data['user_id'] ?? get_current_user_id();
116 if ($userId) {
117 $existingReview = $this->reviewRepository->findByUserAndTrip($userId, (int) $data['trip_id']);
118 if ($existingReview) {
119 return ['success' => false, 'message' => __('You have already reviewed this trip.', 'yatra')];
120 }
121 $data['user_id'] = $userId;
122 }
123
124 // Set reviewer info from user if logged in
125 if ($userId && empty($data['reviewer_name'])) {
126 $user = get_userdata($userId);
127 if ($user) {
128 $data['reviewer_name'] = $user->display_name;
129 $data['reviewer_email'] = $user->user_email;
130 }
131 }
132
133 // Set default status
134 $settings = SettingsService::getSettings();
135 $autoApprove = $settings['reviews']['auto_approve'] ?? false;
136 $data['status'] = $autoApprove ? 'approved' : 'pending';
137
138 // Create review
139 $reviewId = $this->reviewRepository->create($data);
140
141 if (!$reviewId) {
142 return ['success' => false, 'message' => __('Failed to submit review.', 'yatra')];
143 }
144
145 // Update trip rating cache
146 $this->updateTripRatingCache((int) $data['trip_id']);
147
148 return [
149 'success' => true,
150 'review_id' => $reviewId,
151 'message' => $autoApprove
152 ? __('Thank you for your review!', 'yatra')
153 : __('Thank you! Your review is pending approval.', 'yatra'),
154 ];
155 }
156
157 /**
158 * Create a review on behalf of a customer (admin path).
159 *
160 * Differs from {@see submitReview()} in three deliberate ways:
161 *
162 * 1. Skips the "already reviewed this trip" guard. An admin entering
163 * a customer's quote from email isn't a duplicate the user can fix.
164 * 2. Skips the `reviews.auto_approve` setting lookup. The status
165 * arrives from the operator (already enum-clamped by the
166 * controller) and is the source of truth.
167 * 3. Doesn't fall back to the current user's display_name/email as
168 * reviewer — admins enter the customer's identity by hand.
169 *
170 * Field aliasing (customer_name → author_name etc.) is handled at the
171 * controller layer; this method receives canonical column names only.
172 *
173 * @param array $data Canonical review payload: trip_id, rating, title,
174 * content, author_name, author_email, status,
175 * optional created_by.
176 * @return array {success: bool, review_id?: int, message: string}
177 */
178 public function createReviewAsAdmin(array $data): array
179 {
180 if (empty($data['trip_id']) || empty($data['rating'])) {
181 return ['success' => false, 'message' => __('Trip and rating are required.', 'yatra')];
182 }
183
184 $trip = $this->tripRepository->find((int) $data['trip_id']);
185 if (!$trip) {
186 return ['success' => false, 'message' => __('Trip not found.', 'yatra')];
187 }
188
189 $rating = (int) $data['rating'];
190 if ($rating < 1 || $rating > 5) {
191 return ['success' => false, 'message' => __('Rating must be between 1 and 5.', 'yatra')];
192 }
193
194 if (empty($data['author_name'])) {
195 return ['success' => false, 'message' => __('Customer name is required.', 'yatra')];
196 }
197 if (empty($data['content'])) {
198 return ['success' => false, 'message' => __('Review content is required.', 'yatra')];
199 }
200
201 // Default status only when the controller didn't supply one. The
202 // controller already clamps the value against the ENUM so we
203 // trust it as-is when present.
204 if (!isset($data['status']) || $data['status'] === '') {
205 $data['status'] = 'pending';
206 }
207
208 try {
209 $reviewId = $this->reviewRepository->create($data);
210 } catch (\Throwable $e) {
211 return [
212 'success' => false,
213 'message' => __('Failed to create review.', 'yatra') . ' ' . $e->getMessage(),
214 ];
215 }
216
217 if (!$reviewId) {
218 return ['success' => false, 'message' => __('Failed to create review.', 'yatra')];
219 }
220
221 $this->updateTripRatingCache((int) $data['trip_id']);
222
223 return [
224 'success' => true,
225 'review_id' => $reviewId,
226 'message' => __('Review created successfully.', 'yatra'),
227 ];
228 }
229
230 /**
231 * Update a review
232 *
233 * @param int $id Review ID
234 * @param array $data Review data
235 * @return array {success: bool, message: string}
236 */
237 public function updateReview(int $id, array $data): array
238 {
239 $review = $this->reviewRepository->find($id);
240
241 if (!$review) {
242 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
243 }
244
245 // Check if user can edit
246 $userId = get_current_user_id();
247 if ($userId && !current_user_can('manage_options')) {
248 if (!$this->reviewRepository->canUserEdit($id, $userId)) {
249 return ['success' => false, 'message' => __('You cannot edit this review.', 'yatra')];
250 }
251 }
252
253 // Validate rating if provided
254 if (isset($data['rating'])) {
255 $rating = (int) $data['rating'];
256 if ($rating < 1 || $rating > 5) {
257 return ['success' => false, 'message' => __('Rating must be between 1 and 5.', 'yatra')];
258 }
259 }
260
261 $updated = $this->reviewRepository->update($id, $data);
262
263 if (!$updated) {
264 return ['success' => false, 'message' => __('Failed to update review.', 'yatra')];
265 }
266
267 // Update trip rating cache
268 $this->updateTripRatingCache((int) $review->trip_id);
269
270 return [
271 'success' => true,
272 'message' => __('Review updated successfully.', 'yatra'),
273 ];
274 }
275
276 /**
277 * Update review status
278 *
279 * @param int $id Review ID
280 * @param string $status New status
281 * @return array {success: bool, message: string}
282 */
283 public function updateStatus(int $id, string $status): array
284 {
285 $validStatuses = ['pending', 'approved', 'rejected', 'spam', 'trash'];
286
287 if (!in_array($status, $validStatuses, true)) {
288 return ['success' => false, 'message' => __('Invalid status.', 'yatra')];
289 }
290
291 $review = $this->reviewRepository->find($id);
292
293 if (!$review) {
294 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
295 }
296
297 $updated = $this->reviewRepository->updateStatus($id, $status);
298
299 if (!$updated) {
300 return ['success' => false, 'message' => __('Failed to update status.', 'yatra')];
301 }
302
303 // Update trip rating cache
304 $this->updateTripRatingCache((int) $review->trip_id);
305
306 return [
307 'success' => true,
308 'message' => sprintf(
309 /* translators: %s: new review status. */
310 __('Review status updated to %s.', 'yatra'),
311 $status
312 ),
313 ];
314 }
315
316 /**
317 * Bulk update review status
318 *
319 * @param array $ids Review IDs
320 * @param string $status New status
321 * @return array {success: bool, affected: int, message: string}
322 */
323 public function bulkUpdateStatus(array $ids, string $status): array
324 {
325 $validStatuses = ['pending', 'approved', 'rejected', 'spam', 'trash'];
326
327 if (!in_array($status, $validStatuses, true)) {
328 return ['success' => false, 'affected' => 0, 'message' => __('Invalid status.', 'yatra')];
329 }
330
331 $affected = $this->reviewRepository->bulkUpdateStatus($ids, $status);
332
333 return [
334 'success' => true,
335 'affected' => $affected,
336 'message' => sprintf(
337 /* translators: %d: number of reviews updated. */
338 __('%d reviews updated.', 'yatra'),
339 $affected
340 ),
341 ];
342 }
343
344 /**
345 * Bulk delete reviews
346 *
347 * @param array $ids Review IDs
348 * @return array {success: bool, affected: int, message: string}
349 */
350 public function bulkDelete(array $ids): array
351 {
352 $affected = $this->reviewRepository->bulkDelete($ids);
353
354 return [
355 'success' => true,
356 'affected' => $affected,
357 'message' => sprintf(
358 /* translators: %d: number of reviews deleted. */
359 __('%d reviews deleted.', 'yatra'),
360 $affected
361 ),
362 ];
363 }
364
365 /**
366 * Delete a review
367 *
368 * @param int $id Review ID
369 * @return array {success: bool, message: string}
370 */
371 public function deleteReview(int $id): array
372 {
373 $review = $this->reviewRepository->find($id);
374
375 if (!$review) {
376 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
377 }
378
379 $tripId = (int) $review->trip_id;
380
381 $deleted = $this->reviewRepository->delete($id);
382
383 if (!$deleted) {
384 return ['success' => false, 'message' => __('Failed to delete review.', 'yatra')];
385 }
386
387 // Update trip rating cache
388 $this->updateTripRatingCache($tripId);
389
390 return [
391 'success' => true,
392 'message' => __('Review deleted successfully.', 'yatra'),
393 ];
394 }
395
396 /**
397 * Check if user can review a trip
398 *
399 * @param int $tripId Trip ID
400 * @param int|null $userId User ID (current user if null)
401 * @return array {can_review: bool, reason?: string}
402 */
403 public function canUserReview(int $tripId, ?int $userId = null): array
404 {
405 $userId = $userId ?? get_current_user_id();
406
407 // Check if user is logged in
408 $settings = SettingsService::getSettings();
409 $requireLogin = $settings['reviews']['require_login'] ?? true;
410
411 if ($requireLogin && !$userId) {
412 return [
413 'can_review' => false,
414 'reason' => __('You must be logged in to leave a review.', 'yatra'),
415 ];
416 }
417
418 // Check if user already reviewed
419 if ($userId) {
420 $existingReview = $this->reviewRepository->findByUserAndTrip($userId, $tripId);
421 if ($existingReview) {
422 return [
423 'can_review' => false,
424 'reason' => __('You have already reviewed this trip.', 'yatra'),
425 'existing_review_id' => (int) $existingReview->id,
426 ];
427 }
428 }
429
430 return ['can_review' => true];
431 }
432
433 /**
434 * Get review statistics
435 *
436 * @return array
437 */
438 public function getStats(): array
439 {
440 return $this->reviewRepository->getStats();
441 }
442
443 /**
444 * Format review for API response
445 *
446 * @param object $review Raw review data
447 * @return array
448 */
449 private function formatReview(object $review): array
450 {
451 return [
452 'id' => (int) $review->id,
453 'trip_id' => (int) $review->trip_id,
454 'trip_title' => $review->trip_title ?? null,
455 'trip_slug' => $review->trip_slug ?? null,
456 'user_id' => $review->user_id ? (int) $review->user_id : null,
457 'user_display_name' => $review->user_display_name ?? null,
458 'rating' => (int) $review->rating,
459 'title' => $review->title ?? '',
460 'content' => $review->content ?? '',
461 'author_name' => $review->author_name ?? '',
462 'author_email' => $review->author_email ?? '',
463 'author_location' => $review->author_location ?? null,
464 'status' => $review->status,
465 'helpful_count' => (int) ($review->helpful_count ?? 0),
466 'can_edit' => $review->user_id
467 ? $this->reviewRepository->canUserEdit((int) $review->id, (int) $review->user_id)
468 : false,
469 'created_at' => $review->created_at,
470 'updated_at' => $review->updated_at,
471 ];
472 }
473
474 /**
475 * Update trip's cached rating values
476 *
477 * @param int $tripId Trip ID
478 */
479 private function updateTripRatingCache(int $tripId): void
480 {
481 $averageRating = $this->reviewRepository->getAverageRating($tripId);
482 $reviewCount = $this->reviewRepository->getReviewCount($tripId);
483
484 // Trips table columns are avg_rating + reviews_count (not average_rating / review_count).
485 $this->tripRepository->update($tripId, [
486 'avg_rating' => $averageRating,
487 'reviews_count' => $reviewCount,
488 ]);
489 }
490 }
491
492