feat: add needs-vehicle-selection endpoint (refs #60)

- Add GET /api/subscriptions/needs-vehicle-selection endpoint
- Returns { needsSelection, vehicleCount, maxAllowed }
- Checks: free tier, >2 vehicles, no existing selections

🤖 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:25:57 -06:00
parent 7c39d2f042
commit 684615a8a2
4 changed files with 98 additions and 0 deletions

View File

@@ -15,7 +15,9 @@ import {
BillingCycle,
SubscriptionStatus,
UpdateSubscriptionData,
NeedsVehicleSelectionResponse,
} from './subscriptions.types';
import { VehiclesRepository } from '../../vehicles/data/vehicles.repository';
interface StripeWebhookEvent {
id: string;
@@ -27,6 +29,7 @@ interface StripeWebhookEvent {
export class SubscriptionsService {
private userProfileRepository: UserProfileRepository;
private vehiclesRepository: VehiclesRepository;
constructor(
private repository: SubscriptionsRepository,
@@ -34,6 +37,7 @@ export class SubscriptionsService {
pool: Pool
) {
this.userProfileRepository = new UserProfileRepository(pool);
this.vehiclesRepository = new VehiclesRepository(pool);
}
/**
@@ -57,6 +61,65 @@ export class SubscriptionsService {
}
}
/**
* Check if user needs to make a vehicle selection after auto-downgrade
* Returns true if: user is on free tier, has >2 vehicles, and hasn't made selections
*/
async checkNeedsVehicleSelection(userId: string): Promise<NeedsVehicleSelectionResponse> {
const FREE_TIER_VEHICLE_LIMIT = 2;
try {
// Get current subscription
const subscription = await this.repository.findByUserId(userId);
// No subscription or not on free tier = no selection needed
if (!subscription || subscription.tier !== 'free') {
return {
needsSelection: false,
vehicleCount: 0,
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
};
}
// Count user's active vehicles
const vehicleCount = await this.vehiclesRepository.countByUserId(userId);
// If within limit, no selection needed
if (vehicleCount <= FREE_TIER_VEHICLE_LIMIT) {
return {
needsSelection: false,
vehicleCount,
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
};
}
// Check if user already has vehicle selections
const existingSelections = await this.repository.findVehicleSelectionsByUserId(userId);
// If user already made selections, no selection needed
if (existingSelections.length > 0) {
return {
needsSelection: false,
vehicleCount,
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
};
}
// User is on free tier, has >2 vehicles, and hasn't made selections
return {
needsSelection: true,
vehicleCount,
maxAllowed: FREE_TIER_VEHICLE_LIMIT,
};
} catch (error: any) {
logger.error('Failed to check needs vehicle selection', {
userId,
error: error.message,
});
throw error;
}
}
/**
* Create new subscription (Stripe customer + initial free tier record)
*/