Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-11-11 18:16:52 -08:00
commit 3a8a5354aa
14 changed files with 269 additions and 14 deletions

View file

@ -115,6 +115,7 @@ module.exports = class SpriteBuilder
if _.isString(childData)
child = @buildShapeFromStore(childData)
else
continue if not childData.gn
child = @buildContainerFromStore(childData.gn)
child.setTransform(childData.t...)
cont.addChild(child)

View file

@ -370,6 +370,8 @@ module.exports = class ThangType extends CocoModel
attackRange: 'range'
shieldDefenseFactor: 'blocks'
visualRange: 'range'
throwDamage: 'attack'
throwRange: 'range'
}[name]
if i18nKey

View 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

View file

@ -83,6 +83,7 @@ module.exports = class InventoryModal extends ModalView
# sort into one of the four groups
locked = not (item.get('original') in me.items())
locked = false if me.get('slug') is 'nick'
if locked and item.get('slug') isnt 'simple-boots'
@itemGroups.lockedItems.add(item)

View file

@ -249,7 +249,7 @@ module.exports = class HeroVictoryModal extends ModalView
onGameSubmitted: (e) ->
ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches"
# Preserve the supermodel as we navigate back to the ladder.
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: require('views/play/ladder/LadderView'), viewArgs: [{supermodel: @supermodel}]
Backbone.Mediator.publish 'router:navigate', route: ladderURL, viewClass: require('views/play/ladder/LadderView'), viewArgs: [{supermodel: @supermodel}, @level.get('slug')]
playSelectionSound: (hero, preload=false) ->
return unless sounds = hero.get('soundTriggers')?.selected

View file

@ -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'

View 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)

View 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()

View file

@ -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'})

View file

@ -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

View file

@ -34,6 +34,7 @@ models_path = [
'../../server/patches/Patch'
'../../server/achievements/Achievement'
'../../server/achievements/EarnedAchievement'
'../../server/payments/Payment'
]
for m in models_path

View file

@ -50,15 +50,6 @@ describe 'Achievement', ->
expect(res.statusCode).toBe(403)
done()
it 'can\'t be updated by ordinary users', (done) ->
loginJoe ->
request.put {uri: url, json: unlockable}, (err, res, body) ->
expect(res.statusCode).toBe(403)
request {method: 'patch', uri: url, json: unlockable}, (err, res, body) ->
expect(res.statusCode).toBe(403)
done()
it 'can be created by admins', (done) ->
loginAdmin ->
request.post {uri: url, json: unlockable}, (err, res, body) ->
@ -77,6 +68,17 @@ describe 'Achievement', ->
expect(docs.length).toBe 3
done()
it 'can\'t be updated by ordinary users', (done) ->
loginJoe ->
unlockable3 = _.clone(unlockable)
unlockable3.description = 'alsdfkhasdkfhajksdhfjkasdhfj'
request.put {uri: url, json: unlockable3}, (err, res, body) ->
expect(res.statusCode).toBe(403)
request {method: 'patch', uri: url, json: unlockable}, (err, res, body) ->
expect(res.statusCode).toBe(403)
done()
it 'can get all for ordinary users', (done) ->
loginJoe ->
request.get {uri: url, json: unlockable}, (err, res, body) ->

File diff suppressed because one or more lines are too long

View file

@ -69,7 +69,7 @@ describe 'POST /db/user', ->
expect(user.get('password')).toBeUndefined()
expect(user?.get('passwordHash')).not.toBeUndefined()
if user?.get('passwordHash')?
expect(user.get('passwordHash')[..5]).toBe('31dc3d')
expect(user.get('passwordHash')[..5]).toBe('948c7e')
expect(user.get('permissions').length).toBe(0)
done()