mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
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:
parent
bd797e8cfd
commit
d0ee8cb7c7
7 changed files with 278 additions and 63 deletions
|
@ -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'
|
||||
|
|
48
server/payments/discount_handler.coffee
Normal file
48
server/payments/discount_handler.coffee
Normal 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()
|
|
@ -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({
|
||||
card: token
|
||||
email: req.user.get('email')
|
||||
metadata: {
|
||||
id: req.user._id + ''
|
||||
slug: req.user.get('slug')
|
||||
}
|
||||
}).then(((customer) =>
|
||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||
stripeInfo.customerID = customer.id
|
||||
req.user.set('stripe', stripeInfo)
|
||||
req.user.save((err) =>
|
||||
if err
|
||||
@logPaymentError(req, 'Stripe customer id save db error. '+err)
|
||||
return @sendDatabaseError(res, err)
|
||||
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)
|
||||
)
|
||||
),
|
||||
(err) =>
|
||||
@logPaymentError(req, 'Stripe customer creation error. '+err)
|
||||
return @sendDatabaseError(res, err)
|
||||
)
|
||||
|
||||
else
|
||||
newCustomer = {
|
||||
card: token
|
||||
email: req.user.get('email')
|
||||
metadata: { id: req.user._id + '', slug: req.user.get('slug') }
|
||||
}
|
||||
|
||||
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) =>
|
||||
if err
|
||||
@logPaymentError(req, 'Stripe customer id save db error. '+err)
|
||||
return @sendDatabaseError(res, err)
|
||||
@beginStripePayment(req, res, timestamp, productID)
|
||||
|
||||
else
|
||||
@beginStripePayment(req, res, timestamp, productID)
|
||||
|
|
|
@ -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
|
||||
email: req.user.get('email')
|
||||
metadata: {
|
||||
id: req.user._id + ''
|
||||
slug: req.user.get('slug')
|
||||
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') }
|
||||
}
|
||||
}).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)
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
114
test/server/functional/discount_handler.spec.coffee
Normal file
114
test/server/functional/discount_handler.spec.coffee
Normal 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()
|
||||
|
|
@ -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) ->
|
||||
|
@ -131,6 +132,34 @@ describe '/db/payment', ->
|
|||
done()
|
||||
)
|
||||
)
|
||||
|
||||
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) ->
|
||||
|
|
Loading…
Reference in a new issue