* VRFS-3080 - partial refund manual process done

This commit is contained in:
Seth Call 2015-04-23 16:20:21 -05:00
parent 989518fa68
commit 4cce21e7f8
23 changed files with 322 additions and 75 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -279,3 +279,4 @@ recurly_adjustments.sql
signup_hints.sql
packaging_notices.sql
first_played_jamtrack_at.sql
payment_history.sql

17
db/up/payment_history.sql Normal file
View File

@ -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;

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: $('<div class="infinite-scroll-loader">Loading ...</div>')
img: '/assets/shared/spinner.gif'
path: (page) =>
'/api/sales?' + $.param(that.buildQuery())
'/api/payment_histories?' + $.param(that.buildQuery())
}, (json, opts) =>
this.salesHistoryDone(json)

View File

@ -1508,7 +1508,7 @@
function getSalesHistory(options) {
return $.ajax({
type: "GET",
url: '/api/sales?' + $.param(options),
url: '/api/payment_histories?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});

View File

@ -20,8 +20,6 @@
}
&.voided {
text-decoration:line-through;
}
}

View File

@ -0,0 +1,18 @@
class ApiPaymentHistoriesController < ApiController
before_filter :api_signed_in_user
respond_to :json
def index
data = PaymentHistory.index(current_user,
page: params[:page],
per_page: params[:per_page])
@payment_histories = data[:query]
@next = data[:next_page]
render "api_payment_histories/index", :layout => nil
end
end

View File

@ -4,6 +4,7 @@ class ApiSalesController < ApiController
respond_to :json
# deprecated in favor of ApiPaymentHistoriesController
def index
data = Sale.index(current_user,
page: params[:page],

View File

@ -0,0 +1,11 @@
node :next do |page|
@next
end
node :entries do |page|
partial "api_payment_histories/show", object: @payment_histories
end
node :total_entries do |page|
@payment_histories.total_entries
end

View File

@ -0,0 +1,14 @@
object @payment_history
child :sale do
attributes :id, :recurly_invoice_id, :recurly_subtotal_in_cents, :recurly_tax_in_cents, :recurly_total_in_cents, :recurly_currency, :sale_type, :recurly_invoice_number, :state, :created_at
child(:sale_line_items => :line_items) {
attributes :id, :product_info
}
end
child(:recurly_transaction_web_hook => :transaction) do
attributes :id, :transaction_type, :subscription_id, :amount_in_cents, :invoice_id, :admin_description, :message, :transaction_at, :admin_description
end

View File

@ -41,4 +41,4 @@ script#template-payment-history-row type="text/template"
td.capitalize
| {{data.status}}
td.amount class="{{data.status}}"
| ${{data.amount}}
| {{data.amount}}

View File

@ -280,7 +280,7 @@ SampleApp::Application.routes.draw do
match '/recurly/place_order' => 'api_recurly#place_order', :via => :post
# sale info
match '/sales' => 'api_sales#index', :via => :get
match '/payment_histories' => 'api_payment_histories#index', :via => :get
# login/logout
match '/auth_session' => 'api_users#auth_session_create', :via => :post

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe ApiSalesController do
describe ApiPaymentHistoriesController do
render_views
let(:user) {FactoryGirl.create(:user)}
@ -19,6 +19,7 @@ describe ApiSalesController do
body = JSON.parse(response.body)
body['next_page'].should be_nil
body['entries'].should eq([])
body['total_entries'].should eq(0)
end
it "one item" do
@ -36,21 +37,28 @@ describe ApiSalesController do
entries = body['entries']
entries.should have(1).items
sale_entry = entries[0]
sale_entry["line_items"].should have(1).items
sale_entry["recurly_transactions"].should have(0).items
sale_entry['recurly_total_in_cents'].should eq(sale.recurly_total_in_cents)
sale_json = sale_entry['sale']
sale_json.should_not be_nil
sale_json["line_items"].should have(1).items
transaction = FactoryGirl.create(:recurly_transaction_web_hook, invoice_id: sale.recurly_invoice_id, transaction_type: RecurlyTransactionWebHook::VOID)
transaction = FactoryGirl.create(:recurly_transaction_web_hook, invoice_id: sale.recurly_invoice_id, transaction_type: RecurlyTransactionWebHook::VOID, user: user, transaction_at: 1.minute.from_now)
get :index, { :format => 'json'}
response.should be_success
body = JSON.parse(response.body)
body['next_page'].should be_nil
entries = body['entries']
entries.should have(1).items
sale_entry = entries[0]
sale_entry["line_items"].should have(1).items
sale_entry["recurly_transactions"].should have(1).items
entries.should have(2).items
void_entry = entries[0]
void = void_entry['transaction']
void.should_not be_nil
void['amount_in_cents'].should eq(199)
sale_entry = entries[1]
sale_json = sale_entry['sale']
sale_json.should_not be_nil
sale_json["line_items"].should have(1).items
end
end