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 / Controllers / ItineraryController.php

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

432 lines 15.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\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\ItineraryService;
11 use Yatra\Repositories\ItineraryRepository;
12
13 /**
14 * Itinerary REST API Controller
15 * API endpoints for itinerary entries management
16 */
17 class ItineraryController extends BaseController
18 {
19 private ItineraryService $service;
20
21 public function __construct()
22 {
23 $this->service = new ItineraryService(new ItineraryRepository());
24 }
25
26 /**
27 * Register routes
28 */
29 public function register_routes(): void
30 {
31 $namespace = 'yatra/v1';
32 $base = 'itinerary';
33
34 // Collection routes
35 register_rest_route($namespace, '/' . $base, [
36 [
37 'methods' => \WP_REST_Server::CREATABLE,
38 'callback' => [$this, 'create_item'],
39 'permission_callback' => [$this, 'check_permission'],
40 ],
41 [
42 'methods' => \WP_REST_Server::DELETABLE,
43 'callback' => [$this, 'bulk_delete_items'],
44 'permission_callback' => [$this, 'check_permission'],
45 ],
46 [
47 'methods' => \WP_REST_Server::READABLE,
48 'callback' => [$this, 'get_items_by_trip'],
49 'permission_callback' => [$this, 'check_permission'],
50 ],
51 ]);
52
53 // Single item routes
54 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
55 [
56 'methods' => \WP_REST_Server::READABLE,
57 'callback' => [$this, 'get_item'],
58 'permission_callback' => [$this, 'check_permission'],
59 ],
60 [
61 'methods' => \WP_REST_Server::EDITABLE,
62 'callback' => [$this, 'update_item'],
63 'permission_callback' => [$this, 'check_permission'],
64 ],
65 [
66 'methods' => \WP_REST_Server::DELETABLE,
67 'callback' => [$this, 'delete_item'],
68 'permission_callback' => [$this, 'check_permission'],
69 ],
70 ]);
71
72 // Get day entry ID by day_id
73 register_rest_route($namespace, '/' . $base . '/day-entry-by-day-id/(?P<day_id>[\d]+)', [
74 [
75 'methods' => \WP_REST_Server::READABLE,
76 'callback' => [$this, 'get_day_entry_id_by_day_id'],
77 'permission_callback' => [$this, 'check_permission'],
78 ],
79 ]);
80
81 // Bulk-update / -create activities for a single day in one request.
82 // Replaces the React day-edit "save" path that previously fired one PUT
83 // per activity (5 activities = 5 sequential round-trips). The endpoint
84 // accepts an `activities` array carrying full payloads + optional `id`
85 // per row; rows with `id` are PUTs, rows without are POSTs.
86 register_rest_route($namespace, '/' . $base . '/day/(?P<day_id>[\d]+)/activities/bulk', [
87 [
88 'methods' => \WP_REST_Server::EDITABLE,
89 'callback' => [$this, 'bulk_save_day_activities'],
90 'permission_callback' => [$this, 'check_permission'],
91 ],
92 ]);
93 }
94
95 /**
96 * Itinerary endpoints are bound to a trip. Reads use the view-trips
97 * cap (Sales Agent / Front Desk / Guide / Auditor all hold this);
98 * writes use the edit-trips cap. WP admins pass via the Team
99 * module's admin-fallback filter. Overrides BaseController's default
100 * which was the coarse `manage_options` check.
101 */
102 public function check_permission(?WP_REST_Request $request = null): bool
103 {
104 if ($request === null) {
105 return true;
106 }
107 if (!is_user_logged_in()) {
108 return false;
109 }
110 switch ($request->get_method()) {
111 case 'GET':
112 return current_user_can('yatra_view_trips');
113 case 'POST':
114 case 'PUT':
115 case 'PATCH':
116 case 'DELETE':
117 return current_user_can('yatra_edit_trips');
118 default:
119 return current_user_can('yatra_view_trips');
120 }
121 }
122
123 /**
124 * Get items by trip ID
125 */
126 public function get_items_by_trip(WP_REST_Request $request)
127 {
128 try {
129 $trip_id = (int) $request->get_param('trip_id');
130
131 if (!$trip_id) {
132 return $this->error_response('Trip ID is required', 400);
133 }
134
135 $items = $this->service->getByTripId($trip_id);
136
137 // Debug logging
138 $prepared_items = array_map(function ($item) use ($request) {
139 $prepared = $this->prepare_item_for_response($item, $request);
140 return $prepared;
141 }, $items);
142
143 $response = $this->success_response($prepared_items);
144 return $response;
145 } catch (\Exception $e) {
146 return $this->error_response($e->getMessage(), 500);
147 }
148 }
149
150 /**
151 * Get item
152 */
153 public function get_item(WP_REST_Request $request)
154 {
155 try {
156 $id = (int) $request->get_param('id');
157 $mode = $request->get_param('mode') ?: 'activity'; // Default to activity mode
158
159 // Use mode to determine which method to call
160 if ($mode === 'day') {
161 // Get day entry (from days table)
162 $item = $this->service->find($id);
163 } else {
164 // Get activity entry (from entries table)
165 $item = $this->service->findActivity($id);
166 }
167
168 if (!$item) {
169 return $this->error_response('Itinerary entry not found', 404);
170 }
171
172 $prepared = $this->prepare_item_for_response($item, $request);
173 return $this->success_response($prepared);
174 } catch (\Exception $e) {
175 return $this->error_response($e->getMessage(), 500);
176 }
177 }
178
179 /**
180 * Create item
181 */
182 public function create_item(WP_REST_Request $request)
183 {
184 try {
185 $data = $request->get_json_params();
186 $id = $this->service->create($data);
187
188 return $this->success_response([
189 'id' => $id,
190 'message' => __('Itinerary entry created successfully', 'yatra'),
191 ], 201);
192 } catch (\InvalidArgumentException $e) {
193 return $this->error_response($e->getMessage(), 400);
194 } catch (\Exception $e) {
195 return $this->error_response($e->getMessage(), 500);
196 }
197 }
198
199 /**
200 * Update item
201 */
202 public function update_item(WP_REST_Request $request)
203 {
204 try {
205 $id = (int) $request->get_param('id');
206 $mode = $request->get_param('mode') ?: 'activity'; // Default to activity mode
207 $data = $request->get_json_params();
208 $result = $this->service->update($id, $data, $mode);
209
210 if (!$result) {
211 return $this->error_response('Failed to update itinerary entry', 500);
212 }
213
214 return $this->success_response([
215 'message' => __('Itinerary entry updated successfully', 'yatra'),
216 ]);
217 } catch (\InvalidArgumentException $e) {
218 return $this->error_response($e->getMessage(), 400);
219 } catch (\Exception $e) {
220 return $this->error_response($e->getMessage(), 500);
221 }
222 }
223
224 /**
225 * Delete item
226 */
227 public function delete_item(WP_REST_Request $request)
228 {
229 try {
230 $id = (int) $request->get_param('id');
231 $mode = $request->get_param('mode') ?: 'activity'; // Default to activity mode
232 $result = $this->service->delete($id, $mode);
233
234 if (!$result) {
235 return $this->error_response('Failed to delete itinerary entry', 500);
236 }
237
238 return $this->success_response([
239 'message' => __('Itinerary entry deleted successfully', 'yatra'),
240 ]);
241 } catch (\Exception $e) {
242 return $this->error_response($e->getMessage(), 500);
243 }
244 }
245
246 /**
247 * Bulk-save the activities of a single day in one request.
248 *
249 * Body shape:
250 * {
251 * "trip_id": 14,
252 * "activities": [
253 * { "id": 39, "title": "...", "order": 0, ...full activity fields... },
254 * { "title": "...", "order": 1, ... } // no id => create
255 * ]
256 * }
257 *
258 * Each row goes through the same validation / sanitisation as a single
259 * update / create — the difference is one HTTP round-trip and one cache
260 * invalidation instead of N. Returns per-row results so the React layer
261 * can surface partial failures without losing the rows that succeeded.
262 */
263 public function bulk_save_day_activities(WP_REST_Request $request)
264 {
265 try {
266 $dayId = (int) $request->get_param('day_id');
267 $body = $request->get_json_params() ?: [];
268 $tripId = isset($body['trip_id']) ? (int) $body['trip_id'] : 0;
269 $activities = $body['activities'] ?? [];
270
271 if ($dayId <= 0) {
272 return $this->error_response('Invalid day_id', 400);
273 }
274 if (!is_array($activities)) {
275 return $this->error_response('activities must be an array', 400);
276 }
277
278 $result = $this->service->bulkSaveDayActivities($dayId, $tripId, $activities);
279
280 return $this->success_response($result);
281 } catch (\InvalidArgumentException $e) {
282 return $this->error_response($e->getMessage(), 400);
283 } catch (\Exception $e) {
284 return $this->error_response($e->getMessage(), 500);
285 }
286 }
287
288 /**
289 * Bulk delete items
290 */
291 public function bulk_delete_items(WP_REST_Request $request)
292 {
293 try {
294 $data = $request->get_json_params();
295 $ids = $data['ids'] ?? [];
296 $dayIds = $data['day_ids'] ?? [];
297
298 $ids = is_array($ids) ? $ids : [];
299 $dayIds = is_array($dayIds) ? $dayIds : [];
300
301 if (empty($ids) && empty($dayIds)) {
302 return $this->error_response('No IDs provided for bulk delete', 400);
303 }
304
305 $result = $this->service->bulkDelete($ids, $dayIds);
306
307 return $this->success_response([
308 'deleted' => $result['deleted'],
309 'failed' => $result['failed'],
310 'message' => sprintf(
311 /* translators: 1: number of items deleted, 2: number of items that failed to delete. */
312 __('%1$d item(s) deleted successfully. %2$d item(s) failed to delete.', 'yatra'),
313 $result['deleted'],
314 $result['failed']
315 ),
316 ]);
317 } catch (\Exception $e) {
318 return $this->error_response($e->getMessage(), 500);
319 }
320 }
321
322 /**
323 * Get day entry ID by day_id
324 */
325 public function get_day_entry_id_by_day_id(WP_REST_Request $request)
326 {
327 try {
328 $dayId = (int) $request->get_param('day_id');
329 $repository = new ItineraryRepository();
330 $dayEntryId = $repository->getDayEntryIdByDayId($dayId);
331
332 return $this->success_response([
333 'day_entry_id' => $dayEntryId,
334 ]);
335 } catch (\Exception $e) {
336 return $this->error_response($e->getMessage(), 500);
337 }
338 }
339
340 /**
341 * Prepare item for response
342 */
343 protected function prepare_item_for_response($item, WP_REST_Request $request): array
344 {
345 // Parse included_items and excluded_items from JSON if they're strings
346 $includedItems = $item->included_items ?? [];
347 if (is_string($includedItems)) {
348 $decoded = json_decode($includedItems, true);
349 $includedItems = is_array($decoded) ? $decoded : [];
350 }
351
352 $excludedItems = $item->excluded_items ?? [];
353 if (is_string($excludedItems)) {
354 $decoded = json_decode($excludedItems, true);
355 $excludedItems = is_array($decoded) ? $decoded : [];
356 }
357
358 return [
359 'id' => (int) $item->id,
360 'trip_id' => (int) $item->trip_id,
361 'day_id' => (int) $item->day_id,
362 'day' => isset($item->day_number) ? (int) $item->day_number : null,
363 'day_title' => $item->day_title ?? null,
364 'day_description' => $item->day_description ?? null,
365 'title' => $item->title ?? '',
366 'description' => $item->description ?? '',
367 'time' => $item->time ?? null,
368 'start_time' => $item->start_time ?? null,
369 'end_time' => $item->end_time ?? null,
370 'time_type' => $item->time_type ?? 'exact',
371 'location' => $item->location ?? null,
372 'location_latitude' => $item->location_latitude ?? null,
373 'location_longitude' => $item->location_longitude ?? null,
374 'duration' => $item->duration ?? null,
375 'cost' => isset($item->cost) ? (float) $item->cost : null,
376 'cost_per_person' => isset($item->cost_per_person) ? (bool) $item->cost_per_person : false,
377 'notes' => $item->notes ?? null,
378 'item_type_id' => ($item->item_type_id !== null && $item->item_type_id !== '') ? (int) $item->item_type_id : null,
379 'item_id' => ($item->item_id !== null && $item->item_id !== '') ? (int) $item->item_id : null,
380 'item_type_name' => $item->item_type_name ?? null,
381 'item_name' => $item->item_name ?? null,
382 'item_type_icon' => $item->item_type_icon ?? null,
383 'included_items' => $includedItems,
384 'excluded_items' => $excludedItems,
385 'status' => $item->status ?? 'draft',
386 'images' => $item->images ?? [],
387 'gallery' => $this->decodeGallery($item->gallery ?? null),
388 'video_url' => $item->video_url ?? null,
389 'order' => (int) ($item->order ?? 0),
390 'created_at' => $item->created_at ?? null,
391 'updated_at' => $item->updated_at ?? null,
392 ];
393 }
394
395 /**
396 * Decode gallery JSON from database and convert attachment IDs to URLs
397 */
398 private function decodeGallery(?string $galleryJson): array
399 {
400 if (empty($galleryJson)) {
401 return [];
402 }
403
404 $gallery = json_decode($galleryJson, true);
405 if (!is_array($gallery)) {
406 return [];
407 }
408
409 // Convert attachment IDs to URLs for frontend compatibility
410 foreach ($gallery as &$item) {
411 if (isset($item['attachment_id']) && $item['attachment_id'] > 0) {
412 // Get attachment URL from WordPress
413 $attachment_url = wp_get_attachment_url($item['attachment_id']);
414 if ($attachment_url) {
415 $item['url'] = $attachment_url;
416 }
417
418 // Get thumbnail URL for images
419 if (isset($item['type']) && $item['type'] === 'image') {
420 $thumbnail_url = wp_get_attachment_image_src($item['attachment_id'], 'medium');
421 if ($thumbnail_url) {
422 $item['thumbnail_url'] = $thumbnail_url[0];
423 }
424 }
425 }
426 }
427
428 return $gallery;
429 }
430 }
431
432