time() + 30 * 86400, 'path' => '/', 'secure' => is_https(), 'httponly' => true, 'samesite' => 'Lax', ]); $qs = $_SERVER['QUERY_STRING'] ?? ''; redirect('/' . ($qs !== '' ? '?' . $qs : '')); } if (($_COOKIE['hggc_21'] ?? '') !== '1' && !in_array($view, ['success', 'cancel'], true)) { page_start($config['site_name']); ?>

Welcome to the club.

Hit Greens Golf Club is a social golf club for cannabis-friendly adults. You must be 21 or older to continue.

'', 'email' => '', 'phone' => '', 'slots' => 1]; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['book'])) { csrf_check(); $view = 'tee'; $_GET['id'] = $_POST['tee_time_id'] ?? 0; $old = [ 'name' => trim((string)($_POST['name'] ?? '')), 'email' => trim((string)($_POST['email'] ?? '')), 'phone' => trim((string)($_POST['phone'] ?? '')), 'slots' => (int)($_POST['slots'] ?? 1), ]; try { redirect(start_checkout((int)$_GET['id'], $old, isset($_POST['is21']))); } catch (UserError $e) { $errors[] = $e->getMessage(); } catch (Throwable $e) { error_log('HGGC checkout: ' . $e->getMessage()); $errors[] = "We couldn't start checkout just now. Please try again in a minute."; } } function start_checkout(int $teeId, array $in, bool $is21): string { global $config, $payments_ready; if (!$payments_ready) throw new UserError('Booking opens soon. Check back shortly.'); if ($in['name'] === '' || mb_strlen($in['name']) > 80) throw new UserError('Please enter your name.'); if (!filter_var($in['email'], FILTER_VALIDATE_EMAIL)) throw new UserError('Please enter a valid email address.'); if (mb_strlen($in['phone']) > 30) throw new UserError('That phone number is too long.'); if (!$is21) throw new UserError('Please confirm you are 21 or older.'); $pdo = db(); $pdo->exec('BEGIN IMMEDIATE'); try { $t = tee_time($teeId); if (!$t || tee_state($t) !== 'open') throw new UserError('Sorry, this tee time is no longer taking bookings.'); $left = spots_left($t); if ($in['slots'] < 1 || $in['slots'] > $left) { throw new UserError($left === 1 ? 'Only 1 spot is left.' : "Only $left spots are left."); } $st = $pdo->prepare("SELECT COUNT(*) FROM bookings WHERE status = 'pending' AND hold_expires_at > ? AND ip = ?"); $st->execute([time(), client_ip()]); if ($st->fetchColumn() >= 2) { throw new UserError('You already have a checkout in progress. Finish it, or wait about 30 minutes for the hold to release.'); } $token = bin2hex(random_bytes(16)); $expires_at = time() + $config['hold_minutes'] * 60; $pdo->prepare("INSERT INTO bookings (tee_time_id, token, name, email, phone, slots, amount_cents, status, hold_expires_at, ip, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)") ->execute([$teeId, $token, $in['name'], $in['email'], $in['phone'], $in['slots'], $in['slots'] * $t['price_cents'], $expires_at + 120, client_ip(), time()]); $bookingId = (int)$pdo->lastInsertId(); $pdo->exec('COMMIT'); } catch (Throwable $e) { $pdo->exec('ROLLBACK'); throw $e; } try { $session = stripe('POST', 'checkout/sessions', [ 'mode' => 'payment', 'payment_method_types' => ['card'], 'customer_email' => $in['email'], 'client_reference_id' => $bookingId, 'expires_at' => $expires_at, 'line_items' => [[ 'price' => stripe_price_for($t), 'quantity' => $in['slots'], ]], 'metadata' => ['booking_id' => $bookingId, 'tee_time_id' => $teeId], 'success_url' => base_url() . '/?view=success&session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => base_url() . '/?view=cancel&b=' . $token, ], 'hggc-booking-' . $token); } catch (Throwable $e) { db()->prepare("UPDATE bookings SET status = 'cancelled' WHERE id = ?")->execute([$bookingId]); throw $e; } db()->prepare('UPDATE bookings SET stripe_session_id = ? WHERE id = ?')->execute([$session['id'], $bookingId]); return $session['url']; } // --- Views --------------------------------------------------- $nav = 'Tee times'; if ($view === 'success') { $booking = null; $sessionId = (string)($_GET['session_id'] ?? ''); if (preg_match('/^cs_[A-Za-z0-9_]+$/', $sessionId)) { try { apply_checkout_session(stripe('GET', 'checkout/sessions/' . $sessionId)); } catch (Throwable $e) { error_log('HGGC success page: ' . $e->getMessage()); // the webhook will still confirm it } $st = db()->prepare('SELECT * FROM bookings WHERE stripe_session_id = ?'); $st->execute([$sessionId]); $booking = $st->fetch() ?: null; } $t = $booking ? tee_time((int)$booking['tee_time_id']) : null; page_start("You're in", $nav); if (!$booking || !$t): ?>

Thanks!

We couldn't find that booking. If you were charged, your spot is safe; check your email for the Stripe receipt.

Back to tee times

Booking confirmed

You're on the tee sheet.

·

spot 1 ? 's' : '' ?> · paid. A receipt is on its way to .

View your tee time

Almost there.

We're still waiting for Stripe to confirm your payment. Refresh this page in a moment.

Refresh
prepare('SELECT * FROM bookings WHERE token = ?'); $st->execute([(string)($_GET['b'] ?? '')]); $booking = $st->fetch(); if ($booking && $booking['status'] === 'pending' && $booking['stripe_session_id']) { // Close the Stripe page first so nobody can pay for a spot we've released. try { $session = stripe('GET', 'checkout/sessions/' . $booking['stripe_session_id']); if (($session['status'] ?? '') === 'open') { $session = stripe('POST', 'checkout/sessions/' . $booking['stripe_session_id'] . '/expire'); } apply_checkout_session($session); db()->prepare("UPDATE bookings SET status = 'cancelled' WHERE id = ? AND status IN ('pending', 'expired')") ->execute([$booking['id']]); } catch (Throwable $e) { error_log('HGGC cancel: ' . $e->getMessage()); // the hold expires on its own } } redirect($booking ? '/?view=tee&id=' . (int)$booking['tee_time_id'] . '&cancelled=1' : '/'); } if ($view === 'tee') { $t = tee_time((int)($_GET['id'] ?? 0)); if (!$t || $t['cancelled']) { http_response_code(404); page_start('Not found', $nav); echo '

Tee time not found.

It may have been cancelled.

' . 'See all tee times
'; page_end(); exit; } $state = tee_state($t); $left = spots_left($t); $players = players((int)$t['id']); page_start($t['course'], $nav); ?> ← All tee times

Party of

  • Open spot
:after', [':after' => time()]); page_start($config['site_name'], $nav); ?>

· Pilot

Tee times with
good company.

Meet cannabis-friendly golfers in San Diego. Grab a spot in a twosome or foursome. Each tee time stays open for a week, or until the party fills.

Preview: booking opens soon. Look around and pick your tee time.

No tee times posted yet.

New tee times are posted every week. Check back soon.

Party of
/ player