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_body: "Do you want to get more?"
prompt_button: "Enter Shop"
recovered: "Previous gems purchase recovered. Please refresh the page."
subscribe:
subscribe_title: "Subscribe"

View file

@ -29,3 +29,9 @@
span(aria-hidden="true") ×
p(data-i18n="loading_error.unknown")
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) ->
super(options)
@timestampForPurchase = new Date().getTime()
@state = 'standby'
if application.isIPadApp
@products = []
Backbone.Mediator.publish 'buy-gems-modal:update-products'
else
@products = @originalProducts
$.post '/db/payment/check-stripe-charges', (something, somethingElse, jqxhr) =>
if jqxhr.status is 201
@state = 'recovered_charge'
@render()
getRenderData: ->
c = super()
@ -67,7 +72,6 @@ module.exports = class BuyGemsModal extends ModalView
@productBeingPurchased = product
onStripeReceivedToken: (e) ->
@timestampForPurchase = new Date().getTime()
data = {
productID: @productBeingPurchased.id
stripe: {

View file

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

View file

@ -54,7 +54,10 @@ PaymentHandler = class PaymentHandler extends Handler
payment.set 'created', new Date().toISOString()
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()
return @sendForbiddenError(res)
@ -225,11 +228,11 @@ PaymentHandler = class PaymentHandler extends Handler
if not (payment or charge)
# Proceed normally from the beginning
@chargeStripe(req, res, payment, product)
@chargeStripe(req, res, product)
else if charge and not payment
# Initialized Payment. Start from charging.
@recordStripeCharge(req, res, payment, product, charge)
@recordStripeCharge(req, res, charge)
else
# 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({
amount: product.amount
currency: 'usd'
@ -258,7 +261,7 @@ PaymentHandler = class PaymentHandler extends Handler
receipt_email: req.user.get('email')
}).then(
# success case
((charge) => @recordStripeCharge(req, res, payment, product, charge)),
((charge) => @recordStripeCharge(req, res, charge)),
# error case
((err) =>
@ -270,19 +273,19 @@ 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
payment = @makeNewInstance(req)
payment.set 'service', 'stripe'
payment.set 'productID', req.body.productID
payment.set 'amount', product.amount
payment.set 'gems', product.gems
payment.set 'productID', charge.metadata.productID
payment.set 'amount', parseInt(charge.amount)
payment.set 'gems', parseInt(charge.metadata.gems)
payment.set 'stripe', {
customerID: req.user.get('stripe')?.customerID
timestamp: parseInt(req.body.stripe.timestamp)
customerID: charge.customer
timestamp: parseInt(charge.metadata.timestamp)
chargeID: charge.id
}
validation = @validateDocumentInput(payment.toObject())
if validation.valid is false
@logPaymentError(req, 'Invalid stripe payment object.')
@ -291,7 +294,7 @@ PaymentHandler = class PaymentHandler extends Handler
# Credit gems
return @sendDatabaseError(res, err) if err
@incrementGemsFor(req.user, product.gems, (err) =>
@incrementGemsFor(req.user, parseInt(charge.metadata.gems), (err) =>
if err
@logPaymentError(req, 'Stripe incr db error. '+err)
return @sendDatabaseError(res, err)
@ -299,6 +302,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

View file

@ -6,6 +6,7 @@ require '../common'
describe '/db/payment', ->
request = require 'request'
paymentURL = getURL('/db/payment')
checkChargesURL = getURL('/db/payment/check-stripe-charges')
firstApplePayment = {
apple: {
@ -47,7 +48,7 @@ describe '/db/payment', ->
expect(user.get('purchased').gems).toBe(5000)
done()
)
it 'is idempotent', (done) ->
loginJoe ->
request.post {uri: paymentURL, json: firstApplePayment}, (err, res, body) ->
@ -261,4 +262,37 @@ describe '/db/payment', ->
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()
)
)