* VRFS-3028 - switch to using Recurly Adjustments for JamTrack purchases

This commit is contained in:
Seth Call 2015-04-10 15:19:08 -05:00
parent c230f57592
commit e42b926a5e
24 changed files with 860 additions and 361 deletions

View File

@ -2,12 +2,9 @@ ActiveAdmin.register_page "Recurly Health" do
menu :parent => 'Misc'
content :title => "Recurly Transaction Totals" do
table_for Sale.check_integrity do
table_for Sale.check_integrity_of_jam_track_sales do
column "Total", :total
column "Unknown", :not_known
column "Successes", :succeeded
column "Failures", :failed
column "Refunds", :refunded
column "Voids", :voided
end
end

View File

@ -274,4 +274,5 @@ recording_client_metadata.sql
preview_support_mp3.sql
jam_track_duration.sql
sales.sql
show_whats_next_count.sql
show_whats_next_count.sql
recurly_adjustments.sql

View File

@ -0,0 +1,24 @@
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE sales ADD COLUMN recurly_invoice_id VARCHAR(500) UNIQUE;
ALTER TABLE sales ADD COLUMN recurly_invoice_number INTEGER;
ALTER TABLE sales ADD COLUMN recurly_subtotal_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_discount_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN sale_type VARCHAR NOT NULL;
ALTER TABLE recurly_transaction_web_hooks ALTER COLUMN subscription_id DROP NOT NULL;
CREATE INDEX recurly_transaction_web_hooks_invoice_id_ndx ON recurly_transaction_web_hooks(invoice_id);
ALTER TABLE jam_track_rights DROP COLUMN recurly_subscription_uuid;

View File

@ -69,6 +69,7 @@ require "jam_ruby/connection_manager"
require "jam_ruby/version"
require "jam_ruby/environment"
require "jam_ruby/init"
require "jam_ruby/app/mailers/admin_mailer"
require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/mailers/corp_mailer"

View File

@ -0,0 +1,22 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
include SendGrid
DEFAULT_SENDER = "JamKazam <noreply@jamkazam.com>"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
def alerts(options)
mail(to: APP_CONFIG.email_alerts_alias,
body: options[:body],
content_type: "text/plain",
subject: options[:subject])
end
end
end

View File

@ -614,7 +614,8 @@ module JamRuby
def synchronize_recurly(jam_track)
begin
recurly = RecurlyClient.new
recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
# no longer create JamTrack plans: VRFS-3028
# recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
rescue RecurlyClientError => x
finish('recurly_create_plan', x.errors.to_s)
return false

View File

@ -199,5 +199,6 @@ module JamRuby
def right_for_user(user)
jam_track_rights.where("user_id=?", user).first
end
end
end

View File

