mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Prepaid subscriptions
Admins can generate a prepaid code, which a user can use to subscribe for free via the account/subscription page. The subscription will be identical to the normal monthly subscription (e.g. 3500 gems per month), except they won’t be charged. Does not require the recipient to enter billing information. Can be applied to an existing subscription, which will be converted to free. Prepaid code can only be used once. Prepaid subscription cannot be unsubscribed via the UI.
This commit is contained in:
parent
aee68e899a
commit
fec3ac38e9
18 changed files with 798 additions and 232 deletions
|
@ -455,6 +455,8 @@
|
||||||
no_users_subscribed: "No users subscribed, please double check your email addresses."
|
no_users_subscribed: "No users subscribed, please double check your email addresses."
|
||||||
current_recipients: "Current Recipients"
|
current_recipients: "Current Recipients"
|
||||||
unsubscribing: "Unsubscribing..."
|
unsubscribing: "Unsubscribing..."
|
||||||
|
subscribe_prepaid: "Click Subscribe to use prepaid code"
|
||||||
|
using_prepaid: "Using prepaid code for monthly subscription"
|
||||||
|
|
||||||
choose_hero:
|
choose_hero:
|
||||||
choose_hero: "Choose Your Hero"
|
choose_hero: "Choose Your Hero"
|
||||||
|
|
14
app/schemas/models/prepaid.schema.coffee
Normal file
14
app/schemas/models/prepaid.schema.coffee
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
c = require './../schemas'
|
||||||
|
|
||||||
|
PrepaidSchema = c.object({title: 'Prepaid', required: ['creator', 'redeemer', 'type']}, {
|
||||||
|
creator: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
|
||||||
|
redeemer: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
|
||||||
|
code: c.shortString(title: "Unique code to redeem")
|
||||||
|
type: { type: 'string' }
|
||||||
|
status: { enum: ['active', 'used'], default: 'active' }
|
||||||
|
properties: {type: 'object'}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.extendBasicProperties(PrepaidSchema, 'prepaid')
|
||||||
|
|
||||||
|
module.exports = PrepaidSchema
|
|
@ -288,6 +288,7 @@ _.extend UserSchema.properties,
|
||||||
token: { type: 'string' }
|
token: { type: 'string' }
|
||||||
couponID: { type: 'string' }
|
couponID: { type: 'string' }
|
||||||
free: { type: ['boolean', 'string'], format: 'date-time', description: 'Type string is subscription end date' }
|
free: { type: ['boolean', 'string'], format: 'date-time', description: 'Type string is subscription end date' }
|
||||||
|
prepaidCode: c.shortString description: 'Prepaid code to apply to sub purchase'
|
||||||
|
|
||||||
# Sponsored subscriptions
|
# Sponsored subscriptions
|
||||||
subscribeEmails: c.array { description: 'Input for subscribing other users' }, c.shortString()
|
subscribeEmails: c.array { description: 'Input for subscribing other users' }, c.shortString()
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
#admin-view
|
#admin-view
|
||||||
color: black
|
color: black
|
||||||
|
|
||||||
|
#free-sub-input
|
||||||
|
min-width: 50%
|
||||||
|
|
|
@ -19,9 +19,15 @@ block content
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
h3(data-i18n="subscribe.personal_sub")
|
h3(data-i18n="subscribe.personal_sub")
|
||||||
|
if personalSub.prepaidCode && !personalSub.usingPrepaidCode
|
||||||
|
div
|
||||||
|
span(data-i18n="subscribe.subscribe_prepaid")
|
||||||
|
span.spl.spr= personalSub.prepaidCode
|
||||||
.panel-body
|
.panel-body
|
||||||
if personalSub.state === 'loading'
|
if personalSub.state === 'loading'
|
||||||
.alert.alert-info(data-i18n="subscribe.loading_info")
|
.alert.alert-info(data-i18n="subscribe.loading_info")
|
||||||
|
else if personalSub.state === 'subscribing'
|
||||||
|
.alert.alert-info(data-i18n="subscribe.subscribing")
|
||||||
else if personalSub.sponsor
|
else if personalSub.sponsor
|
||||||
div
|
div
|
||||||
span(data-i18n="subscribe.managed_by")
|
span(data-i18n="subscribe.managed_by")
|
||||||
|
@ -30,11 +36,30 @@ block content
|
||||||
div
|
div
|
||||||
span(data-i18n="subscribe.will_be_cancelled")
|
span(data-i18n="subscribe.will_be_cancelled")
|
||||||
span.spl.spr= moment(personalSub.endDate).format('l')
|
span.spl.spr= moment(personalSub.endDate).format('l')
|
||||||
|
|
||||||
|
else if personalSub.usingPrepaidCode
|
||||||
|
div(data-i18n="subscribe.using_prepaid")
|
||||||
|
|
||||||
else if personalSub.self
|
else if personalSub.self
|
||||||
if personalSub.subscribed
|
if personalSub.state === 'declined'
|
||||||
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
.alert.alert-danger.alert-dismissible
|
||||||
else
|
span(data-i18n="buy_gems.declined")
|
||||||
|
button.close(type="button" data-dismiss="alert")
|
||||||
|
span(aria-hidden="true") ×
|
||||||
|
br
|
||||||
|
else if personalSub.state === 'unknown_error'
|
||||||
|
.alert.alert-danger.alert-dismissible
|
||||||
|
button.close(type="button" data-dismiss="alert")
|
||||||
|
span(aria-hidden="true") ×
|
||||||
|
p(data-i18n="loading_error.unknown")
|
||||||
|
p= personalSub.stateMessage
|
||||||
|
br
|
||||||
|
|
||||||
|
if !personalSub.subscribed || personalSub.prepaidCode
|
||||||
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||||
|
else
|
||||||
|
button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe
|
||||||
|
|
||||||
.unsubscribe-feedback.row.secret
|
.unsubscribe-feedback.row.secret
|
||||||
.col-lg-7
|
.col-lg-7
|
||||||
h3
|
h3
|
||||||
|
@ -129,7 +154,7 @@ block content
|
||||||
button.close(type="button" data-dismiss="alert")
|
button.close(type="button" data-dismiss="alert")
|
||||||
span(aria-hidden="true") ×
|
span(aria-hidden="true") ×
|
||||||
p(data-i18n="loading_error.unknown")
|
p(data-i18n="loading_error.unknown")
|
||||||
p= stateMessage
|
p= recipientSubs.stateMessage
|
||||||
else if recipientSubs.justSubscribed && recipientSubs.justSubscribed.length > 0
|
else if recipientSubs.justSubscribed && recipientSubs.justSubscribed.length > 0
|
||||||
br
|
br
|
||||||
.alert.alert-success.alert-dismissible
|
.alert.alert-success.alert-dismissible
|
||||||
|
|
|
@ -47,6 +47,14 @@ block content
|
||||||
li
|
li
|
||||||
a(href="/admin/growth", data-i18n="admin.growth") Growth
|
a(href="/admin/growth", data-i18n="admin.growth") Growth
|
||||||
|
|
||||||
|
if me.isAdmin()
|
||||||
|
hr
|
||||||
|
h3 Prepaids
|
||||||
|
a.btn.btn-secondary#create-free-sub-btn Create Free Subscription Link
|
||||||
|
span.spl.spr
|
||||||
|
if freeSubLink
|
||||||
|
input#free-sub-input(type="text", readonly, value="#{freeSubLink}")
|
||||||
|
|
||||||
hr
|
hr
|
||||||
|
|
||||||
h3 Achievements
|
h3 Achievements
|
||||||
|
|
|
@ -18,9 +18,14 @@ utils = require 'core/utils'
|
||||||
# TODO: next payment amount incorrect if have an expiring personal sub
|
# TODO: next payment amount incorrect if have an expiring personal sub
|
||||||
# TODO: consider hiding managed subscription body UI while things are updating to avoid brief legacy data
|
# TODO: consider hiding managed subscription body UI while things are updating to avoid brief legacy data
|
||||||
# TODO: Next payment info for personal sub displays most recent payment when resubscribing before trial end
|
# TODO: Next payment info for personal sub displays most recent payment when resubscribing before trial end
|
||||||
|
# TODO: PersonalSub and RecipientSubs have similar subscribe APIs
|
||||||
|
# TODO: Better recovery from trying to reuse a prepaid
|
||||||
|
# TODO: No way to unsubscribe from prepaid subscription
|
||||||
|
# TODO: Refactor state machines driving the UI. They've become a hot mess.
|
||||||
|
|
||||||
# TODO: Get basic plan price dynamically
|
# TODO: Get basic plan price dynamically
|
||||||
basicPlanPrice = 999
|
basicPlanPrice = 999
|
||||||
|
basicPlanID = 'basic'
|
||||||
|
|
||||||
module.exports = class SubscriptionView extends RootView
|
module.exports = class SubscriptionView extends RootView
|
||||||
id: "subscription-view"
|
id: "subscription-view"
|
||||||
|
@ -41,7 +46,8 @@ module.exports = class SubscriptionView extends RootView
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
@personalSub = new PersonalSub(@supermodel)
|
prepaidCode = utils.getQueryVariable '_ppc'
|
||||||
|
@personalSub = new PersonalSub(@supermodel, prepaidCode)
|
||||||
@recipientSubs = new RecipientSubs(@supermodel)
|
@recipientSubs = new RecipientSubs(@supermodel)
|
||||||
@personalSub.update => @render?()
|
@personalSub.update => @render?()
|
||||||
@recipientSubs.update => @render?()
|
@recipientSubs.update => @render?()
|
||||||
|
@ -55,6 +61,9 @@ module.exports = class SubscriptionView extends RootView
|
||||||
# Personal Subscriptions
|
# Personal Subscriptions
|
||||||
|
|
||||||
onClickStartSubscription: (e) ->
|
onClickStartSubscription: (e) ->
|
||||||
|
if @personalSub.prepaidCode
|
||||||
|
@personalSub.subscribe(=> @render?())
|
||||||
|
else
|
||||||
@openModalView new SubscribeModal()
|
@openModalView new SubscribeModal()
|
||||||
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view'
|
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view'
|
||||||
|
|
||||||
|
@ -95,7 +104,45 @@ module.exports = class SubscriptionView extends RootView
|
||||||
# Helper classes for managing subscription actions and updating UI state
|
# Helper classes for managing subscription actions and updating UI state
|
||||||
|
|
||||||
class PersonalSub
|
class PersonalSub
|
||||||
constructor: (@supermodel) ->
|
constructor: (@supermodel, @prepaidCode) ->
|
||||||
|
|
||||||
|
subscribe: (render) ->
|
||||||
|
return unless @prepaidCode
|
||||||
|
|
||||||
|
if @prepaidCode is me.get('stripe')?.prepaidCode
|
||||||
|
delete @prepaidCode
|
||||||
|
return render()
|
||||||
|
|
||||||
|
@state = 'subscribing'
|
||||||
|
@stateMessage = ''
|
||||||
|
render()
|
||||||
|
|
||||||
|
stripeInfo = _.clone(me.get('stripe') ? {})
|
||||||
|
stripeInfo.planID = basicPlanID
|
||||||
|
stripeInfo.prepaidCode = @prepaidCode
|
||||||
|
me.set('stripe', stripeInfo)
|
||||||
|
|
||||||
|
me.once 'sync', =>
|
||||||
|
application.tracker?.trackEvent 'Finished subscription purchase', revenue: 0
|
||||||
|
delete @prepaidCode
|
||||||
|
@update(render)
|
||||||
|
me.once 'error', (user, response, options) =>
|
||||||
|
console.error 'We got an error subscribing with Stripe from our server:', response
|
||||||
|
stripeInfo = me.get('stripe') ? {}
|
||||||
|
delete stripeInfo.planID
|
||||||
|
delete stripeInfo.prepaidCode
|
||||||
|
me.set('stripe', stripeInfo)
|
||||||
|
xhr = options.xhr
|
||||||
|
if xhr.status is 402
|
||||||
|
@state = 'declined'
|
||||||
|
@stateMessage = ''
|
||||||
|
else
|
||||||
|
if xhr.status is 403
|
||||||
|
delete @prepaidCode
|
||||||
|
@state = 'unknown_error'
|
||||||
|
@stateMessage = "#{xhr.status}: #{xhr.responseText}"
|
||||||
|
render()
|
||||||
|
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||||
|
|
||||||
unsubscribe: (message) ->
|
unsubscribe: (message) ->
|
||||||
removeStripe = =>
|
removeStripe = =>
|
||||||
|
@ -133,6 +180,11 @@ class PersonalSub
|
||||||
success: onSubSponsorSuccess
|
success: onSubSponsorSuccess
|
||||||
}, 0).load()
|
}, 0).load()
|
||||||
|
|
||||||
|
else if stripeInfo.prepaidCode
|
||||||
|
@usingPrepaidCode = true
|
||||||
|
delete @state
|
||||||
|
render()
|
||||||
|
|
||||||
else if stripeInfo.subscriptionID
|
else if stripeInfo.subscriptionID
|
||||||
@self = true
|
@self = true
|
||||||
@active = me.isPremium()
|
@active = me.isPremium()
|
||||||
|
@ -146,7 +198,7 @@ class PersonalSub
|
||||||
periodEnd = new Date((sub.trial_end or sub.current_period_end) * 1000)
|
periodEnd = new Date((sub.trial_end or sub.current_period_end) * 1000)
|
||||||
if sub.cancel_at_period_end
|
if sub.cancel_at_period_end
|
||||||
@activeUntil = periodEnd
|
@activeUntil = periodEnd
|
||||||
else
|
else if sub.discount?.coupon?.id isnt 'free'
|
||||||
@nextPaymentDate = periodEnd
|
@nextPaymentDate = periodEnd
|
||||||
@cost = "$#{(sub.plan.amount/100).toFixed(2)}"
|
@cost = "$#{(sub.plan.amount/100).toFixed(2)}"
|
||||||
else
|
else
|
||||||
|
@ -165,6 +217,7 @@ class PersonalSub
|
||||||
@free = stripeInfo.free
|
@free = stripeInfo.free
|
||||||
delete @state
|
delete @state
|
||||||
render()
|
render()
|
||||||
|
|
||||||
else
|
else
|
||||||
delete @state
|
delete @state
|
||||||
render()
|
render()
|
||||||
|
|
|
@ -14,6 +14,12 @@ module.exports = class MainAdminView extends RootView
|
||||||
'click #user-search-button': 'searchForUser'
|
'click #user-search-button': 'searchForUser'
|
||||||
'click #increment-button': 'incrementUserAttribute'
|
'click #increment-button': 'incrementUserAttribute'
|
||||||
'click #user-search-result': 'onClickUserSearchResult'
|
'click #user-search-result': 'onClickUserSearchResult'
|
||||||
|
'click #create-free-sub-btn': 'onClickFreeSubLink'
|
||||||
|
|
||||||
|
getRenderData: ->
|
||||||
|
context = super()
|
||||||
|
context.freeSubLink = @freeSubLink
|
||||||
|
context
|
||||||
|
|
||||||
checkForFormSubmissionEnterPress: (e) ->
|
checkForFormSubmissionEnterPress: (e) ->
|
||||||
if e.which is 13 and @$el.find('#espionage-name-or-email').val() isnt ''
|
if e.which is 13 and @$el.find('#espionage-name-or-email').val() isnt ''
|
||||||
|
@ -65,3 +71,21 @@ module.exports = class MainAdminView extends RootView
|
||||||
onClickUserSearchResult: (e) ->
|
onClickUserSearchResult: (e) ->
|
||||||
userID = $(e.target).closest('tr').data('user-id')
|
userID = $(e.target).closest('tr').data('user-id')
|
||||||
@openModalView new AdministerUserModal({}, userID) if userID
|
@openModalView new AdministerUserModal({}, userID) if userID
|
||||||
|
|
||||||
|
onClickFreeSubLink: (e) =>
|
||||||
|
delete @freeSubLink
|
||||||
|
return unless me.isAdmin()
|
||||||
|
options =
|
||||||
|
url: '/db/prepaid/-/create'
|
||||||
|
data: {type: 'subscription'}
|
||||||
|
method: 'POST'
|
||||||
|
options.success = (model, response, options) =>
|
||||||
|
# TODO: Don't hardcode domain.
|
||||||
|
if application.isProduction()
|
||||||
|
@freeSubLink = "https://codecombat.com/account/subscription?_ppc=#{model.code}"
|
||||||
|
else
|
||||||
|
@freeSubLink = "http://localhost:3000/account/subscription?_ppc=#{model.code}"
|
||||||
|
@render?()
|
||||||
|
options.error = (model, response, options) =>
|
||||||
|
console.error 'Failed to create prepaid', response
|
||||||
|
@supermodel.addRequestResource('create_prepaid', options, 0).load()
|
||||||
|
|
|
@ -154,6 +154,7 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
stripe = me.get('stripe') ? {}
|
stripe = me.get('stripe') ? {}
|
||||||
delete stripe.token
|
delete stripe.token
|
||||||
delete stripe.planID
|
delete stripe.planID
|
||||||
|
# TODO: Need me.set('stripe', stripe) here?
|
||||||
xhr = options.xhr
|
xhr = options.xhr
|
||||||
if xhr.status is 402
|
if xhr.status is 402
|
||||||
@state = 'declined'
|
@state = 'declined'
|
||||||
|
|
|
@ -23,6 +23,7 @@ module.exports.handlers =
|
||||||
'earned_achievement': 'achievements/earned_achievement_handler'
|
'earned_achievement': 'achievements/earned_achievement_handler'
|
||||||
'poll': 'polls/poll_handler'
|
'poll': 'polls/poll_handler'
|
||||||
'user_polls_record': 'polls/user_polls_record_handler'
|
'user_polls_record': 'polls/user_polls_record_handler'
|
||||||
|
'prepaid': 'prepaids/prepaid_handler'
|
||||||
|
|
||||||
module.exports.routes =
|
module.exports.routes =
|
||||||
[
|
[
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
discountHandler = require './discount_handler'
|
discountHandler = require './discount_handler'
|
||||||
|
Prepaid = require '../prepaids/Prepaid'
|
||||||
User = require '../users/User'
|
User = require '../users/User'
|
||||||
{findStripeSubscription} = require '../lib/utils'
|
{findStripeSubscription} = require '../lib/utils'
|
||||||
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
||||||
|
@ -25,26 +26,32 @@ class SubscriptionHandler extends Handler
|
||||||
return done({res: 'You must be signed in to subscribe.', code: 403})
|
return done({res: 'You must be signed in to subscribe.', code: 403})
|
||||||
|
|
||||||
token = req.body.stripe.token
|
token = req.body.stripe.token
|
||||||
|
prepaidCode = req.body.stripe.prepaidCode
|
||||||
customerID = user.get('stripe')?.customerID
|
customerID = user.get('stripe')?.customerID
|
||||||
if not (token or customerID)
|
if not (token or customerID or prepaidCode)
|
||||||
@logSubscriptionError(user, 'Missing stripe token or customer ID.')
|
@logSubscriptionError(user, 'Missing Stripe token or customer ID or prepaid code')
|
||||||
return done({res: 'Missing stripe token or customer ID.', code: 422})
|
return done({res: 'Missing Stripe token or customer ID or prepaid code', code: 422})
|
||||||
|
|
||||||
# Create/retrieve Stripe customer
|
# Get Stripe customer
|
||||||
if token
|
|
||||||
if customerID
|
if customerID
|
||||||
|
if token
|
||||||
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
||||||
if err or not customer
|
if err or not customer
|
||||||
# should not happen outside of test and production polluting each other
|
# should not happen outside of test and production polluting each other
|
||||||
@logSubscriptionError(user, 'Cannot find customer: ' + customerID + '\n\n' + err)
|
@logSubscriptionError(user, 'Cannot find customer: ' + customerID + '\n\n' + err)
|
||||||
return done({res: 'Cannot find customer.', code: 404})
|
return done({res: 'Cannot find customer.', code: 404})
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
@checkForCoupon(req, user, customer, done)
|
||||||
|
else
|
||||||
|
stripe.customers.retrieve customerID, (err, customer) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe customer retrieve error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
@checkForCoupon(req, user, customer, done)
|
||||||
else
|
else
|
||||||
options =
|
options =
|
||||||
card: token
|
|
||||||
email: user.get('email')
|
email: user.get('email')
|
||||||
metadata: { id: user._id + '', slug: user.get('slug') }
|
metadata: { id: user._id + '', slug: user.get('slug') }
|
||||||
|
options.card = token if token?
|
||||||
stripe.customers.create options, (err, customer) =>
|
stripe.customers.create options, (err, customer) =>
|
||||||
if err
|
if err
|
||||||
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
||||||
|
@ -60,17 +67,9 @@ class SubscriptionHandler extends Handler
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe customer id save db error. ' + err)
|
@logSubscriptionError(user, 'Stripe customer id save db error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
@checkForCoupon(req, user, customer, done)
|
||||||
|
|
||||||
else
|
checkForCoupon: (req, user, customer, done) ->
|
||||||
stripe.customers.retrieve(customerID, (err, customer) =>
|
|
||||||
if err
|
|
||||||
@logSubscriptionError(user, 'Stripe customer retrieve error. ' + err)
|
|
||||||
return done({res: 'Database error.', code: 500})
|
|
||||||
@checkForExistingSubscription(req, user, customer, done)
|
|
||||||
)
|
|
||||||
|
|
||||||
checkForExistingSubscription: (req, user, customer, done) ->
|
|
||||||
# Check if user is subscribing someone else
|
# Check if user is subscribing someone else
|
||||||
if req.body.stripe?.subscribeEmails?
|
if req.body.stripe?.subscribeEmails?
|
||||||
return @updateStripeRecipientSubscriptions req, user, customer, done
|
return @updateStripeRecipientSubscriptions req, user, customer, done
|
||||||
|
@ -78,12 +77,31 @@ class SubscriptionHandler extends Handler
|
||||||
if user.get('stripe')?.sponsorID
|
if user.get('stripe')?.sponsorID
|
||||||
return done({res: 'You already have a sponsored subscription.', code: 403})
|
return done({res: 'You already have a sponsored subscription.', code: 403})
|
||||||
|
|
||||||
couponID = user.get('stripe')?.couponID
|
if req.body?.stripe?.prepaidCode
|
||||||
|
Prepaid.findOne code: req.body.stripe.prepaidCode, (err, prepaid) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Prepaid lookup error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
return done({res: 'Prepaid not found', code: 404}) unless prepaid?
|
||||||
|
return done({res: 'Prepaid not for subscription', code: 403}) unless prepaid.get('type') is 'subscription'
|
||||||
|
return done({res: 'Prepaid has already been used', code: 403}) unless prepaid.get('status') is 'active'
|
||||||
|
return done({res: 'Database error.', code: 500}) unless prepaid.get('properties')?.couponID
|
||||||
|
couponID = prepaid.get('properties').couponID
|
||||||
|
|
||||||
|
# Update user
|
||||||
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
|
stripeInfo.couponID = couponID
|
||||||
|
stripeInfo.prepaidCode = req.body.stripe.prepaidCode
|
||||||
|
user.set('stripe', stripeInfo)
|
||||||
|
@checkForExistingSubscription(req, user, customer, couponID, done)
|
||||||
|
else
|
||||||
|
couponID = user.get('stripe')?.couponID
|
||||||
# SALE LOGIC
|
# SALE LOGIC
|
||||||
# overwrite couponID with another for everyone-sales
|
# overwrite couponID with another for everyone-sales
|
||||||
#couponID = 'hoc_399' if not couponID
|
#couponID = 'hoc_399' if not couponID
|
||||||
|
@checkForExistingSubscription(req, user, customer, couponID, done)
|
||||||
|
|
||||||
|
checkForExistingSubscription: (req, user, customer, couponID, done) ->
|
||||||
findStripeSubscription customer.id, subscriptionID: user.get('stripe')?.subscriptionID, (subscription) =>
|
findStripeSubscription customer.id, subscriptionID: user.get('stripe')?.subscriptionID, (subscription) =>
|
||||||
|
|
||||||
if subscription
|
if subscription
|
||||||
|
@ -97,19 +115,25 @@ class SubscriptionHandler extends Handler
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
options = { plan: 'basic', metadata: {id: user.id}, trial_end: subscription.current_period_end }
|
options = { plan: 'basic', metadata: {id: user.id}, trial_end: subscription.current_period_end }
|
||||||
options.coupon = couponID if couponID
|
options.coupon = couponID if couponID
|
||||||
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe customer plan setting error. ' + err)
|
@logSubscriptionError(user, 'Stripe customer plan setting error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
@updateUser(req, user, customer, subscription, false, done)
|
||||||
|
|
||||||
|
else if couponID
|
||||||
|
# Update subscription with given couponID
|
||||||
|
stripe.customers.updateSubscription customer.id, subscription.id, coupon: couponID, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe update subscription coupon error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
@updateUser(req, user, customer, subscription, false, done)
|
@updateUser(req, user, customer, subscription, false, done)
|
||||||
|
|
||||||
else
|
else
|
||||||
# can skip creating the subscription
|
# Skip creating the subscription
|
||||||
return @updateUser(req, user, customer, subscription, false, done)
|
@updateUser(req, user, customer, subscription, false, done)
|
||||||
|
|
||||||
else
|
else
|
||||||
options = { plan: 'basic', metadata: {id: user.id}}
|
options = { plan: 'basic', metadata: {id: user.id}}
|
||||||
|
@ -143,8 +167,25 @@ class SubscriptionHandler extends Handler
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
|
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
user?.saveActiveUser 'subscribe'
|
|
||||||
return done()
|
if stripeInfo.prepaidCode?
|
||||||
|
# Update prepaid to 'used'
|
||||||
|
Prepaid.findOne code: stripeInfo.prepaidCode, (err, prepaid) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Prepaid find error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
unless prepaid?
|
||||||
|
@logSubscriptionError(user, "Expected prepaid not found: #{stripeInfo.prepaidCode}")
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
prepaid.set('status', 'used')
|
||||||
|
prepaid.set('redeemer', user.get('_id'))
|
||||||
|
prepaid.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Prepaid update error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
else
|
||||||
|
done()
|
||||||
|
|
||||||
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
|
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
|
||||||
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
|
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
|
||||||
|
|
5
server/prepaids/Prepaid.coffee
Normal file
5
server/prepaids/Prepaid.coffee
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mongoose = require 'mongoose'
|
||||||
|
|
||||||
|
PrepaidSchema = new mongoose.Schema {}, {strict: false, minimize: false}
|
||||||
|
|
||||||
|
module.exports = Prepaid = mongoose.model('prepaid', PrepaidSchema)
|
45
server/prepaids/prepaid_handler.coffee
Normal file
45
server/prepaids/prepaid_handler.coffee
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
Handler = require '../commons/Handler'
|
||||||
|
Prepaid = require './Prepaid'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
|
modelClass: Prepaid
|
||||||
|
jsonSchema: require '../../app/schemas/models/prepaid.schema'
|
||||||
|
allowedMethods: ['POST']
|
||||||
|
|
||||||
|
hasAccess: (req) ->
|
||||||
|
req.user?.isAdmin()
|
||||||
|
|
||||||
|
getByRelationship: (req, res, args...) ->
|
||||||
|
relationship = args[1]
|
||||||
|
return @createPrepaid(req, res) if relationship is 'create'
|
||||||
|
super arguments...
|
||||||
|
|
||||||
|
createPrepaid: (req, res) ->
|
||||||
|
return @sendForbiddenError(res) unless @hasAccess(req)
|
||||||
|
return @sendForbiddenError(res) unless req.body.type is 'subscription'
|
||||||
|
@generateNewCode (code) =>
|
||||||
|
return @sendDatabaseError(res, 'Database error.') unless code
|
||||||
|
prepaid = new Prepaid
|
||||||
|
creator: req.user.id
|
||||||
|
type: req.body.type
|
||||||
|
status: 'active'
|
||||||
|
code: code
|
||||||
|
properties:
|
||||||
|
couponID: 'free'
|
||||||
|
prepaid.save (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, prepaid.toObject())
|
||||||
|
|
||||||
|
generateNewCode: (done) ->
|
||||||
|
tryCode = ->
|
||||||
|
code = _.sample("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
|
||||||
|
Prepaid.findOne code: code, (err, prepaid) ->
|
||||||
|
return if err
|
||||||
|
return done(code) unless prepaid
|
||||||
|
tryCode()
|
||||||
|
tryCode()
|
||||||
|
|
||||||
|
module.exports = new PrepaidHandler()
|
|
@ -108,6 +108,7 @@ module.exports.setup = (app) ->
|
||||||
|
|
||||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||||
delete stripeInfo.planID
|
delete stripeInfo.planID
|
||||||
|
delete stripeInfo.prepaidCode
|
||||||
delete stripeInfo.subscriptionID
|
delete stripeInfo.subscriptionID
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
user.save (err) =>
|
user.save (err) =>
|
||||||
|
|
|
@ -143,7 +143,7 @@ UserHandler = class UserHandler extends Handler
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
wantsPlan = req.body.stripe.planID?
|
wantsPlan = req.body.stripe.planID?
|
||||||
hasPlan = user.get('stripe')?.planID?
|
hasPlan = user.get('stripe')?.planID? and not req.body.stripe.prepaidCode?
|
||||||
finishSubscription hasPlan, wantsPlan
|
finishSubscription hasPlan, wantsPlan
|
||||||
|
|
||||||
# Discount setting
|
# Discount setting
|
||||||
|
|
|
@ -37,6 +37,7 @@ models_path = [
|
||||||
'../../server/achievements/Achievement'
|
'../../server/achievements/Achievement'
|
||||||
'../../server/achievements/EarnedAchievement'
|
'../../server/achievements/EarnedAchievement'
|
||||||
'../../server/payments/Payment'
|
'../../server/payments/Payment'
|
||||||
|
'../../server/prepaids/Prepaid'
|
||||||
]
|
]
|
||||||
|
|
||||||
for m in models_path
|
for m in models_path
|
||||||
|
@ -113,6 +114,11 @@ wrapUpGetUser = (email, user, done) ->
|
||||||
GLOBAL.getURL = (path) ->
|
GLOBAL.getURL = (path) ->
|
||||||
return 'http://localhost:3001' + path
|
return 'http://localhost:3001' + path
|
||||||
|
|
||||||
|
GLOBAL.createPrepaid = (type, done) ->
|
||||||
|
options = uri: GLOBAL.getURL('/db/prepaid/-/create')
|
||||||
|
options.json = type: type if type?
|
||||||
|
request.post options, done
|
||||||
|
|
||||||
newUserCount = 0
|
newUserCount = 0
|
||||||
GLOBAL.createNewUser = (done) ->
|
GLOBAL.createNewUser = (done) ->
|
||||||
name = password = "user#{newUserCount++}"
|
name = password = "user#{newUserCount++}"
|
||||||
|
|
95
test/server/functional/prepaid.spec.coffee
Normal file
95
test/server/functional/prepaid.spec.coffee
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
require '../common'
|
||||||
|
|
||||||
|
describe '/db/prepaid', ->
|
||||||
|
prepaidURL = getURL('/db/prepaid')
|
||||||
|
prepaidCreateURL = getURL('/db/prepaid/-/create')
|
||||||
|
|
||||||
|
verifyPrepaid = (user, prepaid, done) ->
|
||||||
|
expect(prepaid.creator).toEqual(user.id)
|
||||||
|
expect(prepaid.type).toEqual('subscription')
|
||||||
|
expect(prepaid.status).toEqual('active')
|
||||||
|
expect(prepaid.code).toMatch(/^\w{8}$/)
|
||||||
|
expect(prepaid.properties?.couponID).toEqual('free')
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Clear database users and prepaids', (done) ->
|
||||||
|
clearModels [User, Prepaid], (err) ->
|
||||||
|
throw err if err
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Anonymous creates prepaid code', (done) ->
|
||||||
|
createPrepaid 'subscription', (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(401)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Non-admin creates prepaid code', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
expect(user1.isAdmin()).toEqual(false)
|
||||||
|
createPrepaid 'subscription', (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Admin creates prepaid code with type subscription', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
verifyPrepaid user1, body, done
|
||||||
|
|
||||||
|
it 'Admin creates prepaid code with invalid type', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'bulldozer', (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Admin creates prepaid code with no type specified', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid null, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Non-admin requests /db/prepaid', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
expect(user1.isAdmin()).toEqual(false)
|
||||||
|
request.get {uri: prepaidURL}, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Admin requests /db/prepaid', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
request.get {uri: prepaidURL}, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
prepaids = JSON.parse(body)
|
||||||
|
found = false
|
||||||
|
for p in prepaids
|
||||||
|
if p._id is prepaid._id
|
||||||
|
found = true
|
||||||
|
verifyPrepaid user1, p, done
|
||||||
|
break
|
||||||
|
expect(found).toEqual(true)
|
||||||
|
done() unless found
|
|
@ -217,7 +217,7 @@ describe '/db/user, editing stripe property', ->
|
||||||
setTimeout(f, 500) # bit of a race condition here, response returns before stripe has been updated
|
setTimeout(f, 500) # bit of a race condition here, response returns before stripe has been updated
|
||||||
|
|
||||||
|
|
||||||
describe 'Sponsored subscriptions', ->
|
describe 'Subscriptions', ->
|
||||||
# TODO: Test recurring billing via webhooks
|
# TODO: Test recurring billing via webhooks
|
||||||
# TODO: Test error rollbacks, Stripe is authority
|
# TODO: Test error rollbacks, Stripe is authority
|
||||||
|
|
||||||
|
@ -359,23 +359,37 @@ describe 'Sponsored subscriptions', ->
|
||||||
expect(payment.get('gems')).toBeGreaterThan(subGems - 1)
|
expect(payment.get('gems')).toBeGreaterThan(subGems - 1)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
subscribeUser = (user, token, done) ->
|
subscribeUser = (user, token, prepaidCode, done) ->
|
||||||
requestBody = user.toObject()
|
requestBody = user.toObject()
|
||||||
requestBody.stripe =
|
requestBody.stripe =
|
||||||
planID: 'basic'
|
planID: 'basic'
|
||||||
requestBody.stripe.token = token.id if token?
|
requestBody.stripe.token = token.id if token?
|
||||||
|
requestBody.stripe.prepaidCode = prepaidCode if prepaidCode?
|
||||||
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
expect(body.stripe.customerID).toBeDefined()
|
expect(body.stripe.customerID).toBeDefined()
|
||||||
expect(body.stripe.planID).toBe('basic')
|
expect(body.stripe.planID).toBe('basic')
|
||||||
expect(body.stripe.token).toBeUndefined()
|
expect(body.stripe.token).toBeUndefined()
|
||||||
|
if prepaidCode?
|
||||||
|
expect(body.stripe.prepaidCode).toEqual(prepaidCode)
|
||||||
|
expect(body.stripe.couponID).toEqual('free')
|
||||||
expect(body.purchased.gems).toBeGreaterThan(subGems - 1)
|
expect(body.purchased.gems).toBeGreaterThan(subGems - 1)
|
||||||
|
User.findById user.id, (err, user) ->
|
||||||
|
stripeInfo = user.get('stripe')
|
||||||
|
expect(stripeInfo.customerID).toBeDefined()
|
||||||
|
expect(stripeInfo.planID).toBe('basic')
|
||||||
|
expect(stripeInfo.token).toBeUndefined()
|
||||||
|
if prepaidCode?
|
||||||
|
expect(stripeInfo.prepaidCode).toEqual(prepaidCode)
|
||||||
|
expect(stripeInfo.couponID).toEqual('free')
|
||||||
|
expect(user.get('purchased').gems).toBeGreaterThan(subGems - 1)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
unsubscribeUser = (user, done) ->
|
unsubscribeUser = (user, done) ->
|
||||||
requestBody = user.toObject()
|
requestBody = user.toObject()
|
||||||
delete requestBody.stripe.planID
|
delete requestBody.stripe.planID
|
||||||
|
delete requestBody.stripe.prepaidCode
|
||||||
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(res.statusCode).toBe(200)
|
expect(res.statusCode).toBe(200)
|
||||||
|
@ -383,6 +397,10 @@ describe 'Sponsored subscriptions', ->
|
||||||
expect(user.get('stripe').customerID).toBeDefined()
|
expect(user.get('stripe').customerID).toBeDefined()
|
||||||
expect(user.get('stripe').planID).toBeUndefined()
|
expect(user.get('stripe').planID).toBeUndefined()
|
||||||
expect(user.get('stripe').token).toBeUndefined()
|
expect(user.get('stripe').token).toBeUndefined()
|
||||||
|
stripe.customers.retrieveSubscription user.get('stripe').customerID, user.get('stripe').subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription?.cancel_at_period_end).toEqual(true)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
subscribeRecipients = (sponsor, recipients, token, done) ->
|
subscribeRecipients = (sponsor, recipients, token, done) ->
|
||||||
|
@ -444,7 +462,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
# Simulate subscription ending after cancellation
|
# Simulate subscription ending after cancellation
|
||||||
return done() unless immediately
|
return done() unless immediately
|
||||||
|
|
||||||
# Simulate subscription cancelling a trial end
|
# Simulate subscription cancelling at period end
|
||||||
stripe.customers.cancelSubscription customerID, subscriptionID, (err) ->
|
stripe.customers.cancelSubscription customerID, subscriptionID, (err) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
@ -494,7 +512,177 @@ describe 'Sponsored subscriptions', ->
|
||||||
throw err if err
|
throw err if err
|
||||||
done()
|
done()
|
||||||
|
|
||||||
describe 'Basic', ->
|
describe 'Personal', ->
|
||||||
|
it 'Subscribe user with new token', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeUser user1, token, null, done
|
||||||
|
|
||||||
|
it 'Admin subscribes self with valid prepaid', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
Prepaid.findById prepaid._id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(prepaid.get('status')).toEqual('used')
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Admin subscribes self with invalid prepaid', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
requestBody = user1.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
planID: 'basic'
|
||||||
|
requestBody.stripe.prepaidCode = 'MattMatt'
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(404)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'User2 subscribes with used prepaid', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
requestBody = user2.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
planID: 'basic'
|
||||||
|
requestBody.stripe.prepaidCode = prepaid.code
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribe normally, subscribe with valid prepaid', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
subscribeUser user1, token, null, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
Prepaid.findById prepaid._id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(prepaid.get('status')).toEqual('used')
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
customerID = user1.get('stripe').customerID
|
||||||
|
subscriptionID = user1.get('stripe').subscriptionID
|
||||||
|
stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.discount?.coupon?.id).toEqual('free')
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribe with coupon, subscribe with valid prepaid', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
requestBody = user1.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
planID: 'basic'
|
||||||
|
token: token.id
|
||||||
|
couponID: '20pct'
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, updatedUser) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
Prepaid.findById prepaid._id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(prepaid.get('status')).toEqual('used')
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
customerID = user1.get('stripe').customerID
|
||||||
|
subscriptionID = user1.get('stripe').subscriptionID
|
||||||
|
stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(subscription).not.toBeNull()
|
||||||
|
expect(subscription.discount?.coupon?.id).toEqual('free')
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribe with prepaid, then cancel', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
Prepaid.findById prepaid._id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(prepaid.get('status')).toEqual('used')
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
unsubscribeUser user1, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.get('stripe').prepaidCode).toEqual(prepaid.get('code'))
|
||||||
|
done()
|
||||||
|
|
||||||
|
it 'Subscribe with prepaid, then delete', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
Prepaid.findById prepaid._id, (err, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(prepaid.get('status')).toEqual('used')
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
unsubscribeUser user1, ->
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.prepaidCode).toEqual(prepaid.get('code'))
|
||||||
|
|
||||||
|
# Delete subscription
|
||||||
|
stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.subscriptionID, (err, subscription) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
event = _.cloneDeep(customerSubscriptionDeletedSampleEvent)
|
||||||
|
event.data.object = subscription
|
||||||
|
request.post {uri: webhookURL, json: event}, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
User.findById user1.id, (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = user1.get('stripe')
|
||||||
|
expect(stripeInfo.planID).toBeUndefined()
|
||||||
|
expect(stripeInfo.prepaidCode).toBeUndefined()
|
||||||
|
expect(stripeInfo.subscriptionID).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
|
||||||
|
describe 'Sponsored', ->
|
||||||
it 'Unsubscribed user1 subscribes user2', (done) ->
|
it 'Unsubscribed user1 subscribes user2', (done) ->
|
||||||
stripe.tokens.create {
|
stripe.tokens.create {
|
||||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
@ -572,7 +760,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
}, (err, token) ->
|
}, (err, token) ->
|
||||||
createNewUser (user2) ->
|
createNewUser (user2) ->
|
||||||
loginNewUser (user1) ->
|
loginNewUser (user1) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
@ -588,7 +776,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
}, (err, token) ->
|
}, (err, token) ->
|
||||||
createNewUser (user2) ->
|
createNewUser (user2) ->
|
||||||
loginNewUser (user1) ->
|
loginNewUser (user1) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
|
@ -608,12 +796,12 @@ describe 'Sponsored subscriptions', ->
|
||||||
}, (err, token) ->
|
}, (err, token) ->
|
||||||
createNewUser (user2) ->
|
createNewUser (user2) ->
|
||||||
loginNewUser (user1) ->
|
loginNewUser (user1) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
subscribeRecipients user1, [user2], null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
unsubscribeUser user1, (updatedUser) ->
|
unsubscribeUser user1, ->
|
||||||
verifySponsorship user1.id, user2.id, done
|
verifySponsorship user1.id, user2.id, done
|
||||||
|
|
||||||
it 'Sponsored user2 tries to subscribe', (done) ->
|
it 'Sponsored user2 tries to subscribe', (done) ->
|
||||||
|
@ -636,6 +824,29 @@ describe 'Sponsored subscriptions', ->
|
||||||
expect(res.statusCode).toBe(403)
|
expect(res.statusCode).toBe(403)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'Sponsored user2 tries to subscribe with valid prepaid', (done) ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
createNewUser (user2) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
subscribeRecipients user1, [user2], token, (updatedUser) ->
|
||||||
|
loginUser user2, (user2) ->
|
||||||
|
user2.set('permissions', ['admin'])
|
||||||
|
user2.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user2.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
requestBody = user2.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
planID: 'basic'
|
||||||
|
prepaidCode: prepaid.code
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(403)
|
||||||
|
done()
|
||||||
|
|
||||||
it 'Sponsored user2 tries to unsubscribe', (done) ->
|
it 'Sponsored user2 tries to unsubscribe', (done) ->
|
||||||
stripe.tokens.create {
|
stripe.tokens.create {
|
||||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
@ -771,7 +982,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
stripe.tokens.create {
|
stripe.tokens.create {
|
||||||
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
}, (err, token) ->
|
}, (err, token) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
expect(user1.get('stripe').subscriptionID).toBeDefined()
|
expect(user1.get('stripe').subscriptionID).toBeDefined()
|
||||||
|
@ -791,6 +1002,36 @@ describe 'Sponsored subscriptions', ->
|
||||||
expect(sub.metadata?.id).toEqual(user1.id)
|
expect(sub.metadata?.id).toEqual(user1.id)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
|
it 'Subscribe with prepaid, then get sponsored', (done) ->
|
||||||
|
loginNewUser (user1) ->
|
||||||
|
user1.set('permissions', ['admin'])
|
||||||
|
user1.save (err, user1) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(user1.isAdmin()).toEqual(true)
|
||||||
|
createPrepaid 'subscription', (err, res, prepaid) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
subscribeUser user1, null, prepaid.code, ->
|
||||||
|
stripe.tokens.create {
|
||||||
|
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
|
||||||
|
}, (err, token) ->
|
||||||
|
loginNewUser (user2) ->
|
||||||
|
requestBody = user2.toObject()
|
||||||
|
requestBody.stripe =
|
||||||
|
token: token.id
|
||||||
|
subscribeEmails: [user1.get('emailLower')]
|
||||||
|
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
User.findById user1.id, (err, user) ->
|
||||||
|
expect(err).toBeNull()
|
||||||
|
stripeInfo = user.get('stripe')
|
||||||
|
expect(stripeInfo.customerID).toBeDefined()
|
||||||
|
expect(stripeInfo.planID).toBeDefined()
|
||||||
|
expect(stripeInfo.subscriptionID).toBeDefined()
|
||||||
|
expect(stripeInfo.sponsorID).toBeUndefined()
|
||||||
|
done()
|
||||||
|
|
||||||
|
|
||||||
describe 'Bulk discounts', ->
|
describe 'Bulk discounts', ->
|
||||||
# Bulk discount algorithm (includes personal sub):
|
# Bulk discount algorithm (includes personal sub):
|
||||||
# 1 100%
|
# 1 100%
|
||||||
|
@ -823,7 +1064,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
|
|
||||||
# Create sponsor user
|
# Create sponsor user
|
||||||
loginNewUser (user1) ->
|
loginNewUser (user1) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
@ -870,7 +1111,7 @@ describe 'Sponsored subscriptions', ->
|
||||||
|
|
||||||
# Create sponsor user
|
# Create sponsor user
|
||||||
loginNewUser (user1) ->
|
loginNewUser (user1) ->
|
||||||
subscribeUser user1, token, (updatedUser) ->
|
subscribeUser user1, token, null, (updatedUser) ->
|
||||||
User.findById user1.id, (err, user1) ->
|
User.findById user1.id, (err, user1) ->
|
||||||
expect(err).toBeNull()
|
expect(err).toBeNull()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue