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

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

441 lines 14.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
41 register_rest_route($namespace, '/' . $base, [
42 [
43 'methods' => \WP_REST_Server::READABLE,
44 'callback' => [$this, 'get_items'],
45 'permission_callback' => [$this, 'check_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
87 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
88 [
89 'methods' => \WP_REST_Server::READABLE,
90 'callback' => [$this, 'get_item'],
91 'permission_callback' => [$this, 'check_permission'],
92 ],
93 [
94 'methods' => \WP_REST_Server::EDITABLE,
95 'callback' => [$this, 'update_item'],
96 'permission_callback' => [$this, 'check_permission'],
97 ],
98 [
99 'methods' => \WP_REST_Server::DELETABLE,
100 'callback' => [$this, 'delete_item'],
101 'permission_callback' => [$this, 'check_permission'],
102 ],
103 ]);
104
105 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/duplicate', [
106 [
107 'methods' => \WP_REST_Server::CREATABLE,
108 'callback' => [$this, 'duplicate_item'],
109 'permission_callback' => [$this, 'check_permission'],
110 ],
111 ]);
112 }
113
114 /**
115 * Get all availability dates for a trip
116 */
117 public function get_items(WP_REST_Request $request)
118 {
119 try {
120 $tripId = (int) $request->get_param('trip_id');
121
122 if ($tripId <= 0) {
123 return new WP_Error(
124 'invalid_trip_id',
125 'Valid trip_id is required',
126 ['status' => 400]
127 );
128 }
129
130 $filters = [
131 'status' => $request->get_param('status') ?? 'all',
132 'month' => $request->get_param('month') ?? 'all',
133 'search' => $request->get_param('search') ?? '',
134 'page' => (int) ($request->get_param('page') ?? 1),
135 'per_page' => (int) ($request->get_param('per_page') ?? 50),
136 ];
137
138 $items = $this->service->getByTripId($tripId, $filters);
139 $total = $this->service->countByTripId($tripId, $filters);
140
141 global $wpdb;
142 $bookingsTable = BookingsTable::getTableName();
143
144 $availabilityIdByDate = [];
145 $dateCounts = [];
146 foreach ($items as $item) {
147 $date = (string) ($item->departure_date ?? '');
148 if ($date === '') {
149 continue;
150 }
151 $dateCounts[$date] = ($dateCounts[$date] ?? 0) + 1;
152 $availabilityIdByDate[$date] = (int) ($item->id ?? 0);
153 }
154
155 foreach ($dateCounts as $date => $count) {
156 if ($count !== 1) {
157 unset($availabilityIdByDate[$date]);
158 }
159 }
160
161 // Use AvailabilityService to update booking availability IDs
162 if (!empty($availabilityIdByDate)) {
163 $this->service->updateBookingAvailabilityIds((int) $tripId, $availabilityIdByDate);
164 }
165
166 // Aggregate bookings count per availability date for this trip
167 // Use AvailabilityService to get booking counts
168 $countsByAvailabilityId = [];
169 $bookingCounts = $this->service->getBookingCountsByAvailabilityIds(array_column($items, 'id'));
170
171 foreach ($bookingCounts as $row) {
172 $aid = (int) ($row->availability_id ?? 0);
173 if ($aid > 0) {
174 $countsByAvailabilityId[$aid] = (int) ($row->booked_count ?? 0);
175 }
176 }
177
178 $data = array_map(function ($item) use ($request, $countsByAvailabilityId) {
179 $prepared = $this->prepare_item_for_response($item, $request);
180
181 $availabilityId = (int) ($prepared['id'] ?? 0);
182 $bookedCount = 0;
183
184 if ($availabilityId > 0 && isset($countsByAvailabilityId[$availabilityId])) {
185 $bookedCount = (int) $countsByAvailabilityId[$availabilityId];
186 }
187
188 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
189 $seatsReserved = (int) ($prepared['seats_reserved'] ?? 0);
190 $available = max(0, $seatsTotal - $bookedCount);
191
192 $prepared['booked_seats'] = $bookedCount;
193 $prepared['total_seats'] = $seatsTotal;
194 $prepared['available_seats'] = $available;
195 $prepared['seats_available'] = $available;
196
197 // Preserve original database status - don't override calculated status
198 // The status should reflect what's actually stored in the database
199 $original_status = $prepared['status'] ?? 'available';
200
201 // Only update status if seats are actually sold out (0 available)
202 if ($available === 0 && $original_status !== 'blocked' && $original_status !== 'closed' && $original_status !== 'cancelled') {
203 $prepared['status'] = 'sold_out';
204 }
205 // For all other cases, preserve the original database status
206 // This allows 'available', 'limited', 'blocked', 'closed', 'cancelled' to show correctly
207
208 return $prepared;
209 }, $items);
210
211 return new WP_REST_Response([
212 'dates' => $data,
213 'total' => $total,
214 'page' => $filters['page'],
215 'per_page' => $filters['per_page'],
216 ], 200);
217 } catch (\Exception $e) {
218 return new WP_Error(
219 'availability_fetch_error',
220 $e->getMessage(),
221 ['status' => 500]
222 );
223 }
224 }
225
226 public function duplicate_item(WP_REST_Request $request)
227 {
228 try {
229 $id = (int) $request->get_param('id');
230 $data = $request->get_json_params();
231
232 if (empty($data)) {
233 $data = $request->get_body_params();
234 }
235
236 $item = $this->service->duplicate($id, is_array($data) ? $data : []);
237
238 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
239 } catch (\InvalidArgumentException $e) {
240 return new WP_Error(
241 'validation_error',
242 $e->getMessage(),
243 ['status' => 400]
244 );
245 } catch (\Exception $e) {
246 return new WP_Error(
247 'availability_duplicate_error',
248 $e->getMessage(),
249 ['status' => 500]
250 );
251 }
252 }
253
254 /**
255 * Get single availability date
256 */
257 public function get_item(WP_REST_Request $request)
258 {
259 try {
260 $id = (int) $request->get_param('id');
261 $item = $this->service->getById($id);
262
263 if (!$item) {
264 return new WP_Error(
265 'availability_not_found',
266 'Availability date not found',
267 ['status' => 404]
268 );
269 }
270
271 $prepared = $this->prepare_item_for_response($item, $request);
272
273 // Compute live booked seats for this availability_id
274 if (!empty($prepared['id'])) {
275
276 // Use AvailabilityService to get booked count
277 $bookedCount = $this->service->getBookedCountByAvailabilityId((int) $prepared['id']);
278
279 $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
280
281 $available = max(0, $seatsTotal - $bookedCount);
282
283 $prepared['booked_seats'] = $bookedCount;
284 $prepared['seats_available'] = $available;
285 }
286
287 return new WP_REST_Response($prepared, 200);
288 } catch (\Exception $e) {
289 return new WP_Error(
290 'availability_fetch_error',
291 $e->getMessage(),
292 ['status' => 500]
293 );
294 }
295 }
296
297 /**
298 * Create availability date
299 */
300 public function create_item(WP_REST_Request $request)
301 {
302 try {
303 $data = $request->get_json_params();
304
305 if (empty($data)) {
306 $data = $request->get_body_params();
307 }
308
309 $item = $this->service->create($data);
310
311 // Trigger hook to sync departure capacity
312 do_action('yatra_availability_updated', $item->id);
313
314 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
315 } catch (\InvalidArgumentException $e) {
316 return new WP_Error(
317 'validation_error',
318 $e->getMessage(),
319 ['status' => 400]
320 );
321 } catch (\Exception $e) {
322 return new WP_Error(
323 'availability_create_error',
324 $e->getMessage(),
325 ['status' => 500]
326 );
327 }
328 }
329
330 /**
331 * Update availability date
332 */
333 public function update_item(WP_REST_Request $request)
334 {
335 try {
336 $id = (int) $request->get_param('id');
337 $data = $request->get_json_params();
338
339 if (empty($data)) {
340 $data = $request->get_body_params();
341 }
342
343 $item = $this->service->update($id, $data);
344
345 // Trigger hook to sync departure capacity
346 do_action('yatra_availability_updated', $id);
347
348 return new WP_REST_Response($this->prepare_item_for_response($item, $request), 200);
349 } catch (\InvalidArgumentException $e) {
350 return new WP_Error(
351 'validation_error',
352 $e->getMessage(),
353 ['status' => 400]
354 );
355 } catch (\Exception $e) {
356 return new WP_Error(
357 'availability_update_error',
358 $e->getMessage(),
359 ['status' => 500]
360 );
361 }
362 }
363
364 /**
365 * Delete availability date
366 */
367 public function delete_item(WP_REST_Request $request)
368 {
369 try {
370 $id = (int) $request->get_param('id');
371 $this->service->delete($id);
372
373 return new WP_REST_Response([
374 'message' => 'Availability date deleted successfully',
375 'id' => $id,
376 ], 200);
377 } catch (\InvalidArgumentException $e) {
378 return new WP_Error(
379 'validation_error',
380 $e->getMessage(),
381 ['status' => 400]
382 );
383 } catch (\Exception $e) {
384 return new WP_Error(
385 'availability_delete_error',
386 $e->getMessage(),
387 ['status' => 500]
388 );
389 }
390 }
391
392 /**
393 * Prepare item for response
394 */
395 protected function prepare_item_for_response($item, WP_REST_Request $request): array
396 {
397 $data = (array) $item;
398
399 // Format prices as strings for frontend
400 if (isset($data['original_price'])) {
401 $data['original_price'] = $data['original_price'] !== null ? number_format((float) $data['original_price'], 2, '.', '') : null;
402 }
403 if (isset($data['discounted_price'])) {
404 $data['discounted_price'] = $data['discounted_price'] !== null ? number_format((float) $data['discounted_price'], 2, '.', '') : null;
405 }
406 if (isset($data['discount_percentage'])) {
407 $data['discount_percentage'] = $data['discount_percentage'] !== null ? number_format((float) $data['discount_percentage'], 2, '.', '') : null;
408 }
409
410 // Ensure pricing_type has a default value
411 if (!isset($data['pricing_type']) || empty($data['pricing_type'])) {
412 $data['pricing_type'] = 'regular';
413 }
414
415 // Decode price_types JSON string from DB and ensure it's an array
416 if (isset($data['price_types']) && is_string($data['price_types'])) {
417 $decoded = json_decode($data['price_types'], true);
418 $data['price_types'] = is_array($decoded) ? $decoded : [];
419 } elseif (!isset($data['price_types']) || !is_array($data['price_types'])) {
420 $data['price_types'] = [];
421 }
422
423 // Ensure status matches frontend expectations
424 if ($data['status'] === 'blocked' || !empty($data['is_blocked'])) {
425 $data['status'] = 'blocked';
426 $data['is_blocked'] = true;
427 }
428
429 return $data;
430 }
431
432 /**
433 * Check permission
434 */
435 public function check_permission(?WP_REST_Request $request = null): bool
436 {
437 return current_user_can('manage_options');
438 }
439 }
440
441