mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Merge pull request #1130 from sharelatex/ja-subscription-dashboard
Refactor subscription dashboard GitOrigin-RevId: 3573822b8b48c7181c661b2c253d7713f4a4328c
This commit is contained in:
parent
8bf9d79d2f
commit
0f1c732d15
26 changed files with 622 additions and 625 deletions
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -29,4 +29,5 @@ module.exports =
|
|||
return "#{symbol}#{dollars}.#{cents}"
|
||||
|
||||
formatDate: (date) ->
|
||||
return null if !date?
|
||||
dateformat date, "dS mmmm yyyy"
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")}
|
|
@ -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")}
|
||||
|
|
||||
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
|
||||
|
|
||||
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:"<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
|
||||
|
|
|
@ -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)
|
|
@ -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")}...
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
p To make changes to your subscription please contact accounts@sharelatex.com
|
|
@ -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")}
|
||||
|
|
||||
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")}...
|
||||
|
|
@ -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
|
|
@ -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}
|
||||
| .
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
191
services/web/test/acceptance/coffee/SubscriptionTests.coffee
Normal file
191
services/web/test/acceptance/coffee/SubscriptionTests.coffee
Normal 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
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)->
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue