Files

289 lines
17 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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];
$rentalDays = ($package === 'full-day') ? max(1, min(3, (int)($input['rental_days'] ?? 1))) : 1;
$rentalDate = $date;
$endDate = ($package === 'full-day')
? date('Y-m-d', strtotime($date . ' +' . ($rentalDays - 1) . ' days'))
: date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days'));
$totalAmount = ($package === 'full-day') ? $pkg['amount'] * $rentalDays : $pkg['amount'];
// 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 = ($package === 'full-day' && $rentalDays > 1)
? $pkg['label'] . ' × ' . $rentalDays . ' days'
: $pkg['label'];
$amountLabel = '$' . number_format($totalAmount, 2);
$depositLabel = '$' . number_format(DEPOSIT_AMOUNT, 2);
$balance = $totalAmount - 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, $totalAmount, $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,
]);