Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perform recurring donation transaction #9

Closed
wants to merge 9 commits into from
19 changes: 13 additions & 6 deletions app/controllers/store_controller.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
class StoreController < ApplicationController

include StoreHelper

skip_before_filter :verify_authenticity_token, :only => %w(show_changed showdate_changed)

before_filter :set_customer, :except => %w[process_donation]
before_filter :is_logged_in, :only => %w[checkout place_order]
before_filter :order_is_not_empty, :only => %w[shipping_address checkout place_order]

# ACTION INVARIANT BEFORE ACTION
# ------ -----------------------
# index, subscribe, donate_to_fund valid @customer
Expand All @@ -26,6 +26,8 @@ class StoreController < ApplicationController

private

DONATION_FREQUENCIES = %w[one-time monthly]

# if order is nil or empty after checkout phase, abort. This should never happen normally,
# but can happen if customer does weird things trying to have multiple sessions open.
def order_is_not_empty
Expand All @@ -41,7 +43,7 @@ def set_customer
redirect_customer = resolve_customer_in_url(logged_in_user, specified_customer)
if redirect_customer == specified_customer # ok to proceed as is
@customer = specified_customer
else
else
redirect_to url_for(params.merge(:customer_id => redirect_customer.id, :only_path => true))
end
end
Expand Down Expand Up @@ -113,6 +115,7 @@ def donate_to_fund
# Serve quick_donate page; POST calls #process_donation
def donate
reset_shopping # even if order in progress, going to donation page cancels it
@donation_frequency_options = DONATION_FREQUENCIES
if @customer == Customer.anonymous_customer
# handle donation as a 'guest checkout', even though may end up being tied to real customer
@customer = Customer.new
Expand All @@ -134,6 +137,10 @@ def process_donation
@amount > 0 or return redirect_to(redirect_route, :alert => 'Donation amount must be provided')
# Given valid donation, customer, and charge token, create & place credit card order.
@gOrderInProgress = Order.new_from_donation(@amount, Donation.default_code, @customer)
case params[:donation_frequency]
when DONATION_FREQUENCIES[1]
@gOrderInProgress.add_recurring_donation()
end
@gOrderInProgress.purchasemethod = Purchasemethod.get_type_by_name('web_cc')
@gOrderInProgress.purchase_args = {:credit_card_token => params[:credit_card_token]}
@gOrderInProgress.processed_by = @customer
Expand Down Expand Up @@ -195,10 +202,10 @@ def shipping_address
recipient = recipient_from_params(customer_params)
@recipient = recipient[0]
if @recipient.email == @customer.email
flash.now[:alert] = I18n.t('store.errors.gift_diff_email_notice')
flash.now[:alert] = I18n.t('store.errors.gift_diff_email_notice')
render :action => :shipping_address
return
end
end
if Customer.email_matches_diff_last_name?(try_customer)
flash.now[:alert] = I18n.t('store.errors.gift_matching_email_diff_last_name')
render :action => :shipping_address
Expand All @@ -219,7 +226,7 @@ def shipping_address
if Customer.email_last_name_match_diff_address?(try_customer)
flash[:notice] = I18n.t('store.gift_matching_email_last_name_diff_address')
elsif recipient_from_params(customer_params)[1] == "found_matching_customer"
flash[:notice] = I18n.t('store.gift_recipient_on_file')
flash[:notice] = I18n.t('store.gift_recipient_on_file')
end
redirect_to_checkout
end
Expand Down
7 changes: 4 additions & 3 deletions app/models/account_code.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def donation_prompt ; '' ; end
default_scope { order('code') }

has_many :donations
has_many :recurring_donations
has_many :vouchertypes

validates_length_of :name, :maximum => 255, :allow_nil => true
Expand All @@ -28,7 +29,7 @@ def name_or_code_given
def self.default_account_code_id
self.default_account_code.id
end

def self.default_account_code
AccountCode.first
end
Expand All @@ -42,12 +43,12 @@ def <=>(other)
end

class CannotDelete < RuntimeError ; end

# convenience accessors

def name_or_code ; name.blank? ? code : name ; end
def name_with_code ; sprintf("%-6.6s %s", code, name) ; end

# cannot delete the last account code or the one associated as any
# of the defaults

Expand Down
7 changes: 4 additions & 3 deletions app/models/customer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Customer < ActiveRecord::Base
def active_vouchers
vouchers.select { |v| Time.current <= Time.at_end_of_season(v.season) }
end

