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

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

407 lines 12.2 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 * Update a review
159 *
160 * @param int $id Review ID
161 * @param array $data Review data
162 * @return array {success: bool, message: string}
163 */
164 public function updateReview(int $id, array $data): array
165 {
166 $review = $this->reviewRepository->find($id);
167
168 if (!$review) {
169 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
170 }
171
172 // Check if user can edit
173 $userId = get_current_user_id();
174 if ($userId && !current_user_can('manage_options')) {
175 if (!$this->reviewRepository->canUserEdit($id, $userId)) {
176 return ['success' => false, 'message' => __('You cannot edit this review.', 'yatra')];
177 }
178 }
179
180 // Validate rating if provided
181 if (isset($data['rating'])) {
182 $rating = (int) $data['rating'];
183 if ($rating < 1 || $rating > 5) {
184 return ['success' => false, 'message' => __('Rating must be between 1 and 5.', 'yatra')];
185 }
186 }
187
188 $updated = $this->reviewRepository->update($id, $data);
189
190 if (!$updated) {
191 return ['success' => false, 'message' => __('Failed to update review.', 'yatra')];
192 }
193
194 // Update trip rating cache
195 $this->updateTripRatingCache((int) $review->trip_id);
196
197 return [
198 'success' => true,
199 'message' => __('Review updated successfully.', 'yatra'),
200 ];
201 }
202
203 /**
204 * Update review status
205 *
206 * @param int $id Review ID
207 * @param string $status New status
208 * @return array {success: bool, message: string}
209 */
210 public function updateStatus(int $id, string $status): array
211 {
212 $validStatuses = ['pending', 'approved', 'rejected', 'spam', 'trash'];
213
214 if (!in_array($status, $validStatuses, true)) {
215 return ['success' => false, 'message' => __('Invalid status.', 'yatra')];
216 }
217
218 $review = $this->reviewRepository->find($id);
219
220 if (!$review) {
221 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
222 }
223
224 $updated = $this->reviewRepository->updateStatus($id, $status);
225
226 if (!$updated) {
227 return ['success' => false, 'message' => __('Failed to update status.', 'yatra')];
228 }
229
230 // Update trip rating cache
231 $this->updateTripRatingCache((int) $review->trip_id);
232
233 return [
234 'success' => true,
235 'message' => sprintf(__('Review status updated to %s.', 'yatra'), $status),
236 ];
237 }
238
239 /**
240 * Bulk update review status
241 *
242 * @param array $ids Review IDs
243 * @param string $status New status
244 * @return array {success: bool, affected: int, message: string}
245 */
246 public function bulkUpdateStatus(array $ids, string $status): array
247 {
248 $validStatuses = ['pending', 'approved', 'rejected', 'spam', 'trash'];
249
250 if (!in_array($status, $validStatuses, true)) {
251 return ['success' => false, 'affected' => 0, 'message' => __('Invalid status.', 'yatra')];
252 }
253
254 $affected = $this->reviewRepository->bulkUpdateStatus($ids, $status);
255
256 return [
257 'success' => true,
258 'affected' => $affected,
259 'message' => sprintf(__('%d reviews updated.', 'yatra'), $affected),
260 ];
261 }
262
263 /**
264 * Bulk delete reviews
265 *
266 * @param array $ids Review IDs
267 * @return array {success: bool, affected: int, message: string}
268 */
269 public function bulkDelete(array $ids): array
270 {
271 $affected = $this->reviewRepository->bulkDelete($ids);
272
273 return [
274 'success' => true,
275 'affected' => $affected,
276 'message' => sprintf(__('%d reviews deleted.', 'yatra'), $affected),
277 ];
278 }
279
280 /**
281 * Delete a review
282 *
283 * @param int $id Review ID
284 * @return array {success: bool, message: string}
285 */
286 public function deleteReview(int $id): array
287 {
288 $review = $this->reviewRepository->find($id);
289
290 if (!$review) {
291 return ['success' => false, 'message' => __('Review not found.', 'yatra')];
292 }
293
294 $tripId = (int) $review->trip_id;
295
296 $deleted = $this->reviewRepository->delete($id);
297
298 if (!$deleted) {
299 return ['success' => false, 'message' => __('Failed to delete review.', 'yatra')];
300 }
301
302 // Update trip rating cache
303 $this->updateTripRatingCache($tripId);
304
305 return [
306 'success' => true,
307 'message' => __('Review deleted successfully.', 'yatra'),
308 ];
309 }
310
311 /**
312 * Check if user can review a trip
313 *
314 * @param int $tripId Trip ID
315 * @param int|null $userId User ID (current user if null)
316 * @return array {can_review: bool, reason?: string}
317 */
318 public function canUserReview(int $tripId, ?int $userId = null): array
319 {
320 $userId = $userId ?? get_current_user_id();
321
322 // Check if user is logged in
323 $settings = SettingsService::getSettings();
324 $requireLogin = $settings['reviews']['require_login'] ?? true;
325
326 if ($requireLogin && !$userId) {
327 return [
328 'can_review' => false,
329 'reason' => __('You must be logged in to leave a review.', 'yatra'),
330 ];
331 }
332
333 // Check if user already reviewed
334 if ($userId) {
335 $existingReview = $this->reviewRepository->findByUserAndTrip($userId, $tripId);
336 if ($existingReview) {
337 return [
338 'can_review' => false,
339 'reason' => __('You have already reviewed this trip.', 'yatra'),
340 'existing_review_id' => (int) $existingReview->id,
341 ];
342 }
343 }
344
345 return ['can_review' => true];
346 }
347
348 /**
349 * Get review statistics
350 *
351 * @return array
352 */
353 public function getStats(): array
354 {
355 return $this->reviewRepository->getStats();
356 }
357
358 /**
359 * Format review for API response
360 *
361 * @param object $review Raw review data
362 * @return array
363 */
364 private function formatReview(object $review): array
365 {
366 return [
367 'id' => (int) $review->id,
368 'trip_id' => (int) $review->trip_id,
369 'trip_title' => $review->trip_title ?? null,
370 'trip_slug' => $review->trip_slug ?? null,
371 'user_id' => $review->user_id ? (int) $review->user_id : null,
372 'user_display_name' => $review->user_display_name ?? null,
373 'rating' => (int) $review->rating,
374 'title' => $review->title ?? '',
375 'content' => $review->content ?? '',
376 'author_name' => $review->author_name ?? '',
377 'author_email' => $review->author_email ?? '',
378 'author_location' => $review->author_location ?? null,
379 'status' => $review->status,
380 'helpful_count' => (int) ($review->helpful_count ?? 0),
381 'can_edit' => $review->user_id
382 ? $this->reviewRepository->canUserEdit((int) $review->id, (int) $review->user_id)
383 : false,
384 'created_at' => $review->created_at,
385 'updated_at' => $review->updated_at,
386 ];
387 }
388
389 /**
390 * Update trip's cached rating values
391 *
392 * @param int $tripId Trip ID
393 */
394 private function updateTripRatingCache(int $tripId): void
395 {
396 $averageRating = $this->reviewRepository->getAverageRating($tripId);
397 $reviewCount = $this->reviewRepository->getReviewCount($tripId);
398
399 // Trips table columns are avg_rating + reviews_count (not average_rating / review_count).
400 $this->tripRepository->update($tripId, [
401 'avg_rating' => $averageRating,
402 'reviews_count' => $reviewCount,
403 ]);
404 }
405 }
406
407