@ -4,7 +4,6 @@ module JamRuby
belongs_to :user, class_name: 'JamRuby::User'
validates :recurly_transaction_id, presence: true
validates :subscription_id, presence: true
validates :action, presence: true
validates :status, presence: true
validates :amount_in_cents, numericality: {only_integer: true}
@ -68,8 +67,42 @@ module JamRuby
# now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
right = JamTrackRight.find_by_recurly_subscription_uuid(transaction.subscription_id)
right.destroy if right
sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
if sale && sale.is_jam_track_sale?
if sale.sale_line_items.length == 1
if sale.recurly_total_in_cents == transaction.amount_in_cents
jam_track = sale.sale_line_items[0].product
jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
if jam_track_right
jam_track_right.destroy
AdminMailer.alerts({
subject:"NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
body: "A void event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
}).deliver
else
AdminMailer.alerts({
subject:"NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
}).deliver
end
else
AdminMailer.alerts({
subject:"ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
body: "We received a refund notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
}).deliver
end
else
AdminMailer.alerts({
subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
end
end
end
transaction
end

View File

@ -3,30 +3,288 @@ module JamRuby
# a sale is created every time someone tries to buy something
class Sale < ActiveRecord::Base
JAMTRACK_SALE = 'jamtrack'
belongs_to :user, class_name: 'JamRuby::User'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
validates :order_total, numericality: { only_integer: false }
validates :order_total, numericality: {only_integer: false}
validates :user, presence: true
def self.create(user)
def self.preview_invoice(current_user, shopping_carts)
line_items = {jam_tracks: []}
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
line_items[:jam_tracks] = jam_track_items if jam_track_items
# TODO: process shopping_carts_subscriptions
line_items
end
# place_order will create one or more sales based on the contents of shopping_carts for the current user
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
def self.place_order(current_user, shopping_carts)
sales = []
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
sales
end
def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
### XXX TODO;
# we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
# if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
# just a pain to implement
end
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
client = RecurlyClient.new
sale = nil
Sale.transaction do
sale = create_jam_track_sale(current_user)
if sale.valid?
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
# now invoice the sale ... almost done
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
puts "Sale Line Items #{sale.sale_line_items.inspect}"
puts "----"
puts "Invoice Line Items #{invoice.line_items.inspect}"
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
if !found_line_item
@@loge.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
end
unless sale.save
raise RecurlyClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
end
else
raise RecurlyClientError, "Could not find account to place order."
end
else
raise RecurlyClientError, "Invalid sale."
end
end
sale
end
def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
created_adjustments = []
begin
shopping_carts_jam_tracks.each do |shopping_cart|
process_jam_track(current_user, shopping_cart, account, created_adjustments)
end
rescue Recurly::Error, NoMethodError => x
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise RecurlyClientError, x.to_s
rescue Exception => e
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise e
end
created_adjustments
end
def process_jam_track(current_user, shopping_cart, account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
# we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
shopping_cart.reload
# get the JamTrack in this shopping cart
jam_track = shopping_cart.cart_product
if jam_track.right_for_user(current_user)
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
return
end
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
adjustments.each do |adjustment|
# create the adjustment at Recurly (this may not look like it, but it is a REST API)
created_adjustment = account.adjustments.new(adjustment)
created_adjustment.save
# if the adjustment could not be made, bail
raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?
# keep track of adjustments we created for this order, in case we have to roll them back
created_adjustments << created_adjustment
if ShoppingCart.is_product_purchase?(adjustment)
# this was a normal product adjustment, so track it as such
recurly_adjustment_uuid = created_adjustment.uuid
else
# this was a 'credit' adjustment, so track it as such
recurly_adjustment_credit_uuid = created_adjustment.uuid
end
end
# create one sale line item for every jam track
sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
# if the sale line item is invalid, blow up the transaction
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
raise RecurlyClientError.new(sale_line_item.errors)
end
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = shopping_cart.free?
end
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if shopping_cart.free?
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
unless jam_track_right.save
raise RecurlyClientError.new(jam_track_right.errors)
end
end
# delete the shopping cart; it's been dealt with
shopping_cart.destroy if shopping_cart
# blow up the transaction if the JamTrackRight did not get created
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
end
def rollback_adjustments(current_user, adjustments)
begin
adjustments.each { |adjustment| adjustment.destroy }
rescue Exception => e
AdminMailer.alerts({
subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
}).deliver
end
end
def self.purge_pending_adjustments(account)
account.adjustments.pending.find_each do |adjustment|
# we only pre-emptively destroy pending adjustments if they appear to be created by the server
adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
end
end
def is_jam_track_sale?
sale_type == JAMTRACK_SALE
end
def self.create_jam_track_sale(user)
sale = Sale.new
sale.user = user
sale.sale_type = JAMTRACK_SALE
sale.order_total = 0
sale.save
sale
end
def self.check_integrity
SaleLineItem.select([:total, :not_known, :succeeded, :failed, :refunded, :voided]).find_by_sql(
"SELECT COUNT(sale_line_items.id) AS total,
COUNT(CASE WHEN transactions.id IS NULL THEN 1 ELSE null END) not_known,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT}' THEN 1 ELSE null END) succeeded,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::FAILED_PAYMENT}' THEN 1 ELSE null END) failed,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::REFUND}' THEN 1 ELSE null END) refunded,
# this checks just jamtrack sales appropriately
def self.check_integrity_of_jam_track_sales
Sale.select([:total, :voided]).find_by_sql(
"SELECT COUNT(sales.id) AS total,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
FROM sale_line_items
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON subscription_id = recurly_subscription_uuid")
FROM sales
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
WHERE sale_type = '#{JAMTRACK_SALE}'")
end
end
end

View File

@ -18,10 +18,15 @@ module JamRuby
validates :recurly_plan_code, presence:true
validates :sale, presence:true
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid)
def product
# TODO: beef up if there is more than one sort of sale
JamTrack.find(product_id)
end
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
product_info = shopping_cart.product_info
sale.order_total = sale.order_total + product_info[:total_price]
sale.order_total = sale.order_total + product_info[:real_price]
sale_line_item = SaleLineItem.new
sale_line_item.product_type = shopping_cart.cart_type
@ -33,7 +38,9 @@ module JamRuby
sale_line_item.recurly_plan_code = product_info[:plan_code]
sale_line_item.product_id = shopping_cart.cart_id
sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid
sale_line_item.sale = sale
sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid
sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
sale.sale_line_items << sale_line_item
sale_line_item.save
sale_line_item
end

View File

@ -1,8 +1,19 @@
module JamRuby
class ShoppingCart < ActiveRecord::Base
# just a normal purchase; used on the description field of a recurly adjustment
PURCHASE_NORMAL = 'purchase-normal'
# a free purchase; used on the description field of a recurly adjustment
PURCHASE_FREE = 'purchase-free'
# a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment
PURCHASE_FREE_CREDIT = 'purchase-free-credit'
PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT]
attr_accessible :quantity, :cart_type, :product_info
validates_uniqueness_of :cart_id, scope: :cart_type
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
validates :cart_id, presence: true
@ -14,14 +25,20 @@ module JamRuby
def product_info
product = self.cart_product
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil?
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil?
end
# multiply quantity by price
def total_price(product)
quantity * product.price
end
# multiply (quantity - redeemable) by price
def total_price(product)
def real_price(product)
(quantity - marked_for_redeem) * product.price
end
def cart_product
self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
end
@ -51,6 +68,59 @@ module JamRuby
cart
end
def is_jam_track?
cart_type == JamTrack::PRODUCT_TYPE
end
# returns an array of adjustments for the shopping cart
def create_adjustment_attributes(current_user)
raise "not a jam track" unless is_jam_track?
info = self.product_info
if free?
# create the credit, then the pseudo charge
[
{
accounting_code: PURCHASE_FREE_CREDIT,
currency: 'USD',
unit_amount_in_cents: -(info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name] + " (Credit)",
tax_exempt: true
},
{
accounting_code: PURCHASE_FREE,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: true
}
]
else
[
{
accounting_code: PURCHASE_NORMAL,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: false
}
]
end
end
def self.is_product_purchase?(adjustment)
(adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT)
end
# recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment)
# this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).'
def self.is_server_pending_adjustment?(recurly_adjustment)
recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT))
end
# if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked.
# if no shpping carts have been marked, then mark it redeemable
# should be wrapped in a TRANSACTION
@ -73,20 +143,8 @@ module JamRuby
def self.add_jam_track_to_cart(any_user, jam_track)
cart = nil
ShoppingCart.transaction do
# does this user already have this JamTrack in their cart? If so, don't add it.
duplicate_found = false
any_user.shopping_carts.each do |shopping_cart|
if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.cart_id == jam_track.id
duplicate_found = true
return
end
end
unless duplicate_found
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
cart
end

