Initial commit — Parker County Slingshot Rentals booking site

Full booking system with Square card-on-file, 10-step booking flow,
pre-departure checklist, and Mailjet email integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 18:31:12 +00:00
commit 3e18d71378
13 changed files with 4334 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
<?php
/**
* Parker County Slingshot - Booking API
*/
require_once __DIR__ . '/../config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonOut(['success'=>false,'error'=>'Method not allowed'], 405);
}
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$dob = trim($_POST['dob'] ?? '');
$method = trim($_POST['payment_method'] ?? 'cash');
$notes = trim($_POST['notes'] ?? '');
$startD = trim($_POST['start_date'] ?? '');
$endD = trim($_POST['end_date'] ?? '');
// Validate required fields
if (!$name || !$email || !$phone || !$dob || !$startD || !$endD) {
jsonOut(['success'=>false,'error'=>'All fields are required.']);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonOut(['success'=>false,'error'=>'Invalid email address.']);
}
// Validate dates
$start = DateTime::createFromFormat('Y-m-d', $startD);
$end = DateTime::createFromFormat('Y-m-d', $endD);
if (!$start || !$end || $end <= $start) {
jsonOut(['success'=>false,'error'=>'Invalid dates selected.']);
}
$days = (int)$start->diff($end)->days;
$minDays = (int)(getSetting('min_days', 1));
$maxDays = (int)(getSetting('max_days', 3));
if ($days < $minDays || $days > $maxDays) {
jsonOut(['success'=>false,'error'=>"Rental must be $minDays-$maxDays days."]);
}
// Validate age (25+)
$dobDt = DateTime::createFromFormat('Y-m-d', $dob);
if (!$dobDt) jsonOut(['success'=>false,'error'=>'Invalid date of birth.']);
$age = (new DateTime())->diff($dobDt)->y;
if ($age < 25) {
jsonOut(['success'=>false,'error'=>'Must be 25 or older to rent.']);
}
// Check availability
$conflict = db()->prepare("
SELECT COUNT(*) FROM pcs_bookings
WHERE status IN ('approved','active','pending')
AND NOT (end_date <= :start OR start_date >= :end)
");
$conflict->execute(['start'=>$startD,'end'=>$endD]);
if ($conflict->fetchColumn() > 0) {
jsonOut(['success'=>false,'error'=>'Those dates are no longer available.']);
}
// Check blocked dates
$cursor = clone $start;
while ($cursor <= $end) {
$blocked = db()->prepare("SELECT id FROM pcs_blocked_dates WHERE blocked_date=?");
$blocked->execute([$cursor->format('Y-m-d')]);
if ($blocked->fetch()) {
jsonOut(['success'=>false,'error'=>'One or more selected dates are unavailable.']);
}
$cursor->modify('+1 day');
}
// Handle file uploads
$licenseFile = null;
$insuranceFile = null;
function saveUpload(string $field, string $dir, string $prefix): ?string {
if (empty($_FILES[$field]['tmp_name'])) return null;
$file = $_FILES[$field];
$allowed = ['image/jpeg','image/png','image/gif','image/webp','application/pdf'];
if (!in_array($file['type'], $allowed)) return null;
if ($file['size'] > MAX_UPLOAD_BYTES) return null;
if (!is_dir($dir)) mkdir($dir, 0755, true);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$name = $prefix.'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
$path = $dir.$name;
move_uploaded_file($file['tmp_name'], $path);
return $name;
}
$licenseFile = saveUpload('license', LICENSE_DIR, 'lic');
$insuranceFile = saveUpload('insurance', INSURANCE_DIR, 'ins');
if (!$licenseFile) {
jsonOut(['success'=>false,'error'=>"Driver's license upload failed. Check file type and size."]);
}
if (!$insuranceFile) {
jsonOut(['success'=>false,'error'=>'Insurance upload failed. Check file type and size.']);
}
// Create or find customer
$customer = db()->prepare("SELECT id FROM pcs_customers WHERE email=?");
$customer->execute([$email]);
$cust = $customer->fetch();
if ($cust) {
$custId = $cust['id'];
db()->prepare("UPDATE pcs_customers SET name=?,phone=?,dob=? WHERE id=?")
->execute([$name,$phone,$dob,$custId]);
} else {
db()->prepare("INSERT INTO pcs_customers (email,name,phone,dob) VALUES (?,?,?,?)")
->execute([$email,$name,$phone,$dob]);
$custId = db()->lastInsertId();
}
// Generate booking ref
$ref = 'PSR-'.strtoupper(bin2hex(random_bytes(3)));
$pricePerDay = (float)(getSetting('price_per_day', 150));
$total = $days * $pricePerDay;
// Get available fleet
$fleet = db()->query("SELECT id FROM pcs_fleet WHERE is_available=1 LIMIT 1")->fetch();
$fleetId = $fleet['id'] ?? null;
// Insert booking
db()->prepare("INSERT INTO pcs_bookings
(booking_ref,customer_id,fleet_id,start_date,end_date,days,price_per_day,total_amount,
payment_method,license_file,insurance_file,customer_notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")->execute([
$ref, $custId, $fleetId, $startD, $endD, $days,
$pricePerDay, $total, $method, $licenseFile, $insuranceFile, $notes
]);
jsonOut(['success'=>true,'ref'=>$ref,'message'=>'Reservation submitted successfully.']);