Filtering by event or campaign will include refunds now

This commit is contained in:
Eric Schultz 2018-07-25 13:20:05 -05:00
parent e3fb6e6c92
commit bf5b10f280
12 changed files with 465 additions and 19 deletions

View file

@ -42,7 +42,7 @@ gem 'font_assets'
# Database (postgres)
gem 'pg' # Postgresql
gem 'qx', git: 'https://github.com/commitchange/ruby-qx.git'
gem 'qx', git: 'https://github.com/ericschultz/ruby-qx.git'
gem 'dalli'
gem 'memcachier'

View file

@ -5,14 +5,6 @@ GIT
param_validation (0.0.2)
chronic
GIT
remote: https://github.com/commitchange/ruby-qx.git
revision: 3582c9a3c5d03f23480bc9b8ff1948a351ed8d6c
specs:
qx (0.1.1)
activerecord (>= 3.0)
colorize (~> 0.8)
GIT
remote: https://github.com/commitchange/stripe-ruby-mock.git
revision: ee4471a8f654672d5596218c2b68a2913ea3f4cc
@ -32,6 +24,14 @@ GIT
grape (> 0.7)
rails (> 3.2, < 5)
GIT
remote: https://github.com/ericschultz/ruby-qx.git
revision: 179467cd4a8c791beb36428975c287acc11b98b2
specs:
qx (0.1.1)
activerecord (>= 3.0)
colorize (~> 0.8)
GIT
remote: https://github.com/ruby-grape/grape-entity.git
revision: 0e04aa561373b510c2486282979085eaef2ae663

View file

@ -0,0 +1,6 @@
class AddChargeIdIndexes < ActiveRecord::Migration
def change
add_index :refunds, :charge_id
end
end

View file

@ -0,0 +1,5 @@
class AddPaymentIdToTickets < ActiveRecord::Migration
def change
add_index :tickets, :payment_id
end
end

View file

@ -0,0 +1,5 @@
class AddIndexesToRefunds < ActiveRecord::Migration
def change
add_index :refunds, :payment_id
end
end

View file

