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/employers': go('admin/EmployersListView')
'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/users': go('admin/UsersView')
'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
a(href="/admin/clas", data-i18n="admin.clas") CLAs
if me.isAdmin()
li
a(href="/admin/growth", data-i18n="admin.growth") Growth
li Analytics
ul
li
a(href="/admin/analytics/subscriptions") Subscriptions
li
a(href="/admin/analytics/users") Users (needs updating)
if me.isAdmin()
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
h1(data-i18n="admin.growth_title") Growth
h1(data-i18n="admin.growth_title") Users
if me.isAdmin()
if crunchingData
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'
template = require 'templates/admin/growth'
template = require 'templates/admin/analytics-users'
RealTimeCollection = require 'collections/RealTimeCollection'
require 'vendor/d3'
@ -16,8 +16,8 @@ require 'vendor/d3'
# TODO: aggregate recent data if missing?
#
module.exports = class GrowthView extends RootView
id: 'admin-growth-view'
module.exports = class AnalyticsUsersView extends RootView
id: 'admin-analytics-users-view'
template: template
height: 300
width: 1000
@ -55,7 +55,7 @@ module.exports = class GrowthView extends RootView
if me.isAdmin()
@createPerDayChart()
@createPerMonthChart()
createPerDayChart: ->
addedData = []
totalData = []
@ -76,7 +76,7 @@ module.exports = class GrowthView extends RootView
createLineChart: (selector, data, guidelineSpacing, sevenDayAverage=false) ->
return unless data.length > 1
minVal = d3.min(data, (d) -> d.value)
maxVal = d3.max(data, (d) -> d.value)
@ -129,16 +129,16 @@ module.exports = class GrowthView extends RootView
.attr("cx", (d) -> d.x )
.attr("cy", (d) -> d.y )
.attr("r", "2px")
.attr("fill", "black")
.attr("fill", "black")
chart.selectAll(".text")
.data(points)
.enter()
.append("text")
.attr("dy", ".35em")
.attr("transform", (d, i) => "translate(" + d.x + "," + @height + ") rotate(270)")
.text((d) ->
if d.id.length is 8
.text((d) ->
if d.id.length is 8
return "#{parseInt(d.id[4..5])}/#{parseInt(d.id[6..7])}/#{d.id[0..3]}"
else
return "#{parseInt(d.id[4..5])}/#{d.id[0..3]}"
@ -161,7 +161,7 @@ module.exports = class GrowthView extends RootView
.attr("cx", (d) -> d.x )
.attr("cy", (d) -> d.y )
.attr("r", "2px")
.attr("fill", "purple")
.attr("fill", "purple")
chart.selectAll('.line')
.data(sevenLinks)
@ -191,4 +191,3 @@ module.exports = class GrowthView extends RootView
.attr("y", (d) -> d.start.y - 6)
.attr("dy", ".35em")
.text((d) -> d.start.id)

View file

@ -29,7 +29,7 @@ getSubscriptions(null, function () {
});
function getSubscriptions(starting_after, done)
function getSubscriptions(starting_after, done)
{
var options = {limit: 100};
if (starting_after) options.starting_after = starting_after;

View file

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

View file

@ -2,6 +2,7 @@
# the stripe property in the user with what's being stored in Stripe.
async = require 'async'
config = require '../../server_config'
Handler = require '../commons/Handler'
discountHandler = require './discount_handler'
Prepaid = require '../prepaids/Prepaid'
@ -21,6 +22,70 @@ class SubscriptionHandler extends Handler
logSubscriptionError: (user, 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) ->
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
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
stripe.invoices.list options, (err, invoices) ->
expect(err).toBeNull()
expect(invoices).not.toBeNull()
return done(updatedUser) unless invoices?
expect(invoices.has_more).toEqual(false)
makeWebhookCall = (invoice) ->
(callback) ->