From fec3ac38e998667fd9be7620adecadb228b70334 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 19 Mar 2015 15:02:45 -0700 Subject: [PATCH] Prepaid subscriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/locale/en.coffee | 2 + app/schemas/models/prepaid.schema.coffee | 14 + app/schemas/models/user.coffee | 1 + app/styles/admin.sass | 5 +- app/templates/account/subscription-view.jade | 41 +- app/templates/admin.jade | 8 + app/views/account/SubscriptionView.coffee | 61 +- app/views/admin/MainAdminView.coffee | 26 +- app/views/core/SubscribeModal.coffee | 1 + server/commons/mapping.coffee | 1 + server/payments/subscription_handler.coffee | 129 ++-- server/prepaids/Prepaid.coffee | 5 + server/prepaids/prepaid_handler.coffee | 45 ++ server/routes/stripe.coffee | 1 + server/users/user_handler.coffee | 2 +- test/server/common.coffee | 6 + test/server/functional/prepaid.spec.coffee | 95 +++ .../functional/subscription.spec.coffee | 587 ++++++++++++------ 18 files changed, 798 insertions(+), 232 deletions(-) create mode 100644 app/schemas/models/prepaid.schema.coffee create mode 100644 server/prepaids/Prepaid.coffee create mode 100644 server/prepaids/prepaid_handler.coffee create mode 100644 test/server/functional/prepaid.spec.coffee diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 0bc84bf19..a412b056b 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -455,6 +455,8 @@ no_users_subscribed: "No users subscribed, please double check your email addresses." current_recipients: "Current Recipients" unsubscribing: "Unsubscribing..." + subscribe_prepaid: "Click Subscribe to use prepaid code" + using_prepaid: "Using prepaid code for monthly subscription" choose_hero: choose_hero: "Choose Your Hero" diff --git a/app/schemas/models/prepaid.schema.coffee b/app/schemas/models/prepaid.schema.coffee new file mode 100644 index 000000000..799ad409b --- /dev/null +++ b/app/schemas/models/prepaid.schema.coffee @@ -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 diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 933ab9179..26f81a4f6 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -288,6 +288,7 @@ _.extend UserSchema.properties, token: { type: 'string' } couponID: { type: 'string' } 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 subscribeEmails: c.array { description: 'Input for subscribing other users' }, c.shortString() diff --git a/app/styles/admin.sass b/app/styles/admin.sass index 91219b7a3..e111dd9d4 100644 --- a/app/styles/admin.sass +++ b/app/styles/admin.sass @@ -1,2 +1,5 @@ #admin-view - color: black \ No newline at end of file + color: black + + #free-sub-input + min-width: 50% diff --git a/app/templates/account/subscription-view.jade b/app/templates/account/subscription-view.jade index 7db09459d..6bbe9cec8 100644 --- a/app/templates/account/subscription-view.jade +++ b/app/templates/account/subscription-view.jade @@ -19,9 +19,15 @@ block content .panel.panel-default .panel-heading 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 if personalSub.state === 'loading' .alert.alert-info(data-i18n="subscribe.loading_info") + else if personalSub.state === 'subscribing' + .alert.alert-info(data-i18n="subscribe.subscribing") else if personalSub.sponsor div span(data-i18n="subscribe.managed_by") @@ -30,11 +36,30 @@ block content div span(data-i18n="subscribe.will_be_cancelled") span.spl.spr= moment(personalSub.endDate).format('l') + + else if personalSub.usingPrepaidCode + div(data-i18n="subscribe.using_prepaid") + else if personalSub.self - if personalSub.subscribed - button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe - else + if personalSub.state === 'declined' + .alert.alert-danger.alert-dismissible + 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 + else + button.end-subscription-button.btn.btn-lg.btn-warning(data-i18n="subscribe.unsubscribe") Unsubscribe + .unsubscribe-feedback.row.secret .col-lg-7 h3 @@ -75,10 +100,10 @@ block content tr th(data-i18n="account.cost") td= personalSub.cost - if personalSub.card - tr - th(data-i18n="account.card") - td= personalSub.card + if personalSub.card + tr + th(data-i18n="account.card") + td= personalSub.card else button.start-subscription-button.btn.btn-lg.btn-success(data-i18n="subscribe.subscribe_title") Subscribe @@ -129,7 +154,7 @@ block content button.close(type="button" data-dismiss="alert") span(aria-hidden="true") × p(data-i18n="loading_error.unknown") - p= stateMessage + p= recipientSubs.stateMessage else if recipientSubs.justSubscribed && recipientSubs.justSubscribed.length > 0 br .alert.alert-success.alert-dismissible diff --git a/app/templates/admin.jade b/app/templates/admin.jade index 36d754468..2887717a6 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -47,6 +47,14 @@ block content li 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 h3 Achievements diff --git a/app/views/account/SubscriptionView.coffee b/app/views/account/SubscriptionView.coffee index 307bd04b3..6fd31f706 100644 --- a/app/views/account/SubscriptionView.coffee +++ b/app/views/account/SubscriptionView.coffee @@ -18,9 +18,14 @@ utils = require 'core/utils' # 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: 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 basicPlanPrice = 999 +basicPlanID = 'basic' module.exports = class SubscriptionView extends RootView id: "subscription-view" @@ -41,7 +46,8 @@ module.exports = class SubscriptionView extends RootView constructor: (options) -> super(options) - @personalSub = new PersonalSub(@supermodel) + prepaidCode = utils.getQueryVariable '_ppc' + @personalSub = new PersonalSub(@supermodel, prepaidCode) @recipientSubs = new RecipientSubs(@supermodel) @personalSub.update => @render?() @recipientSubs.update => @render?() @@ -55,7 +61,10 @@ module.exports = class SubscriptionView extends RootView # Personal Subscriptions onClickStartSubscription: (e) -> - @openModalView new SubscribeModal() + if @personalSub.prepaidCode + @personalSub.subscribe(=> @render?()) + else + @openModalView new SubscribeModal() window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'account subscription view' onSubscribed: -> @@ -95,7 +104,45 @@ module.exports = class SubscriptionView extends RootView # Helper classes for managing subscription actions and updating UI state 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) -> removeStripe = => @@ -133,6 +180,11 @@ class PersonalSub success: onSubSponsorSuccess }, 0).load() + else if stripeInfo.prepaidCode + @usingPrepaidCode = true + delete @state + render() + else if stripeInfo.subscriptionID @self = true @active = me.isPremium() @@ -146,7 +198,7 @@ class PersonalSub periodEnd = new Date((sub.trial_end or sub.current_period_end) * 1000) if sub.cancel_at_period_end @activeUntil = periodEnd - else + else if sub.discount?.coupon?.id isnt 'free' @nextPaymentDate = periodEnd @cost = "$#{(sub.plan.amount/100).toFixed(2)}" else @@ -165,6 +217,7 @@ class PersonalSub @free = stripeInfo.free delete @state render() + else delete @state render() diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee index e05dd20a9..33a00bd30 100644 --- a/app/views/admin/MainAdminView.coffee +++ b/app/views/admin/MainAdminView.coffee @@ -14,6 +14,12 @@ module.exports = class MainAdminView extends RootView 'click #user-search-button': 'searchForUser' 'click #increment-button': 'incrementUserAttribute' 'click #user-search-result': 'onClickUserSearchResult' + 'click #create-free-sub-btn': 'onClickFreeSubLink' + + getRenderData: -> + context = super() + context.freeSubLink = @freeSubLink + context checkForFormSubmissionEnterPress: (e) -> if e.which is 13 and @$el.find('#espionage-name-or-email').val() isnt '' @@ -61,7 +67,25 @@ module.exports = class MainAdminView extends RootView val = $('#increment-field').val() me.set(val, me.get(val) + 1) me.save() - + onClickUserSearchResult: (e) -> userID = $(e.target).closest('tr').data('user-id') @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() diff --git a/app/views/core/SubscribeModal.coffee b/app/views/core/SubscribeModal.coffee index 3802211f0..bef3e660f 100644 --- a/app/views/core/SubscribeModal.coffee +++ b/app/views/core/SubscribeModal.coffee @@ -154,6 +154,7 @@ module.exports = class SubscribeModal extends ModalView stripe = me.get('stripe') ? {} delete stripe.token delete stripe.planID + # TODO: Need me.set('stripe', stripe) here? xhr = options.xhr if xhr.status is 402 @state = 'declined' diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 3abbeda7e..8c7e333cc 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -23,6 +23,7 @@ module.exports.handlers = 'earned_achievement': 'achievements/earned_achievement_handler' 'poll': 'polls/poll_handler' 'user_polls_record': 'polls/user_polls_record_handler' + 'prepaid': 'prepaids/prepaid_handler' module.exports.routes = [ diff --git a/server/payments/subscription_handler.coffee b/server/payments/subscription_handler.coffee index 6d4c8f93a..fb3df7f51 100644 --- a/server/payments/subscription_handler.coffee +++ b/server/payments/subscription_handler.coffee @@ -4,6 +4,7 @@ async = require 'async' Handler = require '../commons/Handler' discountHandler = require './discount_handler' +Prepaid = require '../prepaids/Prepaid' User = require '../users/User' {findStripeSubscription} = require '../lib/utils' {getSponsoredSubsAmount} = require '../../app/core/utils' @@ -25,52 +26,50 @@ class SubscriptionHandler extends Handler return done({res: 'You must be signed in to subscribe.', code: 403}) token = req.body.stripe.token + prepaidCode = req.body.stripe.prepaidCode customerID = user.get('stripe')?.customerID - if not (token or customerID) - @logSubscriptionError(user, 'Missing stripe token or customer ID.') - return done({res: 'Missing stripe token or customer ID.', code: 422}) + if not (token or customerID or prepaidCode) + @logSubscriptionError(user, 'Missing Stripe token or customer ID or prepaid code') + return done({res: 'Missing Stripe token or customer ID or prepaid code', code: 422}) - # Create/retrieve Stripe customer - if token - if customerID + # Get Stripe customer + if customerID + if token stripe.customers.update customerID, { card: token }, (err, customer) => if err or not customer # should not happen outside of test and production polluting each other @logSubscriptionError(user, 'Cannot find customer: ' + customerID + '\n\n' + err) return done({res: 'Cannot find customer.', code: 404}) - @checkForExistingSubscription(req, user, customer, done) - + @checkForCoupon(req, user, customer, done) else - options = - card: token - email: user.get('email') - metadata: { id: user._id + '', slug: user.get('slug') } - stripe.customers.create options, (err, customer) => + stripe.customers.retrieve customerID, (err, customer) => if err - if err.type in ['StripeCardError', 'StripeInvalidRequestError'] - return done({res: 'Card error', code: 402}) - else - @logSubscriptionError(user, 'Stripe customer creation error. ' + err) - return done({res: 'Database error.', code: 500}) - - stripeInfo = _.cloneDeep(user.get('stripe') ? {}) - stripeInfo.customerID = customer.id - user.set('stripe', stripeInfo) - user.save (err) => - if err - @logSubscriptionError(user, 'Stripe customer id save db error. ' + err) - return done({res: 'Database error.', code: 500}) - @checkForExistingSubscription(req, user, customer, done) - + @logSubscriptionError(user, 'Stripe customer retrieve error. ' + err) + return done({res: 'Database error.', code: 500}) + @checkForCoupon(req, user, customer, done) else - stripe.customers.retrieve(customerID, (err, customer) => + options = + email: user.get('email') + metadata: { id: user._id + '', slug: user.get('slug') } + options.card = token if token? + stripe.customers.create options, (err, customer) => if err - @logSubscriptionError(user, 'Stripe customer retrieve error. ' + err) - return done({res: 'Database error.', code: 500}) - @checkForExistingSubscription(req, user, customer, done) - ) + if err.type in ['StripeCardError', 'StripeInvalidRequestError'] + return done({res: 'Card error', code: 402}) + else + @logSubscriptionError(user, 'Stripe customer creation error. ' + err) + return done({res: 'Database error.', code: 500}) - checkForExistingSubscription: (req, user, customer, done) -> + stripeInfo = _.cloneDeep(user.get('stripe') ? {}) + stripeInfo.customerID = customer.id + user.set('stripe', stripeInfo) + user.save (err) => + if err + @logSubscriptionError(user, 'Stripe customer id save db error. ' + err) + return done({res: 'Database error.', code: 500}) + @checkForCoupon(req, user, customer, done) + + checkForCoupon: (req, user, customer, done) -> # Check if user is subscribing someone else if req.body.stripe?.subscribeEmails? return @updateStripeRecipientSubscriptions req, user, customer, done @@ -78,12 +77,31 @@ class SubscriptionHandler extends Handler if user.get('stripe')?.sponsorID 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 - # SALE LOGIC - # overwrite couponID with another for everyone-sales - #couponID = 'hoc_399' if not 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 + # overwrite couponID with another for everyone-sales + #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) => if subscription @@ -97,19 +115,25 @@ class SubscriptionHandler extends Handler if err @logSubscriptionError(user, 'Stripe cancel subscription error. ' + err) return done({res: 'Database error.', code: 500}) - options = { plan: 'basic', metadata: {id: user.id}, trial_end: subscription.current_period_end } options.coupon = couponID if couponID stripe.customers.createSubscription customer.id, options, (err, subscription) => if err @logSubscriptionError(user, 'Stripe customer plan setting error. ' + err) 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) + else - # can skip creating the subscription - return @updateUser(req, user, customer, subscription, false, done) + # Skip creating the subscription + @updateUser(req, user, customer, subscription, false, done) else options = { plan: 'basic', metadata: {id: user.id}} @@ -143,8 +167,25 @@ class SubscriptionHandler extends Handler if err @logSubscriptionError(user, 'Stripe user plan saving error. ' + err) 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) -> return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails? diff --git a/server/prepaids/Prepaid.coffee b/server/prepaids/Prepaid.coffee new file mode 100644 index 000000000..0464faa96 --- /dev/null +++ b/server/prepaids/Prepaid.coffee @@ -0,0 +1,5 @@ +mongoose = require 'mongoose' + +PrepaidSchema = new mongoose.Schema {}, {strict: false, minimize: false} + +module.exports = Prepaid = mongoose.model('prepaid', PrepaidSchema) diff --git a/server/prepaids/prepaid_handler.coffee b/server/prepaids/prepaid_handler.coffee new file mode 100644 index 000000000..efcb267b9 --- /dev/null +++ b/server/prepaids/prepaid_handler.coffee @@ -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() diff --git a/server/routes/stripe.coffee b/server/routes/stripe.coffee index e54cb7413..b06fea792 100644 --- a/server/routes/stripe.coffee +++ b/server/routes/stripe.coffee @@ -108,6 +108,7 @@ module.exports.setup = (app) -> stripeInfo = _.cloneDeep(user.get('stripe') ? {}) delete stripeInfo.planID + delete stripeInfo.prepaidCode delete stripeInfo.subscriptionID user.set('stripe', stripeInfo) user.save (err) => diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 1c92f746c..0701b4063 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -143,7 +143,7 @@ UserHandler = class UserHandler extends Handler ) else wantsPlan = req.body.stripe.planID? - hasPlan = user.get('stripe')?.planID? + hasPlan = user.get('stripe')?.planID? and not req.body.stripe.prepaidCode? finishSubscription hasPlan, wantsPlan # Discount setting diff --git a/test/server/common.coffee b/test/server/common.coffee index 8d91a3db1..eb594f712 100644 --- a/test/server/common.coffee +++ b/test/server/common.coffee @@ -37,6 +37,7 @@ models_path = [ '../../server/achievements/Achievement' '../../server/achievements/EarnedAchievement' '../../server/payments/Payment' + '../../server/prepaids/Prepaid' ] for m in models_path @@ -113,6 +114,11 @@ wrapUpGetUser = (email, user, done) -> GLOBAL.getURL = (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 GLOBAL.createNewUser = (done) -> name = password = "user#{newUserCount++}" diff --git a/test/server/functional/prepaid.spec.coffee b/test/server/functional/prepaid.spec.coffee new file mode 100644 index 000000000..4280d4423 --- /dev/null +++ b/test/server/functional/prepaid.spec.coffee @@ -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 diff --git a/test/server/functional/subscription.spec.coffee b/test/server/functional/subscription.spec.coffee index 7a39713f3..6a9d0f7e5 100644 --- a/test/server/functional/subscription.spec.coffee +++ b/test/server/functional/subscription.spec.coffee @@ -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 -describe 'Sponsored subscriptions', -> +describe 'Subscriptions', -> # TODO: Test recurring billing via webhooks # TODO: Test error rollbacks, Stripe is authority @@ -359,23 +359,37 @@ describe 'Sponsored subscriptions', -> expect(payment.get('gems')).toBeGreaterThan(subGems - 1) done() - subscribeUser = (user, token, done) -> + subscribeUser = (user, token, prepaidCode, done) -> requestBody = user.toObject() requestBody.stripe = planID: 'basic' requestBody.stripe.token = token.id if token? + requestBody.stripe.prepaidCode = prepaidCode if prepaidCode? request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) expect(body.stripe.customerID).toBeDefined() expect(body.stripe.planID).toBe('basic') 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) + 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() unsubscribeUser = (user, done) -> requestBody = user.toObject() delete requestBody.stripe.planID + delete requestBody.stripe.prepaidCode request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) @@ -383,7 +397,11 @@ describe 'Sponsored subscriptions', -> expect(user.get('stripe').customerID).toBeDefined() expect(user.get('stripe').planID).toBeUndefined() expect(user.get('stripe').token).toBeUndefined() - done() + 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() subscribeRecipients = (sponsor, recipients, token, done) -> # console.log 'subscribeRecipients', sponsor.id, (recipient.id for recipient in recipients), token? @@ -444,7 +462,7 @@ describe 'Sponsored subscriptions', -> # Simulate subscription ending after cancellation return done() unless immediately - # Simulate subscription cancelling a trial end + # Simulate subscription cancelling at period end stripe.customers.cancelSubscription customerID, subscriptionID, (err) -> expect(err).toBeNull() @@ -494,7 +512,177 @@ describe 'Sponsored subscriptions', -> throw err if err 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) -> stripe.tokens.create { card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } @@ -572,7 +760,7 @@ describe 'Sponsored subscriptions', -> }, (err, token) -> createNewUser (user2) -> loginNewUser (user1) -> - subscribeUser user1, token, (updatedUser) -> + subscribeUser user1, token, null, (updatedUser) -> User.findById user1.id, (err, user1) -> expect(err).toBeNull() subscribeRecipients user1, [user2], null, (updatedUser) -> @@ -588,7 +776,7 @@ describe 'Sponsored subscriptions', -> }, (err, token) -> createNewUser (user2) -> loginNewUser (user1) -> - subscribeUser user1, token, (updatedUser) -> + subscribeUser user1, token, null, (updatedUser) -> User.findById user1.id, (err, user1) -> expect(err).toBeNull() subscribeRecipients user1, [user2], null, (updatedUser) -> @@ -608,12 +796,12 @@ describe 'Sponsored subscriptions', -> }, (err, token) -> createNewUser (user2) -> loginNewUser (user1) -> - subscribeUser user1, token, (updatedUser) -> + subscribeUser user1, token, null, (updatedUser) -> User.findById user1.id, (err, user1) -> expect(err).toBeNull() subscribeRecipients user1, [user2], null, (updatedUser) -> User.findById user1.id, (err, user1) -> - unsubscribeUser user1, (updatedUser) -> + unsubscribeUser user1, -> verifySponsorship user1.id, user2.id, done it 'Sponsored user2 tries to subscribe', (done) -> @@ -636,6 +824,29 @@ describe 'Sponsored subscriptions', -> expect(res.statusCode).toBe(403) 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) -> stripe.tokens.create { card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } @@ -771,7 +982,7 @@ describe 'Sponsored subscriptions', -> stripe.tokens.create { card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } }, (err, token) -> - subscribeUser user1, token, (updatedUser) -> + subscribeUser user1, token, null, (updatedUser) -> User.findById user1.id, (err, user1) -> expect(err).toBeNull() expect(user1.get('stripe').subscriptionID).toBeDefined() @@ -791,186 +1002,216 @@ describe 'Sponsored subscriptions', -> expect(sub.metadata?.id).toEqual(user1.id) done() - describe 'Bulk discounts', -> - # Bulk discount algorithm (includes personal sub): - # 1 100% - # 2-11 80% - # 12+ 60% + 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() - it 'Unsubscribed user1 subscribes two users', (done) -> - stripe.tokens.create { - card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } - }, (err, token) -> - createNewUser (user3) -> - createNewUser (user2) -> + + describe 'Bulk discounts', -> + # Bulk discount algorithm (includes personal sub): + # 1 100% + # 2-11 80% + # 12+ 60% + + it 'Unsubscribed user1 subscribes two users', (done) -> + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + createNewUser (user3) -> + createNewUser (user2) -> + loginNewUser (user1) -> + subscribeRecipients user1, [user2, user3], token, (updatedUser) -> + verifySponsorship user1.id, user2.id, -> + verifySponsorship user1.id, user3.id, done + + it 'Subscribed user1 subscribes 2 users, unsubscribes 2', (done) -> + recipientCount = 2 + recipientsToVerify = [0, 1] + recipients = new SubbedRecipients recipientCount, recipientsToVerify + + # Create recipients + recipients.createRecipients -> + expect(recipients.length()).toEqual(recipientCount) + + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> + + # Create sponsor user loginNewUser (user1) -> - subscribeRecipients user1, [user2, user3], token, (updatedUser) -> - verifySponsorship user1.id, user2.id, -> - verifySponsorship user1.id, user3.id, done + subscribeUser user1, token, null, (updatedUser) -> + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() - it 'Subscribed user1 subscribes 2 users, unsubscribes 2', (done) -> - recipientCount = 2 - recipientsToVerify = [0, 1] - recipients = new SubbedRecipients recipientCount, recipientsToVerify + # Subscribe recipients + recipients.subRecipients user1, null, -> + User.findById user1.id, (err, user1) -> - # Create recipients - recipients.createRecipients -> - expect(recipients.length()).toEqual(recipientCount) + # Unsubscribe recipient0 + unsubscribeRecipient user1, recipients.get(0), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(1) + verifyNotSponsoring user1.id, recipients.get(0).id, -> + verifyNotRecipient recipients.get(0).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(getSubscribedQuantity(1)) - stripe.tokens.create { - card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } - }, (err, token) -> + # Unsubscribe recipient1 + unsubscribeRecipient user1, recipients.get(1), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(0) + verifyNotSponsoring user1.id, recipients.get(1).id, -> + verifyNotRecipient recipients.get(1).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(0) + done() - # Create sponsor user - loginNewUser (user1) -> - subscribeUser user1, token, (updatedUser) -> - User.findById user1.id, (err, user1) -> - expect(err).toBeNull() + it 'Subscribed user1 subscribes 3 users, unsubscribes 2, themselves, then 1', (done) -> + recipientCount = 3 + recipientsToVerify = [0, 1, 2] + recipients = new SubbedRecipients recipientCount, recipientsToVerify - # Subscribe recipients - recipients.subRecipients user1, null, -> - User.findById user1.id, (err, user1) -> + # Create recipients + recipients.createRecipients -> + expect(recipients.length()).toEqual(recipientCount) + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> - # Unsubscribe recipient0 - unsubscribeRecipient user1, recipients.get(0), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(1) - verifyNotSponsoring user1.id, recipients.get(0).id, -> - verifyNotRecipient recipients.get(0).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(1)) + # Create sponsor user + loginNewUser (user1) -> + subscribeUser user1, token, null, (updatedUser) -> + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() - # Unsubscribe recipient1 - unsubscribeRecipient user1, recipients.get(1), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(0) - verifyNotSponsoring user1.id, recipients.get(1).id, -> - verifyNotRecipient recipients.get(1).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(0) - done() + # Subscribe recipients + recipients.subRecipients user1, null, -> + User.findById user1.id, (err, user1) -> - it 'Subscribed user1 subscribes 3 users, unsubscribes 2, themselves, then 1', (done) -> - recipientCount = 3 - recipientsToVerify = [0, 1, 2] - recipients = new SubbedRecipients recipientCount, recipientsToVerify + # Unsubscribe first recipient + unsubscribeRecipient user1, recipients.get(0), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) + verifyNotSponsoring user1.id, recipients.get(0).id, -> + verifyNotRecipient recipients.get(0).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 1)) - # Create recipients - recipients.createRecipients -> - expect(recipients.length()).toEqual(recipientCount) - stripe.tokens.create { - card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } - }, (err, token) -> + # Unsubscribe second recipient + unsubscribeRecipient user1, recipients.get(1), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) + verifyNotSponsoring user1.id, recipients.get(1).id, -> + verifyNotRecipient recipients.get(1).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 2)) - # Create sponsor user - loginNewUser (user1) -> - subscribeUser user1, token, (updatedUser) -> - User.findById user1.id, (err, user1) -> - expect(err).toBeNull() + # Unsubscribe self + User.findById user1.id, (err, user1) -> + unsubscribeUser user1, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.planID).toBeUndefined() - # Subscribe recipients - recipients.subRecipients user1, null, -> - User.findById user1.id, (err, user1) -> + # Unsubscribe third recipient + verifySponsorship user1.id, recipients.get(2).id, -> + unsubscribeRecipient user1, recipients.get(2), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(recipientCount - 3) + verifyNotSponsoring user1.id, recipients.get(2).id, -> + verifyNotRecipient recipients.get(2).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 3)) + done() - # Unsubscribe first recipient - unsubscribeRecipient user1, recipients.get(0), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) - verifyNotSponsoring user1.id, recipients.get(0).id, -> - verifyNotRecipient recipients.get(0).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 1)) + it 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) -> + # TODO: verify interim invoices? + recipientCount = 13 + recipientsToVerify = [0, 1, 10, 11, 12] + recipients = new SubbedRecipients recipientCount, recipientsToVerify - # Unsubscribe second recipient - unsubscribeRecipient user1, recipients.get(1), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) - verifyNotSponsoring user1.id, recipients.get(1).id, -> - verifyNotRecipient recipients.get(1).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 2)) + # Create recipients + recipients.createRecipients -> + expect(recipients.length()).toEqual(recipientCount) - # Unsubscribe self - User.findById user1.id, (err, user1) -> - unsubscribeUser user1, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.planID).toBeUndefined() + stripe.tokens.create { + card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } + }, (err, token) -> - # Unsubscribe third recipient - verifySponsorship user1.id, recipients.get(2).id, -> - unsubscribeRecipient user1, recipients.get(2), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 3) - verifyNotSponsoring user1.id, recipients.get(2).id, -> - verifyNotRecipient recipients.get(2).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 3)) - done() + # Create sponsor user + loginNewUser (user1) -> - it 'Unsubscribed user1 subscribes 13 users, unsubcribes 2', (done) -> - # TODO: verify interim invoices? - recipientCount = 13 - recipientsToVerify = [0, 1, 10, 11, 12] - recipients = new SubbedRecipients recipientCount, recipientsToVerify + # Subscribe recipients + recipients.subRecipients user1, token, -> + User.findById user1.id, (err, user1) -> - # Create recipients - recipients.createRecipients -> - expect(recipients.length()).toEqual(recipientCount) + # Unsubscribe first recipient + unsubscribeRecipient user1, recipients.get(0), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) + verifyNotSponsoring user1.id, recipients.get(0).id, -> + verifyNotRecipient recipients.get(0).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1)) - stripe.tokens.create { - card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' } - }, (err, token) -> - - # Create sponsor user - loginNewUser (user1) -> - - # Subscribe recipients - recipients.subRecipients user1, token, -> - User.findById user1.id, (err, user1) -> - - # Unsubscribe first recipient - unsubscribeRecipient user1, recipients.get(0), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 1) - verifyNotSponsoring user1.id, recipients.get(0).id, -> - verifyNotRecipient recipients.get(0).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1)) - - # Unsubscribe last recipient - unsubscribeRecipient user1, recipients.get(recipientCount - 1), true, -> - User.findById user1.id, (err, user1) -> - stripeInfo = user1.get('stripe') - expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) - verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, -> - verifyNotRecipient recipients.get(recipientCount - 1).id, -> - stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> - expect(err).toBeNull() - expect(subscription).not.toBeNull() - numSponsored = recipientCount - 2 - if numSponsored <= 1 - expect(subscription.quantity).toEqual(subPrice) - else if numSponsored <= 11 - expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8) - else - expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6) - done() + # Unsubscribe last recipient + unsubscribeRecipient user1, recipients.get(recipientCount - 1), true, -> + User.findById user1.id, (err, user1) -> + stripeInfo = user1.get('stripe') + expect(stripeInfo.recipients.length).toEqual(recipientCount - 2) + verifyNotSponsoring user1.id, recipients.get(recipientCount - 1).id, -> + verifyNotRecipient recipients.get(recipientCount - 1).id, -> + stripe.customers.retrieveSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, (err, subscription) -> + expect(err).toBeNull() + expect(subscription).not.toBeNull() + numSponsored = recipientCount - 2 + if numSponsored <= 1 + expect(subscription.quantity).toEqual(subPrice) + else if numSponsored <= 11 + expect(subscription.quantity).toEqual(subPrice + (numSponsored - 1) * subPrice * 0.8) + else + expect(subscription.quantity).toEqual(subPrice + 10 * subPrice * 0.8 + (numSponsored - 11) * subPrice * 0.6) + done()