Move product information to the db

This commit is contained in:
Scott Erickson 2015-12-14 11:10:37 -08:00
parent ae1621ea09
commit 7c516c4d9f
19 changed files with 334 additions and 261 deletions

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

View file

@ -244,7 +244,7 @@ module.exports.getCoursePraise = getCoursePraise = ->
]
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
total = price * users * months
total

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

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

View file

@ -39,7 +39,7 @@ block content
label.control-label.col-md-2(data-i18n="account_prepaid.purchase_total")
.col-md-10
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")
.row
.col-md-12
@ -66,7 +66,7 @@ block content
button#redeem-code-btn.btn.btn-success(data-i18n="account_prepaid.apply_account")
.row
.col-md-12
.panel.panel-default
#codes-panel.panel.panel-default
.panel-heading
.panel-title
a(data-toggle="collapse" href="#codeslist")

View file

@ -24,8 +24,11 @@
th.free-cell(data-i18n="subscribe.free")
th
//- TODO: find a better way to localize '$9.99/month'
span $#{(view.product.amount / 100)}/
span(data-i18n="subscribe.month")
if view.basicProduct
span $#{(view.basicProduct.get('amount') / 100)}/
span(data-i18n="subscribe.month")
else
span '...'
tbody
tr
td.feature-description

View file

@ -82,7 +82,7 @@ block content
span(data-i18n="account_prepaid.purchase_total")
span.spr : #{view.numberOfStudents}
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
button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now")

View file

@ -1,10 +1,10 @@
.modal-dialog
.modal-content
if state === 'purchasing'
if view.state === '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")
else
@ -12,12 +12,12 @@
h1(data-i18n="play.buy_gems")
#products
for product in products
for product in view.products.models
.product
h4 x#{product.gems}
h3(data-i18n=product.i18n)
button.btn.btn-illustrated.btn-lg(value=product.id)
span= product.price
h4 x#{product.get('gems')}
h3(data-i18n=product.get('i18n'))
button.btn.btn-illustrated.btn-lg(value=product.get('name'))
span= product.get('priceString')
.product
h4(data-i18n="buy_gems.price") x3500 / mo
@ -29,20 +29,20 @@
else
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
span(data-i18n="buy_gems.declined")
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") ×
if state === 'unknown_error'
if view.state === 'unknown_error'
#error-alert.alert.alert-danger.alert-dismissible
button.close(type="button" data-dismiss="alert")
span(aria-hidden="true") ×
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
span(data-i18n="buy_gems.recovered")
button.close(type="button" data-dismiss="alert")

View file

@ -7,6 +7,7 @@ Prepaid = require '../../models/Prepaid'
utils = require 'core/utils'
RedeemModal = require 'views/account/PrepaidRedeemModal'
forms = require 'core/forms'
Products = require 'collections/Products'
# TODO: remove redeem code modal
@ -26,12 +27,10 @@ module.exports = class PrepaidView extends RootView
subscriptions:
'stripe:received-token': 'onStripeReceivedToken'
baseAmount: 9.99
constructor: (options) ->
super(options)
@purchase =
total: @baseAmount
total: 0
users: 3
months: 3
@updateTotal()
@ -45,6 +44,14 @@ module.exports = class PrepaidView extends RootView
@ppcQuery = true
@loadPrepaid(@ppc)
@products = new Products()
@supermodel.loadCollection(@products, 'products')
onLoaded: ->
@prepaidProduct = @products.findWhere { name: 'prepaid_subscription' }
@updateTotal()
super()
getRenderData: ->
c = super()
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
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")
# Form Input Callbacks
@ -99,7 +107,7 @@ module.exports = class PrepaidView extends RootView
onClickPurchaseButton: (e) ->
return unless $("#users-input").val() >= 3 or $("#months-input").val() >= 3
@purchaseTimestamp = new Date().getTime()
@stripeAmount = @purchase.total * 100
@stripeAmount = @purchase.total
@description = "Prepaid Code for " + @purchase.users + " users / " + @purchase.months + " months"
stripeHandler.open
@ -179,6 +187,7 @@ module.exports = class PrepaidView extends RootView
# console.log 'SUCCESS: Prepaid purchase', model.code
@statusMessage "Successfully purchased Prepaid Code!", "success"
@codes.add(model)
@renderSelectors('#codes-panel')
@statusMessage "Finalizing purchase...", "information"
@supermodel.addRequestResource('purchase_prepaid', options, 0).load()

