Merge pull request #2609 from overleaf/ta-cmg-recurly-email-update

Add UI to Update Recurly Email

GitOrigin-RevId: 920a741fd9b4312f031bdd40e3d6bec48f1bd579
This commit is contained in:
Timothée Alby 2020-02-20 11:08:30 -05:00 committed by Copybot
parent 506543d6a0
commit f5e2983a6b
12 changed files with 213 additions and 6 deletions

View file

@ -574,6 +574,27 @@ module.exports = RecurlyWrapper = {
)
},
updateAccountEmailAddress(accountId, newEmail, callback) {
const data = {
email: newEmail
}
const requestBody = RecurlyWrapper._buildXml('account', data)
RecurlyWrapper.apiRequest(
{
url: `accounts/${accountId}`,
method: 'PUT',
body: requestBody
},
(error, response, body) => {
if (error != null) {
return callback(error)
}
RecurlyWrapper._parseAccountXml(body, callback)
}
)
},
getAccountActiveCoupons(accountId, callback) {
return RecurlyWrapper.apiRequest(
{

View file

@ -326,6 +326,18 @@ module.exports = SubscriptionController = {
)
},
updateAccountEmailAddress(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
RecurlyWrapper.updateAccountEmailAddress(user._id, user.email, function(
error
) {
if (error) {
return next(new HttpErrors.InternalServerError({}).withCause(error))
}
res.sendStatus(200)
})
},
reactivateSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'reactivating subscription')

View file

@ -127,6 +127,12 @@ module.exports = {
SubscriptionController.processUpgradeToAnnualPlan
)
webRouter.post(
'/user/subscription/account/email',
AuthenticationController.requireLogin(),
SubscriptionController.updateAccountEmailAddress
)
// Currently used in acceptance tests only, as a way to trigger the syncing logic
return publicApiRouter.post(
'/user/:user_id/features/sync',

View file

@ -223,7 +223,8 @@ module.exports = {
: undefined
),
trial_ends_at: recurlySubscription.trial_ends_at,
activeCoupons: recurlyCoupons
activeCoupons: recurlyCoupons,
account: recurlySubscription.account
}
}

View file

@ -1,6 +1,7 @@
if (personalSubscription.recurly)
include ./_personal_subscription_recurly
include ./_personal_subscription_recurly_sync_email
else
include ./_personal_subscription_custom
hr
hr

View file

@ -0,0 +1,18 @@
-if (user.email !== personalSubscription.recurly.account.email)
div
hr
form(async-form="updateAccountEmailAddress", name="updateAccountEmailAddress", action='/user/subscription/account/email', method="POST")
input(name='_csrf', type='hidden', value=csrfToken)
.form-group
form-messages(for="updateAccountEmailAddress")
.alert.alert-success(ng-show="updateAccountEmailAddress.response.success")
| #{translate('recurly_email_updated')}
div(ng-hide="updateAccountEmailAddress.response.success")
p !{translate("recurly_email_update_needed", { recurlyEmail: "<i>" + personalSubscription.recurly.account.email + "</i>", userEmail: "<i>" + user.email + "</i>" })}
.actions
button.btn-primary.btn(
type='submit',
ng-disabled="updateAccountEmailAddress.inflight"
)
span(ng-show="!updateAccountEmailAddress.inflight") #{translate("update")}
span(ng-show="updateAccountEmailAddress.inflight") #{translate("updating")}...

View file

@ -0,0 +1,43 @@
const { expect } = require('chai')
const async = require('async')
const User = require('./helpers/User')
const RecurlySubscription = require('./helpers/RecurlySubscription')
require('./helpers/MockV1Api')
describe('Subscriptions', function() {
describe('update', function() {
beforeEach(function(done) {
this.recurlyUser = new User()
async.series(
[
cb => this.recurlyUser.ensureUserExists(cb),
cb => {
this.recurlySubscription = new RecurlySubscription({
adminId: this.recurlyUser._id,
account: {
email: 'stale-recurly@email.com'
}
})
this.recurlySubscription.ensureExists(cb)
},
cb => this.recurlyUser.login(cb)
],
done
)
})
it('updates the email address for the account', function(done) {
let url = '/user/subscription/account/email'
this.recurlyUser.request.post({ url }, (error, { statusCode }) => {
if (error) {
return done(error)
}
expect(statusCode).to.equal(200)
// the actual email update is not tested as the mocked Recurly API
// doesn't handle it
done()
})
})
})
})

View file

