Merge pull request #8289 from overleaf/ta-redundant-subscription-email

Create Redundant Subscription Notification on Email Confirmation

GitOrigin-RevId: 77baab93ebaae85d09681051641e663bb680c17e
This commit is contained in:
Timothée Alby 2022-06-08 16:08:11 +02:00 committed by Copybot
parent d94478b4a8
commit e6c7025813
4 changed files with 110 additions and 87 deletions

View file

@ -262,19 +262,10 @@ async function linkAccounts(userId, samlData, auditLog) {
await _removeIdentifier(userId, providerId)
throw error
}
await UserUpdater.promises.confirmEmail(userId, institutionEmail) // will set confirmedAt if not set, and will always update reconfirmedAt
await UserUpdater.promises.confirmEmail(userId, institutionEmail, {
entitlement: hasEntitlement,
}) // will set confirmedAt if not set, and will always update reconfirmedAt
await _sendLinkedEmail(userId, providerName, institutionEmail)
// update v1 affiliations record
if (hasEntitlement) {
await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail)
try {
await redundantSubscription(userId, providerId, providerName)
} catch (error) {
logger.err({ err: error }, 'error checking redundant subscription')
}
} else {
await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail)
}
}
async function unlinkAccounts(

View file

@ -14,6 +14,8 @@ const NewsletterManager = require('../Newsletter/NewsletterManager')
const RecurlyWrapper = require('../Subscription/RecurlyWrapper')
const UserAuditLogHandler = require('./UserAuditLogHandler')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
const _ = require('lodash')
async function _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) {
@ -223,7 +225,7 @@ async function setDefaultEmailAddress(
}
}
async function confirmEmail(userId, email) {
async function confirmEmail(userId, email, affiliationOptions) {
// used for initial email confirmation (non-SSO and SSO)
// also used for reconfirmation of non-SSO emails
const confirmedAt = new Date()
@ -234,9 +236,13 @@ async function confirmEmail(userId, email) {
logger.debug({ userId, email }, 'confirming user email')
try {
await InstitutionsAPI.promises.addAffiliation(userId, email, {
confirmedAt,
})
affiliationOptions = affiliationOptions || {}
affiliationOptions.confirmedAt = confirmedAt
await InstitutionsAPI.promises.addAffiliation(
userId,
email,
affiliationOptions
)
} catch (error) {
throw OError.tag(error, 'problem adding affiliation while confirming email')
}
@ -268,6 +274,40 @@ async function confirmEmail(userId, email) {
throw new Errors.NotFoundError('user id and email do no match')
}
await FeaturesUpdater.promises.refreshFeatures(userId, 'confirm-email')
try {
await maybeCreateRedundantSubscriptionNotification(userId, email)
} catch (error) {
logger.err(
{ err: error },
'error checking redundant subscription on email confirmation'
)
}
}
async function maybeCreateRedundantSubscriptionNotification(userId, email) {
const subscription =
await SubscriptionLocator.promises.getUserIndividualSubscription(userId)
if (!subscription || subscription.groupPlan) {
return
}
const affiliations = await InstitutionsAPI.promises.getUserAffiliations(
userId
)
const confirmedAffiliation = affiliations.find(a => a.email === email)
if (confirmedAffiliation.licence === 'free') {
return
}
await NotificationsBuilder.promises
.redundantPersonalSubscription(
{
institutionId: confirmedAffiliation.institution.id,
institutionName: confirmedAffiliation.institution.name,
},
{ _id: userId }
)
.create()
}
async function removeEmailAddress(userId, email, skipParseEmail = false) {

View file

@ -545,77 +545,6 @@ describe('SAMLIdentityManager', function () {
})
})
describe('redundantSubscription', function () {
const userId = '1bv'
const providerId = 123
const providerName = 'University Name'
describe('with a personal subscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUserIndividualSubscription.resolves(
{
planCode: 'professional',
}
)
})
it('should create redundant personal subscription notification ', async function () {
try {
await this.SAMLIdentityManager.redundantSubscription(
userId,
providerId,
providerName
)
} catch (error) {
expect(error).to.not.exist
}
expect(this.NotificationsBuilder.promises.redundantPersonalSubscription)
.to.have.been.calledOnce
})
})
describe('with a group subscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUserIndividualSubscription.resolves(
{
planCode: 'professional',
groupPlan: true,
}
)
})
it('should create redundant personal subscription notification ', async function () {
try {
await this.SAMLIdentityManager.redundantSubscription(
userId,
providerId,
providerName
)
} catch (error) {
expect(error).to.not.exist
}
expect(this.NotificationsBuilder.promises.redundantPersonalSubscription)
.to.not.have.been.called
})
})
describe('without a personal subscription', function () {
it('should not create redundant personal subscription notification ', async function () {
try {
await this.SAMLIdentityManager.redundantSubscription(
userId,
providerId,
providerName
)
} catch (error) {
expect(error).to.not.exist
}
expect(this.NotificationsBuilder.promises.redundantPersonalSubscription)
.to.not.have.been.called
})
})
})
describe('migrateIdentifier', function () {
const userId = '5efb8b6e9b647b0027e4c0b0'
const externalUserId = '987zyx'

View file

@ -73,6 +73,7 @@ describe('UserUpdater', function () {
promises: {
addAffiliation: sinon.stub().resolves(),
removeAffiliation: sinon.stub().resolves(),
getUserAffiliations: sinon.stub().resolves(),
},
}
this.EmailHandler = {
@ -94,6 +95,20 @@ describe('UserUpdater', function () {
},
}
this.SubscriptionLocator = {
promises: {
getUserIndividualSubscription: sinon.stub().resolves(),
},
}
this.NotificationsBuilder = {
promises: {
redundantPersonalSubscription: sinon
.stub()
.returns({ create: () => {} }),
},
}
this.UserUpdater = SandboxedModule.require(MODULE_PATH, {
requires: {
'../Helpers/Mongo': { normalizeQuery },
@ -109,6 +124,8 @@ describe('UserUpdater', function () {
'./UserAuditLogHandler': this.UserAuditLogHandler,
'../Analytics/AnalyticsManager': this.AnalyticsManager,
'../../Errors/Errors': Errors,
'../Subscription/SubscriptionLocator': this.SubscriptionLocator,
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
},
})
@ -910,5 +927,51 @@ describe('UserUpdater', function () {
this.user._id
)
})
describe('with institution licence and subscription', function () {
beforeEach(async function () {
this.affiliation = {
email: this.newEmail,
licence: 'pro_plus',
institution: {
id: 123,
name: 'Institution',
},
}
this.InstitutionsAPI.promises.getUserAffiliations.resolves([
this.affiliation,
{ email: 'other@email.edu' },
])
this.SubscriptionLocator.promises.getUserIndividualSubscription.resolves(
{
planCode: 'personal',
groupPlan: false,
}
)
})
it('creates redundant subscription notification', async function () {
await this.UserUpdater.promises.confirmEmail(
this.user._id,
this.newEmail
)
sinon.assert.calledWith(
this.InstitutionsAPI.promises.getUserAffiliations,
this.user._id
)
sinon.assert.calledWith(
this.SubscriptionLocator.promises.getUserIndividualSubscription,
this.user._id
)
sinon.assert.calledWith(
this.NotificationsBuilder.promises.redundantPersonalSubscription,
{
institutionId: 123,
institutionName: 'Institution',
},
{ _id: this.user._id }
)
})
})
})
})