Files
yogibook_aury_new/public/apiwp/book.php
T
2026-09-17 10:22:30 +02:00

183 lines
6.3 KiB
PHP

<?php
require_once __DIR__ . '/bootstrap.php';
/**
* Endpoint pubblico PRENOTAZIONE (POST).
* Riceve i dati dal widget WordPress e crea la prenotazione.
*
* Campi attesi (POST form-encoded o JSON):
* event_id, name, surname, email, seats, consent (1), marketing (1|0)
* website -> honeypot anti-spam (deve restare vuoto)
*
* Logica posti (conferma immediata):
* - seats <= posti rimanenti -> status 'confirmed'
* - evento pieno + lista d'attesa ON -> status 'waiting'
* - evento pieno + lista d'attesa OFF -> errore sold_out
* - 0 < rimanenti < seats -> errore 'partial' (avvisa: solo N posti)
*
* Anti-oversell: transazione + lock della riga evento (SELECT ... FOR UPDATE),
* così due prenotazioni simultanee non sforano la capienza.
*/
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
send_json(['ok' => false, 'code' => 'method', 'error' => 'Metodo non consentito.'], 405);
}
// Input: form-encoded oppure JSON
$in = $_POST;
if (empty($in)) {
$raw = file_get_contents('php://input');
if ($raw) {
$j = json_decode($raw, true);
if (is_array($j)) {
$in = $j;
}
}
}
// Honeypot: se compilato, è un bot
if (!empty($in['website'])) {
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Richiesta non valida.'], 400);
}
$eventId = (int) ($in['event_id'] ?? 0);
$name = trim((string) ($in['name'] ?? ''));
$surname = trim((string) ($in['surname'] ?? ''));
$email = trim((string) ($in['email'] ?? ''));
$seats = (int) ($in['seats'] ?? 0);
$consent = !empty($in['consent']) && $in['consent'] !== '0';
$marketing = (!empty($in['marketing']) && $in['marketing'] !== '0') ? 'Y' : 'N';
// Validazioni
if ($eventId <= 0 || $name === '' || $surname === '') {
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Compila nome, cognome ed evento.'], 400);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
send_json(['ok' => false, 'code' => 'invalid', 'error' => 'Email non valida.'], 400);
}
if (!$consent) {
send_json(['ok' => false, 'code' => 'consent', 'error' => "Devi accettare l'informativa privacy."], 400);
}
if ($seats < 1) {
$seats = 1;
}
$pdo = pdo_conn();
try {
$pdo->beginTransaction();
// Lock della riga evento: serializza le prenotazioni dello stesso evento
$es = $pdo->prepare("SELECT * FROM events WHERE id = ? FOR UPDATE");
$es->execute([$eventId]);
$event = $es->fetch(PDO::FETCH_ASSOC);
if (!$event || $event['status'] !== 'published') {
$pdo->rollBack();
send_json(['ok' => false, 'code' => 'not_available', 'error' => 'Evento non disponibile.'], 404);
}
// Prenotazioni chiuse all'orario di inizio
if (strtotime($event['starts_at']) <= time()) {
$pdo->rollBack();
send_json(['ok' => false, 'code' => 'closed', 'error' => 'Le prenotazioni per questo evento sono chiuse.'], 409);
}
$maxSeats = (int) $event['max_seats'];
$maxPerBooking = max(1, (int) $event['max_per_booking']);
$waitlistOn = ($event['waitlist_enabled'] ?? 'N') === 'Y';
if ($seats > $maxPerBooking) {
$pdo->rollBack();
send_json([
'ok' => false,
'code' => 'too_many',
'max' => $maxPerBooking,
'error' => "Puoi prenotare al massimo {$maxPerBooking} posti per prenotazione."
], 400);
}
// Posti occupati (solo confermate/pending): protetto dal lock evento
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
WHERE event_id = ? AND status IN ('confirmed','pending')");
$os->execute([$eventId]);
$occupied = (int) $os->fetchColumn();
$remaining = max(0, $maxSeats - $occupied);
// Decisione stato
if ($seats <= $remaining) {
$status = 'confirmed';
} elseif ($remaining <= 0 && $waitlistOn) {
$status = 'waiting';
} elseif ($remaining <= 0 && !$waitlistOn) {
$pdo->rollBack();
send_json(['ok' => false, 'code' => 'sold_out', 'error' => 'Evento esaurito.'], 409);
} else {
// 0 < remaining < seats: avvisa, non prenota
$pdo->rollBack();
send_json([
'ok' => false,
'code' => 'partial',
'remaining' => $remaining,
'error' => "Sono rimasti solo {$remaining} posti. Riduci il numero di persone; per gli altri potrai iscriverti alla lista d'attesa.",
], 409);
}
// Token univoco per gestione/disdetta da link email
$token = '';
for ($i = 0; $i < 5; $i++) {
$candidate = bin2hex(random_bytes(16));
$ck = $pdo->prepare("SELECT 1 FROM event_bookings WHERE token = ? LIMIT 1");
$ck->execute([$candidate]);
if (!$ck->fetchColumn()) {
$token = $candidate;
break;
}
}
if ($token === '') {
throw new RuntimeException('Impossibile generare il token.');
}
$amount = round($seats * (float) $event['cost'], 2);
$fullName = trim($name . ' ' . $surname);
$ins = $pdo->prepare("INSERT INTO event_bookings
(event_id, user_id, name, email, phone, seats, status, amount, token,
consent_at, marketing_opt_in, created_at, updated_at)
VALUES
(:event_id, NULL, :name, :email, NULL, :seats, :status, :amount, :token,
NOW(), :marketing, NOW(), NOW())");
$ins->execute([
':event_id' => $eventId,
':name' => $fullName,
':email' => $email,
':seats' => $seats,
':status' => $status,
':amount' => $amount,
':token' => $token,
':marketing' => $marketing,
]);
$pdo->commit();
// TODO Step 4: email di conferma / lista d'attesa all'iscritto (usando $token).
$msg = $status === 'confirmed'
? 'Prenotazione confermata! Ti abbiamo inviato una email di riepilogo.'
: "Sei in lista d'attesa: ti avviseremo appena si libera un posto.";
send_json([
'ok' => true,
'status' => $status,
'seats' => $seats,
'amount' => $amount,
'token' => $token,
'message' => $msg,
]);
} catch (Throwable $ex) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('[apiwp/book] ' . $ex->getMessage());
send_json(['ok' => false, 'code' => 'server', 'error' => 'Errore durante la prenotazione. Riprova.'], 500);
}