mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Move product information to the db
This commit is contained in:
parent
ae1621ea09
commit
7c516c4d9f
19 changed files with 334 additions and 261 deletions
8
app/collections/Products.coffee
Normal file
8
app/collections/Products.coffee
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CocoCollection = require './CocoCollection'
|
||||||
|
Product = require 'models/Product'
|
||||||
|
|
||||||
|
module.exports = class Products extends CocoCollection
|
||||||
|
model: Product
|
||||||
|
url: '/db/products'
|
||||||
|
|
||||||
|
getByName: (name) -> @findWhere { name: name }
|
|
@ -244,7 +244,7 @@ module.exports.getCoursePraise = getCoursePraise = ->
|
||||||
]
|
]
|
||||||
praise[_.random(0, praise.length - 1)]
|
praise[_.random(0, praise.length - 1)]
|
||||||
|
|
||||||
module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=999, users=0, months=0) ->
|
module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=0, users=0, months=0) ->
|
||||||
return 0 unless users > 0 and months > 0
|
return 0 unless users > 0 and months > 0
|
||||||
total = price * users * months
|
total = price * users * months
|
||||||
total
|
total
|
||||||
|
|
6
app/models/Product.coffee
Normal file
6
app/models/Product.coffee
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CocoModel = require './CocoModel'
|
||||||
|
|
||||||
|
module.exports = class ProductModel extends CocoModel
|
||||||
|
@className: 'Product'
|
||||||
|
@schema: require 'schemas/models/product.schema'
|
||||||
|
urlRoot: '/db/products'
|
13
app/schemas/models/product.schema.coffee
Normal file
13
app/schemas/models/product.schema.coffee
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
c = require './../schemas'
|
||||||
|
|
||||||
|
module.exports = ProductSchema = {
|
||||||
|
type: 'object'
|
||||||
|
additionalProperties: false
|
||||||
|
properties: {
|
||||||
|
name: { type: 'string' }
|
||||||
|
amount: { type: 'integer', description: 'Cost in cents' }
|
||||||
|
gems: { type: 'integer', description: 'Number of gems awarded' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.extendBasicProperties ProductSchema, 'Product'
|
|
@ -39,7 +39,7 @@ block content
|
||||||
label.control-label.col-md-2(data-i18n="account_prepaid.purchase_total")
|
label.control-label.col-md-2(data-i18n="account_prepaid.purchase_total")
|
||||||
.col-md-10
|
.col-md-10
|
||||||
p.form-control-static $
|
p.form-control-static $
|
||||||
span#total #{purchase.total.toFixed(2)}
|
span#total #{(purchase.total/100).toFixed(2)}
|
||||||
button#purchase-btn.btn.btn-success.pull-right(data-i18n="account_prepaid.purchase_button")
|
button#purchase-btn.btn.btn-success.pull-right(data-i18n="account_prepaid.purchase_button")
|
||||||
.row
|
.row
|
||||||
.col-md-12
|
.col-md-12
|
||||||
|
@ -66,7 +66,7 @@ block content
|
||||||
button#redeem-code-btn.btn.btn-success(data-i18n="account_prepaid.apply_account")
|
button#redeem-code-btn.btn.btn-success(data-i18n="account_prepaid.apply_account")
|
||||||
.row
|
.row
|
||||||
.col-md-12
|
.col-md-12
|
||||||
.panel.panel-default
|
#codes-panel.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
.panel-title
|
.panel-title
|
||||||
a(data-toggle="collapse" href="#codeslist")
|
a(data-toggle="collapse" href="#codeslist")
|
||||||
|
|
|
@ -24,8 +24,11 @@
|
||||||
th.free-cell(data-i18n="subscribe.free")
|
th.free-cell(data-i18n="subscribe.free")
|
||||||
th
|
th
|
||||||
//- TODO: find a better way to localize '$9.99/month'
|
//- TODO: find a better way to localize '$9.99/month'
|
||||||
span $#{(view.product.amount / 100)}/
|
if view.basicProduct
|
||||||
span(data-i18n="subscribe.month")
|
span $#{(view.basicProduct.get('amount') / 100)}/
|
||||||
|
span(data-i18n="subscribe.month")
|
||||||
|
else
|
||||||
|
span '...'
|
||||||
tbody
|
tbody
|
||||||
tr
|
tr
|
||||||
td.feature-description
|
td.feature-description
|
||||||
|
|
|
@ -82,7 +82,7 @@ block content
|
||||||
span(data-i18n="account_prepaid.purchase_total")
|
span(data-i18n="account_prepaid.purchase_total")
|
||||||
span.spr : #{view.numberOfStudents}
|
span.spr : #{view.numberOfStudents}
|
||||||
span(data-i18n="courses.enrollments")
|
span(data-i18n="courses.enrollments")
|
||||||
span.spl x $#{view.pricePerStudent.toFixed(2)} = #{view.getPriceString()}
|
span.spl x $#{(view.pricePerStudent/100).toFixed(2)} = #{view.getPriceString()}
|
||||||
|
|
||||||
p.text-center
|
p.text-center
|
||||||
button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now")
|
button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
.modal-dialog
|
.modal-dialog
|
||||||
.modal-content
|
.modal-content
|
||||||
|
|
||||||
if state === 'purchasing'
|
if view.state === 'purchasing'
|
||||||
.alert.alert-info(data-i18n="buy_gems.purchasing")
|
.alert.alert-info(data-i18n="buy_gems.purchasing")
|
||||||
|
|
||||||
else if state === 'retrying'
|
else if view.state === 'retrying'
|
||||||
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
||||||
|
|
||||||
else
|
else
|
||||||
|
@ -12,12 +12,12 @@
|
||||||
h1(data-i18n="play.buy_gems")
|
h1(data-i18n="play.buy_gems")
|
||||||
|
|
||||||
#products
|
#products
|
||||||
for product in products
|
for product in view.products.models
|
||||||
.product
|
.product
|
||||||
h4 x#{product.gems}
|
h4 x#{product.get('gems')}
|
||||||
h3(data-i18n=product.i18n)
|
h3(data-i18n=product.get('i18n'))
|
||||||
button.btn.btn-illustrated.btn-lg(value=product.id)
|
button.btn.btn-illustrated.btn-lg(value=product.get('name'))
|
||||||
span= product.price
|
span= product.get('priceString')
|
||||||
|
|
||||||
.product
|
.product
|
||||||
h4(data-i18n="buy_gems.price") x3500 / mo
|
h4(data-i18n="buy_gems.price") x3500 / mo
|
||||||
|
@ -29,20 +29,20 @@
|
||||||
else
|
else
|
||||||
button.start-subscription-button.btn.btn-lg.btn-illustrated.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
button.start-subscription-button.btn.btn-lg.btn-illustrated.btn-success(data-i18n="subscribe.subscribe_title") Subscribe
|
||||||
|
|
||||||
if state === 'declined'
|
if view.state === 'declined'
|
||||||
#declined-alert.alert.alert-danger.alert-dismissible
|
#declined-alert.alert.alert-danger.alert-dismissible
|
||||||
span(data-i18n="buy_gems.declined")
|
span(data-i18n="buy_gems.declined")
|
||||||
button.close(type="button" data-dismiss="alert")
|
button.close(type="button" data-dismiss="alert")
|
||||||
span(aria-hidden="true") ×
|
span(aria-hidden="true") ×
|
||||||
|
|
||||||
if state === 'unknown_error'
|
if view.state === 'unknown_error'
|
||||||
#error-alert.alert.alert-danger.alert-dismissible
|
#error-alert.alert.alert-danger.alert-dismissible
|
||||||
button.close(type="button" data-dismiss="alert")
|
button.close(type="button" data-dismiss="alert")
|
||||||
span(aria-hidden="true") ×
|
span(aria-hidden="true") ×
|
||||||
p(data-i18n="loading_error.unknown")
|
p(data-i18n="loading_error.unknown")
|
||||||
p= stateMessage
|
p= view.stateMessage
|
||||||
|
|
||||||
if state === 'recovered_charge'
|
if view.state === 'recovered_charge'
|
||||||
#recovered-alert.alert.alert-danger.alert-dismissible
|
#recovered-alert.alert.alert-danger.alert-dismissible
|
||||||
span(data-i18n="buy_gems.recovered")
|
span(data-i18n="buy_gems.recovered")
|
||||||
button.close(type="button" data-dismiss="alert")
|
button.close(type="button" data-dismiss="alert")
|
||||||
|
|
|
@ -7,6 +7,7 @@ Prepaid = require '../../models/Prepaid'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
RedeemModal = require 'views/account/PrepaidRedeemModal'
|
RedeemModal = require 'views/account/PrepaidRedeemModal'
|
||||||
forms = require 'core/forms'
|
forms = require 'core/forms'
|
||||||
|
Products = require 'collections/Products'
|
||||||
|
|
||||||
# TODO: remove redeem code modal
|
# TODO: remove redeem code modal
|
||||||
|
|
||||||
|
@ -26,12 +27,10 @@ module.exports = class PrepaidView extends RootView
|
||||||
subscriptions:
|
subscriptions:
|
||||||
'stripe:received-token': 'onStripeReceivedToken'
|
'stripe:received-token': 'onStripeReceivedToken'
|
||||||
|
|
||||||
baseAmount: 9.99
|
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
@purchase =
|
@purchase =
|
||||||
total: @baseAmount
|
total: 0
|
||||||
users: 3
|
users: 3
|
||||||
months: 3
|
months: 3
|
||||||
@updateTotal()
|
@updateTotal()
|
||||||
|
@ -45,6 +44,14 @@ module.exports = class PrepaidView extends RootView
|
||||||
@ppcQuery = true
|
@ppcQuery = true
|
||||||
@loadPrepaid(@ppc)
|
@loadPrepaid(@ppc)
|
||||||
|
|
||||||
|
@products = new Products()
|
||||||
|
@supermodel.loadCollection(@products, 'products')
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
@prepaidProduct = @products.findWhere { name: 'prepaid_subscription' }
|
||||||
|
@updateTotal()
|
||||||
|
super()
|
||||||
|
|
||||||
getRenderData: ->
|
getRenderData: ->
|
||||||
c = super()
|
c = super()
|
||||||
c.purchase = @purchase
|
c.purchase = @purchase
|
||||||
|
@ -62,7 +69,8 @@ module.exports = class PrepaidView extends RootView
|
||||||
noty text: message, layout: 'topCenter', type: type, killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3
|
noty text: message, layout: 'topCenter', type: type, killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3
|
||||||
|
|
||||||
updateTotal: ->
|
updateTotal: ->
|
||||||
@purchase.total = getPrepaidCodeAmount(@baseAmount, @purchase.users, @purchase.months)
|
return unless @prepaidProduct
|
||||||
|
@purchase.total = getPrepaidCodeAmount(@prepaidProduct.get('amount'), @purchase.users, @purchase.months)
|
||||||
@renderSelectors("#total", "#users-input", "#months-input")
|
@renderSelectors("#total", "#users-input", "#months-input")
|
||||||
|
|
||||||
# Form Input Callbacks
|
# Form Input Callbacks
|
||||||
|
@ -99,7 +107,7 @@ module.exports = class PrepaidView extends RootView
|
||||||
onClickPurchaseButton: (e) ->
|
onClickPurchaseButton: (e) ->
|
||||||
return unless $("#users-input").val() >= 3 or $("#months-input").val() >= 3
|
return unless $("#users-input").val() >= 3 or $("#months-input").val() >= 3
|
||||||
@purchaseTimestamp = new Date().getTime()
|
@purchaseTimestamp = new Date().getTime()
|
||||||
@stripeAmount = @purchase.total * 100
|
@stripeAmount = @purchase.total
|
||||||
@description = "Prepaid Code for " + @purchase.users + " users / " + @purchase.months + " months"
|
@description = "Prepaid Code for " + @purchase.users + " users / " + @purchase.months + " months"
|
||||||
|
|
||||||
stripeHandler.open
|
stripeHandler.open
|
||||||
|
@ -179,6 +187,7 @@ module.exports = class PrepaidView extends RootView
|
||||||
# console.log 'SUCCESS: Prepaid purchase', model.code
|
# console.log 'SUCCESS: Prepaid purchase', model.code
|
||||||
@statusMessage "Successfully purchased Prepaid Code!", "success"
|
@statusMessage "Successfully purchased Prepaid Code!", "success"
|
||||||
@codes.add(model)
|
@codes.add(model)
|
||||||
|
@renderSelectors('#codes-panel')
|
||||||
|
|
||||||
@statusMessage "Finalizing purchase...", "information"
|
@statusMessage "Finalizing purchase...", "information"
|
||||||
@supermodel.addRequestResource('purchase_prepaid', options, 0).load()
|
@supermodel.addRequestResource('purchase_prepaid', options, 0).load()
|
||||||
|
|
|
@ -3,16 +3,14 @@ template = require 'templates/core/subscribe-modal'
|
||||||
stripeHandler = require 'core/services/stripe'
|
stripeHandler = require 'core/services/stripe'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
AuthModal = require 'views/core/AuthModal'
|
AuthModal = require 'views/core/AuthModal'
|
||||||
|
Products = require 'collections/Products'
|
||||||
|
|
||||||
module.exports = class SubscribeModal extends ModalView
|
module.exports = class SubscribeModal extends ModalView
|
||||||
id: 'subscribe-modal'
|
id: 'subscribe-modal'
|
||||||
template: template
|
template: template
|
||||||
plain: true
|
plain: true
|
||||||
closesOnClickOutside: false
|
closesOnClickOutside: false
|
||||||
product:
|
planID: 'basic'
|
||||||
amount: 999
|
|
||||||
planID: 'basic'
|
|
||||||
yearAmount: 9900
|
|
||||||
|
|
||||||
subscriptions:
|
subscriptions:
|
||||||
'stripe:received-token': 'onStripeReceivedToken'
|
'stripe:received-token': 'onStripeReceivedToken'
|
||||||
|
@ -27,6 +25,13 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
@state = 'standby'
|
@state = 'standby'
|
||||||
|
@products = new Products()
|
||||||
|
@supermodel.loadCollection(@products, 'products')
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
@basicProduct = @products.findWhere { name: 'basic_subscription' }
|
||||||
|
@yearProduct = @products.findWhere { name: 'year_subscription' }
|
||||||
|
super()
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
|
@ -109,12 +114,13 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
@$el.find('.parent-button').popover('hide')
|
@$el.find('.parent-button').popover('hide')
|
||||||
|
|
||||||
onClickPurchaseButton: (e) ->
|
onClickPurchaseButton: (e) ->
|
||||||
|
return unless @basicProduct and @yearProduct
|
||||||
@playSound 'menu-button-click'
|
@playSound 'menu-button-click'
|
||||||
return @openModalView new AuthModal() if me.get('anonymous')
|
return @openModalView new AuthModal() if me.get('anonymous')
|
||||||
application.tracker?.trackEvent 'Started subscription purchase'
|
application.tracker?.trackEvent 'Started subscription purchase'
|
||||||
options = {
|
options = {
|
||||||
description: $.i18n.t('subscribe.stripe_description')
|
description: $.i18n.t('subscribe.stripe_description')
|
||||||
amount: @product.amount
|
amount: @basicProduct.get('amount')
|
||||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||||
alipayReusable: true
|
alipayReusable: true
|
||||||
}
|
}
|
||||||
|
@ -138,7 +144,7 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
application.tracker?.trackEvent 'Started 1 year subscription purchase'
|
application.tracker?.trackEvent 'Started 1 year subscription purchase'
|
||||||
options =
|
options =
|
||||||
description: $.i18n.t('subscribe.stripe_description_year_sale')
|
description: $.i18n.t('subscribe.stripe_description_year_sale')
|
||||||
amount: @product.yearAmount
|
amount: @yearProduct.get('amount')
|
||||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||||
alipayReusable: true
|
alipayReusable: true
|
||||||
@purchasedAmount = options.amount
|
@purchasedAmount = options.amount
|
||||||
|
@ -148,15 +154,15 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
@state = 'purchasing'
|
@state = 'purchasing'
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
if @purchasedAmount is @product.amount
|
if @purchasedAmount is @basicProduct.get('amount')
|
||||||
stripe = _.clone(me.get('stripe') ? {})
|
stripe = _.clone(me.get('stripe') ? {})
|
||||||
stripe.planID = @product.planID
|
stripe.planID = @basicProduct.get('planID')
|
||||||
stripe.token = e.token.id
|
stripe.token = e.token.id
|
||||||
me.set 'stripe', stripe
|
me.set 'stripe', stripe
|
||||||
@listenToOnce me, 'sync', @onSubscriptionSuccess
|
@listenToOnce me, 'sync', @onSubscriptionSuccess
|
||||||
@listenToOnce me, 'error', @onSubscriptionError
|
@listenToOnce me, 'error', @onSubscriptionError
|
||||||
me.patch({headers: {'X-Change-Plan': 'true'}})
|
me.patch({headers: {'X-Change-Plan': 'true'}})
|
||||||
else if @purchasedAmount is @product.yearAmount
|
else if @purchasedAmount is @yearProduct.get('amount')
|
||||||
# Purchasing a year
|
# Purchasing a year
|
||||||
data =
|
data =
|
||||||
stripe:
|
stripe:
|
||||||
|
|
|
@ -9,12 +9,13 @@ stripeHandler = require 'core/services/stripe'
|
||||||
template = require 'templates/courses/purchase-courses-view'
|
template = require 'templates/courses/purchase-courses-view'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
|
Products = require 'collections/Products'
|
||||||
|
|
||||||
module.exports = class PurchaseCoursesView extends RootView
|
module.exports = class PurchaseCoursesView extends RootView
|
||||||
id: 'purchase-courses-view'
|
id: 'purchase-courses-view'
|
||||||
template: template
|
template: template
|
||||||
numberOfStudents: 30
|
numberOfStudents: 30
|
||||||
pricePerStudent: 4
|
pricePerStudent: 0
|
||||||
|
|
||||||
initialize: (options) ->
|
initialize: (options) ->
|
||||||
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
|
||||||
|
@ -29,13 +30,19 @@ module.exports = class PurchaseCoursesView extends RootView
|
||||||
@prepaids.comparator = '_id'
|
@prepaids.comparator = '_id'
|
||||||
@prepaids.fetchByCreator(me.id)
|
@prepaids.fetchByCreator(me.id)
|
||||||
@supermodel.loadCollection(@prepaids, 'prepaids')
|
@supermodel.loadCollection(@prepaids, 'prepaids')
|
||||||
|
@products = new Products()
|
||||||
|
@supermodel.loadCollection(@products, 'products')
|
||||||
super(options)
|
super(options)
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'input #students-input': 'onInputStudentsInput'
|
'input #students-input': 'onInputStudentsInput'
|
||||||
'click #purchase-btn': 'onClickPurchaseButton'
|
'click #purchase-btn': 'onClickPurchaseButton'
|
||||||
|
|
||||||
|
onLoaded: ->
|
||||||
|
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
|
||||||
|
super()
|
||||||
|
|
||||||
getPriceString: -> '$' + (@getPrice()).toFixed(2)
|
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
|
||||||
getPrice: -> @pricePerStudent * @numberOfStudents
|
getPrice: -> @pricePerStudent * @numberOfStudents
|
||||||
|
|
||||||
onceClassroomsSync: ->
|
onceClassroomsSync: ->
|
||||||
|
@ -80,7 +87,7 @@ module.exports = class PurchaseCoursesView extends RootView
|
||||||
application.tracker?.trackEvent 'Started course prepaid purchase', {
|
application.tracker?.trackEvent 'Started course prepaid purchase', {
|
||||||
price: @pricePerStudent, students: @numberOfStudents}
|
price: @pricePerStudent, students: @numberOfStudents}
|
||||||
stripeHandler.open
|
stripeHandler.open
|
||||||
amount: @numberOfStudents * @pricePerStudent * 100
|
amount: @numberOfStudents * @pricePerStudent
|
||||||
description: "Full course access for #{@numberOfStudents} students"
|
description: "Full course access for #{@numberOfStudents} students"
|
||||||
bitcoin: true
|
bitcoin: true
|
||||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||||
|
|
|
@ -3,6 +3,7 @@ template = require 'templates/play/modal/buy-gems-modal'
|
||||||
stripeHandler = require 'core/services/stripe'
|
stripeHandler = require 'core/services/stripe'
|
||||||
utils = require 'core/utils'
|
utils = require 'core/utils'
|
||||||
SubscribeModal = require 'views/core/SubscribeModal'
|
SubscribeModal = require 'views/core/SubscribeModal'
|
||||||
|
Products = require 'collections/Products'
|
||||||
|
|
||||||
module.exports = class BuyGemsModal extends ModalView
|
module.exports = class BuyGemsModal extends ModalView
|
||||||
id: 'buy-gems-modal'
|
id: 'buy-gems-modal'
|
||||||
|
@ -29,22 +30,21 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
super(options)
|
super(options)
|
||||||
@timestampForPurchase = new Date().getTime()
|
@timestampForPurchase = new Date().getTime()
|
||||||
@state = 'standby'
|
@state = 'standby'
|
||||||
|
@products = new Products()
|
||||||
|
@products.comparator = 'amount'
|
||||||
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
|
@supermodel.loadCollection(@products, 'products')
|
||||||
$.post '/db/payment/check-stripe-charges', (something, somethingElse, jqxhr) =>
|
$.post '/db/payment/check-stripe-charges', (something, somethingElse, jqxhr) =>
|
||||||
if jqxhr.status is 201
|
if jqxhr.status is 201
|
||||||
@state = 'recovered_charge'
|
@state = 'recovered_charge'
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
getRenderData: ->
|
onLoaded: ->
|
||||||
c = super()
|
@products.reset @products.filter (product) -> _.string.startsWith(product.get('name'), 'gems_')
|
||||||
c.products = @products
|
super()
|
||||||
c.state = @state
|
|
||||||
c.stateMessage = @stateMessage
|
|
||||||
return c
|
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
|
@ -56,19 +56,20 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
@playSound 'game-menu-close'
|
@playSound 'game-menu-close'
|
||||||
|
|
||||||
onIPadProducts: (e) ->
|
onIPadProducts: (e) ->
|
||||||
newProducts = []
|
# TODO: Update to handle new products collection
|
||||||
for iapProduct in e.products
|
# newProducts = []
|
||||||
localProduct = _.find @originalProducts, { id: iapProduct.id }
|
# for iapProduct in e.products
|
||||||
continue unless localProduct
|
# localProduct = _.find @originalProducts, { id: iapProduct.id }
|
||||||
localProduct.price = iapProduct.price
|
# continue unless localProduct
|
||||||
newProducts.push localProduct
|
# localProduct.price = iapProduct.price
|
||||||
@products = _.sortBy newProducts, 'gems'
|
# newProducts.push localProduct
|
||||||
@render()
|
# @products = _.sortBy newProducts, 'gems'
|
||||||
|
# @render()
|
||||||
|
|
||||||
onClickProductButton: (e) ->
|
onClickProductButton: (e) ->
|
||||||
@playSound 'menu-button-click'
|
@playSound 'menu-button-click'
|
||||||
productID = $(e.target).closest('button').val()
|
productID = $(e.target).closest('button').val()
|
||||||
product = _.find @products, { id: productID }
|
product = @products.findWhere { name: productID }
|
||||||
|
|
||||||
if application.isIPadApp
|
if application.isIPadApp
|
||||||
Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID }
|
Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID }
|
||||||
|
@ -76,8 +77,8 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
else
|
else
|
||||||
application.tracker?.trackEvent 'Started gem purchase', { productID: productID }
|
application.tracker?.trackEvent 'Started gem purchase', { productID: productID }
|
||||||
stripeHandler.open({
|
stripeHandler.open({
|
||||||
description: $.t(product.i18n)
|
description: $.t(product.get('i18n'))
|
||||||
amount: product.amount
|
amount: product.get('amount')
|
||||||
bitcoin: true
|
bitcoin: true
|
||||||
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
|
||||||
})
|
})
|
||||||
|
@ -86,7 +87,7 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
|
|
||||||
onStripeReceivedToken: (e) ->
|
onStripeReceivedToken: (e) ->
|
||||||
data = {
|
data = {
|
||||||
productID: @productBeingPurchased.id
|
productID: @productBeingPurchased.get('name')
|
||||||
stripe: {
|
stripe: {
|
||||||
token: e.token.id
|
token: e.token.id
|
||||||
timestamp: @timestampForPurchase
|
timestamp: @timestampForPurchase
|
||||||
|
@ -97,8 +98,8 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
jqxhr = $.post('/db/payment', data)
|
jqxhr = $.post('/db/payment', data)
|
||||||
jqxhr.done(=>
|
jqxhr.done(=>
|
||||||
application.tracker?.trackEvent 'Finished gem purchase',
|
application.tracker?.trackEvent 'Finished gem purchase',
|
||||||
productID: @productBeingPurchased.id
|
productID: @productBeingPurchased.get('name')
|
||||||
value: @productBeingPurchased.amount
|
value: @productBeingPurchased.get('amount')
|
||||||
document.location.reload()
|
document.location.reload()
|
||||||
)
|
)
|
||||||
jqxhr.fail(=>
|
jqxhr.fail(=>
|
||||||
|
@ -116,7 +117,7 @@ module.exports = class BuyGemsModal extends ModalView
|
||||||
)
|
)
|
||||||
|
|
||||||
onIAPComplete: (e) ->
|
onIAPComplete: (e) ->
|
||||||
product = _.find @products, { id: e.productID }
|
product = @products.findWhere { name: e.productID }
|
||||||
purchased = me.get('purchased') ? {}
|
purchased = me.get('purchased') ? {}
|
||||||
purchased = _.clone purchased
|
purchased = _.clone purchased
|
||||||
purchased.gems ?= 0
|
purchased.gems ?= 0
|
||||||
|
|
5
server/models/Product.coffee
Normal file
5
server/models/Product.coffee
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mongoose = require('mongoose')
|
||||||
|
config = require '../../server_config'
|
||||||
|
ProductSchema = new mongoose.Schema({}, {strict: false,read:config.mongo.readpref})
|
||||||
|
|
||||||
|
module.exports = mongoose.model('product', ProductSchema)
|
|
@ -1,4 +1,5 @@
|
||||||
Payment = require './Payment'
|
Payment = require './Payment'
|
||||||
|
Product = require '../models/Product'
|
||||||
User = require '../users/User'
|
User = require '../users/User'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
{handlers} = require '../commons/mapping'
|
{handlers} = require '../commons/mapping'
|
||||||
|
@ -11,30 +12,6 @@ request = require 'request'
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
apple_utils = require '../lib/apple_utils'
|
apple_utils = require '../lib/apple_utils'
|
||||||
|
|
||||||
products = {
|
|
||||||
'gems_5': {
|
|
||||||
amount: 499
|
|
||||||
gems: 5000
|
|
||||||
id: 'gems_5'
|
|
||||||
}
|
|
||||||
|
|
||||||
'gems_10': {
|
|
||||||
amount: 999
|
|
||||||
gems: 11000
|
|
||||||
id: 'gems_10'
|
|
||||||
}
|
|
||||||
|
|
||||||
'gems_20': {
|
|
||||||
amount: 1999
|
|
||||||
gems: 25000
|
|
||||||
id: 'gems_20'
|
|
||||||
}
|
|
||||||
|
|
||||||
'custom': {
|
|
||||||
# amount expected in request body
|
|
||||||
id: 'custom'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PaymentHandler = class PaymentHandler extends Handler
|
PaymentHandler = class PaymentHandler extends Handler
|
||||||
modelClass: Payment
|
modelClass: Payment
|
||||||
|
@ -134,33 +111,34 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
|
|
||||||
payment = @makeNewInstance(req)
|
payment = @makeNewInstance(req)
|
||||||
payment.set 'service', 'ios'
|
payment.set 'service', 'ios'
|
||||||
product = products[transaction.product_id]
|
Product.findOne({name: transaction.product_id}).exec (err, product) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
payment.set 'amount', product.amount
|
return @sendNotFoundError(res) if not product
|
||||||
payment.set 'gems', product.gems
|
payment.set 'amount', product.get('amount')
|
||||||
payment.set 'ios', {
|
payment.set 'gems', product.get('gems')
|
||||||
transactionID: transactionID
|
payment.set 'ios', {
|
||||||
rawReceipt: receipt
|
transactionID: transactionID
|
||||||
localPrice: localPrice
|
rawReceipt: receipt
|
||||||
}
|
localPrice: localPrice
|
||||||
|
}
|
||||||
validation = @validateDocumentInput(payment.toObject())
|
|
||||||
if validation.valid is false
|
validation = @validateDocumentInput(payment.toObject())
|
||||||
@logPaymentError(req, 'Invalid apple payment object.')
|
if validation.valid is false
|
||||||
return @sendBadInputError(res, validation.errors)
|
@logPaymentError(req, 'Invalid apple payment object.')
|
||||||
|
return @sendBadInputError(res, validation.errors)
|
||||||
payment.save((err) =>
|
|
||||||
if err
|
payment.save((err) =>
|
||||||
@logPaymentError(req, 'Apple payment save error.'+err)
|
|
||||||
return @sendDatabaseError(res, err)
|
|
||||||
@incrementGemsFor(req.user, product.gems, (err) =>
|
|
||||||
if err
|
if err
|
||||||
@logPaymentError(req, 'Apple incr db error.'+err)
|
@logPaymentError(req, 'Apple payment save error.'+err)
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
@sendPaymentHipChatMessage user: req.user, payment: payment
|
@incrementGemsFor(req.user, product.get('gems'), (err) =>
|
||||||
@sendCreated(res, @formatEntity(req, payment))
|
if err
|
||||||
|
@logPaymentError(req, 'Apple incr db error.'+err)
|
||||||
|
return @sendDatabaseError(res, err)
|
||||||
|
@sendPaymentHipChatMessage user: req.user, payment: payment
|
||||||
|
@sendCreated(res, @formatEntity(req, payment))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -203,7 +181,6 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
|
|
||||||
|
|
||||||
beginStripePayment: (req, res, timestamp, productID) ->
|
beginStripePayment: (req, res, timestamp, productID) ->
|
||||||
product = products[productID]
|
|
||||||
|
|
||||||
async.parallel([
|
async.parallel([
|
||||||
((callback) ->
|
((callback) ->
|
||||||
|
@ -218,6 +195,10 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
charge = _.find recentCharges.data, (c) -> c.metadata.timestamp is timestamp
|
charge = _.find recentCharges.data, (c) -> c.metadata.timestamp is timestamp
|
||||||
callback(null, charge)
|
callback(null, charge)
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
((callback) ->
|
||||||
|
Product.findOne({name: productID}).exec (err, product) =>
|
||||||
|
callback(err, product)
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -225,7 +206,10 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
if err
|
if err
|
||||||
@logPaymentError(req, 'Stripe async load db error. '+err)
|
@logPaymentError(req, 'Stripe async load db error. '+err)
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
[payment, charge] = results
|
[payment, charge, product] = results
|
||||||
|
|
||||||
|
if not product
|
||||||
|
return @sendNotFoundError(res, 'could not find product with id '+productID)
|
||||||
|
|
||||||
if not (payment or charge)
|
if not (payment or charge)
|
||||||
# Proceed normally from the beginning
|
# Proceed normally from the beginning
|
||||||
|
@ -236,7 +220,7 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
@recordStripeCharge(req, res, charge)
|
@recordStripeCharge(req, res, charge)
|
||||||
|
|
||||||
else
|
else
|
||||||
return @sendSuccess(res, @formatEntity(req, payment)) if product.id is 'custom'
|
return @sendSuccess(res, @formatEntity(req, payment)) if product.get('name') is 'custom'
|
||||||
|
|
||||||
# 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.
|
||||||
@recalculateGemsFor(req.user, (err) =>
|
@recalculateGemsFor(req.user, (err) =>
|
||||||
|
@ -250,7 +234,7 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
)
|
)
|
||||||
|
|
||||||
chargeStripe: (req, res, product) ->
|
chargeStripe: (req, res, product) ->
|
||||||
amount = parseInt product.amount ? req.body.amount
|
amount = parseInt product.get('amount') ? req.body.amount
|
||||||
return @sendError(res, 400, "Invalid amount.") if isNaN(amount)
|
return @sendError(res, 400, "Invalid amount.") if isNaN(amount)
|
||||||
|
|
||||||
stripe.charges.create({
|
stripe.charges.create({
|
||||||
|
@ -258,9 +242,9 @@ PaymentHandler = class PaymentHandler extends Handler
|
||||||
currency: 'usd'
|
currency: 'usd'
|
||||||
customer: req.user.get('stripe')?.customerID
|
customer: req.user.get('stripe')?.customerID
|
||||||
metadata: {
|
metadata: {
|
||||||
productID: product.id
|
productID: product.get('name')
|
||||||
userID: req.user._id + ''
|
userID: req.user._id + ''
|
||||||
gems: product.gems
|
gems: product.get('gems')
|
||||||
timestamp: parseInt(req.body.stripe?.timestamp)
|
timestamp: parseInt(req.body.stripe?.timestamp)
|
||||||
description: req.body.description
|
description: req.body.description
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,20 +15,10 @@ User = require '../users/User'
|
||||||
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
{getSponsoredSubsAmount} = require '../../app/core/utils'
|
||||||
StripeUtils = require '../lib/stripe_utils'
|
StripeUtils = require '../lib/stripe_utils'
|
||||||
moment = require 'moment'
|
moment = require 'moment'
|
||||||
|
Product = require '../models/Product'
|
||||||
|
|
||||||
recipientCouponID = 'free'
|
recipientCouponID = 'free'
|
||||||
|
|
||||||
# TODO: rename this to avoid collisions with 'subscriptions' variables
|
|
||||||
subscriptions = {
|
|
||||||
basic: {
|
|
||||||
gems: 3500
|
|
||||||
amount: 999 # For calculating incremental quantity before sub creation
|
|
||||||
}
|
|
||||||
year_sale: {
|
|
||||||
amount: 9900
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SubscriptionHandler extends Handler
|
class SubscriptionHandler extends Handler
|
||||||
logSubscriptionError: (user, msg) ->
|
logSubscriptionError: (user, msg) ->
|
||||||
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
||||||
|
@ -149,51 +139,56 @@ class SubscriptionHandler extends Handler
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, "Purchase year sale Stripe cancel subscription error: #{JSON.stringify(err)}")
|
@logSubscriptionError(user, "Purchase year sale Stripe cancel subscription error: #{JSON.stringify(err)}")
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
metadata =
|
|
||||||
type: req.body.type
|
Product.findOne({name: 'year_subscription'}).exec (err, product) =>
|
||||||
userID: req.user._id + ''
|
return @sendDatabaseError(res, err) if err
|
||||||
gems: subscriptions.basic.gems * 12
|
return @sendNotFoundError(res, 'year_subscription product not found') if not product
|
||||||
timestamp: parseInt(req.body.stripe?.timestamp)
|
|
||||||
description: req.body.description
|
metadata =
|
||||||
|
type: req.body.type
|
||||||
StripeUtils.createCharge req.user, subscriptions.year_sale.amount, metadata, (err, charge) =>
|
userID: req.user._id + ''
|
||||||
if err
|
gems: product.get('gems')
|
||||||
@logSubscriptionError(req.user, "Purchase year sale create charge: #{JSON.stringify(err)}")
|
timestamp: parseInt(req.body.stripe?.timestamp)
|
||||||
return @sendDatabaseError(res, err)
|
description: req.body.description
|
||||||
|
|
||||||
StripeUtils.createPayment req.user, charge, (err, payment) =>
|
StripeUtils.createCharge req.user, product.get('amount'), metadata, (err, charge) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(req.user, "Purchase year sale create payment: #{JSON.stringify(err)}")
|
@logSubscriptionError(req.user, "Purchase year sale create charge: #{JSON.stringify(err)}")
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
|
|
||||||
# Add terminal subscription to User with extensions for existing subscriptions
|
StripeUtils.createPayment req.user, charge, (err, payment) =>
|
||||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
|
||||||
endDate = new Date()
|
|
||||||
if stripeSubscriptionPeriodEndDate
|
|
||||||
endDate = stripeSubscriptionPeriodEndDate
|
|
||||||
else if _.isString(stripeInfo.free) and new Date() < new Date(stripeInfo.free)
|
|
||||||
endDate = new Date(stripeInfo.free)
|
|
||||||
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
|
|
||||||
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
|
||||||
req.user.set('stripe', stripeInfo)
|
|
||||||
|
|
||||||
# Add year's worth of gems to User
|
|
||||||
purchased = _.clone(req.user.get('purchased'))
|
|
||||||
purchased ?= {}
|
|
||||||
purchased.gems ?= 0
|
|
||||||
purchased.gems += parseInt(charge.metadata.gems)
|
|
||||||
req.user.set('purchased', purchased)
|
|
||||||
|
|
||||||
req.user.save (err, user) =>
|
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
@logSubscriptionError(req.user, "Purchase year sale create payment: #{JSON.stringify(err)}")
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
try
|
|
||||||
msg = "Year subscription purchased by #{req.user.get('email')} #{req.user.id}"
|
# Add terminal subscription to User with extensions for existing subscriptions
|
||||||
hipchat.sendHipChatMessage msg, ['tower']
|
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||||
catch error
|
endDate = new Date()
|
||||||
@logSubscriptionError(req.user, "Year sub sale HipChat tower msg error: #{JSON.stringify(error)}")
|
if stripeSubscriptionPeriodEndDate
|
||||||
@sendSuccess(res, user)
|
endDate = stripeSubscriptionPeriodEndDate
|
||||||
|
else if _.isString(stripeInfo.free) and new Date() < new Date(stripeInfo.free)
|
||||||
|
endDate = new Date(stripeInfo.free)
|
||||||
|
endDate.setUTCFullYear(endDate.getUTCFullYear() + 1)
|
||||||
|
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
||||||
|
req.user.set('stripe', stripeInfo)
|
||||||
|
|
||||||
|
# Add year's worth of gems to User
|
||||||
|
purchased = _.clone(req.user.get('purchased'))
|
||||||
|
purchased ?= {}
|
||||||
|
purchased.gems ?= 0
|
||||||
|
purchased.gems += parseInt(charge.metadata.gems)
|
||||||
|
req.user.set('purchased', purchased)
|
||||||
|
|
||||||
|
req.user.save (err, user) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
||||||
|
return @sendDatabaseError(res, err)
|
||||||
|
try
|
||||||
|
msg = "Year subscription purchased by #{req.user.get('email')} #{req.user.id}"
|
||||||
|
hipchat.sendHipChatMessage msg, ['tower']
|
||||||
|
catch error
|
||||||
|
@logSubscriptionError(req.user, "Year sub sale HipChat tower msg error: #{JSON.stringify(error)}")
|
||||||
|
@sendSuccess(res, user)
|
||||||
|
|
||||||
subscribeWithPrepaidCode: (req, res) ->
|
subscribeWithPrepaidCode: (req, res) ->
|
||||||
return @sendUnauthorizedError(res) unless req.user?
|
return @sendUnauthorizedError(res) unless req.user?
|
||||||
|
@ -241,31 +236,35 @@ class SubscriptionHandler extends Handler
|
||||||
@logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}")
|
@logSubscriptionError(user, "Redeem Prepaid Code Stripe cancel subscription error: #{JSON.stringify(err)}")
|
||||||
return @sendDatabaseError(res, err)
|
return @sendDatabaseError(res, err)
|
||||||
|
|
||||||
# Add terminal subscription to User, extending existing subscriptions
|
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
|
||||||
# TODO: refactor this into some form useable by both this and purchaseYearSale
|
return @sendDatabaseError(res, err) if err
|
||||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
|
||||||
endDate = new moment()
|
|
||||||
if stripeSubscriptionPeriodEndDate
|
|
||||||
endDate = new moment(stripeSubscriptionPeriodEndDate)
|
|
||||||
else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free))
|
|
||||||
endDate = new moment(stripeInfo.free)
|
|
||||||
|
|
||||||
endDate = endDate.add(months, 'months')
|
# Add terminal subscription to User, extending existing subscriptions
|
||||||
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
# TODO: refactor this into some form useable by both this and purchaseYearSale
|
||||||
req.user.set('stripe', stripeInfo)
|
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||||
|
endDate = new moment()
|
||||||
# Add gems to User
|
if stripeSubscriptionPeriodEndDate
|
||||||
purchased = _.clone(req.user.get('purchased'))
|
endDate = new moment(stripeSubscriptionPeriodEndDate)
|
||||||
purchased ?= {}
|
else if _.isString(stripeInfo.free) and new moment().isBefore(new moment(stripeInfo.free))
|
||||||
purchased.gems ?= 0
|
endDate = new moment(stripeInfo.free)
|
||||||
purchased.gems += subscriptions.basic.gems * months
|
|
||||||
req.user.set('purchased', purchased)
|
endDate = endDate.add(months, 'months')
|
||||||
|
stripeInfo.free = endDate.toISOString().substring(0, 10)
|
||||||
req.user.save (err, user) =>
|
req.user.set('stripe', stripeInfo)
|
||||||
if err
|
|
||||||
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
# Add gems to User
|
||||||
return @sendDatabaseError(res, err)
|
purchased = _.clone(req.user.get('purchased'))
|
||||||
@sendSuccess(res, user)
|
purchased ?= {}
|
||||||
|
purchased.gems ?= 0
|
||||||
|
purchased.gems += product.get('gems') * months
|
||||||
|
req.user.set('purchased', purchased)
|
||||||
|
|
||||||
|
req.user.save (err, user) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(req.user, "User save error: #{JSON.stringify(err)}")
|
||||||
|
return @sendDatabaseError(res, err)
|
||||||
|
@sendSuccess(res, user)
|
||||||
|
|
||||||
subscribeUser: (req, user, done) ->
|
subscribeUser: (req, user, done) ->
|
||||||
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
|
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
|
||||||
|
@ -427,18 +426,22 @@ class SubscriptionHandler extends Handler
|
||||||
req.body.stripe = stripeInfo
|
req.body.stripe = stripeInfo
|
||||||
user.set('stripe', stripeInfo)
|
user.set('stripe', stripeInfo)
|
||||||
|
|
||||||
if increment
|
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
|
||||||
purchased = _.clone(user.get('purchased'))
|
return @sendDatabaseError(res, err) if err
|
||||||
purchased ?= {}
|
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
|
||||||
purchased.gems ?= 0
|
|
||||||
purchased.gems += subscriptions.basic.gems # TODO: Put actual subscription amount here
|
|
||||||
user.set('purchased', purchased)
|
|
||||||
|
|
||||||
user.save (err) =>
|
if increment
|
||||||
if err
|
purchased = _.clone(user.get('purchased'))
|
||||||
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
|
purchased ?= {}
|
||||||
return done({res: 'Database error.', code: 500})
|
purchased.gems ?= 0
|
||||||
done()
|
purchased.gems += product.get('gems') # TODO: Put actual subscription amount here
|
||||||
|
user.set('purchased', purchased)
|
||||||
|
|
||||||
|
user.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
|
updateStripeRecipientSubscriptions: (req, user, customer, done) ->
|
||||||
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
|
return done({res: 'Database error.', code: 500}) unless req.body.stripe?.subscribeEmails?
|
||||||
|
@ -527,36 +530,40 @@ class SubscriptionHandler extends Handler
|
||||||
@logSubscriptionError(user, 'User saving stripe error. ' + err)
|
@logSubscriptionError(user, 'User saving stripe error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
createUpdateFn = (recipient, increment) =>
|
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
|
||||||
(done) =>
|
return @sendDatabaseError(res, err) if err
|
||||||
# Update recipient
|
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
|
||||||
stripeInfo = _.cloneDeep(recipient.get('stripe') ? {})
|
|
||||||
stripeInfo.sponsorID = user.id
|
|
||||||
recipient.set 'stripe', stripeInfo
|
|
||||||
if increment
|
|
||||||
purchased = _.clone(recipient.get('purchased'))
|
|
||||||
purchased ?= {}
|
|
||||||
purchased.gems ?= 0
|
|
||||||
purchased.gems += subscriptions.basic.gems
|
|
||||||
recipient.set('purchased', purchased)
|
|
||||||
recipient.save (err) =>
|
|
||||||
if err
|
|
||||||
@logSubscriptionError(user, 'Stripe user saving stripe error. ' + err)
|
|
||||||
return done({res: 'Database error.', code: 500})
|
|
||||||
done()
|
|
||||||
|
|
||||||
tasks = []
|
createUpdateFn = (recipient, increment) =>
|
||||||
for sub in stripeRecipients
|
(done) =>
|
||||||
tasks.push createUpdateFn(sub.recipient, sub.increment)
|
# Update recipient
|
||||||
|
stripeInfo = _.cloneDeep(recipient.get('stripe') ? {})
|
||||||
|
stripeInfo.sponsorID = user.id
|
||||||
|
recipient.set 'stripe', stripeInfo
|
||||||
|
if increment
|
||||||
|
purchased = _.clone(recipient.get('purchased'))
|
||||||
|
purchased ?= {}
|
||||||
|
purchased.gems ?= 0
|
||||||
|
purchased.gems += product.get('gems')
|
||||||
|
recipient.set('purchased', purchased)
|
||||||
|
recipient.save (err) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Stripe user saving stripe error. ' + err)
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
for sub in stripeRecipients
|
||||||
|
tasks.push createUpdateFn(sub.recipient, sub.increment)
|
||||||
|
|
||||||
|
async.parallel tasks, (err, results) =>
|
||||||
|
return done(err) if err
|
||||||
|
@updateStripeSponsorSubscription(req, user, customer, product, done)
|
||||||
|
|
||||||
async.parallel tasks, (err, results) =>
|
updateStripeSponsorSubscription: (req, user, customer, product, done) ->
|
||||||
return done(err) if err
|
|
||||||
@updateStripeSponsorSubscription(req, user, customer, done)
|
|
||||||
|
|
||||||
updateStripeSponsorSubscription: (req, user, customer, done) ->
|
|
||||||
stripeInfo = user.get('stripe') ? {}
|
stripeInfo = user.get('stripe') ? {}
|
||||||
numSponsored = stripeInfo.recipients.length
|
numSponsored = stripeInfo.recipients.length
|
||||||
quantity = getSponsoredSubsAmount(subscriptions.basic.amount, numSponsored, stripeInfo.subscriptionID?)
|
quantity = getSponsoredSubsAmount(product.get('amount'), numSponsored, stripeInfo.subscriptionID?)
|
||||||
|
|
||||||
findStripeSubscription customer.id, subscriptionID: stripeInfo.sponsorSubscriptionID, (subscription) =>
|
findStripeSubscription customer.id, subscriptionID: stripeInfo.sponsorSubscriptionID, (subscription) =>
|
||||||
if stripeInfo.sponsorSubscriptionID? and not subscription?
|
if stripeInfo.sponsorSubscriptionID? and not subscription?
|
||||||
|
@ -656,38 +663,42 @@ class SubscriptionHandler extends Handler
|
||||||
@logSubscriptionError(user, 'Unable to find recipient subscription. ')
|
@logSubscriptionError(user, 'Unable to find recipient subscription. ')
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
# Update recipient user
|
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
|
||||||
deleteUserStripeProp(recipient, 'sponsorID')
|
return @sendDatabaseError(res, err) if err
|
||||||
recipient.save (err) =>
|
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
|
||||||
if err
|
|
||||||
@logSubscriptionError(user, 'Recipient user save unsubscribe error. ' + err)
|
|
||||||
return done({res: 'Database error.', code: 500})
|
|
||||||
|
|
||||||
# Cancel Stripe subscription
|
# Update recipient user
|
||||||
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (err) =>
|
deleteUserStripeProp(recipient, 'sponsorID')
|
||||||
|
recipient.save (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
|
@logSubscriptionError(user, 'Recipient user save unsubscribe error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
# Update sponsor user
|
# Cancel Stripe subscription
|
||||||
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
|
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (err) =>
|
||||||
delete stripeInfo.unsubscribeEmail
|
|
||||||
user.set('stripe', stripeInfo)
|
|
||||||
req.body.stripe = stripeInfo
|
|
||||||
user.save (err) =>
|
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Sponsor user save unsubscribe error. ' + err)
|
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
|
|
||||||
return done() unless stripeInfo.sponsorSubscriptionID?
|
# Update sponsor user
|
||||||
|
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
|
||||||
# Update sponsored subscription quantity
|
delete stripeInfo.unsubscribeEmail
|
||||||
options =
|
user.set('stripe', stripeInfo)
|
||||||
quantity: getSponsoredSubsAmount(subscriptions.basic.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?)
|
req.body.stripe = stripeInfo
|
||||||
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
|
user.save (err) =>
|
||||||
if err
|
if err
|
||||||
@logSubscriptionError(user, 'Sponsored subscription quantity update error. ' + JSON.stringify(err))
|
@logSubscriptionError(user, 'Sponsor user save unsubscribe error. ' + err)
|
||||||
return done({res: 'Database error.', code: 500})
|
return done({res: 'Database error.', code: 500})
|
||||||
done()
|
|
||||||
|
return done() unless stripeInfo.sponsorSubscriptionID?
|
||||||
|
|
||||||
|
# Update sponsored subscription quantity
|
||||||
|
options =
|
||||||
|
quantity: getSponsoredSubsAmount(product.get('amount'), stripeInfo.recipients.length, stripeInfo.subscriptionID?)
|
||||||
|
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
|
||||||
|
if err
|
||||||
|
@logSubscriptionError(user, 'Sponsored subscription quantity update error. ' + JSON.stringify(err))
|
||||||
|
return done({res: 'Database error.', code: 500})
|
||||||
|
done()
|
||||||
|
|
||||||
module.exports = new SubscriptionHandler()
|
module.exports = new SubscriptionHandler()
|
||||||
|
|
|
@ -6,6 +6,7 @@ User = require '../users/User'
|
||||||
StripeUtils = require '../lib/stripe_utils'
|
StripeUtils = require '../lib/stripe_utils'
|
||||||
utils = require '../../app/core/utils'
|
utils = require '../../app/core/utils'
|
||||||
mongoose = require 'mongoose'
|
mongoose = require 'mongoose'
|
||||||
|
Product = require '../models/Product'
|
||||||
|
|
||||||
# TODO: Should this happen on a save() call instead of a prepaid/-/create post?
|
# TODO: Should this happen on a save() call instead of a prepaid/-/create post?
|
||||||
# TODO: Probably a better way to create a unique 8 charactor string property using db voodoo
|
# TODO: Probably a better way to create a unique 8 charactor string property using db voodoo
|
||||||
|
@ -17,8 +18,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
jsonSchema: require '../../app/schemas/models/prepaid.schema'
|
jsonSchema: require '../../app/schemas/models/prepaid.schema'
|
||||||
allowedMethods: ['GET','POST']
|
allowedMethods: ['GET','POST']
|
||||||
|
|
||||||
baseAmount: 999
|
|
||||||
|
|
||||||
logError: (user, msg) ->
|
logError: (user, msg) ->
|
||||||
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
|
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
|
||||||
|
|
||||||
|
@ -134,9 +133,13 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
return @sendBadInputError(res) unless isNaN(months) is false and months > 0
|
return @sendBadInputError(res) unless isNaN(months) is false and months > 0
|
||||||
return @sendError(res, 403, "Users or Months must be greater than 3") if maxRedeemers < 3 and months < 3
|
return @sendError(res, 403, "Users or Months must be greater than 3") if maxRedeemers < 3 and months < 3
|
||||||
|
|
||||||
@purchasePrepaidTerminalSubscription req.user, description, maxRedeemers, months, timestamp, token, (err, prepaid) =>
|
Product.findOne({name: 'prepaid_subscription'}).exec (err, product) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, prepaid.toObject())
|
return @sendNotFoundError(res, 'prepaid_subscription product not found') if not product
|
||||||
|
|
||||||
|
@purchasePrepaidTerminalSubscription req.user, description, maxRedeemers, months, timestamp, token, product, (err, prepaid) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, prepaid.toObject())
|
||||||
|
|
||||||
else if req.body.type is 'course'
|
else if req.body.type is 'course'
|
||||||
maxRedeemers = parseInt(req.body.maxRedeemers)
|
maxRedeemers = parseInt(req.body.maxRedeemers)
|
||||||
|
@ -145,18 +148,22 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
|
|
||||||
return @sendBadInputError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0
|
return @sendBadInputError(res) unless isNaN(maxRedeemers) is false and maxRedeemers > 0
|
||||||
|
|
||||||
@purchasePrepaidCourse req.user, maxRedeemers, timestamp, token, (err, prepaid) =>
|
Product.findOne({name: 'course'}).exec (err, product) =>
|
||||||
# TODO: this badinput detection is fragile, in course instance handler as well
|
|
||||||
return @sendBadInputError(res, err) if err is 'Missing required Stripe token'
|
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, prepaid.toObject())
|
return @sendNotFoundError(res, 'course product not found') if not product
|
||||||
|
|
||||||
|
@purchasePrepaidCourse req.user, maxRedeemers, timestamp, token, product, (err, prepaid) =>
|
||||||
|
# TODO: this badinput detection is fragile, in course instance handler as well
|
||||||
|
return @sendBadInputError(res, err) if err is 'Missing required Stripe token'
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, prepaid.toObject())
|
||||||
else
|
else
|
||||||
@sendForbiddenError(res)
|
@sendForbiddenError(res)
|
||||||
|
|
||||||
purchasePrepaidCourse: (user, maxRedeemers, timestamp, token, done) ->
|
purchasePrepaidCourse: (user, maxRedeemers, timestamp, token, product, done) ->
|
||||||
type = 'course'
|
type = 'course'
|
||||||
|
|
||||||
amount = maxRedeemers * 400
|
amount = maxRedeemers * product.get('amount')
|
||||||
if amount > 0 and not (token or user.isAdmin())
|
if amount > 0 and not (token or user.isAdmin())
|
||||||
@logError(user, "Purchase prepaid courses missing required Stripe token #{amount}")
|
@logError(user, "Purchase prepaid courses missing required Stripe token #{amount}")
|
||||||
return done('Missing required Stripe token')
|
return done('Missing required Stripe token')
|
||||||
|
@ -190,7 +197,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
hipchat.sendHipChatMessage msg, ['tower']
|
hipchat.sendHipChatMessage msg, ['tower']
|
||||||
@createPrepaid(user, type, maxRedeemers, {}, done)
|
@createPrepaid(user, type, maxRedeemers, {}, done)
|
||||||
|
|
||||||
purchasePrepaidTerminalSubscription: (user, description, maxRedeemers, months, timestamp, token, done) ->
|
purchasePrepaidTerminalSubscription: (user, description, maxRedeemers, months, timestamp, token, product, done) ->
|
||||||
type = 'terminal_subscription'
|
type = 'terminal_subscription'
|
||||||
|
|
||||||
StripeUtils.getCustomer user, token, (err, customer) =>
|
StripeUtils.getCustomer user, token, (err, customer) =>
|
||||||
|
@ -207,7 +214,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
|
||||||
months: months
|
months: months
|
||||||
productID: "prepaid #{type}"
|
productID: "prepaid #{type}"
|
||||||
|
|
||||||
amount = utils.getPrepaidCodeAmount(@baseAmount, maxRedeemers, months)
|
amount = utils.getPrepaidCodeAmount(product.get('amount'), maxRedeemers, months)
|
||||||
|
|
||||||
StripeUtils.createCharge user, amount, metadata, (err, charge) =>
|
StripeUtils.createCharge user, amount, metadata, (err, charge) =>
|
||||||
if err
|
if err
|
||||||
|
|
2
server/routes/index.coffee
Normal file
2
server/routes/index.coffee
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports.setup = (app) ->
|
||||||
|
app.get('/db/products', require('./db/products').get)
|
|
@ -14,6 +14,7 @@ user = require './server/users/user_handler'
|
||||||
logging = require './server/commons/logging'
|
logging = require './server/commons/logging'
|
||||||
config = require './server_config'
|
config = require './server_config'
|
||||||
auth = require './server/routes/auth'
|
auth = require './server/routes/auth'
|
||||||
|
routes = require './server/routes'
|
||||||
UserHandler = require './server/users/user_handler'
|
UserHandler = require './server/users/user_handler'
|
||||||
hipchat = require './server/hipchat'
|
hipchat = require './server/hipchat'
|
||||||
global.tv4 = require 'tv4' # required for TreemaUtils to work
|
global.tv4 = require 'tv4' # required for TreemaUtils to work
|
||||||
|
@ -166,6 +167,7 @@ setupFacebookCrossDomainCommunicationRoute = (app) ->
|
||||||
res.sendfile path.join(__dirname, 'public', 'channel.html')
|
res.sendfile path.join(__dirname, 'public', 'channel.html')
|
||||||
|
|
||||||
exports.setupRoutes = (app) ->
|
exports.setupRoutes = (app) ->
|
||||||
|
routes.setup(app)
|
||||||
app.use app.router
|
app.use app.router
|
||||||
|
|
||||||
baseRoute.setup app
|
baseRoute.setup app
|
||||||
|
|
|
@ -67,5 +67,14 @@ describe('Server Test Helper', function() {
|
||||||
if (err) { console.log(err); }
|
if (err) { console.log(err); }
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes products', function(done) {
|
||||||
|
var request = require('request');
|
||||||
|
request.get(getURL('/db/products'), function(err, res, body) {
|
||||||
|
expect(err).toBe(null);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
done();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
Loading…
Reference in a new issue