mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 22:13:32 -04:00
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
|
||||
width: 100%
|
||||
|
||||
// TODO: figure out why this is necessary
|
||||
margin-bottom: 100px
|
||||
|
||||
.x.axis
|
||||
font-size: 9pt
|
||||
path
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue