Merge pull request #1107 from sharelatex/ja-purchase-groups

Purchase group/team accounts directly via app

GitOrigin-RevId: 1a502878753de77758fb431f45a6366f199f1cb0
This commit is contained in:
James Allen 2018-11-13 14:35:50 +01:00 committed by sharelatex
parent f1c8dcdf1e
commit 140f97eb20
15 changed files with 464 additions and 117 deletions

View file

@ -0,0 +1,37 @@
Settings = require 'settings-sharelatex'
fs = require('fs')
# The groups.json file encodes the various group plan options we provide, and
# is used in the app the render the appropriate dialog in the plans page, and
# to generate the appropriate entries in the Settings.plans array.
# It is also used by scripts/recurly/sync_recurly.rb, which will make sure
# Recurly has a plan configured for all the groups, and that the prices are
# up to date with the data in groups.json.
data = fs.readFileSync(__dirname + '/../../../templates/plans/groups.json')
groups = JSON.parse(data.toString())
capitalize = (string) ->
string.charAt(0).toUpperCase() + string.slice(1);
# With group accounts in Recurly, we end up with a lot of plans to manage.
# Rather than hand coding them in the settings file, and then needing to keep
# that data in sync with the data in groups.json, we can auto generate the
# group plan entries and append them to Settings.plans at boot time. This is not
# a particularly clean pattern, since it's a little surprising that settings
# are modified at boot-time, but I think it's a better option than trying to
# keep two sources of data in sync.
for usage, plan_data of groups
for plan_code, currency_data of plan_data
for currency, price_data of currency_data
for size, price of price_data
Settings.plans.push {
planCode: "group_#{plan_code}_#{size}_#{usage}",
name: "#{Settings.appName} #{capitalize(plan_code)} - Group Account (#{size} licenses) - #{capitalize(usage)}",
hideFromUsers: true,
annual: true
features: Settings.features[plan_code]
groupPlan: true
membersLimit: parseInt(size)
}
module.exports = groups

View file

@ -11,6 +11,7 @@ SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
UserGetter = require "../User/UserGetter"
FeaturesUpdater = require './FeaturesUpdater'
planFeatures = require './planFeatures'
GroupPlansData = require './GroupPlansData'
module.exports = SubscriptionController =
@ -31,6 +32,7 @@ module.exports = SubscriptionController =
gaExperiments: Settings.gaExperiments.plansPage
recomendedCurrency:recomendedCurrency
planFeatures: planFeatures
groupPlans: GroupPlansData
user_id = AuthenticationController.getLoggedInUserId(req)
if user_id?
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->

View file

@ -0,0 +1,122 @@
{
"enterprise": {
"collaborator": {
"USD": {
"2": 252,
"3": 376,
"4": 495,
"5": 615,
"10": 1170,
"20": 2160,
"50": 4950
},
"EUR": {
"2": 235,
"3": 352,
"4": 468,
"5": 584,
"10": 1090,
"20": 2015,
"50": 4620
},
"GBP": {
"2": 198,
"3": 296,
"4": 394,
"5": 492,
"10": 935,
"20": 1730,
"50": 3960
}
},
"professional": {
"USD": {
"2": 504,
"3": 752,
"4": 990,
"5": 1230,
"10": 2340,
"20": 4320,
"50": 9900
},
"EUR": {
"2": 470,
"3": 704,
"4": 936,
"5": 1168,
"10": 2185,
"20": 4030,
"50": 9240
},
"GBP": {
"2": 396,
"3": 592,
"4": 788,
"5": 984,
"10": 1870,
"20": 3455,
"50": 7920
}
}
},
"educational": {
"collaborator": {
"USD": {
"2": 252,
"3": 376,
"4": 495,
"5": 615,
"10": 695,
"20": 1295,
"50": 2970
},
"EUR": {
"2": 235,
"3": 352,
"4": 468,
"5": 584,
"10": 655,
"20": 1210,
"50": 2770
},
"GBP": {
"2": 198,
"3": 296,
"4": 394,
"5": 492,
"10": 560,
"20": 1035,
"50": 2375
}
},
"professional": {
"USD": {
"2": 504,
"3": 752,
"4": 990,
"5": 1230,
"10": 1390,
"20": 2590,
"50": 5940
},
"EUR": {
"2": 470,
"3": 704,
"4": 936,
"5": 1168,
"10": 1310,
"20": 2420,
"50": 5545
},
"GBP": {
"2": 396,
"3": 592,
"4": 788,
"5": 984,
"10": 1125,
"20": 2075,
"50": 4750
}
}
}
}

View file

@ -1,4 +1,4 @@
script(type="text/ng-template", id="groupPlanModalTemplate")
script(type="text/ng-template", id="groupPlanModalInquiryTemplate")
.modal-header
h3 #{translate("group_plan_enquiry")}
.modal-body

View file

@ -0,0 +1,52 @@
script(type="text/ng-template", id="groupPlanModalPurchaseTemplate")
.modal-header
h3 Save 30% or more with a group license
.modal-body.plans
.container-fluid
.row
.col-md-6.text-center
.circle.circle-lg
| {{ displayPrice }}
span.small / year
br
span.circle-subtext For {{ selected.size }} users
ul.list-unstyled
li Each user will have access to:
li  
li(ng-if="selected.plan_code == 'collaborator'")
strong #{translate("collabs_per_proj", {collabcount:10})}
li(ng-if="selected.plan_code == 'professional'")
strong #{translate("unlimited_collabs")}
+features_premium
.col-md-6
form.form
.form-group
label(for='plan_code')
| Plan
select.form-control(id="plan_code", ng-model="selected.plan_code")
option(ng-repeat="plan_code in options.plan_codes", value="{{plan_code.code}}") {{ plan_code.display }}
.form-group
label(for='size')
| Number of users
select.form-control(id="size", ng-model="selected.size")
option(ng-repeat="size in options.sizes", value="{{size}}") {{ size }}
.form-group
label(for='currency')
| Currency
select.form-control(id="currency", ng-model="selected.currency")
option(ng-repeat="currency in options.currencies", value="{{currency.code}}") {{ currency.display }}
.form-group
label(for='usage')
| Usage
select.form-control(id="usage", ng-model="selected.usage")
option(ng-repeat="usage in options.usages", value="{{usage.code}}") {{ usage.display }}
p.small.text-center.row-spaced-small(ng-show="selected.usage == 'educational'")
| Save an additional 40% on groups of 10 or more with our educational discount
.modal-footer
.text-center
button.btn.btn-primary.btn-lg(ng-click="purchase()") Purchase Now
br
| or
br
a(href, ng-click="payByInvoice()") Pay by invoice or VAT invoice

View file

@ -26,10 +26,10 @@ block content
data-toggle="dropdown",
dropdown-toggle
)
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
| {{currencyCode}} ({{allCurrencies[currencyCode]['symbol']}})
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat="(currency, value) in plans")
li(ng-repeat="(currency, value) in availableCurrencies")
a(
ng-click="changeCurrency(currency)",
) {{currency}} ({{value['symbol']}})
@ -44,11 +44,11 @@ block content
span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
span(ng-if="discountMonths && discountRate")   - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off
div(ng-if="price")
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
strong {{availableCurrencies[currencyCode]['symbol']}}{{price.next.total}}
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
div(ng-if="normalPrice")
span.small Normally {{plans[currencyCode]['symbol']}}{{normalPrice}}
span.small Normally {{availableCurrencies[currencyCode]['symbol']}}{{normalPrice}}
.row
div()
.col-md-12()
@ -188,8 +188,8 @@ block content
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
span ({{plans[currencyCode]['symbol']}}{{price.next.subtotal}} + {{plans[currencyCode]['symbol']}}{{price.next.tax}} tax)
strong {{availableCurrencies[currencyCode]['symbol']}}{{price.next.total}}
span ({{availableCurrencies[currencyCode]['symbol']}}{{price.next.subtotal}} + {{availableCurrencies[currencyCode]['symbol']}}{{price.next.tax}} tax)
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
hr.thin

