feat: Add tier-based vehicle limit enforcement (refs #23)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

Backend:
- Add VEHICLE_LIMITS configuration to feature-tiers.ts
- Add getVehicleLimit, canAddVehicle helper functions
- Implement transaction-based limit check with FOR UPDATE locking
- Add VehicleLimitExceededError and 403 TIER_REQUIRED response
- Add countByUserId to VehiclesRepository
- Add comprehensive tests for all limit logic

Frontend:
- Add getResourceLimit, isAtResourceLimit to useTierAccess hook
- Create VehicleLimitDialog component with mobile/desktop modes
- Add useVehicleLimitCheck shared hook for limit state
- Update VehiclesPage with limit checks and lock icon
- Update VehiclesMobileScreen with limit checks
- Add tests for VehicleLimitDialog

Implements vehicle limits per tier (Free: 2, Pro: 5, Enterprise: unlimited)
with race condition prevention and consistent UX across mobile/desktop.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-11 16:36:53 -06:00
parent dff743ca36
commit 20189a1d37
15 changed files with 1179 additions and 48 deletions

View File

@@ -6,10 +6,13 @@
import React, { useTransition, useMemo } from 'react';
import { Box, Typography, Grid, Button } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import LockIcon from '@mui/icons-material/Lock';
import { useVehicles } from '../hooks/useVehicles';
import { useOptimisticVehicles } from '../hooks/useOptimisticVehicles';
import { useVehicleSearch } from '../hooks/useVehicleTransitions';
import { useVehicleLimitCheck } from '../hooks/useVehicleLimitCheck';
import { MobileVehiclesSuspense } from '../../../components/SuspenseWrappers';
import { VehicleLimitDialog } from '../../../shared-minimal/components';
import { VehicleMobileCard } from './VehicleMobileCard';
import { Vehicle } from '../types/vehicles.types';
@@ -56,6 +59,15 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
// Enhanced search with transitions (auto-syncs when vehicles change)
const { filteredVehicles } = useVehicleSearch(optimisticVehicles);
// Vehicle limit check
const {
isAtLimit,
limit,
tier,
showLimitDialog,
setShowLimitDialog,
} = useVehicleLimitCheck(safeVehicles.length);
const handleVehicleSelect = (vehicle: Vehicle) => {
// Use transition to avoid blocking UI during navigation
startTransition(() => {
@@ -63,6 +75,14 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
});
};
const handleAddVehicleClick = () => {
if (isAtLimit) {
setShowLimitDialog(true);
} else {
onAddVehicle?.();
}
};
if (isLoading) {
return (
<Box sx={{ pb: 10 }}>
@@ -92,8 +112,8 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => onAddVehicle?.()}
startIcon={isAtLimit ? <LockIcon /> : <AddIcon />}
onClick={handleAddVehicleClick}
sx={{ minWidth: 160 }}
>
Add Vehicle
@@ -119,6 +139,15 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
))}
</Grid>
</Section>
{/* Vehicle Limit Dialog */}
<VehicleLimitDialog
open={showLimitDialog}
onClose={() => setShowLimitDialog(false)}
currentCount={safeVehicles.length}
limit={limit ?? 0}
currentTier={tier}
/>
</Box>
</MobileVehiclesSuspense>
);