Shored up stripe transaction payment defences to try and fix #1906.

This commit is contained in:
Scott Erickson 2014-12-12 15:27:58 -08:00
parent e5c5f0ba04
commit b0de331a10
6 changed files with 103 additions and 20 deletions

View file

@ -333,6 +333,7 @@
prompt_title: "Not Enough Gems" prompt_title: "Not Enough Gems"
prompt_body: "Do you want to get more?" prompt_body: "Do you want to get more?"
prompt_button: "Enter Shop" prompt_button: "Enter Shop"
recovered: "Previous gems purchase recovered. Please refresh the page."
subscribe: subscribe:
subscribe_title: "Subscribe" subscribe_title: "Subscribe"

View file

@ -29,3 +29,9 @@
span(aria-hidden="true") × span(aria-hidden="true") ×
p(data-i18n="loading_error.unknown") p(data-i18n="loading_error.unknown")
p= stateMessage p= stateMessage
if state === 'recovered_charge'
#recovered-alert.alert.alert-danger.alert-dismissible
span(data-i18n="buy_gems.recovered")
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") ×

View file

@ -24,12 +24,17 @@ module.exports = class BuyGemsModal extends ModalView
constructor: (options) -> constructor: (options) ->
super(options) super(options)
@timestampForPurchase = new Date().getTime()
@state = 'standby' @state = 'standby'
if application.isIPadApp if application.isIPadApp
@products = [] @products = []
Backbone.Mediator.publish 'buy-gems-modal:update-products' Backbone.Mediator.publish 'buy-gems-modal:update-products'
else else
@products = @originalProducts @products = @originalProducts
$.post '/db/payment/check-stripe-charges', (something, somethingElse, jqxhr) =>
if jqxhr.status is 201
@state = 'recovered_charge'
@render()
getRenderData: -> getRenderData: ->
c = super() c = super()
@ -67,7 +72,6 @@ module.exports = class BuyGemsModal extends ModalView
@productBeingPurchased = product @productBeingPurchased = product
onStripeReceivedToken: (e) -> onStripeReceivedToken: (e) ->
@timestampForPurchase = new Date().getTime()
data = { data = {
productID: @productBeingPurchased.id productID: @productBeingPurchased.id
stripe: { stripe: {

View file

@ -88,15 +88,15 @@ module.exports = class Handler
sendError: (res, code, message) -> sendError: (res, code, message) ->
errors.custom(res, code, message) errors.custom(res, code, message)
sendSuccess: (res, message) -> sendSuccess: (res, message='{}') ->
res.send(message) res.send 200, message
res.end() res.end()
sendCreated: (res, message) -> sendCreated: (res, message='{}') ->
res.send 201, message res.send 201, message
res.end() res.end()
sendAccepted: (res, message) -> sendAccepted: (res, message='{}') ->
res.send 202, message res.send 202, message
res.end() res.end()

View file

@ -54,7 +54,10 @@ PaymentHandler = class PaymentHandler extends Handler
payment.set 'created', new Date().toISOString() payment.set 'created', new Date().toISOString()
payment payment
post: (req, res) -> post: (req, res, pathName) ->
if pathName is 'check-stripe-charges'
return @checkStripeCharges(req, res)
if (not req.user) or req.user.isAnonymous() if (not req.user) or req.user.isAnonymous()
return @sendForbiddenError(res) return @sendForbiddenError(res)
@ -225,11 +228,11 @@ PaymentHandler = class PaymentHandler extends Handler
if not (payment or charge) if not (payment or charge)
# Proceed normally from the beginning # Proceed normally from the beginning
@chargeStripe(req, res, payment, product) @chargeStripe(req, res, product)
else if charge and not payment else if charge and not payment
# Initialized Payment. Start from charging. # Initialized Payment. Start from charging.
@recordStripeCharge(req, res, payment, product, charge) @recordStripeCharge(req, res, charge)
else else
# Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase. # Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
@ -244,7 +247,7 @@ PaymentHandler = class PaymentHandler extends Handler
) )
chargeStripe: (req, res, payment, product) -> chargeStripe: (req, res, product) ->
stripe.charges.create({ stripe.charges.create({
amount: product.amount amount: product.amount
currency: 'usd' currency: 'usd'
@ -258,7 +261,7 @@ PaymentHandler = class PaymentHandler extends Handler
receipt_email: req.user.get('email') receipt_email: req.user.get('email')
}).then( }).then(
# success case # success case
((charge) => @recordStripeCharge(req, res, payment, product, charge)), ((charge) => @recordStripeCharge(req, res, charge)),
# error case # error case
((err) => ((err) =>
@ -270,16 +273,16 @@ PaymentHandler = class PaymentHandler extends Handler
) )
recordStripeCharge: (req, res, payment, product, charge) -> recordStripeCharge: (req, res, charge) ->
return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging
payment = @makeNewInstance(req) payment = @makeNewInstance(req)
payment.set 'service', 'stripe' payment.set 'service', 'stripe'
payment.set 'productID', req.body.productID payment.set 'productID', charge.metadata.productID
payment.set 'amount', product.amount payment.set 'amount', parseInt(charge.amount)
payment.set 'gems', product.gems payment.set 'gems', parseInt(charge.metadata.gems)
payment.set 'stripe', { payment.set 'stripe', {
customerID: req.user.get('stripe')?.customerID customerID: charge.customer
timestamp: parseInt(req.body.stripe.timestamp) timestamp: parseInt(charge.metadata.timestamp)
chargeID: charge.id chargeID: charge.id
} }
@ -291,7 +294,7 @@ PaymentHandler = class PaymentHandler extends Handler
# Credit gems # Credit gems
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@incrementGemsFor(req.user, product.gems, (err) => @incrementGemsFor(req.user, parseInt(charge.metadata.gems), (err) =>
if err if err
@logPaymentError(req, 'Stripe incr db error. '+err) @logPaymentError(req, 'Stripe incr db error. '+err)
return @sendDatabaseError(res, err) return @sendDatabaseError(res, err)
@ -300,6 +303,41 @@ PaymentHandler = class PaymentHandler extends Handler
) )
#- Confirm all Stripe charges are recorded on our server
checkStripeCharges: (req, res) ->
return @sendSuccess(res) unless customerID = req.user.get('stripe')?.customerID
async.parallel([
((callback) ->
criteria = { recipient: req.user._id, 'stripe.invoiceID': { $exists: false } }
Payment.find(criteria).limit(100).sort({_id:-1}).exec((err, payments) =>
callback(err, payments)
)
),
((callback) ->
stripe.charges.list({customer: customerID, limit: 100}, (err, recentCharges) =>
return callback(err) if err
callback(null, recentCharges.data)
)
)
],
((err, results) =>
if err
@logPaymentError(req, 'Stripe async load db error. '+err)
return @sendDatabaseError(res, err)
[payments, charges] = results
recordedChargeIDs = (p.get('stripe').chargeID for p in payments)
for charge in charges
continue if charge.invoice # filter out subscription charges
if charge.id not in recordedChargeIDs
return @recordStripeCharge(req, res, charge)
@sendSuccess(res)
)
)
#- Incrementing/recalculating gems #- Incrementing/recalculating gems
incrementGemsFor: (user, gems, done) -> incrementGemsFor: (user, gems, done) ->

View file

@ -6,6 +6,7 @@ require '../common'
describe '/db/payment', -> describe '/db/payment', ->
request = require 'request' request = require 'request'
paymentURL = getURL('/db/payment') paymentURL = getURL('/db/payment')
checkChargesURL = getURL('/db/payment/check-stripe-charges')
firstApplePayment = { firstApplePayment = {
apple: { apple: {
@ -261,4 +262,37 @@ describe '/db/payment', ->
done() done()
) )
describe '/db/payment/check-stripe-charges', ->
stripe = require('stripe')(config.stripe.secretKey)
it 'clears the db', (done) ->
clearModels [User, Payment], (err) ->
throw err if err
done()
it 'finds and records charges which are not in our db', (done) ->
timestamp = new Date().getTime()
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
data = {
productID: 'gems_5'
stripe: { token: token.id, timestamp: timestamp }
breakAfterCharging: true
}
loginJoe (joe) ->
request.post {uri: paymentURL, json: data }, (err, res, body) ->
expect(res.statusCode).toBe 500
request.post { uri: checkChargesURL }, (err, res, body) ->
expect(res.statusCode).toBe 201
Payment.count({}, (err, count) ->
expect(count).toBe(1)
User.findById(joe.get('_id'), (err, user) ->
expect(user.get('purchased').gems).toBe(5000)
done()
)
)