bug: Free tier users can access Add Vehicle modal beyond vehicle limit #23
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Free tier users are allowed to open the Add Vehicle modal after reaching their vehicle limit (2 vehicles). The tier gate should be enforced at the "+ Add Vehicle" button.
Current Behavior
Expected Behavior
Frontend
UpgradeRequiredDialoginforming them they need to upgradeBackend
POST /api/vehiclesendpoint should enforce the limitTIER_REQUIREDerror format if user attempts to exceed limit via APIVehicle Limits by Tier
Implementation Notes
Feature Tier Config
Add to
backend/src/core/config/feature-tiers.ts:Note: This feature is count-based rather than tier-based, so implementation will need a helper to check current vehicle count against tier limits.
Frontend Changes
UpgradeRequiredDialoginstead of opening modal when at limitBackend Changes
Reference
See
docs/TIER-GATING.mdfor the tier gating pattern implementation.Acceptance Criteria
Out of Scope
Plan: Vehicle Limit Tier Enforcement
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Summary
Implement tier-based vehicle limits to prevent free tier users from adding vehicles beyond their limit. This requires both backend enforcement (security) and frontend UX (lock icon + upgrade dialog).
Root Cause: Current tier gating is feature-based only. Count-based limits were never implemented.
Codebase Analysis Findings
CRITICAL: Backend
POST /api/vehicleshas no limit enforcementbackend/src/features/vehicles/api/vehicles.controller.ts:63-66HIGH: No vehicle limit configuration exists
backend/src/core/config/feature-tiers.tsVEHICLE_LIMITS: { free: 2, pro: 5, enterprise: null }HIGH: Frontend Add buttons have no limit awareness
VehiclesPage.tsx:148,VehiclesMobileScreen.tsx:96HIGH:
useTierAccesshook lacks count-based methodsfrontend/src/core/hooks/useTierAccess.ts:75-82hasAccess(featureKey), not limitsArchitecture Decision
Extend existing tier system (not create separate limit system)
feature-tiers.tsImplementation Plan
Milestone 1: Backend - Limit Configuration and Helpers
Files:
backend/src/core/config/feature-tiers.tsVEHICLE_LIMITS: Record<SubscriptionTier, number | null>getVehicleLimit(tier: SubscriptionTier): number | nullcanAddVehicle(tier: SubscriptionTier, currentCount: number): booleanbackend/src/core/config/tests/feature-tiers.test.tscanAddVehicle()with various count/tier combinationsAcceptance:
getVehicleLimit('free')returns 2getVehicleLimit('pro')returns 5getVehicleLimit('enterprise')returns nullcanAddVehicle('free', 2)returns falsecanAddVehicle('pro', 4)returns truecanAddVehicle('enterprise', 100)returns trueMilestone 2: Backend - Repository and Controller Enforcement
Files:
backend/src/features/vehicles/data/vehicles.repository.tscountByUserId(userId: string): Promise<number>backend/src/features/vehicles/api/vehicles.controller.tsVEHICLE_LIMIT_EXCEEDEDerror formatbackend/src/features/vehicles/tests/unit/vehicles.controller.test.ts(update)403 Response Format:
Acceptance:
countByUserIdreturns accurate count (active vehicles only)Milestone 3: Frontend - Tier Access Hook Enhancement
Files:
frontend/src/core/hooks/useTierAccess.tsgetVehicleLimit(): number | nullmethodisAtVehicleLimit(currentCount: number): booleanmethodVEHICLE_LIMITSfrom hook or configAcceptance:
getVehicleLimit()returns tier-appropriate limitisAtVehicleLimit(2)returns true for free tierisAtVehicleLimit(2)returns false for pro/enterprise tierMilestone 4: Frontend - VehicleLimitDialog Component
Files:
frontend/src/shared-minimal/components/VehicleLimitDialog.tsx(NEW)UpgradeRequiredDialogbut for limits{ open, onClose, currentCount, limit, currentTier }Acceptance:
Milestone 5: Frontend - Desktop Add Button Limit Check
Files:
frontend/src/features/vehicles/pages/VehiclesPage.tsxuseTierAccessandVehicleLimitDialogisAtVehicleLimit(vehicles.length)before showing formVehicleLimitDialogwhen at limit and button clickedAcceptance:
Milestone 6: Frontend - Mobile Add Button Limit Check
Files:
frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsxuseTierAccessandVehicleLimitDialogonAddVehiclecallbackVehicleLimitDialogwhen at limitAcceptance:
Milestone 7: Quality Assurance and Documentation
Tasks:
npm test)npm run lint)npm run type-check)docs/TIER-GATING.mdwith vehicle limits sectionAcceptance:
Files Summary
feature-tiers.tsfeature-tiers.test.tsvehicles.repository.tsvehicles.controller.tsvehicles.controller.test.tsuseTierAccess.tsVehicleLimitDialog.tsxVehiclesPage.tsxVehiclesMobileScreen.tsxTIER-GATING.mdTotal: 10 files (9 modified, 1 new)
Risk Assessment
Verdict: AWAITING_REVIEW | Next: QR plan-completeness review
QR Review: Plan-Completeness
Phase: Plan-Review | Agent: Quality Reviewer | Status: PASS_WITH_CONCERNS
Verdict: PASS_WITH_CONCERNS
Plan is structurally sound with clear milestones and acceptance criteria. Several items need addressing before implementation.
Findings Addressed (Plan Amendments)
1. [HIGH] Missing Test Files
Issue: VehicleLimitDialog.test.tsx and vehicles.repository.test.ts not listed
Resolution: Added to plan:
vehicles.repository.test.tsVehicleLimitDialog.test.tsx2. [SHOULD_FIX] Decision Log - Alternatives Not Documented
Resolution: Alternatives considered:
3. [SHOULD_FIX] Policy Defaults - upgradePrompt Fallback
Resolution: Added to M1 acceptance:
getVehicleLimitConfig(tier)returns config with defaultupgradePromptif missing4. [SHOULD_FIX] Enterprise Unlimited Behavior
Resolution: Added to M1 acceptance:
nullexplicitly means unlimited (documented in code)canAddVehicle('enterprise', 999999)returns true@returns null for enterprise tier (unlimited vehicles)5. [SUGGESTION] Mobile Dialog UX Risk
Resolution: Added to Risk Assessment:
Updated Files Summary
feature-tiers.tsfeature-tiers.test.tsvehicles.repository.tsvehicles.repository.test.tsvehicles.controller.tsvehicles.controller.test.tsuseTierAccess.tsVehicleLimitDialog.tsxVehicleLimitDialog.test.tsxVehiclesPage.tsxVehiclesMobileScreen.tsxTIER-GATING.mdTotal: 12 files (10 modified, 2 new)
Verdict: PASS_WITH_CONCERNS (addressed) | Next: TW plan-scrub
TW Review: Plan-Scrub
Phase: Plan-Review | Agent: Technical Writer | Status: PASS (with implementation notes)
Verdict: PASS
Plan language is appropriate for planning phase. Implementation code and comments must follow Timeless Present Rule.
Implementation Guidance
When implementing, ensure all code comments follow Timeless Present Rule:
Key Documentation Points
When updating
docs/TIER-GATING.md, document behavior not history:Avoid: "Vehicle limits were added to address issue #23"
Use: "Vehicle limits are enforced per tier:
Backend returns 403
VEHICLE_LIMIT_EXCEEDEDwhen limit is reached."Code Comment Standards
For new helper functions, comment the WHY:
Verdict: PASS | Next: QR plan-code
QR Review: Plan-Code
Phase: Plan-Review | Agent: Quality Reviewer | Status: NEEDS_CHANGES
Verdict: NEEDS_CHANGES
1 CRITICAL issue and 4 HIGH conformance issues identified. Must address before implementation.
CRITICAL Finding: Race Condition
[RULE 0] Race Condition in Vehicle Limit Check
Issue: Checking count then creating vehicle in two operations allows concurrent requests to bypass limits.
Scenario:
Resolution: Add to Milestone 2 - use database transaction with row-level locking:
Acceptance (added to M2):
HIGH Findings (Conformance)
1. Error Response Format
Issue: Proposed
VEHICLE_LIMIT_EXCEEDEDformat differs from existingTIER_REQUIRED.Resolution: Align with existing format:
Frontend handles
TIER_REQUIREDconsistently; addcontextfor limit-specific data.2. Hook Method Naming
Issue: Proposed
getVehicleLimit()andisAtVehicleLimit()break feature-key abstraction.Resolution: Use resource-agnostic naming:
This allows future extension for other resources (documents, etc.).
3. Viewport Testing Sizes
Issue: "Manual testing on mobile/desktop" not specific.
Resolution: Add to M7 acceptance:
4. State Management Duplication
Issue: Limit check logic duplicated in VehiclesPage and VehiclesMobileScreen.
Resolution: Add to M5:
useVehicleLimitCheck(vehicleCount)hook{ isAtLimit, limit, showLimitDialog, setShowLimitDialog }Plan Amendments Summary
getResourceLimit,isAtResourceLimituseVehicleLimitCheckhookVerdict: NEEDS_CHANGES (addressed above) | Next: QR plan-docs
QR Review: Plan-Docs
Phase: Plan-Review | Agent: Quality Reviewer | Status: NEEDS_REVISION
Verdict: NEEDS_REVISION
Documentation requirements need to be explicit in the plan.
Required Documentation Updates
1. docs/TIER-GATING.md (CRITICAL - was missing)
Add "Vehicle Limits" section with:
VEHICLE_LIMITSconfiguration structureVehicleLimitDialog2. JSDoc Comments (Specify in M1, M3, M4)
Backend (M1):
Frontend Component (M4):
3. backend/src/features/vehicles/README.md
Update with:
Plan Amendments (Milestone 7)
Replace vague "Update tier-gating documentation" with:
Documentation Tasks:
docs/TIER-GATING.md- add "Vehicle Limits" sectionfeature-tiers.ts@ai-summary/@ai-contexttoVehicleLimitDialog.tsxbackend/src/features/vehicles/README.mdwith limits documentationDocumentation Acceptance:
@ai-summary/@ai-contextheadersVerdict: NEEDS_REVISION (addressed above) | Next: Plan review complete
Final Plan: Vehicle Limit Tier Enforcement (Consolidated)
Phase: Planning | Agent: Planner | Status: APPROVED (after review cycle)
Summary
Implement tier-based vehicle limits to prevent users from exceeding their subscription tier's vehicle allowance. Backend enforcement is mandatory (security); frontend provides UX (lock icon + upgrade dialog).
Tier Vehicle Limits
Implementation Plan (7 Milestones)
Milestone 1: Backend - Limit Configuration
Files:
backend/src/core/config/feature-tiers.tsbackend/src/core/config/tests/feature-tiers.test.tsChanges:
VEHICLE_LIMITS: Record<SubscriptionTier, number | null>getVehicleLimit(tier)with JSDoccanAddVehicle(tier, count)with JSDocgetVehicleLimitConfig(tier)returning config with defaultupgradePromptAcceptance:
getVehicleLimit('free')returns 2getVehicleLimit('enterprise')returns null (unlimited)canAddVehicle('free', 2)returns falsecanAddVehicle('enterprise', 999999)returns trueMilestone 2: Backend - Repository and Controller Enforcement
Files:
backend/src/features/vehicles/data/vehicles.repository.tsbackend/src/features/vehicles/data/vehicles.repository.test.tsbackend/src/features/vehicles/api/vehicles.controller.tsbackend/src/features/vehicles/tests/unit/vehicles.controller.test.tsChanges:
countByUserId(userId)to repositoryTIER_REQUIREDformat (aligned with existing pattern)Race Condition Prevention:
403 Response Format (aligned with TIER_REQUIRED):
Acceptance:
countByUserIdaccurate (active vehicles only)Milestone 3: Frontend - Hook Enhancement
Files:
frontend/src/core/hooks/useTierAccess.tsChanges:
getResourceLimit(resourceType: 'vehicles'): number | nullisAtResourceLimit(resourceType: 'vehicles', count: number): booleanAcceptance:
getResourceLimit('vehicles')returns tier-appropriate limitisAtResourceLimit('vehicles', 2)true for free tierMilestone 4: Frontend - VehicleLimitDialog
Files:
frontend/src/shared-minimal/components/VehicleLimitDialog.tsx(NEW)frontend/src/shared-minimal/components/VehicleLimitDialog.test.tsx(NEW)Changes:
@ai-summary/@ai-contextheaders{ open, onClose, currentCount, limit, currentTier }UpgradeRequiredDialogstylingAcceptance:
Milestone 5: Frontend - Desktop Limit Check
Files:
frontend/src/features/vehicles/pages/VehiclesPage.tsxfrontend/src/features/vehicles/hooks/useVehicleLimitCheck.ts(NEW)Changes:
useVehicleLimitCheck(vehicleCount)hook{ isAtLimit, limit, showLimitDialog, setShowLimitDialog }Acceptance:
Milestone 6: Frontend - Mobile Limit Check
Files:
frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsxChanges:
useVehicleLimitCheckhookAcceptance:
Milestone 7: Quality Assurance and Documentation
QA Tasks:
npm test- all tests passnpm run lint- no errorsnpm run type-check- no errorsDocumentation Tasks:
docs/TIER-GATING.md- add "Vehicle Limits" sectionfeature-tiers.ts@ai-summary/@ai-contexttoVehicleLimitDialog.tsxbackend/src/features/vehicles/README.mdwith limitsDocumentation Acceptance:
docs/TIER-GATING.mdincludes vehicle limits section@ai-summary/@ai-contextQA Acceptance:
Files Summary (Final)
feature-tiers.tsfeature-tiers.test.tsvehicles.repository.tsvehicles.repository.test.tsvehicles.controller.tsvehicles.controller.test.tsuseTierAccess.tsuseVehicleLimitCheck.tsVehicleLimitDialog.tsxVehicleLimitDialog.test.tsxVehiclesPage.tsxVehiclesMobileScreen.tsxTIER-GATING.mdvehicles/README.mdTotal: 14 files (11 modified, 3 new)
Risk Assessment (Final)
Review Cycle Summary
Verdict: APPROVED | Next: Create branch and begin implementation
Implementation Complete
Issue #23 has been implemented and PR #25 has been created for review.
Implementation Summary
All 7 milestones completed:
✅ Milestone 1: Backend limit configuration with VEHICLE_LIMITS constant and helper functions
✅ Milestone 2: Backend enforcement with transaction-based locking and 403 error handling
✅ Milestone 3: Frontend hook enhancement with resource limit methods
✅ Milestone 4: VehicleLimitDialog component with mobile/desktop responsiveness
✅ Milestone 5: Desktop limit check in VehiclesPage
✅ Milestone 6: Mobile limit check in VehiclesMobileScreen
✅ Milestone 7: Quality assurance and documentation
Key Features
Backend:
Frontend:
useVehicleLimitCheckhook for consistent behaviorFiles Changed
Backend (10 files):
backend/src/core/config/feature-tiers.ts- VEHICLE_LIMITS + helpersbackend/src/core/config/tests/feature-tiers.test.ts- limit testsbackend/src/features/vehicles/data/vehicles.repository.ts- countByUserIdbackend/src/features/vehicles/data/vehicles.repository.test.ts- NEWbackend/src/features/vehicles/domain/vehicles.service.ts- transaction logicbackend/src/features/vehicles/api/vehicles.controller.ts- error handlingbackend/src/features/vehicles/tests/integration/vehicles.integration.test.ts- limit testsbackend/src/features/vehicles/tests/unit/vehicles.service.test.ts- constructor fixFrontend (5 files):
frontend/src/core/hooks/useTierAccess.ts- resource limit methodsfrontend/src/features/vehicles/hooks/useVehicleLimitCheck.ts- NEWfrontend/src/shared-minimal/components/VehicleLimitDialog.tsx- NEWfrontend/src/shared-minimal/components/VehicleLimitDialog.test.tsx- NEWfrontend/src/features/vehicles/pages/VehiclesPage.tsx- desktop checksfrontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsx- mobile checksNext Steps
🤖 Generated with Claude Code