mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-07 18:44:29 -04:00
Merge branch 'master' into production
This commit is contained in:
commit
0689e487dd
22 changed files with 519 additions and 84 deletions
app
locale
models
schemas
styles
templates
views
core
courses
play/level
server
test/server/functional
|
@ -875,6 +875,10 @@
|
|||
play_time: "Play time:"
|
||||
completed: "Completed:"
|
||||
invite_students: "Invite students to join this class."
|
||||
invite_link_header: "Link to join course"
|
||||
invite_link_p_1: "Give this link to students you would like to have join the course."
|
||||
invite_link_p_2: "Or have us email them directly:"
|
||||
capacity_used: "Course slots used:"
|
||||
enter_emails: "Enter student emails to invite, one per line"
|
||||
send_invites: "Send Invites"
|
||||
title: "Title"
|
||||
|
|
|
@ -51,7 +51,10 @@ class CocoModel extends Backbone.Model
|
|||
@loading = false
|
||||
@jqxhr = null
|
||||
if jqxhr.status is 402
|
||||
Backbone.Mediator.publish 'level:subscription-required', {}
|
||||
if _.contains(jqxhr.responseText, 'be in a course')
|
||||
Backbone.Mediator.publish 'level:course-membership-required', {}
|
||||
else
|
||||
Backbone.Mediator.publish 'level:subscription-required', {}
|
||||
|
||||
onLoaded: ->
|
||||
@loaded = true
|
||||
|
|
|
@ -244,7 +244,7 @@ GeneralArticleSchema = c.object {
|
|||
links: [{rel: 'db', href: '/db/article/{(original)}/version/{(majorVersion)}'}]
|
||||
},
|
||||
original: c.objectId(title: 'Original', description: 'A reference to the original Article.')#, format: 'hidden') # hidden?
|
||||
majorVersion: {title: 'Major Version', description: 'Which major version of the Article is being used.', type: 'integer', minimum: 0}#, format: 'hidden'} # hidden?
|
||||
majorVersion: {title: 'Major Version', description: 'Which major version of the Article is being used.', type: 'integer', minimum: 0} #, format: 'hidden'} # hidden?
|
||||
|
||||
LevelSchema = c.object {
|
||||
title: 'Level'
|
||||
|
|
|
@ -171,3 +171,5 @@ module.exports =
|
|||
hero: {type: 'object'}
|
||||
|
||||
'level:subscription-required': c.object {}
|
||||
|
||||
'level:course-membership-required': c.object {}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#course-details-view
|
||||
|
||||
.invite-emails
|
||||
#invite-emails-textarea
|
||||
width: 50%
|
||||
|
||||
.progress-cell
|
||||
|
|
|
@ -111,13 +111,13 @@
|
|||
font-variant: small-caps
|
||||
text-transform: none
|
||||
|
||||
.subscription-required
|
||||
.subscription-required, .course-membership-required
|
||||
display: none
|
||||
margin-top: -160px
|
||||
color: black
|
||||
font-size: 24px
|
||||
|
||||
.start-subscription-button
|
||||
.btn
|
||||
width: 100%
|
||||
margin: 0px auto
|
||||
font-size: 40px
|
||||
|
|
|
@ -247,11 +247,25 @@ mixin progress-members-popup-started(i, level)
|
|||
|
||||
mixin invite-tab
|
||||
p(data-i18n="courses.invite_students")
|
||||
p TODO: Student unlock code
|
||||
p TODO: Class capacity
|
||||
textarea.invite-emails(rows=3, data-i18n="[placeholder]courses.enter_emails", placeholder="Enter student emails to invite, one per line")
|
||||
div(style='margin-top:10px;')
|
||||
button.btn.btn-success.btn-invite(data-i18n="courses.send_invites")
|
||||
h3(data-i18n="courses.invite_link_header")
|
||||
p(data-i18n="courses.invite_link_p_1")
|
||||
.alert.alert-info
|
||||
strong= document.location.origin + "/courses?_ppc=" + view.prepaid.get('code')
|
||||
p(data-i18n="courses.invite_link_p_2")
|
||||
.form
|
||||
.form-group
|
||||
textarea#invite-emails-textarea.form-control(
|
||||
rows=3, data-i18n="[placeholder]courses.enter_emails", placeholder="Enter student emails to invite, one per line")
|
||||
.form-group
|
||||
button#invite-btn.btn.btn-success(data-i18n="courses.send_invites")
|
||||
#invite-emails-sending-alert.alert.alert-info.hide(data-i18n="common.sending")
|
||||
#invite-emails-success-alert.alert.alert-success.hide(data-i18n="play_level.done")
|
||||
|
||||
h3 Class Capacity
|
||||
if view.prepaid.loaded
|
||||
p
|
||||
span.spr(data-i18n="courses.capacity_used")
|
||||
span #{view.prepaid.get('redeemers').length} / #{view.prepaid.get('maxRedeemers')}.
|
||||
|
||||
mixin levels-tab
|
||||
table.table.table-striped.table-condensed
|
||||
|
|
|
@ -7,19 +7,25 @@ block content
|
|||
a.spl(href='mailto:team@codecombat.com') team@codecombat.com
|
||||
|
||||
br
|
||||
if studentMode
|
||||
+student-main
|
||||
if state === 'enrolling'
|
||||
.alert.alert-info Enrolling in course..
|
||||
else
|
||||
+teacher-main
|
||||
.container-fluid
|
||||
- var i = 0
|
||||
while i < courses.length
|
||||
.row
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
if i < courses.length
|
||||
if state === 'unknown_error'
|
||||
.alert.alert-danger.alert-dismissible= stateMessage
|
||||
|
||||
if studentMode
|
||||
+student-main
|
||||
else
|
||||
+teacher-main
|
||||
.container-fluid
|
||||
- var i = 0
|
||||
while i < courses.length
|
||||
.row
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
if i < courses.length
|
||||
+course-block(courses[i], instances)
|
||||
- i++
|
||||
|
||||
mixin student-main
|
||||
button.btn.btn-warning.btn-teacher(data-i18n="courses.teachers_click")
|
||||
|
@ -68,9 +74,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 +110,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
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
span(data-i18n="subscribe.subscription_required_to_play") You'll need a subscription to play this level.
|
||||
button.start-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.subscribe") Subscribe
|
||||
|
||||
.course-membership-required
|
||||
span(data-i18n="courses.course_membership_required_to_play") You'll need to join a course to play this level.
|
||||
a.btn.btn-lg.btn-warning(data-i18n="courses.go_to_courses", href="/courses") Go To Courses
|
||||
|
||||
#tip-wrapper
|
||||
strong.tip(data-i18n='play_level.tip_toggle_play') Toggle play/paused with Ctrl+P.
|
||||
strong.tip(data-i18n='play_level.tip_scrub_shortcut') Ctrl+[ and Ctrl+] rewind and fast-forward.
|
||||
|
|
|
@ -129,6 +129,7 @@ module.exports = class CocoView extends Backbone.View
|
|||
context.isIE = @isIE()
|
||||
context.moment = moment
|
||||
context.translate = $.i18n.t
|
||||
context.view = @
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
|
|
@ -7,6 +7,7 @@ RootView = require 'views/core/RootView'
|
|||
template = require 'templates/courses/course-details'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
Prepaid = require 'models/Prepaid'
|
||||
|
||||
module.exports = class CourseDetailsView extends RootView
|
||||
id: 'course-details-view'
|
||||
|
@ -22,6 +23,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
'click .progress-level-cell': 'onClickProgressLevelCell'
|
||||
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
|
||||
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
|
||||
'click #invite-btn': 'onClickInviteButton'
|
||||
|
||||
constructor: (options, @courseID, @courseInstanceID) ->
|
||||
super options
|
||||
|
@ -31,6 +33,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@memberSort = 'nameAsc'
|
||||
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
|
||||
@listenTo @course, 'sync', @onCourseSync
|
||||
@prepaid = new Prepaid()
|
||||
if @course.loaded
|
||||
@onCourseSync()
|
||||
else
|
||||
|
@ -55,6 +58,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
context.sortedMembers = @sortedMembers ? []
|
||||
context.userConceptStateMap = @userConceptStateMap ? {}
|
||||
context.userLevelStateMap = @userLevelStateMap ? {}
|
||||
context.document = document
|
||||
context
|
||||
|
||||
onCourseSync: ->
|
||||
|
@ -119,6 +123,16 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
|
||||
@listenToOnce @members, 'sync', @onMembersSync
|
||||
@supermodel.loadCollection @members, 'members', cache: false
|
||||
if @adminMode and prepaidID = @courseInstance.get('prepaidID')
|
||||
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
|
||||
@listenTo @prepaid, 'sync', @onPrepaidSync
|
||||
if @prepaid.loaded
|
||||
@onPrepaidSync()
|
||||
else
|
||||
@supermodel.loadModel @prepaid, 'prepaid'
|
||||
@render?()
|
||||
|
||||
onPrepaidSync: ->
|
||||
@render?()
|
||||
|
||||
onLevelSessionsSync: ->
|
||||
|
@ -226,6 +240,26 @@ module.exports = class CourseDetailsView extends RootView
|
|||
viewArgs: [{}, levelSlug]
|
||||
}
|
||||
|
||||
onClickInviteButton: (e) ->
|
||||
emails = @$('#invite-emails-textarea').val()
|
||||
emails = emails.split('\n')
|
||||
emails = _.filter((_.string.trim(email) for email in emails))
|
||||
if not emails.length
|
||||
return
|
||||
url = @courseInstance.url() + '/invite_students'
|
||||
@$('#invite-btn, #invite-emails-textarea').addClass('hide')
|
||||
@$('#invite-emails-sending-alert').removeClass('hide')
|
||||
|
||||
$.ajax({
|
||||
url: url
|
||||
data: {emails: emails}
|
||||
method: 'POST'
|
||||
context: @
|
||||
success: ->
|
||||
@$('#invite-emails-sending-alert').addClass('hide')
|
||||
@$('#invite-emails-success-alert').removeClass('hide')
|
||||
})
|
||||
|
||||
onMouseEnterPoint: (e) ->
|
||||
$('.progress-popup-container').hide()
|
||||
container = $(e.target).find('.progress-popup-container').show()
|
||||
|
@ -249,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
|
||||
|
|
|
@ -26,6 +26,7 @@ module.exports = class CoursesView extends RootView
|
|||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
|
||||
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
||||
@courseEnroll(prepaidCode) if prepaidCode = utils.getQueryVariable('_ppc', false)
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
|
@ -33,6 +34,8 @@ module.exports = class CoursesView extends RootView
|
|||
context.enrolledCourses = @enrolledCourses ? {}
|
||||
context.instances = @courseInstances.models ? []
|
||||
context.praise = @praise
|
||||
context.state = @state
|
||||
context.stateMessage = @stateMessage
|
||||
context.studentMode = @studentMode
|
||||
context
|
||||
|
||||
|
@ -70,7 +73,10 @@ 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()
|
||||
@courseEnroll(prepaidCode)
|
||||
|
||||
onClickEnter: (e) ->
|
||||
$('.continue-dialog').modal('hide')
|
||||
|
@ -95,3 +101,32 @@ module.exports = class CoursesView extends RootView
|
|||
viewArgs = [studentMode: false]
|
||||
navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs
|
||||
Backbone.Mediator.publish 'router:navigate', navigationEvent
|
||||
|
||||
courseEnroll: (prepaidCode) ->
|
||||
@state = 'enrolling'
|
||||
@render?()
|
||||
data = prepaidCode: prepaidCode
|
||||
jqxhr = $.post('/db/course_instance/-/redeem_prepaid', data)
|
||||
jqxhr.done (data, textStatus, jqXHR) =>
|
||||
application.tracker?.trackEvent 'Redeemed course prepaid code', {prepaidCode: prepaidCode}
|
||||
# TODO: handle fetch errors
|
||||
me.fetch(cache: false).always =>
|
||||
if data?.length > 0 && data[0].courseID && data[0]._id
|
||||
courseID = data[0].courseID
|
||||
courseInstanceID = data[0]._id
|
||||
route = "/courses/#{courseID}/#{courseInstanceID}"
|
||||
viewArgs = [{}, courseID, courseInstanceID]
|
||||
Backbone.Mediator.publish 'router:navigate',
|
||||
route: route
|
||||
viewClass: 'views/courses/CourseDetailsView'
|
||||
viewArgs: viewArgs
|
||||
else
|
||||
@state = 'unknown_error'
|
||||
@stateMessage = "Database error."
|
||||
@render?()
|
||||
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?()
|
||||
|
|
|
@ -15,6 +15,7 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
subscriptions:
|
||||
'level:loaded': 'onLevelLoaded' # If Level loads after level loading view.
|
||||
'level:subscription-required': 'onSubscriptionRequired' # If they'd need a subscription to start playing.
|
||||
'level:course-membership-required': 'onCourseMembershipRequired' # If they'd need a subscription to start playing.
|
||||
'subscribe-modal:subscribed': 'onSubscribed'
|
||||
|
||||
shortcuts:
|
||||
|
@ -109,6 +110,10 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
@$el.find('.level-loading-goals, .tip, .load-progress').hide()
|
||||
@$el.find('.subscription-required').show()
|
||||
|
||||
onCourseMembershipRequired: (e) ->
|
||||
@$el.find('.level-loading-goals, .tip, .load-progress').hide()
|
||||
@$el.find('.course-membership-required').show()
|
||||
|
||||
onClickStartSubscription: (e) ->
|
||||
@openModalView new SubscribeModal()
|
||||
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'level loading', level: @level?.get('slug') or @options.level?.get('slug')
|
||||
|
|
|
@ -397,7 +397,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
AudioPlayer.playSound name, 1
|
||||
|
||||
getNextLevelCampaign: ->
|
||||
{'kithgard-gates': 'forest', 'kithgard-mastery': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
|
||||
{'kithgard-gates': 'forest', 'kithgard-mastery': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain', 'summits-gate': 'glacier'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
|
||||
|
||||
getNextLevelLink: (returnToCourse=false) ->
|
||||
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel') and not returnToCourse
|
||||
|
|
|
@ -9,6 +9,7 @@ PrepaidHandler = require '../prepaids/prepaid_handler'
|
|||
User = require '../users/User'
|
||||
UserHandler = require '../users/user_handler'
|
||||
utils = require '../../app/core/utils'
|
||||
sendwithus = require '../sendwithus'
|
||||
|
||||
CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
||||
modelClass: CourseInstance
|
||||
|
@ -31,6 +32,8 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
return @createAPI(req, res) if relationship is 'create'
|
||||
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) ->
|
||||
|
@ -101,4 +104,62 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
|
|||
cleandocs = (UserHandler.formatEntity(req, doc) for doc in users)
|
||||
@sendSuccess(res, cleandocs)
|
||||
|
||||
inviteStudents: (req, res, courseInstanceID) ->
|
||||
if not req.body.emails
|
||||
return @sendBadInputError(res, 'Emails not included')
|
||||
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless courseInstance
|
||||
return @sendForbiddenError(res) unless @hasAccessToDocument(req, courseInstance)
|
||||
|
||||
Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless prepaid
|
||||
return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length
|
||||
for email in req.body.emails
|
||||
context =
|
||||
email_id: sendwithus.templates.course_invite_email
|
||||
recipient:
|
||||
address: email
|
||||
email_data:
|
||||
class_name: courseInstance.get('name')
|
||||
join_link: "https://codecombat.com/courses?_ppc=" + prepaid.get('code')
|
||||
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, courseInstances)
|
||||
|
||||
module.exports = new CourseInstanceHandler()
|
||||
|
|
|
@ -8,6 +8,9 @@ mongoose = require 'mongoose'
|
|||
async = require 'async'
|
||||
utils = require '../lib/utils'
|
||||
log = require 'winston'
|
||||
Campaign = require '../campaigns/Campaign'
|
||||
Course = require '../courses/Course'
|
||||
CourseInstance = require '../courses/CourseInstance'
|
||||
|
||||
LevelHandler = class LevelHandler extends Handler
|
||||
modelClass: Level
|
||||
|
@ -105,11 +108,26 @@ LevelHandler = class LevelHandler extends Handler
|
|||
Session.findOne(sessionQuery).exec (err, doc) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendSuccess(res, doc) if doc?
|
||||
if level.get('type') is 'course'
|
||||
return @makeOrRejectCourseLevelSession(req, res, level, sessionQuery)
|
||||
requiresSubscription = level.get('requiresSubscription') or (req.user.get('chinaVersion') and level.get('campaign') and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names']))
|
||||
canPlayAnyway = req.user.isPremium() or level.get 'adventurer'
|
||||
return @sendPaymentRequiredError(res, err) if requiresSubscription and not canPlayAnyway
|
||||
@createAndSaveNewSession sessionQuery, req, res
|
||||
|
||||
makeOrRejectCourseLevelSession: (req, res, level, sessionQuery) ->
|
||||
CourseInstance.find {members: req.user.get('_id')}, (err, courseInstances) =>
|
||||
courseIDs = (ci.get('courseID') for ci in courseInstances)
|
||||
Course.find { _id: { $in: courseIDs }}, (err, courses) =>
|
||||
campaignIDs = (c.get('campaignID') for c in courses)
|
||||
Campaign.find { _id: { $in: campaignIDs }}, (err, campaigns) =>
|
||||
levelOriginals = (_.keys(c.get('levels')) for c in campaigns)
|
||||
levelOriginals = _.flatten(levelOriginals)
|
||||
if level.get('original').toString() in levelOriginals
|
||||
@createAndSaveNewSession(sessionQuery, req, res)
|
||||
else
|
||||
return @sendPaymentRequiredError(res, 'You must be in a course which includes this level to play it')
|
||||
|
||||
createAndSaveNewSession: (sessionQuery, req, res) =>
|
||||
initVals = sessionQuery
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ class SubscriptionHandler extends Handler
|
|||
@sendSuccess(res, user)
|
||||
|
||||
subscribeWithPrepaidCode: (req, res) ->
|
||||
return @sendForbiddenError(res) unless req.user?
|
||||
return @sendUnauthorizedError(res) unless req.user?
|
||||
return @sendBadInputError(res,"You must provide a valid prepaid code") unless req.body?.ppc
|
||||
|
||||
# Check if code exists and has room for more redeemers
|
||||
|
@ -206,14 +206,30 @@ class SubscriptionHandler extends Handler
|
|||
return @sendDatabaseError(res, err)
|
||||
unless prepaid
|
||||
@logSubscriptionError(req.user, "Could not find prepaid code #{req.body.ppc}")
|
||||
return @sendForbiddenError(res)
|
||||
return @sendNotFoundError(res, "Prepaid not found")
|
||||
|
||||
oldRedeemers = prepaid.get('redeemers') ? []
|
||||
return @sendForbiddenError(res) if oldRedeemers.length >= prepaid.get('maxRedeemers')
|
||||
return @sendError(res, 403, "Too many redeemers") if oldRedeemers.length >= prepaid.get('maxRedeemers')
|
||||
months = parseInt(prepaid.get('properties')?.months)
|
||||
return @sendForbiddenError(res) if isNaN(months) or months < 1
|
||||
return @sendBadInputError(res, "Bad months") if isNaN(months) or months < 1
|
||||
for redeemer in oldRedeemers
|
||||
return @sendForbiddenError(res) if redeemer.userID.equals(req.user._id)
|
||||
return @sendError(res, 403, "User already redeemed") if redeemer.userID.equals(req.user._id)
|
||||
|
||||
@redeemPrepaidCode(req, res, months)
|
||||
|
||||
redeemPrepaidCode: (req, res, months) =>
|
||||
return @sendUnauthorizedError(res) unless req.user?
|
||||
return @sendForbiddenError(res) unless req.body?.ppc
|
||||
return @sendForbiddenError(res) if isNaN(months) or months < 1
|
||||
|
||||
newRedeemerPush = { $push: { redeemers : { date: new Date(), userID: req.user._id } }}
|
||||
|
||||
Prepaid.update { 'code': req.body.ppc, 'redeemers.userID': { $ne: req.user._id }, '$where': 'this.redeemers.length < this.maxRedeemers'}, newRedeemerPush, (err, num, info) =>
|
||||
if err
|
||||
@logSubscriptionError(req.user, "Subscribe with Prepaid Code update: #{JSON.stringify(err)}")
|
||||
return @sendDatabaseError(res, err)
|
||||
|
||||
return @sendError(res, 403, "Can't add user to prepaid redeemers") if num isnt 1
|
||||
|
||||
customerID = req.user.get('stripe')?.customerID
|
||||
subscriptionID = req.user.get('stripe')?.subscriptionID
|
||||
|
@ -224,51 +240,32 @@ class SubscriptionHandler extends Handler
|
|||
if err
|
||||
@logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}")
|
||||
return @sendDatabaseError(res, err)
|
||||
@redeemPrepaidCode(req, res, oldRedeemers, months, stripeSubscriptionPeriodEndDate)
|
||||
|
||||
redeemPrepaidCode: (req, res, oldRedeemers, months, startDate=null) =>
|
||||
return @sendForbiddenError(res) unless req.user?
|
||||
return @sendForbiddenError(res) unless req.body?.ppc
|
||||
return @sendForbiddenError(res) unless oldRedeemers
|
||||
return @sendForbiddenError(res) if isNaN(months) or months < 1
|
||||
# Add terminal subscription to User, extending existing subscriptions
|
||||
# TODO: refactor this into some form useable by both this and purchaseYearSale
|
||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||
endDate = new moment()
|
||||
if stripeSubscriptionPeriodEndDate
|
||||
endDate = new moment(stripeSubscriptionPeriodEndDate)
|
||||
else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free))
|
||||
endDate = new moment(stripeInfo.free)
|
||||
|
||||
newRedeemerPush = { $push: { redeemers : { date: new Date().toISOString(), userID: req.user._id } }}
|
||||
endDate = endDate.add(months, 'months')
|
||||
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
||||
req.user.set('stripe', stripeInfo)
|
||||
|
||||
# Only update the prepaid document if the length of the redeemers array hasn't changed in the db.
|
||||
# This will probably fail if redeemers isn't defined. new terminal_subscriptions created should be sure to set the redeemers array
|
||||
# TODO: find a better way?
|
||||
Prepaid.update { 'code': req.body.ppc, 'redeemers': { $size: oldRedeemers.length }}, newRedeemerPush, (err, num, info) =>
|
||||
if err
|
||||
@logSubscriptionError(req.user, "Subscribe with Prepaid Code update: #{JSON.stringify(err)}")
|
||||
return @sendDatabaseError(res, err)
|
||||
# Add gems to User
|
||||
purchased = _.clone(req.user.get('purchased'))
|
||||
purchased ?= {}
|
||||
purchased.gems ?= 0
|
||||
purchased.gems += subscriptions.basic.gems * months
|
||||
req.user.set('purchased', purchased)
|
||||
|
||||
return @sendNotFoundError(res, "Error while updating prepaid redeemer") if num isnt 1
|
||||
|
||||
# Add terminal subscription to User, extending existing subscriptions
|
||||
# TODO: refactor this into some form useable by both this and purchaseYearSale
|
||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||
endDate = new moment()
|
||||
if startDate
|
||||
endDate = new moment(startDate)
|
||||
else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free))
|
||||
endDate = new moment(stripeInfo.free)
|
||||
|
||||
endDate = endDate.add(months, 'months')
|
||||
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
||||
req.user.set('stripe', stripeInfo)
|
||||
|
||||
# Add gems to User
|
||||
purchased = _.clone(req.user.get('purchased'))
|
||||
purchased ?= {}
|
||||
purchased.gems ?= 0
|
||||
purchased.gems += subscriptions.basic.gems * months
|
||||
req.user.set('purchased', purchased)
|
||||
|
||||
req.user.save (err, user) =>
|
||||
if err
|
||||
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
||||
return @sendDatabaseError(res, err)
|
||||
@sendSuccess(res, user)
|
||||
req.user.save (err, user) =>
|
||||
if err
|
||||
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
||||
return @sendDatabaseError(res, err)
|
||||
@sendSuccess(res, user)
|
||||
|
||||
subscribeUser: (req, user, done) ->
|
||||
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -22,3 +22,5 @@ module.exports.templates =
|
|||
generic_email: 'tem_JhRnQ4pvTS4KdQjYoZdbei'
|
||||
plain_text_email: 'tem_85UvKDCCNPXsFckERTig6Y'
|
||||
next_steps_email: 'tem_RDHhTG5inXQi8pthyqWr5D'
|
||||
course_invite_email: 'tem_u6D2EFWYC5Ptk38bSykjsU'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
@ -196,3 +197,132 @@ describe 'CourseInstance', ->
|
|||
expect(prepaid.get('maxRedeemers')).toEqual(50)
|
||||
expect(prepaid.get('properties')?.courseIDs?.length).toEqual(courses.length)
|
||||
done()
|
||||
|
||||
describe 'Invite to course', ->
|
||||
it 'takes a list of emails and sends invites', (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)
|
||||
inviteStudentsURL = getURL("/db/course_instance/#{courseInstances[0]._id}/invite_students")
|
||||
requestBody = {
|
||||
emails: ['test@test.com']
|
||||
}
|
||||
request.post { uri: inviteStudentsURL, json: requestBody }, (err, res) ->
|
||||
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()
|
||||
|
|
|
@ -29,3 +29,75 @@ describe 'Level', ->
|
|||
body = JSON.parse(body)
|
||||
expect(body.type).toBeDefined()
|
||||
done()
|
||||
|
||||
|
||||
describe 'GET /db/level/<id>/session', ->
|
||||
|
||||
describe 'when level is a course level', ->
|
||||
|
||||
levelID = null
|
||||
|
||||
it 'sets up a course instance', (done) ->
|
||||
|
||||
clearModels [Campaign, Course, CourseInstance, Level, User], (err) ->
|
||||
|
||||
loginAdmin (admin) ->
|
||||
|
||||
url = getURL('/db/level')
|
||||
body =
|
||||
name: 'Course Level'
|
||||
type: 'course'
|
||||
permissions: simplePermissions
|
||||
|
||||
request.post {uri: url, json: body }, (err, res, level) ->
|
||||
levelID = level._id
|
||||
|
||||
url = getURL('/db/campaign')
|
||||
body =
|
||||
name: 'Course Campaign'
|
||||
levels: {}
|
||||
body.levels[level.original] = { 'original': level.original }
|
||||
|
||||
request.post { uri: url, json: body }, (err, res, campaign) ->
|
||||
|
||||
course = new Course({
|
||||
name: 'Test Course'
|
||||
campaignID: ObjectId(campaign._id)
|
||||
})
|
||||
|
||||
course.save (err) ->
|
||||
|
||||
expect(err).toBeNull()
|
||||
|
||||
loginJoe (joe) ->
|
||||
|
||||
courseInstance = new CourseInstance({
|
||||
name: 'Course Instance'
|
||||
members: [
|
||||
joe.get('_id')
|
||||
]
|
||||
courseID: ObjectId(course.id)
|
||||
})
|
||||
|
||||
courseInstance.save (err) ->
|
||||
|
||||
expect(err).toBeNull()
|
||||
done()
|
||||
|
||||
it 'creates a new session if the user is in a course with that level', (done) ->
|
||||
loginJoe (joe) ->
|
||||
|
||||
url = getURL("/db/level/#{levelID}/session")
|
||||
|
||||
request.get { uri: url }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(200)
|
||||
done()
|
||||
|
||||
it 'does not create a new session if the user is not in a course with that level', (done) ->
|
||||
loginSam (sam) ->
|
||||
|
||||
url = getURL("/db/level/#{levelID}/session")
|
||||
|
||||
request.get { uri: url }, (err, res, body) ->
|
||||
expect(res.statusCode).toBe(402)
|
||||
done()
|
||||
|
|
|
@ -2,6 +2,7 @@ require '../common'
|
|||
config = require '../../../server_config'
|
||||
moment = require 'moment'
|
||||
{findStripeSubscription} = require '../../../server/lib/utils'
|
||||
async = require 'async'
|
||||
|
||||
describe '/db/prepaid', ->
|
||||
prepaidURL = getURL('/db/prepaid')
|
||||
|
@ -320,7 +321,6 @@ describe '/db/prepaid', ->
|
|||
expect(payments[0].get('amount')).toEqual(8991)
|
||||
done()
|
||||
|
||||
|
||||
it 'Anonymous cant redeem a prepaid code', (done) ->
|
||||
logoutUser () ->
|
||||
subscribeWithPrepaid joeCode, (err, res) ->
|
||||
|
@ -332,7 +332,7 @@ describe '/db/prepaid', ->
|
|||
loginJoe (joe) ->
|
||||
subscribeWithPrepaid 'abc123', (err, res) ->
|
||||
expect(err).toBeNull()
|
||||
expect(res.statusCode).toEqual(403)
|
||||
expect(res.statusCode).toEqual(404)
|
||||
done()
|
||||
|
||||
it 'User cant redeem empty code', (done) ->
|
||||
|
@ -463,4 +463,48 @@ describe '/db/prepaid', ->
|
|||
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 'Test a bunch of people trying to redeem at once', (done) ->
|
||||
doRedeem = (userX, code, testnum, retry, fnDone) =>
|
||||
loginUser userX, () =>
|
||||
endDate = new moment().add(3, 'months').toISOString().substring(0, 10)
|
||||
subscribeWithPrepaid code, (err, res, result) ->
|
||||
if err
|
||||
return fnDone(err)
|
||||
|
||||
expect(err).toBeNull()
|
||||
expect(result).toBeDefined()
|
||||
if result.stripe
|
||||
expect(result.stripe).toBeDefined()
|
||||
expect(result.stripe.free).toEqual(endDate)
|
||||
expect(result?.purchased?.gems).toEqual(10500)
|
||||
return fnDone(null, {status: "ok", msg: "Redeemed " + retry})
|
||||
else
|
||||
return fnDone(null, {status: 'error', msg: "Redeem attempt Error #{result} (#{userX.id})" + retry })
|
||||
|
||||
redeemPrepaidFn = (code, testnum) =>
|
||||
(fnDone) =>
|
||||
loginNewUser (user1) =>
|
||||
doRedeem(user1, code, testnum, 0, fnDone)
|
||||
|
||||
stripe.tokens.create {
|
||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||
}, (err, token) ->
|
||||
loginNewUser (user) =>
|
||||
codeRedeemers = 50
|
||||
codeMonths = 3
|
||||
redeemers = 51
|
||||
purchasePrepaid 'terminal_subscription', months: codeMonths, codeRedeemers, token.id, (err, res, prepaid) ->
|
||||
expect(err).toBeNull()
|
||||
expect(prepaid).toBeDefined()
|
||||
expect(prepaid.code).toBeDefined()
|
||||
tasks = (redeemPrepaidFn(prepaid.code, i) for i in [0...redeemers])
|
||||
async.parallel tasks, (err, results) =>
|
||||
redeemed = 0
|
||||
error = 0
|
||||
for result in results
|
||||
redeemed += 1 if result.status is 'ok'
|
||||
error += 1 if result.status is 'error'
|
||||
expect(redeemed).toEqual(codeRedeemers)
|
||||
expect(error).toEqual(redeemers - codeRedeemers)
|
||||
done()
|
||||
|
|
Loading…
Add table
Reference in a new issue