mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Built payment endpoint for processing Apple IAPs.
This commit is contained in:
parent
56e62bb4c8
commit
7012d5dfbe
8 changed files with 252 additions and 3 deletions
27
app/schemas/models/payment.schema.coffee
Normal file
27
app/schemas/models/payment.schema.coffee
Normal file
|
@ -0,0 +1,27 @@
|
|||
c = require './../schemas'
|
||||
|
||||
PaymentSchema = c.object({title: 'Payment', required: []}, {
|
||||
purchaser: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ]) # in case of gifts
|
||||
recipient: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
|
||||
|
||||
service: { enum: ['stripe', 'ios' ]}
|
||||
amount: { type: 'integer', description: 'Payment in cents.' }
|
||||
created: c.date({title: 'Created', readOnly: true})
|
||||
gems: { type: 'integer', description: 'The number of gems acquired.' }
|
||||
|
||||
ios: c.object({title: 'iOS IAP Data'}, {
|
||||
transactionID: { type: 'string' }
|
||||
rawReceipt: { type: 'string' }
|
||||
localPrice: { type: 'string' }
|
||||
})
|
||||
|
||||
stripe: c.object({title: 'Stripe Data'}, {
|
||||
timestamp: { type: 'integer', description: 'Unique identifier provided by the client, to guard against duplicate payments.' }
|
||||
transactionID: { type: 'string' }
|
||||
customerID: { type: 'string' }
|
||||
})
|
||||
})
|
||||
|
||||
c.extendBasicProperties(PaymentSchema, 'payment')
|
||||
|
||||
module.exports = PaymentSchema
|
|
@ -6,6 +6,7 @@ module.exports.handlers =
|
|||
'level_session': 'levels/sessions/level_session_handler'
|
||||
'level_system': 'levels/systems/level_system_handler'
|
||||
'patch': 'patches/patch_handler'
|
||||
'payment': 'payments/payment_handler'
|
||||
'purchase': 'purchases/purchase_handler'
|
||||
'thang_type': 'levels/thangs/thang_type_handler'
|
||||
'user': 'users/user_handler'
|
||||
|
|
6
server/payments/Payment.coffee
Normal file
6
server/payments/Payment.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
mongoose = require('mongoose')
|
||||
|
||||
PaymentSchema = new mongoose.Schema({}, {strict: false})
|
||||
PaymentSchema.index({recipient: 1, 'stripe.timestamp': 1, 'ios.transactionID'}, {unique: true, name: 'unique payment'})
|
||||
|
||||
module.exports = mongoose.model('payment', PaymentSchema)
|
141
server/payments/payment_handler.coffee
Normal file
141
server/payments/payment_handler.coffee
Normal file
|
@ -0,0 +1,141 @@
|
|||
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'
|
||||
|
||||
products = {
|
||||
'gems_5': {
|
||||
amount: 500
|
||||
gems: 5000
|
||||
}
|
||||
|
||||
'gems_10': {
|
||||
amount: 1000
|
||||
gems: 11000
|
||||
}
|
||||
|
||||
'gems_20': {
|
||||
amount: 2000
|
||||
gems: 25000
|
||||
}
|
||||
}
|
||||
|
||||
PaymentHandler = class PaymentHandler extends Handler
|
||||
modelClass: Payment
|
||||
editableProperties: []
|
||||
postEditableProperties: ['purchased']
|
||||
jsonSchema: require '../../app/schemas/models/payment.schema'
|
||||
|
||||
makeNewInstance: (req) ->
|
||||
payment = super(req)
|
||||
payment.set 'purchaser', req.user._id
|
||||
payment.set 'recipient', req.user._id
|
||||
payment.set 'created', new Date().toISOString()
|
||||
payment
|
||||
|
||||
post: (req, res) ->
|
||||
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)
|
||||
|
||||
if not (appleReceipt or stripeTimestamp)
|
||||
return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp')
|
||||
|
||||
if appleReceipt
|
||||
if not appleTransactionID
|
||||
return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.')
|
||||
@handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
|
||||
|
||||
else
|
||||
@handleStripePaymentPost(req, res, stripeTimestamp, stripeToken)
|
||||
|
||||
handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
|
||||
formFields = { 'receipt-data': receipt }
|
||||
|
||||
#- verify receipt with Apple
|
||||
|
||||
verifyReq = request.post({url: config.apple.verifyURL, json: formFields}, (err, verifyRes, body) =>
|
||||
if err or not body?.receipt?.in_app or (not body?.bundle_id is 'com.codecombat.CodeCombat')
|
||||
console.warn 'apple receipt error?', err, body
|
||||
@sendBadInputError(res, 'Unable to verify Apple receipt.')
|
||||
return
|
||||
|
||||
transaction = _.find body.receipt.in_app, { transaction_id: transactionID }
|
||||
return @sendBadInputError(res, 'Invalid transactionID.') unless transaction
|
||||
|
||||
#- Check existence
|
||||
transactionID = transaction.transaction_id
|
||||
criteria = { recipient: req.user._id, 'ios.transactionID': transactionID }
|
||||
Payment.findOne(criteria).exec((err, payment) =>
|
||||
|
||||
if payment
|
||||
@recalculateGemsFor(req.user, (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, @formatEntity(req, payment))
|
||||
)
|
||||
return
|
||||
|
||||
payment = @makeNewInstance(req)
|
||||
payment.set 'service', 'ios'
|
||||
product = products[transaction.product_id]
|
||||
|
||||
product ?= _.values(products)[0] # TEST
|
||||
|
||||
payment.set 'amount', product.amount
|
||||
payment.set 'gems', product.gems
|
||||
payment.set 'ios', {
|
||||
transactionID: transactionID
|
||||
rawReceipt: receipt
|
||||
localPrice: localPrice
|
||||
}
|
||||
|
||||
validation = @validateDocumentInput(payment.toObject())
|
||||
return @sendBadInputError(res, validation.errors) if validation.valid is false
|
||||
payment.save((err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@incrementGemsFor(req.user, product.gems, (err) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendCreated(res, @formatEntity(req, payment))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
handleStripePaymentPost: (req, res, timestamp, token) ->
|
||||
console.log 'lol not implemented yet'
|
||||
@sendNotFoundError(res)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
else
|
||||
user.update({$inc: {'purchased.gems': gems}}, {}, (err) -> done(err))
|
||||
|
||||
recalculateGemsFor: (user, done) ->
|
||||
|
||||
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 ?= {}
|
||||
purchased.gems = gems
|
||||
user.set('purchased', purchased)
|
||||
user.save((err) -> done(err))
|
||||
|
||||
)
|
||||
|
||||
module.exports = new PaymentHandler()
|
|
@ -1,7 +1,4 @@
|
|||
mongoose = require('mongoose')
|
||||
deltas = require '../../app/lib/deltas'
|
||||
log = require 'winston'
|
||||
{handlers} = require '../commons/mapping'
|
||||
|
||||
PurchaseSchema = new mongoose.Schema({status: String}, {strict: false})
|
||||
PurchaseSchema.index({recipient: 1, 'purchased.original': 1}, {unique: true, name: 'unique purchase'})
|
||||
|
|
|
@ -16,6 +16,9 @@ config.mongo =
|
|||
host: process.env.COCO_MONGO_HOST or 'localhost'
|
||||
db: process.env.COCO_MONGO_DATABASE_NAME or 'coco'
|
||||
mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or ''
|
||||
|
||||
config.apple =
|
||||
verifyURL: process.env.COCO_APPLE_VERIFY_URL or 'https://sandbox.itunes.apple.com/verifyReceipt' # 'https://buy.itunes.apple.com/verifyReceipt'
|
||||
|
||||
config.redis =
|
||||
port: process.env.COCO_REDIS_PORT or 6379
|
||||
|
|
|
@ -34,6 +34,7 @@ models_path = [
|
|||
'../../server/patches/Patch'
|
||||
'../../server/achievements/Achievement'
|
||||
'../../server/achievements/EarnedAchievement'
|
||||
'../../server/payments/Payment'
|
||||
]
|
||||
|
||||
for m in models_path
|
||||
|
|
73
test/server/functional/payment.spec.coffee
Normal file
73
test/server/functional/payment.spec.coffee
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue