fix: review #19 — offer accept race condition (FOR UPDATE), geo search HAVING→subquery, count query
Universal: Auto Version Bump / Version Bump (push) Successful in 12s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 17s

This commit is contained in:
Jonathan Miller
2026-06-21 17:55:12 -05:00
parent d53a5335f0
commit e89467b6b7
2 changed files with 43 additions and 20 deletions
@@ -60,27 +60,37 @@ class OfferHelper
{
$db = Factory::getContainer()->get(DatabaseInterface::class);
$db->setQuery($db->getQuery(true)
->select('listing_id')
->from('#__mokosuiterealty_offers')
->where('id = ' . (int) $offerId));
$listingId = (int) $db->loadResult();
if (!$listingId) {
return (object) ['success' => false, 'error' => 'Offer not found'];
}
$db->transactionStart();
try {
// Lock and verify the offer is still in submitted status
$db->setQuery(
'SELECT listing_id FROM #__mokosuiterealty_offers'
. ' WHERE id = ' . (int) $offerId
. ' AND ' . $db->quoteName('status') . ' = ' . $db->quote('submitted')
. ' FOR UPDATE'
);
$listingId = (int) $db->loadResult();
if (!$listingId) {
$db->transactionRollback();
return (object) ['success' => false, 'error' => 'Offer not found or already processed'];
}
// Accept this offer
$db->setQuery($db->getQuery(true)
->update('#__mokosuiterealty_offers')
->set($db->quoteName('status') . ' = ' . $db->quote('accepted'))
->set('accepted_at = ' . $db->quote(Factory::getDate()->toSql()))
->where('id = ' . (int) $offerId));
->where('id = ' . (int) $offerId)
->where($db->quoteName('status') . ' = ' . $db->quote('submitted')));
$db->execute();
if ($db->getAffectedRows() === 0) {
$db->transactionRollback();
return (object) ['success' => false, 'error' => 'Offer already accepted or rejected'];
}
// Reject all other offers on this listing
$db->setQuery($db->getQuery(true)
->update('#__mokosuiterealty_offers')
@@ -90,11 +100,12 @@ class OfferHelper
->where($db->quoteName('status') . ' = ' . $db->quote('submitted')));
$db->execute();
// Update listing to pending
// Update listing to pending (only if still active)
$db->setQuery($db->getQuery(true)
->update('#__mokosuiterealty_listings')
->set($db->quoteName('status') . ' = ' . $db->quote('pending'))
->where('id = ' . $listingId));
->where('id = ' . $listingId)
->where($db->quoteName('status') . ' = ' . $db->quote('active')));
$db->execute();
$db->transactionCommit();
@@ -45,21 +45,33 @@ class PropertySearchHelper
$radius = (float) $filters['radius_miles'];
$query->select("(3959 * ACOS(COS(RADIANS({$lat})) * COS(RADIANS(l.latitude)) * COS(RADIANS(l.longitude) - RADIANS({$lng})) + SIN(RADIANS({$lat})) * SIN(RADIANS(l.latitude)))) AS distance");
$query->having('distance <= ' . $radius);
$query->order('distance ASC');
// Bounding box pre-filter for index usage
$latDelta = $radius / 69.0;
$lngDelta = $radius / (69.0 * cos(deg2rad($lat)));
$query->where('l.latitude BETWEEN ' . ($lat - $latDelta) . ' AND ' . ($lat + $latDelta));
$query->where('l.longitude BETWEEN ' . ($lng - $lngDelta) . ' AND ' . ($lng + $lngDelta));
// Wrap in subquery to filter by computed distance
$db->setQuery('SELECT * FROM (' . $query . ') AS geo WHERE distance <= ' . $radius . ' ORDER BY distance ASC', $offset, min(max(1, $limit), 100));
} else {
$query->order('l.listed_at DESC');
$db->setQuery($query, $offset, min(max(1, $limit), 100));
}
// Count total before limit
$countQuery = clone $query;
$countQuery->clear('select')->clear('order')->select('COUNT(DISTINCT l.id)');
$db->setQuery($query, $offset, min(max(1, $limit), 100));
$results = $db->loadObjectList() ?: [];
// Get total count (without pagination)
$countQuery = $db->getQuery(true)
->select('COUNT(*)')
->from($db->quoteName('#__mokosuiterealty_listings', 'l'))
->where($db->quoteName('l.status') . ' = ' . $db->quote('active'));
$db->setQuery($countQuery);
$total = (int) $db->loadResult();
return (object) [
'listings' => $results,
'total' => $total,
'count' => count($results),
'offset' => $offset,
'limit' => $limit,