# yatra/3.0.5/app/Controllers/AvailabilityController.php

Yatra – Travel Booking &amp; Tour Operator Software, version 3.0.5. 461 lines.

- Page: https://pluginprobe.com/plugins/yatra/3.0.5/code/app/Controllers/AvailabilityController.php
- Raw: https://pluginprobe.com/plugins/yatra/3.0.5/raw/app/Controllers/AvailabilityController.php
- Modified: 2026-05-25T04:28:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/yatra/3.0.5/code/app/Controllers/AvailabilityController.php#L10-L20`.

```php
<?php
/**
 * Availability REST API Controller
 * API endpoints for trip availability dates management
 * 
 * This is a FREE feature - no Pro plugin required
 * 
 * @package Yatra\Controllers
 * @since 3.0.0
 */

declare(strict_types=1);

namespace Yatra\Controllers;

use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Yatra\Database\Tables\BookingsTable;
use Yatra\Services\AvailabilityService;
use Yatra\Repositories\AvailabilityRepository;

class AvailabilityController extends BaseController
{
    private AvailabilityService $service;

    public function __construct()
    {
        $this->service = new AvailabilityService(new AvailabilityRepository());
    }

    /**
     * Register routes
     */
    public function register_routes(): void
    {
        $namespace = 'yatra/v1';
        $base = 'availability';

        // Collection routes — view cap for reads, edit cap for writes.
        register_rest_route($namespace, '/' . $base, [
            [
                'methods' => \WP_REST_Server::READABLE,
                'callback' => [$this, 'get_items'],
                'permission_callback' => [$this, 'check_view_permission'],
                'args' => [
                    'trip_id' => [
                        'required' => true,
                        'type' => 'integer',
                        'validate_callback' => function ($param) {
                            return is_numeric($param) && $param > 0;
                        },
                    ],
                    'status' => [
                        'type' => 'string',
                        'default' => 'all',
                    ],
                    'month' => [
                        'type' => 'string',
                        'default' => 'all',
                    ],
                    'search' => [
                        'type' => 'string',
                        'default' => '',
                    ],
                    'page' => [
                        'type' => 'integer',
                        'default' => 1,
                        'minimum' => 1,
                    ],
                    'per_page' => [
                        'type' => 'integer',
                        'default' => 50,
                        'minimum' => 1,
                        'maximum' => 100,
                    ],
                ],
            ],
            [
                'methods' => \WP_REST_Server::CREATABLE,
                'callback' => [$this, 'create_item'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        // Single item routes — view cap for read, edit cap for the
        // mutations. Delete uses edit as well — there's no separate
        // "delete availability date" cap in the registry because
        // removing a date is functionally part of trip availability
        // editing, not a destructive operation in its own right.
        register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
            [
                'methods' => \WP_REST_Server::READABLE,
                'callback' => [$this, 'get_item'],
                'permission_callback' => [$this, 'check_view_permission'],
            ],
            [
                'methods' => \WP_REST_Server::EDITABLE,
                'callback' => [$this, 'update_item'],
                'permission_callback' => [$this, 'check_permission'],
            ],
            [
                'methods' => \WP_REST_Server::DELETABLE,
                'callback' => [$this, 'delete_item'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);

        register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/duplicate', [
            [
                'methods' => \WP_REST_Server::CREATABLE,
                'callback' => [$this, 'duplicate_item'],
                'permission_callback' => [$this, 'check_permission'],
            ],
        ]);
    }

    /**
     * Get all availability dates for a trip
     */
    public function get_items(WP_REST_Request $request)
    {
        try {
            $tripId = (int) $request->get_param('trip_id');
            
            if ($tripId <= 0) {
                return new WP_Error(
                    'invalid_trip_id',
                    'Valid trip_id is required',
                    ['status' => 400]
                );
            }

            $filters = [
                'status' => $request->get_param('status') ?? 'all',
                'month' => $request->get_param('month') ?? 'all',
                'search' => $request->get_param('search') ?? '',
                'page' => (int) ($request->get_param('page') ?? 1),
                'per_page' => (int) ($request->get_param('per_page') ?? 50),
            ];

            $items = $this->service->getByTripId($tripId, $filters);
            $total = $this->service->countByTripId($tripId, $filters);

            global $wpdb;
            $bookingsTable = BookingsTable::getTableName();

            $availabilityIdByDate = [];
            $dateCounts = [];
            foreach ($items as $item) {
                $date = (string) ($item->departure_date ?? '');
                if ($date === '') {
                    continue;
                }
                $dateCounts[$date] = ($dateCounts[$date] ?? 0) + 1;
                $availabilityIdByDate[$date] = (int) ($item->id ?? 0);
            }

            foreach ($dateCounts as $date => $count) {
                if ($count !== 1) {
                    unset($availabilityIdByDate[$date]);
                }
            }

            // Use AvailabilityService to update booking availability IDs
            if (!empty($availabilityIdByDate)) {
                $this->service->updateBookingAvailabilityIds((int) $tripId, $availabilityIdByDate);
            }

            // Aggregate bookings count per availability date for this trip
            // Use AvailabilityService to get booking counts
            $countsByAvailabilityId = [];
            $bookingCounts = $this->service->getBookingCountsByAvailabilityIds(array_column($items, 'id'));
            
            foreach ($bookingCounts as $row) {
                $aid = (int) ($row->availability_id ?? 0);
                if ($aid > 0) {
                    $countsByAvailabilityId[$aid] = (int) ($row->booked_count ?? 0);
                }
            }

            $data = array_map(function ($item) use ($request, $countsByAvailabilityId) {
                $prepared = $this->prepare_item_for_response($item, $request);

                $availabilityId = (int) ($prepared['id'] ?? 0);
                $bookedCount = 0;

                if ($availabilityId > 0 && isset($countsByAvailabilityId[$availabilityId])) {
                    $bookedCount = (int) $countsByAvailabilityId[$availabilityId];
                }

                $seatsTotal = (int) ($prepared['seats_total'] ?? 0);
                $seatsReserved = (int) ($prepared['seats_reserved'] ?? 0);
                $available = max(0, $seatsTotal - $bookedCount);

                $prepared['booked_seats'] = $bookedCount;
                $prepared['total_seats'] = $seatsTotal;
                $prepared['available_seats'] = $available;
                $prepared['seats_available'] = $available;

                // Preserve original database status - don't override calculated status
                // The status should reflect what's actually stored in the database
                $original_status = $prepared['status'] ?? 'available';
                
                // Only update status if seats are actually sold out (0 available)
                if ($available === 0 && $original_status !== 'blocked' && $original_status !== 'closed' && $original_status !== 'cancelled') {
                    $prepared['status'] = 'sold_out';
                }
                // For all other cases, preserve the original database status
                // This allows 'available', 'limited', 'blocked', 'closed', 'cancelled' to show correctly

                return $prepared;
            }, $items);

            return new WP_REST_Response([
                'dates' => $data,
                'total' => $total,
                'page' => $filters['page'],
                'per_page' => $filters['per_page'],
            ], 200);
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    public function duplicate_item(WP_REST_Request $request)
    {
        try {
            $id = (int) $request->get_param('id');
            $data = $request->get_json_params();

            if (empty($data)) {
                $data = $request->get_body_params();
            }

            $item = $this->service->duplicate($id, is_array($data) ? $data : []);

            return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
        } catch (\InvalidArgumentException $e) {
            return new WP_Error(
                'validation_error',
                $e->getMessage(),
                ['status' => 400]
            );
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_duplicate_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Get single availability date
     */
    public function get_item(WP_REST_Request $request)
    {
        try {
            $id = (int) $request->get_param('id');
            $item = $this->service->getById($id);

            if (!$item) {
                return new WP_Error(
                    'availability_not_found',
                    'Availability date not found',
                    ['status' => 404]
                );
            }

            $prepared = $this->prepare_item_for_response($item, $request);

            // Compute live booked seats for this availability_id
            if (!empty($prepared['id'])) {

                // Use AvailabilityService to get booked count
                $bookedCount = $this->service->getBookedCountByAvailabilityId((int) $prepared['id']);

                $seatsTotal = (int) ($prepared['seats_total'] ?? 0);

                $available = max(0, $seatsTotal - $bookedCount);

                $prepared['booked_seats'] = $bookedCount;
                $prepared['seats_available'] = $available;
            }

            return new WP_REST_Response($prepared, 200);
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Create availability date
     */
    public function create_item(WP_REST_Request $request)
    {
        try {
            $data = $request->get_json_params();
            
            if (empty($data)) {
                $data = $request->get_body_params();
            }

            $item = $this->service->create($data);
            
            // Trigger hook to sync departure capacity
            do_action('yatra_availability_updated', $item->id);

            return new WP_REST_Response($this->prepare_item_for_response($item, $request), 201);
        } catch (\InvalidArgumentException $e) {
            return new WP_Error(
                'validation_error',
                $e->getMessage(),
                ['status' => 400]
            );
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_create_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Update availability date
     */
    public function update_item(WP_REST_Request $request)
    {
        try {
            $id = (int) $request->get_param('id');
            $data = $request->get_json_params();
            
            if (empty($data)) {
                $data = $request->get_body_params();
            }

            $item = $this->service->update($id, $data);
            
            // Trigger hook to sync departure capacity
            do_action('yatra_availability_updated', $id);

            return new WP_REST_Response($this->prepare_item_for_response($item, $request), 200);
        } catch (\InvalidArgumentException $e) {
            return new WP_Error(
                'validation_error',
                $e->getMessage(),
                ['status' => 400]
            );
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_update_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Delete availability date
     */
    public function delete_item(WP_REST_Request $request)
    {
        try {
            $id = (int) $request->get_param('id');
            $this->service->delete($id);

            return new WP_REST_Response([
                'message' => 'Availability date deleted successfully',
                'id' => $id,
            ], 200);
        } catch (\InvalidArgumentException $e) {
            return new WP_Error(
                'validation_error',
                $e->getMessage(),
                ['status' => 400]
            );
        } catch (\Exception $e) {
            return new WP_Error(
                'availability_delete_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Prepare item for response
     */
    protected function prepare_item_for_response($item, WP_REST_Request $request): array
    {
        $data = (array) $item;
        
        // Format prices as strings for frontend
        if (isset($data['original_price'])) {
            $data['original_price'] = $data['original_price'] !== null ? number_format((float) $data['original_price'], 2, '.', '') : null;
        }
        if (isset($data['discounted_price'])) {
            $data['discounted_price'] = $data['discounted_price'] !== null ? number_format((float) $data['discounted_price'], 2, '.', '') : null;
        }
        if (isset($data['discount_percentage'])) {
            $data['discount_percentage'] = $data['discount_percentage'] !== null ? number_format((float) $data['discount_percentage'], 2, '.', '') : null;
        }
        
        // Ensure pricing_type has a default value
        if (!isset($data['pricing_type']) || empty($data['pricing_type'])) {
            $data['pricing_type'] = 'regular';
        }
        
        // Decode price_types JSON string from DB and ensure it's an array
        if (isset($data['price_types']) && is_string($data['price_types'])) {
            $decoded = json_decode($data['price_types'], true);
            $data['price_types'] = is_array($decoded) ? $decoded : [];
        } elseif (!isset($data['price_types']) || !is_array($data['price_types'])) {
            $data['price_types'] = [];
        }
        
        // Ensure status matches frontend expectations
        if ($data['status'] === 'blocked' || !empty($data['is_blocked'])) {
            $data['status'] = 'blocked';
            $data['is_blocked'] = true;
        }
        
        return $data;
    }

    /**
     * Check permission
     */
    /**
     * Write permission — trip-edits cap. Adding, updating, deleting,
     * and duplicating availability dates all mutate trip data, so the
     * registered `yatra_edit_trips` cap is the right gate. WP admins
     * pass via the Team module's admin-fallback filter.
     */
    public function check_permission(?WP_REST_Request $request = null): bool
    {
        return current_user_can('yatra_edit_trips');
    }

    /**
     * Read permission — view-trips cap. Listing availability dates is
     * a read-only operation against trip data; Sales Agent / Front
     * Desk / Guide / Accountant / Auditor roles all hold this.
     */
    public function check_view_permission(?WP_REST_Request $request = null): bool
    {
        return current_user_can('yatra_view_trips');
    }
}


```
