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 ? " ✓ " . htmlspecialchars($sqCardBrand ?? 'Card') . " •••• " . htmlspecialchars($sqCardLast4) . " on file" : ''; if ($paymentDeclined) { // ── Payment failed emails ──────────────────────────────────────────────── $adminFailHtml = "

Payment Declined — Booking {$ref}

⚠ The customer's card was declined. Please contact them to arrange an alternate payment method.

Ref{$ref}
Name" . htmlspecialchars($name) . "
Email" . 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.

"; $custFailHtml = "

Parker County Slingshot Rentals

We Received Your Booking Request

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

"; 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 = "

New Booking Request — {$ref}

Ref{$ref}
Name" . htmlspecialchars($name) . "
Email" . htmlspecialchars($email) . "
Phone" . htmlspecialchars($phone ?: '—') . "
Package{$pkgLabel} — {$amountLabel}
Date{$dateLabel}
Deposit Hold{$depositLabel} (card held — not charged yet){$cardBadge}
Balance Due{$balanceLabel} at pickup
" . ($message ? "
" . nl2br(htmlspecialchars($message)) . "
" : "") . "

✓ \$" . number_format(DEPOSIT_AMOUNT, 2) . " deposit hold authorized (Square — not yet captured)

Submitted " . date('F j, Y g:i A') . " CT

"; $confirmHtml = "

Parker County Slingshot Rentals

Booking Request Received!

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

"; 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, ]);