@ -62,7 +62,8 @@ describe('Subscriptions', function() {
describe('when the user has a subscription with recurly', function() {
beforeEach(function(done) {
MockRecurlyApi.accounts['mock-account-id'] = this.accounts = {
hosted_login_token: 'mock-login-token'
hosted_login_token: 'mock-login-token',
email: 'mock@email.com'
}
MockRecurlyApi.subscriptions[
'mock-subscription-id'
@ -138,7 +139,12 @@ describe('Subscriptions', function() {
tax: 100,
taxRate: 0.2,
trial_ends_at: new Date(2018, 6, 7),
trialEndsAtFormatted: '7th July 2018'
trialEndsAtFormatted: '7th July 2018',
account: {
account_code: 'mock-account-id',
email: 'mock@email.com',
hosted_login_token: 'mock-login-token'
}
})
})
@ -176,6 +182,12 @@ describe('Subscriptions', function() {
}
)
})
it('should return Recurly account email', function() {
expect(this.data.personalSubscription.recurly.account.email).to.equal(
'mock@email.com'
)
})
})
describe('when the user has a subscription without recurly', function() {

View file

@ -14,6 +14,7 @@ let MockRecurlyApi
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const SubscriptionController = require('../../../../app/src/Features/Subscription/SubscriptionController')
app.use(bodyParser.json())
@ -69,11 +70,30 @@ module.exports = MockRecurlyApi = {
<account>
<account_code>${req.params.id}</account_code>
<hosted_login_token>${account.hosted_login_token}</hosted_login_token>
<email>${account.email}</email>
</account>\
`)
}
})
app.put(
'/accounts/:id',
SubscriptionController.recurlyNotificationParser, // required to parse XML requests
(req, res, next) => {
const account = this.accounts[req.params.id]
if (account == null) {
return res.status(404).end()
} else {
return res.send(`\
<account>
<account_code>${req.params.id}</account_code>
<email>${account.email}</email>
</account>\
`)
}
}
)
app.get('/coupons/:code', (req, res, next) => {
const coupon = this.coupons[req.params.code]
if (coupon == null) {

View file

@ -10,6 +10,9 @@ class RecurlySubscription {
this.uuid = ObjectId().toString()
this.accountId = this.subscription.admin_id.toString()
this.state = options.state || 'active'
this.account = {
email: options.account && options.account.email
}
}
ensureExists(callback) {
@ -22,7 +25,10 @@ class RecurlySubscription {
account_id: this.accountId,
state: this.state
})
MockRecurlyApi.addAccount({ id: this.accountId })
MockRecurlyApi.addAccount({
id: this.accountId,
email: this.account.email
})
callback()
})
}

View file

@ -257,6 +257,49 @@ describe('RecurlyWrapper', function() {
})
})
describe('updateAccountEmailAddress', function() {
beforeEach(function(done) {
this.recurlyAccountId = 'account-id-123'
this.newEmail = 'example@overleaf.com'
this.apiRequest = sinon
.stub(this.RecurlyWrapper, 'apiRequest')
.callsFake((options, callback) => {
this.requestOptions = options
callback(null, {}, fixtures['accounts/104'])
})
this.RecurlyWrapper.updateAccountEmailAddress(
this.recurlyAccountId,
this.newEmail,
(error, recurlyAccount) => {
this.recurlyAccount = recurlyAccount
done()
}
)
})
afterEach(function() {
return this.RecurlyWrapper.apiRequest.restore()
})
it('sends correct XML', function() {
this.apiRequest.called.should.equal(true)
const { body } = this.apiRequest.lastCall.args[0]
expect(body).to.equal(`\
<account>
<email>example@overleaf.com</email>
</account>\
`)
this.requestOptions.url.should.equal(`accounts/${this.recurlyAccountId}`)
this.requestOptions.method.should.equal('PUT')
})
it('should return the updated account', function() {
should.exist(this.recurlyAccount)
this.recurlyAccount.account_code.should.equal('104')
})
})
describe('updateSubscription', function() {
beforeEach(function(done) {
this.recurlySubscriptionId = 'subscription-id-123'

View file

@ -115,7 +115,9 @@ describe('SubscriptionController', function() {
},
'settings-sharelatex': this.settings,
'../User/UserGetter': this.UserGetter,
'./RecurlyWrapper': (this.RecurlyWrapper = {}),
'./RecurlyWrapper': (this.RecurlyWrapper = {
updateAccountEmailAddress: sinon.stub().yields()
}),
'./FeaturesUpdater': (this.FeaturesUpdater = {}),
'./GroupPlansData': (this.GroupPlansData = {}),
'./V1SubscriptionManager': (this.V1SubscriptionManager = {}),
@ -477,6 +479,28 @@ describe('SubscriptionController', function() {
})
})
describe('updateAccountEmailAddress via put', function() {
beforeEach(function(done) {
this.res = {
sendStatus() {
return done()
}
}
sinon.spy(this.res, 'sendStatus')
this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
})
it('should send the user and subscriptionId to RecurlyWrapper', function() {
this.RecurlyWrapper.updateAccountEmailAddress
.calledWith(this.user._id, this.user.email)
.should.equal(true)
})
it('shouldrespond with 200', function() {
this.res.sendStatus.calledWith(200).should.equal(true)
})
})
describe('reactivateSubscription', function() {
beforeEach(function(done) {
this.res = {