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 stripe = getStripe()
|
||||||
const updated = await stripe.subscriptions.update(org.stripeSubscriptionId, {
|
// Stripe expose 2 mécaniques d'annulation et REFUSE qu'on passe les 2
|
||||||
cancel_at_period_end: false,
|
// dans le même update :
|
||||||
})
|
// - `cancel_at_period_end: true` (booléen) — API directe / CLI
|
||||||
org.cancelAtPeriodEnd = !!updated.cancel_at_period_end // = false normalement
|
// - `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
|
org.subscriptionStatus = updated.status
|
||||||
await org.save()
|
await org.save()
|
||||||
|
|
||||||
@ -336,9 +350,17 @@ export default class BillingController {
|
|||||||
org.currentPeriodEnd = item.current_period_end
|
org.currentPeriodEnd = item.current_period_end
|
||||||
? DateTime.fromSeconds(item.current_period_end)
|
? DateTime.fromSeconds(item.current_period_end)
|
||||||
: null
|
: 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.
|
// Détection de l'annulation programmée. Stripe expose DEUX mécaniques :
|
||||||
org.cancelAtPeriodEnd = !!subscription.cancel_at_period_end
|
// - `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()
|
await org.save()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -348,6 +370,8 @@ export default class BillingController {
|
|||||||
cycle,
|
cycle,
|
||||||
status: subscription.status,
|
status: subscription.status,
|
||||||
subscriptionId: subscription.id,
|
subscriptionId: subscription.id,
|
||||||
|
cancelAtPeriodEnd: !!subscription.cancel_at_period_end,
|
||||||
|
cancelAt: subscription.cancel_at,
|
||||||
},
|
},
|
||||||
'Subscription appliquée à l\'org'
|
'Subscription appliquée à l\'org'
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user