diff --git a/app/styles/admin/analytics-subscriptions.sass b/app/styles/admin/analytics-subscriptions.sass index c182ff9c5..43c0e7543 100644 --- a/app/styles/admin/analytics-subscriptions.sass +++ b/app/styles/admin/analytics-subscriptions.sass @@ -26,9 +26,6 @@ height: 500px width: 100% - // TODO: figure out why this is necessary - margin-bottom: 100px - .x.axis font-size: 9pt path diff --git a/app/templates/admin/analytics-subscriptions.jade b/app/templates/admin/analytics-subscriptions.jade index 0f98913f4..200dff01a 100644 --- a/app/templates/admin/analytics-subscriptions.jade +++ b/app/templates/admin/analytics-subscriptions.jade @@ -1,12 +1,13 @@ extends /templates/base block content - + if !me.isAdmin() div You must be logged in as an admin to view this page. else + if total === 0 - h1 Fetching subscriptions data... + h4 Fetching dashboard data... else .container-fluid .row @@ -37,15 +38,56 @@ block content div *Stripe APIs do not return information about inactive subs. - br - - table.table.table-condensed.concepts-table + h2 Recent Subscribers + if !subscribers || subscribers.length < 1 + h4 Fetching recent subscribers... + else + table.table.table-condensed + thead + tr + th User Start + th Sub Start + th + th + //- th Name + th Email + th Hero + th Level + th Last Level + th Age + th Spoken + tbody + each subscriber in subscribers + tr + td= subscriber.user.dateCreated.substring(0, 10) + td= subscriber.start.substring(0, 10) + td + if subscriber.cancel + span= subscriber.cancel.substring(0, 10) + td + if subscriber.user.stripe.sponsorID + span Sponsored + //- td + //- a(href="/user/#{subscriber.user._id}")= subscriber.user.name || 'Anoner' + td= subscriber.user.emailLower + td= subscriber.hero + td= subscriber.level + td= subscriber.user.lastLevel + td= subscriber.user.ageRange + td= subscriber.user.preferredLanguage + + h2 Subscriptions + if !subs || subs.length < 1 + h4 Fetching subscriptions... + else + table.table.table-condensed thead tr th Day th Total th Started th Cancelled + th Net tbody each sub in subs tr @@ -53,3 +95,4 @@ block content td= sub.total td= sub.started td= sub.cancelled + td= sub.started - sub.cancelled diff --git a/app/views/admin/AnalyticsSubscriptionsView.coffee b/app/views/admin/AnalyticsSubscriptionsView.coffee index cd44a17aa..9a86ab724 100644 --- a/app/views/admin/AnalyticsSubscriptionsView.coffee +++ b/app/views/admin/AnalyticsSubscriptionsView.coffee @@ -1,6 +1,7 @@ RootView = require 'views/core/RootView' template = require 'templates/admin/analytics-subscriptions' -RealTimeCollection = require 'collections/RealTimeCollection' +ThangType = require 'models/ThangType' +User = require 'models/User' # TODO: Add last N subscribers table # TODO: Add revenue line @@ -23,6 +24,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView context = super() context.analytics = @analytics ? graphs: [] context.subs = _.cloneDeep(@subs ? []).reverse() + context.subscribers = @subscribers ? [] context.total = @total ? 0 context.cancelled = @cancelled ? 0 context.monthlyChurn = @monthlyChurn ? 0.0 @@ -43,7 +45,28 @@ module.exports = class AnalyticsSubscriptionsView extends RootView refreshData: -> return unless me.isAdmin() @resetData() + @getSubscribers() + @getSubscriptions() + getSubscribers: -> + options = + url: '/db/subscription/-/subscribers' + method: 'POST' + data: {maxCount: 30} + options.error = (model, response, options) => + return if @destroyed + console.error 'Failed to get subscribers', response + options.success = (subscribers, response, options) => + return if @destroyed + @subscribers = subscribers + for subscriber in @subscribers + subscriber.level = User.levelFromExp subscriber.user.points + if hero = subscriber.user.heroConfig?.thangType + subscriber.hero = slug for slug, original of ThangType.heroes when original is hero + @render?() + @supermodel.addRequestResource('get_subscribers', options, 0).load() + + getSubscriptions: -> options = url: '/db/subscription/-/subscriptions' method: 'GET' @@ -66,7 +89,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView for day of subDayMap @subs.push day: day - started: subDayMap[day]['start'] + started: subDayMap[day]['start'] or 0 cancelled: subDayMap[day]['cancel'] or 0 @subs.sort (a, b) -> a.day.localeCompare(b.day) @@ -78,9 +101,9 @@ module.exports = class AnalyticsSubscriptionsView extends RootView startedLastMonth += sub.started if @subs.length - i < 31 @monthlyChurn = @cancelled / startedLastMonth * 100.0 if startedLastMonth > 0 if @subs.length > 30 and @subs[@subs.length - 31].total > 0 - lastMonthTotal = @subs[@subs.length - 31].total - thisMonthTotal = @subs[@subs.length - 1].total - @monthlyGrowth = (thisMonthTotal - lastMonthTotal) / lastMonthTotal * 100 + startMonthTotal = @subs[@subs.length - 31].total + endMonthTotal = @subs[@subs.length - 1].total + @monthlyGrowth = (endMonthTotal / startMonthTotal - 1) * 100 @updateAnalyticsGraphData() @render?() @supermodel.addRequestResource('get_subscriptions', options, 0).load() diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 79005ea0f..d8fc6ea52 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -1,6 +1,7 @@ # Not paired with a document in the DB, just handles coordinating between # the stripe property in the user with what's being stored in Stripe. +mongoose = require 'mongoose' async = require 'async' config = require '../../server_config' Handler = require '../commons/Handler' @@ -23,9 +24,70 @@ class SubscriptionHandler extends Handler console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'" getByRelationship: (req, res, args...) -> + return @getSubscribers(req, res) if args[1] is 'subscribers' return @getSubscriptions(req, res) if args[1] is 'subscriptions' super(arguments...) + getSubscribers: (req, res) -> + return @sendForbiddenError(res) unless req.user and req.user.isAdmin() + + maxReturnCount = req.body.maxCount or 20 + + # @subscribers ?= [] + # return @sendSuccess(res, @subscribers) unless _.isEmpty(@subscribers) + @subscribers = [] + + subscriberIDs = [] + + customersProcessed = 0 + nextBatch = (starting_after, done) => + options = limit: 100 + options.starting_after = starting_after if starting_after + stripe.customers.list options, (err, customers) => + return done(err) if err + customersProcessed += customers.data.length + + for customer in customers.data + break unless @subscribers.length < maxReturnCount + continue unless customer?.subscriptions?.data?.length > 0 + for subscription in customer.subscriptions.data + continue unless subscription.plan.id is 'basic' + + amount = subscription.plan.amount + if subscription?.discount?.coupon? + if subscription.discount.coupon.percent_off + amount = amount * (100 - subscription.discount.coupon.percent_off) / 100; + else if subscription.discount.coupon.amount_off + amount -= subscription.discount.coupon.amount_off + else if customer.discount?.coupon? + if customer.discount.coupon.percent_off + amount = amount * (100 - customer.discount.coupon.percent_off) / 100 + else if customer.discount.coupon.amount_off + amount -= customer.discount.coupon.amount_off + + continue unless amount > 0 + + subscriber = start: new Date(subscription.start * 1000) + if subscription.metadata?.id? + subscriber.userID = subscription.metadata.id + subscriberIDs.push subscription.metadata.id + if subscription.cancel_at_period_end + subscriber.cancel = new Date(subscription.canceled_at * 1000) + subscriber.end = new Date(subscription.current_period_end * 1000) + @subscribers.push(subscriber) + + if customers.has_more and @subscribers.length < maxReturnCount + return nextBatch(customers.data[customers.data.length - 1].id, done) + else + return done() + nextBatch null, (err) => + return @sendDatabaseError(res, err) if err + User.find {_id: {$in: subscriberIDs}}, (err, users) => + return @sendDatabaseError(res, err) if err + for user in users + subscriber.user = user for subscriber in @subscribers when subscriber.userID is user.id + @sendSuccess(res, @subscribers) + getSubscriptions: (req, res) -> # Returns a list of active subscriptions # TODO: does not handle customers with 11+ active subscriptions @@ -54,7 +116,6 @@ class SubscriptionHandler extends Handler for subscription in customer.subscriptions.data continue unless subscription.plan.id is 'basic' - amount = subscription.plan.amount if subscription?.discount?.coupon? if subscription.discount.coupon.percent_off @@ -72,7 +133,7 @@ class SubscriptionHandler extends Handler sub = start: new Date(subscription.start * 1000) if subscription.cancel_at_period_end sub.cancel = new Date(subscription.canceled_at * 1000) - sub.end = new Date(sub.current_period_end * 1000) + sub.end = new Date(subscription.current_period_end * 1000) @subs.push(sub) # Can't fetch all the test Stripe data @@ -84,9 +145,6 @@ class SubscriptionHandler extends Handler return @sendDatabaseError(res, err) if err @sendSuccess(res, @subs) - - - subscribeUser: (req, user, done) -> if (not req.user) or req.user.isAnonymous() or user.isAnonymous() return done({res: 'You must be signed in to subscribe.', code: 403})