has_many :vouchertypes, :through => :vouchers
has_many :showdates, :through => :vouchers
has_many :orders, -> { where( 'orders.sold_on IS NOT NULL').order(:sold_on => :desc) }
Expand All @@ -37,6 +37,7 @@ def shows ; self.showdates.map(&:show).uniq ; end
has_many :txns
has_one :most_recent_txn, -> { order('txn_date DESC') }, :class_name=>'Txn'
has_many :donations
has_many :recurring_donations
has_many :retail_items
has_many :items # the superclass of vouchers,donations,retail_items

Expand All @@ -61,7 +62,7 @@ def restricted_email
!email.blank? && !email.match( /#{domain}\z/i )
end
validate :restricted_email, :if => :self_created?, :on => :create

EMAIL_UNIQUENESS_ERROR_MESSAGE = 'has already been registered.'
validates_uniqueness_of :email,
:allow_blank => true,
Expand Down Expand Up @@ -427,7 +428,7 @@ def self.csv_header
['First name', 'Last name', 'ID', 'Email', 'Street', 'City', 'State', 'Zip',
'Day/main phone', 'Eve/alt phone', "Don't mail", "Don't email"].freeze
end

def self.to_csv(custs,opts={})
CSV::Writer.generate(output='') do |csv|
unless opts[:suppress_header]
Expand Down
7 changes: 4 additions & 3 deletions app/models/items/donation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ class Donation < Item
def self.default_code
AccountCode.find(Option.default_donation_account_code)
end

belongs_to :account_code
validates_associated :account_code
validates_presence_of :account_code_id

belongs_to :customer

belongs_to :recurring_donation, class_name: 'RecurringDonation', foreign_key: :recurring_donation_id

validates_numericality_of :amount
validates_inclusion_of :amount, :in => 1..10_000_000, :message => "must be at least 1 dollar"

Expand Down
9 changes: 9 additions & 0 deletions app/models/items/recurring_donation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class RecurringDonation < Item

belongs_to :account_code
belongs_to :customer
has_many :donations, foreign_key: :recurring_donation_id

def one_line_description ; end
def description_for_audit_txn ; end
end
34 changes: 21 additions & 13 deletions app/models/order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ class Order < ActiveRecord::Base
has_many :items, :autosave => true, :dependent => :destroy
has_many :vouchers, :autosave => true, :dependent => :destroy
has_many :donations, :autosave => true, :dependent => :destroy
has_many :recurring_donations, :autosave => true, :dependent => :destroy
has_many :retail_items, :autosave => true, :dependent => :destroy

attr_accessor :purchase_args
attr_accessor :comments
attr_reader :donation
attr_reader :recurring_donation

# pending and errored states of a CC order (pending = payment has occurred but order has not
# yet been finalized; errored = payment has occurred and order NEVER got finalized, eg due
# to server timeout or other event that happens during that step)

PENDING = 'pending' # string in authorization field that marks in-process CC order
ERRORED = 'errored' # payment made, but timeout/something bad happened that prevented order finalization

Expand Down Expand Up @@ -76,7 +78,7 @@ def check_purchaser_info
scope :completed, ->() { where('sold_on IS NOT NULL') }
scope :abandoned_since, ->(since) { where('sold_on IS NULL').where('updated_at < ?', since) }
scope :pending_but_paid, ->() { where(:authorization => PENDING) }

scope :for_customer_reporting, ->() {
includes(:vouchers => [:customer, :showdate,:vouchertype]).
includes(:donations => [:customer, :account_code]).
Expand All @@ -98,7 +100,7 @@ def customer_name ; customer.full_name ; end
def purchaser_name ; purchaser.full_name ; end

def purchase_medium ; Purchasemethod.get(purchasemethod).purchase_medium ; end

def self.new_from_donation(amount, account_code, donor)
order = Order.new(:purchaser => donor, :customer => donor)
order.add_donation(Donation.from_amount_and_account_code_id(amount, account_code.id))
Expand Down Expand Up @@ -185,10 +187,10 @@ def add_tickets_without_capacity_checks(valid_voucher, number, seats=[])
self.vouchers += new_vouchers
self.save!
else # since order can't proceed, DESTROY all vouchers so not orphaned
new_vouchers.each { |v| v.destroy if v.persisted? }
new_vouchers.each { |v| v.destroy if v.persisted? }
end
end

def ticket_count ; vouchers.size ; end
def item_count ; ticket_count + (includes_donation? ? 1 : 0) + retail_items.size; end

Expand Down Expand Up @@ -221,7 +223,7 @@ def reserved_seating_params
nil
end
end

def add_donation(d) ; self.donation = d ; end
def donation=(d)
self.donation_data[:amount] = d.amount
Expand All @@ -237,14 +239,18 @@ def includes_donation?
end
end

def add_recurring_donation()
@recurring_donation = @donation.build_recurring_donation(amount: 0, account_code_id: @donation.account_code_id)
end

def includes_bundle?
if completed?
items.any? { |v| v.kind_of?(Voucher) && v.bundle? }
else
vouchers.any? { |v| v.bundle? }
end
end

def add_retail_item(r)
raise Order::NotPersistedError unless persisted?
self.retail_items << r if r
Expand Down Expand Up @@ -312,7 +318,7 @@ def streaming_access_instructions
# this is almost certainly a Demeter violation...but not sure how to make better
vouchers.first.showdate.access_instructions
end

def collect_notes
# collect the showdate-specific (= show-specific) notes in the order
items.map(&:showdate).compact.uniq.map(&:patron_notes).compact
Expand Down Expand Up @@ -357,7 +363,7 @@ def finalize_with_existing_customer_id!(cid,processed_by,sold_on=Time.current)
self.processed_by = processed_by
self.finalize!(sold_on)
end

def finalize_with_new_customer!(customer,processed_by,sold_on=Time.current)
customer.force_valid = true
self.customer = self.purchaser = customer
Expand Down Expand Up @@ -385,6 +391,7 @@ def finalize!(sold_on_date = Time.current)
self.items += vouchers
self.items += retail_items
self.items << donation if donation
self.items << recurring_donation if recurring_donation
self.items.each do |i|
i.assign_attributes(:finalized => true,
:sold_on => sold_on_date,
Expand All @@ -397,6 +404,7 @@ def finalize!(sold_on_date = Time.current)
customer.add_items(vouchers)
customer.add_items(retail_items)
purchaser.add_items([donation]) if donation
purchaser.add_items([recurring_donation]) if recurring_donation
customer.save!
purchaser.save!
self.sold_on = sold_on_date
Expand All @@ -413,7 +421,7 @@ def finalize!(sold_on_date = Time.current)
raise e
end
end

def refundable?
completed? &&
items.any? { |i| !i.kind_of?(CanceledItem) } # in case all items were ALREADY refunded and now marked as canceled
Expand Down Expand Up @@ -451,13 +459,13 @@ def item_descriptions
end

def summary_of_contents
if includes_vouchers? && includes_donation?
'order and donation'
if includes_vouchers? && includes_donation?
'order and donation'
elsif includes_donation?
'donation'
else
'order'
end
end
end

end
7 changes: 3 additions & 4 deletions app/views/store/donate.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@
.form-group.form-row
%label.col-form-label.text-right.col-sm-6{:for => :donation} Donation frequency
.radio-group.col-sm-6.col-md-2.form-inline
= radio_button_tag 'donation_type', 'one', Option.default_donation_type == 'one', class: 'form-control', id: 'one'
= label_tag 'donation_type_one_time', 'One Time', class: 'form-control', for: 'one'
= radio_button_tag 'donation_type', 'monthly', Option.default_donation_type == 'monthly', class: 'form-control', id: 'monthly'
= label_tag 'donation_type_monthly', 'Monthly', class: 'form-control', for: 'monthly'
- @donation_frequency_options.each do |frequency|
= radio_button_tag 'donation_frequency', frequency, Option.default_donation_type == frequency, class: 'form-control', id: frequency
= label_tag 'donation_type_#{frequency}', frequency, class: 'form-control', for: frequency
.form-group.form-row
%label.col-form-label{:for => :donation_comments}
If you'd like to be recognized as Anonymous, or if you'd like to donate in honor
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20240310070441_add_recurring_donation_to_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddRecurringDonationToItems < ActiveRecord::Migration
def change
add_column :items, :recurring_donation_id, :integer
add_foreign_key :items, :items, column: :recurring_donation_id
end
end
4 changes: 2 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20240225093946) do

ActiveRecord::Schema.define(version: 20240310070441) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -97,6 +96,7 @@
t.boolean "finalized"
t.string "seat"
t.datetime "sold_on"
t.integer "recurring_donation_id"
end

add_index "items", ["account_code_id"], name: "index_items_on_account_code_id", using: :btree
Expand Down
Loading
Loading