diff --git a/admin/app/admin/recurly_health.rb b/admin/app/admin/recurly_health.rb index 518648403..aebd21250 100644 --- a/admin/app/admin/recurly_health.rb +++ b/admin/app/admin/recurly_health.rb @@ -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 diff --git a/db/manifest b/db/manifest index ba11edce4..6634d9381 100755 --- a/db/manifest +++ b/db/manifest @@ -274,4 +274,5 @@ recording_client_metadata.sql preview_support_mp3.sql jam_track_duration.sql sales.sql -show_whats_next_count.sql \ No newline at end of file +show_whats_next_count.sql +recurly_adjustments.sql \ No newline at end of file diff --git a/db/up/recurly_adjustments.sql b/db/up/recurly_adjustments.sql new file mode 100644 index 000000000..d86e6ba89 --- /dev/null +++ b/db/up/recurly_adjustments.sql @@ -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; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index da4fc6a48..69718cdf9 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -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" diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb new file mode 100644 index 000000000..c4addb5cc --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -0,0 +1,22 @@ +module JamRuby + # sends out a boring ale + class AdminMailer < ActionMailer::Base + include SendGrid + + + DEFAULT_SENDER = "JamKazam " + + 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 diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 58138dbf9..5535f6e08 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 5c8eb1d3a..b08533c1e 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -199,5 +199,6 @@ module JamRuby def right_for_user(user) jam_track_rights.where("user_id=?", user).first end + end end diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb index 0139cba9e..1d3232169 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 63f42657b..06addb8e3 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -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 \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index f90b460fa..0685244f2 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index b2bbc13ce..ed74d2490 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -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 diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index ce9f44f9f..1ff419667 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -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 diff --git a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb index 3f69ce5eb..ba89aee20 100644 --- a/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb +++ b/ruby/spec/jam_ruby/models/recurly_transaction_web_hook_spec.rb @@ -9,6 +9,7 @@ require 'spec_helper' describe RecurlyTransactionWebHook do + let(:refund_xml) {' @@ -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) diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 0c9b089b1..91d298b32 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -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 diff --git a/ruby/spec/jam_ruby/recurly_client_spec.rb b/ruby/spec/jam_ruby/recurly_client_spec.rb index cf51c4b8c..108fe1600 100644 --- a/ruby/spec/jam_ruby/recurly_client_spec.rb +++ b/ruby/spec/jam_ruby/recurly_client_spec.rb @@ -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 diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 94294b86c..5bed19487 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -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 diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js index 9024f5745..bbf95b98b 100644 --- a/web/app/assets/javascripts/checkout_order.js +++ b/web/app/assets/javascripts/checkout_order.js @@ -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)) } } diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index 44b50c48a..d69b7b3cf 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -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 \ No newline at end of file diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 928715adf..51813a8d3 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -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 diff --git a/web/app/views/clients/_checkout_order.html.slim b/web/app/views/clients/_checkout_order.html.slim index 76db4d7ff..ed3578d68 100644 --- a/web/app/views/clients/_checkout_order.html.slim +++ b/web/app/views/clients/_checkout_order.html.slim @@ -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 diff --git a/web/config/application.rb b/web/config/application.rb index 33ec5248a..173220b55 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -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 diff --git a/web/spec/features/checkout_spec.rb b/web/spec/features/checkout_spec.rb index 89b37a366..9484ceed7 100644 --- a/web/spec/features/checkout_spec.rb +++ b/web/spec/features/checkout_spec.rb @@ -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) diff --git a/web/spec/features/individual_jamtrack_band_spec.rb b/web/spec/features/individual_jamtrack_band_spec.rb index 5db9ac971..b72e10982 100644 --- a/web/spec/features/individual_jamtrack_band_spec.rb +++ b/web/spec/features/individual_jamtrack_band_spec.rb @@ -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 diff --git a/web/spec/spec_helper.rb b/web/spec/spec_helper.rb index fdb947180..20ca30779 100644 --- a/web/spec/spec_helper.rb +++ b/web/spec/spec_helper.rb @@ -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