mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
Add Square deposit payment integration
- Square Web Payments SDK card element in booking form - Delayed-capture hold ($100) on booking submit — not charged until confirmed - Live payment status field: Verifying card → Authorizing → Confirmed w/ hold ID - Admin: Capture / Void / Refund actions for each booking - square_payment_id returned in API response for frontend confirmation display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+151
-7
@@ -174,6 +174,65 @@ if ($isAjax) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'square_capture') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$stmt = db()->prepare("SELECT square_payment_id FROM bookings WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$b = $stmt->fetch();
|
||||||
|
$pid = $b['square_payment_id'] ?? '';
|
||||||
|
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
|
||||||
|
$resp = squareApi('POST', "/payments/{$pid}/complete");
|
||||||
|
if (($resp['payment']['status'] ?? '') === 'COMPLETED') {
|
||||||
|
db()->prepare("UPDATE bookings SET square_payment_status='COMPLETED', deposit_paid=?, deposit_received=1 WHERE id=?")
|
||||||
|
->execute([DEPOSIT_AMOUNT, $id]);
|
||||||
|
echo json_encode(['ok'=>true,'status'=>'COMPLETED']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Capture failed']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'square_void') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$stmt = db()->prepare("SELECT square_payment_id FROM bookings WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$b = $stmt->fetch();
|
||||||
|
$pid = $b['square_payment_id'] ?? '';
|
||||||
|
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
|
||||||
|
$resp = squareApi('POST', "/payments/{$pid}/cancel");
|
||||||
|
if (($resp['payment']['status'] ?? '') === 'CANCELED') {
|
||||||
|
db()->prepare("UPDATE bookings SET square_payment_status='CANCELED' WHERE id=?")->execute([$id]);
|
||||||
|
echo json_encode(['ok'=>true,'status'=>'CANCELED']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Void failed']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'square_refund') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$stmt = db()->prepare("SELECT square_payment_id, deposit_paid FROM bookings WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$b = $stmt->fetch();
|
||||||
|
$pid = $b['square_payment_id'] ?? '';
|
||||||
|
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
|
||||||
|
$cents = (int)(((float)($b['deposit_paid'] ?: DEPOSIT_AMOUNT)) * 100);
|
||||||
|
$resp = squareApi('POST', '/refunds', [
|
||||||
|
'idempotency_key' => $pid . '-refund-' . time(),
|
||||||
|
'payment_id' => $pid,
|
||||||
|
'amount_money' => ['amount' => $cents, 'currency' => 'USD'],
|
||||||
|
'reason' => 'Security deposit refund — booking returned in good condition',
|
||||||
|
]);
|
||||||
|
if (!empty($resp['refund']['id'])) {
|
||||||
|
db()->prepare("UPDATE bookings SET square_payment_status='REFUNDED', square_refund_id=?, deposit_paid=0 WHERE id=?")
|
||||||
|
->execute([$resp['refund']['id'], $id]);
|
||||||
|
echo json_encode(['ok'=>true,'status'=>'REFUNDED']);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Refund failed']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($action === 'block_date') {
|
if ($action === 'block_date') {
|
||||||
$date = $_POST['date'] ?? '';
|
$date = $_POST['date'] ?? '';
|
||||||
$reason = substr($_POST['reason'] ?? '', 0, 200);
|
$reason = substr($_POST['reason'] ?? '', 0, 200);
|
||||||
@@ -581,24 +640,47 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 5: Deposit -->
|
<!-- Step 5: Deposit (Square-aware) -->
|
||||||
|
<?php
|
||||||
|
$sqStatus = $b['square_payment_status'] ?? '';
|
||||||
|
$sqId = $b['square_payment_id'] ?? '';
|
||||||
|
$depositIcon = 'pending';
|
||||||
|
$depositLabel = '5';
|
||||||
|
if ($cancelled) { $depositIcon='skip'; $depositLabel='—'; }
|
||||||
|
elseif ($stepDeposit ||
|
||||||
|
$sqStatus==='COMPLETED') { $depositIcon='done'; $depositLabel='✓'; }
|
||||||
|
?>
|
||||||
<div class="flow-step">
|
<div class="flow-step">
|
||||||
<div class="flow-icon <?= $stepDeposit?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-deposit_received">
|
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
|
||||||
<?= $stepDeposit?'✓':($cancelled?'—':'5') ?>
|
<?= $depositLabel ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-body">
|
<div class="flow-body">
|
||||||
<span class="flow-label">Security Deposit Received</span>
|
<span class="flow-label">Security Deposit — $<?= number_format(DEPOSIT_AMOUNT,0) ?></span>
|
||||||
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
|
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
|
||||||
<?= $stepDeposit?'Deposit received':($cancelled?'N/A':'Pending — collect at pickup') ?>
|
<?php if ($cancelled): ?>N/A
|
||||||
|
<?php elseif ($sqStatus === 'COMPLETED'): ?>Captured — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
|
||||||
|
<?php elseif ($sqStatus === 'REFUNDED'): ?>Refunded — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?>
|
||||||
|
<?php elseif ($sqStatus === 'CANCELED'): ?>Hold voided — no charge
|
||||||
|
<?php elseif ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>Hold active — card authorized, not yet charged
|
||||||
|
<?php elseif ($stepDeposit): ?>Deposit marked received (manual)
|
||||||
|
<?php else: ?>Pending — no card on file yet
|
||||||
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
<?php if (!$cancelled): ?>
|
<?php if (!$cancelled): ?>
|
||||||
<div class="flow-action">
|
<div class="flow-action" id="deposit-actions-<?= $bid ?>">
|
||||||
|
<?php if ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>
|
||||||
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_capture',this)" style="margin-right:4px">Capture $<?= number_format(DEPOSIT_AMOUNT,0) ?></button>
|
||||||
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void Hold</button>
|
||||||
|
<?php elseif ($sqStatus === 'COMPLETED'): ?>
|
||||||
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?></button>
|
||||||
|
<?php elseif (!$sqId): ?>
|
||||||
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
|
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
|
||||||
id="btn-<?= $bid ?>-deposit_received"
|
id="btn-<?= $bid ?>-deposit_received"
|
||||||
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
|
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
|
||||||
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,6 +913,68 @@ function sendReminder(id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Square payment actions (capture / void / refund) ─────────────────────────
|
||||||
|
function squareAction(id, action, btn) {
|
||||||
|
const labels = {
|
||||||
|
square_capture: ['Capture', 'Capturing…', 'Captured ✓'],
|
||||||
|
square_void: ['Void Hold', 'Voiding…', 'Voided ✓'],
|
||||||
|
square_refund: ['Refund', 'Refunding…', 'Refunded ✓'],
|
||||||
|
};
|
||||||
|
const [orig, working, done] = labels[action];
|
||||||
|
const confirmMsg = {
|
||||||
|
square_capture: 'Charge the deposit hold to this card?',
|
||||||
|
square_void: 'Void the deposit hold? The customer will NOT be charged.',
|
||||||
|
square_refund: 'Refund the full deposit to this card?',
|
||||||
|
}[action];
|
||||||
|
if (!confirm(confirmMsg)) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = working;
|
||||||
|
fetch('/admin/', {
|
||||||
|
method:'POST',
|
||||||
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||||
|
body:'action='+action+'&id='+id
|
||||||
|
}).then(r=>r.json()).then(d=>{
|
||||||
|
if (d.ok) {
|
||||||
|
btn.textContent = done;
|
||||||
|
btn.style.background = '#16a34a';
|
||||||
|
btn.style.color = '#fff';
|
||||||
|
btn.style.border = 'none';
|
||||||
|
// Update meta text and dot
|
||||||
|
const meta = document.getElementById('meta-'+id+'-deposit_received');
|
||||||
|
const icon = document.getElementById('icon-'+id+'-deposit_received');
|
||||||
|
const dot = document.getElementById('dot-'+id+'-deposit_received');
|
||||||
|
if (d.status === 'COMPLETED') {
|
||||||
|
if (meta) meta.textContent = 'Captured — deposit charged';
|
||||||
|
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
|
||||||
|
if (dot) dot.className='dot dot-done';
|
||||||
|
// Replace action area with refund button
|
||||||
|
const area = document.getElementById('deposit-actions-'+id);
|
||||||
|
if (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+id+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund Deposit</button>';
|
||||||
|
} else if (d.status === 'CANCELED') {
|
||||||
|
if (meta) meta.textContent = 'Hold voided — no charge';
|
||||||
|
if (icon) { icon.className='flow-icon skip'; icon.textContent='—'; }
|
||||||
|
if (dot) dot.className='dot dot-skip';
|
||||||
|
const area = document.getElementById('deposit-actions-'+id);
|
||||||
|
if (area) area.innerHTML = '';
|
||||||
|
} else if (d.status === 'REFUNDED') {
|
||||||
|
if (meta) meta.textContent = 'Refunded — deposit returned';
|
||||||
|
if (icon) { icon.className='flow-icon pending'; icon.textContent='↩'; }
|
||||||
|
if (dot) dot.className='dot dot-skip';
|
||||||
|
const area = document.getElementById('deposit-actions-'+id);
|
||||||
|
if (area) area.innerHTML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.textContent = orig;
|
||||||
|
btn.disabled = false;
|
||||||
|
alert('Error: ' + (d.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
}).catch(()=>{
|
||||||
|
btn.textContent = orig;
|
||||||
|
btn.disabled = false;
|
||||||
|
alert('Request failed. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Block / unblock dates ─────────────────────────────────────────────────────
|
// ── Block / unblock dates ─────────────────────────────────────────────────────
|
||||||
function blockDate(e) {
|
function blockDate(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
+52
-7
@@ -10,12 +10,13 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_
|
|||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||||
|
|
||||||
$name = trim(strip_tags($input['name'] ?? ''));
|
$name = trim(strip_tags($input['name'] ?? ''));
|
||||||
$email = trim(strip_tags($input['email'] ?? ''));
|
$email = trim(strip_tags($input['email'] ?? ''));
|
||||||
$phone = trim(strip_tags($input['phone'] ?? ''));
|
$phone = trim(strip_tags($input['phone'] ?? ''));
|
||||||
$package = trim(strip_tags($input['package'] ?? ''));
|
$package = trim(strip_tags($input['package'] ?? ''));
|
||||||
$date = trim(strip_tags($input['date'] ?? ''));
|
$date = trim(strip_tags($input['date'] ?? ''));
|
||||||
$message = trim(strip_tags($input['message'] ?? ''));
|
$message = trim(strip_tags($input['message'] ?? ''));
|
||||||
|
$squareToken = trim($input['square_token'] ?? '');
|
||||||
|
|
||||||
if (!$name || !$email || !$package || !$date) {
|
if (!$name || !$email || !$package || !$date) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
@@ -113,7 +114,51 @@ $confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans
|
|||||||
</div>
|
</div>
|
||||||
</div>";
|
</div>";
|
||||||
|
|
||||||
|
// Square deposit authorization (delayed capture — hold only, not charged yet)
|
||||||
|
$depositStatus = null;
|
||||||
|
if ($squareToken) {
|
||||||
|
$sqResp = squareApi('POST', '/payments', [
|
||||||
|
'source_id' => $squareToken,
|
||||||
|
'idempotency_key' => $ref . '-dep-' . time(),
|
||||||
|
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
|
||||||
|
'autocomplete' => false, // hold only — capture when confirmed
|
||||||
|
'location_id' => SQUARE_LOCATION_ID,
|
||||||
|
'note' => "Deposit hold — booking {$ref}",
|
||||||
|
'reference_id' => $ref,
|
||||||
|
'buyer_email_address' => $email,
|
||||||
|
]);
|
||||||
|
if (!empty($sqResp['payment']['id'])) {
|
||||||
|
$sqId = $sqResp['payment']['id'];
|
||||||
|
$sqSts = $sqResp['payment']['status']; // APPROVED
|
||||||
|
db()->prepare("UPDATE bookings SET square_payment_id=?, square_payment_status=? WHERE booking_ref=?")
|
||||||
|
->execute([$sqId, $sqSts, $ref]);
|
||||||
|
$depositStatus = $sqSts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$depositLine = $depositStatus
|
||||||
|
? "<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit Hold:</strong> \$" . number_format(DEPOSIT_AMOUNT, 2) . " authorized (not charged — released if booking is declined)</p>"
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Inject deposit line into confirmation email
|
||||||
|
$confirmHtml = str_replace(
|
||||||
|
"<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>Total:</strong> {$amountLabel}</p>{$depositLine}",
|
||||||
|
$confirmHtml
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add deposit note to admin email if applicable
|
||||||
|
if ($depositStatus) {
|
||||||
|
$adminHtml = str_replace(
|
||||||
|
"<p style='margin-top:16px;font-size:13px;color:#9ca3af'>",
|
||||||
|
"<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:8px;font-size:13px;color:#9ca3af'>",
|
||||||
|
$adminHtml
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "New Booking {$ref}: {$name} — {$pkgLabel} on {$dateLabel}", $adminHtml);
|
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);
|
sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Rentals", $confirmHtml);
|
||||||
|
|
||||||
echo json_encode(['success'=>true,'ref'=>$ref,'message'=>"Booking request received! Your reference is {$ref}. We'll be in touch shortly."]);
|
$msg = "Booking request received! Your reference is {$ref}. We'll be in touch shortly.";
|
||||||
|
if ($depositStatus) $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)$depositStatus,'square_payment_id'=>$sqId??null,'message'=>$msg]);
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ define('MAIL_FROM', 'noreply@parkerslingshotrentals.com');
|
|||||||
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
|
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
|
||||||
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
|
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
|
||||||
|
|
||||||
|
define('SQUARE_ACCESS_TOKEN', 'EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v');
|
||||||
|
define('SQUARE_APP_ID', 'sq0idp-YSM7BU9IVyOWSzpeP-0nzQ');
|
||||||
|
define('SQUARE_LOCATION_ID', 'L8GZYHYKE95CE');
|
||||||
|
define('SQUARE_VERSION', '2024-01-18');
|
||||||
|
define('DEPOSIT_AMOUNT', 100.00); // $100 refundable security deposit hold
|
||||||
|
|
||||||
define('PACKAGES', [
|
define('PACKAGES', [
|
||||||
'half-day' => ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0],
|
'half-day' => ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0],
|
||||||
'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0],
|
'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0],
|
||||||
@@ -31,6 +37,24 @@ function db(): PDO {
|
|||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function squareApi(string $method, string $path, array $body = []): array {
|
||||||
|
$ch = curl_init('https://connect.squareup.com/v2' . $path);
|
||||||
|
$headers = [
|
||||||
|
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Square-Version: ' . SQUARE_VERSION,
|
||||||
|
];
|
||||||
|
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false];
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$opts[CURLOPT_POST] = true;
|
||||||
|
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
|
||||||
|
}
|
||||||
|
curl_setopt_array($ch, $opts);
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
return json_decode($resp ?: '{}', true);
|
||||||
|
}
|
||||||
|
|
||||||
function generateRef(): string {
|
function generateRef(): string {
|
||||||
return 'PSR-' . strtoupper(substr(uniqid(), -6));
|
return 'PSR-' . strtoupper(substr(uniqid(), -6));
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-21
@@ -811,7 +811,17 @@
|
|||||||
</select>
|
</select>
|
||||||
<input type="date" name="date" required />
|
<input type="date" name="date" required />
|
||||||
<textarea name="message" placeholder="Anything else we should know? (optional)"></textarea>
|
<textarea name="message" placeholder="Anything else we should know? (optional)"></textarea>
|
||||||
<button type="submit">Send Booking Request</button>
|
|
||||||
|
<!-- Square deposit card -->
|
||||||
|
<div style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:1rem;margin-top:0.25rem">
|
||||||
|
<p style="font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:rgba(249,115,22,0.8);margin-bottom:0.5rem">Refundable Deposit — $100</p>
|
||||||
|
<p style="font-size:0.8rem;color:rgba(255,255,255,0.5);margin-bottom:0.85rem;line-height:1.5">A $100 hold will be placed on your card — <strong style="color:rgba(255,255,255,0.75)">not charged</strong> until your booking is confirmed. Released in full if declined or at return.</p>
|
||||||
|
<div id="card-container" style="min-height:44px"></div>
|
||||||
|
<p id="card-errors" style="color:#f87171;font-size:0.78rem;margin-top:0.4rem;display:none"></p>
|
||||||
|
<div id="deposit-status" style="display:none;margin-top:0.6rem;font-size:0.82rem;border-radius:6px;padding:0.5rem 0.75rem;line-height:1.5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submitBtn">Submit Booking Request</button>
|
||||||
<div class="form-msg" id="formMsg"></div>
|
<div class="form-msg" id="formMsg"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -834,6 +844,7 @@
|
|||||||
<p style="margin-top:0.5rem;">Polaris Slingshot® is a registered trademark of Polaris Inc. We are an independent rental operator.</p>
|
<p style="margin-top:0.5rem;">Polaris Slingshot® is a registered trademark of Polaris Inc. We are an independent rental operator.</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://web.squarecdn.com/v1/square.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Mobile nav ───────────────────────────────────────────────────────────────
|
// ── Mobile nav ───────────────────────────────────────────────────────────────
|
||||||
const navToggle = document.getElementById('navToggle');
|
const navToggle = document.getElementById('navToggle');
|
||||||
@@ -973,52 +984,117 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Square Web Payments ───────────────────────────────────────────────────────
|
||||||
|
let squareCard = null;
|
||||||
|
async function initSquare() {
|
||||||
|
if (!window.Square) return;
|
||||||
|
const cardEl = document.getElementById('card-container');
|
||||||
|
if (cardEl) cardEl.style.display = '';
|
||||||
|
try {
|
||||||
|
const payments = Square.payments('sq0idp-YSM7BU9IVyOWSzpeP-0nzQ', 'L8GZYHYKE95CE');
|
||||||
|
squareCard = await payments.card({
|
||||||
|
style: {
|
||||||
|
'.input-container': { borderColor: 'rgba(255,255,255,0.12)', borderRadius: '6px' },
|
||||||
|
'.input-container.is-focus': { borderColor: '#f97316' },
|
||||||
|
'.input-container.is-error': { borderColor: '#ef4444' },
|
||||||
|
'input': { color: '#ffffff', fontSize: '15px' },
|
||||||
|
'.message-text': { color: '#f87171' },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await squareCard.attach('#card-container');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Square init error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initSquare();
|
||||||
|
|
||||||
// ── Booking Form ─────────────────────────────────────────────────────────────
|
// ── Booking Form ─────────────────────────────────────────────────────────────
|
||||||
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
|
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const form = this;
|
const form = this;
|
||||||
const msg = document.getElementById('formMsg');
|
const msg = document.getElementById('formMsg');
|
||||||
const btn = form.querySelector('button[type="submit"]');
|
const btn = document.getElementById('submitBtn');
|
||||||
btn.textContent = 'Sending...';
|
const cardErrors = document.getElementById('card-errors');
|
||||||
|
const depositStatus = document.getElementById('deposit-status');
|
||||||
|
|
||||||
|
btn.textContent = 'Processing…';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
msg.className = 'form-msg';
|
msg.className = 'form-msg';
|
||||||
msg.style.display = 'none';
|
msg.style.display = 'none';
|
||||||
|
if (cardErrors) { cardErrors.style.display = 'none'; cardErrors.textContent = ''; }
|
||||||
|
|
||||||
const data = {
|
function setDepStatus(text, type) {
|
||||||
name: form.querySelector('[name="name"]').value,
|
if (!depositStatus) return;
|
||||||
email: form.querySelector('[name="email"]').value,
|
if (!text) { depositStatus.style.display = 'none'; depositStatus.textContent = ''; return; }
|
||||||
phone: form.querySelector('[name="phone"]').value,
|
depositStatus.textContent = text;
|
||||||
package: form.querySelector('[name="package"]').value,
|
depositStatus.style.display = 'block';
|
||||||
date: form.querySelector('[name="date"]').value,
|
const map = {
|
||||||
message: form.querySelector('[name="message"]').value,
|
processing: { background: 'rgba(249,115,22,0.1)', color: 'rgba(249,115,22,0.95)', border: '1px solid rgba(249,115,22,0.25)' },
|
||||||
};
|
success: { background: 'rgba(22,163,74,0.15)', color: '#86efac', border: '1px solid rgba(22,163,74,0.35)' },
|
||||||
|
error: { background: 'rgba(239,68,68,0.1)', color: '#fca5a5', border: '1px solid rgba(239,68,68,0.25)' },
|
||||||
|
};
|
||||||
|
Object.assign(depositStatus.style, map[type] || {});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/contact.php', {
|
let squareToken = null;
|
||||||
method: 'POST',
|
if (squareCard) {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
setDepStatus('Verifying card…', 'processing');
|
||||||
body: JSON.stringify(data),
|
const result = await squareCard.tokenize();
|
||||||
});
|
if (result.status !== 'OK') {
|
||||||
|
const errMsg = result.errors ? result.errors.map(x => x.message).join(', ') : 'Card error — please check your details.';
|
||||||
|
if (cardErrors) { cardErrors.textContent = errMsg; cardErrors.style.display = 'block'; }
|
||||||
|
setDepStatus('', '');
|
||||||
|
btn.textContent = 'Submit Booking Request';
|
||||||
|
btn.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
squareToken = result.token;
|
||||||
|
setDepStatus('Card verified — authorizing $100 deposit hold…', 'processing');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: form.querySelector('[name="name"]').value,
|
||||||
|
email: form.querySelector('[name="email"]').value,
|
||||||
|
phone: form.querySelector('[name="phone"]').value,
|
||||||
|
package: form.querySelector('[name="package"]').value,
|
||||||
|
date: form.querySelector('[name="date"]').value,
|
||||||
|
message: form.querySelector('[name="message"]').value,
|
||||||
|
square_token: squareToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch('/contact.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
if (json.success) {
|
if (json.success) {
|
||||||
|
if (json.deposit_held) {
|
||||||
|
const holdSuffix = json.square_payment_id ? ' · Confirmation: …' + json.square_payment_id.slice(-10).toUpperCase() : '';
|
||||||
|
setDepStatus('✓ $100 deposit hold authorized' + holdSuffix, 'success');
|
||||||
|
const cardEl = document.getElementById('card-container');
|
||||||
|
if (cardEl) cardEl.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
setDepStatus('', '');
|
||||||
|
}
|
||||||
|
|
||||||
msg.className = 'form-msg success';
|
msg.className = 'form-msg success';
|
||||||
msg.innerHTML = json.message || 'Thanks! We received your request and will be in touch soon.';
|
msg.innerHTML = 'Booking request received! Your reference is <strong>' + json.ref + '</strong>. We\'ll be in touch shortly.';
|
||||||
msg.style.display = 'block';
|
msg.style.display = 'block';
|
||||||
form.reset();
|
form.reset();
|
||||||
|
if (squareCard) { squareCard.destroy(); squareCard = null; }
|
||||||
selectedDate = null;
|
selectedDate = null;
|
||||||
if (dateInput) dateInput.min = new Date().toISOString().split('T')[0];
|
if (dateInput) dateInput.min = new Date().toISOString().split('T')[0];
|
||||||
btn.textContent = 'Request Sent!';
|
btn.textContent = 'Request Sent!';
|
||||||
btn.style.background = '#16a34a';
|
btn.style.background = '#16a34a';
|
||||||
// Refresh calendar to show newly booked date
|
|
||||||
loadCalendar(calMonth, calYear);
|
loadCalendar(calMonth, calYear);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(json.error || 'Something went wrong.');
|
throw new Error(json.error || 'Something went wrong.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setDepStatus('', '');
|
||||||
msg.className = 'form-msg error';
|
msg.className = 'form-msg error';
|
||||||
msg.textContent = err.message || 'Something went wrong. Please try again or call us directly.';
|
msg.textContent = err.message || 'Something went wrong. Please try again or call us directly.';
|
||||||
msg.style.display = 'block';
|
msg.style.display = 'block';
|
||||||
btn.textContent = 'Send Booking Request';
|
btn.textContent = 'Submit Booking Request';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user