Kaynağa Gözat

Merge pull request #34130 from overleaf/rh-enterprise-cio

Expose enterprise indicators and previous_plan_type for first subscriptions to customer.io

GitOrigin-RevId: 693db7f796609f00ecd31216a6d6be32c1f569c8
roo hutton 2 ay önce
ebeveyn
işleme
06e99fe62a

+ 2 - 0
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -28,6 +28,7 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs'
 import SplitTestSessionHandler from '../SplitTests/SplitTestSessionHandler.mjs'
 import TutorialHandler from '../Tutorial/TutorialHandler.mjs'
 import SubscriptionHelper from '../Subscription/SubscriptionHelper.mjs'
+import CustomerIoPlanHelpers from '../Subscription/CustomerIoPlanHelpers.mjs'
 import PermissionsManager from '../Authorization/PermissionsManager.mjs'
 import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
 import { OnboardingDataCollection } from '../../models/OnboardingDataCollection.mjs'
@@ -585,6 +586,7 @@ async function projectListPage(req, res, next) {
       ...(usedLatex && { used_latex: usedLatex }),
       ...(countryCode && { country: countryCode }),
       ...(commonsInstitution && { commons_institution: commonsInstitution }),
+      ...CustomerIoPlanHelpers.getAffiliationProperties(userEmails),
       ...(groupRole && { group_role: groupRole }),
       is_managed_user: Boolean(user.enrollment?.managedBy),
       ...(user.email && { email: user.email }),

+ 24 - 0
services/web/app/src/Features/Subscription/CustomerIoPlanHelpers.mjs

@@ -7,6 +7,7 @@ import FeaturesHelper from './FeaturesHelper.mjs'
  * @typedef {InstanceType<typeof import('../../models/Subscription.mjs').Subscription>} MongoSubscription
  * @typedef {import('../../../../types/subscription/plan').Plan} Plan
  * @typedef {import('../../../../modules/subscriptions/app/src/PaymentService.mjs').PaymentRecord} PaymentRecord
+ * @typedef {import('../../../../types/user-email').UserEmailData} UserEmailData
  */
 
 /**
@@ -540,6 +541,28 @@ function getGroupRole(
   return 'member'
 }
 
+/**
+ * Customer.io properties derived from a user's institutional affiliations.
+ *
+ * @param {UserEmailData[]} userEmails - email data from UserGetter.getUserFullEmails
+ * @returns {{ enterprise_commons: boolean, domain_capture: boolean }}
+ */
+function getAffiliationProperties(userEmails) {
+  const enterpriseCommons = userEmails.some(
+    emailData =>
+      emailData.emailHasInstitutionLicence &&
+      emailData.affiliation?.institution?.commonsAccount &&
+      emailData.affiliation?.institution?.enterpriseCommons
+  )
+  const domainCapture = userEmails.some(
+    emailData => emailData.affiliation?.group?.domainCaptureEnabled
+  )
+  return {
+    enterprise_commons: enterpriseCommons,
+    domain_capture: domainCapture,
+  }
+}
+
 /**
  * Compute plan-related user properties for sending to customer.io.
  *
@@ -657,4 +680,5 @@ export default {
   shouldUseCommonsBestSubscription,
   getGroupRole,
   getPlanProperties,
+  getAffiliationProperties,
 }

+ 1 - 0
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

@@ -26,6 +26,7 @@ const userEmailData: UserEmailData & { affiliation: Affiliation } = {
     department: 'Art History',
     institution: {
       commonsAccount: false,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 1,

+ 2 - 0
services/web/test/frontend/features/settings/components/emails/emails-section-institution-and-role.test.tsx

@@ -24,6 +24,7 @@ const userData1: UserEmailData & { affiliation: Affiliation } = {
     department: null,
     institution: {
       commonsAccount: false,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 1,
@@ -55,6 +56,7 @@ const userData2: UserEmailData & { affiliation: Affiliation } = {
     department: 'Art History',
     institution: {
       commonsAccount: false,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 1,

+ 3 - 0
services/web/test/frontend/features/settings/fixtures/test-user-email-data.ts

@@ -29,6 +29,7 @@ export const professionalUserData: UserEmailData & {
     department: 'Art History',
     institution: {
       commonsAccount: false,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 1,
@@ -62,6 +63,7 @@ export const unconfirmedCommonsUserData: UserEmailData & {
     department: 'Art History',
     institution: {
       commonsAccount: true,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 1,
@@ -92,6 +94,7 @@ export const ssoUserData: UserEmailData = {
     department: 'Art History',
     institution: {
       commonsAccount: true,
+      enterpriseCommons: false,
       writefullCommonsAccount: false,
       confirmed: true,
       id: 2,

+ 2 - 0
services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx

@@ -12,6 +12,7 @@ const memberships: Institution[] = [
     id: 9258,
     name: 'Test University',
     commonsAccount: true,
+    enterpriseCommons: false,
     isUniversity: true,
     confirmed: true,
     ssoBeta: false,
@@ -23,6 +24,7 @@ const memberships: Institution[] = [
     id: 9259,
     name: 'Example Institution',
     commonsAccount: true,
+    enterpriseCommons: false,
     isUniversity: true,
     confirmed: true,
     ssoBeta: false,

+ 101 - 0
services/web/test/unit/src/Project/ProjectListController.test.mjs

@@ -553,6 +553,107 @@ describe('ProjectListController', function () {
       )
     })
 
+    it('should send enterprise_commons=true when user has commons from an enterprise_commons institution', async function (ctx) {
+      ctx.Features.hasFeature.withArgs('saas').returns(true)
+      ctx.UserGetter.promises.getUserFullEmails.resolves([
+        {
+          email: 'test@overleaf.com',
+          emailHasInstitutionLicence: true,
+          affiliation: {
+            institution: {
+              id: 1,
+              name: 'Overleaf',
+              commonsAccount: true,
+              enterpriseCommons: true,
+            },
+          },
+        },
+      ])
+      ctx.res.render = () => {}
+
+      await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
+
+      expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
+        'setUserProperties',
+        ctx.user._id,
+        sinon.match({ enterprise_commons: true })
+      )
+    })
+
+    it('should send enterprise_commons=false when affiliated with an enterprise_commons institution but without active commons access', async function (ctx) {
+      ctx.Features.hasFeature.withArgs('saas').returns(true)
+      ctx.UserGetter.promises.getUserFullEmails.resolves([
+        {
+          email: 'test@overleaf.com',
+          emailHasInstitutionLicence: false,
+          affiliation: {
+            institution: {
+              id: 1,
+              name: 'Overleaf',
+              commonsAccount: true,
+              enterpriseCommons: true,
+            },
+          },
+        },
+      ])
+      ctx.res.render = () => {}
+
+      await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
+
+      expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
+        'setUserProperties',
+        ctx.user._id,
+        sinon.match({ enterprise_commons: false })
+      )
+    })
+
+    it('should send domain_capture=true when user has an affiliation with domain capture enabled', async function (ctx) {
+      ctx.Features.hasFeature.withArgs('saas').returns(true)
+      ctx.UserGetter.promises.getUserFullEmails.resolves([
+        {
+          email: 'test@overleaf.com',
+          affiliation: {
+            institution: { id: 1, name: 'Overleaf' },
+            group: {
+              _id: 'g1',
+              domainCaptureEnabled: true,
+              managedUsersEnabled: false,
+            },
+          },
+        },
+      ])
+      ctx.res.render = () => {}
+
+      await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
+
+      expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
+        'setUserProperties',
+        ctx.user._id,
+        sinon.match({ domain_capture: true })
+      )
+    })
+
+    it('should send domain_capture=false when no affiliation has domain capture enabled', async function (ctx) {
+      ctx.Features.hasFeature.withArgs('saas').returns(true)
+      ctx.UserGetter.promises.getUserFullEmails.resolves([
+        {
+          email: 'test@overleaf.com',
+          affiliation: {
+            institution: { id: 1, name: 'Overleaf' },
+          },
+        },
+      ])
+      ctx.res.render = () => {}
+
+      await ctx.ProjectListController.projectListPage(ctx.req, ctx.res)
+
+      expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
+        'setUserProperties',
+        ctx.user._id,
+        sinon.match({ domain_capture: false })
+      )
+    })
+
     it('should show INR Banner for Indian users with free account', async function (ctx) {
       // usersBestSubscription is only available when saas feature is present
       ctx.Features.hasFeature.withArgs('saas').returns(true)

+ 62 - 0
services/web/test/unit/src/Subscription/CustomerIoPlanHelpers.test.mjs

@@ -80,4 +80,66 @@ describe('CustomerIoPlanHelpers', function () {
       expect(properties.past_due).to.equal(false)
     })
   })
+
+  describe('getAffiliationProperties', function () {
+    it('sets enterprise_commons=true when the user has active commons access at an enterprise_commons institution', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([
+        {
+          emailHasInstitutionLicence: true,
+          affiliation: {
+            institution: { commonsAccount: true, enterpriseCommons: true },
+          },
+        },
+      ])
+      expect(properties.enterprise_commons).to.equal(true)
+    })
+
+    it('sets enterprise_commons=false when affiliated with an enterprise_commons institution but without active commons access', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([
+        {
+          emailHasInstitutionLicence: false,
+          affiliation: {
+            institution: { commonsAccount: true, enterpriseCommons: true },
+          },
+        },
+      ])
+      expect(properties.enterprise_commons).to.equal(false)
+    })
+
+    it('sets enterprise_commons=false when active access is not at an enterprise_commons institution', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([
+        {
+          emailHasInstitutionLicence: true,
+          affiliation: {
+            institution: { commonsAccount: true, enterpriseCommons: false },
+          },
+        },
+      ])
+      expect(properties.enterprise_commons).to.equal(false)
+    })
+
+    it('sets enterprise_commons=false when there are no emails', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([])
+      expect(properties.enterprise_commons).to.equal(false)
+    })
+
+    it('sets domain_capture=true when an affiliation has domain capture enabled', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([
+        {
+          affiliation: {
+            institution: {},
+            group: { domainCaptureEnabled: true },
+          },
+        },
+      ])
+      expect(properties.domain_capture).to.equal(true)
+    })
+
+    it('sets domain_capture=false when no affiliation has domain capture enabled', function () {
+      const properties = CustomerIoPlanHelpers.getAffiliationProperties([
+        { affiliation: { institution: {} } },
+      ])
+      expect(properties.domain_capture).to.equal(false)
+    })
+  })
 })

+ 1 - 0
services/web/types/institution.ts

@@ -2,6 +2,7 @@ import { Nullable } from './utils'
 
 export type Institution = {
   commonsAccount: boolean
+  enterpriseCommons: boolean
   writefullCommonsAccount: boolean
   confirmed: boolean
   id: number