Set up the server to allow admins to assign customers coupons. Fixed up how customers are created/updated by the server to allow setting new payment information rather than forever using the payment info first provided.

This commit is contained in:
Scott Erickson 2014-12-04 17:41:17 -08:00
parent bd797e8cfd
commit d0ee8cb7c7
7 changed files with 278 additions and 63 deletions

View file

@ -278,6 +278,9 @@ _.extend UserSchema.properties,
planID: { enum: ['basic'] }
subscriptionID: { type: 'string' }
token: { type: 'string' }
couponID: { type: 'string' }
discountID: { type: 'string' }
free: { type: ['boolean', 'string'], format: 'date-time' }
}
c.extendBasicProperties UserSchema, 'user'

View file

@ -0,0 +1,48 @@
# 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.
Handler = require '../commons/Handler'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
class DiscountHandler extends Handler
logDiscountError: (req, msg) ->
console.warn "Discount Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"
discountUser: (req, user, done) ->
if (not user) or user.isAnonymous()
return done({res: 'User must not be anonymous.', code: 403})
couponID = req.body.stripe.couponID
if not couponID
@logDiscountError(req, 'Missing couponID.')
return done({res: 'Missing couponID.', code: 422})
stripe.coupons.retrieve couponID, (err, coupon) =>
if (err)
return done({res: 'No coupon with id '+couponID, code: 404})
if customerID = user.get('stripe')?.customerID
options = { coupon: coupon.id }
stripe.customers.update customerID, options, (err, customer) =>
if err
@logDiscountError(req, 'Error applying coupon to customer'+customerID)
return done({res: 'Error applying coupon to customer.', code: 500})
done()
else
# couponID will be set on the user by the handler
done()
removeDiscountFromCustomer: (req, user, done) ->
customerID = user.get('stripe').customerID
return done() unless customerID
stripe.customers.deleteDiscount customerID, (err, customer) =>
if err
console.log 'err?', err
@logDiscountError(req, 'Error removing coupon from customer ' + customerID)
return done({res: 'Error applying coupon to customer.', code: 500})
done()
module.exports = new DiscountHandler()

View file

