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:
@@ -1,11 +1,15 @@
|
||||
import {
|
||||
TIER_LEVELS,
|
||||
FEATURE_TIERS,
|
||||
VEHICLE_LIMITS,
|
||||
getTierLevel,
|
||||
canAccessFeature,
|
||||
getRequiredTier,
|
||||
getFeatureConfig,
|
||||
getAllFeatureConfigs,
|
||||
getVehicleLimit,
|
||||
canAddVehicle,
|
||||
getVehicleLimitConfig,
|
||||
} from '../feature-tiers';
|
||||
|
||||
describe('feature-tiers', () => {
|
||||
@@ -101,4 +105,97 @@ describe('feature-tiers', () => {
|
||||
expect(FEATURE_TIERS['test' as keyof typeof FEATURE_TIERS]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('VEHICLE_LIMITS', () => {
|
||||
it('defines correct limits for each tier', () => {
|
||||
expect(VEHICLE_LIMITS.free).toBe(2);
|
||||
expect(VEHICLE_LIMITS.pro).toBe(5);
|
||||
expect(VEHICLE_LIMITS.enterprise).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVehicleLimit', () => {
|
||||
it('returns 2 for free tier', () => {
|
||||
expect(getVehicleLimit('free')).toBe(2);
|
||||
});
|
||||
|
||||
it('returns 5 for pro tier', () => {
|
||||
expect(getVehicleLimit('pro')).toBe(5);
|
||||
});
|
||||
|
||||
it('returns null for enterprise tier (unlimited)', () => {
|
||||
expect(getVehicleLimit('enterprise')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAddVehicle', () => {
|
||||
describe('free tier (limit 2)', () => {
|
||||
it('returns true when below limit', () => {
|
||||
expect(canAddVehicle('free', 0)).toBe(true);
|
||||
expect(canAddVehicle('free', 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when at limit', () => {
|
||||
expect(canAddVehicle('free', 2)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when over limit', () => {
|
||||
expect(canAddVehicle('free', 3)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pro tier (limit 5)', () => {
|
||||
it('returns true when below limit', () => {
|
||||
expect(canAddVehicle('pro', 0)).toBe(true);
|
||||
expect(canAddVehicle('pro', 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when at limit', () => {
|
||||
expect(canAddVehicle('pro', 5)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when over limit', () => {
|
||||
expect(canAddVehicle('pro', 6)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enterprise tier (unlimited)', () => {
|
||||
it('always returns true regardless of count', () => {
|
||||
expect(canAddVehicle('enterprise', 0)).toBe(true);
|
||||
expect(canAddVehicle('enterprise', 100)).toBe(true);
|
||||
expect(canAddVehicle('enterprise', 999999)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVehicleLimitConfig', () => {
|
||||
it('returns correct config for free tier', () => {
|
||||
const config = getVehicleLimitConfig('free');
|
||||
expect(config.limit).toBe(2);
|
||||
expect(config.tier).toBe('free');
|
||||
expect(config.upgradePrompt).toContain('Free tier is limited to 2 vehicles');
|
||||
expect(config.upgradePrompt).toContain('Pro');
|
||||
expect(config.upgradePrompt).toContain('Enterprise');
|
||||
});
|
||||
|
||||
it('returns correct config for pro tier', () => {
|
||||
const config = getVehicleLimitConfig('pro');
|
||||
expect(config.limit).toBe(5);
|
||||
expect(config.tier).toBe('pro');
|
||||
expect(config.upgradePrompt).toContain('Pro tier is limited to 5 vehicles');
|
||||
expect(config.upgradePrompt).toContain('Enterprise');
|
||||
});
|
||||
|
||||
it('returns correct config for enterprise tier', () => {
|
||||
const config = getVehicleLimitConfig('enterprise');
|
||||
expect(config.limit).toBeNull();
|
||||
expect(config.tier).toBe('enterprise');
|
||||
expect(config.upgradePrompt).toBeTruthy();
|
||||
});
|
||||
|
||||
it('provides default upgradePrompt fallback', () => {
|
||||
const config = getVehicleLimitConfig('enterprise');
|
||||
expect(config.upgradePrompt).toBe('Upgrade to access additional vehicles.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user