Purchase prepaid on course instance creation

Updating purchase prepaid API to support courses.
Refactoring the prepaid server code.

Completes #54270567235517
This commit is contained in:
Matt Lott 2015-10-01 15:23:20 -07:00
parent 079e3a59f5
commit 5095eac4ac
10 changed files with 553 additions and 351 deletions

View file

@ -199,9 +199,9 @@ module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, sub
else
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
module.exports.getCoursesPrice = getSponsoredSubsAmount = (courses, seats=20) ->
totalPricePerSeat = courses.reduce ((a, b) -> a + b.get('pricePerSeat')), 0
if courses.length > 2
module.exports.getCourseBundlePrice = getCourseBundlePrice = (coursePrices, seats=20) ->
totalPricePerSeat = coursePrices.reduce ((a, b) -> a + b), 0
if coursePrices.length > 2
pricePerSeat = Math.round(totalPricePerSeat / 2.0)
else
pricePerSeat = parseInt(totalPricePerSeat)

View file

@ -39,7 +39,7 @@ block content
label.control-label.col-md-2(data-i18n="account_prepaid.purchase_total")
.col-md-10
p.form-control-static $
span#total #{purchase.total}
span#total #{purchase.total.toFixed(2)}
button#purchase-button.btn.btn-success.pull-right(data-i18n="account_prepaid.purchase_button")
.row
.col-md-12

View file

@ -2,10 +2,10 @@ app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
{getCoursesPrice} = require 'core/utils'
RootView = require 'views/core/RootView'
stripeHandler = require 'core/services/stripe'
template = require 'templates/courses/course-enroll'
utils = require 'core/utils'
module.exports = class CourseEnrollView extends RootView
id: 'course-enroll-view'
@ -101,7 +101,9 @@ module.exports = class CourseEnrollView extends RootView
data =
name: @className
seats: @seats
token: token
stripe:
token: token
timestamp: new Date().getTime()
data.courseID = @selectedCourse.id if @selectedCourse
jqxhr = $.post('/db/course_instance/-/create', data)
jqxhr.done (data, textStatus, jqXHR) =>
@ -109,9 +111,14 @@ module.exports = class CourseEnrollView extends RootView
# TODO: handle fetch errors
me.fetch(cache: false).always =>
courseID = @selectedCourse?.id ? @courses.models[0]?.id
viewArgs = if data?.length > 0 then [courseInstanceID: data[0]._id, courseID] else [{}, courseID]
route = "/courses/#{courseID}"
viewArgs = [{}, courseID]
if data?.length > 0
courseInstanceID = data[0]._id
route += "/#{courseInstanceID}"
viewArgs[0].courseInstanceID = courseInstanceID
Backbone.Mediator.publish 'router:navigate',
route: "/courses/#{courseID}"
route: route
viewClass: 'views/courses/CourseDetailsView'
viewArgs: viewArgs
jqxhr.fail (xhr, textStatus, errorThrown) =>
@ -127,7 +134,8 @@ module.exports = class CourseEnrollView extends RootView
renderNewPrice: ->
if @selectedCourse
@price = getCoursesPrice([@selectedCourse], @seats)
coursePrices = [@selectedCourse.get('pricePerSeat')]
else
@price = getCoursesPrice(@courses.models, @seats)
coursePrices = (c.get('pricePerSeat') for c in @courses.models)
@price = utils.getCourseBundlePrice(coursePrices, @seats)
@render?()

View file

@ -61,6 +61,7 @@ module.exports = class CoursesView extends RootView
application.tracker?.trackEvent 'Subscription payment methods hover'
onClickBuy: (e) ->
$('.continue-dialog').modal('hide')
courseID = $(e.target).data('course-id')
route = "/courses/enroll/#{courseID}"
viewClass = require 'views/courses/CourseEnrollView'

View file

