diff --git a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js index 472747e52d..52896d9bb0 100644 --- a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js +++ b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js @@ -1,5 +1,9 @@ // @ts-check +/** + * @import { PaymentProvider } from '../../../../types/subscription/dashboard/subscription' + */ + const OError = require('@overleaf/o-error') const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors') const PlansLocator = require('./PlansLocator') @@ -27,8 +31,9 @@ class PaymentProviderSubscription { * @param {Date} props.periodEnd * @param {string} props.collectionMethod * @param {PaymentProviderSubscriptionChange} [props.pendingChange] - * @param {string} [props.service] + * @param {PaymentProvider['service']} [props.service] * @param {string} [props.state] + * @param {Date|null} [props.trialPeriodStart] * @param {Date|null} [props.trialPeriodEnd] * @param {Date|null} [props.pausePeriodStart] * @param {number|null} [props.remainingPauseCycles] @@ -51,6 +56,7 @@ class PaymentProviderSubscription { this.pendingChange = props.pendingChange ?? null this.service = props.service ?? 'recurly' this.state = props.state ?? 'active' + this.trialPeriodStart = props.trialPeriodStart ?? null this.trialPeriodEnd = props.trialPeriodEnd ?? null this.pausePeriodStart = props.pausePeriodStart ?? null this.remainingPauseCycles = props.remainingPauseCycles ?? null diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.js b/services/web/app/src/Features/Subscription/RecurlyClient.js index ee56487b43..51375074fb 100644 --- a/services/web/app/src/Features/Subscription/RecurlyClient.js +++ b/services/web/app/src/Features/Subscription/RecurlyClient.js @@ -411,6 +411,7 @@ function subscriptionFromApi(apiSubscription) { collectionMethod: apiSubscription.collectionMethod, service: 'recurly', state: apiSubscription.state ?? 'active', + trialPeriodStart: apiSubscription.trialStartedAt, trialPeriodEnd: apiSubscription.trialEndsAt, pausePeriodStart: apiSubscription.pausedAt, remainingPauseCycles: apiSubscription.remainingPauseCycles, diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js index 2e43454da9..edaf74bcd4 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -13,6 +13,11 @@ const UserAuditLogHandler = require('../User/UserAuditLogHandler') const AccountMappingHelper = require('../Analytics/AccountMappingHelper') 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. * @@ -51,7 +56,7 @@ async function syncSubscription( let subscription = await SubscriptionLocator.promises.getUsersSubscription(adminUserId) if (subscription == null) { - subscription = await _createNewSubscription(adminUserId) + subscription = await createNewSubscription(adminUserId) } await updateSubscriptionFromRecurly( recurlySubscription, @@ -226,7 +231,13 @@ async function createDeletedSubscription(subscription, deleterData) { 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} + */ +async function createNewSubscription(adminUserId) { const subscription = new Subscription({ admin_id: adminUserId, manager_ids: [adminUserId], @@ -242,7 +253,7 @@ async function _deleteAndReplaceSubscriptionFromRecurly( ) { const adminUserId = subscription.admin_id await deleteSubscription(subscription, requesterData) - const newSubscription = await _createNewSubscription(adminUserId) + const newSubscription = await createNewSubscription(adminUserId) await updateSubscriptionFromRecurly( recurlySubscription, newSubscription, @@ -428,6 +439,7 @@ async function _sendSubscriptionEventForAllMembers(subscriptionId, event) { module.exports = { updateAdmin: callbackify(updateAdmin), syncSubscription: callbackify(syncSubscription), + createNewSubscription: callbackify(createNewSubscription), deleteSubscription: callbackify(deleteSubscription), createDeletedSubscription: callbackify(createDeletedSubscription), addUserToGroup: callbackify(addUserToGroup), @@ -440,6 +452,7 @@ module.exports = { promises: { updateAdmin, syncSubscription, + createNewSubscription, addUserToGroup, refreshUsersFeatures, removeUserFromGroup, diff --git a/services/web/app/src/models/Subscription.js b/services/web/app/src/models/Subscription.js index 0145eb66d9..92a7739515 100644 --- a/services/web/app/src/models/Subscription.js +++ b/services/web/app/src/models/Subscription.js @@ -64,6 +64,15 @@ const SubscriptionSchema = new Schema( subscriptionId: { type: String, }, + state: { + type: String, + }, + trialStartedAt: { + type: Date, + }, + trialEndsAt: { + type: Date, + }, }, collectionMethod: { type: String, diff --git a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js index 0bd0d24558..e6ff94dd66 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js @@ -114,6 +114,7 @@ describe('SubscriptionHandler', function () { promises: { updateSubscriptionFromRecurly: sinon.stub().resolves(), syncSubscription: sinon.stub().resolves(), + syncStripeSubscription: sinon.stub().resolves(), startFreeTrial: sinon.stub().resolves(), }, } diff --git a/services/web/types/project/dashboard/subscription.ts b/services/web/types/project/dashboard/subscription.ts index 3501ad8c38..e8b595c49f 100644 --- a/services/web/types/project/dashboard/subscription.ts +++ b/services/web/types/project/dashboard/subscription.ts @@ -1,3 +1,5 @@ +import { SubscriptionState } from '../../subscription/dashboard/subscription' + type SubscriptionBase = { featuresPageURL: string } @@ -9,7 +11,7 @@ export type FreePlanSubscription = { type FreeSubscription = FreePlanSubscription type RecurlyStatus = { - state: 'active' | 'canceled' | 'expired' | 'paused' + state: SubscriptionState } type PaidSubscriptionBase = { diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index b43fc8a81e..cd467c80a5 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -8,7 +8,7 @@ import { } from '../plan' 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 export type PurchasingAddOnCode = { @@ -99,3 +99,13 @@ export type MemberGroupSubscription = Omit & { planLevelName: string admin_id: User } + +type PaymentProviderService = 'stripe' | 'recurly' + +export type PaymentProvider = { + service: PaymentProviderService + subscriptionId: string + state: SubscriptionState + trialStartedAt?: Nullable + trialEndsAt?: Nullable +}