Merge pull request #27215 from overleaf/rh-stripe-pause-status

Update features and subscription state when Stripe pause starts and ends

GitOrigin-RevId: 368f5d9b046cfe26e996be336189081b96926713
This commit is contained in:
roo hutton
2025-08-06 08:04:57 +00:00
committed by Copybot
parent 215059f461
commit 58b8e36739
9 changed files with 276 additions and 0 deletions
@@ -40,6 +40,7 @@ class PaymentProviderSubscription {
* @param {Date|null} [props.trialPeriodStart]
* @param {Date|null} [props.trialPeriodEnd]
* @param {Date|null} [props.pausePeriodStart]
* @param {Date|null} [props.pausePeriodEnd]
* @param {number|null} [props.remainingPauseCycles]
*/
constructor(props) {
@@ -66,6 +67,7 @@ class PaymentProviderSubscription {
this.trialPeriodStart = props.trialPeriodStart ?? null
this.trialPeriodEnd = props.trialPeriodEnd ?? null
this.pausePeriodStart = props.pausePeriodStart ?? null
this.pausePeriodEnd = props.pausePeriodEnd ?? null
this.remainingPauseCycles = props.remainingPauseCycles ?? null
}
@@ -1,6 +1,63 @@
const { formatCurrency } = require('../../util/currency')
const GroupPlansData = require('./GroupPlansData')
const { isStandaloneAiAddOnPlanCode } = require('./AiHelper')
const { Subscription } = require('../../models/Subscription')
const MILLISECONDS = 1_000
/**
* Recompute the subscription state for Stripe subscriptions based on pause periods.
* This function checks if a subscription should transition between 'active' and 'paused'
* states based on the current time and pause period metadata.
*
* @param {Object} subscription - The MongoDB subscription document
* @returns {Promise<Object>} - The updated subscription document with recomputed state
*/
async function recomputeSubscriptionState(subscription) {
if (
!subscription?.paymentProvider?.subscriptionId ||
!subscription.paymentProvider.pausePeriodStart ||
!subscription.paymentProvider.pausePeriodEnd ||
!subscription?.paymentProvider.service.includes('stripe')
) {
return subscription
}
const now = Date.now() / MILLISECONDS
const pauseStartTime =
new Date(subscription.paymentProvider.pausePeriodStart).getTime() /
MILLISECONDS
const currentState = subscription.paymentProvider.state
const pauseEndTime =
new Date(subscription.paymentProvider.pausePeriodEnd).getTime() /
MILLISECONDS
const shouldBePaused =
pauseEndTime && now >= pauseStartTime && now < pauseEndTime
let newState
if (shouldBePaused && currentState !== 'paused') {
newState = 'paused'
} else if (
!shouldBePaused &&
currentState === 'paused' &&
pauseEndTime &&
now >= pauseEndTime
) {
newState = 'active'
}
if (newState) {
await Subscription.updateOne(
{ _id: subscription._id },
{ 'paymentProvider.state': newState }
).exec()
subscription.paymentProvider.state = newState
}
return subscription
}
/**
* If the user changes to a less expensive plan, we shouldn't apply the change immediately.
@@ -149,4 +206,5 @@ module.exports = {
getSubscriptionTrialStartedAt,
getSubscriptionTrialEndsAt,
isInTrial,
recomputeSubscriptionState,
}
@@ -15,6 +15,11 @@ const SubscriptionLocator = {
const userId = SubscriptionLocator._getUserId(userOrId)
const subscription = await Subscription.findOne({ admin_id: userId }).exec()
logger.debug({ userId }, 'got users subscription')
if (subscription) {
return await SubscriptionHelper.recomputeSubscriptionState(subscription)
}
return subscription
},
@@ -76,6 +76,12 @@ const SubscriptionSchema = new Schema(
state: {
type: String,
},
pausePeriodStart: {
type: Date,
},
pausePeriodEnd: {
type: Date,
},
trialStartedAt: {
type: Date,
},