mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Add current subscription counts view under the admin page
This commit is contained in:
parent
155eb4a4a8
commit
53de6c6134
11 changed files with 195 additions and 16 deletions
|
@ -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')
|
||||
|
|
19
app/styles/admin/analytics-subscriptions.sass
Normal file
19
app/styles/admin/analytics-subscriptions.sass
Normal 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
|
|
@ -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
|
||||
|
|
35
app/templates/admin/analytics-subscriptions.jade
Normal file
35
app/templates/admin/analytics-subscriptions.jade
Normal 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
|
|
@ -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..
|
53
app/views/admin/AnalyticsSubscriptionsView.coffee
Normal file
53
app/views/admin/AnalyticsSubscriptionsView.coffee
Normal 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()
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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 =
|
||||
[
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Reference in a new issue