PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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 / Controllers / AvailabilityController.php

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

461 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Availability REST API Controller
4 * API endpoints for trip availability dates management
5 *
6 * This is a FREE feature - no Pro plugin required
7 *
8 * @package Yatra\Controllers
9 * @since 3.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace Yatra\Controllers;
15
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_Error;
19 use Yatra\Database\Tables\BookingsTable;
20 use Yatra\Services\AvailabilityService;
21 use Yatra\Repositories\AvailabilityRepository;
22
23 class AvailabilityController extends BaseController
24 {
25 private AvailabilityService $service;
26
27 public function __construct()
28 {
29 $this->service = new AvailabilityService(new AvailabilityRepository());
30 }
31
32 /**
33 * Register routes
34 */
35 public function register_routes(): void
36 {
37 $namespace = 'yatra/v1';
38 $base = 'availability';
39
40 // Collection routes — view cap for reads, edit cap for writes.
41 register_rest_route($namespace, '/' . $base, [
42 [
43 'methods' => \WP_REST_Server::READABLE,
44 'callback' => [$this, 'get_items'],
45 'permission_callback' => [$this, 'check_view_permission'],
46 'args' => [
47 'trip_id' => [
48 'required' => true,
49 'type' => 'integer',
50 'validate_callback' => function ($param) {
51 return is_numeric($param) && $param > 0;
52 },
53 ],
54 'status' => [
55 'type' => 'string',
56 'default' => 'all',
57 ],
58 'month' => [
59 'type' => 'string',
60 'default' => 'all',
61 ],
62 'search' => [
63 'type' => 'string',
64 'default' => '',
65 ],
66 'page' => [
67 'type' => 'integer',
68 'default' => 1,
69 'minimum' => 1,
70 ],
71 'per_page' => [
72 'type' => 'integer',
73 'default' => 50,
74 'minimum' => 1,
75 'maximum' => 100,
76 ],
77 ],
78 ],
79 [
80 'methods' => \WP_REST_Server::CREATABLE,
81 'callback' => [$this, 'create_item'],
82 'permission_callback' => [$this, 'check_permission'],
83 ],
84 ]);
85
86 // Single item routes — view cap for read, edit cap for the
87 // mutations. Delete uses edit as well — there's no separate
88 // "delete availability date" cap in the registry because
89 // removing a date is functionally part of trip availability
90 // editing, not a destructive operation in its own right.
91 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
92 [
93 'methods' => \WP_REST_Server::READABLE,
94 'callback' => [$this, 'get_item'],
95 'permission_callback' => [$this, 'check_view_permission'],
96 ],
97 [
98 'methods' => \WP_REST_Server::EDITABLE,
99 'callback' => [$this, 'update_item'],
100 'permission_callback' => [$this, 'check_permission'],
101 ],
102 [
103 'methods' => \WP_REST_Server::DELETABLE,
104 'callback' => [$this, 'delete_item'],
105 'permission_callback' => [$this, 'check_permission'],
106 ],
107 ]);
108
109 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/duplicate', [
110 [
111 'methods' => \WP_REST_Server::CREATABLE,
112 'callback' => [$this, 'duplicate_item'],
113 'permission_callback' => [$this, 'check_permission'],
114 ],
115 ]);
116 }
117
118 /**
119 * Get all availability dates for a trip
120 */
121 public function get_items(WP_REST_Request $request)
122 {
123 try {
124 $tripId = (int) $request->get_param('trip_id');
125
126 if ($tripId <= 0) {
127 return new WP_Error(
128 'invalid_trip_id',
129 'Valid trip_id is required',
130 ['status' => 400]
131 );
132 }
133
134 $filters = [
135 'status' => $request->get_param('status') ?? 'all',
136 'month' => $request->get_param('month') ?? 'all',
137 'search' => $request->get_param('search') ?? '',
138 'page' => (int) ($request->get_param('page') ?? 1),
139 'per_page' => (int) ($request->get_param('per_page') ?? 50),
140 ];
141
142 $items = $this->service->getByTripId($tripId, $filters);
143 $total = $this->service->countByTripId($tripId, $filters);
144
145 global $wpdb;
146 $bookingsTable = BookingsTable::getTableName();
147
148 $availabilityIdByDate = [];
149 $dateCounts = [];
150 foreach ($items as $item) {
151 $date = (string) ($item->departure_date ?? '');
152 if ($date === '') {
153 continue;
154 }
155 $dateCounts[$date] = ($dateCounts[$date] ?? 0) + 1;
156 $availabilityIdByDate[$date] = (int) ($item->id ?? 0);
157 }
158
159 foreach ($dateCounts as $date => $count) {
160 if ($count !== 1) {
161 unset($availabilityIdByDate[$date]);
162 }
163 }
164
165 // Use AvailabilityService to update booking availability IDs
166 if (!empty($availabilityIdByDate)) {
167 $this->service->updateBookingAvailabilityIds((int) $tripId, $availabilityIdByDate);
168 }
169
170 // Aggregate bookings count per availability date for this trip
171 // Use AvailabilityService to get booking counts
172 $countsByAvailabilityId = [];
173 $bookingCounts = $this->service->getBookingCountsByAvailabilityIds(array_column($items, 'id'));
174
175 foreach ($bookingCounts as $row) {
176 $aid = (int) ($row->availability_id ?? 0);
177 if ($aid > 0) {
178 $countsByAvailabilityId[$aid] = (int) ($row->booked_count ?? 0);
179 }
180 }
181
182 $data = array_map(function ($item) use ($request, $countsByAvailabilityId) {
183 $prepared = $this->prepare_item_for_response($item, $request);
184
185 $availabilityId = (int) ($prepared['id'] ?? 0);
186 $bookedCount = 0;
187
188 if ($availabilityId > 0 && isset($countsByAvailabilityId[$availabilityId])) {
189 $bookedCount = (int) $countsByAvailabilityId[$availabilityId];
190 }
191
192 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
193 $seatsReserved = (int) ($prepared['seats_reserved'] ?? 0);
194 $available = max(0, $seatsTotal - $bookedCount);
195
196 $prepared['booked_seats'] = $bookedCount;
197 $prepared['total_seats'] = $seatsTotal;
198 $prepared['available_seats'] = $available;
199 $prepared['seats_available'] = $available;
200
201 // Preserve original database status - don't override calculated status
202 // The status should reflect what's actually stored in the database
203 $original_status = $prepared['status'] ?? 'available';
204
205 // Only update status if seats are actually sold out (0 available)
206 if ($available === 0 && $original_status !== 'blocked' && $original_status !== 'closed' && $original_status !== 'cancelled') {
207 $prepared['status'] = 'sold_out';
208 }
209 // For all other cases, preserve the original database status
210 // This allows 'available', 'limited', 'blocked', 'closed', 'cancelled' to show correctly
211
212 return $prepared;
213 }, $items);
214
215 return new WP_REST_Response([
216 'dates' => $data,
217 'total' => $total,
218 'page' => $filters['page'],
219 'per_page' => $filters['per_page'],
220 ], 200);
221 } catch (\Exception $e) {
222 return new WP_Error(
223 'availability_fetch_error',
224 $e->getMessage(),
225 ['status' => 500]
226 );
227 }
228 }
229
230 public function duplicate_item(WP_REST_Request $request)
231 {
232 try {
233 $id = (int) $request->get_param('id');
234 $data = $request->get_json_params();
235
236 if (empty($data)) {
237 $data = $request->get_body_params();
238 }
239
240 $item = $this->service->duplicate($id, is_array($data) ? $data : []);
241
242 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
243 } catch (\InvalidArgumentException $e) {
244 return new WP_Error(
245 'validation_error',
246 $e->getMessage(),
247 ['status' => 400]
248 );
249 } catch (\Exception $e) {
250 return new WP_Error(
251 'availability_duplicate_error',
252 $e->getMessage(),
253 ['status' => 500]
254 );
255 }
256 }
257
258 /**
259 * Get single availability date
260 */
261 public function get_item(WP_REST_Request $request)
262 {
263 try {
264 $id = (int) $request->get_param('id');
265 $item = $this->service->getById($id);
266
267 if (!$item) {
268 return new WP_Error(
269 'availability_not_found',
270 'Availability date not found',
271 ['status' => 404]
272 );
273 }
274
275 $prepared = $this->prepare_item_for_response($item, $request);
276
277 // Compute live booked seats for this availability_id
278 if (!empty($prepared['id'])) {
279
280 // Use AvailabilityService to get booked count
281 $bookedCount = $this->service->getBookedCountByAvailabilityId((int) $prepared['id']);
282
283 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
284
285 $available = max(0, $seatsTotal - $bookedCount);
286
287 $prepared['booked_seats'] = $bookedCount;
288 $prepared['seats_available'] = $available;
289 }
290
291 return new WP_REST_Response($prepared, 200);
292 } catch (\Exception $e) {
293 return new WP_Error(
294 'availability_fetch_error',
295 $e->getMessage(),
296 ['status' => 500]
297 );
298 }
299 }
300
301 /**
302 * Create availability date
303 */
304 public function create_item(WP_REST_Request $request)
305 {
306 try {
307 $data = $request->get_json_params();
308
309 if (empty($data)) {
310 $data = $request->get_body_params();
311 }
312
313 $item = $this->service->create($data);
314
315 // Trigger hook to sync departure capacity
316 do_action('yatra_availability_updated', $item->id);
317
318 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
319 } catch (\InvalidArgumentException $e) {
320 return new WP_Error(
321 'validation_error',
322 $e->getMessage(),
323 ['status' => 400]
324 );
325 } catch (\Exception $e) {
326 return new WP_Error(
327 'availability_create_error',
328 $e->getMessage(),
329 ['status' => 500]
330 );
331 }
332 }
333
334 /**
335 * Update availability date
336 */
337 public function update_item(WP_REST_Request $request)
338 {
339 try {
340 $id = (int) $request->get_param('id');
341 $data = $request->get_json_params();
342
343 if (empty($data)) {
344 $data = $request->get_body_params();
345 }
346
347 $item = $this->service->update($id, $data);
348
349 // Trigger hook to sync departure capacity
350 do_action('yatra_availability_updated', $id);
351
352 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 200);
353 } catch (\InvalidArgumentException $e) {
354 return new WP_Error(
355 'validation_error',
356 $e->getMessage(),
357 ['status' => 400]
358 );
359 } catch (\Exception $e) {
360 return new WP_Error(
361 'availability_update_error',
362 $e->getMessage(),
363 ['status' => 500]
364 );
365 }
366 }
367
368 /**
369 * Delete availability date
370 */
371 public function delete_item(WP_REST_Request $request)
372 {
373 try {
374 $id = (int) $request->get_param('id');
375 $this->service->delete($id);
376
377 return new WP_REST_Response([
378 'message' => 'Availability date deleted successfully',
379 'id' => $id,
380 ], 200);
381 } catch (\InvalidArgumentException $e) {
382 return new WP_Error(
383 'validation_error',
384 $e->getMessage(),
385 ['status' => 400]
386 );
387 } catch (\Exception $e) {
388 return new WP_Error(
389 'availability_delete_error',
390 $e->getMessage(),
391 ['status' => 500]
392 );
393 }
394 }
395
396 /**
397 * Prepare item for response
398 */
399 protected function prepare_item_for_response($item, WP_REST_Request $request): array
400 {
401 $data = (array) $item;
402
403 // Format prices as strings for frontend
404 if (isset($data['original_price'])) {
405 $data['original_price'] = $data['original_price'] !== null ? number_format((float) $data['original_price'], 2, '.', '') : null;
406 }
407 if (isset($data['discounted_price'])) {
408 $data['discounted_price'] = $data['discounted_price'] !== null ? number_format((float) $data['discounted_price'], 2, '.', '') : null;
409 }
410 if (isset($data['discount_percentage'])) {
411 $data['discount_percentage'] = $data['discount_percentage'] !== null ? number_format((float) $data['discount_percentage'], 2, '.', '') : null;
412 }
413
414 // Ensure pricing_type has a default value
415 if (!isset($data['pricing_type']) || empty($data['pricing_type'])) {
416 $data['pricing_type'] = 'regular';
417 }
418
419 // Decode price_types JSON string from DB and ensure it's an array
420 if (isset($data['price_types']) && is_string($data['price_types'])) {
421 $decoded = json_decode($data['price_types'], true);
422 $data['price_types'] = is_array($decoded) ? $decoded : [];
423 } elseif (!isset($data['price_types']) || !is_array($data['price_types'])) {
424 $data['price_types'] = [];
425 }
426
427 // Ensure status matches frontend expectations
428 if ($data['status'] === 'blocked' || !empty($data['is_blocked'])) {
429 $data['status'] = 'blocked';
430 $data['is_blocked'] = true;
431 }
432
433 return $data;
434 }
435
436 /**
437 * Check permission
438 */
439 /**
440 * Write permission — trip-edits cap. Adding, updating, deleting,
441 * and duplicating availability dates all mutate trip data, so the
442 * registered `yatra_edit_trips` cap is the right gate. WP admins
443 * pass via the Team module's admin-fallback filter.
444 */
445 public function check_permission(?WP_REST_Request $request = null): bool
446 {
447 return current_user_can('yatra_edit_trips');
448 }
449
450 /**
451 * Read permission — view-trips cap. Listing availability dates is
452 * a read-only operation against trip data; Sales Agent / Front
453 * Desk / Guide / Accountant / Auditor roles all hold this.
454 */
455 public function check_view_permission(?WP_REST_Request $request = null): bool
456 {
457 return current_user_can('yatra_view_trips');
458 }
459 }
460
461