fix: filter locked vehicles after tier downgrade selection (refs #60)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 2m38s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 29s
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

- GET /api/vehicles now uses getUserVehiclesWithTierStatus() and filters
  out vehicles with tierStatus='locked' so only selected vehicles appear
  in the vehicle list
- GET /api/vehicles/:id now checks tier status and returns 403 TIER_REQUIRED
  if user tries to access a locked vehicle directly

This ensures that after a user selects 2 vehicles during downgrade to
free tier, only those 2 vehicles appear in the summary and details screens.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-24 11:51:36 -06:00
parent b06a5e692b
commit 68948484a4

View File

@@ -28,8 +28,13 @@ export class VehiclesController {
async getUserVehicles(request: FastifyRequest, reply: FastifyReply) { async getUserVehicles(request: FastifyRequest, reply: FastifyReply) {
try { try {
const userId = (request as any).user.sub; const userId = (request as any).user.sub;
const vehicles = await this.vehiclesService.getUserVehicles(userId); // Use tier-aware method to filter out locked vehicles after downgrade
const vehiclesWithStatus = await this.vehiclesService.getUserVehiclesWithTierStatus(userId);
// Only return active vehicles (filter out locked ones)
const vehicles = vehiclesWithStatus
.filter(v => v.tierStatus === 'active')
.map(({ tierStatus, ...vehicle }) => vehicle);
return reply.code(200).send(vehicles); return reply.code(200).send(vehicles);
} catch (error) { } catch (error) {
logger.error('Error getting user vehicles', { error, userId: (request as any).user?.sub }); logger.error('Error getting user vehicles', { error, userId: (request as any).user?.sub });
@@ -107,20 +112,34 @@ export class VehiclesController {
try { try {
const userId = (request as any).user.sub; const userId = (request as any).user.sub;
const { id } = request.params; const { id } = request.params;
// Check tier status - block access to locked vehicles
const vehiclesWithStatus = await this.vehiclesService.getUserVehiclesWithTierStatus(userId);
const vehicleStatus = vehiclesWithStatus.find(v => v.id === id);
if (vehicleStatus && vehicleStatus.tierStatus === 'locked') {
return reply.code(403).send({
error: 'TIER_REQUIRED',
requiredTier: 'pro',
feature: 'vehicle.access',
featureName: 'Vehicle Access',
upgradePrompt: 'Upgrade to Pro to access all your vehicles',
message: 'This vehicle is not available on your current subscription tier'
});
}
const vehicle = await this.vehiclesService.getVehicle(id, userId); const vehicle = await this.vehiclesService.getVehicle(id, userId);
return reply.code(200).send(vehicle); return reply.code(200).send(vehicle);
} catch (error: any) { } catch (error: any) {
logger.error('Error getting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub }); logger.error('Error getting vehicle', { error, vehicleId: request.params.id, userId: (request as any).user?.sub });
if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') { if (error.message === 'Vehicle not found' || error.message === 'Unauthorized') {
return reply.code(404).send({ return reply.code(404).send({
error: 'Not Found', error: 'Not Found',
message: 'Vehicle not found' message: 'Vehicle not found'
}); });
} }
return reply.code(500).send({ return reply.code(500).send({
error: 'Internal server error', error: 'Internal server error',
message: 'Failed to get vehicle' message: 'Failed to get vehicle'