effective_orders 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|