@ -3162,6 +3162,20 @@ CREATE INDEX index_exports_on_nonprofit_id ON public.exports USING btree (nonpro
CREATE INDEX index_exports_on_user_id ON public.exports USING btree (user_id);
--
-- Name: index_refunds_on_charge_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_refunds_on_charge_id ON public.refunds USING btree (charge_id);
--
-- Name: index_refunds_on_payment_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_refunds_on_payment_id ON public.refunds USING btree (payment_id);
--
-- Name: index_sessions_on_session_id; Type: INDEX; Schema: public; Owner: -
--
@ -3232,6 +3246,13 @@ CREATE INDEX index_supporters_on_name ON public.supporters USING btree (name);
CREATE INDEX index_tickets_on_event_id ON public.tickets USING btree (event_id);
--
-- Name: index_tickets_on_payment_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_tickets_on_payment_id ON public.tickets USING btree (payment_id);
--
-- Name: index_tickets_on_supporter_id; Type: INDEX; Schema: public; Owner: -
--
@ -4300,3 +4321,9 @@ INSERT INTO schema_migrations (version) VALUES ('20180608205049');
INSERT INTO schema_migrations (version) VALUES ('20180608212658');
INSERT INTO schema_migrations (version) VALUES ('20180713213748');
INSERT INTO schema_migrations (version) VALUES ('20180713215825');
INSERT INTO schema_migrations (version) VALUES ('20180713220028');

View file

@ -16,7 +16,7 @@ module InsertRefunds
def self.with_stripe(charge, h)
ParamValidation.new(charge, {
payment_id: {required: true, is_integer: true},
stripe_charge_id: {required: true, format: /^ch_.*$/},
stripe_charge_id: {required: true, format: /^(test_)?ch_.*$/},
amount: {required: true, is_integer: true, min: 1},
id: {required: true, is_integer: true},
nonprofit_id: {required: true, is_integer: true},

View file

@ -105,13 +105,49 @@ module QueryPayments
end
# we must provide payments.*, supporters.*, donations.*, associated event_id, associated campaign_id
def self.full_search_expr(npo_id, query)
expr = Qexpr.new.from('payments')
.left_outer_join('supporters', "supporters.id=payments.supporter_id")
.left_outer_join('donations', 'donations.id=payments.donation_id' )
.where('payments.nonprofit_id=$id', id: npo_id.to_i)
.join("(#{select_to_filter_search(npo_id, query)}) AS \"filtered_payments\"", 'payments.id = filtered_payments.id')
.order_by('payments.date DESC')
if ['asc', 'desc'].include? query[:sort_amount]
expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}")
end
if ['asc', 'desc'].include? query[:sort_date]
expr = expr.order_by("payments.date #{query[:sort_date]}")
end
if ['asc', 'desc'].include? query[:sort_name]
expr = expr.order_by("coalesce(NULLIF(supporters.name, ''), NULLIF(supporters.email, '')) #{query[:sort_name]}")
end
if ['asc', 'desc'].include? query[:sort_type]
expr = expr.order_by("payments.kind #{query[:sort_type]}")
end
if ['asc', 'desc'].include? query[:sort_towards]
expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}")
end
return expr
end
# perform the search but only get the relevant payment_ids
def self.select_to_filter_search(npo_id, query)
inner_donation_search = Qexpr.new.select('donations.*').from('donations')
if (query[:event_id].present?)
inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id])
end
if (query[:campaign_id].present?)
inner_donation_search = inner_donation_search.where('donations.campaign_id=$id', id: query[:campaign_id])
end
expr = Qexpr.new.select('payments.id').from('payments')
.left_outer_join('supporters', "supporters.id=payments.supporter_id")
.left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments.donation_id' )
.where('payments.nonprofit_id=$id', id: npo_id.to_i)
if query[:search].present?
expr = SearchVector.query(query[:search], expr)
end
@ -156,16 +192,105 @@ module QueryPayments
end
if query[:campaign_id].present?
expr = expr
.left_outer_join("campaigns", "campaigns.id=donations.campaign_id" )
.where("campaigns.id=$id", id: query[:campaign_id])
.left_outer_join("campaigns", "campaigns.id=donations.campaign_id" )
.where("campaigns.id=$id", id: query[:campaign_id])
end
if query[:event_id].present?
tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tix")
tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix")
expr = expr
.left_outer_join(tickets_subquery, "tix.payment_id=payments.id")
.where("tix.event_id=$id OR donations.event_id=$id", id: query[:event_id])
.left_outer_join(tickets_subquery, "tix.payment_id=payments.id")
.where("tix.event_id=$id OR donations.event_id=$id", id: query[:event_id])
end
return expr
expr = expr
#we have the first part of the search. We need to create the second in certain situations
filtered_payment_id_search = expr.parse
if query[:event_id].present? || query[:campaign_id].present?
filtered_payment_id_search = filtered_payment_id_search + " UNION DISTINCT " + create_reverse_select(npo_id, query).parse
end
filtered_payment_id_search
end
# we use this when we need to get the refund info
def self.create_reverse_select(npo_id, query)
inner_donation_search = Qexpr.new.select('donations.*').from('donations')
if (query[:event_id].present?)
inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id])
end
if (query[:campaign_id].present?)
inner_donation_search = inner_donation_search.where('donations.campaign_id=$id', id: query[:campaign_id])
end
expr = Qexpr.new.select('payments.id').from('payments')
.left_outer_join('supporters', "supporters.id=payments.supporter_id")
.left_outer_join('refunds', 'payments.id=refunds.payment_id')
.left_outer_join('charges', 'refunds.charge_id=charges.id')
.left_outer_join('payments AS payments_orig', 'payments_orig.id=charges.payment_id')
.left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments_orig.donation_id' )
.where('payments.nonprofit_id=$id', id: npo_id.to_i)
if query[:search].present?
expr = SearchVector.query(query[:search], expr)
end
if ['asc', 'desc'].include? query[:sort_amount]
expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}")
end
if ['asc', 'desc'].include? query[:sort_date]
expr = expr.order_by("payments.date #{query[:sort_date]}")
end
if ['asc', 'desc'].include? query[:sort_name]
expr = expr.order_by("coalesce(NULLIF(supporters.name, ''), NULLIF(supporters.email, '')) #{query[:sort_name]}")
end
if ['asc', 'desc'].include? query[:sort_type]
expr = expr.order_by("payments.kind #{query[:sort_type]}")
end
if ['asc', 'desc'].include? query[:sort_towards]
expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}")
end
if query[:after_date].present?
expr = expr.where('payments.date >= $date', date: query[:after_date])
end
if query[:before_date].present?
expr = expr.where('payments.date <= $date', date: query[:before_date])
end
if query[:amount_greater_than].present?
expr = expr.where('payments.gross_amount >= $amt', amt: query[:amount_greater_than].to_i * 100)
end
if query[:amount_less_than].present?
expr = expr.where('payments.gross_amount <= $amt', amt: query[:amount_less_than].to_i * 100)
end
if query[:year].present?
expr = expr.where("to_char(payments.date, 'YYYY')=$year", year: query[:year])
end
if query[:designation].present?
expr = expr.where("donations.designation @@ $s", s: "#{query[:designation]}")
end
if query[:dedication].present?
expr = expr.where("donations.dedication @@ $s", s: "#{query[:dedication]}")
end
if query[:donation_type].present?
expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(','))
end
if query[:campaign_id].present?
expr = expr
.left_outer_join("campaigns", "campaigns.id=donations.campaign_id" )
.where("campaigns.id=$id", id: query[:campaign_id])
end
if query[:event_id].present?
tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix")
expr = expr
.left_outer_join(tickets_subquery, "tix.payment_id=payments_orig.id")
.where("tix.event_id=$id OR donations.event_id=$id", id: query[:event_id])
end
expr
end
def self.for_export_enumerable(npo_id, query, chunk_limit=35000)

