mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #2119 from overleaf/ta-subscription-deletion
Store Deleted Subscriptions GitOrigin-RevId: c7004f1807dee6b6ec82eeb2a8fe939801ce3e8b
This commit is contained in:
parent
bc233e6eba
commit
d0fc8d90e5
12 changed files with 286 additions and 32 deletions
|
@ -361,14 +361,16 @@ module.exports = SubscriptionController = {
|
|||
) {
|
||||
const recurlySubscription =
|
||||
req.body['expired_subscription_notification'].subscription
|
||||
return SubscriptionHandler.recurlyCallback(recurlySubscription, function(
|
||||
err
|
||||
) {
|
||||
return SubscriptionHandler.recurlyCallback(
|
||||
recurlySubscription,
|
||||
{ ip: req.ip },
|
||||
function(err) {
|
||||
if (err != null) {
|
||||
return next(err)
|
||||
}
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
|
|
|
@ -209,7 +209,7 @@ const SubscriptionHandler = {
|
|||
})
|
||||
},
|
||||
|
||||
recurlyCallback(recurlySubscription, callback) {
|
||||
recurlyCallback(recurlySubscription, requesterData, callback) {
|
||||
return RecurlyWrapper.getSubscription(
|
||||
recurlySubscription.uuid,
|
||||
{ includeAccount: true },
|
||||
|
@ -230,6 +230,7 @@ const SubscriptionHandler = {
|
|||
return SubscriptionUpdater.syncSubscription(
|
||||
recurlySubscription,
|
||||
user != null ? user._id : undefined,
|
||||
requesterData,
|
||||
callback
|
||||
)
|
||||
})
|
||||
|
|
|
@ -8,6 +8,7 @@ const PlansLocator = require('./PlansLocator')
|
|||
const logger = require('logger-sharelatex')
|
||||
const { ObjectId } = require('mongoose').Types
|
||||
const FeaturesUpdater = require('./FeaturesUpdater')
|
||||
const { DeletedSubscription } = require('../../models/DeletedSubscription')
|
||||
|
||||
const SubscriptionUpdater = {
|
||||
/**
|
||||
|
@ -33,7 +34,11 @@ const SubscriptionUpdater = {
|
|||
Subscription.update(query, update, callback)
|
||||
},
|
||||
|
||||
syncSubscription(recurlySubscription, adminUserId, callback) {
|
||||
syncSubscription(recurlySubscription, adminUserId, requesterData, callback) {
|
||||
if (!callback) {
|
||||
callback = requesterData
|
||||
requesterData = {}
|
||||
}
|
||||
logger.log(
|
||||
{ adminUserId, recurlySubscription },
|
||||
'syncSubscription, creating new if subscription does not exist'
|
||||
|
@ -53,6 +58,7 @@ const SubscriptionUpdater = {
|
|||
SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
recurlySubscription,
|
||||
subscription,
|
||||
requesterData,
|
||||
callback
|
||||
)
|
||||
} else {
|
||||
|
@ -70,6 +76,7 @@ const SubscriptionUpdater = {
|
|||
SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
recurlySubscription,
|
||||
subscription,
|
||||
requesterData,
|
||||
callback
|
||||
)
|
||||
})
|
||||
|
@ -172,11 +179,11 @@ const SubscriptionUpdater = {
|
|||
Subscription.deleteOne({ 'overleaf.id': v1TeamId }, callback)
|
||||
},
|
||||
|
||||
deleteSubscription(subscriptionId, callback) {
|
||||
deleteSubscription(subscription, deleterData, callback) {
|
||||
if (callback == null) {
|
||||
callback = function() {}
|
||||
}
|
||||
SubscriptionLocator.getSubscription(subscriptionId, function(
|
||||
SubscriptionLocator.getSubscription(subscription._id, function(
|
||||
err,
|
||||
subscription
|
||||
) {
|
||||
|
@ -187,10 +194,17 @@ const SubscriptionUpdater = {
|
|||
subscription.member_ids || []
|
||||
)
|
||||
logger.log(
|
||||
{ subscriptionId, affectedUserIds },
|
||||
{ subscriptionId: subscription._id, affectedUserIds },
|
||||
'deleting subscription and downgrading users'
|
||||
)
|
||||
Subscription.remove({ _id: ObjectId(subscriptionId) }, function(err) {
|
||||
SubscriptionUpdater._createDeletedSubscription(
|
||||
subscription,
|
||||
deleterData,
|
||||
error => {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
Subscription.remove({ _id: subscription._id }, function(err) {
|
||||
if (err != null) {
|
||||
return callback(err)
|
||||
}
|
||||
|
@ -200,9 +214,26 @@ const SubscriptionUpdater = {
|
|||
callback
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
_createDeletedSubscription(subscription, deleterData, callback) {
|
||||
subscription.teamInvites = []
|
||||
subscription.invited_emails = []
|
||||
const filter = { 'subscription._id': subscription._id }
|
||||
const data = {
|
||||
deleterData: {
|
||||
deleterId: deleterData.id,
|
||||
deleterIpAddress: deleterData.ip
|
||||
},
|
||||
subscription: subscription
|
||||
}
|
||||
const options = { upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
DeletedSubscription.findOneAndUpdate(filter, data, options, callback)
|
||||
},
|
||||
|
||||
_createNewSubscription(adminUserId, callback) {
|
||||
logger.log({ adminUserId }, 'creating new subscription')
|
||||
const subscription = new Subscription({
|
||||
|
@ -212,10 +243,19 @@ const SubscriptionUpdater = {
|
|||
subscription.save(err => callback(err, subscription))
|
||||
},
|
||||
|
||||
_updateSubscriptionFromRecurly(recurlySubscription, subscription, callback) {
|
||||
_updateSubscriptionFromRecurly(
|
||||
recurlySubscription,
|
||||
subscription,
|
||||
requesterData,
|
||||
callback
|
||||
) {
|
||||
logger.log({ recurlySubscription, subscription }, 'updaing subscription')
|
||||
if (recurlySubscription.state === 'expired') {
|
||||
return SubscriptionUpdater.deleteSubscription(subscription._id, callback)
|
||||
return SubscriptionUpdater.deleteSubscription(
|
||||
subscription,
|
||||
requesterData,
|
||||
callback
|
||||
)
|
||||
}
|
||||
subscription.recurlySubscription_id = recurlySubscription.uuid
|
||||
subscription.planCode = recurlySubscription.plan.plan_code
|
||||
|
|
42
services/web/app/src/models/DeletedSubscription.js
Normal file
42
services/web/app/src/models/DeletedSubscription.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
const mongoose = require('mongoose')
|
||||
const Settings = require('settings-sharelatex')
|
||||
const { SubscriptionSchema } = require('./Subscription')
|
||||
|
||||
const { Schema } = mongoose
|
||||
const { ObjectId } = Schema
|
||||
|
||||
const DeleterDataSchema = new Schema(
|
||||
{
|
||||
deleterId: { type: ObjectId, ref: 'User' },
|
||||
deleterIpAddress: { type: String },
|
||||
deletedAt: {
|
||||
type: Date,
|
||||
default() {
|
||||
return new Date()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ _id: false }
|
||||
)
|
||||
|
||||
const DeletedSubscriptionSchema = new Schema(
|
||||
{
|
||||
deleterData: DeleterDataSchema,
|
||||
subscription: SubscriptionSchema
|
||||
},
|
||||
{ collection: 'deletedSubscriptions' }
|
||||
)
|
||||
|
||||
const conn = mongoose.createConnection(Settings.mongo.url, {
|
||||
server: { poolSize: Settings.mongo.poolSize || 10 },
|
||||
config: { autoIndex: false }
|
||||
})
|
||||
|
||||
const DeletedSubscription = conn.model(
|
||||
'DeletedSubscription',
|
||||
DeletedSubscriptionSchema
|
||||
)
|
||||
|
||||
mongoose.model('DeletedSubscription', DeletedSubscriptionSchema)
|
||||
exports.DeletedSubscription = DeletedSubscription
|
||||
exports.DeletedSubscriptionSchema = DeletedSubscriptionSchema
|
|
@ -0,0 +1,73 @@
|
|||
const { expect } = require('chai')
|
||||
const async = require('async')
|
||||
const request = require('./helpers/request')
|
||||
const User = require('./helpers/User')
|
||||
const RecurlySubscription = require('./helpers/RecurlySubscription')
|
||||
const SubscriptionUpdater = require('../../../app/src/Features/Subscription/SubscriptionUpdater')
|
||||
require('./helpers/MockV1Api')
|
||||
|
||||
describe('Subscriptions', function() {
|
||||
describe('deletion', function() {
|
||||
beforeEach(function(done) {
|
||||
this.adminUser = new User()
|
||||
this.memberUser = new User()
|
||||
async.series(
|
||||
[
|
||||
cb => this.adminUser.ensureUserExists(cb),
|
||||
cb => this.memberUser.ensureUserExists(cb),
|
||||
cb => {
|
||||
this.recurlySubscription = new RecurlySubscription({
|
||||
adminId: this.adminUser._id,
|
||||
memberIds: [this.memberUser._id],
|
||||
invitedEmails: ['foo@bar.com'],
|
||||
teamInvites: [{ email: 'foo@baz.com' }],
|
||||
groupPlan: true,
|
||||
state: 'expired'
|
||||
})
|
||||
this.subscription = this.recurlySubscription.subscription
|
||||
this.recurlySubscription.ensureExists(cb)
|
||||
}
|
||||
],
|
||||
done
|
||||
)
|
||||
})
|
||||
|
||||
it('deletes via Recurly callback', function(done) {
|
||||
let url = '/user/subscription/callback'
|
||||
let body = this.recurlySubscription.buildCallbackXml()
|
||||
|
||||
request.post({ url, body }, (error, { statusCode }) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
expect(statusCode).to.equal(200)
|
||||
this.subscription.expectDeleted({ ip: '127.0.0.1' }, done)
|
||||
})
|
||||
})
|
||||
|
||||
it('allows deletion when deletedSubscription exists', function(done) {
|
||||
let url = '/user/subscription/callback'
|
||||
let body = this.recurlySubscription.buildCallbackXml()
|
||||
|
||||
// create fake deletedSubscription
|
||||
SubscriptionUpdater._createDeletedSubscription(
|
||||
this.subscription,
|
||||
{},
|
||||
error => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
|
||||
// try deleting the subscription
|
||||
request.post({ url, body }, (error, { statusCode }) => {
|
||||
if (error) {
|
||||
return done(error)
|
||||
}
|
||||
expect(statusCode).to.equal(200)
|
||||
this.subscription.expectDeleted({ ip: '127.0.0.1' }, done)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -26,6 +26,14 @@ module.exports = MockRecurlyApi = {
|
|||
|
||||
coupons: {},
|
||||
|
||||
addSubscription(subscription) {
|
||||
this.subscriptions[subscription.uuid] = subscription
|
||||
},
|
||||
|
||||
addAccount(account) {
|
||||
this.accounts[account.id] = account
|
||||
},
|
||||
|
||||
run() {
|
||||
app.get('/subscriptions/:id', (req, res, next) => {
|
||||
const subscription = this.subscriptions[req.params.id]
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
const { ObjectId } = require('../../../../app/src/infrastructure/mongojs')
|
||||
const Subscription = require('./Subscription')
|
||||
const MockRecurlyApi = require('./MockRecurlyApi')
|
||||
const RecurlyWrapper = require('../../../../app/src/Features/Subscription/RecurlyWrapper')
|
||||
|
||||
class RecurlySubscription {
|
||||
constructor(options = {}) {
|
||||
this.subscription = new Subscription(options)
|
||||
|
||||
this.uuid = ObjectId().toString()
|
||||
this.accountId = this.subscription.admin_id.toString()
|
||||
this.state = options.state || 'active'
|
||||
}
|
||||
|
||||
ensureExists(callback) {
|
||||
this.subscription.ensureExists(error => {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
MockRecurlyApi.addSubscription({
|
||||
uuid: this.uuid,
|
||||
account_id: this.accountId,
|
||||
state: this.state
|
||||
})
|
||||
MockRecurlyApi.addAccount({ id: this.accountId })
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
buildCallbackXml() {
|
||||
return RecurlyWrapper._buildXml('expired_subscription_notification', {
|
||||
subscription: {
|
||||
uuid: this.uuid
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RecurlySubscription
|
|
@ -1,12 +1,19 @@
|
|||
const { ObjectId } = require('../../../../app/src/infrastructure/mongojs')
|
||||
const { expect } = require('chai')
|
||||
const SubscriptionModel = require('../../../../app/src/models/Subscription')
|
||||
.Subscription
|
||||
const DeletedSubscriptionModel = require(`../../../../app/src/models/DeletedSubscription`)
|
||||
.DeletedSubscription
|
||||
|
||||
class Subscription {
|
||||
constructor(options = {}) {
|
||||
this.admin_id = options.adminId || ObjectId()
|
||||
this.overleaf = options.overleaf || {}
|
||||
this.groupPlan = options.groupPlan
|
||||
this.manager_ids = []
|
||||
this.member_ids = options.memberIds || []
|
||||
this.invited_emails = options.invitedEmails || []
|
||||
this.teamInvites = options.teamInvites || []
|
||||
}
|
||||
|
||||
ensureExists(callback) {
|
||||
|
@ -32,6 +39,36 @@ class Subscription {
|
|||
callback
|
||||
)
|
||||
}
|
||||
|
||||
expectDeleted(deleterData, callback) {
|
||||
DeletedSubscriptionModel.find(
|
||||
{ 'subscription._id': this._id },
|
||||
(error, deletedSubscriptions) => {
|
||||
if (error) {
|
||||
return callback(error)
|
||||
}
|
||||
expect(deletedSubscriptions.length).to.equal(1)
|
||||
|
||||
const deletedSubscription = deletedSubscriptions[0]
|
||||
expect(deletedSubscription.subscription.teamInvites).to.be.empty
|
||||
expect(deletedSubscription.subscription.invited_emails).to.be.empty
|
||||
expect(deletedSubscription.deleterData.deleterIpAddress).to.equal(
|
||||
deleterData.ip
|
||||
)
|
||||
if (deleterData.id) {
|
||||
expect(deletedSubscription.deleterData.deleterId.toString()).to.equal(
|
||||
deleterData.id.toString()
|
||||
)
|
||||
} else {
|
||||
expect(deletedSubscription.deleterData.deleterId).to.be.undefined
|
||||
}
|
||||
SubscriptionModel.findById(this._id, (error, subscription) => {
|
||||
expect(subscription).to.be.null
|
||||
callback(error)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Subscription
|
||||
|
|
|
@ -60,7 +60,7 @@ describe('SubscriptionController', function() {
|
|||
updateSubscription: sinon.stub().callsArgWith(3),
|
||||
reactivateSubscription: sinon.stub().callsArgWith(1),
|
||||
cancelSubscription: sinon.stub().callsArgWith(1),
|
||||
recurlyCallback: sinon.stub().callsArgWith(1),
|
||||
recurlyCallback: sinon.stub().yields(),
|
||||
startFreeTrial: sinon.stub()
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('SubscriptionHandler', function() {
|
|||
this.DropboxHandler = { unlinkAccount: sinon.stub().callsArgWith(1) }
|
||||
|
||||
this.SubscriptionUpdater = {
|
||||
syncSubscription: sinon.stub().callsArgWith(2),
|
||||
syncSubscription: sinon.stub().yields(),
|
||||
startFreeTrial: sinon.stub().callsArgWith(1)
|
||||
}
|
||||
|
||||
|
@ -391,6 +391,7 @@ describe('SubscriptionHandler', function() {
|
|||
}
|
||||
return this.SubscriptionHandler.recurlyCallback(
|
||||
this.activeRecurlySubscription,
|
||||
{},
|
||||
done
|
||||
)
|
||||
})
|
||||
|
|
|
@ -94,6 +94,9 @@ describe('SubscriptionUpdater', function() {
|
|||
this.FeaturesUpdater = {
|
||||
refreshFeatures: sinon.stub().yields()
|
||||
}
|
||||
this.DeletedSubscription = {
|
||||
findOneAndUpdate: sinon.stub().yields()
|
||||
}
|
||||
this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
|
||||
globals: {
|
||||
console: console
|
||||
|
@ -111,7 +114,10 @@ describe('SubscriptionUpdater', function() {
|
|||
warn() {}
|
||||
},
|
||||
'settings-sharelatex': this.Settings,
|
||||
'./FeaturesUpdater': this.FeaturesUpdater
|
||||
'./FeaturesUpdater': this.FeaturesUpdater,
|
||||
'../../models/DeletedSubscription': {
|
||||
DeletedSubscription: this.DeletedSubscription
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -153,7 +159,7 @@ describe('SubscriptionUpdater', function() {
|
|||
)
|
||||
this.SubscriptionUpdater._updateSubscriptionFromRecurly = sinon
|
||||
.stub()
|
||||
.callsArgWith(2)
|
||||
.yields()
|
||||
})
|
||||
|
||||
it('should update the subscription if the user already is admin of one', function(done) {
|
||||
|
@ -214,6 +220,7 @@ describe('SubscriptionUpdater', function() {
|
|||
this.SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
this.recurlySubscription,
|
||||
this.subscription,
|
||||
{},
|
||||
err => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
|
@ -238,12 +245,13 @@ describe('SubscriptionUpdater', function() {
|
|||
this.SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
this.recurlySubscription,
|
||||
this.subscription,
|
||||
{},
|
||||
err => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
}
|
||||
this.SubscriptionUpdater.deleteSubscription
|
||||
.calledWith(this.subscription._id)
|
||||
.calledWithMatch(this.subscription)
|
||||
.should.equal(true)
|
||||
done()
|
||||
}
|
||||
|
@ -254,6 +262,7 @@ describe('SubscriptionUpdater', function() {
|
|||
this.SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
this.recurlySubscription,
|
||||
this.subscription,
|
||||
{},
|
||||
err => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
|
@ -282,6 +291,7 @@ describe('SubscriptionUpdater', function() {
|
|||
this.SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
this.recurlySubscription,
|
||||
this.subscription,
|
||||
{},
|
||||
err => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
|
@ -297,6 +307,7 @@ describe('SubscriptionUpdater', function() {
|
|||
this.SubscriptionUpdater._updateSubscriptionFromRecurly(
|
||||
this.recurlySubscription,
|
||||
this.subscription,
|
||||
{},
|
||||
err => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
|
@ -441,8 +452,8 @@ describe('SubscriptionUpdater', function() {
|
|||
|
||||
describe('deleteSubscription', function() {
|
||||
beforeEach(function(done) {
|
||||
this.subscription_id = ObjectId().toString()
|
||||
this.subscription = {
|
||||
_id: ObjectId().toString(),
|
||||
mock: 'subscription',
|
||||
admin_id: ObjectId(),
|
||||
member_ids: [ObjectId(), ObjectId(), ObjectId()]
|
||||
|
@ -451,18 +462,18 @@ describe('SubscriptionUpdater', function() {
|
|||
.stub()
|
||||
.yields(null, this.subscription)
|
||||
this.FeaturesUpdater.refreshFeatures = sinon.stub().yields()
|
||||
this.SubscriptionUpdater.deleteSubscription(this.subscription_id, done)
|
||||
this.SubscriptionUpdater.deleteSubscription(this.subscription, {}, done)
|
||||
})
|
||||
|
||||
it('should look up the subscription', function() {
|
||||
this.SubscriptionLocator.getSubscription
|
||||
.calledWith(this.subscription_id)
|
||||
.calledWith(this.subscription._id)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should remove the subscription', function() {
|
||||
this.SubscriptionModel.remove
|
||||
.calledWith({ _id: ObjectId(this.subscription_id) })
|
||||
.calledWith({ _id: this.subscription._id })
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue