| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- const Settings = require('@overleaf/settings')
- const RecurlyWrapper = require('./RecurlyWrapper')
- const PlansLocator = require('./PlansLocator')
- const SubscriptionFormatters = require('./SubscriptionFormatters')
- const SubscriptionLocator = require('./SubscriptionLocator')
- const V1SubscriptionManager = require('./V1SubscriptionManager')
- const InstitutionsGetter = require('../Institutions/InstitutionsGetter')
- const PublishersGetter = require('../Publishers/PublishersGetter')
- const sanitizeHtml = require('sanitize-html')
- const _ = require('underscore')
- const async = require('async')
- const SubscriptionHelper = require('./SubscriptionHelper')
- const { promisify } = require('../../util/promises')
- function buildHostedLink(recurlySubscription, type) {
- const recurlySubdomain = Settings.apis.recurly.subdomain
- const hostedLoginToken = recurlySubscription.account.hosted_login_token
- let path = ''
- if (type === 'billingDetails') {
- path = 'billing_info/edit?ht='
- }
- if (hostedLoginToken && recurlySubdomain) {
- return [
- 'https://',
- recurlySubdomain,
- '.recurly.com/account/',
- path,
- hostedLoginToken,
- ].join('')
- }
- }
- function buildUsersSubscriptionViewModel(user, callback) {
- async.auto(
- {
- personalSubscription(cb) {
- SubscriptionLocator.getUsersSubscription(user, cb)
- },
- recurlySubscription: [
- 'personalSubscription',
- (cb, { personalSubscription }) => {
- if (
- personalSubscription == null ||
- personalSubscription.recurlySubscription_id == null ||
- personalSubscription.recurlySubscription_id === ''
- ) {
- return cb(null, null)
- }
- RecurlyWrapper.getSubscription(
- personalSubscription.recurlySubscription_id,
- { includeAccount: true },
- cb
- )
- },
- ],
- recurlyCoupons: [
- 'recurlySubscription',
- (cb, { recurlySubscription }) => {
- if (!recurlySubscription) {
- return cb(null, null)
- }
- const accountId = recurlySubscription.account.account_code
- RecurlyWrapper.getAccountActiveCoupons(accountId, cb)
- },
- ],
- plan: [
- 'personalSubscription',
- (cb, { personalSubscription }) => {
- if (personalSubscription == null) {
- return cb()
- }
- const plan = PlansLocator.findLocalPlanInSettings(
- personalSubscription.planCode
- )
- if (plan == null) {
- return cb(
- new Error(
- `No plan found for planCode '${personalSubscription.planCode}'`
- )
- )
- }
- cb(null, plan)
- },
- ],
- memberGroupSubscriptions(cb) {
- SubscriptionLocator.getMemberSubscriptions(user, cb)
- },
- managedGroupSubscriptions(cb) {
- SubscriptionLocator.getManagedGroupSubscriptions(user, cb)
- },
- confirmedMemberAffiliations(cb) {
- InstitutionsGetter.getConfirmedAffiliations(user._id, cb)
- },
- managedInstitutions(cb) {
- InstitutionsGetter.getManagedInstitutions(user._id, cb)
- },
- managedPublishers(cb) {
- PublishersGetter.getManagedPublishers(user._id, cb)
- },
- v1SubscriptionStatus(cb) {
- V1SubscriptionManager.getSubscriptionStatusFromV1(
- user._id,
- (error, status, v1Id) => {
- if (error) {
- return cb(error)
- }
- cb(null, status)
- }
- )
- },
- },
- (err, results) => {
- if (err) {
- return callback(err)
- }
- let {
- personalSubscription,
- memberGroupSubscriptions,
- managedGroupSubscriptions,
- confirmedMemberAffiliations,
- managedInstitutions,
- managedPublishers,
- v1SubscriptionStatus,
- recurlySubscription,
- recurlyCoupons,
- plan,
- } = results
- if (memberGroupSubscriptions == null) {
- memberGroupSubscriptions = []
- }
- if (managedGroupSubscriptions == null) {
- managedGroupSubscriptions = []
- }
- if (confirmedMemberAffiliations == null) {
- confirmedMemberAffiliations = []
- }
- if (managedInstitutions == null) {
- managedInstitutions = []
- }
- if (v1SubscriptionStatus == null) {
- v1SubscriptionStatus = {}
- }
- if (recurlyCoupons == null) {
- recurlyCoupons = []
- }
- if (
- personalSubscription &&
- typeof personalSubscription.toObject === 'function'
- ) {
- // Downgrade from Mongoose object, so we can add a recurly and plan attribute
- personalSubscription = personalSubscription.toObject()
- }
- if (plan != null) {
- personalSubscription.plan = plan
- }
- if (personalSubscription && recurlySubscription) {
- const tax = recurlySubscription.tax_in_cents || 0
- // Some plans allow adding more seats than the base plan provides.
- // This is recorded as a subscription add on.
- // Note: tax_in_cents already includes the tax for any addon.
- let addOnPrice = 0
- let additionalLicenses = 0
- if (
- plan.membersLimitAddOn &&
- Array.isArray(recurlySubscription.subscription_add_ons)
- ) {
- recurlySubscription.subscription_add_ons.forEach(addOn => {
- if (addOn.add_on_code === plan.membersLimitAddOn) {
- addOnPrice += addOn.quantity * addOn.unit_amount_in_cents
- additionalLicenses += addOn.quantity
- }
- })
- }
- const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
- personalSubscription.recurly = {
- tax,
- taxRate: recurlySubscription.tax_rate
- ? parseFloat(recurlySubscription.tax_rate._)
- : 0,
- billingDetailsLink: buildHostedLink(
- recurlySubscription,
- 'billingDetails'
- ),
- accountManagementLink: buildHostedLink(recurlySubscription),
- additionalLicenses,
- totalLicenses,
- 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,
- activeCoupons: recurlyCoupons,
- account: recurlySubscription.account,
- }
- if (recurlySubscription.pending_subscription) {
- const pendingPlan = PlansLocator.findLocalPlanInSettings(
- recurlySubscription.pending_subscription.plan.plan_code
- )
- if (pendingPlan == null) {
- return callback(
- new Error(
- `No plan found for planCode '${personalSubscription.planCode}'`
- )
- )
- }
- let pendingAdditionalLicenses = 0
- let pendingAddOnTax = 0
- let pendingAddOnPrice = 0
- if (recurlySubscription.pending_subscription.subscription_add_ons) {
- if (
- pendingPlan.membersLimitAddOn &&
- Array.isArray(
- recurlySubscription.pending_subscription.subscription_add_ons
- )
- ) {
- recurlySubscription.pending_subscription.subscription_add_ons.forEach(
- addOn => {
- if (addOn.add_on_code === pendingPlan.membersLimitAddOn) {
- pendingAddOnPrice +=
- addOn.quantity * addOn.unit_amount_in_cents
- pendingAdditionalLicenses += addOn.quantity
- }
- }
- )
- }
- // Need to calculate tax ourselves as we don't get tax amounts for pending subs
- pendingAddOnTax =
- personalSubscription.recurly.taxRate * pendingAddOnPrice
- }
- const pendingSubscriptionTax =
- personalSubscription.recurly.taxRate *
- recurlySubscription.pending_subscription.unit_amount_in_cents
- personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
- recurlySubscription.pending_subscription.unit_amount_in_cents +
- pendingAddOnPrice +
- pendingAddOnTax +
- pendingSubscriptionTax,
- recurlySubscription.currency
- )
- const pendingTotalLicenses =
- (pendingPlan.membersLimit || 0) + pendingAdditionalLicenses
- personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses
- personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses
- personalSubscription.pendingPlan = pendingPlan
- } else {
- personalSubscription.recurly.price = SubscriptionFormatters.formatPrice(
- recurlySubscription.unit_amount_in_cents + addOnPrice + tax,
- recurlySubscription.currency
- )
- }
- }
- for (const memberGroupSubscription of memberGroupSubscriptions) {
- if (memberGroupSubscription.teamNotice) {
- memberGroupSubscription.teamNotice = sanitizeHtml(
- memberGroupSubscription.teamNotice
- )
- }
- }
- callback(null, {
- personalSubscription,
- managedGroupSubscriptions,
- memberGroupSubscriptions,
- confirmedMemberAffiliations,
- managedInstitutions,
- managedPublishers,
- v1SubscriptionStatus,
- })
- }
- )
- }
- function buildPlansList(currentPlan) {
- const { plans } = Settings
- const allPlans = {}
- plans.forEach(plan => {
- allPlans[plan.planCode] = plan
- })
- const result = { allPlans }
- if (currentPlan) {
- result.planCodesChangingAtTermEnd = _.pluck(
- _.filter(plans, plan => {
- if (!plan.hideFromUsers) {
- return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
- }
- }),
- 'planCode'
- )
- }
- result.studentAccounts = _.filter(
- plans,
- plan => plan.planCode.indexOf('student') !== -1
- )
- result.groupMonthlyPlans = _.filter(
- plans,
- plan => plan.groupPlan && !plan.annual
- )
- result.groupAnnualPlans = _.filter(
- plans,
- plan => plan.groupPlan && plan.annual
- )
- result.individualMonthlyPlans = _.filter(
- plans,
- plan =>
- !plan.groupPlan &&
- !plan.annual &&
- plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page
- plan.planCode.indexOf('student') === -1
- )
- result.individualAnnualPlans = _.filter(
- plans,
- plan =>
- !plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1
- )
- return result
- }
- module.exports = {
- buildUsersSubscriptionViewModel,
- buildPlansList,
- promises: {
- buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),
- },
- }
|