190 lines
6.7 KiB
PHP
190 lines
6.7 KiB
PHP
<?php
|
|
require_once __DIR__ . '/bootstrap.php';
|
|
require_once dirname(__DIR__) . '/include/event_mailer.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','offered')");
|
|
$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();
|
|
|
|
// Email di conferma / lista d'attesa (best-effort: non blocca la prenotazione)
|
|
try {
|
|
$bookingRow = ['email' => $email, 'name' => $fullName, 'seats' => $seats, 'status' => $status, 'token' => $token];
|
|
yse_mail_booking($bookingRow, $event);
|
|
} catch (Throwable $mailEx) {
|
|
error_log('[book mail] ' . $mailEx->getMessage());
|
|
}
|
|
|
|
$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);
|
|
}
|