diff --git a/.htaccess b/.htaccess
index 8dde019..29f81da 100644
--- a/.htaccess
+++ b/.htaccess
@@ -12,3 +12,10 @@ RewriteRule .* - [F,L]
# issue found and fixed on sibling sites this session.
RewriteCond %{REQUEST_URI} ^/\.git
RewriteRule .* - [F,L]
+
+# uploads/ (customer license/insurance docs) must never be directly downloadable -
+# same Order/Deny-doesn't-work-on-OpenLiteSpeed gotcha as .git above. Docs are
+# served only through view-doc.php / admin/view-doc.php, which read by filesystem
+# path via readfile() - blocking direct URL access here doesn't break that flow.
+RewriteCond %{REQUEST_URI} ^/uploads/
+RewriteRule .* - [F,L]
diff --git a/contact.php b/contact.php
index 8d43b80..8d8c5fb 100644
--- a/contact.php
+++ b/contact.php
@@ -39,29 +39,43 @@ $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;
+// Check availability + create booking atomically. A named advisory lock (scoped to the
+// requested date range) prevents two concurrent submissions from both passing the
+// conflict check before either has inserted, which would otherwise double-book the
+// vehicle for overlapping dates.
+$lockName = 'psr_booking_' . $rentalDate . '_' . $endDate;
+$gotLock = (int) db()->query("SELECT GET_LOCK('" . addslashes($lockName) . "', 5)")->fetchColumn();
+if (!$gotLock) {
+ http_response_code(503);
+ echo json_encode(['success'=>false,'error'=>'Please try again in a moment.']); exit;
}
-// Create booking
-$ref = generateRef();
-$stmt = db()->prepare(
- "INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes)
- VALUES (?,?,?,?,?,?,?,?,?)"
-);
-$stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]);
+try {
+ $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;
+ }
+
+ // Create booking
+ $ref = generateRef();
+ $stmt = db()->prepare(
+ "INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes)
+ VALUES (?,?,?,?,?,?,?,?,?)"
+ );
+ $stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]);
+} finally {
+ db()->query("SELECT RELEASE_LOCK('" . addslashes($lockName) . "')");
+}
$dateLabel = date('F j, Y', strtotime($rentalDate));
$pkgLabel = $pkg['label'];
@@ -127,7 +141,10 @@ $sqId = null;
if ($squareToken) {
$sqResp = squareApi('POST', '/payments', [
'source_id' => $squareToken,
- 'idempotency_key' => $ref . '-dep-' . time(),
+ // Stable per booking ref (not time()-suffixed) so a network retry or
+ // accidental double-submit of this same deposit hold is recognized by
+ // Square as a duplicate instead of placing a second hold on the card.
+ 'idempotency_key' => $ref . '-dep',
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
'autocomplete' => false, // hold only — capture when confirmed
'location_id' => SQUARE_LOCATION_ID,
diff --git a/uploads/.htaccess b/uploads/.htaccess
index 1975525..23a9814 100644
--- a/uploads/.htaccess
+++ b/uploads/.htaccess
@@ -1,8 +1,5 @@
-# Block direct web access — documents served only through admin/view-doc.php
-
- Require all denied
-
-
- Order deny,allow
- Deny from all
-
+# Block direct web access - documents served only through view-doc.php / admin/view-doc.php.
+# Require all denied / Order deny,allow do NOT work on this OpenLiteSpeed setup -
+# only RewriteRule [F,L] actually blocks requests here (confirmed this session).
+RewriteEngine On
+RewriteRule .* - [F,L]