feat: add vehicle selection and downgrade flow - M5 (refs #55)

This commit is contained in:
Eric Gullickson
2026-01-18 16:44:45 -06:00
parent 94d1c677bc
commit 6c1a100eb9
11 changed files with 509 additions and 7 deletions

View File

@@ -271,6 +271,95 @@ export class SubscriptionsService {
}
}
/**
* Downgrade subscription to a lower tier with vehicle selection
*/
async downgradeSubscription(
userId: string,
targetTier: SubscriptionTier,
vehicleIdsToKeep: string[]
): Promise<Subscription> {
try {
logger.info('Downgrading subscription', { userId, targetTier, vehicleCount: vehicleIdsToKeep.length });
// Get current subscription
const currentSubscription = await this.repository.findByUserId(userId);
if (!currentSubscription) {
throw new Error('No subscription found for user');
}
// Define tier limits
const tierLimits: Record<SubscriptionTier, number | null> = {
free: 2,
pro: 5,
enterprise: null, // unlimited
};
const targetLimit = tierLimits[targetTier];
// Validate vehicle selection count
if (targetLimit !== null && vehicleIdsToKeep.length > targetLimit) {
throw new Error(`Vehicle selection exceeds tier limit. ${targetTier} tier allows ${targetLimit} vehicles, but ${vehicleIdsToKeep.length} were selected.`);
}
// Cancel current Stripe subscription if exists (downgrading from paid tier)
if (currentSubscription.stripeSubscriptionId) {
await this.stripeClient.cancelSubscription(
currentSubscription.stripeSubscriptionId,
false // Cancel immediately, not at period end
);
}
// Clear previous vehicle selections
await this.repository.deleteVehicleSelectionsByUserId(userId);
// Save new vehicle selections
for (const vehicleId of vehicleIdsToKeep) {
await this.repository.createVehicleSelection({
userId,
vehicleId,
});
}
// Update subscription tier
const updateData: UpdateSubscriptionData = {
tier: targetTier,
status: 'active',
stripeSubscriptionId: undefined,
billingCycle: undefined,
cancelAtPeriodEnd: false,
};
const updatedSubscription = await this.repository.update(
currentSubscription.id,
updateData
);
if (!updatedSubscription) {
throw new Error('Failed to update subscription');
}
// Sync tier to user profile
await this.syncTierToUserProfile(userId, targetTier);
logger.info('Subscription downgraded', {
subscriptionId: updatedSubscription.id,
userId,
targetTier,
vehicleCount: vehicleIdsToKeep.length,
});
return updatedSubscription;
} catch (error: any) {
logger.error('Failed to downgrade subscription', {
userId,
targetTier,
error: error.message,
});
throw error;
}
}
/**
* Handle incoming Stripe webhook event
*/