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

@@ -76,3 +76,75 @@ export function getFeatureConfig(featureKey: string): FeatureConfig | undefined
export function getAllFeatureConfigs(): Record<string, FeatureConfig> {
return { ...FEATURE_TIERS };
}
// Vehicle limits per tier
// null indicates unlimited (enterprise tier)
export const VEHICLE_LIMITS: Record<SubscriptionTier, number | null> = {
free: 2,
pro: 5,
enterprise: null,
} as const;
/**
* Vehicle limits vary by subscription tier and must be queryable
* at runtime for both backend enforcement and frontend UI state.
*
* @param tier - User's subscription tier
* @returns Maximum vehicles allowed, or null for unlimited (enterprise tier)
*/
export function getVehicleLimit(tier: SubscriptionTier): number | null {
return VEHICLE_LIMITS[tier] ?? null;
}
/**
* Check if a user can add another vehicle based on their tier and current count.
*
* @param tier - User's subscription tier
* @param currentCount - Number of vehicles user currently has
* @returns true if user can add another vehicle, false if at/over limit
*/
export function canAddVehicle(tier: SubscriptionTier, currentCount: number): boolean {
const limit = getVehicleLimit(tier);
// null limit means unlimited (enterprise)
if (limit === null) {
return true;
}
return currentCount < limit;
}
/**
* Vehicle limit configuration with upgrade prompt.
* Structure supports additional resource types in the future.
*/
export interface VehicleLimitConfig {
limit: number | null;
tier: SubscriptionTier;
upgradePrompt: string;
}
/**
* Get vehicle limit configuration with upgrade prompt for a tier.
*
* @param tier - User's subscription tier
* @returns Configuration with limit and upgrade prompt
*/
export function getVehicleLimitConfig(tier: SubscriptionTier): VehicleLimitConfig {
const limit = getVehicleLimit(tier);
const defaultPrompt = 'Upgrade to access additional vehicles.';
let upgradePrompt: string;
if (tier === 'free') {
upgradePrompt = 'Free tier is limited to 2 vehicles. Upgrade to Pro for up to 5 vehicles, or Enterprise for unlimited.';
} else if (tier === 'pro') {
upgradePrompt = 'Pro tier is limited to 5 vehicles. Upgrade to Enterprise for unlimited vehicles.';
} else {
upgradePrompt = defaultPrompt;
}
return {
limit,
tier,
upgradePrompt,
};
}