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_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"
|
||||||
|
|
|
@ -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") ×
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
validation = @validateDocumentInput(payment.toObject())
|
validation = @validateDocumentInput(payment.toObject())
|
||||||
if validation.valid is false
|
if validation.valid is false
|
||||||
@logPaymentError(req, 'Invalid stripe payment object.')
|
@logPaymentError(req, 'Invalid stripe payment object.')
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
#- Incrementing/recalculating gems
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
@ -47,7 +48,7 @@ describe '/db/payment', ->
|
||||||
expect(user.get('purchased').gems).toBe(5000)
|
expect(user.get('purchased').gems).toBe(5000)
|
||||||
done()
|
done()
|
||||||
)
|
)
|
||||||
|
|
||||||
it 'is idempotent', (done) ->
|
it 'is idempotent', (done) ->
|
||||||
loginJoe ->
|
loginJoe ->
|
||||||
request.post {uri: paymentURL, json: firstApplePayment}, (err, res, body) ->
|
request.post {uri: paymentURL, json: firstApplePayment}, (err, res, body) ->
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue