Merge pull request #1130 from sharelatex/ja-subscription-dashboard

Refactor subscription dashboard

GitOrigin-RevId: 3573822b8b48c7181c661b2c253d7713f4a4328c
This commit is contained in:
James Allen 2018-11-19 16:44:51 +01:00 committed by sharelatex
parent 8bf9d79d2f
commit 0f1c732d15
26 changed files with 622 additions and 625 deletions

View file

@ -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']

View file

@ -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)

View file

@ -29,4 +29,5 @@ module.exports =
return "#{symbol}#{dollars}.#{cents}"
formatDate: (date) ->
return null if !date?
dateformat date, "dS mmmm yyyy"

View file

@ -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

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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")}

View file

@ -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:"<strong>" + subscription.expiresAt + "</strong>"})}
when "active"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
span(ng-show="!isNextGenPlan")
a(href, ng-click="changePlan = true") !{translate("change_plan")}.
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
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")}
| &nbsp;
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
when "canceled"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
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: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
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: <strong>#{team.name}</strong>
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
| &nbsp;
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
| &nbsp;
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")}
| &nbsp;
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:"<strong>{{plan.name}}</strong>"})}
.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

View file

@ -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)

View file

@ -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: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
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")}...

View file

@ -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

View file

@ -0,0 +1 @@
p To make changes to your subscription please contact accounts@sharelatex.com

View file

@ -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:"<strong>" + personalSubscription.plan.name + "</strong>"})}
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 <strong>#{personalSubscription.recurly.trialEndsAtFormatted}</strong>
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + personalSubscription.recurly.price + "</strong>", collectionDate:"<strong>" + personalSubscription.recurly.nextPaymentDueAt + "</strong>"})}
p.pull-right
p
a(href=personalSubscription.recurly.billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")}
| &nbsp;
a(href, ng-click="switchToCancellationView()").btn.btn-primary !{translate("cancel_your_subscription")}
when "canceled"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + personalSubscription.plan.name + "</strong>"})}
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"<strong>" + personalSubscription.recurly.nextPaymentDueAt + "</strong>"})}
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:"<strong>{{plan.name}}</strong>"})}
.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")}...

View file

@ -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: <strong>#{team.name}</strong>
p
a.btn.btn-primary(href="/sign_in_to_v1?return_to=/teams") Manage v1 Team Membership
hr

View file

@ -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:"<strong>"+subscription.price+"</strong>", collectionDate:"<strong>"+subscription.nextPaymentDueAt+"</strong>"})}
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>"+subscription.recurly.price+"</strong>", collectionDate:"<strong>"+subscription.recurly.nextPaymentDueAt+"</strong>"})}
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}
| .

View file

@ -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
}

View file

@ -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

View file

@ -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.plan_code}</plan_code>
<currency>#{subscription.currency}</currency>
<state>#{subscription.state}</state>
<tax_in_cents type="integer">#{subscription.tax_in_cents}</tax_in_cents>
<tax_rate type="float">#{subscription.tax_rate}</tax_rate>
<current_period_ends_at type="datetime">#{subscription.current_period_ends_at}</current_period_ends_at>
<unit_amount_in_cents type="integer">#{subscription.unit_amount_in_cents}</unit_amount_in_cents>
<account href="accounts/#{subscription.account_id}" />
<trial_ends_at type="datetime">#{subscription.trial_ends_at}</trial_ends_at>
</subscription>
"""
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>#{account.hosted_login_token}</hosted_login_token>
</account>
"""
app.listen 6034, (error) ->
throw error if error?
MockRecurlyApi.run()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)->

View file

@ -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()

View file

@ -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