feat: add vehicle selection and downgrade flow - M5 (refs #55)
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user