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;
|
||||
}
|
||||
|
||||
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') {
|
||||
$date = $_POST['date'] ?? '';
|
||||
$reason = substr($_POST['reason'] ?? '', 0, 200);
|
||||
@@ -581,24 +640,47 @@ textarea.notes-ta:focus{border-color:#f97316}
|
||||
</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-icon <?= $stepDeposit?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-deposit_received">
|
||||
<?= $stepDeposit?'✓':($cancelled?'—':'5') ?>
|
||||
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
|
||||
<?= $depositLabel ?>
|
||||
</div>
|
||||
<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">
|
||||
<?= $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>
|
||||
<?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':'' ?>"
|
||||
id="btn-<?= $bid ?>-deposit_received"
|
||||
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
|
||||
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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 ─────────────────────────────────────────────────────
|
||||
function blockDate(e) {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user