mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Shored up stripe transaction payment defences to try and fix #1906.
This commit is contained in:
parent
e5c5f0ba04
commit
b0de331a10
6 changed files with 103 additions and 20 deletions
|
@ -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"
|
||||
|
|
|
@ -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") ×
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in a new issue