Add current subscription counts view under the admin page

This commit is contained in:
Matt Lott 2015-03-27 11:22:21 -07:00
parent 155eb4a4a8
commit 53de6c6134
11 changed files with 195 additions and 16 deletions

View file

@ -33,7 +33,8 @@ module.exports = class CocoRouter extends Backbone.Router
'admin/clas': go('admin/CLAsView') 'admin/clas': go('admin/CLAsView')
'admin/employers': go('admin/EmployersListView') 'admin/employers': go('admin/EmployersListView')
'admin/files': go('admin/FilesView') 'admin/files': go('admin/FilesView')
'admin/growth': go('admin/GrowthView') 'admin/analytics/users': go('admin/AnalyticsUsersView')
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
'admin/level-sessions': go('admin/LevelSessionsView') 'admin/level-sessions': go('admin/LevelSessionsView')
'admin/users': go('admin/UsersView') 'admin/users': go('admin/UsersView')
'admin/base': go('admin/BaseView') 'admin/base': go('admin/BaseView')

View file

@ -0,0 +1,19 @@
#admin-analytics-subscriptions-view
.total-count
width: 33%
float: left
color: green
.remaining-count
width: 33%
float: left
color: blue
.cancelled-count
width: 33%
float: left
color: red
.count
font-size: 50pt
.description
font-size: 8pt

View file

@ -44,8 +44,12 @@ block content
li li
a(href="/admin/clas", data-i18n="admin.clas") CLAs a(href="/admin/clas", data-i18n="admin.clas") CLAs
if me.isAdmin() if me.isAdmin()
li Analytics
ul
li li
a(href="/admin/growth", data-i18n="admin.growth") Growth a(href="/admin/analytics/subscriptions") Subscriptions
li
a(href="/admin/analytics/users") Users (needs updating)
if me.isAdmin() if me.isAdmin()
hr hr

View file

@ -0,0 +1,35 @@
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...
else
div
.total-count
div.description Total
div.count= total
.remaining-count
div.description Remaining
div.count= total - cancelled
.cancelled-count
div.description cancelled
div.count= cancelled
table.table.table-condensed.concepts-table
thead
tr
th Day
th Total
th Started
th Cancelled
tbody
each sub in subs
tr
td= sub.day
td= sub.total
td= sub.started
td= sub.cancelled

View file

@ -2,7 +2,7 @@ extends /templates/base
block content block content
h1(data-i18n="admin.growth_title") Growth h1(data-i18n="admin.growth_title") Users
if me.isAdmin() if me.isAdmin()
if crunchingData if crunchingData
h4 Crunching Data.. h4 Crunching Data..

View file

@ -0,0 +1,53 @@
RootView = require 'views/core/RootView'
template = require 'templates/admin/analytics-subscriptions'
RealTimeCollection = require 'collections/RealTimeCollection'
require 'vendor/d3'
module.exports = class AnalyticsSubscriptionsView extends RootView
id: 'admin-analytics-subscriptions-view'
template: template
constructor: (options) ->
super options
@refreshData()
getRenderData: ->
context = super()
context.subs = @subs ? []
context.total = @total ? 0
context.cancelled = @cancelled ? 0
context
refreshData: ->
@subs = []
@total = 0
@cancelled = 0
onSuccess = (subs) =>
subDayMap = {}
for sub in subs
startDay = sub.start.substring(0, 10)
subDayMap[startDay] ?= {}
subDayMap[startDay]['start'] ?= 0
subDayMap[startDay]['start']++
if cancelDay = sub?.cancel?.substring(0, 10)
subDayMap[cancelDay] ?= {}
subDayMap[cancelDay]['cancel'] ?= 0
subDayMap[cancelDay]['cancel']++
for day of subDayMap
@subs.push
day: day
started: subDayMap[day]['start']
cancelled: subDayMap[day]['cancel'] or 0
@subs.sort (a, b) -> -a.day.localeCompare(b.day)
for i in [@subs.length - 1..0]
@total += @subs[i].started
@cancelled += @subs[i].cancelled
@subs[i].total = @total
@render()
@supermodel.addRequestResource('subscriptions', {
url: '/db/subscription/-/subscriptions'
method: 'GET'
success: onSuccess
}, 0).load()

