fix(subscription): recover stepper when a pending checkout is abandoned
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled
Some checks failed
Deploy / Deploy to Production (push) Has been cancelled
A user who picked a plan + brands, clicked the checkout button to create the pending subscription, then closed the tab without paying came back to a frozen "Mevcut Plan · Bekliyor" card with no payment button and no way to edit. Status was "pending", which my conditional treated as an active subscription and routed away from the stepper. Frontend - hasActiveSub narrowed to active|cancelled only. Pending users now see the stepper again. - New effect pre-fills selectedPlanKey, selectedBrandIds, billingPeriod from the pending row on first render and jumps straight to the payment step. - New PendingPaymentBanner at the top of the stepper with "Odemeye devam et" (scrolls + sets step=payment) and "Vazgec" (calls cancel-pending then resets selection). Backend - subscriptions.service.create() auto-expires existing pending rows for the user before inserting a new one — abandoned checkouts no longer accumulate and a fresh attempt with a different plan doesn't 409. - New PATCH /subscriptions/cancel-pending + service method for the banner's explicit cancel action. i18n: subscription.pendingBanner.* (tr/en). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,11 @@ export class SubscriptionsController {
|
||||
return this.subscriptionsService.startFullTrial(userId);
|
||||
}
|
||||
|
||||
@Patch("cancel-pending")
|
||||
async cancelPending(@CurrentUser("id") userId: string) {
|
||||
return this.subscriptionsService.cancelPending(userId);
|
||||
}
|
||||
|
||||
@Patch("cancel")
|
||||
async cancel(@CurrentUser("id") userId: string) {
|
||||
return this.subscriptionsService.cancel(userId);
|
||||
|
||||
@@ -34,6 +34,13 @@ export class SubscriptionsService {
|
||||
.set({ status: "expired", updatedAt: new Date() })
|
||||
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "trial")));
|
||||
|
||||
// Expire any existing pending subscription so an abandoned checkout
|
||||
// doesn't block the user from starting a new one with different choices.
|
||||
await this.db
|
||||
.update(userSubscriptions)
|
||||
.set({ status: "expired", updatedAt: new Date() })
|
||||
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "pending")));
|
||||
|
||||
// Validate plan
|
||||
const plan = await this.db.select().from(plans).where(eq(plans.id, data.planId)).limit(1);
|
||||
if (plan.length === 0) throw new NotFoundException("Plan bulunamadı");
|
||||
@@ -157,6 +164,15 @@ export class SubscriptionsService {
|
||||
return { ...sub, plan: plan[0], brands: subBrands };
|
||||
}
|
||||
|
||||
async cancelPending(userId: string) {
|
||||
const result = await this.db
|
||||
.update(userSubscriptions)
|
||||
.set({ status: "expired", updatedAt: new Date() })
|
||||
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "pending")))
|
||||
.returning({ id: userSubscriptions.id });
|
||||
return { cancelled: result.length };
|
||||
}
|
||||
|
||||
async cancel(userId: string) {
|
||||
const [sub] = await this.db
|
||||
.select()
|
||||
|
||||
Reference in New Issue
Block a user