View file

@ -3,16 +3,14 @@ template = require 'templates/core/subscribe-modal'
stripeHandler = require 'core/services/stripe'
utils = require 'core/utils'
AuthModal = require 'views/core/AuthModal'
Products = require 'collections/Products'
module.exports = class SubscribeModal extends ModalView
id: 'subscribe-modal'
template: template
plain: true
closesOnClickOutside: false
product:
amount: 999
planID: 'basic'
yearAmount: 9900
planID: 'basic'
subscriptions:
'stripe:received-token': 'onStripeReceivedToken'
@ -27,6 +25,13 @@ module.exports = class SubscribeModal extends ModalView
constructor: (options) ->
super(options)
@state = 'standby'
@products = new Products()
@supermodel.loadCollection(@products, 'products')
onLoaded: ->
@basicProduct = @products.findWhere { name: 'basic_subscription' }
@yearProduct = @products.findWhere { name: 'year_subscription' }
super()
afterRender: ->
super()
@ -109,12 +114,13 @@ module.exports = class SubscribeModal extends ModalView
@$el.find('.parent-button').popover('hide')
onClickPurchaseButton: (e) ->
return unless @basicProduct and @yearProduct
@playSound 'menu-button-click'
return @openModalView new AuthModal() if me.get('anonymous')
application.tracker?.trackEvent 'Started subscription purchase'
options = {
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'
alipayReusable: true
}
@ -138,7 +144,7 @@ module.exports = class SubscribeModal extends ModalView
application.tracker?.trackEvent 'Started 1 year subscription purchase'
options =
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'
alipayReusable: true
@purchasedAmount = options.amount
@ -148,15 +154,15 @@ module.exports = class SubscribeModal extends ModalView
@state = 'purchasing'
@render()
if @purchasedAmount is @product.amount
if @purchasedAmount is @basicProduct.get('amount')
stripe = _.clone(me.get('stripe') ? {})
stripe.planID = @product.planID
stripe.planID = @basicProduct.get('planID')
stripe.token = e.token.id
me.set 'stripe', stripe
@listenToOnce me, 'sync', @onSubscriptionSuccess
@listenToOnce me, 'error', @onSubscriptionError
me.patch({headers: {'X-Change-Plan': 'true'}})
else if @purchasedAmount is @product.yearAmount
else if @purchasedAmount is @yearProduct.get('amount')
# Purchasing a year
data =
stripe:

View file

@ -9,12 +9,13 @@ stripeHandler = require 'core/services/stripe'
template = require 'templates/courses/purchase-courses-view'
User = require 'models/User'
utils = require 'core/utils'
Products = require 'collections/Products'
module.exports = class PurchaseCoursesView extends RootView
id: 'purchase-courses-view'
template: template
numberOfStudents: 30
pricePerStudent: 4
pricePerStudent: 0
initialize: (options) ->
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
@ -29,13 +30,19 @@ module.exports = class PurchaseCoursesView extends RootView
@prepaids.comparator = '_id'
@prepaids.fetchByCreator(me.id)
@supermodel.loadCollection(@prepaids, 'prepaids')
@products = new Products()
@supermodel.loadCollection(@products, 'products')
super(options)
events:
'input #students-input': 'onInputStudentsInput'
'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
onceClassroomsSync: ->
@ -80,7 +87,7 @@ module.exports = class PurchaseCoursesView extends RootView
application.tracker?.trackEvent 'Started course prepaid purchase', {
price: @pricePerStudent, students: @numberOfStudents}
stripeHandler.open
amount: @numberOfStudents * @pricePerStudent * 100
amount: @numberOfStudents * @pricePerStudent
description: "Full course access for #{@numberOfStudents} students"
bitcoin: true
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'

View file

