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); }