diff --git a/admin/app/admin/jam_track_right.rb b/admin/app/admin/jam_track_right.rb index 48da8231a..e2cc0bbe9 100644 --- a/admin/app/admin/jam_track_right.rb +++ b/admin/app/admin/jam_track_right.rb @@ -1,20 +1,29 @@ require 'jam_ruby/recurly_client' ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do - menu :label => 'Purchased JamTracks', :parent => 'JamTracks' + menu :label => 'Purchased JamTracks', :parent => 'Purchases' - config.sort_order = 'updated_at DESC' + config.sort_order = 'created_at DESC' config.batch_actions = false #form :partial => 'form' + filter :user_id + + filter :user_id, + :label => "USER ID", :required => false, + :wrapper_html => { :style => "list-style: none" } + + + filter :jam_track + index do default_actions - column "Order" do |right| - link_to("Place", order_admin_jam_track_right_path(right)) + " | " + - link_to("Refund", refund_admin_jam_track_right_path(right)) - end + #column "Order" do |right| + #link_to("Place", order_admin_jam_track_right_path(right)) + " | " + + # link_to("Refund", refund_admin_jam_track_right_path(right)) + #end column "Last Name" do |right| right.user.last_name @@ -23,13 +32,15 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do right.user.first_name end column "Jam Track" do |right| - link_to(right.jam_track.name, admin_jam_track_right_path(right.jam_track)) + link_to(right.jam_track.name, admin_jam_track_path(right.jam_track)) # right.jam_track end column "Plan Code" do |right| - right.jam_track.plan_code end + column "Redeemed" do |right| + right.redeemed ? 'Y' : 'N' + end end @@ -42,6 +53,9 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do f.actions end +=begin + + member_action :order, :method => :get do right = JamTrackRight.where("id=?",params[:id]).first user = right.user @@ -84,4 +98,5 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do redirect_to admin_jam_track_rights_path, notice: "Issued full refund on #{right.jam_track} for #{right.user.to_s}" end end +=end end \ No newline at end of file diff --git a/admin/app/admin/recurly_transaction_web_hook.rb b/admin/app/admin/recurly_transaction_web_hook.rb new file mode 100644 index 000000000..0c6942d82 --- /dev/null +++ b/admin/app/admin/recurly_transaction_web_hook.rb @@ -0,0 +1,40 @@ +ActiveAdmin.register JamRuby::RecurlyTransactionWebHook, :as => 'RecurlyHooks' do + + menu :label => 'Recurly Transaction Hooks', :parent => 'Purchases' + + config.sort_order = 'created_at DESC' + config.batch_actions = false + + actions :all, :except => [:destroy] + + #form :partial => 'form' + + + filter :transaction_type, :as => :select, :collection => JamRuby::RecurlyTransactionWebHook::HOOK_TYPES + filter :user_id, + :label => "USER ID", :required => false, + :wrapper_html => { :style => "list-style: none" } + + filter :invoice_id + + form :partial => 'form' + + index do + + default_actions + + column :transaction_type + column :transaction_at + column :amount_in_cents + column 'Transaction' do |hook| link_to('Go to Recurly', Rails.application.config.recurly_root_url + "/transactions/#{hook.recurly_transaction_id}") end + column 'Invoice' do |hook| link_to(hook.invoice_number, Rails.application.config.recurly_root_url + "/invoices/#{hook.invoice_number}") end + column :admin_description + column 'User' do |hook| link_to("#{hook.user.email} (#{hook.user.name})", admin_user_path(hook.user.id)) end + + #column "Order" do |right| + #link_to("Place", order_admin_jam_track_right_path(right)) + " | " + + # link_to("Refund", refund_admin_jam_track_right_path(right)) + #end + end + +end \ No newline at end of file diff --git a/admin/app/views/admin/recurly_hooks/_form.html.slim b/admin/app/views/admin/recurly_hooks/_form.html.slim new file mode 100644 index 000000000..3e1305e23 --- /dev/null +++ b/admin/app/views/admin/recurly_hooks/_form.html.slim @@ -0,0 +1,6 @@ += semantic_form_for([:admin, resource], :html => {:multipart => true}, :url => resource.new_record? ? admin_recurly_transaction_web_hooks_path : "#{ENV['RAILS_RELATIVE_URL_ROOT']}/admin/recurly_hooks/#{resource.id}") do |f| + = f.semantic_errors *f.object.errors.keys + = f.inputs name: 'Recurly Web Hook fields' do + = f.input :admin_description, :input_html => { :rows=>1, :maxlength=>200, }, hint: "this will display on the user's payment history page" + = f.input :jam_track, collection: JamRuby::JamTrack.all, include_blank: true, hint: "Please indicate which JamTrack this refund for, if not set" + = f.actions \ No newline at end of file diff --git a/admin/config/application.rb b/admin/config/application.rb index 102a03176..edb978be7 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -83,6 +83,7 @@ module JamAdmin config.external_port = ENV['EXTERNAL_PORT'] || 3000 config.external_protocol = ENV['EXTERNAL_PROTOCOL'] || 'http://' config.external_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.external_port == 80 || config.external_port == 443) ? '' : ':' + config.external_port.to_s}" + config.recurly_root_url = 'https://jamkazam-development.recurly.com' # where is rabbitmq? config.rabbitmq_host = "localhost" diff --git a/db/manifest b/db/manifest index 94acb060f..c79ea8187 100755 --- a/db/manifest +++ b/db/manifest @@ -279,3 +279,4 @@ recurly_adjustments.sql signup_hints.sql packaging_notices.sql first_played_jamtrack_at.sql +payment_history.sql diff --git a/db/up/payment_history.sql b/db/up/payment_history.sql new file mode 100644 index 000000000..cb350c61d --- /dev/null +++ b/db/up/payment_history.sql @@ -0,0 +1,17 @@ +ALTER TABLE recurly_transaction_web_hooks ADD COLUMN admin_description VARCHAR; +ALTER TABLE recurly_transaction_web_hooks ADD COLUMN jam_track_id VARCHAR(64) REFERENCES jam_tracks(id); + +CREATE VIEW payment_histories AS + SELECT id AS sale_id, + CAST(NULL as VARCHAR) AS recurly_transaction_web_hook_id, + user_id, + created_at, + 'sale' AS transaction_type + FROM sales s + UNION ALL + SELECT CAST(NULL as VARCHAR) AS sale_id, + id AS recurly_transaction_web_hook_id, + user_id, + transaction_at AS created_at, + transaction_type + FROM recurly_transaction_web_hooks; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 0741342b6..8a767ad6f 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -207,6 +207,7 @@ require "jam_ruby/models/generic_state" require "jam_ruby/models/score_history" require "jam_ruby/models/jam_company" require "jam_ruby/models/user_sync" +require "jam_ruby/models/payment_history" require "jam_ruby/models/video_source" require "jam_ruby/models/text_message" require "jam_ruby/models/sale" diff --git a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb index 1a7f35e37..f14d8e424 100644 --- a/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/admin_mailer.rb @@ -20,10 +20,16 @@ module JamRuby subject: options[:subject]) end - def recurly_alerts(options) + def recurly_alerts(user, options) + + body = options[:body] + body << "\n\n" + body << "User " << user.admin_url + "\n" + body << "User's JamTracks " << user.jam_track_rights_admin_url + "\n" + mail(to: APP_CONFIG.email_recurly_notice, from: APP_CONFIG.email_generic_from, - body: options[:body], + body: body, content_type: "text/plain", subject: options[:subject]) end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 0197a57ef..594358ee3 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -60,7 +60,10 @@ module JamRuby # has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy # VRFS-2916 jam_tracks.id is varchar: ADD has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy - + + # when we know what JamTrack this refund is related to, these are associated + belongs_to :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook' + accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true diff --git a/ruby/lib/jam_ruby/models/payment_history.rb b/ruby/lib/jam_ruby/models/payment_history.rb new file mode 100644 index 000000000..4862e4dd1 --- /dev/null +++ b/ruby/lib/jam_ruby/models/payment_history.rb @@ -0,0 +1,39 @@ +module JamRuby + class PaymentHistory < ActiveRecord::Base + + self.table_name = 'payment_histories' + + belongs_to :sale + belongs_to :recurly_transaction_web_hook + + + def self.index(user, params = {}) + + limit = params[:per_page] + limit ||= 20 + limit = limit.to_i + + query = PaymentHistory.limit(limit) + .includes(sale: [:sale_line_items], recurly_transaction_web_hook:[]) + .where(user_id: user.id) + .where("transaction_type = 'sale' OR transaction_type = 'refund' OR transaction_type = 'void'") + .order('created_at DESC') + + + current_page = params[:page].nil? ? 1 : params[:page].to_i + next_page = current_page + 1 + + # will_paginate gem + query = query.paginate(:page => current_page, :per_page => limit) + + if query.length == 0 # no more results + { query: query, next_page: nil} + elsif query.length < limit # no more results + { query: query, next_page: nil} + else + { query: query, next_page: next_page } + end + + end + end +end \ No newline at end of file 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 1766a913e..05824e322 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -1,10 +1,15 @@ module JamRuby - class RecurlyTransactionWebHook < ActiveRecord::Base + class RecurlyTransactionWebHook < ActiveRecord::Base + + attr_accessible :admin_description, :jam_track_id, as: :admin belongs_to :user, class_name: 'JamRuby::User' belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions + # when we know what JamTrack this refund is related to, we set this value + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + validates :recurly_transaction_id, presence: true validates :action, presence: true validates :status, presence: true @@ -17,6 +22,9 @@ module JamRuby REFUND = 'refund' VOID = 'void' + + HOOK_TYPES = [SUCCESSFUL_PAYMENT, FAILED_PAYMENT, REFUND, VOID] + def is_credit_type? transaction_type == REFUND || transaction_type == VOID end @@ -46,6 +54,10 @@ module JamRuby end end + def admin_url + APP_CONFIG.admin_root_url + "/admin/recurly_hooks/" + id + end + # see spec for examples of XML def self.create_from_xml(document) @@ -81,6 +93,7 @@ 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' sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id) @@ -91,33 +104,44 @@ module JamRuby jam_track_right = jam_track.right_for_user(transaction.user) if jam_track if jam_track_right jam_track_right.destroy - AdminMailer.recurly_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 + + # associate which JamTrack we assume this is related to in this one success case + transaction.jam_track = jam_track + transaction.save! + + AdminMailer.recurly_alerts(transaction.user, { + subject: "NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked", + body: "A #{transaction.transaction_type} 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.recurly_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 + AdminMailer.recurly_alerts(transaction.user, { + 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.recurly_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 + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale", + body: "We received a #{transaction.transaction_type} 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.recurly_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 + AdminMailer.recurly_alerts(transaction.user, { + 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 + else + AdminMailer.recurly_alerts(transaction.user, { + subject: "ACTION REQUIRED: #{transaction.user.email} has refund with no correlator to sales", + body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks" + }).deliver end + end transaction end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index d05f325de..f49d0e511 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1573,6 +1573,10 @@ module JamRuby APP_CONFIG.admin_root_url + "/admin/users/" + id end + def jam_track_rights_admin_url + APP_CONFIG.admin_root_url + "/admin/jam_track_rights?q[user_id_equals]=#{id}&commit=Filter&order=created_at DESC" + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/spec/jam_ruby/models/payment_history_spec.rb b/ruby/spec/jam_ruby/models/payment_history_spec.rb new file mode 100644 index 000000000..b28cffec3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/payment_history_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe PaymentHistory do + + let(:user) {FactoryGirl.create(:user)} + let(:user2) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + before(:each) do + + end + + describe "index" do + it "empty" do + result = PaymentHistory.index(user) + result[:query].length.should eq(0) + result[:next].should eq(nil) + end + + it "one" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + + it "user filtered correctly" do + sale = Sale.create_jam_track_sale(user) + shopping_cart = ShoppingCart.create(user, jam_track) + sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + + sale2 = Sale.create_jam_track_sale(user2) + shopping_cart = ShoppingCart.create(user2, jam_track) + sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil) + + result = PaymentHistory.index(user) + result[:query].length.should eq(1) + result[:next].should eq(nil) + end + end +end diff --git a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee index 0ecbf4a72..82dbe73eb 100644 --- a/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee +++ b/web/app/assets/javascripts/accounts_payment_history_screen.js.coffee @@ -51,41 +51,33 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen renderPayments:(response) => if response.entries? && response.entries.length > 0 - for sale in response.entries - amt = sale.recurly_total_in_cents - amt = 0 if !amt? - - original_total = sale.state.original_total - refund_total = sale.state.refund_total - - refund_state = null - if original_total != 0 # the enclosed logic does not work for free purchases - if refund_total == original_total - refund_state = 'refunded' - else if refund_total != 0 and refund_total < original_total - refund_state = 'partial refund' - - - displayAmount = (amt/100).toFixed(2) - status = 'paid' - - if sale.state.voided - status = 'voided' - displayAmount = (amt/100).toFixed(2) - else if refund_state? - status = refund_state - displayAmount = (amt/100).toFixed(2) + " (refunded: #{(refund_total/100).toFixed(2)})" - - description = [] - for line_item in sale.line_items - description.push(line_item.product_info?.name) + for paymentHistory in response.entries + if paymentHistory.sale? + # this is a sale + sale = paymentHistory.sale + amt = sale.recurly_total_in_cents + status = 'paid' + displayAmount = ' $' + (amt/100).toFixed(2) + date = context.JK.formatDate(sale.created_at, true) + items = [] + for line_item in sale.line_items + items.push(line_item.product_info?.name) + description = items.join(', ') + else + # this is a recurly webhook + transaction = paymentHistory.transaction + amt = transaction.amount_in_cents + status = transaction.transaction_type + displayAmount = '($' + (amt/100).toFixed(2) + ')' + date = context.JK.formatDate(transaction.transaction_at, true) + description = transaction.admin_description payment = { - date: context.JK.formatDate(sale.created_at, true) + date: date amount: displayAmount status: status - payment_method: 'Credit Card', - description: description.join(', ') + payment_method: 'Credit Card' + description: description } tr = $(context._.template(@rowTemplate, payment, { variable: 'data' })); @@ -98,9 +90,9 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen # Turn in to HTML rows and append: #@tbody.html("") - @next = response.next_page + @next = response.next @renderPayments(response) - if response.next_page == null + if response.next == null # if we less results than asked for, end searching @scroller.infinitescroll 'pause' @logger.debug("end of history") @@ -146,7 +138,7 @@ context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen msg: $('