Redeem course prepaid code on /courses page

Completes #54270566052118
This commit is contained in:
Matt Lott 2015-10-06 11:20:53 -07:00
parent ff69bb8c89
commit 9c7345fed0
6 changed files with 180 additions and 14 deletions

View file

@ -68,9 +68,9 @@ mixin student-dialog(course)
.container-fluid
.row
.col-md-8
input.code-input(type='text', data-i18n="[placeholder]courses.enter_code1", placeholder="Enter unlock code")
input.code-input(type='text', data-course-id="#{course.id}", data-i18n="[placeholder]courses.enter_code1", placeholder="Enter unlock code")
.col-md-4
button.btn.btn-success.btn-enroll(data-i18n="courses.enroll")
button.btn.btn-success.btn-enroll(data-course-id="#{course.id}", data-i18n="courses.enroll")
mixin teacher-dialog(course)
.modal.continue-dialog(id="continueModal#{course.id}")
@ -104,7 +104,10 @@ mixin teacher-dialog(course)
div.or(data-i18n="courses.or")
.row.button-row.center
.col-md-12
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}", data-i18n="courses.buy_course1")
if course.get('pricePerSeat') === 0
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}") Start new class
else
button.btn.btn-success.btn-lg.btn-buy(data-course-id="#{course.id}", data-i18n="courses.buy_course1")
mixin course-block(course)
if studentMode

View file

@ -283,17 +283,17 @@ module.exports = class CourseDetailsView extends RootView
when "progressAsc"
@sortedMembers.sort (a, b) =>
for levelID, level of @campaign.get('levels')
if @userLevelStateMap[a][levelID] isnt 'complete' and @userLevelStateMap[b][levelID] is 'complete'
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
return -1
else if @userLevelStateMap[a][levelID] is 'complete' and @userLevelStateMap[b][levelID] isnt 'complete'
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
return 1
0
when "progressDesc"
@sortedMembers.sort (a, b) =>
for levelID, level of @campaign.get('levels')
if @userLevelStateMap[a][levelID] isnt 'complete' and @userLevelStateMap[b][levelID] is 'complete'
if @userLevelStateMap[a]?[levelID] isnt 'complete' and @userLevelStateMap[b]?[levelID] is 'complete'
return 1
else if @userLevelStateMap[a][levelID] is 'complete' and @userLevelStateMap[b][levelID] isnt 'complete'
else if @userLevelStateMap[a]?[levelID] is 'complete' and @userLevelStateMap[b]?[levelID] isnt 'complete'
return -1
0
else

View file

@ -70,7 +70,27 @@ module.exports = class CoursesView extends RootView
Backbone.Mediator.publish 'router:navigate', navigationEvent
onClickEnroll: (e) ->
alert('TODO: redeem course prepaid and navigate to correct course instance')
$('.continue-dialog').modal('hide')
courseID = $(e.target).data('course-id')
prepaidCode = $(".code-input[data-course-id=#{courseID}]").val()
data = prepaidCode: prepaidCode
jqxhr = $.post('/db/course_instance/-/redeem_prepaid', data)
jqxhr.done (data, textStatus, jqXHR) =>
application.tracker?.trackEvent 'Redeemed course prepaid code', {courseID: courseID, prepaidCode: prepaidCode}
# TODO: handle fetch errors
me.fetch(cache: false).always =>
route = "/courses/#{courseID}"
viewArgs = [{}, courseID]
Backbone.Mediator.publish 'router:navigate',
route: route
viewClass: 'views/courses/CourseDetailsView'
viewArgs: viewArgs
jqxhr.fail (xhr, textStatus, errorThrown) =>
console.error 'Got an error redeeming a course prepaid code:', textStatus, errorThrown
application.tracker?.trackEvent 'Failed to redeem course prepaid code', status: textStatus
@state = 'unknown_error'
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
@render?()
onClickEnter: (e) ->
$('.continue-dialog').modal('hide')

View file

