Files
parkerslingshot/contact.php
T
myron 3e18d71378 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>
2026-05-25 18:31:12 +00:00

283 lines
16 KiB
PHP

<?php
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: ' . SITE_URL . '');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; }
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$name = trim(strip_tags($input['name'] ?? ''));
$email = trim(strip_tags($input['email'] ?? ''));
$phone = trim(strip_tags($input['phone'] ?? ''));
$package = trim(strip_tags($input['package'] ?? ''));
$date = trim(strip_tags($input['date'] ?? ''));
$message = trim(strip_tags($input['message'] ?? ''));
$squareToken = trim($input['square_token'] ?? '');
if (!$name || !$email || !$package || !$date) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Name, email, package, and date are required.']); exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Invalid email address.']); exit;
}
if (!isset(PACKAGES[$package])) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Invalid package.']); exit;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) || strtotime($date) < strtotime('today')) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Invalid or past date.']); exit;
}
$pkg = PACKAGES[$package];
$rentalDate = $date;
$endDate = date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days'));
// Check availability
$conflict = db()->prepare(
"SELECT id FROM bookings
WHERE status IN ('pending','confirmed')
AND rental_date <= ? AND end_date >= ?"
);
$conflict->execute([$endDate, $rentalDate]);
if ($conflict->fetch()) {
echo json_encode(['success'=>false,'error'=>'Sorry, that date is already booked. Please choose another date.']); exit;
}
$blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?");
$blockedCheck->execute([$rentalDate, $endDate]);
if ($blockedCheck->fetch()) {
echo json_encode(['success'=>false,'error'=>'That date is unavailable. Please choose another date.']); exit;
}
$ref = generateRef();
$dateLabel = date('F j, Y', strtotime($rentalDate));
$pkgLabel = $pkg['label'];
$amountLabel = '$' . number_format($pkg['amount'], 2);
$depositLabel = '$' . number_format(DEPOSIT_AMOUNT, 2);
$balance = $pkg['amount'] - DEPOSIT_AMOUNT;
$balanceLabel = '$' . number_format($balance, 2);
// ── Square: create customer + card on file + deposit hold ─────────────────────
$sqCustomerId = null;
$sqCardId = null;
$sqCardLast4 = null;
$sqCardBrand = null;
$sqPaymentId = null;
$sqPaymentStatus = null;
$paymentDeclined = false;
$paymentError = '';
if ($squareToken) {
// 1. Create Square Customer
$custResp = squareApi('POST', '/customers', [
'idempotency_key' => $ref . '-cust',
'given_name' => $name,
'email_address' => $email,
'phone_number' => $phone ?: null,
'reference_id' => $ref,
]);
$sqCustomerId = $custResp['customer']['id'] ?? null;
// 2. Create card on file
if ($sqCustomerId) {
$cardResp = squareApi('POST', '/cards', [
'idempotency_key' => $ref . '-card',
'source_id' => $squareToken,
'card' => [
'cardholder_name' => $name,
'customer_id' => $sqCustomerId,
],
]);
$sqCardId = $cardResp['card']['id'] ?? null;
$sqCardLast4 = $cardResp['card']['last_4'] ?? null;
$sqCardBrand = $cardResp['card']['card_brand'] ?? null;
}
// 3. Deposit hold — use card_id if card was saved, else fall back to nonce
$sourceId = $sqCardId ?: $squareToken;
$payBody = [
'source_id' => $sourceId,
'idempotency_key' => $ref . '-dep-' . time(),
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
'autocomplete' => false,
'location_id' => SQUARE_LOCATION_ID,
'note' => "Deposit hold — booking {$ref}",
'reference_id' => $ref,
'buyer_email_address' => $email,
];
if ($sqCustomerId) $payBody['customer_id'] = $sqCustomerId;
$sqResp = squareApi('POST', '/payments', $payBody);
if (!empty($sqResp['payment']['id'])) {
$sqPaymentId = $sqResp['payment']['id'];
$sqPaymentStatus = $sqResp['payment']['status']; // APPROVED
} else {
// Card declined or error
$paymentDeclined = true;
$errDetail = $sqResp['errors'][0]['detail'] ?? ($sqResp['errors'][0]['code'] ?? '');
$errCode = $sqResp['errors'][0]['code'] ?? '';
$paymentError = match($errCode) {
'CARD_DECLINED', 'CARD_DECLINED_VERIFICATION_REQUIRED' => 'Your card was declined.',
'INSUFFICIENT_FUNDS' => 'Insufficient funds on card.',
'INVALID_CARD' => 'Card information is invalid.',
'EXPIRED_CARD' => 'Your card has expired.',
'CVV_FAILURE' => 'Card security code (CVV) did not match.',
'ADDRESS_VERIFICATION_FAILURE' => 'Card address verification failed.',
default => 'Payment could not be processed. Please try a different card.',
};
}
}
// ── Always create the booking (admin sees declined attempts too) ──────────────
$stmt = db()->prepare(
"INSERT INTO bookings
(booking_ref, name, email, phone, package, rental_date, end_date, amount, notes,
square_customer_id, square_card_id, card_last4, card_brand,
square_payment_id, square_payment_status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
);
$stmt->execute([
$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message,
$sqCustomerId, $sqCardId, $sqCardLast4, $sqCardBrand,
$sqPaymentId, $sqPaymentStatus,
]);
// ── Email templates ───────────────────────────────────────────────────────────
$cardBadge = $sqCardLast4
? " <span style='font-size:12px;color:#16a34a;font-weight:700'>✓ " . htmlspecialchars($sqCardBrand ?? 'Card') . " •••• " . htmlspecialchars($sqCardLast4) . " on file</span>"
: '';
if ($paymentDeclined) {
// ── Payment failed emails ────────────────────────────────────────────────
$adminFailHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#dc2626;padding:20px;text-align:center'>
<h1 style='color:#fff;margin:0;font-size:18px'>Payment Declined — Booking {$ref}</h1>
</div>
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
<p style='color:#dc2626;font-weight:700;font-size:15px'>⚠ The customer's card was declined. Please contact them to arrange an alternate payment method.</p>
<table style='width:100%;font-size:14px;margin-top:16px'>
<tr><td style='color:#6b7280;padding:6px 0;width:100px'>Ref</td><td style='padding:6px 0;font-weight:700'>{$ref}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Name</td><td style='padding:6px 0'>" . htmlspecialchars($name) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Email</td><td style='padding:6px 0'><a href='mailto:" . htmlspecialchars($email) . "'>" . htmlspecialchars($email) . "</a></td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Phone</td><td style='padding:6px 0'>" . htmlspecialchars($phone ?: '—') . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Package</td><td style='padding:6px 0;font-weight:700'>{$pkgLabel}{$amountLabel}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Date</td><td style='padding:6px 0;font-weight:700'>{$dateLabel}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Error</td><td style='padding:6px 0;color:#dc2626'>" . htmlspecialchars($paymentError) . "</td></tr>
</table>
<div style='margin-top:16px;padding:12px;background:#fef2f2;border-left:4px solid #dc2626'>
<p style='margin:0;font-size:13px;color:#991b1b'><strong>Contact this customer to:</strong> take payment over the phone, ask them to try a different card online, or arrange alternative payment at pickup.</p>
</div>
</div>
</div>";
$custFailHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#0d0d0d;padding:24px;text-align:center'>
<h1 style='color:#f97316;margin:0;font-size:20px'>Parker County Slingshot Rentals</h1>
</div>
<div style='padding:32px;background:#fff'>
<h2 style='margin-top:0;color:#0d0d0d'>We Received Your Booking Request</h2>
<p style='color:#374151'>Hey " . htmlspecialchars($name) . ", we received your reservation request (Ref: <strong>{$ref}</strong>) but unfortunately we weren't able to process your payment.</p>
<div style='margin:20px 0;padding:16px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px'>
<p style='margin:0 0 8px;font-weight:700;color:#991b1b'>Payment Issue</p>
<p style='margin:0;font-size:14px;color:#374151'>" . htmlspecialchars($paymentError) . "</p>
</div>
<p style='color:#374151'>To secure your booking, please:</p>
<ul style='color:#374151;font-size:14px;line-height:1.8;padding-left:20px'>
<li>Reply to this email with a preferred callback time</li>
<li>Call or text us at <strong>" . ADMIN_PHONE . "</strong></li>
<li>Try again online at <a href='" . SITE_URL . "' style='color:#f97316'>parkerslingshot.epictravelexpeditions.com</a> with a different card</li>
</ul>
<p style='color:#374151'>Your requested date is <strong>{$dateLabel}</strong> and we'll hold it for 24 hours while we sort out payment.</p>
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong><br>" . ADMIN_PHONE . "</p>
</div>
<div style='background:#f3f4f6;padding:16px;text-align:center'>
<p style='margin:0;font-size:12px;color:#9ca3af'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "⚠ Payment Declined — {$ref}: {$name} ({$pkgLabel})", $adminFailHtml);
sendEmail($email, $name, "About Your Booking Request {$ref} — Parker County Slingshot Rentals", $custFailHtml);
echo json_encode([
'success' => false,
'payment_failed' => true,
'ref' => $ref,
'error' => $paymentError,
'contact_phone' => ADMIN_PHONE,
'contact_email' => ADMIN_EMAIL,
'message' => "We received your request (Ref: {$ref}) but couldn't process your card. Please call or text us at " . ADMIN_PHONE . " or reply to the confirmation email we sent.",
]);
exit;
}
// ── Payment succeeded — send standard confirmation emails ─────────────────────
$adminHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#f97316;padding:20px;text-align:center'>
<h1 style='color:#fff;margin:0;font-size:20px'>New Booking Request — {$ref}</h1>
</div>
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
<table style='width:100%;font-size:15px'>
<tr><td style='color:#6b7280;padding:8px 0;width:110px'>Ref</td><td style='padding:8px 0;font-weight:700'>{$ref}</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Name</td><td style='padding:8px 0'>" . htmlspecialchars($name) . "</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Email</td><td style='padding:8px 0'>" . htmlspecialchars($email) . "</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Phone</td><td style='padding:8px 0'>" . htmlspecialchars($phone ?: '—') . "</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Package</td><td style='padding:8px 0;font-weight:700;color:#f97316'>{$pkgLabel}{$amountLabel}</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Date</td><td style='padding:8px 0;font-weight:700'>{$dateLabel}</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Deposit Hold</td><td style='padding:8px 0'>{$depositLabel} (card held — not charged yet){$cardBadge}</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Balance Due</td><td style='padding:8px 0;font-weight:700;color:#16a34a'>{$balanceLabel} at pickup</td></tr>
</table>
" . ($message ? "<div style='margin-top:12px;padding:12px;background:#fff7ed;border-left:4px solid #f97316'>" . nl2br(htmlspecialchars($message)) . "</div>" : "") . "
<p style='margin-top:8px;font-size:13px;color:#16a34a;font-weight:700'>✓ \$" . number_format(DEPOSIT_AMOUNT, 2) . " deposit hold authorized (Square — not yet captured)</p>
<p style='margin-top:4px;font-size:13px;color:#9ca3af'>Submitted " . date('F j, Y g:i A') . " CT</p>
</div>
</div>";
$confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#0d0d0d;padding:24px;text-align:center'>
<h1 style='color:#f97316;margin:0;font-size:20px'>Parker County Slingshot Rentals</h1>
</div>
<div style='padding:32px;background:#fff'>
<h2 style='margin-top:0;color:#0d0d0d'>Booking Request Received!</h2>
<p style='color:#374151'>Hey " . htmlspecialchars($name) . ", your request is in. We'll confirm availability and reach out within a few hours.</p>
<div style='background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:20px;margin:20px 0'>
<p style='margin:0 0 6px;font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Booking Reference</p>
<p style='margin:0 0 16px;font-size:22px;font-weight:700;color:#f97316'>{$ref}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Package:</strong> {$pkgLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Requested Date:</strong> {$dateLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amountLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit (card hold today):</strong> {$depositLabel} <span style='font-size:12px;color:#16a34a;font-weight:700'>✓ Authorized</span>{$cardBadge}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Balance due at pickup:</strong> <span style='font-weight:700;color:#16a34a'>{$balanceLabel}</span></p>
</div>
<div style='margin:20px 0;padding:16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;text-align:center'>
<p style='margin:0 0 10px;font-size:14px;font-weight:700;color:#111'>Next Step: Sign Your Rental Agreement</p>
<p style='margin:0 0 14px;font-size:13px;color:#6b7280'>Once your booking is confirmed you'll sign our digital waiver online — no printer needed. Your link:</p>
<a href='" . SITE_URL . "/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement &rarr;</a>
</div>
<p style='color:#374151'>Questions? Call or text <strong>" . ADMIN_PHONE . "</strong> or reply to this email.</p>
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
</div>
<div style='background:#f3f4f6;padding:16px;text-align:center'>
<p style='margin:0;font-size:12px;color:#9ca3af'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "New Booking {$ref}: {$name}{$pkgLabel} on {$dateLabel}", $adminHtml);
sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Rentals", $confirmHtml);
$msg = "Booking request received! Your reference is {$ref}. We'll be in touch shortly.";
if ($sqPaymentId) $msg .= " A \$" . number_format(DEPOSIT_AMOUNT, 2) . " refundable deposit hold has been placed on your card.";
echo json_encode([
'success' => true,
'ref' => $ref,
'deposit_held' => (bool)$sqPaymentId,
'square_payment_id' => $sqPaymentId,
'message' => $msg,
]);