View File

@ -1,6 +1,6 @@
require 'recurly'
module JamRuby
class RecurlyClient
class RecurlyClient
def initialize()
@log = Logging.logger[self]
end
@ -11,37 +11,37 @@ module JamRuby
begin
#puts "Recurly.api_key: #{Recurly.api_key}"
account = Recurly::Account.create(options)
raise RecurlyClientError.new(account.errors) if account.errors.any?
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError.new(account.errors) if account.errors.any?
rescue Recurly::Error, NoMethodError => x
#puts "Error: #{x} : #{Kernel.caller}"
raise RecurlyClientError, x.to_s
else
if account
current_user.update_attribute(:recurly_code, account.account_code)
end
end
account
current_user.update_attribute(:recurly_code, account.account_code)
end
end
account
end
def has_account?(current_user)
account = get_account(current_user)
account = get_account(current_user)
!!account
end
def delete_account(current_user)
account = get_account(current_user)
if (account)
account = get_account(current_user)
if (account)
begin
account.destroy
account.destroy
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
else
else
raise RecurlyClientError, "Could not find account to delete."
end
account
end
def get_account(current_user)
current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil
rescue Recurly::Error => x
@ -51,9 +51,9 @@ module JamRuby
def update_account(current_user, billing_info=nil)
account = get_account(current_user)
if(account.present?)
options = account_hash(current_user, billing_info)
options = account_hash(current_user, billing_info)
begin
account.update_attributes(options)
account.update_attributes(options)
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
@ -95,7 +95,7 @@ module JamRuby
raise RecurlyClientError, x.to_s
end
raise RecurlyClientError.new(account.errors) if account.errors.any?
raise RecurlyClientError.new(account.errors) if account.errors.any?
else
raise RecurlyClientError, "Could not find account to update billing info."
end
@ -121,21 +121,21 @@ module JamRuby
#puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}"
if(subscription.plan.plan_code == jam_track.plan_code)
subscription.terminate(:full)
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
terminated = true
end
end
end
if terminated
jam_track_right.destroy()
jam_track_right.destroy()
else
raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund."
end
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
else
raise RecurlyClientError, "Could not find account to refund order."
end
@ -177,89 +177,18 @@ module JamRuby
raise RecurlyClientError.new(plan.errors) if plan.errors.any?
end
def place_order(current_user, jam_track, shopping_cart, sale)
jam_track_right = nil
account = get_account(current_user)
if (account.present?)
begin
# see if we can find existing plan for this plan_code, which should occur for previous-in-time error scenarios
recurly_subscription_uuid = nil
account.subscriptions.find_each do |subscription|
if subscription.plan.plan_code == jam_track.plan_code
recurly_subscription_uuid = subscription.uuid
break
end
end
free = false
# this means we already have a subscription, so don't try to create a new one for the same plan (Recurly would fail this anyway)
unless recurly_subscription_uuid
# if the shopping cart was specified, see if the item should be free
free = shopping_cart.nil? ? false : shopping_cart.free?
# and if it's free, squish the charge to 0.
unit_amount_in_cents = free ? 0 : nil
subscription = Recurly::Subscription.create(:account=>account, :plan_code=>jam_track.plan_code, unit_amount_in_cents: unit_amount_in_cents)
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
# add a line item for the sale
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, subscription.uuid)
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale.errors.to_s, value:1})
end
# delete from shopping cart the subscription
shopping_cart.destroy if shopping_cart
recurly_subscription_uuid = subscription.uuid
end
#raise RecurlyClientError, "Plan code '#{paid_subscription.plan_code}' doesn't match jam track: '#{jam_track.plan_code}'" unless recurly_subscription_uuid
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = free
end
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if free
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_subscription_uuid != recurly_subscription_uuid
jam_track_right.recurly_subscription_uuid = recurly_subscription_uuid
jam_track_right.save
end
raise RecurlyClientError.new("Error creating jam_track_right for jam_track: #{jam_track.id}") if jam_track_right.nil?
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
raise RecurlyClientError.new(account.errors) if account.errors.any?
else
raise RecurlyClientError, "Could not find account to place order."
end
jam_track_right
end
def find_or_create_account(current_user, billing_info)
account = get_account(current_user)
if(account.nil?)
account = create_account(current_user, billing_info)
else
update_billing_info(current_user, billing_info)
end
account
end
account
end
private
def account_hash(current_user, billing_info)
options = {
@ -273,7 +202,7 @@ module JamRuby
country: current_user.country
}
}
options[:billing_info] = billing_info if billing_info
options
end
@ -282,11 +211,11 @@ module JamRuby
class RecurlyClientError < Exception
attr_accessor :errors
def initialize(data)
if data.respond_to?('has_key?')
self.errors = data
if data.respond_to?('has_key?')
self.errors = data
else
self.errors = {:message=>data.to_s}
end
end
end # initialize
def to_s