@ -33,6 +33,7 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @getLevelSessionsAPI(req, res, args[0]) if args[1] is 'level_sessions'
return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
super arguments...
createAPI: (req, res) ->
@ -126,5 +127,39 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})
redeemPrepaidCodeAPI: (req, res) ->
return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()
return @sendBadInputError(res) unless req.body?.prepaidCode
prepaidCode = req.body?.prepaidCode
Prepaid.find code: prepaidCode, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if prepaids.length < 1
return @sendDatabaseError(res, "Multiple prepaid codes found for #{prepaidCode}") if prepaids.length > 1
prepaid = prepaids[0]
CourseInstance.find prepaidID: prepaid.get('_id'), (err, courseInstances) =>
return @sendDatabaseError(res, err) if err
return @sendForbiddenError(res) if prepaid.get('redeemers')?.length >= prepaid.get('maxRedeemers')
# Add to prepaid redeemers
query =
_id: prepaid.get('_id')
'redeemers.userID': { $ne: req.user.get('_id') }
$where: "this.redeemers.length < #{prepaid.get('maxRedeemers')}"
update = { $push: { redeemers : { date: new Date(), userID: req.user.get('_id') } }}
Prepaid.update query, update, (err, nMatched) =>
return @sendDatabaseError(res, err) if err
if nMatched is 0
@logError(req.user, "Course instance update prepaid lost race on maxRedeemers")
return @sendForbiddenError(res)
# Add to each course instance
makeAddMemberToCourseInstanceFn = (courseInstance) =>
(done) => courseInstance.update({$addToSet: { members: req.user.get('_id')}}, done)
tasks = (makeAddMemberToCourseInstanceFn(courseInstance) for courseInstance in courseInstances)
async.parallel tasks, (err, results) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res)
module.exports = new CourseInstanceHandler()

View file

@ -19,7 +19,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
hasAccess: (req) ->
req.user?.isAdmin()
req.method is 'GET' || req.user?.isAdmin()
getByRelationship: (req, res, args...) ->
relationship = args[1]

View file

@ -6,7 +6,8 @@ stripe = require('stripe')(config.stripe.secretKey)
# TODO: add permissiosn tests
describe 'CourseInstance', ->
courseInstanceURL = getURL('/db/course_instance/-/create')
courseInstanceCreateURL = getURL('/db/course_instance/-/create')
courseInstanceRedeemURL = getURL('/db/course_instance/-/redeem_prepaid')
userURL = getURL('/db/user')
createCourseInstances = (user, courseID, seats, token, done) ->
@ -17,7 +18,7 @@ describe 'CourseInstance', ->
seats: seats
stripe:
token: token
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(201)
CourseInstance.find {name: name}, (err, courseInstances) ->
@ -81,7 +82,7 @@ describe 'CourseInstance', ->
requestBody =
courseID: course.get('_id')
name: createName('course instance ')
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
@ -145,7 +146,7 @@ describe 'CourseInstance', ->
courseID: course.get('_id')
name: createName('course instance ')
seats: 1
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
@ -163,7 +164,7 @@ describe 'CourseInstance', ->
courseID: course.get('_id')
name: createName('course instance ')
seats: -1
request.post {uri: courseInstanceURL, json: requestBody }, (err, res) ->
request.post {uri: courseInstanceCreateURL, json: requestBody }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(422)
done()
@ -218,3 +219,110 @@ describe 'CourseInstance', ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
done()
describe 'Redeem prepaid code', ->
it 'Redeem prepaid code an instance of max 2', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 0, (err, course) ->
expect(err).toBeNull()
return done(err) if err
createCourseInstances user1, course.get('_id'), 2, token.id, (err, courseInstances) ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
loginNewUser (user2) ->
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
# Check prepaid
Prepaid.findById prepaid.id, (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
expect(prepaid.get('redeemers')?.length).toEqual(1)
expect(prepaid.get('redeemers')[0].date).toBeLessThan(new Date())
expect(prepaid.get('redeemers')[0].userID).toEqual(user2.get('_id'))
# Check course instance
CourseInstance.findById courseInstances[0].id, (err, courseInstance) ->
expect(err).toBeNull()
return done(err) if err
members = courseInstance.get('members')
expect(members?.length).toEqual(2)
# TODO: must be a better way to check membership
usersFound = 0
for memberID in members
usersFound++ if memberID.equals(user1.get('_id'))
usersFound++ if memberID.equals(user2.get('_id'))
expect(usersFound).toEqual(2)
done()
it 'Redeem full prepaid code on instance of max 1', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 0, (err, course) ->
expect(err).toBeNull()
return done(err) if err
createCourseInstances user1, course.get('_id'), 1, token.id, (err, courseInstances) ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
loginNewUser (user2) ->
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
loginNewUser (user3) ->
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(403)
done()
it 'Redeem 50 count course prepaid codes 51 times, in parallel', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
seatCount = 50
loginNewUser (user1) ->
createCourse 0, (err, course) ->
expect(err).toBeNull()
return done(err) if err
createCourseInstances user1, course.get('_id'), seatCount, token.id, (err, courseInstances) ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
forbiddenResults = 0
makeRedeemCall = ->
(callback) ->
loginNewUser (user2) ->
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
if res.statusCode is 403
forbiddenResults++
else
expect(res.statusCode).toBe(200)
callback err
tasks = (makeRedeemCall() for i in [1..seatCount+1])
async.parallel tasks, (err, results) ->
expect(err?).toEqual(false)
expect(forbiddenResults).toEqual(1)
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
expect(prepaid.get('redeemers')?.length).toEqual(prepaid.get('maxRedeemers'))
done()