@ -1,13 +1,14 @@
async = require 'async'
Handler = require '../commons/Handler'
{getCoursesPrice} = require '../../app/core/utils'
Course = require './Course'
CourseInstance = require './CourseInstance'
LevelSession = require '../levels/sessions/LevelSession'
LevelSessionHandler = require '../levels/sessions/level_session_handler'
Prepaid = require '../prepaids/Prepaid'
PrepaidHandler = require '../prepaids/prepaid_handler'
User = require '../users/User'
UserHandler = require '../users/user_handler'
utils = require '../../app/core/utils'
CourseInstanceHandler = class CourseInstanceHandler extends Handler
modelClass: CourseInstance
@ -15,7 +16,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
logError: (user, msg) ->
console.warn "Course error: #{user.get('slug')} (#{user._id}): '#{msg}'"
console.warn "Course instance error: #{user.get('slug')} (#{user._id}): '#{msg}'"
hasAccess: (req) ->
req.method in @allowedMethods or req.user?.isAdmin()
@ -33,7 +34,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
super arguments...
createAPI: (req, res) ->
return @sendUnauthorizedError(res) unless req.user?
return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()
# Required Input
seats = req.body.seats
@ -45,43 +46,30 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
# Optional
name = req.body.name
# Optional - as long as course(s) are all free
stripeToken = req.body.token
stripeToken = req.body.stripe?.token
@getCourses courseID, (err, courses) =>
query = if courseID? then {_id: courseID} else {}
Course.find query, (err, courses) =>
if err
@logError(req.user, err)
return @sendDatabaseError(res, err)
@logError(user, "Find courses error: #{JSON.stringify(err)}")
return done(err)
price = getCoursesPrice(courses, seats)
if price > 0 and not stripeToken
@logError(req.user, 'Course create API missing required Stripe token')
return @sendBadInputError(res, 'Missing required Stripe token')
PrepaidHandler.purchasePrepaidCourse req.user, courses, seats, new Date().getTime(), stripeToken, (err, prepaid) =>
if err
@logError(req.user, err)
return @sendBadInputError(res, err) if err is 'Missing required Stripe token'
return @sendDatabaseError(res, err)
# TODO: purchase prepaid for courses, price, and seats
Prepaid.generateNewCode (code) =>
return @sendDatabaseError(res, 'Database error.') unless code
prepaid = new Prepaid
creator: req.user.get('_id')
type: 'course'
code: code
properties:
courseIDs: (course.get('_id') for course in courses)
prepaid.set('maxRedeemers', seats) if seats
prepaid.save (err) =>
courseInstances = []
makeCreateInstanceFn = (course, name, prepaid) =>
(done) =>
@createInstance req, course, name, prepaid, (err, newInstance)=>
courseInstances.push newInstance unless err
done(err)
tasks = (makeCreateInstanceFn(course, name, prepaid) for course in courses)
async.parallel tasks, (err, results) =>
return @sendDatabaseError(res, err) if err
courseInstances = []
makeCreateInstanceFn = (course, name, prepaid) =>
(done) =>
@createInstance req, course, name, prepaid, (err, newInstance)=>
courseInstances.push newInstance unless err
done(err)
# tasks = []
# tasks.push(makeCreateInstanceFn(course, name, prepaid)) for course in courses
tasks = (makeCreateInstanceFn(course, name, prepaid) for course in courses)
async.parallel tasks, (err, results) =>
return @sendDatabaseError(res, err) if err
@sendCreated(res, courseInstances)
@sendCreated(res, courseInstances)
createInstance: (req, course, name, prepaid, done) =>
courseInstance = new CourseInstance
@ -93,14 +81,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
courseInstance.save (err, newInstance) =>
done(err, newInstance)
getCourses: (courseID, done) =>
if courseID
Course.findById courseID, (err, document) =>
done(err, [document])
else
Course.find {}, (err, documents) =>
done(err, documents)
getLevelSessionsAPI: (req, res, courseInstanceID) ->
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
return @sendDatabaseError(res, err) if err

View file

@ -7,6 +7,7 @@ module.exports =
log.error "Stripe Utils Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
createCharge: (user, amount, metadata, done) ->
# TODO: create Stripe customer if necessary
options =
amount: amount
currency: 'usd'

View file