View file

@ -8,8 +8,9 @@ block vars
block scripts
script(type='text/javascript').
window.recomendedCurrency = '#{recomendedCurrency}'
window.abCurrencyFlag = '#{abCurrencyFlag}'
window.recomendedCurrency = '#{recomendedCurrency}';
window.abCurrencyFlag = '#{abCurrencyFlag}';
window.groupPlans = !{JSON.stringify(groupPlans)};
block content
.content.content-alt.content-page
@ -94,7 +95,7 @@ block content
br
br
a.btn.btn-default(
href
href="#groups"
ng-click="openGroupPlanModal()"
) #{translate('find_out_more')}
@ -131,3 +132,4 @@ block content
.row.row-spaced
include _modal_group_inquiry
include _modal_group_purchase

View file

@ -220,6 +220,9 @@ module.exports = settings =
templates: true
trackChanges: true
features:
personal: defaultFeatures
plans: plans = [{
planCode: "personal"
name: "Personal"

View file

@ -1,19 +1,9 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
no-unused-vars,
no-return-assign
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/* global recurly,_,define */
define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
App.controller('NewSubscriptionController', function(
$scope,
@ -22,13 +12,13 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
event_tracking,
ccUtils
) {
let inputHasError, isFormValid, setPaymentMethod
if (typeof recurly === 'undefined') {
throw new Error('Recurly API Library Missing.')
}
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.plans = MultiCurrencyPricing.plans
$scope.allCurrencies = MultiCurrencyPricing.plans
$scope.availableCurrencies = {}
$scope.planCode = window.plan_code
$scope.switchToStudent = function() {
@ -37,9 +27,9 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
event_tracking.sendMB('subscription-form-switch-to-student', {
plan: window.plan_code
})
return (window.location = `/user/subscription/new?planCode=${planCode}&currency=${
window.location = `/user/subscription/new?planCode=${planCode}&currency=${
$scope.currencyCode
}&cc=${$scope.data.coupon}`)
}&cc=${$scope.data.coupon}`
}
event_tracking.sendMB('subscription-form', { plan: window.plan_code })
@ -85,7 +75,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
const pricing = recurly.Pricing()
window.pricing = pricing
const initialPricing = pricing
pricing
.plan(window.plan_code, { quantity: 1 })
.address({ country: $scope.data.country })
.tax({ tax_code: 'digital', vat_number: '' })
@ -96,58 +86,41 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
pricing.on('change', () => {
$scope.planName = pricing.items.plan.name
$scope.price = pricing.price
$scope.trialLength =
pricing.items.plan.trial != null
? pricing.items.plan.trial.length
: undefined
if (pricing.items.plan.trial) {
$scope.trialLength = pricing.items.plan.trial.length
}
$scope.monthlyBilling = pricing.items.plan.period.length === 1
$scope.availableCurrencies = {}
for (let currencyCode in pricing.items.plan.price) {
if (MultiCurrencyPricing.plans[currencyCode]) {
$scope.availableCurrencies[currencyCode] =
MultiCurrencyPricing.plans[currencyCode]
}
}
if (
__guard__(
__guard__(
pricing.items != null ? pricing.items.coupon : undefined,
x1 => x1.discount
),
x => x.type
) === 'percent'
pricing.items &&
pricing.items.coupon &&
pricing.items.coupon.discount &&
pricing.items.coupon.discount.type === 'percent'
) {
const basePrice = parseInt(pricing.price.base.plan.unit)
$scope.normalPrice = basePrice
if (
pricing.items.coupon.applies_for_months > 0 &&
__guard__(
pricing.items.coupon != null
? pricing.items.coupon.discount
: undefined,
x2 => x2.rate
) &&
(pricing.items.coupon != null
? pricing.items.coupon.applies_for_months
: undefined) != null
pricing.items.coupon.discount.rate &&
pricing.items.coupon.applies_for_months
) {
$scope.discountMonths =
pricing.items.coupon != null
? pricing.items.coupon.applies_for_months
: undefined
$scope.discountRate =
__guard__(
pricing.items.coupon != null
? pricing.items.coupon.discount
: undefined,
x3 => x3.rate
) * 100
$scope.discountMonths = pricing.items.coupon.applies_for_months
$scope.discountRate = pricing.items.coupon.discount.rate * 100
}
if (
__guard__(
pricing.price != null ? pricing.price.taxes[0] : undefined,
x4 => x4.rate
) != null
) {
if (pricing.price.taxes[0] && pricing.price.taxes[0].rate) {
$scope.normalPrice += basePrice * pricing.price.taxes[0].rate
}
}
return $scope.$apply()
$scope.$apply()
})
$scope.applyCoupon = () => pricing.coupon($scope.data.coupon).done()
@ -162,7 +135,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
return pricing.currency(newCurrency).done()
}
$scope.inputHasError = inputHasError = function(formItem) {
$scope.inputHasError = function(formItem) {
if (formItem == null) {
return false
}
@ -170,7 +143,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
return formItem.$touched && formItem.$invalid
}
$scope.isFormValid = isFormValid = function(form) {
$scope.isFormValid = function(form) {
if ($scope.paymentMethod.value === 'paypal') {
return $scope.data.country !== ''
} else {
@ -181,10 +154,10 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
$scope.updateCountry = () =>
pricing.address({ country: $scope.data.country }).done()
$scope.setPaymentMethod = setPaymentMethod = function(method) {
$scope.setPaymentMethod = function(method) {
$scope.paymentMethod.value = method
$scope.validation.errorFields = {}
return ($scope.genericError = '')
$scope.genericError = ''
}
const completeSubscription = function(err, recurly_token_id) {
@ -193,10 +166,10 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
event_tracking.sendMB('subscription-error', err)
// We may or may not be in a digest loop here depending on
// whether recurly could do validation locally, so do it async
return $scope.$evalAsync(function() {
$scope.$evalAsync(function() {
$scope.processing = false
$scope.genericError = err.message
return _.each(
_.each(
err.fields,
field => ($scope.validation.errorFields[field] = true)
)
@ -208,11 +181,8 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
subscriptionDetails: {
currencyCode: pricing.items.currency,
plan_code: pricing.items.plan.code,
coupon_code:
__guard__(
pricing.items != null ? pricing.items.coupon : undefined,
x => x.code
) || '',
coupon_code: pricing.items.coupon ? pricing.items.coupon.code : '',
isPaypal: $scope.paymentMethod.value === 'paypal',
address: {
address1: $scope.data.address1,
@ -235,12 +205,11 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
.post('/user/subscription/create', postData)
.then(function() {
event_tracking.sendMB('subscription-submission-success')
return (window.location.href = '/user/subscription/thank-you')
window.location.href = '/user/subscription/thank-you'
})
.catch(function() {
$scope.processing = false
return ($scope.genericError =
'Something went wrong processing the request')
$scope.genericError = 'Something went wrong processing the request'
})
}
}
@ -255,7 +224,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
}
}
return ($scope.countries = [
$scope.countries = [
{ code: 'AF', name: 'Afghanistan' },
{ code: 'AL', name: 'Albania' },
{ code: 'DZ', name: 'Algeria' },
@ -507,10 +476,5 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
{ code: 'YE', name: 'Yemen' },
{ code: 'ZM', name: 'Zambia' },
{ code: 'AX', name: 'Åland Islandscode:' }
])
]
}))
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View file

@ -1,16 +1,8 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
max-len
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
/* global define,history */
define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
App.factory('MultiCurrencyPricing', function() {
const currencyCode = window.recomendedCurrency
@ -197,14 +189,15 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
}
})
return App.controller('PlansController', function(
App.controller('PlansController', function(
$scope,
$modal,
event_tracking,
MultiCurrencyPricing,
$http,
$filter,
ipCookie
ipCookie,
$location
) {
let switchEvent
$scope.showPlans = true
@ -221,7 +214,7 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
$scope.changeCurreny = function(e, newCurrency) {
e.preventDefault()
return ($scope.currencyCode = newCurrency)
$scope.currencyCode = newCurrency
}
// because ternary logic in angular bindings is hard
@ -240,48 +233,139 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
}
plan = eventLabel(plan, location)
event_tracking.sendMB('plans-page-start-trial')
return event_tracking.send(
'subscription-funnel',
'sign_up_now_button',
plan
)
event_tracking.send('subscription-funnel', 'sign_up_now_button', plan)
}
$scope.switchToMonthly = function(e, location) {
const uiView = 'monthly'
switchEvent(e, uiView + '-prices', location)
return ($scope.ui.view = uiView)
$scope.ui.view = uiView
}
$scope.switchToStudent = function(e, location) {
const uiView = 'student'
switchEvent(e, uiView + '-prices', location)
return ($scope.ui.view = uiView)
$scope.ui.view = uiView
}
$scope.switchToAnnual = function(e, location) {
const uiView = 'annual'
switchEvent(e, uiView + '-prices', location)
return ($scope.ui.view = uiView)
$scope.ui.view = uiView
}
$scope.openGroupPlanModal = function() {
$modal.open({
templateUrl: 'groupPlanModalTemplate'
})
return event_tracking.send(
history.replaceState(
null,
document.title,
window.location.pathname + '#groups'
)
$modal
.open({
templateUrl: 'groupPlanModalPurchaseTemplate',
controller: 'GroupPlansModalPurchaseController'
})
.result.finally(() =>
history.replaceState(null, document.title, window.location.pathname)
)
event_tracking.send(
'subscription-funnel',
'plans-page',
'group-inquiry-potential'
)
}
if ($location.hash() === 'groups') {
$scope.openGroupPlanModal()
}
var eventLabel = (label, location) => label
return (switchEvent = function(e, label, location) {
switchEvent = function(e, label, location) {
e.preventDefault()
const gaLabel = eventLabel(label, location)
return event_tracking.send('subscription-funnel', 'plans-page', gaLabel)
})
event_tracking.send('subscription-funnel', 'plans-page', gaLabel)
}
})
App.controller('GroupPlansModalPurchaseController', function($scope, $modal) {
$scope.options = {
plan_codes: [
{
display: 'Collaborator',
code: 'collaborator'
},
{
display: 'Professional',
code: 'professional'
}
],
currencies: [
{
display: 'USD ($)',
code: 'USD'
},
{
display: 'GBP (£)',
code: 'GBP'
},
{
display: 'EUR (€)',
code: 'EUR'
}
],
currencySymbols: {
USD: '$',
EUR: '€',
GBP: '£'
},
sizes: [2, 3, 4, 5, 10, 20, 50],
usages: [
{
display: 'Enterprise',
code: 'enterprise'
},
{
display: 'Educational',
code: 'educational'
}
]
}
$scope.prices = window.groupPlans
let currency = 'USD'
if (['USD', 'GBP', 'EUR'].includes(window.recomendedCurrency)) {
currency = window.recomendedCurrency
}
$scope.selected = {
plan_code: 'collaborator',
currency,
size: '10',
usage: 'educational'
}
$scope.recalculatePrice = function() {
let { usage, plan_code, currency, size } = $scope.selected
const price = $scope.prices[usage][plan_code][currency][size]
const currencySymbol = $scope.options.currencySymbols[currency]
$scope.displayPrice = `${currencySymbol}${price}`
}
$scope.$watch('selected', $scope.recalculatePrice, true)
$scope.recalculatePrice()
$scope.purchase = function() {
let { plan_code, size, usage, currency } = $scope.selected
plan_code = `group_${plan_code}_${size}_${usage}`
window.location = `/user/subscription/new?planCode=${plan_code}&currency=${currency}`
}
$scope.payByInvoice = function() {
$modal.open({
templateUrl: 'groupPlanModalInquiryTemplate'
})
$scope.$close()
}
})
})

View file

@ -179,7 +179,9 @@ define(['base'], function(App) {
if ($scope.subscriptionSuffix === 'free_trial_7_days') {
$scope.subscriptionSuffix = ''
}
$scope.isNextGenPlan = ['heron', 'ibis'].includes($scope.subscriptionSuffix)
$scope.isNextGenPlan =
['heron', 'ibis'].includes($scope.subscriptionSuffix) ||
subscription.groupPlan
$scope.shouldShowPlan = function(planCode) {
let needle
@ -201,7 +203,8 @@ define(['base'], function(App) {
? subscription.planCode
: undefined,
x2 => x2.indexOf('ann')
) === -1
) === -1 &&
!subscription.groupPlan
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
if (isMonthlyCollab && stillInFreeTrial) {

View file

@ -62,7 +62,7 @@
.circle {
font-size: 1.5rem;
font-weight: 700;
padding: 38px 18px;
padding: 46px 18px;
margin: 0 auto @line-height-computed;
text-shadow: 0 -1px 1px darken(@link-color, 10%);
width: 120px;
@ -71,11 +71,20 @@
background-color: @brand-secondary;
color: white;
white-space: nowrap;
line-height: 1;
span.small {
color: rgba(255, 255, 255, 0.85);
font-size: @font-size-base * .8;
}
}
.circle-lg {
width: 150px;
height: 150px;
padding-top: 50px;
}
.circle-subtext {
font-size: 1rem;
}
.circle-img {
float: right;
}

View file

@ -0,0 +1,4 @@
source 'https://rubygems.org'
gem 'recurly'
gem 'json'

View file

@ -0,0 +1,62 @@
require 'rubygems'
require 'recurly'
require 'json'
if ENV['RECURLY_SUBDOMAIN']
Recurly.subdomain = ENV['RECURLY_SUBDOMAIN']
else
print "Defaulting to sharelatex-sandbox. Set RECURLY_SUBDOMAIN environment variable to override\n"
Recurly.subdomain = "sharelatex-sandbox"
end
if ENV['RECURLY_API_KEY']
Recurly.api_key = ENV['RECURLY_API_KEY']
else
print "Please set RECURLY_API_KEY environment variable\n"
exit 1
end
file = File.read('../../app/templates/plans/groups.json')
groups = JSON.parse(file)
# data format: groups[usage][plan_code][currency][size] = price
PLANS = {}
groups.each do |usage, data|
data.each do |plan_code, data|
data.each do |currency, data|
data.each do |size, price|
full_plan_code = "group_#{plan_code}_#{size}_#{usage}"
plan = PLANS[full_plan_code] ||= {
plan_code: full_plan_code,
name: "Overleaf #{plan_code.capitalize} - Group Account (#{size} licenses) - #{usage.capitalize}",
unit_amount_in_cents: {},
plan_interval_length: 12,
plan_interval_unit: 'months'
}
plan[:unit_amount_in_cents][currency] = price * 100
end
end
end
end
PLANS.each do |plan_code, plan|
print "Syncing #{plan_code}...\n"
print "#{plan}\n"
begin
recurly_plan = Recurly::Plan.find(plan_code)
rescue Recurly::Resource::NotFound => e
recurly_plan = nil
end
if recurly_plan.nil?
print "No plan found, creating...\n"
Recurly::Plan.create(plan)
else
print "Existing plan found, updating...\n"
plan.each do |key, value|
recurly_plan[key] = value
recurly_plan.save
end
end
print "Done!\n"
end

View file

@ -77,6 +77,7 @@ describe "SubscriptionController", ->
"../User/UserGetter": @UserGetter
"./RecurlyWrapper": @RecurlyWrapper = {}
"./FeaturesUpdater": @FeaturesUpdater = {}
"./GroupPlansData": @GroupPlansData = {}
@res = new MockResponse()
@ -135,6 +136,8 @@ describe "SubscriptionController", ->
"../User/UserGetter": @UserGetter
"./RecurlyWrapper": @RecurlyWrapper = {}
"./FeaturesUpdater": @FeaturesUpdater = {}
"./GroupPlansData": @GroupPlansData
@SubscriptionController.plansPage(@req, @res)
it 'should not fetch the current user', (done) ->