View File

@ -9,6 +9,7 @@ require 'spec_helper'
describe RecurlyTransactionWebHook do
let(:refund_xml) {'<?xml version="1.0" encoding="UTF-8"?>
<successful_refund_notification>
<account>
@ -120,8 +121,15 @@ describe RecurlyTransactionWebHook do
it "deletes jam_track_right when refunded" do
sale = Sale.create_jam_track_sale(@user)
sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
sale.recurly_total_in_cents = 216
sale.save!
# create a jam_track right, which should be whacked as soon as we craete the web hook
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de')
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'bleh')
shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
document = Nokogiri::XML(refund_xml)
@ -131,8 +139,16 @@ describe RecurlyTransactionWebHook do
end
it "deletes jam_track_right when voided" do
sale = Sale.create_jam_track_sale(@user)
sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
sale.recurly_total_in_cents = 216
sale.save!
# create a jam_track right, which should be whacked as soon as we craete the web hook
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de')
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'blah')
shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
document = Nokogiri::XML(void_xml)

View File

@ -1,72 +1,289 @@
require 'spec_helper'
describe Sale do
describe "check_integrity" do
describe "place_order" do
let(:user) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
let(:jamtrack) { FactoryGirl.create(:jam_track) }
let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i }
let(:client) { RecurlyClient.new }
let(:billing_info) {
info = {}
info[:first_name] = user.first_name
info[:last_name] = user.last_name
info[:address1] = 'Test Address 1'
info[:address2] = 'Test Address 2'
info[:city] = user.city
info[:state] = user.state
info[:country] = user.country
info[:zip] = '12345'
info[:number] = '4111-1111-1111-1111'
info[:month] = '08'
info[:year] = '2017'
info[:verification_value] = '111'
info
}
after(:each) do
if user.recurly_code
account = Recurly::Account.find(user.recurly_code)
if account.present?
account.destroy
end
end
end
it "for a free jam track" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
sales.should eq(user.sales)
sale = sales[0]
sale.recurly_invoice_id.should_not be_nil
sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_tax_in_cents.should eq(0)
sale.recurly_total_in_cents.should eq(0)
sale.recurly_currency.should eq('USD')
sale.order_total.should eq(0)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
sale_line_item.recurly_tax_in_cents.should eq(0)
sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale_line_item.recurly_currency.should eq('USD')
sale_line_item.recurly_discount_in_cents.should eq(0)
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(1)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
sale_line_item.product_id.should eq(jamtrack.id)
sale_line_item.recurly_subscription_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should_not be_nil
sale_line_item.recurly_adjustment_credit_uuid.should_not be_nil
sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid)
# verify subscription is in Recurly
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should_not be_nil
adjustments.should have(2).items
free_purchase= adjustments[0]
free_purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
free_purchase.accounting_code.should eq(ShoppingCart::PURCHASE_FREE)
free_purchase.description.should eq("JamTrack: " + jamtrack.name)
free_purchase.state.should eq('invoiced')
free_purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
free_credit = adjustments[1]
free_credit.unit_amount_in_cents.should eq(-(jamtrack.price * 100).to_i)
free_credit.accounting_code.should eq(ShoppingCart::PURCHASE_FREE_CREDIT)
free_credit.description.should eq("JamTrack: " + jamtrack.name + " (Credit)")
free_credit.state.should eq('invoiced')
free_credit.uuid.should eq(sale_line_item.recurly_adjustment_credit_uuid)
invoices = recurly_account.invoices
invoices.should have(1).items
invoice = invoices[0]
invoice.uuid.should eq(sale.recurly_invoice_id)
invoice.line_items.should have(2).items # should have both adjustments associated
invoice.line_items[0].should eq(free_credit)
invoice.line_items[1].should eq(free_purchase)
invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.total_in_cents.should eq(0)
invoice.state.should eq('collected')
# verify jam_track_rights data
user.jam_track_rights.should_not be_nil
user.jam_track_rights.should have(1).items
user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
user.jam_track_rights.last.redeemed.should be_true
user.has_redeemable_jamtrack.should be_false
end
it "for a normally priced jam track" do
user.has_redeemable_jamtrack = false
user.save!
shopping_cart = ShoppingCart.create user, jamtrack, 1, false
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
sales.should eq(user.sales)
sale = sales[0]
sale.recurly_invoice_id.should_not be_nil
sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_tax_in_cents.should eq(0)
sale.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_currency.should eq('USD')
sale.order_total.should eq(jamtrack.price)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
# validate we are storing pricing info from recurly
sale_line_item.recurly_tax_in_cents.should eq(0)
sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale_line_item.recurly_currency.should eq('USD')
sale_line_item.recurly_discount_in_cents.should eq(0)
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(0)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
sale_line_item.product_id.should eq(jamtrack.id)
sale_line_item.recurly_subscription_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should_not be_nil
sale_line_item.recurly_adjustment_credit_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
# verify subscription is in Recurly
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should_not be_nil
adjustments.should have(1).items
purchase= adjustments[0]
purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL)
purchase.description.should eq("JamTrack: " + jamtrack.name)
purchase.state.should eq('invoiced')
purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
invoices = recurly_account.invoices
invoices.should have(1).items
invoice = invoices[0]
invoice.uuid.should eq(sale.recurly_invoice_id)
invoice.line_items.should have(1).items # should have single adjustment associated
invoice.line_items[0].should eq(purchase)
invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.total_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.state.should eq('collected')
# verify jam_track_rights data
user.jam_track_rights.should_not be_nil
user.jam_track_rights.should have(1).items
user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
user.jam_track_rights.last.redeemed.should be_false
user.has_redeemable_jamtrack.should be_false
end
it "for a jamtrack already owned" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
shopping_cart = ShoppingCart.create user, jamtrack, 1, false
sales = Sale.place_order(user, [shopping_cart])
sales.should have(0).items
# also, verify that no earlier adjustments were affected
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should have(2).items
end
# this test counts on the fact that two adjustments are made when buying a free JamTrack
# so if we make the second adjustment invalid from Recurly's standpoint, then
# we can see if the first one is ultimately destroyed
it "rolls back created adjustments if error" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
# grab the real response; we will modify it to make a nil accounting code
adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
client.find_or_create_account(user, billing_info)
adjustment_attrs[1][:unit_amount_in_cents] = nil # invalid amount
ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs)
expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError)
user.reload
user.sales.should have(0).items
recurly_account = client.get_account(user)
recurly_account.adjustments.should have(0).items
end
it "rolls back adjustments created before the order" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
# create a single adjustment on the account
adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
recurly_account = client.get_account(user)
adjustment = recurly_account.adjustments.new (adjustment_attrs[0])
adjustment.save
adjustment.errors.any?.should be_false
sales = Sale.place_order(user, [shopping_cart])
user.reload
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should have(2).items # two adjustments are created for a free jamtrack; that should be all there is
end
end
describe "check_integrity_of_jam_track_sales" do
let(:user) { FactoryGirl.create(:user) }
let(:jam_track) { FactoryGirl.create(:jam_track) }
it "empty" do
check_integrity = Sale.check_integrity
check_integrity = Sale.check_integrity_of_jam_track_sales
check_integrity.length.should eq(1)
r = check_integrity[0]
r.total.to_i.should eq(0)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one unknown sale" do
sale = Sale.create(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
check_integrity = Sale.check_integrity
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(1)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one succeeded sale" do
sale = Sale.create(user)
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
check_integrity = Sale.check_integrity
check_integrity = Sale.check_integrity_of_jam_track_sales
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(1)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one failed sale" do
sale = Sale.create(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
FactoryGirl.create(:recurly_transaction_web_hook_failed, subscription_id: 'some_recurly_uuid')
check_integrity = Sale.check_integrity
it "one voided sale" do
sale = Sale.create_jam_track_sale(user)
sale.recurly_invoice_id = 'some_recurly_invoice_id'
sale.save!
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
FactoryGirl.create(:recurly_transaction_web_hook, transaction_type: RecurlyTransactionWebHook::VOID, invoice_id: 'some_recurly_invoice_id')
check_integrity = Sale.check_integrity_of_jam_track_sales
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(1)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
r.voided.to_i.should eq(1)
end
end
end

View File

@ -86,42 +86,7 @@ describe RecurlyClient do
found.state.should eq('closed')
end
it "can place order" do
sale = Sale.create(@user)
sale = Sale.find(sale.id)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1, true
history_items = @client.payment_history(@user).length
@client.find_or_create_account(@user, @billing_info)
expect{@client.place_order(@user, @jamtrack, shopping_cart, sale)}.not_to raise_error()
# verify jam_track_rights data
@user.jam_track_rights.should_not be_nil
@user.jam_track_rights.should have(1).items
@user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id)
# verify sales data
sale = Sale.find(sale.id)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(@jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(1)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(@jamtrack.plan_code)
sale_line_item.product_id.should eq(@jamtrack.id)
sale_line_item.recurly_subscription_uuid.should_not be_nil
sale_line_item.recurly_subscription_uuid.should eq(@user.jam_track_rights.last.recurly_subscription_uuid)
# verify subscription is in Recurly
subs = @client.get_account(@user).subscriptions
subs.should_not be_nil
subs.should have(1).items
@client.payment_history(@user).should have(history_items+1).items
end
=begin
it "can refund subscription" do
sale = Sale.create(@user)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
@ -141,18 +106,7 @@ describe RecurlyClient do
@jamtrack.reload
@jamtrack.jam_track_rights.should have(0).items
end
=end
it "detects error on double order" do
sale = Sale.create(@user)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
@client.find_or_create_account(@user, @billing_info)
jam_track_right = @client.place_order(@user, @jamtrack, shopping_cart, sale)
jam_track_right.recurly_subscription_uuid.should_not be_nil
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
jam_track_right2 = @client.place_order(@user, @jamtrack, shopping_cart, sale)
jam_track_right.should eq(jam_track_right2)
jam_track_right.recurly_subscription_uuid.should eq(jam_track_right.recurly_subscription_uuid)
end
end

View File

@ -3,6 +3,14 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws
def app_config
klass = Class.new do
def email_alerts_alias
'alerts@jamkazam.com'
end
def email_generic_from
'nobody@jamkazam.com'
end
def aws_bucket
JAMKAZAM_TESTING_BUCKET
end

View File

@ -99,7 +99,7 @@
var sub_total = 0.0
var taxes = 0.0
$.each(carts, function(index, cart) {
sub_total += parseFloat(cart.product_info.total_price)
sub_total += parseFloat(cart.product_info.real_price)
});
if(carts.length == 0) {
data.grand_total = '-.--'
@ -147,68 +147,45 @@
var planPricing = {}
var priceElement = $screen.find('.order-right-page .plan.jamtrack')
if(priceElement.length == 0) {
logger.error("unable to find price element for jamtrack");
app.notify({title: "Error Encountered", text: "Unable to find plan info for jam track"})
return false;
}
logger.debug("creating recurly pricing element for plan: " + gon.recurly_tax_estimate_jam_track_plan)
var effectiveQuantity = 0
context._.each(carts, function(cart) {
var priceElement = $screen.find('.order-right-page .plan[data-plan-code="' + cart.product_info.plan_code +'"]')
if(priceElement.length == 0) {
logger.error("unable to find price element for " + cart.product_info.plan_code, cart);
app.notify({title: "Error Encountered", text: "Unable to find plan info for " + cart.product_info.plan_code})
return false;
}
logger.debug("creating recurly pricing element for plan: " + cart.product_info.plan_code)
var pricing = context.recurly.Pricing();
pricing.plan_code = cart.product_info.plan_code;
pricing.resolved = false;
pricing.effective_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
planPricing[pricing.plan_code] = pricing;
// this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it.
pricing.on('change', function(price) {
var resolvedPrice = planPricing[this.plan_code];
if(!resolvedPrice) {
logger.error("unable to find price info in storage")
app.notify({title: "Error Encountered", text: "Unable to find plan info in storage"})
return;
}
else {
logger.debug("pricing resolved for plan: " + this.plan_code)
}
resolvedPrice.resolved = true;
var allResolved = true;
var totalTax = 0;
var totalPrice = 0;
// let's see if all plans have been resolved via API; and add up total price and taxes for display
$.each(planPricing, function(plan_code, priceObject) {
logger.debug("resolved recurly priceObject", priceObject)
if(!priceObject.resolved) {
allResolved = false;
return false;
}
else {
var unitTax = Number(priceObject.price.now.tax) * priceObject.effective_quantity;
totalTax += unitTax;
var totalUnitPrice = Number(priceObject.price.now.total) * priceObject.effective_quantity;
totalPrice += totalUnitPrice;
}
})
if(allResolved) {
$screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
$screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
}
else
{
logger.debug("still waiting on more plans to resolve")
}
})
pricing.attach(priceElement.eq(0))
effectiveQuantity += cart.product_info.quantity - cart.product_info.marked_for_redeem
})
var pricing = context.recurly.Pricing();
pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan;
pricing.resolved = false;
pricing.effective_quantity = 1
// this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it.
pricing.on('change', function(price) {
var totalTax = 0;
var totalPrice = 0;
var unitTax = Number(pricing.price.now.tax) * effectiveQuantity;
totalTax += unitTax;
var totalUnitPrice = Number(pricing.price.now.total) * effectiveQuantity;
totalPrice += totalUnitPrice;
$screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
$screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
})
pricing.attach(priceElement.eq(0))
}
}

View File

@ -18,27 +18,27 @@ class ApiRecurlyController < ApiController
if current_user
# keep reuse card up-to-date
User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time])
# keep reuse card up-to-date
User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time])
else
options = {
remote_ip: request.remote_ip,
first_name: billing_info[:first_name],
last_name: billing_info[:last_name],
email: params[:email],
password: params[:password],
password_confirmation: params[:password],
terms_of_service: terms_of_service,
instruments: [{ :instrument_id => 'other', :proficiency_level => 1, :priority => 1 }],
birth_date: nil,
location: { :country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]},
musician: true,
skip_recaptcha: true,
invited_user: nil,
fb_signup: nil,
signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm",
any_user: any_user,
reuse_card: reuse_card_next_time
remote_ip: request.remote_ip,
first_name: billing_info[:first_name],
last_name: billing_info[:last_name],
email: params[:email],
password: params[:password],
password_confirmation: params[:password],
terms_of_service: terms_of_service,
instruments: [{:instrument_id => 'other', :proficiency_level => 1, :priority => 1}],
birth_date: nil,
location: {:country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]},
musician: true,
skip_recaptcha: true,
invited_user: nil,
fb_signup: nil,
signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm",
any_user: any_user,
reuse_card: reuse_card_next_time
}
user = UserManager.new.signup(options)
@ -61,9 +61,9 @@ class ApiRecurlyController < ApiController
@account = @client.find_or_create_account(current_user, billing_info)
end
render :json=>account_json(@account)
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: { :message => x.inspect, errors: x.errors }, :status => 404
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
end
@ -71,86 +71,80 @@ class ApiRecurlyController < ApiController
@client.delete_account(current_user)
render json: {}, status: 200
rescue RecurlyClientError => x
render json: { :message => x.inspect, errors: x.errors}, :status => 404
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
# get Recurly account
def get_account
@account = @client.get_account(current_user)
render :json=>account_json(@account)
render :json => account_json(@account)
rescue RecurlyClientError => e
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# get Recurly payment history
def payment_history
@payments=@client.payment_history(current_user)
render :json=>{payments: @payments}
render :json => {payments: @payments}
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# update Recurly account
def update_account
@account=@client.update_account(current_user, params[:billing_info])
render :json=>account_json(@account)
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
@account=@client.update_account(current_user, params[:billing_info])
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# get Billing Information
def billing_info
@account = @client.get_account(current_user)
if @account
render :json=> account_json(@account)
render :json => account_json(@account)
else
render :json=> {}, :status => 404
render :json => {}, :status => 404
end
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# update Billing Information
def update_billing_info
@account=@client.update_billing_info(current_user, params[:billing_info])
render :json=> account_json(@account)
@account = @client.update_billing_info(current_user, params[:billing_info])
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
def place_order
error=nil
response = {jam_tracks:[]}
response = {jam_tracks: []}
sale = Sale.create(current_user)
sales = Sale.place_order(current_user, current_user.shopping_carts)
if sale.valid?
current_user.shopping_carts.each do |shopping_cart|
jam_track = shopping_cart.cart_product
# if shopping_cart has any marked_for_redeem, then we zero out the price by passing in 'free'
# NOTE: shopping_carts have the idea of quantity, but you should only be able to buy at most one JamTrack. So anything > 0 is considered free for a JamTrack
jam_track_right = @client.place_order(current_user, jam_track, shopping_cart, sale)
# build up the response object with JamTracks that were purchased.
# if this gets more complicated, we should switch to RABL
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
sales.each do |sale|
if sale.is_jam_track_sale?
sale.sale_line_items.each do |line_item|
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(current_user)
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
end
end
else
error = 'can not create sale'
end
if error
render json: { errors: {message:error}}, :status => 404
render json: {errors: {message: error}}, :status => 404
else
render :json=>response, :status=>200
render :json => response, :status => 200
end
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
private
private
def create_client
@client = RecurlyClient.new
end
@ -173,5 +167,5 @@ private
billing_info: billing_info
}
end
end # class

