Add recent subscribers to admin dashboard
This commit is contained in:
parent
603f9f9a37
commit
3c755d39e6
4 changed files with 139 additions and 18 deletions
app
styles/admin
templates/admin
views/admin
server/payments
|
@ -26,9 +26,6 @@
|
||||||
height: 500px
|
height: 500px
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
// TODO: figure out why this is necessary
|
|
||||||
margin-bottom: 100px
|
|
||||||
|
|
||||||
.x.axis
|
.x.axis
|
||||||
font-size: 9pt
|
font-size: 9pt
|
||||||
path
|
path
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
extends /templates/base
|
extends /templates/base
|
||||||
|
|
||||||
block content
|
block content
|
||||||
|
|
||||||
if !me.isAdmin()
|
if !me.isAdmin()
|
||||||
div You must be logged in as an admin to view this page.
|
div You must be logged in as an admin to view this page.
|
||||||
else
|
else
|
||||||
|
|
||||||
if total === 0
|
if total === 0
|
||||||
h1 Fetching subscriptions data...
|
h4 Fetching dashboard data...
|
||||||
else
|
else
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
.row
|
||||||
|
@ -37,15 +38,56 @@ block content
|
||||||
|
|
||||||
div *Stripe APIs do not return information about inactive subs.
|
div *Stripe APIs do not return information about inactive subs.
|
||||||
|
|
||||||
br
|
h2 Recent Subscribers
|
||||||
|
if !subscribers || subscribers.length < 1
|
||||||
table.table.table-condensed.concepts-table
|
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
|
thead
|
||||||
tr
|
tr
|
||||||
th Day
|
th Day
|
||||||
th Total
|
th Total
|
||||||
th Started
|
th Started
|
||||||
th Cancelled
|
th Cancelled
|
||||||
|
th Net
|
||||||
tbody
|
tbody
|
||||||
each sub in subs
|
each sub in subs
|
||||||
tr
|
tr
|
||||||
|
@ -53,3 +95,4 @@ block content
|
||||||
td= sub.total
|
td= sub.total
|
||||||
td= sub.started
|
td= sub.started
|
||||||
td= sub.cancelled
|
td= sub.cancelled
|
||||||
|
td= sub.started - sub.cancelled
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
RootView = require 'views/core/RootView'
|
RootView = require 'views/core/RootView'
|
||||||
template = require 'templates/admin/analytics-subscriptions'
|
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 last N subscribers table
|
||||||
# TODO: Add revenue line
|
# TODO: Add revenue line
|
||||||
|
@ -23,6 +24,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
|
||||||
context = super()
|
context = super()
|
||||||
context.analytics = @analytics ? graphs: []
|
context.analytics = @analytics ? graphs: []
|
||||||
context.subs = _.cloneDeep(@subs ? []).reverse()
|
context.subs = _.cloneDeep(@subs ? []).reverse()
|
||||||
|
context.subscribers = @subscribers ? []
|
||||||
context.total = @total ? 0
|
context.total = @total ? 0
|
||||||
context.cancelled = @cancelled ? 0
|
context.cancelled = @cancelled ? 0
|
||||||
context.monthlyChurn = @monthlyChurn ? 0.0
|
context.monthlyChurn = @monthlyChurn ? 0.0
|
||||||
|
@ -43,7 +45,28 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
|
||||||
refreshData: ->
|
refreshData: ->
|
||||||
return unless me.isAdmin()
|
return unless me.isAdmin()
|
||||||
@resetData()
|
@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 =
|
options =
|
||||||
url: '/db/subscription/-/subscriptions'
|
url: '/db/subscription/-/subscriptions'
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
|
@ -66,7 +89,7 @@ module.exports = class AnalyticsSubscriptionsView extends RootView
|
||||||
for day of subDayMap
|
for day of subDayMap
|
||||||
@subs.push
|
@subs.push
|
||||||
day: day
|
day: day
|
||||||
started: subDayMap[day]['start']
|
started: subDayMap[day]['start'] or 0
|
||||||
cancelled: subDayMap[day]['cancel'] or 0
|
cancelled: subDayMap[day]['cancel'] or 0
|
||||||
|
|
||||||
@subs.sort (a, b) -> a.day.localeCompare(b.day)
|
@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
|
startedLastMonth += sub.started if @subs.length - i < 31
|
||||||
@monthlyChurn = @cancelled / startedLastMonth * 100.0 if startedLastMonth > 0
|
@monthlyChurn = @cancelled / startedLastMonth * 100.0 if startedLastMonth > 0
|
||||||
if @subs.length > 30 and @subs[@subs.length - 31].total > 0
|
if @subs.length > 30 and @subs[@subs.length - 31].total > 0
|
||||||
lastMonthTotal = @subs[@subs.length - 31].total
|
startMonthTotal = @subs[@subs.length - 31].total
|
||||||
thisMonthTotal = @subs[@subs.length - 1].total
|
endMonthTotal = @subs[@subs.length - 1].total
|
||||||
@monthlyGrowth = (thisMonthTotal - lastMonthTotal) / lastMonthTotal * 100
|
@monthlyGrowth = (endMonthTotal / startMonthTotal - 1) * 100
|
||||||
@updateAnalyticsGraphData()
|
@updateAnalyticsGraphData()
|
||||||
@render?()
|
@render?()
|
||||||
@supermodel.addRequestResource('get_subscriptions', options, 0).load()
|
@supermodel.addRequestResource('get_subscriptions', options, 0).load()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Not paired with a document in the DB, just handles coordinating between
|
# 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.
|
# the stripe property in the user with what's being stored in Stripe.
|
||||||
|
|
||||||
|
mongoose = require 'mongoose'
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
config = require '../../server_config'
|
config = require '../../server_config'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
|
@ -23,9 +24,70 @@ class SubscriptionHandler extends Handler
|
||||||
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
||||||
|
|
||||||
getByRelationship: (req, res, args...) ->
|
getByRelationship: (req, res, args...) ->
|
||||||
|
return @getSubscribers(req, res) if args[1] is 'subscribers'
|
||||||
return @getSubscriptions(req, res) if args[1] is 'subscriptions'
|
return @getSubscriptions(req, res) if args[1] is 'subscriptions'
|
||||||
super(arguments...)
|
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) ->
|
getSubscriptions: (req, res) ->
|
||||||
# Returns a list of active subscriptions
|
# Returns a list of active subscriptions
|
||||||
# TODO: does not handle customers with 11+ 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
|
for subscription in customer.subscriptions.data
|
||||||
continue unless subscription.plan.id is 'basic'
|
continue unless subscription.plan.id is 'basic'
|
||||||
|
|
||||||
|
|
||||||
amount = subscription.plan.amount
|
amount = subscription.plan.amount
|
||||||
if subscription?.discount?.coupon?
|
if subscription?.discount?.coupon?
|
||||||
if subscription.discount.coupon.percent_off
|
if subscription.discount.coupon.percent_off
|
||||||
|
@ -72,7 +133,7 @@ class SubscriptionHandler extends Handler
|
||||||
sub = start: new Date(subscription.start * 1000)
|
sub = start: new Date(subscription.start * 1000)
|
||||||
if subscription.cancel_at_period_end
|
if subscription.cancel_at_period_end
|
||||||
sub.cancel = new Date(subscription.canceled_at * 1000)
|
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)
|
@subs.push(sub)
|
||||||
|
|
||||||
# Can't fetch all the test Stripe data
|
# Can't fetch all the test Stripe data
|
||||||
|
@ -84,9 +145,6 @@ class SubscriptionHandler extends Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, @subs)
|
@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})
|
||||||
|
|
Reference in a new issue