@ -1,8 +1,9 @@
Course = require '../courses/Course'
Handler = require '../commons/Handler'
hipchat = require '../hipchat'
Prepaid = require './Prepaid'
StripeUtils = require '../lib/stripe_utils'
{getPrepaidCodeAmount} = require '../../app/core/utils'
hipchat = require '../hipchat'
utils = require '../../app/core/utils'
# TODO: Should this happen on a save() call instead of a prepaid/-/create post?
# TODO: Probably a better way to create a unique 8 charactor string property using db voodoo
@ -14,20 +15,20 @@ PrepaidHandler = class PrepaidHandler extends Handler
baseAmount: 999
logPurchaseError: (user, msg) ->
console.warn "Prepaid Purchase Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
logError: (user, msg) ->
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
hasAccess: (req) ->
req.user?.isAdmin()
getByRelationship: (req, res, args...) ->
relationship = args[1]
return @getPrepaid(req, res, args[2]) if relationship is 'code'
return @createPrepaid(req, res) if relationship is 'create'
return @purchasePrepaid(req, res) if relationship is 'purchase'
return @getPrepaidAPI(req, res, args[2]) if relationship is 'code'
return @createPrepaidAPI(req, res) if relationship is 'create'
return @purchasePrepaidAPI(req, res) if relationship is 'purchase'
super arguments...
getPrepaid: (req, res, code) ->
getPrepaidAPI: (req, res, code) ->
return @sendForbiddenError(res) unless req.user?
return @sendNotFoundError(res, "You must specify a code") unless code
@ -40,85 +41,168 @@ PrepaidHandler = class PrepaidHandler extends Handler
@sendSuccess(res, prepaid.toObject())
createPrepaid: (req, res) ->
createPrepaidAPI: (req, res) ->
return @sendForbiddenError(res) unless @hasAccess(req)
return @sendForbiddenError(res) unless req.body.type in ['subscription','terminal_subscription']
return @sendForbiddenError(res) unless req.body.type in ['course', 'subscription','terminal_subscription']
return @sendForbiddenError(res) unless req.body.maxRedeemers > 0
properties = {}
type = req.body.type
maxRedeemers = req.body.maxRedeemers
if req.body.type is 'course'
return @sendDatabaseError(res, "TODO: need to add courseIDs")
else if req.body.type is 'subscription'
properties.couponID = 'free'
else if req.body.type is 'terminal_subscription'
properties.months = req.body.months
@createPrepaid req.user, req.body.type, req.body.maxRedeemers, properties, (err, prepaid) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, prepaid.toObject())
createPrepaid: (user, type, maxRedeemers, properties, done) ->
Prepaid.generateNewCode (code) =>
return @sendDatabaseError(res, 'Database error.') unless code
# TODO: change the creator to use ObjectID like with the terminal_subscription
return done('Database error.') unless code
options =
creator: req.user.id
type: req.body.type
creator: user._id
type: type
code: code
maxRedeemers: req.body.maxRedeemers
properties: {}
maxRedeemers: maxRedeemers
properties: properties
redeemers: []
if req.body.type is 'subscription'
options.properties.couponID = 'free'
if req.body.type is 'terminal_subscription'
options.properties.months = req.body.months
options.creator = req.user._id
prepaid = new Prepaid options
prepaid.save (err) =>
return done(err) if err
done(err, prepaid)
purchasePrepaidAPI: (req, res) ->
return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()
return @sendForbiddenError(res) unless req.body.type in ['course', 'terminal_subscription']
if req.body.type is 'terminal_subscription'
description = req.body.description
maxRedeemers = parseInt(req.body.maxRedeemers)
months = parseInt(req.body.months)
timestamp = req.body.stripe?.timestamp
token = req.body.stripe?.token
return @sendBadInputError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0
return @sendBadInputError(res) unless isNaN(months) is false and months > 0
return @sendError(res, 403, "Users or Months must be greater than 3") if maxRedeemers < 3 and months < 3
@purchasePrepaidTerminalSubscription req.user, description, maxRedeemers, months, timestamp, token, (err, prepaid) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, prepaid.toObject())
purchasePrepaid: (req, res) ->
return @sendForbiddenError(res) unless req.user?
return @sendForbiddenError(res) unless req.body.type is 'terminal_subscription'
else if req.body.type is 'course'
courseID = req.body.courseID
maxRedeemers = parseInt(req.body.maxRedeemers)
months = parseInt(req.body.months)
maxRedeemers = parseInt(req.body.maxRedeemers)
timestamp = req.body.stripe?.timestamp
token = req.body.stripe?.token
return @sendForbiddenError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0
return @sendForbiddenError(res) unless isNaN(months) is false and months > 0
return @sendError(res, 403, "Users or Months must be greater than 3") if maxRedeemers < 3 and months < 3
return @sendBadInputError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0
StripeUtils.getCustomer req.user, req.body.stripe?.token, (err, customer) =>
query = if courseID? then {_id: courseID} else {}
Course.find query, (err, courses) =>
if err
@logError(user, "Find courses error: #{JSON.stringify(err)}")
return done(err)
@purchasePrepaidCourse req.user, courses, maxRedeemers, timestamp, token, (err, prepaid) =>
# TODO: this badinput detection is fragile, in course instance handler as well
return @sendBadInputError(res, err) if err is 'Missing required Stripe token'
return @sendDatabaseError(res, err) if err
@sendSuccess(res, prepaid.toObject())
else
@sendForbiddenError(res)
purchasePrepaidCourse: (user, courses, maxRedeemers, timestamp, token, done) ->
type = 'course'
courseIDs = (c.get('_id') for c in courses)
coursePrices = (c.get('pricePerSeat') for c in courses)
amount = utils.getCourseBundlePrice(coursePrices, maxRedeemers)
if amount > 0 and not token
@logError(user, "Purchase prepaid courses missing required Stripe token #{amount}")
return done('Missing required Stripe token')
if amount is 0
@createPrepaid(user, type, maxRedeemers, courseIDs: courseIDs, done)
else
StripeUtils.getCustomer user, token, (err, customer) =>
if err
@logError(user, "Stripe getCustomer error: #{JSON.stringify(err)}")
return done(err)
metadata =
type: type
userID: user.id
timestamp: parseInt(timestamp)
description: if courses.length is 1 then courses[0].get('name') else 'All Courses'
maxRedeemers: maxRedeemers
productID: "prepaid #{type}"
courseIDs: courseIDs
StripeUtils.createCharge user, amount, metadata, (err, charge) =>
if err
@logError(user, "createCharge error: #{JSON.stringify(err)}")
return done(err)
StripeUtils.createPayment user, charge, (err, payment) =>
if err
@logError(user, "createPayment error: #{JSON.stringify(err)}")
return done(err)
msg = "Prepaid code purchased: #{type} seats=#{maxRedeemers} courseIDs=#{courseIDs} #{user.get('email')}"
hipchat.sendHipChatMessage msg, ['tower']
@createPrepaid(user, type, maxRedeemers, courseIDs: courseIDs, done)
purchasePrepaidTerminalSubscription: (user, description, maxRedeemers, months, timestamp, token, done) ->
type = 'terminal_subscription'
StripeUtils.getCustomer user, token, (err, customer) =>
if err
@logPurchaseError(req.user, "getCustomer error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
@logError(user, "getCustomer error: #{JSON.stringify(err)}")
return done(err)
metadata =
type: req.body.type
userID: req.user._id + ''
timestamp: parseInt(req.body.stripe?.timestamp)
description: req.body.description
type: type
userID: user.id
timestamp: parseInt(timestamp)
description: description
maxRedeemers: maxRedeemers
months: months
productID: 'prepaid ' + req.body.type
productID: "prepaid #{type}"
amount = getPrepaidCodeAmount(@baseAmount, maxRedeemers, months)
amount = utils.getPrepaidCodeAmount(@baseAmount, maxRedeemers, months)
StripeUtils.createCharge req.user, amount, metadata, (err, charge) =>
StripeUtils.createCharge user, amount, metadata, (err, charge) =>
if err
@logPurchaseError(req.user, "createCharge error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
@logError(user, "createCharge error: #{JSON.stringify(err)}")
return done(err)
StripeUtils.createPayment req.user, charge, (err, payment) =>
StripeUtils.createPayment user, charge, (err, payment) =>
if err
@logPurchaseError(req.user, "createPayment error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
@logError(user, "createPayment error: #{JSON.stringify(err)}")
return done(err)
Prepaid.generateNewCode (code) =>
return @sendDatabaseError(res, 'Database error.') unless code
return done('Database error.') unless code
prepaid = new Prepaid
creator: req.user._id
type: req.body.type
creator: user._id
type: type
code: code
maxRedeemers: req.body.maxRedeemers
maxRedeemers: maxRedeemers
redeemers: []
properties:
months: req.body.months
months: months
prepaid.save (err) =>
return @sendDatabaseError(res, err) if err
msg = "Prepaid code (#{req.body.maxRedeemers} users / #{req.body.months} months) purchased by #{req.user.get('email')}"
return done(err) if err
msg = "Prepaid code purchased: #{type} users=#{maxRedeemers} months=#{months} #{user.get('email')}"
hipchat.sendHipChatMessage msg, ['tower']
@sendSuccess(res, prepaid.toObject())
return done(null, prepaid)
module.exports = new PrepaidHandler()

View file

@ -120,6 +120,23 @@ wrapUpGetUser = (email, user, done) ->
GLOBAL.getURL = (path) ->
return 'http://localhost:3001' + path
nameCount = 0
GLOBAL.createName = (name) ->
name + nameCount++
GLOBAL.createCourse = (pricePerSeat, done) ->
name = createName 'course '
course = new Course
name: name
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d")
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables']
description: "Learn basic syntax, while loops, and the CodeCombat environment."
pricePerSeat: pricePerSeat
screenshot: "/images/pages/courses/101_info.png"
course.save (err, course) =>
return done(err) if err
done(err, course)
GLOBAL.createPrepaid = (type, maxRedeemers, months, done) ->
options = uri: GLOBAL.getURL('/db/prepaid/-/create')
options.json =
@ -133,14 +150,18 @@ GLOBAL.fetchPrepaid = (ppc, done) ->
options = uri: GLOBAL.getURL('/db/prepaid/-/code/'+ppc)
request.get options, done
GLOBAL.purchasePrepaid = (type, maxRedeemers, months, done) ->
GLOBAL.purchasePrepaid = (type, properties, maxRedeemers, token, done) ->
options = uri: GLOBAL.getURL('/db/prepaid/-/purchase')
options.json =
type: type
maxRedeemers: maxRedeemers
months: months
stripe:
timestamp: new Date().getTime()
options.json.stripe.token = token if token?
if type is 'terminal_subscription'
options.json.months = properties.months
else if type is 'course'
options.json.courseID = properties.courseID if properties?.courseID
request.post options, done
GLOBAL.subscribeWithPrepaid = (ppc, done) =>

View file

@ -9,29 +9,14 @@ describe 'CourseInstance', ->
courseInstanceURL = getURL('/db/course_instance/-/create')
userURL = getURL('/db/user')
nameCount = 0
createName = (name) -> name + nameCount++
createCourse = (pricePerSeat, done) ->
name = createName 'course '
course = new Course
name: name
campaignID: ObjectId("55b29efd1cd6abe8ce07db0d")
concepts: ['basic_syntax', 'arguments', 'while_loops', 'strings', 'variables']
description: "Learn basic syntax, while loops, and the CodeCombat environment."
pricePerSeat: pricePerSeat
screenshot: "/images/pages/courses/101_info.png"
course.save (err, course) ->
return done(err) if err
done(err, course)
createCourseInstances = (user, courseID, seats, token, done) ->
name = createName 'course instance '
requestBody =
courseID: courseID
name: name
seats: seats
token: token
stripe:
token: token
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(201)
@ -64,7 +49,7 @@ describe 'CourseInstance', ->
return done(err) if err
done(err, courseInstances)
it 'Clear database users and clans', (done) ->
it 'Clear database', (done) ->
clearModels [User, Course, CourseInstance, Prepaid], (err) ->
throw err if err
done()
@ -124,7 +109,12 @@ describe 'CourseInstance', ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
done()
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
expect(prepaid.get('maxRedeemers')).toEqual(1)
expect(prepaid.get('properties')?.courseIDs).toEqual([course.get('_id')])
done()
it 'Create for paid course 50 seats', (done) ->
stripe.tokens.create {
@ -138,7 +128,12 @@ describe 'CourseInstance', ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
done()
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
expect(prepaid.get('maxRedeemers')).toEqual(50)
expect(prepaid.get('properties')?.courseIDs).toEqual([course.get('_id')])
done()
it 'Create for paid course no token', (done) ->
loginNewUser (user1) ->
@ -192,4 +187,12 @@ describe 'CourseInstance', ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(courses.length)
done()
Prepaid.find creator: user1.get('_id'), (err, prepaids) ->
expect(err).toBeNull()
return done(err) if err
expect(prepaids.length).toEqual(1)
return done('no prepaids found') unless prepaids?.length > 0
prepaid = prepaids[0]
expect(prepaid.get('maxRedeemers')).toEqual(50)
expect(prepaid.get('properties')?.courseIDs?.length).toEqual(courses.length)
done()

View file

@ -13,7 +13,15 @@ describe '/db/prepaid', ->
stripe = require('stripe')(config.stripe.secretKey)
joeCode = null
verifyPrepaid = (user, prepaid, done) ->
verifyCoursePrepaid = (user, prepaid, done) ->
expect(prepaid.creator).toEqual(user.id)
expect(prepaid.type).toEqual('course')
expect(prepaid.maxRedeemers).toBeGreaterThan(0)
expect(prepaid.code).toMatch(/^\w{8}$/)
expect(prepaid.properties?.courseIDs?.length).toBeGreaterThan(0)
done()
verifySubscriptionPrepaid = (user, prepaid, done) ->
expect(prepaid.creator).toEqual(user.id)
expect(prepaid.type).toEqual('subscription')
expect(prepaid.maxRedeemers).toBeGreaterThan(0)
@ -21,8 +29,8 @@ describe '/db/prepaid', ->
expect(prepaid.properties?.couponID).toEqual('free')
done()
it 'Clear database users and prepaids', (done) ->
clearModels [User, Prepaid, Payment], (err) ->
it 'Clear database', (done) ->
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
throw err if err
done()
@ -49,7 +57,7 @@ describe '/db/prepaid', ->
createPrepaid 'subscription', 1, 0, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
verifyPrepaid user1, body, done
verifySubscriptionPrepaid user1, body, done
it 'Admin creates prepaid code with type terminal_subscription', (done) ->
loginNewUser (user1) ->
@ -126,237 +134,333 @@ describe '/db/prepaid', ->
for p in prepaids
if p._id is prepaid._id
found = true
verifyPrepaid user1, p, done
verifySubscriptionPrepaid user1, p, done
break
expect(found).toEqual(true)
done() unless found
# *** Purchase Prepaid Codes *** #
it 'Anonymous submits a prepaid purchase', (done) ->
logoutUser () ->
purchasePrepaid 'terminal_subscription', 3, 3, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(401)
done()
it 'Should error if type isnt terminal_subscription', (done) ->
loginNewUser (user1) ->
purchasePrepaid 'subscription', 3, 3, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'Should error if maxRedeemers is invalid', (done) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', -1, 3, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
purchasePrepaid 'terminal_subscription', 'foo', 3, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'Should error if months is invalid', (done) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', 3, -1, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
purchasePrepaid 'terminal_subscription', 3, 'foo', (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'Should error if maxRedeemers and months are less than 3', (done) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', 1, 1, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'User submits valid prepaid code purchase', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
stripeTokenID = token.id
loginJoe (joe) ->
joeData = joe.toObject()
joeData.stripe = {
token: stripeTokenID
planID: 'basic'
}
request.put {uri: getURL('/db/user'), json: joeData, headers: headers }, (err, res, body) ->
joeData = body
expect(res.statusCode).toBe(200)
expect(joeData.stripe.customerID).toBeDefined()
expect(firstSubscriptionID = joeData.stripe.subscriptionID).toBeDefined()
expect(joeData.stripe.planID).toBe('basic')
expect(joeData.stripe.token).toBeUndefined()
purchasePrepaid 'terminal_subscription', 3, 3, (err, res, prepaid) ->
describe 'Purchase course', ->
it 'Standard user purchases a prepaid for one course, 0 seats', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 700, (err, course) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
expect(prepaid.type).toEqual('terminal_subscription')
expect(prepaid.code).toBeDefined()
# Saving this code for later tests
# TODO: don't make tests dependent on each other
joeCode = prepaid.code
expect(prepaid.creator).toBeDefined()
expect(prepaid.maxRedeemers).toEqual(3)
expect(prepaid.properties).toBeDefined()
expect(prepaid.properties.months).toEqual(3)
done()
it 'Should have logged a Payment with the correct amount', (done) ->
loginJoe (joe) ->
query =
purchaser: joe._id
Payment.find query, (err, payments) ->
expect(err).toBeNull()
expect(payments).not.toBeNull()
expect(payments.length).toEqual(1)
expect(payments[0].get('amount')).toEqual(8991)
done()
it 'Anonymous cant redeem a prepaid code', (done) ->
logoutUser () ->
subscribeWithPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res?.statusCode).toEqual(401)
done()
it 'User cant redeem a nonexistant prepaid code', (done) ->
loginJoe (joe) ->
subscribeWithPrepaid 'abc123', (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'User cant redeem empty code', (done) ->
loginJoe (joe) ->
subscribeWithPrepaid '', (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(422)
done()
it 'Anonymous cant fetch a prepaid code', (done) ->
expect(joeCode).not.toBeNull()
logoutUser () ->
fetchPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'User can fetch a prepaid code', (done) ->
expect(joeCode).not.toBeNull()
loginJoe (joe) ->
fetchPrepaid joeCode, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
expect(body).toBeDefined()
return done() unless body
prepaid = JSON.parse(body)
expect(prepaid.code).toEqual(joeCode)
expect(prepaid.maxRedeemers).toEqual(3)
expect(prepaid.properties?.months).toEqual(3)
done()
# TODO: Move redeem subscription prepaid code tests to subscription tests file
it 'Creator can redeeem a prepaid code', (done) ->
loginJoe (joe) ->
expect(joeCode).not.toBeNull()
expect(joeData.stripe?.customerID).toBeDefined()
expect(joeData.stripe?.subscriptionID).toBeDefined()
return done() unless joeData.stripe?.customerID
# joe has a stripe subscription, so test if the months are added to the end of it.
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) =>
expect(err).toBeNull()
findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) =>
if subscription
stripeSubscriptionPeriodEndDate = new moment(subscription.current_period_end * 1000)
else
expect(stripeSubscriptionPeriodEndDate).toBeDefined()
return done()
subscribeWithPrepaid joeCode, (err, res, result) =>
purchasePrepaid 'course', courseID: course.id, 0, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Standard user purchases a prepaid for one course, 1 seat', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 700, (err, course) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = stripeSubscriptionPeriodEndDate.add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(14000)
findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) =>
expect(subscription).toBeNull()
purchasePrepaid 'course', courseID: course.id, 1, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
verifyCoursePrepaid(user1, prepaid, done)
it 'Standard user purchases a prepaid for one course, 3 seats', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 700, (err, course) ->
expect(err).toBeNull()
purchasePrepaid 'course', courseID: course.id, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
verifyCoursePrepaid(user1, prepaid, done)
it 'Standard user purchases a prepaid for all courses, 10 seats', (done) ->
clearModels [Course, CourseInstance, Payment, Prepaid, User], (err) ->
throw err if err
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 700, (err, course) ->
expect(err).toBeNull()
createCourse 700, (err, course) ->
expect(err).toBeNull()
purchasePrepaid 'course', null, 10, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
expect(prepaid.properties?.courseIDs?.length).toEqual(2)
verifyCoursePrepaid(user1, prepaid, done)
it 'Standard user purchases a prepaid course for 3', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 700, (err, course) ->
expect(err).toBeNull()
purchasePrepaid 'course', courseID: course.id, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
done()
it 'User can redeem a prepaid code', (done) ->
loginSam (sam) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = new moment().add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(10500)
done()
describe 'Purchase terminal_subscription', ->
it 'Anonymous submits a prepaid purchase', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
logoutUser () ->
purchasePrepaid 'terminal_subscription', months: 3, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(401)
done()
it 'Wont allow the same person to redeem twice', (done) ->
loginSam (sam) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'Should error if type isnt terminal_subscription', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'subscription', months: 3, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'Will return redeemed code as part of codes list', (done) ->
loginSam (sam) ->
request.get "#{getURL('/db/user')}/#{sam.id}/prepaid_codes", (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
codes = JSON.parse res.body
expect(codes.length).toEqual(1)
done()
it 'Should error if maxRedeemers is -1', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', months: 3, -1, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Third user can redeem a prepaid code', (done) ->
loginNewUser (user) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = new moment().add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(10500)
done()
it 'Should error if maxRedeemers is foo', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', months: 3, 'foo', token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Fourth user cannot redeem code', (done) ->
loginNewUser (user) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'Should error if months is -1', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', months: -1, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Should error if months is foo', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', months: 'foo', 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
it 'Can fetch a list of purchased and redeemed prepaid codes', (done) ->
loginJoe (joe) ->
purchasePrepaid 'terminal_subscription', 3, 1, (err, res, prepaid) ->
request.get "#{getURL('/db/user')}/#{joe.id}/prepaid_codes", (err, res) ->
it 'Should error if maxRedeemers and months are less than 3', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
purchasePrepaid 'terminal_subscription', months: 1, 1, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'User submits valid prepaid code purchase', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
stripeTokenID = token.id
loginJoe (joe) ->
joeData = joe.toObject()
joeData.stripe = {
token: stripeTokenID
planID: 'basic'
}
request.put {uri: getURL('/db/user'), json: joeData, headers: headers }, (err, res, body) ->
joeData = body
expect(res.statusCode).toBe(200)
expect(joeData.stripe.customerID).toBeDefined()
expect(firstSubscriptionID = joeData.stripe.subscriptionID).toBeDefined()
expect(joeData.stripe.planID).toBe('basic')
expect(joeData.stripe.token).toBeUndefined()
# TODO: is this test still valid after new token?
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
purchasePrepaid 'terminal_subscription', months: 3, 3, token.id, (err, res, prepaid) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
expect(prepaid.type).toEqual('terminal_subscription')
expect(prepaid.code).toBeDefined()
# Saving this code for later tests
# TODO: don't make tests dependent on each other
joeCode = prepaid.code
expect(prepaid.creator).toBeDefined()
expect(prepaid.maxRedeemers).toEqual(3)
expect(prepaid.properties).toBeDefined()
expect(prepaid.properties.months).toEqual(3)
done()
it 'Should have logged a Payment with the correct amount', (done) ->
loginJoe (joe) ->
query =
purchaser: joe._id
Payment.find query, (err, payments) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200);
codes = JSON.parse res.body
expect(codes.length).toEqual(2)
expect(codes[0].maxRedeemers).toEqual(3)
expect(codes[0].properties).toBeDefined()
expect(codes[0].properties.months).toEqual(3)
expect(payments).not.toBeNull()
expect(payments.length).toEqual(1)
expect(payments[0].get('amount')).toEqual(8991)
done()
it 'Test for injection', (done) ->
loginNewUser (user) ->
code = { $exists: true }
subscribeWithPrepaid code, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).not.toEqual(200)
done()
# TODO: add a bunch of parallel tests trying to redeem a code with a high maxRedeemers (50?) to see what happens
it 'Anonymous cant redeem a prepaid code', (done) ->
logoutUser () ->
subscribeWithPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res?.statusCode).toEqual(401)
done()
it 'User cant redeem a nonexistant prepaid code', (done) ->
loginJoe (joe) ->
subscribeWithPrepaid 'abc123', (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'User cant redeem empty code', (done) ->
loginJoe (joe) ->
subscribeWithPrepaid '', (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(422)
done()
it 'Anonymous cant fetch a prepaid code', (done) ->
expect(joeCode).not.toBeNull()
logoutUser () ->
fetchPrepaid joeCode, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'User can fetch a prepaid code', (done) ->
expect(joeCode).not.toBeNull()
loginJoe (joe) ->
fetchPrepaid joeCode, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
expect(body).toBeDefined()
return done() unless body
prepaid = JSON.parse(body)
expect(prepaid.code).toEqual(joeCode)
expect(prepaid.maxRedeemers).toEqual(3)
expect(prepaid.properties?.months).toEqual(3)
done()
# TODO: Move redeem subscription prepaid code tests to subscription tests file
describe 'Subscription redeem tests', ->
it 'Creator can redeeem a prepaid code', (done) ->
loginJoe (joe) ->
expect(joeCode).not.toBeNull()
expect(joeData.stripe?.customerID).toBeDefined()
expect(joeData.stripe?.subscriptionID).toBeDefined()
return done() unless joeData.stripe?.customerID
# joe has a stripe subscription, so test if the months are added to the end of it.
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) =>
expect(err).toBeNull()
findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) =>
if subscription
stripeSubscriptionPeriodEndDate = new moment(subscription.current_period_end * 1000)
else
expect(stripeSubscriptionPeriodEndDate).toBeDefined()
return done()
subscribeWithPrepaid joeCode, (err, res, result) =>
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = stripeSubscriptionPeriodEndDate.add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(14000)
findStripeSubscription customer.id, subscriptionID: joeData.stripe?.subscriptionID, (subscription) =>
expect(subscription).toBeNull()
done()
it 'User can redeem a prepaid code', (done) ->
loginSam (sam) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = new moment().add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(10500)
done()
it 'Wont allow the same person to redeem twice', (done) ->
loginSam (sam) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'Will return redeemed code as part of codes list', (done) ->
loginSam (sam) ->
request.get "#{getURL('/db/user')}/#{sam.id}/prepaid_codes", (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
codes = JSON.parse res.body
expect(codes.length).toEqual(1)
done()
it 'Third user can redeem a prepaid code', (done) ->
loginNewUser (user) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200)
endDate = new moment().add(3, 'months').toISOString().substring(0, 10)
expect(result?.stripe?.free).toEqual(endDate)
expect(result?.purchased?.gems).toEqual(10500)
done()
it 'Fourth user cannot redeem code', (done) ->
loginNewUser (user) ->
subscribeWithPrepaid joeCode, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(403)
done()
it 'Can fetch a list of purchased and redeemed prepaid codes', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginJoe (joe) ->
purchasePrepaid 'terminal_subscription', months: 1, 3, token.id, (err, res, prepaid) ->
request.get "#{getURL('/db/user')}/#{joe.id}/prepaid_codes", (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toEqual(200);
codes = JSON.parse res.body
expect(codes.length).toEqual(2)
expect(codes[0].maxRedeemers).toEqual(3)
expect(codes[0].properties).toBeDefined()
expect(codes[0].properties.months).toEqual(3)
done()
it 'Test for injection', (done) ->
loginNewUser (user) ->
code = { $exists: true }
subscribeWithPrepaid code, (err, res, result) ->
expect(err).toBeNull()
expect(res.statusCode).not.toEqual(200)
done()
# TODO: add a bunch of parallel tests trying to redeem a code with a high maxRedeemers (50?) to see what happens