diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
index fddd4f3567..774707a2ea 100644
--- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
+++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee
@@ -7,7 +7,7 @@ logger = require("logger-sharelatex")
Async = require('async')
module.exports = RecurlyWrapper =
- apiUrl : "https://api.recurly.com/v2"
+ apiUrl : Settings.apis?.recurly?.url or "https://api.recurly.com/v2"
_addressToXml: (address) ->
allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 1d35b12df9..17447ba2e4 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -89,47 +89,19 @@ module.exports = SubscriptionController =
userSubscriptionPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
- LimitationsManager.hasPaidSubscription user, (err, hasPaidSubscription, subscription)->
- return next(err) if err?
- groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
- if subscription?.customAccount
- logger.log user: user, "redirecting to custom account page"
- res.redirect "/user/subscription/custom_account"
- else if groupLicenceInviteUrl? and !hasPaidSubscription
- logger.log user:user, "redirecting to group subscription invite page"
- res.redirect groupLicenceInviteUrl
- else if !hasPaidSubscription
- logger.log user: user, "redirecting to plans"
- res.redirect "/user/subscription/plans"
- else
- SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions, billingDetailsLink, v1Subscriptions) ->
- return next(error) if error?
- logger.log {user, subscription, hasPaidSubscription, groupSubscriptions, billingDetailsLink, v1Subscriptions}, "showing subscription dashboard"
- plans = SubscriptionViewModelBuilder.buildViewModel()
- res.render "subscriptions/dashboard",
- title: "your_subscription"
- recomendedCurrency: subscription?.currency
- taxRate:subscription?.taxRate
- plans: plans
- subscription: subscription || {}
- groupSubscriptions: groupSubscriptions
- subscriptionTabActive: true
- user:user
- saved_billing_details: req.query.saved_billing_details?
- billingDetailsLink: billingDetailsLink
- v1Subscriptions: v1Subscriptions
-
- userCustomSubscriptionPage: (req, res, next)->
- user = AuthenticationController.getSessionUser(req)
- LimitationsManager.hasPaidSubscription user, (err, hasPaidSubscription, subscription)->
- return next(err) if err?
- if !subscription?
- err = new Error("subscription null for custom account, user:#{user?._id}")
- logger.warn err:err, "subscription is null for custom accounts page"
- return next(err)
- res.render "subscriptions/custom_account",
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, results) ->
+ return next(error) if error?
+ { personalSubscription, groupSubscriptions, v1Subscriptions } = results
+ logger.log {user, personalSubscription, groupSubscriptions, v1Subscriptions}, "showing subscription dashboard"
+ plans = SubscriptionViewModelBuilder.buildViewModel()
+ data =
title: "your_subscription"
- subscription: subscription
+ plans: plans
+ user: user
+ personalSubscription: personalSubscription
+ groupSubscriptions: groupSubscriptions
+ v1Subscriptions: v1Subscriptions
+ res.render "subscriptions/dashboard", data
createSubscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
@@ -150,11 +122,13 @@ module.exports = SubscriptionController =
successful_subscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
- SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, {personalSubscription}) ->
return next(error) if error?
+ if !personalSubscription?
+ return res.redirect '/user/subscription/plans'
res.render "subscriptions/successful_subscription",
title: "thank_you"
- subscription:subscription
+ subscription:personalSubscription
cancelSubscription: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee
index c4af40356d..f0929a08cf 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionFormatters.coffee
@@ -29,4 +29,5 @@ module.exports =
return "#{symbol}#{dollars}.#{cents}"
formatDate: (date) ->
+ return null if !date?
dateformat date, "dS mmmm yyyy"
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
index e8c9b916a2..d81b5f3f59 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee
@@ -13,8 +13,6 @@ module.exports =
webRouter.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage
- webRouter.get '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage
-
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
index 0e5530856c..f4cdfcf86d 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee
@@ -75,7 +75,6 @@ module.exports = SubscriptionUpdater =
_createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription"
subscription = new Subscription(admin_id:adminUser_id, manager_ids: [adminUser_id])
- subscription.freeTrial.allowed = false
subscription.save (err)->
callback err, subscription
@@ -84,9 +83,6 @@ module.exports = SubscriptionUpdater =
if recurlySubscription.state == "expired"
return SubscriptionUpdater.deleteSubscription subscription._id, callback
subscription.recurlySubscription_id = recurlySubscription.uuid
- subscription.freeTrial.expiresAt = undefined
- subscription.freeTrial.planCode = undefined
- subscription.freeTrial.allowed = true
subscription.planCode = recurlySubscription.plan.plan_code
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
if !plan?
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
index eee2c33f6b..2293e5c057 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
@@ -7,6 +7,7 @@ SubscriptionLocator = require("./SubscriptionLocator")
V1SubscriptionManager = require("./V1SubscriptionManager")
logger = require('logger-sharelatex')
_ = require("underscore")
+async = require('async')
buildBillingDetails = (recurlySubscription) ->
@@ -21,44 +22,58 @@ buildBillingDetails = (recurlySubscription) ->
].join("")
module.exports =
- buildUsersSubscriptionViewModel: (user, callback = (error, subscription, memberSubscriptions, billingDetailsLink) ->) ->
- SubscriptionLocator.getUsersSubscription user, (err, subscription) ->
+ buildUsersSubscriptionViewModel: (user, callback = (error, data) ->) ->
+ async.auto {
+ personalSubscription: (cb) ->
+ SubscriptionLocator.getUsersSubscription user, cb
+ recurlySubscription: ['personalSubscription', (cb, {personalSubscription}) ->
+ if !personalSubscription?.recurlySubscription_id? or personalSubscription?.recurlySubscription_id == ''
+ return cb(null, null)
+ RecurlyWrapper.getSubscription personalSubscription.recurlySubscription_id, includeAccount: true, cb
+ ]
+ plan: ['personalSubscription', (cb, {personalSubscription}) ->
+ return cb() if !personalSubscription?
+ plan = PlansLocator.findLocalPlanInSettings(personalSubscription.planCode)
+ return cb(new Error("No plan found for planCode '#{personalSubscription.planCode}'")) if !plan?
+ cb(null, plan)
+ ]
+ groupSubscriptions: (cb) ->
+ SubscriptionLocator.getMemberSubscriptions user, cb
+ v1Subscriptions: (cb) ->
+ V1SubscriptionManager.getSubscriptionsFromV1 user._id, (error, subscriptions, v1Id) ->
+ return cb(error) if error?
+ # Only return one argument to async.auto, otherwise it returns an array
+ cb(null, subscriptions)
+ }, (err, results) ->
return callback(err) if err?
+ {personalSubscription, groupSubscriptions, v1Subscriptions, recurlySubscription, plan} = results
+ groupSubscriptions ?= []
+ v1Subscriptions ?= {}
- SubscriptionLocator.getMemberSubscriptions user, (err, memberSubscriptions = []) ->
- return callback(err) if err?
+ if personalSubscription?.toObject?
+ # Downgrade from Mongoose object, so we can add a recurly and plan attribute
+ personalSubscription = personalSubscription.toObject()
- V1SubscriptionManager.getSubscriptionsFromV1 user._id, (err, v1Subscriptions) ->
- return callback(err) if err?
+ if plan?
+ personalSubscription.plan = plan
- if subscription?
- return callback(error) if error?
+ if personalSubscription? and recurlySubscription?
+ tax = recurlySubscription?.tax_in_cents || 0
+ personalSubscription.recurly = {
+ tax: tax
+ taxRate: parseFloat(recurlySubscription?.tax_rate?._)
+ billingDetailsLink: buildBillingDetails(recurlySubscription)
+ price: SubscriptionFormatters.formatPrice (recurlySubscription?.unit_amount_in_cents + tax), recurlySubscription?.currency
+ nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at)
+ currency: recurlySubscription.currency
+ state: recurlySubscription.state
+ trialEndsAtFormatted: SubscriptionFormatters.formatDate(recurlySubscription?.trial_ends_at)
+ trial_ends_at: recurlySubscription.trial_ends_at
+ }
- plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
-
- if !plan?
- err = new Error("No plan found for planCode '#{subscription.planCode}'")
- logger.error {user_id: user._id, err}, "error getting subscription plan for user"
- return callback(err)
-
- RecurlyWrapper.getSubscription subscription.recurlySubscription_id, includeAccount: true, (err, recurlySubscription)->
- tax = recurlySubscription?.tax_in_cents || 0
-
- callback null, {
- admin_id:subscription.admin_id
- name: plan.name
- nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at)
- state: recurlySubscription?.state
- price: SubscriptionFormatters.formatPrice (recurlySubscription?.unit_amount_in_cents + tax), recurlySubscription?.currency
- planCode: subscription.planCode
- currency:recurlySubscription?.currency
- taxRate:parseFloat(recurlySubscription?.tax_rate?._)
- groupPlan: subscription.groupPlan
- trial_ends_at:recurlySubscription?.trial_ends_at
- }, memberSubscriptions, buildBillingDetails(recurlySubscription), v1Subscriptions
-
- else
- callback null, null, memberSubscriptions, null, v1Subscriptions
+ callback null, {
+ personalSubscription, groupSubscriptions, v1Subscriptions
+ }
buildViewModel : ->
plans = Settings.plans
diff --git a/services/web/app/coffee/models/Subscription.coffee b/services/web/app/coffee/models/Subscription.coffee
index ac3043e3f6..d094f88971 100644
--- a/services/web/app/coffee/models/Subscription.coffee
+++ b/services/web/app/coffee/models/Subscription.coffee
@@ -18,11 +18,6 @@ SubscriptionSchema = new Schema
groupPlan : {type: Boolean, default: false}
membersLimit: {type:Number, default:0}
customAccount: Boolean
- freeTrial:
- expiresAt: Date
- downgraded: Boolean
- planCode: String
- allowed: {type: Boolean, default: true}
overleaf:
id:
type: Number
diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee
index b08b310135..fcfb06742b 100644
--- a/services/web/app/coffee/models/User.coffee
+++ b/services/web/app/coffee/models/User.coffee
@@ -55,17 +55,6 @@ UserSchema = new Schema
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
refered_user_count: { type:Number, default: 0 }
- subscription:
- recurlyToken : String
- freeTrialExpiresAt: Date
- freeTrialDowngraded: Boolean
- freeTrialPlanCode: String
- # This is poorly named. It does not directly correspond
- # to whether the user has has a free trial, but rather
- # whether they should be allowed one in the future.
- # For example, a user signing up directly for a paid plan
- # has this set to true, despite never having had a free trial
- hadFreeTrial: {type: Boolean, default: false}
refProviders: {
mendeley: Boolean # coerce the refProviders values to Booleans
zotero: Boolean
diff --git a/services/web/app/views/subscriptions/custom_account.pug b/services/web/app/views/subscriptions/custom_account.pug
deleted file mode 100644
index 483517d873..0000000000
--- a/services/web/app/views/subscriptions/custom_account.pug
+++ /dev/null
@@ -1,15 +0,0 @@
-extends ../layout
-
-block content
- .content.content-alt
- .container
- .row
- .col-md-8.col-md-offset-2
- .card
- .page-header
- h1 #{translate("your_subscription")}
- div To make changes to your subscription please contact accounts@sharelatex.com
- div
- div
- -if(subscription.groupPlan)
- a(href="/subscription/group").btn.btn-success !{translate("manage_group")}
diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug
index 816b5c32ba..07981e5de0 100644
--- a/services/web/app/views/subscriptions/dashboard.pug
+++ b/services/web/app/views/subscriptions/dashboard.pug
@@ -1,201 +1,27 @@
extends ../layout
-block scripts
- script(src="https://js.recurly.com/v3/recurly.js")
- script(type='text/javascript').
-
- window.recomendedCurrency = '#{recomendedCurrency}'
- window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
- window.taxRate = #{taxRate}
- window.subscription = !{JSON.stringify(subscription)}
-
-
-
-mixin printPlan(plan)
- -if (!plan.hideFromUsers)
- tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan), ng-show="shouldShowPlan(plan.planCode)")
- td
- strong #{plan.name}
- td {{refreshPrice(plan.planCode)}}
- -if (plan.annual)
- | {{prices[plan.planCode]}} / #{translate("year")}
- -else
- | {{prices[plan.planCode]}} / #{translate("month")}
- td
- -if (subscription.state == "free-trial")
- a(href="/user/subscription/new?planCode="+plan.planCode).btn.btn-success #{translate("subscribe_to_this_plan")}
- -else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
- button.btn.disabled #{translate("your_plan")}
- -else
- form
- input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
- input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
-
-
-mixin printPlans(plans)
- each plan in plans
- +printPlan(plan)
block content
.content.content-alt(ng-cloak)
- .container(ng-controller="UserSubscriptionController")
+ .container
.row
.col-md-8.col-md-offset-2
- if saved_billing_details
- .alert.alert-success
- i.fa.fa-check(aria-hidden="true")
- |
- | #{translate("your_billing_details_were_saved")}
- .card(ng-if="view == 'overview'")
- .page-header(x-current-plan=subscription.planCode)
+ .card
+ .page-header
h1 #{translate("your_subscription")}
- - if (subscription && user._id+'' == subscription.admin_id+'')
- case subscription.state
- when "free-trial"
- p !{translate("on_free_trial_expiring_at", {expiresAt:"" + subscription.expiresAt + ""})}
-
- when "active"
- p !{translate("currently_subscribed_to_plan", {planName:"" + subscription.name + ""})}
- span(ng-show="!isNextGenPlan")
- a(href, ng-click="changePlan = true") !{translate("change_plan")}.
- p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"" + subscription.price + "", collectionDate:"" + subscription.nextPaymentDueAt + ""})}
- p.pull-right
- p
- if billingDetailsLink
- a(href=billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")}
- else
- a(href=billingDetailsLink, disabled).btn.btn-info.btn-disabled #{translate("update_your_billing_details")}
- |
- a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
- when "canceled"
- p !{translate("currently_subscribed_to_plan", {planName:"" + subscription.name + ""})}
- p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"" + subscription.nextPaymentDueAt + ""})}
- p: form(action="/user/subscription/reactivate",method="post")
- input(type="hidden", name="_csrf", value=csrfToken)
- input(type="submit",value="Reactivate your subscription").btn.btn-success
- when "expired"
- p !{translate("your_subscription_has_expired")}
- a(href="/user/subscription/plans") !{translate("create_new_subscription")}
- default
- -if(groupSubscriptions.length == 0)
- p !{translate("problem_with_subscription_contact_us")}
+ -var hasAnySubscription = false
+ -if (personalSubscription)
+ -hasAnySubscription = true
+ include ./dashboard/_personal_subscription
- div(ng-show="changePlan", ng-cloak)#changePlanSection
- h2.col-md-7 !{translate("change_plan")}
- span.dropdown.col-md-1.changePlanButton(ng-controller="CurrenyDropdownController", style="display:none", dropdown)
- a.btn.btn-primary.dropdown-toggle(
- href="#",
- data-toggle="dropdown",
- dropdown-toggle
- )
- | {{currencyCode}} ({{plans[currencyCode]['symbol']}})
- ul.dropdown-menu(role="menu")
- li(ng-repeat="(currency, value) in plans", dropdown-toggle)
- a(
- href,
- ng-click="changeCurrency(currency)"
- ) {{currency}} ({{value['symbol']}})
- p: table.table
- tr
- th !{translate("name")}
- th !{translate("price")}
- th
- +printPlans(plans.studentAccounts)
- +printPlans(plans.individualMonthlyPlans)
- +printPlans(plans.individualAnnualPlans)
- hr
+ -if (groupSubscriptions && groupSubscriptions.length > 0)
+ -hasAnySubscription = true
+ include ./dashboard/_group_memberships
- each groupSubscription in groupSubscriptions
- - if (user._id+'' != groupSubscription.admin_id._id+'')
- div
- p !{translate("member_of_group_subscription", {admin_email: "" + groupSubscription.admin_id.email + ""})}
- span
- button.btn.btn-danger.text-capitalise(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
- hr
+ -if (settings.overleaf && v1Subscriptions)
+ include ./dashboard/_v1_subscriptions
- -if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
- div
- a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
- hr
-
- if settings.overleaf && v1Subscriptions && v1Subscriptions.has_subscription
- p
- | You are subscribed to Overleaf through Overleaf v1
- p
- a.btn.btn-primary(href='/sign_in_to_v1?return_to=/users/edit%23status') Manage v1 Subscription
- hr
-
- if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0
- for team in v1Subscriptions.teams
- p
- | You are a member of the Overleaf v1 team: #{team.name}
- p
- a.btn.btn-primary(href="/sign_in_to_v1?return_to=/teams") Manage v1 Team Membership
- hr
-
- .card(ng-if="view == 'cancelation'")
- .page-header
- h1 #{translate("Cancel Subscription")}
-
- div(ng-show="showExtendFreeTrial", style="text-align: center")
- p !{translate("have_more_days_to_try", {days:14})}
- button(type="submit", ng-click="exendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
- p
- |
- p
- a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
-
- div(ng-show="showDowngradeToStudent", style="text-align: center")
- span(ng-controller="ChangePlanFormController")
- p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})}
- button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-success #{translate("yes_please")}
- p
- |
- p
- a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
-
- div(ng-show="showBasicCancel")
- p #{translate("sure_you_want_to_cancel")}
- a(href="/project").btn.btn-info #{translate("i_want_to_stay")}
- |
- a(ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
-
- script(type="text/javascript").
- $('#cancelSubscription').on("click", function() {
- ga('send', 'event', 'subscription-funnel', 'cancelation')
- })
-
- script(type='text/ng-template', id='confirmChangePlanModalTemplate')
- .modal-header
- h3 #{translate("change_plan")}
- .modal-body
- p !{translate("sure_you_want_to_change_plan", {planName:"{{plan.name}}"})}
- .modal-footer
- button.btn.btn-default(
- ng-disabled="inflight"
- ng-click="cancel()"
- ) #{translate("cancel")}
- button.btn.btn-success(
- ng-disabled="state.inflight"
- ng-click="confirmChangePlan()"
- )
- span(ng-hide="inflight") #{translate("change_plan")}
- span(ng-show="inflight") #{translate("processing")}...
-
-
- script(type='text/ng-template', id='LeaveGroupModalTemplate')
- .modal-header
- h3 #{translate("leave_group")}
- .modal-body
- p #{translate("sure_you_want_to_leave_group")}
- .modal-footer
- button.btn.btn-default(
- ng-disabled="inflight"
- ng-click="cancel()"
- ) #{translate("cancel")}
- button.btn.btn-danger(
- ng-disabled="state.inflight"
- ng-click="confirmLeaveGroup()"
- )
- span(ng-hide="inflight") #{translate("leave_now")}
- span(ng-show="inflight") #{translate("processing")}...
+ -if (!hasAnySubscription)
+ p You're on the #{settings.appName} Free plan.
+ |
+ a(href="/user/subscription/plans").btn.btn-primary Upgrade now
diff --git a/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug b/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug
new file mode 100644
index 0000000000..bdc75743c5
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug
@@ -0,0 +1,21 @@
+mixin printPlan(plan)
+ -if (!plan.hideFromUsers)
+ tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan))
+ td
+ strong #{plan.name}
+ td
+ -if (plan.annual)
+ | {{price}} / #{translate("year")}
+ -else
+ | {{price}} / #{translate("month")}
+ td
+ -if (typeof(personalSubscription.planCode) != "undefined" && plan.planCode == personalSubscription.planCode.split("_")[0])
+ button.btn.disabled #{translate("your_plan")}
+ -else
+ form
+ input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
+ input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
+
+mixin printPlans(plans)
+ each plan in plans
+ +printPlan(plan)
\ No newline at end of file
diff --git a/services/web/app/views/subscriptions/dashboard/_group_memberships.pug b/services/web/app/views/subscriptions/dashboard/_group_memberships.pug
new file mode 100644
index 0000000000..0410de0751
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_group_memberships.pug
@@ -0,0 +1,25 @@
+div(ng-controller="GroupMembershipController")
+ each groupSubscription in groupSubscriptions
+ - if (user._id+'' != groupSubscription.admin_id._id+'')
+ div
+ p !{translate("member_of_group_subscription", {admin_email: "" + groupSubscription.admin_id.email + ""})}
+ span
+ button.btn.btn-danger.text-capitalise(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
+ hr
+
+script(type='text/ng-template', id='LeaveGroupModalTemplate')
+ .modal-header
+ h3 #{translate("leave_group")}
+ .modal-body
+ p #{translate("sure_you_want_to_leave_group")}
+ .modal-footer
+ button.btn.btn-default(
+ ng-disabled="inflight"
+ ng-click="cancel()"
+ ) #{translate("cancel")}
+ button.btn.btn-danger(
+ ng-disabled="state.inflight"
+ ng-click="confirmLeaveGroup()"
+ )
+ span(ng-hide="inflight") #{translate("leave_now")}
+ span(ng-show="inflight") #{translate("processing")}...
diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug
new file mode 100644
index 0000000000..6c9fa58fdc
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug
@@ -0,0 +1,13 @@
+if (personalSubscription.recurly)
+ include ./_personal_subscription_recurly
+else
+ include ./_personal_subscription_custom
+
+if(personalSubscription.groupPlan)
+ hr
+ p
+ | You are the manager of a group subscription
+ p
+ a(href="/subscription/group").btn.btn-success !{translate("manage_group")}
+
+hr
\ No newline at end of file
diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug
new file mode 100644
index 0000000000..50cd608640
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug
@@ -0,0 +1 @@
+p To make changes to your subscription please contact accounts@sharelatex.com
diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug
new file mode 100644
index 0000000000..222ea8e074
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug
@@ -0,0 +1,93 @@
+script(src="https://js.recurly.com/v3/recurly.js")
+script(type='text/javascript').
+ window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
+ window.subscription = !{JSON.stringify(personalSubscription)}
+ window.recomendedCurrency = "#{personalSubscription.recurly.currency}"
+
+div(ng-controller="RecurlySubscriptionController")
+ div(ng-show="!showCancellation")
+ case personalSubscription.recurly.state
+ when "active"
+ p !{translate("currently_subscribed_to_plan", {planName:"" + personalSubscription.plan.name + ""})}
+ a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}.
+ -if (personalSubscription.recurly.trialEndsAtFormatted)
+ p You're on a free trial which ends on #{personalSubscription.recurly.trialEndsAtFormatted}
+ p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"" + personalSubscription.recurly.price + "", collectionDate:"" + personalSubscription.recurly.nextPaymentDueAt + ""})}
+ p.pull-right
+ p
+ a(href=personalSubscription.recurly.billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")}
+ |
+ a(href, ng-click="switchToCancellationView()").btn.btn-primary !{translate("cancel_your_subscription")}
+ when "canceled"
+ p !{translate("currently_subscribed_to_plan", {planName:"" + personalSubscription.plan.name + ""})}
+ p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"" + personalSubscription.recurly.nextPaymentDueAt + ""})}
+ p: form(action="/user/subscription/reactivate",method="post")
+ input(type="hidden", name="_csrf", value=csrfToken)
+ input(type="submit",value="Reactivate your subscription").btn.btn-success
+ when "expired"
+ p !{translate("your_subscription_has_expired")}
+ a(href="/user/subscription/plans") !{translate("create_new_subscription")}
+ default
+ p !{translate("problem_with_subscription_contact_us")}
+
+ include ./_change_plans_mixins
+ div(ng-show="showChangePlan", ng-cloak)
+ h2 !{translate("change_plan")}
+ p: table.table
+ tr
+ th !{translate("name")}
+ th !{translate("price")}
+ th
+ +printPlans(plans.studentAccounts)
+ +printPlans(plans.individualMonthlyPlans)
+ +printPlans(plans.individualAnnualPlans)
+
+
+ .div(ng-controller="RecurlyCancellationController", ng-show="showCancellation").text-center
+ p
+ strong Are you sure you want to cancel?
+
+ div(ng-show="showExtendFreeTrial")
+ p !{translate("have_more_days_to_try", {days:14})}
+ p
+ button(type="submit", ng-click="extendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
+ p
+ a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
+
+ div(ng-show="showDowngradeToStudent")
+ div(ng-controller="ChangePlanFormController")
+ p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})}
+ p
+ button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-info #{translate("yes_please")}
+ p
+ a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
+
+ div(ng-show="showBasicCancel")
+ p #{translate("sure_you_want_to_cancel")}
+ p
+ a(href, ng-click="switchToDefaultView()").btn.btn-info #{translate("i_want_to_stay")}
+ p
+ a(href, ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
+
+script(type="text/javascript").
+ $('#cancelSubscription').on("click", function() {
+ ga('send', 'event', 'subscription-funnel', 'cancelation')
+ })
+
+script(type='text/ng-template', id='confirmChangePlanModalTemplate')
+ .modal-header
+ h3 #{translate("change_plan")}
+ .modal-body
+ p !{translate("sure_you_want_to_change_plan", {planName:"{{plan.name}}"})}
+ .modal-footer
+ button.btn.btn-default(
+ ng-disabled="inflight"
+ ng-click="cancel()"
+ ) #{translate("cancel")}
+ button.btn.btn-success(
+ ng-disabled="state.inflight"
+ ng-click="confirmChangePlan()"
+ )
+ span(ng-hide="inflight") #{translate("change_plan")}
+ span(ng-show="inflight") #{translate("processing")}...
+
diff --git a/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug b/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug
new file mode 100644
index 0000000000..1d0377df8c
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_v1_subscriptions.pug
@@ -0,0 +1,16 @@
+- if (v1Subscriptions.has_subscription)
+ -hasAnySubscription = true
+ p
+ | You are subscribed to Overleaf through Overleaf v1
+ p
+ a.btn.btn-primary(href='/sign_in_to_v1?return_to=/users/edit%23status') Manage v1 Subscription
+ hr
+
+- if (v1Subscriptions.teams && v1Subscriptions.teams.length > 0)
+ -hasAnySubscription = true
+ for team in v1Subscriptions.teams
+ p
+ | You are a member of the Overleaf v1 team: #{team.name}
+ p
+ a.btn.btn-primary(href="/sign_in_to_v1?return_to=/teams") Manage v1 Team Membership
+ hr
\ No newline at end of file
diff --git a/services/web/app/views/subscriptions/successful_subscription.pug b/services/web/app/views/subscriptions/successful_subscription.pug
index 6fb4fc7958..cbe4786d22 100644
--- a/services/web/app/views/subscriptions/successful_subscription.pug
+++ b/services/web/app/views/subscriptions/successful_subscription.pug
@@ -2,21 +2,21 @@ extends ../layout
block content
.content.content-alt
- .container(ng-controller="SuccessfulSubscriptionController")
+ .container
.row
.col-md-8.col-md-offset-2
.card(ng-cloak)
.page-header
h2 #{translate("thanks_for_subscribing")}
.alert.alert-success
- p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:""+subscription.price+"", collectionDate:""+subscription.nextPaymentDueAt+""})}
+ p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:""+subscription.recurly.price+"", collectionDate:""+subscription.recurly.nextPaymentDueAt+""})}
p #{translate("to_modify_your_subscription_go_to")}
a(href="/user/subscription") #{translate("manage_subscription")}.
p
- if (subscription.groupPlan == true)
a.btn.btn-success.btn-large(href="/subscription/group") #{translate("add_your_first_group_member_now")}
p.letter-from-founders
- p #{translate("thanks_for_subscribing_you_help_sl", {planName:subscription.name})}
+ p #{translate("thanks_for_subscribing_you_help_sl", {planName:subscription.plan.name})}
p #{translate("need_anything_contact_us_at")}
a(href='mailto:support@sharelatex.com') #{settings.adminEmail}
| .
diff --git a/services/web/public/src/main/subscription-dashboard.js b/services/web/public/src/main/subscription-dashboard.js
index 37ec82fcb9..8ebee84570 100644
--- a/services/web/public/src/main/subscription-dashboard.js
+++ b/services/web/public/src/main/subscription-dashboard.js
@@ -10,47 +10,48 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
* DS104: Avoid inline assignments
* DS204: Change includes calls to have a more natural evaluation order
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], function(App) {
- App.controller('SuccessfulSubscriptionController', function(
- $scope,
- sixpack
- ) {})
-
const SUBSCRIPTION_URL = '/user/subscription/update'
- const setupReturly = _.once(
- () =>
- typeof recurly !== 'undefined' && recurly !== null
- ? recurly.configure(window.recurlyApiKey)
- : undefined
- )
- const PRICES = {}
+ const ensureRecurlyIsSetup = _.once(() => {
+ if (!recurly) return
+ recurly.configure(window.recurlyApiKey)
+ })
- App.controller('CurrenyDropdownController', function(
- $scope,
- MultiCurrencyPricing,
- $q
- ) {
- // $scope.plans = MultiCurrencyPricing.plans
- $scope.currencyCode = MultiCurrencyPricing.currencyCode
-
- return ($scope.changeCurrency = newCurrency =>
- (MultiCurrencyPricing.currencyCode = newCurrency))
+ App.factory('RecurlyPricing', function($q, MultiCurrencyPricing) {
+ return {
+ loadDisplayPriceWithTax: function(planCode, currency, taxRate) {
+ ensureRecurlyIsSetup()
+ const currencySymbol = MultiCurrencyPricing.plans[currency].symbol
+ const pricing = recurly.Pricing()
+ return $q(function(resolve, reject) {
+ pricing
+ .plan(planCode, { quantity: 1 })
+ .currency(currency)
+ .done(function(price) {
+ const totalPriceExTax = parseFloat(price.next.total)
+ let taxAmmount = totalPriceExTax * taxRate
+ if (isNaN(taxAmmount)) {
+ taxAmmount = 0
+ }
+ resolve(`${currencySymbol}${totalPriceExTax + taxAmmount}`)
+ })
+ })
+ }
+ }
})
App.controller('ChangePlanFormController', function(
$scope,
$modal,
- MultiCurrencyPricing
+ RecurlyPricing
) {
- setupReturly()
- const { taxRate } = window
+ ensureRecurlyIsSetup()
$scope.changePlan = () =>
$modal.open({
@@ -59,44 +60,16 @@ define(['base'], function(App) {
scope: $scope
})
- $scope.$watch(
- 'pricing.currencyCode',
- () => ($scope.currencyCode = MultiCurrencyPricing.currencyCode)
- )
-
- $scope.pricing = MultiCurrencyPricing
- // $scope.plans = MultiCurrencyPricing.plans
- $scope.currencySymbol =
- MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode] != null
- ? MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
- : undefined
-
- $scope.currencyCode = MultiCurrencyPricing.currencyCode
-
- $scope.prices = PRICES
- return ($scope.refreshPrice = function(planCode) {
- let price
- if ($scope.prices[planCode] != null) {
- return
- }
- $scope.prices[planCode] = '...'
- const pricing = recurly.Pricing()
- pricing
- .plan(planCode, { quantity: 1 })
- .currency(MultiCurrencyPricing.currencyCode)
- .done(function(price) {
- const totalPriceExTax = parseFloat(price.next.total)
- return $scope.$evalAsync(function() {
- let taxAmmount = totalPriceExTax * taxRate
- if (isNaN(taxAmmount)) {
- taxAmmount = 0
- }
- return ($scope.prices[planCode] =
- $scope.currencySymbol + (totalPriceExTax + taxAmmount))
- })
- })
-
- return (price = '')
+ $scope.$watch('plan', function(plan) {
+ if (!plan) return
+ const planCode = plan.planCode
+ const { currency, taxRate } = window.subscription.recurly
+ $scope.price = '...' // Placeholder while we talk to recurly
+ RecurlyPricing.loadDisplayPriceWithTax(planCode, currency, taxRate).then(
+ price => {
+ $scope.price = price
+ }
+ )
})
})
@@ -141,69 +114,52 @@ define(['base'], function(App) {
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
})
- return App.controller('UserSubscriptionController', function(
+ App.controller('GroupMembershipController', function($scope, $modal) {
+ $scope.removeSelfFromGroup = function(admin_id) {
+ $scope.admin_id = admin_id
+ return $modal.open({
+ templateUrl: 'LeaveGroupModalTemplate',
+ controller: 'LeaveGroupModalController',
+ scope: $scope
+ })
+ }
+ })
+
+ App.controller('RecurlySubscriptionController', function($scope) {
+ $scope.showChangePlanButton = !subscription.groupPlan
+
+ $scope.switchToDefaultView = () => {
+ $scope.showCancellation = false
+ $scope.showChangePlan = false
+ }
+ $scope.switchToDefaultView()
+
+ $scope.switchToCancellationView = () => {
+ $scope.showCancellation = true
+ $scope.showChangePlan = false
+ }
+
+ $scope.switchToChangePlanView = () => {
+ $scope.showCancellation = false
+ $scope.showChangePlan = true
+ }
+ })
+
+ App.controller('RecurlyCancellationController', function(
$scope,
- MultiCurrencyPricing,
- $http,
- sixpack,
- $modal
+ RecurlyPricing,
+ $http
) {
- $scope.plans = MultiCurrencyPricing.plans
-
- const freeTrialEndDate = new Date(
- typeof subscription !== 'undefined' && subscription !== null
- ? subscription.trial_ends_at
- : undefined
- )
-
+ const subscription = window.subscription
const sevenDaysTime = new Date()
sevenDaysTime.setDate(sevenDaysTime.getDate() + 7)
-
+ const freeTrialEndDate = new Date(subscription.recurly.trial_ends_at)
const freeTrialInFuture = freeTrialEndDate > new Date()
const freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
- $scope.view = 'overview'
- $scope.getSuffix = planCode =>
- __guard__(
- planCode != null ? planCode.match(/(.*?)_(.*)/) : undefined,
- x => x[2]
- ) || null
- $scope.subscriptionSuffix = $scope.getSuffix(
- __guard__(
- typeof window !== 'undefined' && window !== null
- ? window.subscription
- : undefined,
- x => x.planCode
- )
- )
- if ($scope.subscriptionSuffix === 'free_trial_7_days') {
- $scope.subscriptionSuffix = ''
- }
- $scope.isNextGenPlan =
- ['heron', 'ibis'].includes($scope.subscriptionSuffix) ||
- subscription.groupPlan
-
- $scope.shouldShowPlan = function(planCode) {
- let needle
- return (
- (needle = $scope.getSuffix(planCode)),
- !['heron', 'ibis'].includes(needle)
- )
- }
-
const isMonthlyCollab =
- __guard__(
- typeof subscription !== 'undefined' && subscription !== null
- ? subscription.planCode
- : undefined,
- x1 => x1.indexOf('collaborator')
- ) !== -1 &&
- __guard__(
- typeof subscription !== 'undefined' && subscription !== null
- ? subscription.planCode
- : undefined,
- x2 => x2.indexOf('ann')
- ) === -1 &&
+ subscription.plan.planCode.indexOf('collaborator') !== -1 &&
+ subscription.plan.planCode.indexOf('ann') === -1 &&
!subscription.groupPlan
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
@@ -215,25 +171,13 @@ define(['base'], function(App) {
$scope.showBasicCancel = true
}
- setupReturly()
-
- recurly
- .Pricing()
- .plan('student', { quantity: 1 })
- .currency(MultiCurrencyPricing.currencyCode)
- .done(function(price) {
- const totalPriceExTax = parseFloat(price.next.total)
- return $scope.$evalAsync(function() {
- let taxAmmount = totalPriceExTax * taxRate
- if (isNaN(taxAmmount)) {
- taxAmmount = 0
- }
- $scope.currencySymbol =
- MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
- return ($scope.studentPrice =
- $scope.currencySymbol + (totalPriceExTax + taxAmmount))
- })
- })
+ const { currency, taxRate } = window.subscription.recurly
+ $scope.studentPrice = '...' // Placeholder while we talk to recurly
+ RecurlyPricing.loadDisplayPriceWithTax('student', currency, taxRate).then(
+ price => {
+ $scope.studentPrice = price
+ }
+ )
$scope.downgradeToStudent = function() {
const body = {
@@ -257,30 +201,13 @@ define(['base'], function(App) {
.catch(() => console.log('something went wrong changing plan'))
}
- $scope.removeSelfFromGroup = function(admin_id) {
- $scope.admin_id = admin_id
- return $modal.open({
- templateUrl: 'LeaveGroupModalTemplate',
- controller: 'LeaveGroupModalController',
- scope: $scope
- })
- }
-
- $scope.switchToCancelationView = () => ($scope.view = 'cancelation')
-
- return ($scope.exendTrial = function() {
+ $scope.extendTrial = function() {
const body = { _csrf: window.csrfToken }
$scope.inflight = true
return $http
.put('/user/subscription/extend', body)
.then(() => location.reload())
.catch(() => console.log('something went wrong changing plan'))
- })
+ }
})
})
-
-function __guard__(value, transform) {
- return typeof value !== 'undefined' && value !== null
- ? transform(value)
- : undefined
-}
diff --git a/services/web/test/acceptance/coffee/SubscriptionTests.coffee b/services/web/test/acceptance/coffee/SubscriptionTests.coffee
new file mode 100644
index 0000000000..5b95edef6e
--- /dev/null
+++ b/services/web/test/acceptance/coffee/SubscriptionTests.coffee
@@ -0,0 +1,191 @@
+expect = require('chai').expect
+async = require("async")
+User = require "./helpers/User"
+{Subscription} = require "../../../app/js/models/Subscription"
+SubscriptionViewModelBuilder = require "../../../app/js/Features/Subscription/SubscriptionViewModelBuilder"
+
+MockRecurlyApi = require "./helpers/MockRecurlyApi"
+MockV1Api = require "./helpers/MockV1Api"
+
+describe 'Subscriptions', ->
+ describe 'dashboard', ->
+ before (done) ->
+ @user = new User()
+ @user.ensureUserExists done
+
+ describe 'when the user has no subscription', ->
+ before (done) ->
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
+ return done(error) if error?
+ done()
+
+ it 'should return no personalSubscription', ->
+ expect(@data.personalSubscription).to.equal null
+
+ it 'should return no groupSubscriptions', ->
+ expect(@data.groupSubscriptions).to.deep.equal []
+
+ it 'should return no v1Subscriptions', ->
+ expect(@data.v1Subscriptions).to.deep.equal {}
+
+ describe 'when the user has a subscription with recurly', ->
+ before (done) ->
+ MockRecurlyApi.accounts['mock-account-id'] = @accounts = {
+ hosted_login_token: 'mock-login-token'
+ }
+ MockRecurlyApi.subscriptions['mock-subscription-id'] = @subscription = {
+ plan_code: 'collaborator',
+ tax_in_cents: 100,
+ tax_rate: 0.2,
+ unit_amount_in_cents: 500,
+ currency: 'GBP',
+ current_period_ends_at: new Date(2018,4,5),
+ state: 'active',
+ account_id: 'mock-account-id',
+ trial_ends_at: new Date(2018, 6, 7)
+ }
+ Subscription.create {
+ admin_id: @user._id,
+ manager_ids: [@user._id],
+ recurlySubscription_id: 'mock-subscription-id',
+ planCode: 'collaborator'
+ }, (error) =>
+ return done(error) if error?
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
+ return done(error) if error?
+ done()
+ return
+
+ after (done) ->
+ MockRecurlyApi.accounts = {}
+ MockRecurlyApi.subscriptions = {}
+ Subscription.remove {
+ admin_id: @user._id
+ }, done
+ return
+
+ it 'should return a personalSubscription with populated recurly data', ->
+ subscription = @data.personalSubscription
+ expect(subscription).to.exist
+ expect(subscription.planCode).to.equal 'collaborator'
+ expect(subscription.recurly).to.exist
+ expect(subscription.recurly).to.deep.equal {
+ "billingDetailsLink": "https://test.recurly.com/account/billing_info/edit?ht=mock-login-token"
+ "currency": "GBP"
+ "nextPaymentDueAt": "5th May 2018"
+ "price": "£6.00"
+ "state": "active"
+ "tax": 100
+ "taxRate": 0.2
+ "trial_ends_at": new Date(2018, 6, 7),
+ "trialEndsAtFormatted": "7th July 2018"
+ }
+
+ it 'should return no groupSubscriptions', ->
+ expect(@data.groupSubscriptions).to.deep.equal []
+
+ describe 'when the user has a subscription without recurly', ->
+ before (done) ->
+ Subscription.create {
+ admin_id: @user._id,
+ manager_ids: [@user._id],
+ planCode: 'collaborator'
+ }, (error) =>
+ return done(error) if error?
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
+ return done(error) if error?
+ done()
+ return
+
+ after (done) ->
+ Subscription.remove {
+ admin_id: @user._id
+ }, done
+ return
+
+ it 'should return a personalSubscription with no recurly data', ->
+ subscription = @data.personalSubscription
+ expect(subscription).to.exist
+ expect(subscription.planCode).to.equal 'collaborator'
+ expect(subscription.recurly).to.not.exist
+
+ it 'should return no groupSubscriptions', ->
+ expect(@data.groupSubscriptions).to.deep.equal []
+
+ describe 'when the user is a member of a group subscription', ->
+ before (done) ->
+ @owner1 = new User()
+ @owner2 = new User()
+ async.series [
+ (cb) => @owner1.ensureUserExists cb
+ (cb) => @owner2.ensureUserExists cb
+ (cb) => Subscription.create {
+ admin_id: @owner1._id,
+ manager_ids: [@owner1._id],
+ planCode: 'collaborator',
+ groupPlan: true,
+ member_ids: [@user._id]
+ }, cb
+ (cb) => Subscription.create {
+ admin_id: @owner2._id,
+ manager_ids: [@owner2._id],
+ planCode: 'collaborator',
+ groupPlan: true,
+ member_ids: [@user._id]
+ }, cb
+ ], (error) =>
+ return done(error) if error?
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
+ return done(error) if error?
+ done()
+ return
+
+ after (done) ->
+ Subscription.remove {
+ admin_id: @owner1._id
+ }, (error) =>
+ return done(error) if error?
+ Subscription.remove {
+ admin_id: @owner2._id
+ }, done
+ return
+
+ it 'should return no personalSubscription', ->
+ expect(@data.personalSubscription).to.equal null
+
+ it 'should return the two groupSubscriptions', ->
+ expect(@data.groupSubscriptions.length).to.equal 2
+ expect(
+ # Mongoose populates the admin_id with the user
+ @data.groupSubscriptions[0].admin_id._id.toString()
+ ).to.equal @owner1._id
+ expect(
+ @data.groupSubscriptions[1].admin_id._id.toString()
+ ).to.equal @owner2._id
+
+ describe 'when the user has a v1 subscription', ->
+ before (done) ->
+ MockV1Api.setUser v1Id = MockV1Api.nextV1Id(), {
+ subscription: @subscription = {
+ trial: false,
+ has_plan: true,
+ teams: [{
+ id: 56,
+ name: 'Test team'
+ }]
+ }
+ }
+ @user.setV1Id v1Id, (error) =>
+ return done(error) if error?
+ SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
+ return done(error) if error?
+ done()
+
+ it 'should return no personalSubscription', ->
+ expect(@data.personalSubscription).to.equal null
+
+ it 'should return no groupSubscriptions', ->
+ expect(@data.groupSubscriptions).to.deep.equal []
+
+ it 'should return a v1Subscriptions', ->
+ expect(@data.v1Subscriptions).to.deep.equal @subscription
\ No newline at end of file
diff --git a/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee b/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee
new file mode 100644
index 0000000000..59ed6ee362
--- /dev/null
+++ b/services/web/test/acceptance/coffee/helpers/MockRecurlyApi.coffee
@@ -0,0 +1,46 @@
+express = require("express")
+app = express()
+bodyParser = require('body-parser')
+
+app.use(bodyParser.json())
+
+module.exports = MockRecurlyApi =
+ subscriptions: {}
+
+ accounts: {}
+
+ run: () ->
+ app.get '/subscriptions/:id', (req, res, next) =>
+ subscription = @subscriptions[req.params.id]
+ if !subscription?
+ res.status(404).end()
+ else
+ res.send """
+
+ #{subscription.plan_code}
+ #{subscription.currency}
+ #{subscription.state}
+ #{subscription.tax_in_cents}
+ #{subscription.tax_rate}
+ #{subscription.current_period_ends_at}
+ #{subscription.unit_amount_in_cents}
+
+ #{subscription.trial_ends_at}
+
+ """
+
+ app.get '/accounts/:id', (req, res, next) =>
+ account = @accounts[req.params.id]
+ if !account?
+ res.status(404).end()
+ else
+ res.send """
+
+ #{account.hosted_login_token}
+
+ """
+
+ app.listen 6034, (error) ->
+ throw error if error?
+
+MockRecurlyApi.run()
diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
index 82e465193f..f44c516267 100644
--- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
+++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee
@@ -5,7 +5,11 @@ sinon = require 'sinon'
app.use(bodyParser.json())
+v1Id = 1000
+
module.exports = MockV1Api =
+ nextV1Id: -> v1Id++
+
users: { }
setUser: (id, user) ->
@@ -42,6 +46,14 @@ module.exports = MockV1Api =
else
res.sendStatus 404
+ app.get "/api/v1/sharelatex/users/:v1_user_id/subscriptions", (req, res, next) =>
+ user = @users[req.params.v1_user_id]
+ if user?.subscription?
+ res.json user.subscription
+ else
+ res.sendStatus 404
+
+
app.post "/api/v1/sharelatex/users/:v1_user_id/sync", (req, res, next) =>
@syncUserFeatures(req.params.v1_user_id)
res.sendStatus 200
diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee
index ebd310355c..053f3fa119 100644
--- a/services/web/test/acceptance/coffee/helpers/User.coffee
+++ b/services/web/test/acceptance/coffee/helpers/User.coffee
@@ -318,4 +318,12 @@ class User
else
return callback(new Error("unexpected status code from /user/personal_info: #{response.statusCode}"))
+ setV1Id: (v1Id, callback) ->
+ UserModel.update {
+ _id: @_id
+ }, {
+ overleaf:
+ id: v1Id
+ }, callback
+
module.exports = User
diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee
index e22d0ebd3b..00a9168623 100644
--- a/services/web/test/acceptance/config/settings.test.coffee
+++ b/services/web/test/acceptance/config/settings.test.coffee
@@ -4,6 +4,12 @@ v1Api =
module.exports =
enableSubscriptions: true
+ apis:
+ recurly:
+ # Set up our own mock recurly server
+ url: 'http://localhost:6034'
+ subdomain: 'test'
+
# for registration via SL, set enableLegacyRegistration to true
# for registration via Overleaf v1, set enableLegacyLogin to true
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
index fa2a069fb2..9fc6b842f3 100644
--- a/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -1,6 +1,7 @@
SandboxedModule = require('sandboxed-module')
sinon = require 'sinon'
should = require("chai").should()
+expect = require("chai").expect
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
modulePath = '../../../../app/js/Features/Subscription/SubscriptionController'
@@ -44,7 +45,7 @@ describe "SubscriptionController", ->
userHasV2Subscription: sinon.stub()
@SubscriptionViewModelBuilder =
- buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, @activeRecurlySubscription)
+ buildUsersSubscriptionViewModel:sinon.stub().callsArgWith(1, null, {})
buildViewModel: sinon.stub()
@settings =
coupon_codes:
@@ -226,92 +227,28 @@ describe "SubscriptionController", ->
@SubscriptionController.successful_subscription @req, @res
describe "userSubscriptionPage", ->
- describe "with a user without a subscription", ->
- beforeEach (done) ->
- @res.callback = done
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
- @SubscriptionController.userSubscriptionPage @req, @res
-
- it "should redirect to the plans page", ->
- @res.redirected.should.equal true
- @res.redirectedTo.should.equal "/user/subscription/plans"
-
- describe "with a potential domain licence", ->
- beforeEach () ->
- @groupUrl = "/go/over-here"
- @SubscriptionDomainHandler.getDomainLicencePage.returns(@groupUrl)
-
- describe "without an existing subscription", ->
- beforeEach (done)->
- @res.callback = done
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, false)
- @SubscriptionController.userSubscriptionPage @req, @res
-
- it "should redirect to the group invite url", ->
- @res.redirected.should.equal true
- @res.redirectedTo.should.equal @groupUrl
-
- describe "with an existing subscription", ->
- beforeEach (done)->
- @res.callback = done
- @settings.apis.recurly.subdomain = 'test'
- @userSub = {account: {hosted_login_token: 'abcd'}}
- @LimitationsManager.hasPaidSubscription
- .callsArgWith(1, null, true, {})
- @SubscriptionController.userSubscriptionPage @req, @res
-
-
- it "should render the dashboard", ->
- @res.renderedTemplate.should.equal "subscriptions/dashboard"
-
- describe "with a user with a paid subscription", ->
- beforeEach (done) ->
- @res.callback = done
- @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
- @SubscriptionController.userSubscriptionPage @req, @res
-
- it "should render the dashboard", (done)->
- @res.rendered.should.equal true
- @res.renderedTemplate.should.equal "subscriptions/dashboard"
- done()
-
- it "should set the correct subscription details", ->
- @res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
-
- describe "with a user with a free trial", ->
- beforeEach (done) ->
- @res.callback = done
- @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, @activeRecurlySubscription)
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
- @SubscriptionController.userSubscriptionPage @req, @res
-
- it "should render the dashboard", ->
- @res.renderedTemplate.should.equal "subscriptions/dashboard"
-
- it "should set the correct subscription details", ->
- @res.renderedVariables.subscription.should.deep.equal @activeRecurlySubscription
-
-
- describe "when its a custom subscription which is non recurly", ->
- beforeEach ()->
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {customAccount:true})
- @SubscriptionController.userSubscriptionPage @req, @res
-
- it "should redirect to /user/subscription/custom_account", ->
- @res.redirectedTo.should.equal("/user/subscription/custom_account")
-
- describe "userCustomSubscriptionPage", ->
beforeEach (done) ->
- @res.callback = done
- @LimitationsManager.hasPaidSubscription.callsArgWith(1, null, true, {})
- @SubscriptionController.userCustomSubscriptionPage @req, @res
+ @SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, {
+ personalSubscription: @personalSubscription = { 'personal-subscription': 'mock' }
+ groupSubscriptions: @groupSubscriptions = { 'personal-subscriptions': 'mock' }
+ v1Subscriptions: @v1Subscriptions = { 'v1-subscriptions': 'mock' }
+ })
+ @SubscriptionViewModelBuilder.buildViewModel.returns(@plans = {'plans': 'mock'})
+ @res.render = (view, @data) =>
+ expect(view).to.equal 'subscriptions/dashboard'
+ done()
+ @SubscriptionController.userSubscriptionPage @req, @res
- it "should render the page", (done)->
- @res.rendered.should.equal true
- @res.renderedTemplate.should.equal "subscriptions/custom_account"
- done()
+ it "should load the personal, groups and v1 subscriptions", ->
+ expect(@data.personalSubscription).to.deep.equal @personalSubscription
+ expect(@data.groupSubscriptions).to.deep.equal @groupSubscriptions
+ expect(@data.v1Subscriptions).to.deep.equal @v1Subscriptions
+ it "should load the user", ->
+ expect(@data.user).to.deep.equal @user
+
+ it "should load the plans", ->
+ expect(@data.plans).to.deep.equal @plans
describe "createSubscription", ->
beforeEach (done)->
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
index 7b3acf1b78..59b45df40c 100644
--- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
+++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee
@@ -24,7 +24,6 @@ describe "SubscriptionUpdater", ->
manager_ids: [@adminUser._id]
member_ids: @allUserIds
save: sinon.stub().callsArgWith(0)
- freeTrial:{}
planCode:"student_or_something"
@user_id = @adminuser_id
@@ -34,7 +33,6 @@ describe "SubscriptionUpdater", ->
manager_ids: [@adminUser._id]
member_ids: @allUserIds
save: sinon.stub().callsArgWith(0)
- freeTrial:{}
planCode:"group_subscription"
@@ -54,7 +52,6 @@ describe "SubscriptionUpdater", ->
getGroupSubscriptionMemberOf:sinon.stub()
@Settings =
- freeTrialPlanCode: "collaborator"
defaultPlanCode: "personal"
defaultFeatures: { "default": "features" }
@@ -116,10 +113,6 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._updateSubscriptionFromRecurly @recurlySubscription, @subscription, (err)=>
@subscription.recurlySubscription_id.should.equal @recurlySubscription.uuid
@subscription.planCode.should.equal @recurlySubscription.plan.plan_code
-
- @subscription.freeTrial.allowed.should.equal true
- assert.equal(@subscription.freeTrial.expiresAt, undefined)
- assert.equal(@subscription.freeTrial.planCode, undefined)
@subscription.save.called.should.equal true
@FeaturesUpdater.refreshFeatures.calledWith(@adminUser._id).should.equal true
done()
@@ -157,7 +150,6 @@ describe "SubscriptionUpdater", ->
@SubscriptionUpdater._createNewSubscription @adminUser._id, =>
@subscription.admin_id.should.equal @adminUser._id
@subscription.manager_ids.should.deep.equal [@adminUser._id]
- @subscription.freeTrial.allowed.should.equal false
@subscription.save.called.should.equal true
done()
diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionViewModelBuilderTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionViewModelBuilderTests.coffee
deleted file mode 100644
index 51ed2dbebd..0000000000
--- a/services/web/test/unit/coffee/Subscription/SubscriptionViewModelBuilderTests.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-SandboxedModule = require('sandboxed-module')
-sinon = require 'sinon'
-should = require("chai").should()
-modulePath = '../../../../app/js/Features/Subscription/SubscriptionViewModelBuilder'
-
-describe 'SubscriptionViewModelBuilder', ->
- mockSubscription =
- uuid: "subscription-123-active"
- plan:
- name: "Gold"
- plan_code: "gold"
- current_period_ends_at: new Date()
- state: "active"
- unit_amount_in_cents: 999
- account:
- account_code: "user-123"
-
-
- beforeEach ->
- @user =
- email:"tom@yahoo.com",
- _id: 'one',
- signUpDate: new Date('2000-10-01')
-
- @plan =
- name: "test plan"
-
- @SubscriptionFormatters =
- formatDate: sinon.stub().returns("Formatted date")
- formatPrice: sinon.stub().returns("Formatted price")
-
- @RecurlyWrapper =
- sign: sinon.stub().callsArgWith(1, null, "something")
- getSubscription: sinon.stub().callsArgWith 2, null,
- account:
- hosted_login_token: "hosted_login_token"
-
- @builder = SandboxedModule.require modulePath, requires:
- "settings-sharelatex": { apis: { recurly: { subdomain: "example.com" }}}
- "./RecurlyWrapper": @RecurlyWrapper
- "./PlansLocator": @PlansLocator = {}
- "./SubscriptionLocator": @SubscriptionLocator = {}
- "./SubscriptionFormatters": @SubscriptionFormatters
- "./LimitationsManager": {}
- "./V1SubscriptionManager": @V1SubscriptionManager = {}
- "logger-sharelatex":
- log:->
- warn:->
- "underscore": {}
-
- @PlansLocator.findLocalPlanInSettings = sinon.stub().returns(@plan)
- @SubscriptionLocator.getUsersSubscription = sinon.stub().callsArgWith(1, null, mockSubscription)
- @SubscriptionLocator.getMemberSubscriptions = sinon.stub().callsArgWith(1, null, null)
- @V1SubscriptionManager.getSubscriptionsFromV1 = sinon.stub().yields(null, @mockV1Sub = ['mock-v1-subs'])
-
- it 'builds the user view model', ->
- callback = (error, subscription, memberSubscriptions, billingDetailsLink, v1Sub) =>
- @error = error
- @subscription = subscription
- @memberSubscriptions = memberSubscriptions
- @billingDetailsLink = billingDetailsLink
- @v1Sub = v1Sub
-
- @builder.buildUsersSubscriptionViewModel(@user, callback)
-
- @subscription.name.should.eq 'test plan'
- @subscription.nextPaymentDueAt.should.eq 'Formatted date'
- @subscription.price.should.eq 'Formatted price'
- @billingDetailsLink.should.eq "https://example.com.recurly.com/account/billing_info/edit?ht=hosted_login_token"
- @v1Sub.should.deep.equal @mockV1Sub
\ No newline at end of file