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:
@@ -49,6 +49,28 @@ export class SubscriptionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/subscriptions/needs-vehicle-selection - Check if user needs vehicle selection
|
||||||
|
*/
|
||||||
|
async checkNeedsVehicleSelection(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userId = (request as any).user.sub;
|
||||||
|
|
||||||
|
const result = await this.service.checkNeedsVehicleSelection(userId);
|
||||||
|
|
||||||
|
reply.status(200).send(result);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Failed to check needs vehicle selection', {
|
||||||
|
userId: (request as any).user?.sub,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
reply.status(500).send({
|
||||||
|
error: 'Failed to check needs vehicle selection',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/subscriptions/checkout - Create Stripe checkout session
|
* POST /api/subscriptions/checkout - Create Stripe checkout session
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ export const subscriptionsRoutes: FastifyPluginAsync = async (
|
|||||||
handler: subscriptionsController.getSubscription.bind(subscriptionsController)
|
handler: subscriptionsController.getSubscription.bind(subscriptionsController)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/subscriptions/needs-vehicle-selection - Check if user needs vehicle selection
|
||||||
|
fastify.get('/subscriptions/needs-vehicle-selection', {
|
||||||
|
preHandler: [fastify.authenticate],
|
||||||
|
handler: subscriptionsController.checkNeedsVehicleSelection.bind(subscriptionsController)
|
||||||
|
});
|
||||||
|
|
||||||
// POST /api/subscriptions/checkout - Create Stripe checkout session
|
// POST /api/subscriptions/checkout - Create Stripe checkout session
|
||||||
fastify.post('/subscriptions/checkout', {
|
fastify.post('/subscriptions/checkout', {
|
||||||
preHandler: [fastify.authenticate],
|
preHandler: [fastify.authenticate],
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
BillingCycle,
|
BillingCycle,
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
UpdateSubscriptionData,
|
UpdateSubscriptionData,
|
||||||
|
NeedsVehicleSelectionResponse,
|
||||||
} from './subscriptions.types';
|
} from './subscriptions.types';
|
||||||
|
import { VehiclesRepository } from '../../vehicles/data/vehicles.repository';
|
||||||
|
|
||||||
interface StripeWebhookEvent {
|
interface StripeWebhookEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -27,6 +29,7 @@ interface StripeWebhookEvent {
|
|||||||
|
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
private userProfileRepository: UserProfileRepository;
|
private userProfileRepository: UserProfileRepository;
|
||||||
|
private vehiclesRepository: VehiclesRepository;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private repository: SubscriptionsRepository,
|
private repository: SubscriptionsRepository,
|
||||||
@@ -34,6 +37,7 @@ export class SubscriptionsService {
|
|||||||
pool: Pool
|
pool: Pool
|
||||||
) {
|
) {
|
||||||
this.userProfileRepository = new UserProfileRepository(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)
|
* Create new subscription (Stripe customer + initial free tier record)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -131,3 +131,10 @@ export interface UpdateSubscriptionData {
|
|||||||
export interface UpdateDonationData {
|
export interface UpdateDonationData {
|
||||||
status?: DonationStatus;
|
status?: DonationStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needs vehicle selection check response
|
||||||
|
export interface NeedsVehicleSelectionResponse {
|
||||||
|
needsSelection: boolean;
|
||||||
|
vehicleCount: number;
|
||||||
|
maxAllowed: number;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user