@ -166,29 +166,34 @@ PaymentHandler = class PaymentHandler extends Handler
handleStripePaymentPost: (req, res, timestamp, productID, token) ->
# First, make sure we save the payment info as a Customer object, if we haven't already.
if not req.user.get('stripe')?.customerID
stripe.customers.create({
if token
customerID = req.user.get('stripe')?.customerID
if customerID
# old customer, new token. Save it.
stripe.customers.update customerID, { card: token }, (err, customer) =>
@beginStripePayment(req, res, timestamp, productID)
else
newCustomer = {
card: token
email: req.user.get('email')
metadata: {
id: req.user._id + ''
slug: req.user.get('slug')
metadata: { id: req.user._id + '', slug: req.user.get('slug') }
}
}).then(((customer) =>
stripe.customers.create newCustomer, (err, customer) =>
if err
@logPaymentError(req, 'Stripe customer creation error. '+err)
return @sendDatabaseError(res, err)
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
stripeInfo.customerID = customer.id
req.user.set('stripe', stripeInfo)
req.user.save((err) =>
req.user.save (err) =>
if err
@logPaymentError(req, 'Stripe customer id save db error. '+err)
return @sendDatabaseError(res, err)
@beginStripePayment(req, res, timestamp, productID)
)
),
(err) =>
@logPaymentError(req, 'Stripe customer creation error. '+err)
return @sendDatabaseError(res, err)
)
else
@beginStripePayment(req, res, timestamp, productID)

View file

@ -4,6 +4,7 @@
Handler = require '../commons/Handler'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
discountHandler = require './discount_handler'
subscriptions = {
basic: {
@ -19,61 +20,52 @@ class SubscriptionHandler extends Handler
if (not req.user) or req.user.isAnonymous()
return done({res: 'You must be signed in to subscribe.', code: 403})
stripeToken = req.body.stripe?.token
extantCustomerID = user.get('stripe')?.customerID
if not (stripeToken or extantCustomerID)
token = req.body.stripe?.token
customerID = user.get('stripe')?.customerID
if not (token or customerID)
@logSubscriptionError(req, 'Missing stripe token or customer ID.')
return done({res: 'Missing stripe token or customer ID.', code: 422})
if stripeToken
stripe.customers.create({
card: stripeToken
if token
if customerID
stripe.customers.update customerID, { card: token }, (err, customer) =>
@checkForExistingSubscription(req, user, customer, done)
else
newCustomer = {
card: token
email: req.user.get('email')
metadata: {
id: req.user._id + ''
slug: req.user.get('slug')
metadata: { id: req.user._id + '', slug: req.user.get('slug') }
}
}).then(((customer) =>
stripe.customers.create newCustomer, (err, customer) =>
if err
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
return done({res: 'Card error', code: 402})
else
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
return done({res: 'Database error.', code: 500})
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
stripeInfo.customerID = customer.id
req.user.set('stripe', stripeInfo)
req.user.save((err) =>
req.user.save (err) =>
if err
@logSubscriptionError(req, 'Stripe customer id save db error. '+err)
return done({res: 'Database error.', code: 500})
@checkForExistingSubscription(req, user, customer, done)
)
),
(err) =>
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
done({res: 'Card error', code: 402})
else
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
return done({res: 'Database error.', code: 500})
)
else
stripe.customers.retrieve(extantCustomerID, (err, customer) =>
stripe.customers.retrieve(customerID, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
return done({res: 'Database error.', code: 500})
else if not customer
# TODO: what actually happens when you try to retrieve a customer and it DNE?
@logSubscriptionError(req, 'Stripe customer id is missing! '+err)
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
delete stripeInfo.customerID
req.user.set('stripe', stripeInfo)
req.user.save (err) =>
if err
@logSubscriptionError(req, 'Stripe customer id delete db error. '+err)
return done({res: 'Database error.', code: 500})
@subscribeUser(req, done)
else
@checkForExistingSubscription(req, user, customer, done)
)
checkForExistingSubscription: (req, user, customer, done) ->
couponID = user.get('stripe')?.couponID
if subscription = customer.subscriptions?.data?[0]
if subscription.cancel_at_period_end
@ -87,30 +79,34 @@ class SubscriptionHandler extends Handler
return done({res: 'Database error.', code: 500})
options = { plan: 'basic', trial_end: subscription.current_period_end }
options.coupon = couponID if couponID
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
return done({res: 'Database error.', code: 500})
@updateUser(req, user, customer.subscriptions.data[0], false, done)
@updateUser(req, user, customer, false, done)
else
# can skip creating the subscription
return @updateUser(req, user, customer.subscriptions.data[0], false, done)
return @updateUser(req, user, customer, false, done)
else
stripe.customers.update req.user.get('stripe').customerID, { plan: 'basic' }, (err, customer) =>
options = { plan: 'basic' }
options.coupon = couponID if couponID
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
return done({res: 'Database error.', code: 500})
@updateUser(req, user, customer.subscriptions.data[0], true, done)
@updateUser(req, user, customer, true, done)
updateUser: (req, user, subscription, increment, done) ->
updateUser: (req, user, customer, increment, done) ->
subscription = customer.subscriptions.data[0]
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.planID = 'basic'
stripeInfo.subscriptionID = subscription.id
stripeInfo.customerID = subscription.customer
stripeInfo.customerID = customer.id
req.body.stripe = stripeInfo # to make sure things work for admins, who are mad with power
user.set('stripe', stripeInfo)

View file

@ -12,6 +12,7 @@ moment = require 'moment'
LevelSession = require '../levels/sessions/LevelSession'
LevelSessionHandler = require '../levels/sessions/level_session_handler'
SubscriptionHandler = require '../payments/subscription_handler'
DiscountHandler = require '../payments/discount_handler'
EarnedAchievement = require '../achievements/EarnedAchievement'
UserRemark = require './remarks/UserRemark'
{isID} = require '../lib/utils'
@ -123,6 +124,25 @@ UserHandler = class UserHandler extends Handler
return callback(err) if err
return callback(null, req, user)
)
# Discount setting
(req, user, callback) ->
return callback(null, req, user) unless req.user?.isAdmin()
hasCoupon = user.get('stripe')?.couponID
wantsCoupon = req.body.stripe?.couponID
return callback(null, req, user) if hasCoupon is wantsCoupon
if wantsCoupon and (hasCoupon isnt wantsCoupon)
DiscountHandler.discountUser(req, user, (err) ->
return callback(err) if err
return callback(null, req, user)
)
else if hasCoupon and not wantsCoupon
DiscountHandler.removeDiscountFromCustomer(req, user, (err) ->
return callback(err) if err
return callback(null, req, user)
)
]
getById: (req, res, id) ->

View file

@ -0,0 +1,114 @@
config = require '../../../server_config'
require '../common'
# sample data that comes in through the webhook when you subscribe
describe '/db/user, editing stripe.couponID property', ->
stripe = require('stripe')(config.stripe.secretKey)
userURL = getURL('/db/user')
webhookURL = getURL('/stripe/webhook')
it 'clears the db first', (done) ->
clearModels [User, Payment], (err) ->
throw err if err
done()
#- shared data between tests
joeData = null
firstSubscriptionID = null
it 'does not work for non-admins', (done) ->
loginJoe (joe) ->
joeData = joe.toObject()
expect(joeData.stripe).toBeUndefined()
joeData.stripe = { couponID: '20pct' }
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200) # fails silently
expect(res.body.stripe).toBeUndefined() # but still fails
done()
it 'does not work with invalid coupons', (done) ->
loginAdmin (admin) ->
joeData.stripe = { couponID: 'DNE' }
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(404)
done()
it 'sets the couponID on a user without an existing stripe object', (done) ->
joeData.stripe = { couponID: '20pct' }
request.put {uri: userURL, json: joeData }, (err, res, body) ->
joeData = body
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('20pct')
done()
it 'just updates the couponID when it changes and there is no existing subscription', (done) ->
joeData.stripe.couponID = '500off'
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('500off')
done()
it 'removes the couponID from the user when the admin makes it so', (done) ->
delete joeData.stripe.couponID
request.put {uri: userURL, json: joeData }, (err, res, body) ->
joeData = body
expect(res.statusCode).toBe(200)
expect(body.stripe).toBeUndefined()
done()
it 'puts the coupon back', (done) ->
joeData.stripe = {couponID: '500off'}
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('500off')
done()
it 'applies a discount to the newly created customer when a plan is set', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
stripeTokenID = token.id
loginJoe (joe) ->
joeData.stripe.token = stripeTokenID
joeData.stripe.planID = 'basic'
request.put {uri: userURL, json: joeData }, (err, res, body) ->
joeData = body
expect(res.statusCode).toBe(200)
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount).toBeDefined()
expect(customer.discount.coupon.id).toBe('500off')
done()
it 'updates the discount on the customer when an admin changes the couponID', (done) ->
loginAdmin (admin) ->
joeData.stripe.couponID = '20pct'
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('20pct')
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount.coupon.id).toBe('20pct')
done()
it 'removes discounts from the customer when an admin removes the couponID', (done) ->
delete joeData.stripe.couponID
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBeUndefined()
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount).toBe(null)
done()
it 'adds a discount to the customer when an admin adds the couponID', (done) ->
joeData.stripe.couponID = '20pct'
request.put {uri: userURL, json: joeData }, (err, res, body) ->
expect(res.statusCode).toBe(200)
expect(body.stripe.couponID).toBe('20pct')
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.discount.coupon.id).toBe('20pct')
done()

View file

@ -82,6 +82,7 @@ describe '/db/payment', ->
joeID = null
timestamp = new Date().getTime()
stripeTokenID = null
joeData = null
it 'clears the db first', (done) ->
clearModels [User, Payment], (err) ->
@ -132,6 +133,34 @@ describe '/db/payment', ->
)
)
it 'allows a new charge on the existing customer', (done) ->
data = { productID: 'gems_5', stripe: { timestamp: new Date().getTime() } }
request.post {uri: paymentURL, json: data }, (err, res, body) ->
expect(res.statusCode).toBe 201
Payment.count {}, (err, count) ->
expect(count).toBe(2)
User.findById joeID, (err, user) ->
joeData = user.toObject()
expect(user.get('purchased').gems).toBe(10000)
done()
it "updates the customer's card when you submit a new token", (done) ->
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
originalCustomerID = customer.id
originalCardID = customer.cards.data[0].id
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
data = { productID: 'gems_5', stripe: { timestamp: new Date().getTime(), token: token.id } }
request.post {uri: paymentURL, json: data }, (err, res, body) ->
expect(res.statusCode).toBe(201)
User.findById joeID, (err, user) ->
joeData = user.toObject()
expect(joeData.stripe.customerID).toBe(originalCustomerID)
stripe.customers.retrieve joeData.stripe.customerID, (err, customer) ->
expect(customer.cards.data[0].id).not.toBe(originalCardID)
done()
it 'clears the db', (done) ->
clearModels [User, Payment], (err) ->
throw err if err