View file

@ -8,6 +8,10 @@ describe InsertDonation do
Settings.payment_provider.stripe_connect = true
}
after(:each) {
Settings.reload!
}
include_context :shared_rd_donation_value_context
describe 'param validation' do

View file

@ -164,4 +164,235 @@ describe QueryPayments do
end
end
describe '.full_search' do
include_context :shared_rd_donation_value_context
before(:each) {
nonprofit.stripe_account_id = Stripe::Account.create()['id']
nonprofit.save!
card.stripe_customer_id = 'some other id'
cust = Stripe::Customer.create()
card.stripe_customer_id = cust['id']
card.save!
expect(Stripe::Charge).to receive(:create).exactly(3).times.and_wrap_original {|m, *args| a = m.call(*args);
@stripe_charge_id = a['id']
a
}
allow(QueueDonations).to receive(:execute_for_donation)
}
let(:charge_amount_small) { 200}
let(:charge_amount_medium) { 400}
let(:charge_amount_large) { 600}
def generate_donation(h)
token = h[:token]
date = h[:date]
amount = h[:amount]
input = {amount: amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
token: token,
date: date,
dedication: 'dedication',
designation: 'designation'}
if h[:event_id]
input[:event_id] = h[:event_id]
end
if h[:campaign_id]
input[:campaign_id] = h[:campaign_id]
end
InsertDonation.with_stripe(input)
end
describe 'general donations' do
let(:donation_result_yesterday) {
generate_donation(amount: charge_amount_small,
token: source_tokens[0].token,
date: (Time.now - 1.day).to_s)
}
let(:donation_result_today) {
generate_donation(amount: charge_amount_medium,
token: source_tokens[1].token,
date: (Time.now).to_s
)
}
let(:donation_result_tomorrow) {
generate_donation(amount: charge_amount_large,
token: source_tokens[2].token,
date: (Time.now - 1.day).to_s
)
}
let (:first_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 100}.with_indifferent_access)
}
let(:second_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 50}.with_indifferent_access)
}
it 'empty filter returns all' do
donation_result_yesterday
donation_result_today
donation_result_tomorrow
first_refund_of_yesterday
second_refund_of_yesterday
result = QueryPayments::full_search(nonprofit.id, {})
expect(result[:data].count).to eq 5
end
end
describe 'event donations' do
let(:donation_result_yesterday) {
generate_donation(amount: charge_amount_small,
event_id: event.id,
token: source_tokens[0].token,
date: (Time.now - 1.day).to_s)
}
let(:donation_result_today) {
generate_donation(amount: charge_amount_medium,
event_id: event.id,
token: source_tokens[1].token,
date: (Time.now).to_s
)
}
let(:donation_result_tomorrow) {
generate_donation(amount: charge_amount_large,
token: source_tokens[2].token,
date: (Time.now - 1.day).to_s
)
}
let (:first_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 100}.with_indifferent_access)
}
let(:second_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 50}.with_indifferent_access)
}
it 'search includes refunds for that event ' do
donation_result_yesterday
donation_result_today
donation_result_tomorrow
first_refund_of_yesterday
second_refund_of_yesterday
result = QueryPayments::full_search(nonprofit.id, {event_id: event.id})
expect(result[:data].count).to eq 4
expect(result[:data]).to_not satisfy {|i| i.any?{|j| j['id'] == donation_result_tomorrow['payment']['id']}}
end
end
describe 'campaign donations' do
let(:donation_result_yesterday) {
generate_donation(amount: charge_amount_small,
campaign_id:campaign.id,
token: source_tokens[0].token,
date: (Time.now - 1.day).to_s)
}
let(:donation_result_today) {
generate_donation(amount: charge_amount_medium,
campaign_id:campaign.id,
token: source_tokens[1].token,
date: (Time.now).to_s
)
}
let(:donation_result_tomorrow) {
generate_donation(amount: charge_amount_large,
token: source_tokens[2].token,
date: (Time.now - 1.day).to_s
)
}
let (:first_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 100}.with_indifferent_access)
}
let(:second_refund_of_yesterday) {
charge = donation_result_yesterday['charge']
InsertRefunds.with_stripe(charge.attributes, {amount: 50}.with_indifferent_access)
}
it 'search includes refunds for that campaign ' do
donation_result_yesterday
donation_result_today
donation_result_tomorrow
first_refund_of_yesterday
second_refund_of_yesterday
result = QueryPayments::full_search(nonprofit.id, {campaign_id: campaign.id})
expect(result[:data].count).to eq 4
expect(result[:data]).to_not satisfy {|i| i.any?{|j| j['id'] == donation_result_tomorrow['payment']['id']}}
end
end
end
end

