Server : Apache System : Linux webd003.cluster111.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64 User : edevmultrx ( 811899) PHP Version : 7.4.33 Disable Function : _dyuweyrj4,_dyuweyrj4r,dl Directory : /home/edevmultrx/www/terra-d-oro/api/ |
<?php
/**
* Terra D'Oro — Availability API
* Route: GET /api/availability
*
* Two operating modes:
*
* Ranges mode (initialise the calendar widget — no date params needed):
* GET ?unit_id=N or ?unit=SLUG
* → { unit_id, booked: [{check_in, check_out}], min_nights,
* max_nights, lead_days, advance_days, price_per_night, max_guests }
*
* Check mode (live availability check for a specific date range):
* GET ?unit_id=N&check_in=YYYY-MM-DD&check_out=YYYY-MM-DD
* → same base object plus: { available, nights, total, reason?, message? }
*
* Authentication: none — public endpoint for the booking widget.
* Cache headers: no-store (dates change after every sync).
*/
declare(strict_types=1);
// ── Bootstrap ─────────────────────────────────────────────────────────────────
if (!defined('ROOT_PATH')) {
define('ROOT_PATH', dirname(__DIR__));
}
require_once ROOT_PATH . '/core/config.php';
require_once ROOT_PATH . '/core/db.php';
require_once ROOT_PATH . '/core/helpers.php';
// ── Response helper ───────────────────────────────────────────────────────────
function avail_respond(int $status, array $data): never
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache');
header('X-Content-Type-Options: nosniff');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// ── Resolve unit ──────────────────────────────────────────────────────────────
$slug = trim($_GET['unit'] ?? '');
$unit_id = (int) ($_GET['unit_id'] ?? 0);
if ($slug === '' && $unit_id === 0) {
avail_respond(400, ['error' => 'Missing parameter: ?unit=SLUG or ?unit_id=ID required.']);
}
$unit = null;
if ($slug !== '') {
$unit = get_unit_by_slug($db, $slug);
} else {
$stmt = $db->prepare(
'SELECT id, slug, name, base_price, max_guests
FROM `' . TBL_UNITS . '` WHERE id = :id AND is_active = 1 LIMIT 1'
);
$stmt->execute([':id' => $unit_id]);
$unit = $stmt->fetch() ?: null;
}
if (!$unit) {
avail_respond(404, ['error' => 'Unit not found or inactive.']);
}
// ── Base response (ranges mode — always returned) ─────────────────────────────
$booked = get_booked_ranges($db, (int) $unit['id']);
$response = [
'unit_id' => (int) $unit['id'],
'unit_slug' => $unit['slug'],
'booked' => $booked,
'min_nights' => BOOKING_MIN_NIGHTS,
'max_nights' => BOOKING_MAX_NIGHTS,
'lead_days' => BOOKING_LEAD_DAYS,
'advance_days' => BOOKING_ADVANCE_DAYS,
'price_per_night' => (float) $unit['base_price'],
'max_guests' => (int) $unit['max_guests'],
];
// ── Check mode — only when both dates are provided ────────────────────────────
$check_in = trim($_GET['check_in'] ?? '');
$check_out = trim($_GET['check_out'] ?? '');
if ($check_in === '' || $check_out === '') {
avail_respond(200, $response); // Ranges-only mode — exit here
}
// Validate date format
$date_re = '/^\d{4}-\d{2}-\d{2}$/';
if (!preg_match($date_re, $check_in) || !preg_match($date_re, $check_out)) {
avail_respond(400, ['error' => 'Dates must be in YYYY-MM-DD format.']);
}
$nights = nights_between($check_in, $check_out);
$earliest = date('Y-m-d', strtotime('+' . BOOKING_LEAD_DAYS . ' day'));
$latest = date('Y-m-d', strtotime('+' . BOOKING_ADVANCE_DAYS . ' day'));
// Rule violations return 200 with available:false (not 4xx — UI uses the message)
if ($check_in < $earliest) {
avail_respond(200, array_merge($response, [
'available' => false,
'reason' => 'too_soon',
'message' => 'La date d\'arrivée est trop proche. Merci de réserver au moins '
. BOOKING_LEAD_DAYS . ' jour(s) à l\'avance.',
]));
}
if ($check_in > $latest) {
avail_respond(200, array_merge($response, [
'available' => false,
'reason' => 'too_far',
'message' => 'Cette date dépasse notre horizon de réservation ('
. BOOKING_ADVANCE_DAYS . ' jours).',
]));
}
if ($nights < BOOKING_MIN_NIGHTS) {
avail_respond(200, array_merge($response, [
'available' => false,
'reason' => 'too_short',
'message' => 'La durée minimale de séjour est de ' . BOOKING_MIN_NIGHTS . ' nuits.',
]));
}
if ($nights > BOOKING_MAX_NIGHTS) {
avail_respond(200, array_merge($response, [
'available' => false,
'reason' => 'too_long',
'message' => 'La durée maximale de séjour est de ' . BOOKING_MAX_NIGHTS . ' nuits.',
]));
}
// ── Availability check (Allen's interval algebra via is_available helper) ─────
$available = is_available($db, (int) $unit['id'], $check_in, $check_out);
$total = round($nights * (float) $unit['base_price'], 2);
avail_respond(200, array_merge($response, [
'available' => $available,
'check_in' => $check_in,
'check_out' => $check_out,
'nights' => $nights,
'total' => $total,
'reason' => $available ? null : 'occupied',
'message' => $available
? null
: 'Ces dates ne sont malheureusement plus disponibles. Veuillez sélectionner d\'autres dates.',
]));