@ -3,6 +3,7 @@ template = require 'templates/play/modal/buy-gems-modal'
stripeHandler = require 'core/services/stripe'
utils = require 'core/utils'
SubscribeModal = require 'views/core/SubscribeModal'
Products = require 'collections/Products'
module.exports = class BuyGemsModal extends ModalView
id: 'buy-gems-modal'
@ -29,22 +30,21 @@ module.exports = class BuyGemsModal extends ModalView
super(options)
@timestampForPurchase = new Date().getTime()
@state = 'standby'
@products = new Products()
@products.comparator = 'amount'
if application.isIPadApp
@products = []
Backbone.Mediator.publish 'buy-gems-modal:update-products'
else
@products = @originalProducts
@supermodel.loadCollection(@products, 'products')
$.post '/db/payment/check-stripe-charges', (something, somethingElse, jqxhr) =>
if jqxhr.status is 201
@state = 'recovered_charge'
@render()
getRenderData: ->
c = super()
c.products = @products
c.state = @state
c.stateMessage = @stateMessage
return c
onLoaded: ->
@products.reset @products.filter (product) -> _.string.startsWith(product.get('name'), 'gems_')
super()
afterRender: ->
super()
@ -56,19 +56,20 @@ module.exports = class BuyGemsModal extends ModalView
@playSound 'game-menu-close'
onIPadProducts: (e) ->
newProducts = []
for iapProduct in e.products
localProduct = _.find @originalProducts, { id: iapProduct.id }
continue unless localProduct
localProduct.price = iapProduct.price
newProducts.push localProduct
@products = _.sortBy newProducts, 'gems'
@render()
# TODO: Update to handle new products collection
# newProducts = []
# for iapProduct in e.products
# localProduct = _.find @originalProducts, { id: iapProduct.id }
# continue unless localProduct
# localProduct.price = iapProduct.price
# newProducts.push localProduct
# @products = _.sortBy newProducts, 'gems'
# @render()
onClickProductButton: (e) ->
@playSound 'menu-button-click'
productID = $(e.target).closest('button').val()
product = _.find @products, { id: productID }
product = @products.findWhere { name: productID }
if application.isIPadApp
Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID }
@ -76,8 +77,8 @@ module.exports = class BuyGemsModal extends ModalView
else
application.tracker?.trackEvent 'Started gem purchase', { productID: productID }
stripeHandler.open({
description: $.t(product.i18n)
amount: product.amount
description: $.t(product.get('i18n'))
amount: product.get('amount')
bitcoin: true
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) ->
data = {
productID: @productBeingPurchased.id
productID: @productBeingPurchased.get('name')
stripe: {
token: e.token.id
timestamp: @timestampForPurchase
@ -97,8 +98,8 @@ module.exports = class BuyGemsModal extends ModalView
jqxhr = $.post('/db/payment', data)
jqxhr.done(=>
application.tracker?.trackEvent 'Finished gem purchase',
productID: @productBeingPurchased.id
value: @productBeingPurchased.amount
productID: @productBeingPurchased.get('name')
value: @productBeingPurchased.get('amount')
document.location.reload()
)
jqxhr.fail(=>
@ -116,7 +117,7 @@ module.exports = class BuyGemsModal extends ModalView
)
onIAPComplete: (e) ->
product = _.find @products, { id: e.productID }
product = @products.findWhere { name: e.productID }
purchased = me.get('purchased') ? {}
purchased = _.clone purchased
purchased.gems ?= 0

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

View file

@ -1,4 +1,5 @@
Payment = require './Payment'
Product = require '../models/Product'
User = require '../users/User'
Handler = require '../commons/Handler'
{handlers} = require '../commons/mapping'
@ -11,30 +12,6 @@ request = require 'request'
async = require 'async'
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
modelClass: Payment
@ -134,33 +111,34 @@ PaymentHandler = class PaymentHandler extends Handler
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())
if validation.valid is false
@logPaymentError(req, 'Invalid apple payment object.')
return @sendBadInputError(res, validation.errors)
payment.save((err) =>
if err
@logPaymentError(req, 'Apple payment save error.'+err)
return @sendDatabaseError(res, err)
@incrementGemsFor(req.user, product.gems, (err) =>
Product.findOne({name: transaction.product_id}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if not product
payment.set 'amount', product.get('amount')
payment.set 'gems', product.get('gems')
payment.set 'ios', {
transactionID: transactionID
rawReceipt: receipt
localPrice: localPrice
}
validation = @validateDocumentInput(payment.toObject())
if validation.valid is false
@logPaymentError(req, 'Invalid apple payment object.')
return @sendBadInputError(res, validation.errors)
payment.save((err) =>
if err
@logPaymentError(req, 'Apple incr db error.'+err)
@logPaymentError(req, 'Apple payment save error.'+err)
return @sendDatabaseError(res, err)
@sendPaymentHipChatMessage user: req.user, payment: payment
@sendCreated(res, @formatEntity(req, payment))
@incrementGemsFor(req.user, product.get('gems'), (err) =>
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) ->
product = products[productID]
async.parallel([
((callback) ->
@ -218,6 +195,10 @@ PaymentHandler = class PaymentHandler extends Handler
charge = _.find recentCharges.data, (c) -> c.metadata.timestamp is timestamp
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
@logPaymentError(req, 'Stripe async load db error. '+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)
# Proceed normally from the beginning
@ -236,7 +220,7 @@ PaymentHandler = class PaymentHandler extends Handler
@recordStripeCharge(req, res, charge)
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.
@recalculateGemsFor(req.user, (err) =>
@ -250,7 +234,7 @@ PaymentHandler = class PaymentHandler extends Handler
)
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)
stripe.charges.create({
@ -258,9 +242,9 @@ PaymentHandler = class PaymentHandler extends Handler
currency: 'usd'
customer: req.user.get('stripe')?.customerID
metadata: {
productID: product.id
productID: product.get('name')
userID: req.user._id + ''
gems: product.gems
gems: product.get('gems')
timestamp: parseInt(req.body.stripe?.timestamp)
description: req.body.description
}

View file

@ -15,20 +15,10 @@ User = require '../users/User'
{getSponsoredSubsAmount} = require '../../app/core/utils'
StripeUtils = require '../lib/stripe_utils'
moment = require 'moment'
Product = require '../models/Product'
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
logSubscriptionError: (user, msg) ->
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
@ -149,51 +139,56 @@ class SubscriptionHandler extends Handler
if err
@logSubscriptionError(user, "Purchase year sale Stripe cancel subscription error: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
metadata =
type: req.body.type
userID: req.user._id + ''
gems: subscriptions.basic.gems * 12
timestamp: parseInt(req.body.stripe?.timestamp)
description: req.body.description
StripeUtils.createCharge req.user, subscriptions.year_sale.amount, metadata, (err, charge) =>
if err
@logSubscriptionError(req.user, "Purchase year sale create charge: #{JSON.stringify(err)}")
return @sendDatabaseError(res, err)
StripeUtils.createPayment req.user, charge, (err, payment) =>
Product.findOne({name: 'year_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'year_subscription product not found') if not product
metadata =
type: req.body.type
userID: req.user._id + ''
gems: product.get('gems')
timestamp: parseInt(req.body.stripe?.timestamp)
description: req.body.description
StripeUtils.createCharge req.user, product.get('amount'), metadata, (err, charge) =>
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)
# Add terminal subscription to User with extensions for existing subscriptions
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) =>
StripeUtils.createPayment req.user, charge, (err, payment) =>
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)
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)
# Add terminal subscription to User with extensions for existing subscriptions
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
@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) ->
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)}")
return @sendDatabaseError(res, err)
# Add terminal subscription to User, extending existing subscriptions
# TODO: refactor this into some form useable by both this and purchaseYearSale
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
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)
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
endDate = endDate.add(months, 'months')
stripeInfo.free = endDate.toISOString().substring(0, 10)
req.user.set('stripe', stripeInfo)
# Add gems to User
purchased = _.clone(req.user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += subscriptions.basic.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)
# Add terminal subscription to User, extending existing subscriptions
# TODO: refactor this into some form useable by both this and purchaseYearSale
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
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')
stripeInfo.free = endDate.toISOString().substring(0, 10)
req.user.set('stripe', stripeInfo)
# Add gems to User
purchased = _.clone(req.user.get('purchased'))
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) ->
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
@ -427,18 +426,22 @@ class SubscriptionHandler extends Handler
req.body.stripe = stripeInfo
user.set('stripe', stripeInfo)
if increment
purchased = _.clone(user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
purchased.gems += subscriptions.basic.gems # TODO: Put actual subscription amount here
user.set('purchased', purchased)
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
user.save (err) =>
if err
@logSubscriptionError(user, 'Stripe user plan saving error. ' + err)
return done({res: 'Database error.', code: 500})
done()
if increment
purchased = _.clone(user.get('purchased'))
purchased ?= {}
purchased.gems ?= 0
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) ->
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)
return done({res: 'Database error.', code: 500})
createUpdateFn = (recipient, increment) =>
(done) =>
# 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 += 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()
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
tasks = []
for sub in stripeRecipients
tasks.push createUpdateFn(sub.recipient, sub.increment)
createUpdateFn = (recipient, increment) =>
(done) =>
# 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) =>
return done(err) if err
@updateStripeSponsorSubscription(req, user, customer, done)
updateStripeSponsorSubscription: (req, user, customer, done) ->
updateStripeSponsorSubscription: (req, user, customer, product, done) ->
stripeInfo = user.get('stripe') ? {}
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) =>
if stripeInfo.sponsorSubscriptionID? and not subscription?
@ -656,38 +663,42 @@ class SubscriptionHandler extends Handler
@logSubscriptionError(user, 'Unable to find recipient subscription. ')
return done({res: 'Database error.', code: 500})
# Update recipient user
deleteUserStripeProp(recipient, 'sponsorID')
recipient.save (err) =>
if err
@logSubscriptionError(user, 'Recipient user save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
Product.findOne({name: 'basic_subscription'}).exec (err, product) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'basic_subscription product not found') if not product
# Cancel Stripe subscription
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (err) =>
# Update recipient user
deleteUserStripeProp(recipient, 'sponsorID')
recipient.save (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})
# Update sponsor user
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
delete stripeInfo.unsubscribeEmail
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
# Cancel Stripe subscription
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (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() unless stripeInfo.sponsorSubscriptionID?
# Update sponsored subscription quantity
options =
quantity: getSponsoredSubsAmount(subscriptions.basic.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?)
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
# Update sponsor user
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
delete stripeInfo.unsubscribeEmail
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (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})
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()

View file

@ -6,6 +6,7 @@ User = require '../users/User'
StripeUtils = require '../lib/stripe_utils'
utils = require '../../app/core/utils'
mongoose = require 'mongoose'
Product = require '../models/Product'
# 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
@ -17,8 +18,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
jsonSchema: require '../../app/schemas/models/prepaid.schema'
allowedMethods: ['GET','POST']
baseAmount: 999
logError: (user, 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 @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
@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'
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
@purchasePrepaidCourse req.user, maxRedeemers, timestamp, token, (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'
Product.findOne({name: 'course'}).exec (err, product) =>
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
@sendForbiddenError(res)
purchasePrepaidCourse: (user, maxRedeemers, timestamp, token, done) ->
purchasePrepaidCourse: (user, maxRedeemers, timestamp, token, product, done) ->
type = 'course'
amount = maxRedeemers * 400
amount = maxRedeemers * product.get('amount')
if amount > 0 and not (token or user.isAdmin())
@logError(user, "Purchase prepaid courses missing required Stripe token #{amount}")
return done('Missing required Stripe token')
@ -190,7 +197,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
hipchat.sendHipChatMessage msg, ['tower']
@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'
StripeUtils.getCustomer user, token, (err, customer) =>
@ -207,7 +214,7 @@ PrepaidHandler = class PrepaidHandler extends Handler
months: months
productID: "prepaid #{type}"
amount = utils.getPrepaidCodeAmount(@baseAmount, maxRedeemers, months)
amount = utils.getPrepaidCodeAmount(product.get('amount'), maxRedeemers, months)
StripeUtils.createCharge user, amount, metadata, (err, charge) =>
if err

View file

@ -0,0 +1,2 @@
module.exports.setup = (app) ->
app.get('/db/products', require('./db/products').get)

View file

@ -14,6 +14,7 @@ user = require './server/users/user_handler'
logging = require './server/commons/logging'
config = require './server_config'
auth = require './server/routes/auth'
routes = require './server/routes'
UserHandler = require './server/users/user_handler'
hipchat = require './server/hipchat'
global.tv4 = require 'tv4' # required for TreemaUtils to work
@ -166,6 +167,7 @@ setupFacebookCrossDomainCommunicationRoute = (app) ->
res.sendfile path.join(__dirname, 'public', 'channel.html')
exports.setupRoutes = (app) ->
routes.setup(app)
app.use app.router
baseRoute.setup app

View file

@ -67,5 +67,14 @@ describe('Server Test Helper', function() {
if (err) { console.log(err); }
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();
});
})
});