View file

@ -2,7 +2,7 @@
require 'stripe_mock'
RSpec.shared_context :shared_donation_charge_context do
let(:nonprofit) { force_create(:nonprofit, name: "nonprofit name")}
let(:nonprofit) { force_create(:nonprofit, name: "nonprofit name", slug: 'nonprofit_nameo')}
let(:other_nonprofit) { force_create(:nonprofit)}
let(:supporter) {force_create(:supporter, :nonprofit => nonprofit, locale: 'locale_one')}
let(:other_nonprofit_supporter) { force_create(:supporter, nonprofit: other_nonprofit, locale: 'locale_two')}

View file

@ -6,7 +6,13 @@ RSpec.shared_context :shared_rd_donation_value_context do
let(:fake_uuid) {"53a6bc06-0789-11e8-bb3f-f34cac607737"}
let(:valid_uuid) {'fcf61bac-078a-11e8-aa53-cba5bdb8dcdd'}
let(:other_uuid) {'a713018c-078f-11e8-ae3b-bf5007844fea'}
let(:source_token) {force_create(:source_token, tokenizable: card, expiration: Time.now + 1.day, max_uses: 1, token: valid_uuid)}
let(:source_token) {force_create(:source_token, tokenizable: card, expiration: Time.now + 1.day, max_uses: 1, token: valid_uuid) }
let(:source_tokens) {
(0..10).map { |i|
force_create(:source_token, tokenizable: card, expiration: Time.now + 1.day, max_uses: 1, token: SecureRandom.uuid)
}
}
let(:other_source_token) {force_create(:source_token, tokenizable: card_for_other_supporter, expiration: Time.now + 1.day, max_uses: 1, token: other_uuid)}
let(:charge_amount) {100}
@ -147,6 +153,11 @@ RSpec.shared_context :shared_rd_donation_value_context do
result
end
def generate_expected_refund(data={})
result = {}.with_indifferent_access
result
end
def validation_unauthorized
expect(QuerySourceToken).to receive(:get_and_increment_source_token).with(fake_uuid, nil).and_raise(AuthenticationError)
expect {yield()}.to raise_error {|e|
@ -382,6 +393,24 @@ RSpec.shared_context :shared_rd_donation_value_context do
end
end
def before_each_successful_refund()
expect(InsertRefund).to receive(:with_stripe).and_wrap_original do |m, *args|
result = m.call(*args);
@all_refunds&.push(result) || @all_refunds = [result]
@fifo_refunds&.unshift(result) || @fifo_refunds = [ result]
result
end
expect(Stripe::Refund).to receive(:create).and_wrap_original do |m, *args|
a = m.call(*args);
@stripe_refund_ids&.unshift(a['id']) || @stripe_refund_ids = [a['id']];
a
end
end
def before_each_sepa_success()
expect(InsertDonation).to receive(:insert_donation).and_wrap_original do |m, *args|
result = m.call(*args);
@ -406,6 +435,8 @@ RSpec.shared_context :shared_rd_donation_value_context do
if (data[:recurring_donation])
expect(result['recurring_donation'].attributes).to eq expected[:recurring_donation]
end
return result
end
def process_campaign_donation(data = {})
@ -422,6 +453,7 @@ RSpec.shared_context :shared_rd_donation_value_context do
if (data[:recurring_donation])
expect(result['recurring_donation'].attributes).to eq expected[:recurring_donation]
end
return result
end
def process_general_donation(data = {})
@ -447,6 +479,17 @@ RSpec.shared_context :shared_rd_donation_value_context do
if (data[:recurring_donation])
expect(result['recurring_donation'].attributes).to eq expected[:recurring_donation]
end
return result
end
def process_general_refund(data = {})
result = yield
expected = generate_expected_refund()
expect(result['payment']).to eq expected[:payment]
expect(result['refund']).to eq expected[:refund]
return result
end
def nil_or_true(item)