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 ? " ✓ " . htmlspecialchars($sqCardBrand ?? 'Card') . " •••• " . htmlspecialchars($sqCardLast4) . " on file" : ''; if ($paymentDeclined) { // ── Payment failed emails ──────────────────────────────────────────────── $adminFailHtml = "
⚠ The customer's card was declined. Please contact them to arrange an alternate payment method.
| Ref | {$ref} |
| Name | " . htmlspecialchars($name) . " |
| " . htmlspecialchars($email) . " | |
| Phone | " . htmlspecialchars($phone ?: '—') . " |
| Package | {$pkgLabel} — {$amountLabel} |
| Date | {$dateLabel} |
| Error | " . htmlspecialchars($paymentError) . " |
Contact this customer to: take payment over the phone, ask them to try a different card online, or arrange alternative payment at pickup.
Hey " . htmlspecialchars($name) . ", we received your reservation request (Ref: {$ref}) but unfortunately we weren't able to process your payment.
Payment Issue
" . htmlspecialchars($paymentError) . "
To secure your booking, please:
Your requested date is {$dateLabel} and we'll hold it for 24 hours while we sort out payment.
Ride on,
The Parker County Slingshot Team
" . ADMIN_PHONE . "
© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX
| Ref | {$ref} |
| Name | " . htmlspecialchars($name) . " |
| " . htmlspecialchars($email) . " | |
| Phone | " . htmlspecialchars($phone ?: '—') . " |
| Package | {$pkgLabel} — {$amountLabel} |
| Date | {$dateLabel} |
| Deposit Hold | {$depositLabel} (card held — not charged yet){$cardBadge} |
| Balance Due | {$balanceLabel} at pickup |
✓ \$" . number_format(DEPOSIT_AMOUNT, 2) . " deposit hold authorized (Square — not yet captured)
Submitted " . date('F j, Y g:i A') . " CT
Hey " . htmlspecialchars($name) . ", your request is in. We'll confirm availability and reach out within a few hours.
Booking Reference
{$ref}
Package: {$pkgLabel}
Requested Date: {$dateLabel}
Total: {$amountLabel}
Deposit (card hold today): {$depositLabel} ✓ Authorized{$cardBadge}
Balance due at pickup: {$balanceLabel}
Next Step: Sign Your Rental Agreement
Once your booking is confirmed you'll sign our digital waiver online — no printer needed. Your link:
Sign Rental Agreement →Questions? Call or text " . ADMIN_PHONE . " or reply to this email.
Ride on,
The Parker County Slingshot Team
© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX