Merge pull request #24848 from overleaf/mf-save-stripe-checkout-data-to-mongo-db

[web] Save stripe checkout data to mongodb

GitOrigin-RevId: 537778a041f92f43ccf6455c29a56c7a961ce765
This commit is contained in:
Kristina
2025-04-24 08:05:40 +00:00
committed by Copybot
parent a426d08b9c
commit 5cef00a15f
7 changed files with 48 additions and 6 deletions
@@ -1,5 +1,9 @@
// @ts-check // @ts-check
/**
* @import { PaymentProvider } from '../../../../types/subscription/dashboard/subscription'
*/
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors') const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
const PlansLocator = require('./PlansLocator') const PlansLocator = require('./PlansLocator')
@@ -27,8 +31,9 @@ class PaymentProviderSubscription {
* @param {Date} props.periodEnd * @param {Date} props.periodEnd
* @param {string} props.collectionMethod * @param {string} props.collectionMethod
* @param {PaymentProviderSubscriptionChange} [props.pendingChange] * @param {PaymentProviderSubscriptionChange} [props.pendingChange]
* @param {string} [props.service] * @param {PaymentProvider['service']} [props.service]
* @param {string} [props.state] * @param {string} [props.state]
* @param {Date|null} [props.trialPeriodStart]
* @param {Date|null} [props.trialPeriodEnd] * @param {Date|null} [props.trialPeriodEnd]
* @param {Date|null} [props.pausePeriodStart] * @param {Date|null} [props.pausePeriodStart]
* @param {number|null} [props.remainingPauseCycles] * @param {number|null} [props.remainingPauseCycles]
@@ -51,6 +56,7 @@ class PaymentProviderSubscription {
this.pendingChange = props.pendingChange ?? null this.pendingChange = props.pendingChange ?? null
this.service = props.service ?? 'recurly' this.service = props.service ?? 'recurly'
this.state = props.state ?? 'active' this.state = props.state ?? 'active'
this.trialPeriodStart = props.trialPeriodStart ?? null
this.trialPeriodEnd = props.trialPeriodEnd ?? null this.trialPeriodEnd = props.trialPeriodEnd ?? null
this.pausePeriodStart = props.pausePeriodStart ?? null this.pausePeriodStart = props.pausePeriodStart ?? null
this.remainingPauseCycles = props.remainingPauseCycles ?? null this.remainingPauseCycles = props.remainingPauseCycles ?? null
@@ -411,6 +411,7 @@ function subscriptionFromApi(apiSubscription) {
collectionMethod: apiSubscription.collectionMethod, collectionMethod: apiSubscription.collectionMethod,
service: 'recurly', service: 'recurly',
state: apiSubscription.state ?? 'active', state: apiSubscription.state ?? 'active',
trialPeriodStart: apiSubscription.trialStartedAt,
trialPeriodEnd: apiSubscription.trialEndsAt, trialPeriodEnd: apiSubscription.trialEndsAt,
pausePeriodStart: apiSubscription.pausedAt, pausePeriodStart: apiSubscription.pausedAt,
remainingPauseCycles: apiSubscription.remainingPauseCycles, remainingPauseCycles: apiSubscription.remainingPauseCycles,
@@ -13,6 +13,11 @@ const UserAuditLogHandler = require('../User/UserAuditLogHandler')
const AccountMappingHelper = require('../Analytics/AccountMappingHelper') const AccountMappingHelper = require('../Analytics/AccountMappingHelper')
const { SSOConfig } = require('../../models/SSOConfig') const { SSOConfig } = require('../../models/SSOConfig')
/**
* @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription
* @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider
*/
/** /**
* Change the admin of the given subscription. * Change the admin of the given subscription.
* *
@@ -51,7 +56,7 @@ async function syncSubscription(
let subscription = let subscription =
await SubscriptionLocator.promises.getUsersSubscription(adminUserId) await SubscriptionLocator.promises.getUsersSubscription(adminUserId)
if (subscription == null) { if (subscription == null) {
subscription = await _createNewSubscription(adminUserId) subscription = await createNewSubscription(adminUserId)
} }
await updateSubscriptionFromRecurly( await updateSubscriptionFromRecurly(
recurlySubscription, recurlySubscription,
@@ -226,7 +231,13 @@ async function createDeletedSubscription(subscription, deleterData) {
await DeletedSubscription.findOneAndUpdate(filter, data, options).exec() await DeletedSubscription.findOneAndUpdate(filter, data, options).exec()
} }
async function _createNewSubscription(adminUserId) { /**
* Creates a new subscription for the given admin user.
*
* @param {string} adminUserId
* @returns {Promise<Subscription>}
*/
async function createNewSubscription(adminUserId) {
const subscription = new Subscription({ const subscription = new Subscription({
admin_id: adminUserId, admin_id: adminUserId,
manager_ids: [adminUserId], manager_ids: [adminUserId],
@@ -242,7 +253,7 @@ async function _deleteAndReplaceSubscriptionFromRecurly(
) { ) {
const adminUserId = subscription.admin_id const adminUserId = subscription.admin_id
await deleteSubscription(subscription, requesterData) await deleteSubscription(subscription, requesterData)
const newSubscription = await _createNewSubscription(adminUserId) const newSubscription = await createNewSubscription(adminUserId)
await updateSubscriptionFromRecurly( await updateSubscriptionFromRecurly(
recurlySubscription, recurlySubscription,
newSubscription, newSubscription,
@@ -428,6 +439,7 @@ async function _sendSubscriptionEventForAllMembers(subscriptionId, event) {
module.exports = { module.exports = {
updateAdmin: callbackify(updateAdmin), updateAdmin: callbackify(updateAdmin),
syncSubscription: callbackify(syncSubscription), syncSubscription: callbackify(syncSubscription),
createNewSubscription: callbackify(createNewSubscription),
deleteSubscription: callbackify(deleteSubscription), deleteSubscription: callbackify(deleteSubscription),
createDeletedSubscription: callbackify(createDeletedSubscription), createDeletedSubscription: callbackify(createDeletedSubscription),
addUserToGroup: callbackify(addUserToGroup), addUserToGroup: callbackify(addUserToGroup),
@@ -440,6 +452,7 @@ module.exports = {
promises: { promises: {
updateAdmin, updateAdmin,
syncSubscription, syncSubscription,
createNewSubscription,
addUserToGroup, addUserToGroup,
refreshUsersFeatures, refreshUsersFeatures,
removeUserFromGroup, removeUserFromGroup,
@@ -64,6 +64,15 @@ const SubscriptionSchema = new Schema(
subscriptionId: { subscriptionId: {
type: String, type: String,
}, },
state: {
type: String,
},
trialStartedAt: {
type: Date,
},
trialEndsAt: {
type: Date,
},
}, },
collectionMethod: { collectionMethod: {
type: String, type: String,
@@ -114,6 +114,7 @@ describe('SubscriptionHandler', function () {
promises: { promises: {
updateSubscriptionFromRecurly: sinon.stub().resolves(), updateSubscriptionFromRecurly: sinon.stub().resolves(),
syncSubscription: sinon.stub().resolves(), syncSubscription: sinon.stub().resolves(),
syncStripeSubscription: sinon.stub().resolves(),
startFreeTrial: sinon.stub().resolves(), startFreeTrial: sinon.stub().resolves(),
}, },
} }
@@ -1,3 +1,5 @@
import { SubscriptionState } from '../../subscription/dashboard/subscription'
type SubscriptionBase = { type SubscriptionBase = {
featuresPageURL: string featuresPageURL: string
} }
@@ -9,7 +11,7 @@ export type FreePlanSubscription = {
type FreeSubscription = FreePlanSubscription type FreeSubscription = FreePlanSubscription
type RecurlyStatus = { type RecurlyStatus = {
state: 'active' | 'canceled' | 'expired' | 'paused' state: SubscriptionState
} }
type PaidSubscriptionBase = { type PaidSubscriptionBase = {
@@ -8,7 +8,7 @@ import {
} from '../plan' } from '../plan'
import { User } from '../../user' import { User } from '../../user'
type SubscriptionState = 'active' | 'canceled' | 'expired' | 'paused' export type SubscriptionState = 'active' | 'canceled' | 'expired' | 'paused'
// when puchasing a new add-on in recurly, we only need to provide the code // when puchasing a new add-on in recurly, we only need to provide the code
export type PurchasingAddOnCode = { export type PurchasingAddOnCode = {
@@ -99,3 +99,13 @@ export type MemberGroupSubscription = Omit<GroupSubscription, 'admin_id'> & {
planLevelName: string planLevelName: string
admin_id: User admin_id: User
} }
type PaymentProviderService = 'stripe' | 'recurly'
export type PaymentProvider = {
service: PaymentProviderService
subscriptionId: string
state: SubscriptionState
trialStartedAt?: Nullable<Date>
trialEndsAt?: Nullable<Date>
}