View File

@ -16,6 +16,7 @@ class ClientsController < ApplicationController
return
end
gon.recurly_tax_estimate_jam_track_plan = Rails.application.config.recurly_tax_estimate_jam_track_plan
render :layout => 'client'
end

View File

@ -115,10 +115,8 @@ script type='text/template' id='template-order-content'
.order-right-page
h2 PLACE ORDER
.recurly-data.hidden
= "{% _.each(data.carts, function(cart) { %}"
.plan data-plan-code="{{cart.product_info.plan_code}}"
input data-recurly="plan" type="text" value="{{cart.product_info.plan_code}}"
= "{% }); %}"
.plan.jamtrack data-plan-code="{{gon.recurly_tax_estimate_jam_track_plan}}"
input data-recurly="plan" type="text" value="{{gon.recurly_tax_estimate_jam_track_plan}}"
.order-summary
.place-order-center
a.button-orange.place-order href="#" PLACE YOUR ORDER

View File

@ -220,7 +220,7 @@ if defined?(Bundler)
# amount of time before we think the queue is stuck
config.signing_job_queue_max_time = 20 # 20 seconds
config.email_alerts_alias = 'nobody@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'
config.email_smtp_port = 587
@ -323,5 +323,6 @@ if defined?(Bundler)
config.one_free_jamtrack_per_user = true
config.nominated_jam_track = 'jamtrack-pearljam-alive'
config.recurly_tax_estimate_jam_track_plan = 'jamtrack-acdc-backinblack'
end
end

