codecombat/server/handlers/subscription_handler.coffee
phoenixeliot bb6262483f Allow username-only signup for classroom users
Address some code review feedback

Correct error code in test

Don't try to send emails to empty addresses

Add tests for subscriptions

Add tests for Next Steps email

Fix specs

Add reason for disabled test
2016-07-18 10:41:17 -07:00

713 lines
33 KiB
CoffeeScript

# Not paired with a document in the DB, just handles coordinating between
# the stripe property in the user with what's being stored in Stripe.
log = require 'winston'
MongoClient = require('mongodb').MongoClient
mongoose = require 'mongoose'
async = require 'async'
config = require '../../server_config'
Handler = require '../commons/Handler'
slack = require '../slack'
discountHandler = require './discount_handler'
Prepaid = require '../models/Prepaid'
User = require '../models/User'
{findStripeSubscription} = require '../lib/utils'
{getSponsoredSubsAmount} = require '../../app/core/utils'
StripeUtils = require '../lib/stripe_utils'
moment = require 'moment'
Product = require '../models/Product'
recipientCouponID = 'free'
class SubscriptionHandler extends Handler
logSubscriptionError: (user, msg) ->
log.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
getByRelationship: (req, res, args...) ->
return @getStripeEvents(req, res) if args[1] is 'stripe_events'
return @getStripeInvoices(req, res) if args[1] is 'stripe_invoices'
return @getStripeSubscriptions(req, res) if args[1] is 'stripe_subscriptions'
return @getSubscribers(req, res) if args[1] is 'subscribers'
return @purchaseYearSale(req, res) if args[1] is 'year_sale'
return @subscribeWithPrepaidCode(req, res) if args[1] is 'subscribe_prepaid'
super(arguments...)
getStripeEvents: (req, res) ->
# console.log 'subscription_handler getStripeEvents', req.body?.options
return @sendForbiddenError(res) unless req.user?.isAdmin()
stripe.events.list req.body.options, (err, events) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, events)
getStripeInvoices: (req, res) ->
# console.log 'subscription_handler getStripeInvoices'
return @sendForbiddenError(res) unless req.user?.isAdmin()
stripe.invoices.list req.body.options, (err, invoices) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, invoices)
getStripeSubscriptions: (req, res) ->
# console.log 'subscription_handler getStripeSubscriptions'
return @sendForbiddenError(res) unless req.user?.isAdmin()
stripeSubscriptions = []
createGetSubFn = (customerID, subscriptionID) =>
(done) =>
stripe.customers.retrieveSubscription customerID, subscriptionID, (err, subscription) =>
# TODO: return error instead of ignore?
stripeSubscriptions.push(subscription) unless err
done()
tasks = []
for subscription in req.body.subscriptions
tasks.push createGetSubFn(subscription.customerID, subscription.subscriptionID)
async.parallel tasks, (err, results) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, stripeSubscriptions)
getSubscribers: (req, res) ->
# console.log 'subscription_handler getSubscribers'
return @sendForbiddenError(res) unless req.user?.isAdmin()
subscriberUserIDs = req.body.ids or []
User.find {_id: {$in: subscriberUserIDs}}, (err, users) =>
return @sendDatabaseError(res, err) if err
userMap = {}
userMap[user.id] = user.toObject() for user in users
try
# Get conversion data directly from analytics database and add it to results
url = "mongodb://#{config.mongo.analytics_host}:#{config.mongo.analytics_port}/#{config.mongo.analytics_db}"
MongoClient.connect url, (err, db) =>
if err
log.debug 'Analytics connect error: ' + err
return @sendDatabaseError(res, err)
userEventMap = {}
events = ['Finished subscription purchase', 'Show subscription modal']
query = {$and: [{user: {$in: subscriberUserIDs}}, {event: {$in: events}}]}
db.collection('log').find(query).sort({_id: -1}).each (err, doc) =>
if err
db.close()
return @sendDatabaseError(res, err)
if (doc)
userEventMap[doc.user] ?= []
userEventMap[doc.user].push doc
else
db.close()
for userID, eventList of userEventMap
finishedPurchase = false
for event in eventList
finishedPurchase = true if event.event is 'Finished subscription purchase'
if finishedPurchase
if event.event is 'Show subscription modal' and event.properties?.level?
userMap[userID].conversion = event.properties.level
break
else if event.event is 'Show subscription modal' and event.properties?.label in ['buy gems modal', 'check private clan', 'create clan']
userMap[userID].conversion = event.properties.label
break
@sendSuccess(res, userMap)
catch err
log.debug 'Analytics error:\n' + err
@sendSuccess(res, userMap)
cancelSubscriptionImmediately: (user, subscription, done) =>
return done() unless user and subscription
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
return done(err) if err
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
delete stripeInfo.planID
delete stripeInfo.prepaidCode
delete stripeInfo.subscriptionID
user.set('stripe', stripeInfo)
user.save (err) =>
return done(err) if err
done()
purchaseYearSale: (req, res) ->
return @sendForbiddenError(res) unless req.user?
return @sendForbiddenError(res) if req.user?.get('stripe')?.sponsorID
StripeUtils.getCustomer req.user, req.body.stripe?.token, (err, customer) =>
if err
@logSubscriptionError(req.user, "Purchase year sale get customer: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
findStripeSubscription customer.id, subscriptionID: req.user.get('stripe')?.subscriptionID, (subscription) =>
stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) if subscription
@cancelSubscriptionImmediately req.user, subscription, (err) =>
if err
@logSubscriptionError(user, "Purchase year sale Stripe cancel subscription error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
Product.findOne({name: 'year_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'year_subscription product not found') if not product
metadata =
type: req.body.type
userID: req.user._id + ''
gems: product.get('gems')
timestamp: parseInt(req.body.stripe?.timestamp)
description: req.body.description
StripeUtils.createCharge req.user, product.get('amount'), metadata, (err, charge) =>
if err
@logSubscriptionError(req.user, "Purchase year sale create charge: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
StripeUtils.createPayment req.user, charge, {}, (err, payment) =>
if err
@logSubscriptionError(req.user, "Purchase year sale create payment: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
# Add terminal subscription to User with extensions for existing subscriptions
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
endDate = new Date()
if stripeSubscriptionPeriodEndDate
endDate = stripeSubscriptionPeriodEndDate
else if _.isString(stripeInfo.free) and new Date() < new Date(stripeInfo.free)
endDate = new Date(stripeInfo.free)
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
stripeInfo.free = endDate.toISOString().substring(0, 10)
req.user.set('stripe', stripeInfo)
# Add year's worth of gems to User
purchased = _.clone(req.user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += parseInt(charge.metadata.gems) if charge.metadata.gems
req.user.set('purchased', purchased)
req.user.save (err, user) =>
if err
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
try
msg = "#{req.user.get('email')} paid #{payment.get('amount')} for year campaign subscription"
slack.sendSlackMessage msg, ['tower']
catch error
@logSubscriptionError(req.user, "Year sub sale Slack tower msg error: #{JSON.stringify(error)}")
@sendSuccess(res, user)
subscribeWithPrepaidCode: (req, res) ->
return @sendUnauthorizedError(res) unless req.user?
return @sendBadInputError(res,"You must provide a valid prepaid code") unless req.body?.ppc
# Check if code exists and has room for more redeemers
Prepaid.findOne({ code: req.body.ppc?.toString() }).exec (err, prepaid) =>
if err
@logSubscriptionError(req.user, "Redeem Prepaid Code find: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
unless prepaid
@logSubscriptionError(req.user, "Could not find prepaid code #{req.body.ppc?.toString()}")
return @sendNotFoundError(res, "Prepaid not found")
oldRedeemers = prepaid.get('redeemers') ? []
return @sendError(res, 403, "Too many redeemers") if oldRedeemers.length >= prepaid.get('maxRedeemers')
months = parseInt(prepaid.get('properties')?.months)
return @sendBadInputError(res, "Bad months") if isNaN(months) or months < 1
for redeemer in oldRedeemers
return @sendError(res, 403, "User already redeemed") if redeemer.userID.equals(req.user._id)
@redeemPrepaidCode(req, res, months)
redeemPrepaidCode: (req, res, months) =>
return @sendUnauthorizedError(res) unless req.user?
return @sendForbiddenError(res) unless req.body?.ppc
return @sendForbiddenError(res) if isNaN(months) or months < 1
newRedeemerPush = { $push: { redeemers : { date: new Date(), userID: req.user._id } }}
Prepaid.update { 'code': req.body.ppc, 'redeemers.userID': { $ne: req.user._id }, '$where': 'this.redeemers.length < this.maxRedeemers'}, newRedeemerPush, (err, result) =>
if err
@logSubscriptionError(req.user, "Subscribe with Prepaid Code update: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
return @sendError(res, 403, "Can't add user to prepaid redeemers") if result.nModified isnt 1
customerID = req.user.get('stripe')?.customerID
subscriptionID = req.user.get('stripe')?.subscriptionID
findStripeSubscription customerID, subscriptionID: subscriptionID, (subscription) =>
stripeSubscriptionPeriodEndDate = new Date(subscription.current_period_end * 1000) if subscription
@cancelSubscriptionImmediately req.user, subscription, (err) =>
if err
@logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
# Add terminal subscription to User, extending existing subscriptions
# TODO: refactor this into some form useable by both this and purchaseYearSale
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
endDate = new moment()
if stripeSubscriptionPeriodEndDate
endDate = new moment(stripeSubscriptionPeriodEndDate)
else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free))
endDate = new moment(stripeInfo.free)
endDate = endDate.add(months, 'months')
stripeInfo.free = endDate.toISOString().substring(0, 10)
req.user.set('stripe', stripeInfo)
# Add gems to User
purchased = _.clone(req.user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems') * months if product.get('gems')
req.user.set('purchased', purchased)
req.user.save (err, user) =>
if err
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
@sendSuccess(res, user)
subscribeUser: (req, user, done) ->
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
return done({res: 'You must be signed in to subscribe.', code: 403})
if not req.user.get('email')
return done({res: 'Your account needs an email address to subscribe.', code: 403})
token = req.body.stripe.token
prepaidCode = req.body.stripe.prepaidCode
customerID = user.get('stripe')?.customerID
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})
# 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})
@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
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
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})
@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
if user.get('stripe')?.sponsorID
return done({res: 'You already have a sponsored subscription.', code: 403})
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'
if prepaid.get('redeemers')?.length >= prepaid.get('maxRedeemers')
@logSubscriptionError(user, "Prepaid #{prepaid.id} note active")
return done({res: 'Prepaid not active', code: 403})
unless couponID = prepaid.get('properties')?.couponID
@logSubscriptionError(user, "Prepaid #{prepaid.id} has no couponID")
return done({res: 'Database error.', code: 500})
redeemers = prepaid.get('redeemers') ? []
if _.find(redeemers, (a) -> a.userID?.equals(user.get('_id')))
@logSubscriptionError(user, "Prepaid code already redeemed by #{user.id}")
return done({res: 'Prepaid code already redeemed', code: 403})
# Redeem prepaid code
query = Prepaid.$where("'#{prepaid.get('_id').valueOf()}' === this._id.valueOf() && (!this.redeemers || this.redeemers.length < this.maxRedeemers)")
redeemers.push
userID: user.get('_id')
date: new Date()
update = {redeemers: redeemers}
Prepaid.update query, update, {}, (err, result) =>
if err
@logSubscriptionError(user, 'Prepaid update error. ' + err)
return done({res: 'Database error.', code: 500})
if result.nModified > 1
@logSubscriptionError(user, "Prepaid nModified=#{result.nModified} error.")
return done({res: 'Database error.', code: 500})
if result.nModified < 1
return done({res: 'Prepaid not active', code: 403})
# 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
if user.get('country') is 'brazil'
couponID ?= 'brazil'
# 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
if subscription.cancel_at_period_end
# Things are a little tricky here. Can't re-enable a cancelled subscription,
# so it needs to be deleted, but also don't want to charge for the new subscription immediately.
# So delete the cancelled subscription (no at_period_end given here) and give the new
# subscription a trial period that ends when the cancelled subscription would have ended.
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
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 resetting 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
# Skip creating the subscription
@updateUser(req, user, customer, subscription, false, done)
else
options = { plan: 'basic', metadata: {id: user.id}}
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, true, done)
updateUser: (req, user, customer, subscription, increment, done) ->
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.planID = 'basic'
stripeInfo.subscriptionID = subscription.id
stripeInfo.customerID = customer.id
# To make sure things work for admins, who are mad with power
# And, so Handler.saveChangesToDocument doesn't undo all our saves here
req.body.stripe = stripeInfo
user.set('stripe', stripeInfo)
productName = 'basic_subscription'
if user.get('country') in ['brazil']
productName = "#{user.get('country')}_basic_subscription"
Product.findOne({name: productName}).exec (err, product) =>
return done({res: 'Database error.', code: 500}) if err
return done({res: 'basic_subscription product not found.', code: 404}) if not product
if increment
purchased = _.clone(user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems') if product.get('gems')
user.set('purchased', purchased)
user.save (err) =>
if err
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
return done({res: 'Database error.', code: 500})
done()
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
emails = req.body.stripe.subscribeEmails.map((email) -> email.trim().toLowerCase() unless _.isEmpty(email))
_.remove(emails, (email) -> _.isEmpty(email))
User.find {emailLower: {$in: emails}}, (err, recipients) =>
if err
@logSubscriptionError(user, "User lookup error. " + err)
return done({res: 'Database error.', code: 500})
createUpdateFn = (recipient) =>
(done) =>
# Find existing recipient subscription
findStripeSubscription customer.id, userID: recipient.id, (subscription) =>
if subscription
if subscription.cancel_at_period_end
# Things are a little tricky here. Can't re-enable a cancelled subscription,
# so it needs to be deleted, but also don't want to charge for the new subscription immediately.
# So delete the cancelled subscription (no at_period_end given here) and give the new
# subscription a trial period that ends when the cancelled subscription would have ended.
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
if err
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
return done({res: 'Database error.', code: 500})
options =
plan: 'basic'
coupon: recipientCouponID
metadata: {id: recipient.id}
trial_end: subscription.current_period_end
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
return done({res: 'Database error.', code: 500})
done(null, recipient: recipient, subscription: subscription, increment: false)
else
# Can skip creating the subscription
done(null, recipient: recipient, subscription: subscription, increment: false)
else
options =
plan: 'basic'
coupon: recipientCouponID
metadata: {id: recipient.id}
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
return done({res: 'Database error.', code: 500})
done(null, recipient: recipient, subscription: subscription, increment: true)
tasks = []
for recipient in recipients
continue if recipient.id is user.id
continue if recipient.get('stripe')?.subscriptionID?
continue if recipient.get('stripe')?.sponsorID? and recipient.get('stripe')?.sponsorID isnt user.id
tasks.push createUpdateFn(recipient)
# NOTE: async.parallel yields this error:
# Subscription Error: user23 (54fe3c8fea98978efa469f3b): 'Stripe new subscription error. Error: Request rate limit exceeded'
async.series tasks, (err, results) =>
return done(err) if err
@updateCocoRecipientSubscriptions(req, user, customer, results, done)
updateCocoRecipientSubscriptions: (req, user, customer, stripeRecipients, done) ->
# Update recipients list
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.recipients ?= []
stripeRecipientIDs = (sub.recipient.id for sub in stripeRecipients)
_.remove(stripeInfo.recipients, (s) -> s.userID in stripeRecipientIDs)
for sub in stripeRecipients
stripeInfo.recipients.push
userID: sub.recipient.id
subscriptionID: sub.subscription.id
couponID: recipientCouponID
# TODO: how does token get removed for personal subs?
delete stripeInfo.subscribeEmails
delete stripeInfo.token
req.body.stripe = stripeInfo
user.set('stripe', stripeInfo)
user.save (err) =>
if err
@logSubscriptionError(user, 'User saving stripe error. ' + err)
return done({res: 'Database error.', code: 500})
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
createUpdateFn = (recipient, increment) =>
(done) =>
# Update recipient
stripeInfo = _.cloneDeep(recipient.get('stripe') ? {})
stripeInfo.sponsorID = user.id
recipient.set 'stripe', stripeInfo
if increment
purchased = _.clone(recipient.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += product.get('gems') if product.get('gems')
recipient.set('purchased', purchased)
recipient.save (err) =>
if err
@logSubscriptionError(user, 'Stripe user saving stripe error. ' + err)
return done({res: 'Database error.', code: 500})
done()
tasks = []
for sub in stripeRecipients
tasks.push createUpdateFn(sub.recipient, sub.increment)
async.parallel tasks, (err, results) =>
return done(err) if err
@updateStripeSponsorSubscription(req, user, customer, product, done)
updateStripeSponsorSubscription: (req, user, customer, product, done) ->
stripeInfo = user.get('stripe') ? {}
numSponsored = stripeInfo.recipients.length
quantity = getSponsoredSubsAmount(product.get('amount'), numSponsored, stripeInfo.subscriptionID?)
findStripeSubscription customer.id, subscriptionID: stripeInfo.sponsorSubscriptionID, (subscription) =>
if stripeInfo.sponsorSubscriptionID? and not subscription?
@logSubscriptionError(user, "Internal sponsor subscription #{stripeInfo.sponsorSubscriptionID} not found on Stripe customer #{customer.id}")
return done({res: 'Database error.', code: 500})
if subscription
return done() if quantity is subscription.quantity # E.g. cancelled sub has been resubbed
options = quantity: quantity
stripe.customers.updateSubscription customer.id, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Stripe updating subscription quantity error. ' + err)
return done({res: 'Database error.', code: 500})
# Invoice proration immediately
stripe.invoices.create customer: customer.id, (err, invoice) =>
if err
@logSubscriptionError(user, 'Stripe proration invoice error. ' + err)
return done({res: 'Database error.', code: 500})
done()
else
options =
plan: 'incremental'
metadata: {id: user.id}
quantity: quantity
stripe.customers.createSubscription customer.id, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Stripe new subscription error. ' + err)
return done({res: 'Database error.', code: 500})
@updateCocoSponsorSubscription(req, user, subscription, done)
updateCocoSponsorSubscription: (req, user, subscription, done) ->
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.sponsorSubscriptionID = subscription.id
req.body.stripe = stripeInfo
user.set('stripe', stripeInfo)
user.save (err) =>
if err
@logSubscriptionError(user, 'Saving user stripe error. ' + err)
return done({res: 'Database error.', code: 500})
done()
unsubscribeUser: (req, user, done) ->
# Check if user is subscribing someone else
return @unsubscribeRecipient(req, user, done) if req.body.stripe?.unsubscribeEmail?
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
if err
@logSubscriptionError(user, 'Stripe cancel subscription error. ' + err)
return done({res: 'Database error.', code: 500})
delete stripeInfo.planID
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
if err
@logSubscriptionError(user, 'User save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
done()
unsubscribeRecipient: (req, user, done) ->
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.unsubscribeEmail?
email = req.body.stripe.unsubscribeEmail.trim().toLowerCase()
return done({res: 'Database error.', code: 500}) if _.isEmpty(email)
deleteUserStripeProp = (user, propName) ->
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
delete stripeInfo[propName]
if _.isEmpty stripeInfo
user.set 'stripe', undefined
else
user.set 'stripe', stripeInfo
User.findOne {emailLower: email}, (err, recipient) =>
if err
@logSubscriptionError(user, "User lookup error. " + err)
return done({res: 'Database error.', code: 500})
unless recipient
@logSubscriptionError(user, "Recipient #{email} not found.")
return done({res: 'Database error.', code: 500})
# Check recipient is currently sponsored
stripeRecipient = recipient.get 'stripe' ? {}
if stripeRecipient?.sponsorID isnt user.id
@logSubscriptionError(user, "Recipient #{recipient.id} not sponsored by #{user.id}. ")
return done({res: 'Can only unsubscribe sponsored subscriptions.', code: 403})
# Find recipient subscription
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
for sponsored in stripeInfo.recipients
if sponsored.userID is recipient.id
sponsoredEntry = sponsored
break
unless sponsoredEntry?
@logSubscriptionError(user, 'Unable to find recipient subscription. ')
return done({res: 'Database error.', code: 500})
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
# Update recipient user
deleteUserStripeProp(recipient, 'sponsorID')
recipient.save (err) =>
if err
@logSubscriptionError(user, 'Recipient user save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
# Cancel Stripe subscription
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (err) =>
if err
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
return done({res: 'Database error.', code: 500})
# Update sponsor user
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
delete stripeInfo.unsubscribeEmail
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
if err
@logSubscriptionError(user, 'Sponsor user save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
return done() unless stripeInfo.sponsorSubscriptionID?
# Update sponsored subscription quantity
options =
quantity: getSponsoredSubsAmount(product.get('amount'), stripeInfo.recipients.length, stripeInfo.subscriptionID?)
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
if err
@logSubscriptionError(user, 'Sponsored subscription quantity update error. ' + JSON.stringify(err))
return done({res: 'Database error.', code: 500})
done()
module.exports = new SubscriptionHandler()