effective_orders 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +856 -0
- data/Rakefile +24 -0
- data/app/assets/images/effective_orders/stripe_connect.png +0 -0
- data/app/assets/javascripts/effective_orders/shipping_address_toggle.js.coffee +30 -0
- data/app/assets/javascripts/effective_orders/stripe_charges.js.coffee +26 -0
- data/app/assets/javascripts/effective_orders/stripe_subscriptions.js.coffee +28 -0
- data/app/assets/javascripts/effective_orders.js +2 -0
- data/app/assets/stylesheets/effective_orders/_order.scss +30 -0
- data/app/assets/stylesheets/effective_orders.css.scss +1 -0
- data/app/controllers/admin/customers_controller.rb +15 -0
- data/app/controllers/admin/orders_controller.rb +22 -0
- data/app/controllers/effective/carts_controller.rb +70 -0
- data/app/controllers/effective/orders_controller.rb +191 -0
- data/app/controllers/effective/providers/moneris.rb +94 -0
- data/app/controllers/effective/providers/paypal.rb +29 -0
- data/app/controllers/effective/providers/stripe.rb +125 -0
- data/app/controllers/effective/providers/stripe_connect.rb +47 -0
- data/app/controllers/effective/subscriptions_controller.rb +123 -0
- data/app/controllers/effective/webhooks_controller.rb +86 -0
- data/app/helpers/effective_carts_helper.rb +90 -0
- data/app/helpers/effective_orders_helper.rb +108 -0
- data/app/helpers/effective_paypal_helper.rb +37 -0
- data/app/helpers/effective_stripe_helper.rb +63 -0
- data/app/mailers/effective/orders_mailer.rb +64 -0
- data/app/models/concerns/acts_as_purchasable.rb +134 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/cart.rb +65 -0
- data/app/models/effective/cart_item.rb +40 -0
- data/app/models/effective/customer.rb +61 -0
- data/app/models/effective/datatables/customers.rb +45 -0
- data/app/models/effective/datatables/orders.rb +53 -0
- data/app/models/effective/order.rb +247 -0
- data/app/models/effective/order_item.rb +69 -0
- data/app/models/effective/stripe_charge.rb +35 -0
- data/app/models/effective/subscription.rb +95 -0
- data/app/models/inputs/price_field.rb +63 -0
- data/app/models/inputs/price_form_input.rb +7 -0
- data/app/models/inputs/price_formtastic_input.rb +9 -0
- data/app/models/inputs/price_input.rb +19 -0
- data/app/models/inputs/price_simple_form_input.rb +8 -0
- data/app/models/validators/effective/sold_out_validator.rb +7 -0
- data/app/views/active_admin/effective_orders/orders/_show.html.haml +70 -0
- data/app/views/admin/customers/_actions.html.haml +2 -0
- data/app/views/admin/customers/index.html.haml +10 -0
- data/app/views/admin/orders/index.html.haml +7 -0
- data/app/views/admin/orders/show.html.haml +11 -0
- data/app/views/effective/carts/_cart.html.haml +33 -0
- data/app/views/effective/carts/show.html.haml +18 -0
- data/app/views/effective/orders/_checkout_step_1.html.haml +39 -0
- data/app/views/effective/orders/_checkout_step_2.html.haml +18 -0
- data/app/views/effective/orders/_my_purchases.html.haml +15 -0
- data/app/views/effective/orders/_order.html.haml +4 -0
- data/app/views/effective/orders/_order_header.html.haml +21 -0
- data/app/views/effective/orders/_order_items.html.haml +39 -0
- data/app/views/effective/orders/_order_payment_details.html.haml +11 -0
- data/app/views/effective/orders/_order_shipping.html.haml +19 -0
- data/app/views/effective/orders/_order_user_fields.html.haml +10 -0
- data/app/views/effective/orders/checkout.html.haml +3 -0
- data/app/views/effective/orders/declined.html.haml +10 -0
- data/app/views/effective/orders/moneris/_form.html.haml +34 -0
- data/app/views/effective/orders/my_purchases.html.haml +6 -0
- data/app/views/effective/orders/my_sales.html.haml +28 -0
- data/app/views/effective/orders/new.html.haml +4 -0
- data/app/views/effective/orders/paypal/_form.html.haml +5 -0
- data/app/views/effective/orders/purchased.html.haml +10 -0
- data/app/views/effective/orders/show.html.haml +17 -0
- data/app/views/effective/orders/stripe/_form.html.haml +8 -0
- data/app/views/effective/orders/stripe/_subscription_fields.html.haml +7 -0
- data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +8 -0
- data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +8 -0
- data/app/views/effective/orders_mailer/order_receipt_to_seller.html.haml +30 -0
- data/app/views/effective/subscriptions/index.html.haml +16 -0
- data/app/views/effective/subscriptions/new.html.haml +10 -0
- data/app/views/effective/subscriptions/show.html.haml +49 -0
- data/config/routes.rb +57 -0
- data/db/migrate/01_create_effective_orders.rb.erb +91 -0
- data/db/upgrade/02_upgrade_effective_orders_from03x.rb.erb +29 -0
- data/db/upgrade/upgrade_price_column_on_table.rb.erb +17 -0
- data/lib/effective_orders/engine.rb +52 -0
- data/lib/effective_orders/version.rb +3 -0
- data/lib/effective_orders.rb +76 -0
- data/lib/generators/effective_orders/install_generator.rb +38 -0
- data/lib/generators/effective_orders/upgrade_from03x_generator.rb +34 -0
- data/lib/generators/effective_orders/upgrade_price_column_generator.rb +34 -0
- data/lib/generators/templates/README +1 -0
- data/lib/generators/templates/effective_orders.rb +210 -0
- data/spec/controllers/carts_controller_spec.rb +143 -0
- data/spec/controllers/moneris_orders_controller_spec.rb +245 -0
- data/spec/controllers/orders_controller_spec.rb +418 -0
- data/spec/controllers/stripe_orders_controller_spec.rb +127 -0
- data/spec/controllers/webhooks_controller_spec.rb +79 -0
- data/spec/dummy/README.rdoc +8 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/product.rb +17 -0
- data/spec/dummy/app/models/product_with_float_price.rb +17 -0
- data/spec/dummy/app/models/user.rb +28 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +31 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +83 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/devise.rb +254 -0
- data/spec/dummy/config/initializers/effective_addresses.rb +15 -0
- data/spec/dummy/config/initializers/effective_orders.rb +22 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/schema.rb +142 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +487 -0
- data/spec/dummy/log/test.log +347 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/helpers/effective_orders_helper_spec.rb +21 -0
- data/spec/models/acts_as_purchasable_spec.rb +107 -0
- data/spec/models/customer_spec.rb +71 -0
- data/spec/models/factories_spec.rb +13 -0
- data/spec/models/order_item_spec.rb +35 -0
- data/spec/models/order_spec.rb +323 -0
- data/spec/models/stripe_charge_spec.rb +39 -0
- data/spec/models/subscription_spec.rb +103 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/factories.rb +118 -0
- metadata +387 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class OrdersMailer < ActionMailer::Base
|
|
3
|
+
helper EffectiveOrdersHelper
|
|
4
|
+
|
|
5
|
+
default :from => EffectiveOrders.mailer[:default_from]
|
|
6
|
+
|
|
7
|
+
def order_receipt_to_admin(order)
|
|
8
|
+
@order = order
|
|
9
|
+
mail(:to => EffectiveOrders.mailer[:admin_email], :subject => receipt_to_admin_subject(order))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def order_receipt_to_buyer(order) # Buyer
|
|
13
|
+
@order = order
|
|
14
|
+
mail(:to => order.user.email, :subject => receipt_to_buyer_subject(order))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def order_receipt_to_seller(order, seller, order_items)
|
|
18
|
+
@order = order
|
|
19
|
+
@user = seller.user
|
|
20
|
+
@order_items = order_items
|
|
21
|
+
|
|
22
|
+
mail(:to => @user.email, :subject => receipt_to_seller_subject(order, order_items, seller.user))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def receipt_to_admin_subject(order)
|
|
28
|
+
string_or_callable = EffectiveOrders.mailer[:subject_for_admin_receipt]
|
|
29
|
+
|
|
30
|
+
if string_or_callable.respond_to?(:call) # This is a Proc or a function, not a string
|
|
31
|
+
string_or_callable = self.instance_exec(order, &string_or_callable)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
prefix_subject(string_or_callable.presence || "Order ##{order.to_param} Receipt")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def receipt_to_buyer_subject(order)
|
|
38
|
+
string_or_callable = EffectiveOrders.mailer[:subject_for_buyer_receipt]
|
|
39
|
+
|
|
40
|
+
if string_or_callable.respond_to?(:call) # This is a Proc or a function, not a string
|
|
41
|
+
string_or_callable = self.instance_exec(order, &string_or_callable)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
prefix_subject(string_or_callable.presence || "Order ##{order.to_param} Receipt")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def receipt_to_seller_subject(order, order_items, seller)
|
|
48
|
+
string_or_callable = EffectiveOrders.mailer[:subject_for_seller_receipt]
|
|
49
|
+
|
|
50
|
+
if string_or_callable.respond_to?(:call) # This is a Proc or a function, not a string
|
|
51
|
+
string_or_callable = self.instance_exec(order, order_items, seller, &string_or_callable)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
prefix_subject(string_or_callable.presence || "#{order_items.count} of your products #{order_items.count > 1 ? 'have' : 'has'} been purchased")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def prefix_subject(text)
|
|
58
|
+
prefix = (EffectiveOrders.mailer[:subject_prefix].to_s rescue '')
|
|
59
|
+
prefix.present? ? (prefix.chomp(' ') + ' ' + text) : text
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module ActsAsPurchasable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
def acts_as_purchasable(*options)
|
|
6
|
+
@acts_as_purchasable = options || []
|
|
7
|
+
include ::ActsAsPurchasable
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
has_many :orders, :through => :order_items, :class_name => 'Effective::Order'
|
|
13
|
+
has_many :order_items, :as => :purchasable, :class_name => 'Effective::OrderItem'
|
|
14
|
+
has_many :cart_items, :as => :purchasable, :dependent => :delete_all, :class_name => 'Effective::CartItem'
|
|
15
|
+
|
|
16
|
+
validates_with Effective::SoldOutValidator, :on => :create
|
|
17
|
+
|
|
18
|
+
validates :price, :presence => true, :numericality => true
|
|
19
|
+
validates :tax_exempt, :inclusion => {:in => [true, false]}
|
|
20
|
+
|
|
21
|
+
# These are breaking on the check for quanitty_enabled?. More research is due
|
|
22
|
+
validates :quantity_purchased, :numericality => {:allow_nil => true}, :if => proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
|
23
|
+
validates :quantity_max, :numericality => {:allow_nil => true}, :if => proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
|
|
24
|
+
|
|
25
|
+
scope :purchased, -> { joins(:order_items).joins(:orders).where(:orders => {:purchase_state => EffectiveOrders::PURCHASED}).uniq }
|
|
26
|
+
scope :purchased_by, lambda { |user| joins(:order_items).joins(:orders).where(:orders => {:user_id => user.try(:id), :purchase_state => EffectiveOrders::PURCHASED}).uniq }
|
|
27
|
+
scope :sold, -> { purchased() }
|
|
28
|
+
scope :sold_by, lambda { |user| joins(:order_items).joins(:orders).where(:order_items => {:seller_id => user.try(:id)}).where(:orders => {:purchase_state => EffectiveOrders::PURCHASED}).uniq }
|
|
29
|
+
|
|
30
|
+
scope :not_purchased, -> { where('id NOT IN (?)', purchased.pluck(:id).presence || [0]) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module ClassMethods
|
|
34
|
+
def after_purchase(&block)
|
|
35
|
+
send :define_method, :after_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def after_decline(&block)
|
|
39
|
+
send :define_method, :after_decline do |order, order_item| self.instance_exec(order, order_item, &block) end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Regular instance methods
|
|
44
|
+
def is_effectively_purchasable?
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def price
|
|
49
|
+
self[:price] || 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# If I have a column type of Integer, and I'm passed a non-Integer, convert it here
|
|
53
|
+
def price=(value)
|
|
54
|
+
integer_column = ((column_for_attribute('price').try(:type) rescue nil) == :integer) # Rails built in method to lookup datatype
|
|
55
|
+
|
|
56
|
+
if integer_column == false
|
|
57
|
+
super
|
|
58
|
+
elsif value.kind_of?(Integer)
|
|
59
|
+
super
|
|
60
|
+
elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
|
|
61
|
+
super
|
|
62
|
+
else # Could be Float, BigDecimal, or String like 9.99
|
|
63
|
+
super((value.to_f * 100.0).to_i)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def title
|
|
68
|
+
self[:title] || 'ActsAsPurchasable'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tax_exempt
|
|
72
|
+
self[:tax_exempt] || false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def tax_rate
|
|
76
|
+
@tax_rate ||= (
|
|
77
|
+
self.instance_exec(self, &EffectiveOrders.tax_rate_method).to_f.tap do |rate|
|
|
78
|
+
raise ArgumentError.new("expected EffectiveOrders.tax_rate_method to return a value between 0 and 1. Received #{rate}. Please return 0.05 for 5% tax.") if (rate > 1.0 || rate < 0.0)
|
|
79
|
+
end
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def seller
|
|
84
|
+
if EffectiveOrders.stripe_connect_enabled
|
|
85
|
+
raise 'acts_as_purchasable object requires the seller be defined to return the User selling this item. This is only a requirement when using StripeConnect.'
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def purchased?
|
|
90
|
+
@is_purchased ||= orders.any? { |order| order.purchased? }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def purchased_by?(user)
|
|
94
|
+
orders.any? { |order| order.purchased? && order.user_id == user.id }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def purchased_orders
|
|
98
|
+
orders.select { |order| order.purchased? }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def quantity_enabled?
|
|
102
|
+
self.respond_to?(:quantity_enabled) ? quantity_enabled == true : false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def quantity_remaining
|
|
106
|
+
(quantity_max - quantity_purchased) rescue 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def sold_out?
|
|
110
|
+
quantity_enabled? ? (quantity_remaining == 0) : false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def purchased!(order = nil, order_item = nil)
|
|
114
|
+
# begin
|
|
115
|
+
# self.quantity_purchased = (self.quantity_purchased + 1)
|
|
116
|
+
# rescue
|
|
117
|
+
# end
|
|
118
|
+
|
|
119
|
+
after_purchase(order, order_item) if self.respond_to?(:after_purchase)
|
|
120
|
+
self.save!
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def declined!(order = nil, order_item = nil)
|
|
124
|
+
after_decline(order, order_item) if self.respond_to?(:after_decline)
|
|
125
|
+
self.save!
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Override me if this is a digital purchase.
|
|
129
|
+
def purchased_download_url
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
end
|
|
134
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
unless defined?(Effective::AccessDenied)
|
|
2
|
+
module Effective
|
|
3
|
+
class AccessDenied < StandardError
|
|
4
|
+
attr_reader :action, :subject
|
|
5
|
+
|
|
6
|
+
def initialize(message = nil, action = nil, subject = nil)
|
|
7
|
+
@message = message
|
|
8
|
+
@action = action
|
|
9
|
+
@subject = subject
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
@message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Cart < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.carts_table_name.to_s
|
|
4
|
+
|
|
5
|
+
belongs_to :user # This is optional. We want to let non-logged-in people have carts too
|
|
6
|
+
has_many :cart_items, :inverse_of => :cart, :dependent => :delete_all
|
|
7
|
+
|
|
8
|
+
structure do
|
|
9
|
+
timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
default_scope -> { includes(:cart_items => :purchasable) }
|
|
13
|
+
|
|
14
|
+
def add(item, quantity = 1)
|
|
15
|
+
raise 'expecting an acts_as_purchasable object' unless item.respond_to?(:is_effectively_purchasable?)
|
|
16
|
+
|
|
17
|
+
existing_item = cart_items.where(:purchasable_id => item.id, :purchasable_type => item.class.name).first
|
|
18
|
+
|
|
19
|
+
if item.quantity_enabled? && (quantity + (existing_item.quantity rescue 0)) > item.quantity_remaining
|
|
20
|
+
raise EffectiveOrders::SoldOutException, "#{item.title} is sold out"
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if existing_item.present?
|
|
25
|
+
existing_item.update_attributes(:quantity => existing_item.quantity + quantity)
|
|
26
|
+
else
|
|
27
|
+
cart_items.create(:cart => self, :purchasable_id => item.id, :purchasable_type => item.class.name, :quantity => quantity)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
alias_method :add_to_cart, :add
|
|
31
|
+
|
|
32
|
+
def remove(obj)
|
|
33
|
+
(cart_items.find(cart_item) || cart_item).try(:destroy)
|
|
34
|
+
end
|
|
35
|
+
alias_method :remove_from_cart, :remove
|
|
36
|
+
|
|
37
|
+
def includes?(item)
|
|
38
|
+
find(item).present?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find(item)
|
|
42
|
+
cart_items.to_a.find { |cart_item| cart_item == item || cart_item.purchasable == item }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def size
|
|
46
|
+
cart_items.size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def empty?
|
|
50
|
+
size == 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def subtotal
|
|
54
|
+
cart_items.map(&:subtotal).sum
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def tax
|
|
58
|
+
cart_items.map(&:tax).sum
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def total
|
|
62
|
+
cart_items.map(&:total).sum
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class CartItem < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.cart_items_table_name.to_s
|
|
4
|
+
|
|
5
|
+
belongs_to :cart
|
|
6
|
+
belongs_to :purchasable, :polymorphic => true
|
|
7
|
+
|
|
8
|
+
structure do
|
|
9
|
+
quantity :integer, :validates => [:presence]
|
|
10
|
+
timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
validates_presence_of :purchasable
|
|
14
|
+
|
|
15
|
+
delegate :title, :tax_exempt, :tax_rate, :to => :purchasable
|
|
16
|
+
|
|
17
|
+
default_scope -> { order(:updated_at) }
|
|
18
|
+
|
|
19
|
+
def price
|
|
20
|
+
if (purchasable.price || 0).kind_of?(Integer)
|
|
21
|
+
purchasable.price || 0
|
|
22
|
+
else
|
|
23
|
+
ActiveSupport::Deprecation.warn('price is a non-integer. It should be an Integer representing the number of cents. Continuing with (price * 100.0).floor conversion') unless EffectiveOrders.silence_deprecation_warnings
|
|
24
|
+
(purchasable.price * 100.0).floor rescue 0
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def subtotal
|
|
29
|
+
price * quantity
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tax
|
|
33
|
+
tax_exempt ? 0 : (subtotal * tax_rate).ceil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def total
|
|
37
|
+
subtotal + tax
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Customer < ActiveRecord::Base
|
|
3
|
+
self.table_name = EffectiveOrders.customers_table_name.to_s
|
|
4
|
+
|
|
5
|
+
attr_accessor :token # This is a convenience method so we have a place to store StripeConnect temporary access tokens
|
|
6
|
+
|
|
7
|
+
belongs_to :user
|
|
8
|
+
has_many :subscriptions, :inverse_of => :customer
|
|
9
|
+
|
|
10
|
+
structure do
|
|
11
|
+
stripe_customer_id :string # cus_xja7acoa03
|
|
12
|
+
stripe_active_card :string # **** **** **** 4242 Visa 05/12
|
|
13
|
+
stripe_connect_access_token :string # If using StripeConnect and this user is a connected Seller
|
|
14
|
+
|
|
15
|
+
timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
validates_presence_of :user
|
|
19
|
+
validates_uniqueness_of :user_id # Only 1 customer per user may exist
|
|
20
|
+
|
|
21
|
+
scope :customers, -> { where("#{EffectiveOrders.customers_table_name.to_s}.stripe_customer_id IS NOT NULL") }
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def for_user(user)
|
|
25
|
+
if user.present?
|
|
26
|
+
Effective::Customer.where(:user_id => (user.try(:id) rescue user.to_i)).first_or_create
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stripe_customer
|
|
32
|
+
@stripe_customer ||= if stripe_customer_id.present?
|
|
33
|
+
::Stripe::Customer.retrieve(stripe_customer_id)
|
|
34
|
+
else
|
|
35
|
+
::Stripe::Customer.create(:email => user.email, :description => user.id.to_s).tap do |stripe_customer|
|
|
36
|
+
self.update_attributes(:stripe_customer_id => stripe_customer.id)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update_card!(token)
|
|
42
|
+
if token.present? # Oh, so they want to use a new credit card...
|
|
43
|
+
stripe_customer.card = token # This sets the default_card to the new card
|
|
44
|
+
|
|
45
|
+
if stripe_customer.save && stripe_customer.default_card.present?
|
|
46
|
+
card = stripe_customer.cards.retrieve(stripe_customer.default_card)
|
|
47
|
+
|
|
48
|
+
self.stripe_active_card = "**** **** **** #{card.last4} #{card.type} #{card.exp_month}/#{card.exp_year}"
|
|
49
|
+
self.save!
|
|
50
|
+
else
|
|
51
|
+
raise Exception.new('unable to update stripe customer with new card')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def is_stripe_connect_seller?
|
|
57
|
+
stripe_connect_access_token.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
if defined?(EffectiveDatatables)
|
|
2
|
+
module Effective
|
|
3
|
+
module Datatables
|
|
4
|
+
class Customers < Effective::Datatable
|
|
5
|
+
table_column :email, :column => 'users.email' do |user|
|
|
6
|
+
mail_to user.email, user.email
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
if EffectiveOrders.stripe_enabled
|
|
10
|
+
table_column :stripe_customer_id
|
|
11
|
+
table_column :stripe_active_card
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
if EffectiveOrders.stripe_connect_enabled
|
|
15
|
+
table_column :stripe_connect_access_token
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
table_column :subscription_types, :column => 'subscription_types'
|
|
19
|
+
|
|
20
|
+
table_column :actions, :sortable => false, :filter => false, :partial => '/admin/customers/actions'
|
|
21
|
+
|
|
22
|
+
def collection
|
|
23
|
+
Effective::Customer.customers.uniq
|
|
24
|
+
.joins(:user)
|
|
25
|
+
.joins(:subscriptions)
|
|
26
|
+
.select('customers.*')
|
|
27
|
+
.select('users.email AS email')
|
|
28
|
+
.select("array_to_string(array(#{Effective::Subscription.purchased.select('subscriptions.stripe_plan_id').where('subscriptions.customer_id = customers.id').to_sql}), ' ,') AS subscription_types")
|
|
29
|
+
.group('customers.id')
|
|
30
|
+
.group('subscriptions.stripe_plan_id')
|
|
31
|
+
.group('users.email')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def search_column(collection, table_column, search_term)
|
|
35
|
+
if table_column[:name] == 'subscription_types'
|
|
36
|
+
collection.where('subscriptions.stripe_plan_id ILIKE ?', "%#{search_term}%")
|
|
37
|
+
else
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
if defined?(EffectiveDatatables)
|
|
2
|
+
module Effective
|
|
3
|
+
module Datatables
|
|
4
|
+
class Orders < Effective::Datatable
|
|
5
|
+
table_column :id do |order|
|
|
6
|
+
order.to_param
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
array_column :email, :label => 'Buyer', :if => Proc.new { attributes[:user_id].blank? } do |order|
|
|
10
|
+
link_to order.user.email, (edit_admin_user_path(order.user) rescue admin_user_path(order.user) rescue '#')
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
array_column :order_items do |order|
|
|
14
|
+
content_tag(:ul) do
|
|
15
|
+
order.order_items.map { |oi| content_tag(:li, oi.title) }.join().html_safe
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
table_column :purchased_at
|
|
20
|
+
|
|
21
|
+
array_column :total do |order|
|
|
22
|
+
price_to_currency(order.total)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
table_column :actions, :sortable => false, :filter => false do |order|
|
|
26
|
+
content_tag(:span, :style => 'white-space: nowrap;') do
|
|
27
|
+
[
|
|
28
|
+
link_to('View', (datatables_admin_path? ? effective_orders.admin_order_path(order) : effective_orders.order_path(order))),
|
|
29
|
+
(link_to('Resend Receipt', effective_orders.resend_buyer_receipt_path(order), {'data-confirm' => 'This action will resend a copy of the original email receipt. Send receipt now?'}) if order.try(:purchased?))
|
|
30
|
+
].compact.join(' - ').html_safe
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def collection
|
|
35
|
+
if attributes[:user_id].present?
|
|
36
|
+
Effective::Order.purchased.where(:user_id => attributes[:user_id]).includes(:user).includes(:order_items)
|
|
37
|
+
else
|
|
38
|
+
Effective::Order.purchased.includes(:user).includes(:order_items)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def search_column(collection, table_column, search_term)
|
|
43
|
+
if table_column[:name] == 'id'
|
|
44
|
+
collection.where(:id => search_term)
|
|
45
|
+
else
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|