fix(billing): détecte aussi cancel_at (Customer Portal) + reactivate sans conflit
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m7s
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m7s
Bug : le Stripe Customer Portal n'utilise pas `cancel_at_period_end:true`
mais `cancel_at:<timestamp>` pour scheduler l'annulation. Notre webhook
ne lisait que le booléen → l'annulation via portail n'était pas remontée
côté DB, l'UI ne montrait jamais le bandeau "annulé".
Webhook handler :
- Détecte l'annulation via EITHER `cancel_at_period_end` OR `cancel_at`
et unifie en un seul booléen `cancelAtPeriodEnd` côté org.
Endpoint /reactivate :
- Stripe REFUSE qu'on passe `cancel_at_period_end:false` ET `cancel_at:null`
dans le même update ("Please pass in only one"). On retrieve d'abord
la sub pour savoir laquelle des 2 mécaniques est active, puis on clear
uniquement celle-là.
Logs enrichis : `cancelAtPeriodEnd` et `cancelAt` désormais loggés à
chaque `applySubscriptionToOrg` pour que le diagnostic soit immédiat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cb87bbc8d1
commit
031b8cc062
@ -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: <timestamp>` — 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: <timestamp>` (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'
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user