diff --git a/apps/api/app/controllers/billing_controller.ts b/apps/api/app/controllers/billing_controller.ts index bf20c92..730af1d 100644 --- a/apps/api/app/controllers/billing_controller.ts +++ b/apps/api/app/controllers/billing_controller.ts @@ -126,10 +126,24 @@ export default class BillingController { } const stripe = getStripe() - const updated = await stripe.subscriptions.update(org.stripeSubscriptionId, { - cancel_at_period_end: false, - }) - org.cancelAtPeriodEnd = !!updated.cancel_at_period_end // = false normalement + // Stripe expose 2 mécaniques d'annulation et REFUSE qu'on passe les 2 + // dans le même update : + // - `cancel_at_period_end: true` (booléen) — API directe / CLI + // - `cancel_at: ` — Customer Portal + // + // On retrieve le sub d'abord pour savoir laquelle est posée, puis on + // clear uniquement celle-là. + const current = await stripe.subscriptions.retrieve(org.stripeSubscriptionId) + const updatePayload: Stripe.SubscriptionUpdateParams = current.cancel_at + ? { cancel_at: null } + : { cancel_at_period_end: false } + + const updated = await stripe.subscriptions.update( + org.stripeSubscriptionId, + updatePayload + ) + org.cancelAtPeriodEnd = + !!updated.cancel_at_period_end || !!updated.cancel_at // = false normalement org.subscriptionStatus = updated.status await org.save() @@ -336,9 +350,17 @@ export default class BillingController { org.currentPeriodEnd = item.current_period_end ? DateTime.fromSeconds(item.current_period_end) : null - // L'user a-t-il programmé une annulation ? (via Customer Portal) - // Reflété en UI pour qu'il sache que son accès s'éteint au period_end. - org.cancelAtPeriodEnd = !!subscription.cancel_at_period_end + + // Détection de l'annulation programmée. Stripe expose DEUX mécaniques : + // - `cancel_at_period_end: true` (booléen) → utilisé par l'API directe + // (`stripe.subscriptions.update --cancel-at-period-end=true`) + // - `cancel_at: ` (epoch) → utilisé par le Customer Portal + // qui schedule un cancel à une date précise (généralement = period_end). + // + // Sémantiquement c'est la même chose : "le sub s'éteindra à cette date". + // On unifie en un seul booléen pour le reste de l'app. + org.cancelAtPeriodEnd = + !!subscription.cancel_at_period_end || !!subscription.cancel_at await org.save() logger.info( @@ -348,6 +370,8 @@ export default class BillingController { cycle, status: subscription.status, subscriptionId: subscription.id, + cancelAtPeriodEnd: !!subscription.cancel_at_period_end, + cancelAt: subscription.cancel_at, }, 'Subscription appliquée à l\'org' )