2014-11-11 20:40:03 -05:00
Payment = require ' ./Payment '
User = require ' ../users/User '
Handler = require ' ../commons/Handler '
{ handlers } = require ' ../commons/mapping '
mongoose = require ' mongoose '
log = require ' winston '
sendwithus = require ' ../sendwithus '
hipchat = require ' ../hipchat '
config = require ' ../../server_config '
request = require ' request '
2014-11-17 18:15:02 -05:00
async = require ' async '
2015-12-09 17:27:10 -05:00
apple_utils = require ' ../lib/apple_utils '
2014-11-11 20:40:03 -05:00
products = {
' gems_5 ' : {
2014-11-17 18:15:02 -05:00
amount: 499
2014-11-11 20:40:03 -05:00
gems: 5000
2014-11-17 18:15:02 -05:00
id: ' gems_5 '
2014-11-11 20:40:03 -05:00
}
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
' gems_10 ' : {
2014-11-17 18:15:02 -05:00
amount: 999
2014-11-11 20:40:03 -05:00
gems: 11000
2014-11-17 18:15:02 -05:00
id: ' gems_10 '
2014-11-11 20:40:03 -05:00
}
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
' gems_20 ' : {
2014-11-17 18:15:02 -05:00
amount: 1999
2014-11-11 20:40:03 -05:00
gems: 25000
2014-11-17 18:15:02 -05:00
id: ' gems_20 '
2014-11-11 20:40:03 -05:00
}
2015-03-04 18:40:32 -05:00
' custom ' : {
# amount expected in request body
id: ' custom '
}
2014-11-11 20:40:03 -05:00
}
PaymentHandler = class PaymentHandler extends Handler
modelClass: Payment
editableProperties: [ ]
postEditableProperties: [ ' purchased ' ]
jsonSchema: require ' ../../app/schemas/models/payment.schema '
2015-02-04 19:17:53 -05:00
2014-11-29 13:44:58 -05:00
get: (req, res) ->
return res . send ( [ ] ) unless req . user
q = Payment . find ( { recipient : req . user . _id } )
q . exec ( (err, payments) ->
return @ sendDatabaseError ( res , err ) if err
res . send ( payments )
)
2015-02-04 19:17:53 -05:00
2014-11-29 13:44:58 -05:00
logPaymentError: (req, msg) ->
console . warn " Payment Error: #{ req . user . get ( ' slug ' ) } ( #{ req . user . _id } ): ' #{ msg } ' "
2014-11-11 20:40:03 -05:00
makeNewInstance: (req) ->
payment = super ( req )
payment . set ' purchaser ' , req . user . _id
payment . set ' recipient ' , req . user . _id
payment . set ' created ' , new Date ( ) . toISOString ( )
payment
2014-11-25 16:20:09 -05:00
2014-12-12 18:27:58 -05:00
post: (req, res, pathName) ->
if pathName is ' check-stripe-charges '
return @ checkStripeCharges ( req , res )
2015-02-04 19:17:53 -05:00
2014-12-04 16:07:00 -05:00
if ( not req . user ) or req . user . isAnonymous ( )
return @ sendForbiddenError ( res )
2015-02-04 19:17:53 -05:00
2014-11-11 20:40:03 -05:00
appleReceipt = req . body . apple ? . rawReceipt
appleTransactionID = req . body . apple ? . transactionID
appleLocalPrice = req . body . apple ? . localPrice
stripeToken = req . body . stripe ? . token
stripeTimestamp = parseInt ( req . body . stripe ? . timestamp )
2014-11-17 18:15:02 -05:00
productID = req . body . productID
2014-11-25 16:20:09 -05:00
2015-03-04 18:40:32 -05:00
if pathName is ' custom '
return @ handleStripePaymentPost ( req , res , stripeTimestamp , ' custom ' , stripeToken )
2014-11-17 18:15:02 -05:00
if not ( appleReceipt or ( stripeTimestamp and productID ) )
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , " Missing data. Apple? #{ ! ! appleReceipt } . Stripe timestamp? #{ ! ! stripeTimestamp } . Product id? #{ ! ! productID } . " )
2014-11-17 18:15:02 -05:00
return @ sendBadInputError ( res , ' Need either apple.rawReceipt or stripe.timestamp and productID ' )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
if stripeTimestamp and not productID
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Missing stripe productID ' )
2014-11-17 18:15:02 -05:00
return @ sendBadInputError ( res , ' Need productID if paying with Stripe. ' )
2014-12-02 14:47:15 -05:00
if stripeTimestamp and ( not stripeToken ) and ( not req . user . get ( ' stripe ' ) ? . customerID )
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Missing stripe token ' )
2014-11-17 18:15:02 -05:00
return @ sendBadInputError ( res , ' Need stripe.token if new customer. ' )
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
if appleReceipt
if not appleTransactionID
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Missing apple transaction id ' )
2014-11-11 20:40:03 -05:00
return @ sendBadInputError ( res , ' Apple purchase? Need to specify which transaction. ' )
@ handleApplePaymentPost ( req , res , appleReceipt , appleTransactionID , appleLocalPrice )
else
2014-11-17 18:15:02 -05:00
@ handleStripePaymentPost ( req , res , stripeTimestamp , productID , stripeToken )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
#- Apple payments
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
2014-11-25 16:20:09 -05:00
#- verify receipt with Apple
2015-12-09 17:27:10 -05:00
apple_utils . verifyReceipt ( receipt , (err, body) =>
2014-11-11 20:40:03 -05:00
if err or not body ? . receipt ? . in_app or ( not body ? . bundle_id is ' com.codecombat.CodeCombat ' )
console . warn ' apple receipt error? ' , err , body
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Unable to verify apple receipt ' )
2014-11-11 20:40:03 -05:00
@ sendBadInputError ( res , ' Unable to verify Apple receipt. ' )
return
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
transaction = _ . find body . receipt . in_app , { transaction_id: transactionID }
2014-11-29 13:44:58 -05:00
unless transaction
@ logPaymentError ( req , ' Missing transaction given id. ' )
return @ sendBadInputError ( res , ' Invalid transactionID. ' )
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
#- Check existence
transactionID = transaction . transaction_id
2014-11-21 13:34:30 -05:00
criteria = { ' ios.transactionID ' : transactionID }
2014-11-11 20:40:03 -05:00
Payment . findOne ( criteria ) . exec ( (err, payment) =>
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
if payment
2014-11-21 13:34:30 -05:00
unless payment . get ( ' recipient ' ) . equals ( req . user . _id )
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Cross user apple payment. ' )
2014-11-21 13:34:30 -05:00
return @ sendForbiddenError ( res )
2014-11-11 20:40:03 -05:00
@ recalculateGemsFor ( req . user , (err) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Apple recalc db error. ' + err )
return @ sendDatabaseError ( res , err )
2014-11-11 20:40:03 -05:00
@ sendSuccess ( res , @ formatEntity ( req , payment ) )
)
return
payment = @ makeNewInstance ( req )
payment . set ' service ' , ' ios '
product = products [ transaction . product_id ]
payment . set ' amount ' , product . amount
payment . set ' gems ' , product . gems
payment . set ' ios ' , {
transactionID: transactionID
rawReceipt: receipt
localPrice: localPrice
}
validation = @ validateDocumentInput ( payment . toObject ( ) )
2014-11-29 13:44:58 -05:00
if validation . valid is false
@ logPaymentError ( req , ' Invalid apple payment object. ' )
return @ sendBadInputError ( res , validation . errors )
2015-02-04 19:17:53 -05:00
2014-11-11 20:40:03 -05:00
payment . save ( (err) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Apple payment save error. ' + err )
return @ sendDatabaseError ( res , err )
2014-11-11 20:40:03 -05:00
@ incrementGemsFor ( req . user , product . gems , (err) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Apple incr db error. ' + err )
return @ sendDatabaseError ( res , err )
2014-11-25 16:20:09 -05:00
@ sendPaymentHipChatMessage user: req . user , payment: payment
2014-11-11 20:40:03 -05:00
@ sendCreated ( res , @ formatEntity ( req , payment ) )
)
)
)
)
2014-11-17 18:15:02 -05:00
#- Stripe payments
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
handleStripePaymentPost: (req, res, timestamp, productID, token) ->
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
# First, make sure we save the payment info as a Customer object, if we haven't already.
2014-12-04 20:41:17 -05:00
if token
customerID = req . user . get ( ' stripe ' ) ? . customerID
2015-02-04 19:17:53 -05:00
2014-12-04 20:41:17 -05:00
if customerID
# old customer, new token. Save it.
stripe . customers . update customerID , { card: token } , (err, customer) =>
@ beginStripePayment ( req , res , timestamp , productID )
2015-02-04 19:17:53 -05:00
2014-12-04 20:41:17 -05:00
else
newCustomer = {
card: token
email: req . user . get ( ' email ' )
metadata: { id: req . user . _id + ' ' , slug: req . user . get ( ' slug ' ) }
2014-12-03 19:35:53 -05:00
}
2015-02-04 19:17:53 -05:00
2014-12-04 20:41:17 -05:00
stripe . customers . create newCustomer , (err, customer) =>
2014-11-29 13:44:58 -05:00
if err
2014-12-04 20:41:17 -05:00
@ logPaymentError ( req , ' Stripe customer creation error. ' + err )
2014-11-29 13:44:58 -05:00
return @ sendDatabaseError ( res , err )
2015-02-04 19:17:53 -05:00
2014-12-04 20:41:17 -05:00
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 )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
else
@ beginStripePayment ( req , res , timestamp , productID )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
beginStripePayment: (req, res, timestamp, productID) ->
product = products [ productID ]
async . parallel ( [
( (callback) ->
criteria = { recipient: req . user . _id , ' stripe.timestamp ' : timestamp }
Payment . findOne ( criteria ) . exec ( (err, payment) =>
callback ( err , payment )
)
) ,
( (callback) ->
2014-12-02 14:47:15 -05:00
stripe . charges . list ( { customer: req . user . get ( ' stripe ' ) ? . customerID } , (err, recentCharges) =>
2014-11-17 18:15:02 -05:00
return callback ( err ) if err
charge = _ . find recentCharges . data , (c) -> c . metadata . timestamp is timestamp
callback ( null , charge )
)
)
] ,
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
( (err, results) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Stripe async load db error. ' + err )
2015-02-04 19:17:53 -05:00
return @ sendDatabaseError ( res , err )
2014-11-17 18:15:02 -05:00
[ payment , charge ] = results
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
if not ( payment or charge )
# Proceed normally from the beginning
2014-12-12 18:27:58 -05:00
@ chargeStripe ( req , res , product )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
else if charge and not payment
# Initialized Payment. Start from charging.
2014-12-12 18:27:58 -05:00
@ recordStripeCharge ( req , res , charge )
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
else
2015-03-04 18:40:32 -05:00
return @ sendSuccess ( res , @ formatEntity ( req , payment ) ) if product . id is ' custom '
2014-11-17 18:15:02 -05:00
# Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
@ recalculateGemsFor ( req . user , (err) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Stripe recalc db error. ' + err )
return @ sendDatabaseError ( res , err )
2014-11-25 16:20:09 -05:00
@ sendPaymentHipChatMessage user: req . user , payment: payment
2014-11-17 18:15:02 -05:00
@ sendSuccess ( res , @ formatEntity ( req , payment ) )
)
)
)
2014-12-12 18:27:58 -05:00
chargeStripe: (req, res, product) ->
2015-03-04 18:40:32 -05:00
amount = parseInt product . amount ? req . body . amount
return @ sendError ( res , 400 , " Invalid amount. " ) if isNaN ( amount )
2014-11-17 18:15:02 -05:00
stripe . charges . create ( {
2015-03-04 18:40:32 -05:00
amount: amount
2014-11-17 18:15:02 -05:00
currency: ' usd '
2014-12-02 14:47:15 -05:00
customer: req . user . get ( ' stripe ' ) ? . customerID
2014-11-17 18:15:02 -05:00
metadata: {
productID: product . id
userID: req . user . _id + ' '
gems: product . gems
timestamp: parseInt ( req . body . stripe ? . timestamp )
2015-03-04 18:40:32 -05:00
description: req . body . description
2014-11-17 18:15:02 -05:00
}
receipt_email: req . user . get ( ' email ' )
2015-06-05 17:58:35 -04:00
statement_descriptor: ' CODECOMBAT.COM '
2014-11-17 18:15:02 -05:00
} ) . then (
# success case
2014-12-12 18:27:58 -05:00
( (charge) => @ recordStripeCharge ( req , res , charge ) ) ,
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
# error case
( (err) =>
if err . type in [ ' StripeCardError ' , ' StripeInvalidRequestError ' ]
@ sendError ( res , 402 , err . message )
else
2014-11-29 13:44:58 -05:00
@ logPaymentError ( req , ' Stripe charge error. ' + err )
2014-11-17 18:15:02 -05:00
@ sendDatabaseError ( res , ' Error charging card, please retry. ' ) )
)
2014-11-25 16:20:09 -05:00
2014-12-12 18:27:58 -05:00
recordStripeCharge: (req, res, charge) ->
2014-11-17 18:15:02 -05:00
return @ sendError ( res , 500 , ' Fake db error for testing. ' ) if req . body . breakAfterCharging
payment = @ makeNewInstance ( req )
payment . set ' service ' , ' stripe '
2014-12-12 18:27:58 -05:00
payment . set ' productID ' , charge . metadata . productID
payment . set ' amount ' , parseInt ( charge . amount )
2015-03-04 18:40:32 -05:00
payment . set ' gems ' , parseInt ( charge . metadata . gems ) if charge . metadata . gems
payment . set ' description ' , charge . metadata . description if charge . metadata . description
2014-11-17 18:15:02 -05:00
payment . set ' stripe ' , {
2014-12-12 18:27:58 -05:00
customerID: charge . customer
timestamp: parseInt ( charge . metadata . timestamp )
2014-11-17 18:15:02 -05:00
chargeID: charge . id
}
2015-02-04 19:17:53 -05:00
2014-11-17 18:15:02 -05:00
validation = @ validateDocumentInput ( payment . toObject ( ) )
2014-11-29 13:44:58 -05:00
if validation . valid is false
@ logPaymentError ( req , ' Invalid stripe payment object. ' )
return @ sendBadInputError ( res , validation . errors )
2014-11-17 18:15:02 -05:00
payment . save ( (err) =>
2015-03-04 18:40:32 -05:00
return @ sendDatabaseError ( res , err ) if err
return @ sendCreated ( res , @ formatEntity ( req , payment ) ) if payment . productID is ' custom '
2014-11-17 18:15:02 -05:00
# Credit gems
2014-12-12 18:27:58 -05:00
@ incrementGemsFor ( req . user , parseInt ( charge . metadata . gems ) , (err) =>
2014-11-29 13:44:58 -05:00
if err
@ logPaymentError ( req , ' Stripe incr db error. ' + err )
return @ sendDatabaseError ( res , err )
2015-08-26 18:37:58 -04:00
@ sendPaymentHipChatMessage user: req . user , payment: payment
2014-11-17 18:15:02 -05:00
@ sendCreated ( res , @ formatEntity ( req , payment ) )
)
)
2014-12-12 18:27:58 -05:00
#- Confirm all Stripe charges are recorded on our server
2015-02-04 19:17:53 -05:00
2014-12-12 18:27:58 -05:00
checkStripeCharges: (req, res) ->
return @ sendSuccess ( res ) unless customerID = req . user . get ( ' stripe ' ) ? . customerID
async . parallel ( [
( (callback) ->
2015-02-09 11:31:44 -05:00
criteria = { recipient: req . user . _id , ' stripe.invoiceID ' : { $exists: false } , ' ios.transactionID ' : { $exists: false } }
2014-12-12 18:27:58 -05:00
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
2015-05-19 14:41:11 -04:00
continue unless charge . paid
2014-12-12 18:27:58 -05:00
continue if charge . invoice # filter out subscription charges
if charge . id not in recordedChargeIDs
return @ recordStripeCharge ( req , res , charge )
@ sendSuccess ( res )
)
)
2014-11-25 16:20:09 -05:00
2014-11-17 18:15:02 -05:00
#- Incrementing/recalculating gems
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
incrementGemsFor: (user, gems, done) ->
purchased = _ . clone ( user . get ( ' purchased ' ) )
if not purchased ? . gems
purchased ? = { }
purchased.gems = gems
user . set ( ' purchased ' , purchased )
user . save ( (err) -> done ( err ) )
2014-11-25 16:20:09 -05:00
2014-11-11 20:40:03 -05:00
else
user . update ( { $inc: { ' purchased.gems ' : gems } } , { } , (err) -> done ( err ) )
2014-11-25 16:20:09 -05:00
2015-02-20 14:11:12 -05:00
recalculateGemsFor: (user, done, saveIfUnchanged=true) ->
2014-11-11 20:40:03 -05:00
Payment . find ( { recipient: user . _id } ) . select ( ' gems ' ) . exec ( (err, payments) ->
gems = _ . reduce payments , ( (sum, p) -> sum + p . get ( ' gems ' ) ) , 0
purchased = _ . clone ( user . get ( ' purchased ' ) )
purchased ? = { }
2015-02-20 14:11:12 -05:00
if ( purchased . gems or 0 ) isnt gems
log . debug " Updating #{ user . get ( ' _id ' ) } gems from #{ purchased . gems } to #{ gems } from #{ payments . length } payments; #{ user . get ( ' email ' ) } #{ user . get ( ' name ' ) } "
else unless saveIfUnchanged
log . debug " #{ user . get ( ' _id ' ) } already had #{ purchased . gems } #{ gems } from #{ payments . length } payments; #{ user . get ( ' email ' ) } #{ user . get ( ' name ' ) } "
return done ( )
2014-11-11 20:40:03 -05:00
purchased.gems = gems
user . set ( ' purchased ' , purchased )
user . save ( (err) -> done ( err ) )
)
2014-11-25 16:20:09 -05:00
sendPaymentHipChatMessage: (options) ->
try
2015-09-03 16:35:20 -04:00
message = " #{ options . user ? . get ( ' name ' ) } bought #{ options . payment ? . get ( ' amount ' ) } via #{ options . payment ? . get ( ' service ' ) } "
message += " for #{ options . payment . get ( ' description ' ) } " if options . payment ? . get ( ' description ' )
2015-02-04 19:17:53 -05:00
hipchat . sendHipChatMessage message , [ ' tower ' ]
2014-11-25 16:20:09 -05:00
catch e
log . error " Couldn ' t send HipChat message on payment because of error: #{ e } "
2014-11-11 20:40:03 -05:00
module.exports = new PaymentHandler ( )