View File

@ -40,8 +40,6 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
# make sure plans are there
@recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
@recurlyClient.create_jam_track_plan(@jamtrack_pearljam_evenflow) unless @recurlyClient.find_jam_track_plan(@jamtrack_pearljam_evenflow)
end
@ -552,7 +550,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
sale = user.sales.first
sale.sale_line_items.length.should eq(2)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(acdc.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(acdc.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_acdc_backinblack.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_acdc_backinblack.id)
@ -560,7 +558,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
acdc_sale.free.should eq(0)
acdc_sale.unit_price.should eq(1.99)
acdc_sale.sale.should eq(sale)
pearljam_sale = SaleLineItem.find_by_recurly_subscription_uuid(pearljam.recurly_subscription_uuid)
pearljam_sale = SaleLineItem.find_by_recurly_adjustment_uuid(pearljam.recurly_adjustment_uuid)
pearljam_sale.recurly_plan_code.should eq(jamtrack_pearljam_evenflow.plan_code)
pearljam_sale.product_type.should eq('JamTrack')
pearljam_sale.product_id.should eq(jamtrack_pearljam_evenflow.id)
@ -660,7 +658,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
guy.sales.length.should eq(1)
sale = guy.sales.first
sale.sale_line_items.length.should eq(1)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(jam_track_right.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(jam_track_right.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_acdc_backinblack.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_acdc_backinblack.id)
@ -710,7 +708,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
guy.sales.length.should eq(2)
sale = guy.sales.last
sale.sale_line_items.length.should eq(1)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(jam_track_right.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(jam_track_right.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_pearljam_evenflow.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_pearljam_evenflow.id)

View File

@ -44,7 +44,7 @@ describe "Individual JamTrack Band", :js => true, :type => :feature, :capybara_f
@jamtrack_acdc_backinblack = FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack')
# make sure plans are there
# make sure tax estimate plans are there
@recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
end

View File

@ -187,6 +187,9 @@ bputs "before register capybara"
config.include Requests::JsonHelpers, type: :request
config.include Requests::FeatureHelpers, type: :feature
# Use the specified formatter
#config.formatter = :documentation
config.before(:suite) do
tests_started = true
end