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
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:
@@ -33,6 +33,15 @@ const TIER_LEVELS: Record<SubscriptionTier, number> = {
|
||||
enterprise: 2,
|
||||
};
|
||||
|
||||
// Resource limits per tier (mirrors backend VEHICLE_LIMITS)
|
||||
const RESOURCE_LIMITS = {
|
||||
vehicles: {
|
||||
free: 2,
|
||||
pro: 5,
|
||||
enterprise: null, // unlimited
|
||||
} as Record<SubscriptionTier, number | null>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if user can access tier-gated features
|
||||
* Fetches user profile for tier and feature config from backend
|
||||
@@ -100,11 +109,47 @@ export const useTierAccess = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get resource limit for current user's tier
|
||||
* Resource-agnostic method for count-based limits (vehicles, documents, etc.)
|
||||
*
|
||||
* @param resourceType - Type of resource (e.g., 'vehicles')
|
||||
* @returns Maximum allowed count, or null for unlimited
|
||||
*/
|
||||
const getResourceLimit = (resourceType: keyof typeof RESOURCE_LIMITS): number | null => {
|
||||
const limits = RESOURCE_LIMITS[resourceType];
|
||||
if (!limits) {
|
||||
return null; // Unknown resource type = unlimited
|
||||
}
|
||||
return limits[tier] ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user is at or over their resource limit
|
||||
* Resource-agnostic method for count-based limits (vehicles, documents, etc.)
|
||||
*
|
||||
* @param resourceType - Type of resource (e.g., 'vehicles')
|
||||
* @param currentCount - Current number of resources user has
|
||||
* @returns true if user is at or over limit, false if under limit or unlimited
|
||||
*/
|
||||
const isAtResourceLimit = (
|
||||
resourceType: keyof typeof RESOURCE_LIMITS,
|
||||
currentCount: number
|
||||
): boolean => {
|
||||
const limit = getResourceLimit(resourceType);
|
||||
if (limit === null) {
|
||||
return false; // Unlimited
|
||||
}
|
||||
return currentCount >= limit;
|
||||
};
|
||||
|
||||
return {
|
||||
tier,
|
||||
loading: profileQuery.isLoading || featureConfigQuery.isLoading,
|
||||
hasAccess,
|
||||
checkAccess,
|
||||
getResourceLimit,
|
||||
isAtResourceLimit,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user