View file

@ -1,5 +1,5 @@
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
template = require 'templates/admin/growth' template = require 'templates/admin/analytics-users'
RealTimeCollection = require 'collections/RealTimeCollection' RealTimeCollection = require 'collections/RealTimeCollection'
require 'vendor/d3' require 'vendor/d3'
@ -16,8 +16,8 @@ require 'vendor/d3'
# TODO: aggregate recent data if missing? # TODO: aggregate recent data if missing?
# #
module.exports = class GrowthView extends RootView module.exports = class AnalyticsUsersView extends RootView
id: 'admin-growth-view' id: 'admin-analytics-users-view'
template: template template: template
height: 300 height: 300
width: 1000 width: 1000
@ -191,4 +191,3 @@ module.exports = class GrowthView extends RootView
.attr("y", (d) -> d.start.y - 6) .attr("y", (d) -> d.start.y - 6)
.attr("dy", ".35em") .attr("dy", ".35em")
.text((d) -> d.start.id) .text((d) -> d.start.id)

View file

@ -24,6 +24,7 @@ module.exports.handlers =
'poll': 'polls/poll_handler' 'poll': 'polls/poll_handler'
'user_polls_record': 'polls/user_polls_record_handler' 'user_polls_record': 'polls/user_polls_record_handler'
'prepaid': 'prepaids/prepaid_handler' 'prepaid': 'prepaids/prepaid_handler'
'subscription': 'payments/subscription_handler'
module.exports.routes = module.exports.routes =
[ [

View file

@ -2,6 +2,7 @@
# the stripe property in the user with what's being stored in Stripe. # the stripe property in the user with what's being stored in Stripe.
async = require 'async' async = require 'async'
config = require '../../server_config'
Handler = require '../commons/Handler' Handler = require '../commons/Handler'
discountHandler = require './discount_handler' discountHandler = require './discount_handler'
Prepaid = require '../prepaids/Prepaid' Prepaid = require '../prepaids/Prepaid'
@ -21,6 +22,70 @@ class SubscriptionHandler extends Handler
logSubscriptionError: (user, msg) -> logSubscriptionError: (user, msg) ->
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'" console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
getByRelationship: (req, res, args...) ->
return @getSubscriptions(req, res) if args[1] is 'subscriptions'
super(arguments...)
getSubscriptions: (req, res) ->
# Returns a list of active subscriptions
# TODO: does not handle customers with 11+ active subscriptions
# TODO: does not track sponsored subs, only basic
# TODO: does not return free subs
# TODO: add tests
# TODO: aggregate this data daily instead of providing it on demand
# TODO: take date range as input
return @sendForbiddenError(res) unless req.user and req.user.isAdmin()
@subs ?= []
# return @sendSuccess(res, @subs) unless _.isEmpty(@subs)
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
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
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)
@subs.push(sub)
# Can't fetch all the test Stripe data
if customers.has_more and (config.isProduction or customersProcessed < 500)
return nextBatch(customers.data[customers.data.length - 1].id, done)
else
return done()
nextBatch null, (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @subs)
subscribeUser: (req, user, done) -> subscribeUser: (req, user, done) ->
if (not req.user) or req.user.isAnonymous() or user.isAnonymous() if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
return done({res: 'You must be signed in to subscribe.', code: 403}) return done({res: 'You must be signed in to subscribe.', code: 403})

View file

@ -419,6 +419,8 @@ describe 'Subscriptions', ->
options = customer: body.stripe.customerID, limit: 100 options = customer: body.stripe.customerID, limit: 100
stripe.invoices.list options, (err, invoices) -> stripe.invoices.list options, (err, invoices) ->
expect(err).toBeNull() expect(err).toBeNull()
expect(invoices).not.toBeNull()
return done(updatedUser) unless invoices?
expect(invoices.has_more).toEqual(false) expect(invoices.has_more).toEqual(false)
makeWebhookCall = (invoice) -> makeWebhookCall = (invoice) ->
(callback) -> (callback) ->