effective_orders 1.8.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +150 -60
- data/Rakefile +0 -3
- data/active_admin/effective_orders.rb +14 -9
- data/app/assets/javascripts/effective_orders.js +2 -0
- data/app/assets/javascripts/effective_orders/{stripe_charges.js.coffee → providers/stripe_charges.js.coffee} +1 -1
- data/app/assets/javascripts/effective_orders/{stripe_subscriptions.js.coffee → providers/stripe_subscriptions.js.coffee} +1 -1
- data/app/assets/stylesheets/effective_orders/_order.scss +9 -3
- data/app/controllers/admin/orders_controller.rb +87 -3
- data/app/controllers/concerns/acts_as_active_admin_controller.rb +2 -1
- data/app/controllers/effective/carts_controller.rb +4 -2
- data/app/controllers/effective/orders_controller.rb +101 -60
- data/app/controllers/effective/providers/app_checkout.rb +10 -2
- data/app/controllers/effective/providers/ccbill.rb +34 -0
- data/app/controllers/effective/providers/cheque.rb +30 -0
- data/app/controllers/effective/providers/moneris.rb +7 -7
- data/app/controllers/effective/providers/paypal.rb +7 -5
- data/app/controllers/effective/providers/pretend.rb +22 -0
- data/app/controllers/effective/providers/stripe.rb +26 -24
- data/app/controllers/effective/webhooks_controller.rb +1 -1
- data/app/helpers/effective_carts_helper.rb +59 -29
- data/app/helpers/effective_ccbill_helper.rb +25 -0
- data/app/helpers/effective_orders_helper.rb +50 -41
- data/app/helpers/effective_paypal_helper.rb +11 -11
- data/app/mailers/effective/orders_mailer.rb +95 -20
- data/app/models/concerns/acts_as_purchasable.rb +14 -22
- data/app/models/effective/cart.rb +9 -15
- data/app/models/effective/cart_item.rb +15 -13
- data/app/models/effective/customer.rb +9 -9
- data/app/models/effective/datatables/order_items.rb +14 -8
- data/app/models/effective/datatables/orders.rb +56 -69
- data/app/models/effective/order.rb +250 -139
- data/app/models/effective/order_item.rb +23 -16
- data/app/models/effective/product.rb +23 -0
- data/app/models/effective/providers/ccbill_postback.rb +85 -0
- data/app/models/effective/{stripe_charge.rb → providers/stripe_charge.rb} +1 -1
- data/app/models/effective/subscription.rb +16 -12
- data/app/models/effective/tax_rate_calculator.rb +45 -0
- data/app/views/admin/customers/index.html.haml +2 -5
- data/app/views/admin/order_items/index.html.haml +2 -5
- data/app/views/admin/orders/_actions.html.haml +2 -0
- data/app/views/admin/orders/_form.html.haml +28 -0
- data/app/views/admin/orders/_order_item_fields.html.haml +9 -0
- data/app/views/admin/orders/index.html.haml +9 -5
- data/app/views/admin/orders/new.html.haml +3 -0
- data/app/views/effective/carts/_cart.html.haml +3 -12
- data/app/views/effective/carts/_cart_actions.html.haml +4 -0
- data/app/views/effective/carts/show.html.haml +10 -12
- data/app/views/effective/orders/_checkout_step1.html.haml +46 -0
- data/app/views/effective/orders/_checkout_step2.html.haml +33 -0
- data/app/views/effective/orders/_order.html.haml +2 -0
- data/app/views/effective/orders/_order_actions.html.haml +10 -5
- data/app/views/effective/orders/_order_footer.html.haml +1 -0
- data/app/views/effective/orders/_order_items.html.haml +11 -9
- data/app/views/effective/orders/_order_note.html.haml +8 -0
- data/app/views/effective/orders/_order_note_fields.html.haml +5 -0
- data/app/views/effective/orders/_order_shipping.html.haml +4 -4
- data/app/views/effective/orders/_order_user_fields.html.haml +1 -0
- data/app/views/effective/orders/_orders_table.html.haml +27 -0
- data/app/views/effective/orders/ccbill/_form.html.haml +24 -0
- data/app/views/effective/orders/checkout_step1.html.haml +3 -0
- data/app/views/effective/orders/checkout_step2.html.haml +3 -0
- data/app/views/effective/orders/cheque/_form.html.haml +2 -0
- data/app/views/effective/orders/cheque/pay_by_cheque.html.haml +9 -0
- data/app/views/effective/orders/declined.html.haml +4 -2
- data/app/views/effective/orders/my_purchases.html.haml +1 -1
- data/app/views/effective/orders/my_sales.html.haml +1 -1
- data/app/views/effective/orders/purchased.html.haml +3 -2
- data/app/views/effective/orders/show.html.haml +1 -1
- data/app/views/effective/orders/stripe/_form.html.haml +5 -5
- data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
- data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
- data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
- data/app/views/layouts/effective_orders_mailer_layout.html.haml +7 -7
- data/config/routes.rb +39 -24
- data/db/migrate/01_create_effective_orders.rb.erb +20 -1
- data/db/upgrade/03_upgrade_effective_orders_from1x.rb.erb +95 -0
- data/lib/effective_orders.rb +67 -9
- data/lib/effective_orders/app_checkout_service.rb +1 -2
- data/lib/effective_orders/engine.rb +46 -14
- data/lib/effective_orders/version.rb +1 -1
- data/lib/generators/effective_orders/install_generator.rb +1 -0
- data/lib/generators/effective_orders/upgrade_from03x_generator.rb +1 -0
- data/lib/generators/effective_orders/upgrade_from1x_generator.rb +31 -0
- data/lib/generators/templates/effective_orders.rb +131 -66
- data/lib/generators/templates/effective_orders_mailer_preview.rb +28 -13
- data/spec/controllers/admin/orders_controller_spec.rb +242 -0
- data/spec/controllers/ccbill_orders_controller_spec.rb +103 -0
- data/spec/controllers/moneris_orders_controller_spec.rb +23 -23
- data/spec/controllers/orders_controller_spec.rb +167 -79
- data/spec/controllers/stripe_orders_controller_spec.rb +7 -7
- data/spec/dummy/app/models/product.rb +0 -8
- data/spec/dummy/app/models/product_with_float_price.rb +0 -8
- data/spec/dummy/app/models/user.rb +2 -19
- data/spec/dummy/config/application.rb +2 -1
- data/spec/dummy/config/environments/test.rb +1 -0
- data/spec/dummy/config/initializers/effective_orders.rb +109 -64
- data/spec/dummy/db/schema.rb +15 -2
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +0 -258
- data/spec/models/acts_as_purchasable_spec.rb +8 -6
- data/spec/models/factories_spec.rb +7 -1
- data/spec/models/order_item_spec.rb +1 -1
- data/spec/models/order_spec.rb +165 -46
- data/spec/models/stripe_charge_spec.rb +5 -5
- data/spec/spec_helper.rb +2 -0
- data/spec/support/factories.rb +49 -33
- metadata +47 -64
- data/app/views/effective/orders/_checkout_step_1.html.haml +0 -43
- data/app/views/effective/orders/_checkout_step_2.html.haml +0 -25
- data/app/views/effective/orders/_my_purchases.html.haml +0 -17
- data/app/views/effective/orders/checkout.html.haml +0 -3
- data/app/views/effective/orders/new.html.haml +0 -4
- data/spec/dummy/log/development.log +0 -82
@@ -5,14 +5,14 @@ module Effective
|
|
5
5
|
belongs_to :cart
|
6
6
|
belongs_to :purchasable, :polymorphic => true
|
7
7
|
|
8
|
-
structure do
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
validates_presence_of :purchasable
|
8
|
+
# structure do
|
9
|
+
# quantity :integer
|
10
|
+
#
|
11
|
+
# timestamps
|
12
|
+
# end
|
14
13
|
|
15
|
-
|
14
|
+
validates :purchasable, presence: true
|
15
|
+
validates :quantity, presence: true
|
16
16
|
|
17
17
|
default_scope -> { order(:updated_at) }
|
18
18
|
|
@@ -25,16 +25,18 @@ module Effective
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
|
28
|
+
def title
|
29
|
+
purchasable.try(:title) || 'New Cart Item'
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
33
|
-
tax_exempt
|
32
|
+
def tax_exempt
|
33
|
+
purchasable.try(:tax_exempt) || false
|
34
34
|
end
|
35
35
|
|
36
|
-
def
|
37
|
-
|
36
|
+
def subtotal
|
37
|
+
price * quantity
|
38
38
|
end
|
39
|
+
alias_method :total, :subtotal
|
40
|
+
|
39
41
|
end
|
40
42
|
end
|
@@ -7,16 +7,16 @@ module Effective
|
|
7
7
|
belongs_to :user
|
8
8
|
has_many :subscriptions, :inverse_of => :customer
|
9
9
|
|
10
|
-
structure do
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
validates_presence_of :user
|
19
|
-
validates_uniqueness_of :user_id # Only 1 customer per user may exist
|
18
|
+
validates :user, presence: true
|
19
|
+
validates :user_id, uniqueness: true
|
20
20
|
|
21
21
|
scope :customers, -> { where("#{EffectiveOrders.customers_table_name.to_s}.stripe_customer_id IS NOT NULL") }
|
22
22
|
|
@@ -5,15 +5,21 @@ if defined?(EffectiveDatatables)
|
|
5
5
|
datatable do
|
6
6
|
default_order :purchased_at, :desc
|
7
7
|
|
8
|
-
table_column(:purchased_at, :
|
8
|
+
table_column(:purchased_at, type: :datetime, column: 'orders.purchased_at') do |order_item|
|
9
9
|
Time.at(order_item[:purchased_at]).in_time_zone if order_item[:purchased_at].present?
|
10
10
|
end
|
11
11
|
|
12
|
-
table_column :id, :
|
12
|
+
table_column :id, visible: false
|
13
13
|
|
14
|
-
|
15
|
-
obfuscated_id
|
16
|
-
|
14
|
+
if EffectiveOrders.obfuscate_order_ids
|
15
|
+
table_column(:order, type: :obfuscated_id, sortable: false) do |order_item|
|
16
|
+
obfuscated_id = Effective::Order.obfuscate(order_item[:order_id])
|
17
|
+
link_to(obfuscated_id, (datatables_admin_path? ? effective_orders.admin_order_path(obfuscated_id) : effective_orders.order_path(obfuscated_id)))
|
18
|
+
end
|
19
|
+
else
|
20
|
+
table_column(:order, sortable: false) do |order_item|
|
21
|
+
link_to(order_item.to_param, (datatables_admin_path? ? effective_orders.admin_order_path(order_item.to_param) : effective_orders.order_path(order_item.to_param)))
|
22
|
+
end
|
17
23
|
end
|
18
24
|
|
19
25
|
table_column :email, column: 'users.email', label: 'Buyer Email', if: proc { attributes[:user_id].blank? } do |order_item|
|
@@ -38,13 +44,13 @@ if defined?(EffectiveDatatables)
|
|
38
44
|
table_column(:tax) { |order_item| price_to_currency(order_item[:tax].to_i) }
|
39
45
|
table_column(:total) { |order_item| price_to_currency(order_item[:total].to_i) }
|
40
46
|
|
41
|
-
table_column :created_at, :
|
42
|
-
table_column :updated_at, :
|
47
|
+
table_column :created_at, visible: false
|
48
|
+
table_column :updated_at, visible: false
|
43
49
|
end
|
44
50
|
|
45
51
|
def collection
|
46
52
|
collection = Effective::OrderItem.unscoped
|
47
|
-
.joins(:
|
53
|
+
.joins(order: :user)
|
48
54
|
.select('order_items.*, orders.*, users.email AS email')
|
49
55
|
.select("#{query_subtotal} AS subtotal, #{query_tax} AS tax, #{query_total} AS total")
|
50
56
|
.group('order_items.id, orders.id, users.email')
|
@@ -3,100 +3,87 @@ if defined?(EffectiveDatatables)
|
|
3
3
|
module Datatables
|
4
4
|
class Orders < Effective::Datatable
|
5
5
|
datatable do
|
6
|
-
default_order :
|
6
|
+
default_order :created_at, :desc
|
7
7
|
|
8
8
|
table_column :purchased_at
|
9
|
-
table_column :id
|
10
9
|
|
11
|
-
table_column :
|
12
|
-
link_to order
|
10
|
+
table_column :id, label: 'ID' do |order|
|
11
|
+
link_to order.to_param, effective_orders.admin_order_path(order)
|
13
12
|
end
|
14
13
|
|
15
|
-
if
|
16
|
-
|
17
|
-
|
14
|
+
# Don't display email or buyer_name column if this is for a specific user
|
15
|
+
if attributes[:user_id].blank?
|
16
|
+
table_column :email, column: 'users.email', label: 'Buyer Email' do |order|
|
17
|
+
link_to order.user.email, (edit_admin_user_path(order.user) rescue admin_user_path(order.user) rescue '#')
|
18
18
|
end
|
19
|
-
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
if EffectiveOrders.use_address_full_name
|
21
|
+
table_column :buyer_name, column: 'addresses.full_name' do |order|
|
22
|
+
order.billing_address.try(:full_name)
|
23
|
+
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
elsif # Not using address full name
|
26
|
+
table_column :buyer_name, column: 'users.*' do |order|
|
27
|
+
order.user.to_s
|
28
|
+
end
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
if EffectiveOrders.require_billing_address
|
33
|
+
table_column :billing_address
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
+
if EffectiveOrders.require_shipping_address
|
37
|
+
table_column :shipping_address
|
38
|
+
end
|
36
39
|
|
37
|
-
table_column :
|
38
|
-
|
40
|
+
table_column :purchase_state, label: 'State', filter: { values: purchase_state_filter_values } do |order|
|
41
|
+
order.purchase_state || 'abandoned'
|
39
42
|
end
|
40
|
-
end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
.
|
49
|
-
.select("#{query_payment_method} AS payment_method")
|
50
|
-
|
51
|
-
if EffectiveOrders.require_billing_address && defined?(EffectiveAddresses)
|
52
|
-
addresses_tbl = EffectiveAddresses.addresses_table_name
|
53
|
-
|
54
|
-
collection = collection
|
55
|
-
.joins("LEFT JOIN (SELECT addressable_id, string_agg(#{addresses_tbl}.full_name, '!!SEP!!') AS buyer_name FROM #{addresses_tbl} WHERE #{addresses_tbl}.category = 'billing' AND #{addresses_tbl}.addressable_type = 'Effective::Order' GROUP BY #{addresses_tbl}.addressable_id) #{addresses_tbl} ON orders.id = #{addresses_tbl}.addressable_id")
|
56
|
-
.group("#{addresses_tbl}.buyer_name")
|
57
|
-
.select("#{addresses_tbl}.buyer_name AS buyer_name")
|
44
|
+
table_column :order_items, column: 'order_items.title', filter: :string
|
45
|
+
|
46
|
+
table_column :subtotal, as: :price
|
47
|
+
table_column :tax, as: :price
|
48
|
+
|
49
|
+
table_column :tax_rate, visible: false do |order|
|
50
|
+
tax_rate_to_percentage(order.tax_rate)
|
58
51
|
end
|
59
52
|
|
60
|
-
|
61
|
-
end
|
53
|
+
table_column :total, as: :price
|
62
54
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
55
|
+
table_column :payment_provider, label: 'Provider', visible: false, filter: { values: ['nil'] + EffectiveOrders.payment_providers }
|
56
|
+
table_column :payment_card, label: 'Card'
|
66
57
|
|
67
|
-
|
68
|
-
'SUM(CASE order_items.tax_exempt WHEN true THEN 0 ELSE ((order_items.price * order_items.quantity) * order_items.tax_rate) END)'
|
69
|
-
end
|
58
|
+
table_column :note, visible: false
|
70
59
|
|
71
|
-
|
72
|
-
|
73
|
-
end
|
60
|
+
table_column :created_at, visible: false
|
61
|
+
table_column :updated_at, visible: false
|
74
62
|
|
75
|
-
|
76
|
-
"string_agg(order_items.title, '!!SEP!!')"
|
63
|
+
table_column :actions, sortable: false, filter: false, partial: 'admin/orders/actions'
|
77
64
|
end
|
78
65
|
|
79
|
-
def
|
80
|
-
|
81
|
-
|
66
|
+
def collection
|
67
|
+
collection = Effective::Order.unscoped
|
68
|
+
.joins(:user)
|
69
|
+
.includes(:addresses)
|
70
|
+
.includes(:user)
|
71
|
+
.includes(:order_items)
|
82
72
|
|
83
|
-
|
84
|
-
|
85
|
-
when 'subtotal'
|
86
|
-
then collection.having("#{query_subtotal} = ?", (search_term.gsub(/[^0-9.]/, '').to_f * 100.0).to_i)
|
87
|
-
when 'tax'
|
88
|
-
then collection.having("#{query_tax} = ?", (search_term.gsub(/[^0-9.]/, '').to_f * 100.0).to_i)
|
89
|
-
when 'total'
|
90
|
-
then collection.having("#{query_total} = ?", (search_term.gsub(/[^0-9.]/, '').to_f * 100.0).to_i)
|
91
|
-
when 'purchase_state'
|
92
|
-
then search_term == 'abandoned' ? collection.where(purchase_state: nil) : super
|
93
|
-
when 'items'
|
94
|
-
then collection.having("#{query_items_list} ILIKE ?", "%#{search_term}%")
|
95
|
-
when 'payment_method'
|
96
|
-
then collection.having("#{query_payment_method} LIKE ?", "#{search_term}%")
|
97
|
-
else
|
98
|
-
super
|
73
|
+
if EffectiveOrders.orders_collection_scope.respond_to?(:call)
|
74
|
+
collection = EffectiveOrders.orders_collection_scope.call(collection)
|
99
75
|
end
|
76
|
+
|
77
|
+
attributes[:user_id].present? ? collection.where(user_id: attributes[:user_id]) : collection
|
78
|
+
end
|
79
|
+
|
80
|
+
def purchase_state_filter_values
|
81
|
+
[
|
82
|
+
%w(abandoned nil),
|
83
|
+
[EffectiveOrders::PURCHASED, EffectiveOrders::PURCHASED],
|
84
|
+
[EffectiveOrders::DECLINED, EffectiveOrders::DECLINED],
|
85
|
+
[EffectiveOrders::PENDING, EffectiveOrders::PENDING]
|
86
|
+
]
|
100
87
|
end
|
101
88
|
end
|
102
89
|
end
|
@@ -3,52 +3,120 @@ module Effective
|
|
3
3
|
self.table_name = EffectiveOrders.orders_table_name.to_s
|
4
4
|
|
5
5
|
if EffectiveOrders.obfuscate_order_ids
|
6
|
-
acts_as_obfuscated :
|
6
|
+
acts_as_obfuscated format: '###-####-###'
|
7
7
|
end
|
8
8
|
|
9
|
-
acts_as_addressable
|
10
|
-
|
9
|
+
acts_as_addressable(
|
10
|
+
:billing => { singular: true, use_full_name: EffectiveOrders.use_address_full_name },
|
11
|
+
:shipping => { singular: true, use_full_name: EffectiveOrders.use_address_full_name }
|
12
|
+
)
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
attr_accessor :save_billing_address, :save_shipping_address, :shipping_address_same_as_billing # save these addresses to the user if selected
|
15
|
+
attr_accessor :send_payment_request_to_buyer # Used by the /admin/orders/new form. Should the payment request email be sent after creating an order?
|
16
|
+
attr_accessor :skip_buyer_validations # Enabled by the /admin/orders/create action
|
17
|
+
|
18
|
+
belongs_to :user, validate: false # This is the user who purchased the order. We validate it below.
|
19
|
+
has_many :order_items, inverse_of: :order
|
20
|
+
|
21
|
+
# structure do
|
22
|
+
# purchase_state :string
|
23
|
+
# purchased_at :datetime
|
24
|
+
#
|
25
|
+
# note :text
|
26
|
+
#
|
27
|
+
# payment :text # serialized hash containing all the payment details. see below.
|
28
|
+
#
|
29
|
+
# payment_provider :string
|
30
|
+
# payment_card :string
|
31
|
+
#
|
32
|
+
# tax_rate :decimal, precision: 6, scale: 3
|
33
|
+
#
|
34
|
+
# subtotal :integer
|
35
|
+
# tax :integer
|
36
|
+
# total :integer
|
37
|
+
#
|
38
|
+
# timestamps
|
39
|
+
# end
|
40
|
+
|
41
|
+
accepts_nested_attributes_for :order_items, allow_destroy: false, reject_if: :all_blank
|
42
|
+
accepts_nested_attributes_for :user, allow_destroy: false, update_only: true
|
43
|
+
|
44
|
+
before_validation { assign_totals! }
|
45
|
+
before_save { assign_totals! unless self[:total].present? } # Incase we save!(validate: false)
|
14
46
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
47
|
+
unless EffectiveOrders.skip_user_validation
|
48
|
+
validates :user_id, presence: true, unless: Proc.new { |order| order.skip_buyer_validations? }
|
49
|
+
validates :user, associated: true, unless: Proc.new { |order| order.skip_buyer_validations? }
|
50
|
+
end
|
19
51
|
|
20
|
-
|
52
|
+
if EffectiveOrders.collect_note_required
|
53
|
+
validates :note, presence: true, unless: Proc.new { |order| order.skip_buyer_validations? }
|
21
54
|
end
|
22
55
|
|
23
|
-
|
24
|
-
|
56
|
+
validates :tax_rate, presence: { message: "can't be determined based on billing address" }, unless: Proc.new { |order| order.skip_buyer_validations? }
|
57
|
+
validates :tax, presence: true, unless: Proc.new { |order| order.skip_buyer_validations? }
|
25
58
|
|
26
|
-
|
27
|
-
|
28
|
-
|
59
|
+
if EffectiveOrders.require_billing_address # An admin creating a new pending order should not be required to have addresses
|
60
|
+
validates :billing_address, presence: true, unless: Proc.new { |order| order.new_record? && order.pending? }
|
61
|
+
end
|
62
|
+
|
63
|
+
if EffectiveOrders.require_shipping_address # An admin creating a new pending order should not be required to have addresses
|
64
|
+
validates :shipping_address, presence: true, unless: Proc.new { |order| order.new_record? && order.pending? }
|
29
65
|
end
|
30
66
|
|
31
67
|
if ((minimum_charge = EffectiveOrders.minimum_charge.to_i) rescue nil).present?
|
32
68
|
if EffectiveOrders.allow_free_orders
|
33
|
-
|
69
|
+
validates :total, numericality: {
|
70
|
+
greater_than_or_equal_to: minimum_charge,
|
71
|
+
message: "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
|
72
|
+
}, unless: Proc.new { |order| order.total == 0 }
|
34
73
|
else
|
35
|
-
|
74
|
+
validates :total, numericality: {
|
75
|
+
greater_than_or_equal_to: minimum_charge,
|
76
|
+
message: "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
|
77
|
+
}
|
36
78
|
end
|
37
79
|
end
|
38
80
|
|
39
|
-
|
40
|
-
validates_associated :order_items
|
81
|
+
validates :purchase_state, inclusion: { in: [nil, EffectiveOrders::PURCHASED, EffectiveOrders::DECLINED, EffectiveOrders::PENDING] }
|
41
82
|
|
42
|
-
|
83
|
+
validates :subtotal, presence: true
|
84
|
+
validates :total, presence: true
|
43
85
|
|
44
|
-
|
86
|
+
validates :order_items, presence: { message: 'No items are present. Please add one or more item to your cart.' }
|
87
|
+
validates :order_items, associated: true
|
45
88
|
|
46
|
-
|
47
|
-
|
48
|
-
|
89
|
+
with_options if: Proc.new { |order| order.purchased? } do |order|
|
90
|
+
order.validates :purchased_at, presence: true
|
91
|
+
order.validates :payment, presence: true
|
92
|
+
|
93
|
+
order.validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.payment_providers }
|
94
|
+
order.validates :payment_card, presence: true
|
95
|
+
end
|
49
96
|
|
50
|
-
|
51
|
-
|
97
|
+
serialize :payment, Hash
|
98
|
+
|
99
|
+
default_scope -> { includes(:user).includes(order_items: :purchasable).order(created_at: :desc) }
|
100
|
+
|
101
|
+
scope :purchased, -> { where(purchase_state: EffectiveOrders::PURCHASED) }
|
102
|
+
scope :purchased_by, lambda { |user| purchased.where(user_id: user.try(:id)) }
|
103
|
+
scope :declined, -> { where(purchase_state: EffectiveOrders::DECLINED) }
|
104
|
+
scope :pending, -> { where(purchase_state: EffectiveOrders::PENDING) }
|
105
|
+
scope :for_users, -> (users) { # Expects a Users relation, an Array of ids, or Array of users
|
106
|
+
users = users.kind_of?(::ActiveRecord::Relation) ? users.pluck(:id) : Array(users)
|
107
|
+
where(user_id: (users.first.kind_of?(Integer) ? users : users.map { |user| user.id }))
|
108
|
+
}
|
109
|
+
|
110
|
+
# Effective::Order.new()
|
111
|
+
# Effective::Order.new(Product.first)
|
112
|
+
# Effective::Order.new(Product.all)
|
113
|
+
# Effective::Order.new(Product.first, user: User.first)
|
114
|
+
# Effective::Order.new(Product.first, Product.second, user: User.first)
|
115
|
+
# Effective::Order.new(user: User.first)
|
116
|
+
# Effective::Order.new(current_cart)
|
117
|
+
|
118
|
+
# items can be an Effective::Cart, a single acts_as_purchasable, or an array of acts_as_purchasables
|
119
|
+
def initialize(*items, user: nil, billing_address: nil, shipping_address: nil)
|
52
120
|
super() # Call super with no arguments
|
53
121
|
|
54
122
|
# Set up defaults
|
@@ -56,36 +124,49 @@ module Effective
|
|
56
124
|
self.save_shipping_address = true
|
57
125
|
self.shipping_address_same_as_billing = true
|
58
126
|
|
59
|
-
self.user = (items.
|
60
|
-
|
127
|
+
self.user = user || (items.first.user if items.first.kind_of?(Effective::Cart))
|
128
|
+
|
129
|
+
if billing_address
|
130
|
+
self.billing_address = billing_address
|
131
|
+
self.billing_address.full_name ||= billing_name
|
132
|
+
end
|
133
|
+
|
134
|
+
if shipping_address
|
135
|
+
self.shipping_address = shipping_address
|
136
|
+
self.shipping_address.full_name ||= billing_name
|
137
|
+
end
|
138
|
+
|
139
|
+
add(items) if items.present?
|
61
140
|
end
|
62
141
|
|
63
|
-
|
142
|
+
# add(Product.first) => returns an Effective::OrderItem
|
143
|
+
# add(Product.first, current_cart) => returns an array of Effective::OrderItems
|
144
|
+
def add(*items, quantity: 1)
|
64
145
|
raise 'unable to alter a purchased order' if purchased?
|
65
146
|
raise 'unable to alter a declined order' if declined?
|
66
147
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
raise ArgumentError.new('Effective::Order.add() expects
|
74
|
-
end
|
75
|
-
|
76
|
-
cart_items = purchasables.map do |purchasable|
|
77
|
-
CartItem.new(:quantity => quantity).tap { |cart_item| cart_item.purchasable = purchasable }
|
148
|
+
cart_items = items.flatten.flat_map do |item|
|
149
|
+
if item.kind_of?(Effective::Cart)
|
150
|
+
item.cart_items.to_a
|
151
|
+
elsif item.kind_of?(ActsAsPurchasable)
|
152
|
+
Effective::CartItem.new(quantity: quantity.to_i).tap { |cart_item| cart_item.purchasable = item }
|
153
|
+
else
|
154
|
+
raise ArgumentError.new('Effective::Order.add() expects one or more acts_as_purchasable objects, or an Effective::Cart')
|
78
155
|
end
|
79
156
|
end
|
80
157
|
|
158
|
+
# Make sure to reset stored aggregates
|
159
|
+
self.total = nil
|
160
|
+
self.subtotal = nil
|
161
|
+
self.tax = nil
|
162
|
+
|
81
163
|
retval = cart_items.map do |item|
|
82
164
|
order_items.build(
|
83
|
-
:
|
84
|
-
:
|
85
|
-
:
|
86
|
-
:
|
87
|
-
:
|
88
|
-
:seller_id => (item.purchasable.try(:seller).try(:id) rescue nil)
|
165
|
+
title: item.title,
|
166
|
+
quantity: item.quantity,
|
167
|
+
price: item.price,
|
168
|
+
tax_exempt: item.tax_exempt || false,
|
169
|
+
seller_id: (item.purchasable.try(:seller).try(:id) rescue nil)
|
89
170
|
).tap { |order_item| order_item.purchasable = item.purchasable }
|
90
171
|
end
|
91
172
|
|
@@ -99,11 +180,11 @@ module Effective
|
|
99
180
|
super
|
100
181
|
|
101
182
|
# Copy user addresses into this order if they are present
|
102
|
-
if user.respond_to?(:billing_address) &&
|
183
|
+
if user.respond_to?(:billing_address) && user.billing_address.present?
|
103
184
|
self.billing_address = user.billing_address
|
104
185
|
end
|
105
186
|
|
106
|
-
if user.respond_to?(:shipping_address) &&
|
187
|
+
if user.respond_to?(:shipping_address) && user.shipping_address.present?
|
107
188
|
self.shipping_address = user.shipping_address
|
108
189
|
end
|
109
190
|
|
@@ -117,15 +198,30 @@ module Effective
|
|
117
198
|
end
|
118
199
|
|
119
200
|
# Ensure the Full Name is assigned when an address exists
|
120
|
-
if billing_address.
|
201
|
+
if billing_address.present? && billing_address.full_name.blank?
|
121
202
|
self.billing_address.full_name = billing_name
|
122
203
|
end
|
123
204
|
|
124
|
-
if shipping_address.
|
205
|
+
if shipping_address.present? && shipping_address.full_name.blank?
|
125
206
|
self.shipping_address.full_name = billing_name
|
126
207
|
end
|
127
208
|
end
|
128
209
|
|
210
|
+
# This is called from admin/orders#create
|
211
|
+
# This is intended for use as an admin action only
|
212
|
+
# It skips any address or bad user validations
|
213
|
+
def create_as_pending
|
214
|
+
self.purchase_state = EffectiveOrders::PENDING
|
215
|
+
|
216
|
+
self.skip_buyer_validations = true
|
217
|
+
self.addresses.clear if addresses.any? { |address| address.valid? == false }
|
218
|
+
|
219
|
+
return false unless save
|
220
|
+
|
221
|
+
send_payment_request_to_buyer! if send_payment_request_to_buyer?
|
222
|
+
true
|
223
|
+
end
|
224
|
+
|
129
225
|
# This is used for updating Subscription codes.
|
130
226
|
# We want to update the underlying purchasable object of an OrderItem
|
131
227
|
# Passing the order_item_attributes using rails default acts_as_nested creates a new object instead of updating the temporary one.
|
@@ -144,27 +240,34 @@ module Effective
|
|
144
240
|
order_item.title = order_item.purchasable.title
|
145
241
|
order_item.price = order_item.purchasable.price
|
146
242
|
order_item.tax_exempt = order_item.purchasable.tax_exempt
|
147
|
-
order_item.tax_rate = order_item.purchasable.tax_rate
|
148
243
|
order_item.seller_id = (order_item.purchasable.try(:seller).try(:id) rescue nil)
|
149
244
|
end
|
150
245
|
end
|
151
246
|
end
|
152
247
|
end
|
153
248
|
|
154
|
-
def
|
155
|
-
|
249
|
+
def purchasables
|
250
|
+
order_items.map { |order_item| order_item.purchasable }
|
156
251
|
end
|
157
252
|
|
158
|
-
def
|
159
|
-
|
253
|
+
def tax_rate
|
254
|
+
self[:tax_rate] || get_tax_rate()
|
160
255
|
end
|
161
256
|
|
162
257
|
def tax
|
163
|
-
[
|
258
|
+
self[:tax] || get_tax()
|
259
|
+
end
|
260
|
+
|
261
|
+
def subtotal
|
262
|
+
self[:subtotal] || order_items.map { |oi| oi.subtotal }.sum
|
263
|
+
end
|
264
|
+
|
265
|
+
def total
|
266
|
+
self[:total] || [(subtotal + tax.to_i), 0].max
|
164
267
|
end
|
165
268
|
|
166
269
|
def num_items
|
167
|
-
order_items.map
|
270
|
+
order_items.map { |oi| oi.quantity }.sum
|
168
271
|
end
|
169
272
|
|
170
273
|
def save_billing_address?
|
@@ -175,12 +278,16 @@ module Effective
|
|
175
278
|
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.save_shipping_address)
|
176
279
|
end
|
177
280
|
|
281
|
+
def send_payment_request_to_buyer?
|
282
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.send_payment_request_to_buyer)
|
283
|
+
end
|
284
|
+
|
178
285
|
def shipping_address_same_as_billing?
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
286
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.shipping_address_same_as_billing)
|
287
|
+
end
|
288
|
+
|
289
|
+
def skip_buyer_validations?
|
290
|
+
::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.skip_buyer_validations)
|
184
291
|
end
|
185
292
|
|
186
293
|
def billing_name
|
@@ -193,103 +300,74 @@ module Effective
|
|
193
300
|
name
|
194
301
|
end
|
195
302
|
|
196
|
-
#
|
197
|
-
|
198
|
-
|
199
|
-
|
303
|
+
# Effective::Order.new(Product.first, user: User.first).purchase!(details: 'manual purchase')
|
304
|
+
# order.purchase!(details: {key: value})
|
305
|
+
def purchase!(details: 'none', provider: 'admin', card: 'none', validate: true, email: true, skip_buyer_validations: false)
|
200
306
|
return false if purchased?
|
201
|
-
raise EffectiveOrders::AlreadyDeclinedException.new('order already declined') if (declined? && opts[:validate])
|
202
307
|
|
203
308
|
success = false
|
204
309
|
|
205
|
-
Order.transaction do
|
310
|
+
Effective::Order.transaction do
|
206
311
|
begin
|
207
312
|
self.purchase_state = EffectiveOrders::PURCHASED
|
208
313
|
self.purchased_at ||= Time.zone.now
|
209
|
-
self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s}
|
210
314
|
|
211
|
-
|
315
|
+
self.payment = details.kind_of?(Hash) ? details : { details: details.to_s }
|
316
|
+
self.payment_provider = provider.to_s
|
317
|
+
self.payment_card = card.to_s.presence || 'none'
|
318
|
+
|
319
|
+
save!(validate: validate)
|
212
320
|
|
213
321
|
order_items.each { |item| (item.purchasable.purchased!(self, item) rescue nil) }
|
214
322
|
|
215
323
|
success = true
|
216
324
|
rescue => e
|
217
|
-
raise ActiveRecord::Rollback
|
325
|
+
raise ::ActiveRecord::Rollback
|
218
326
|
end
|
219
327
|
end
|
220
328
|
|
221
|
-
send_order_receipts! if success &&
|
329
|
+
send_order_receipts! if (success && email)
|
222
330
|
|
331
|
+
raise "Failed to purchase Effective::Order: #{self.errors.full_messages.to_sentence}" unless success
|
223
332
|
success
|
224
333
|
end
|
225
334
|
|
226
|
-
def decline!(
|
335
|
+
def decline!(details: 'none', provider: 'admin', card: 'none', validate: true)
|
227
336
|
return false if declined?
|
228
337
|
|
229
338
|
raise EffectiveOrders::AlreadyPurchasedException.new('order already purchased') if purchased?
|
230
339
|
|
231
|
-
|
232
|
-
self.purchase_state = EffectiveOrders::DECLINED
|
233
|
-
self.payment = payment_details.kind_of?(Hash) ? payment_details : {:details => (payment_details || 'none').to_s}
|
234
|
-
|
235
|
-
order_items.each { |item| (item.purchasable.declined!(self, item) rescue nil) }
|
340
|
+
success = false
|
236
341
|
|
237
|
-
|
238
|
-
|
239
|
-
|
342
|
+
Effective::Order.transaction do
|
343
|
+
begin
|
344
|
+
self.purchase_state = EffectiveOrders::DECLINED
|
345
|
+
self.purchased_at = nil
|
240
346
|
|
241
|
-
|
242
|
-
|
347
|
+
self.payment = details.kind_of?(Hash) ? details : { details: details.to_s }
|
348
|
+
self.payment_provider = provider.to_s
|
349
|
+
self.payment_card = card.to_s.presence || 'none'
|
243
350
|
|
244
|
-
|
245
|
-
'Stripe Connect'
|
246
|
-
elsif purchased?(:stripe)
|
247
|
-
'Stripe'
|
248
|
-
elsif purchased?(:moneris)
|
249
|
-
'Moneris'
|
250
|
-
elsif purchased?(:paypal)
|
251
|
-
'PayPal'
|
252
|
-
else
|
253
|
-
'Online'
|
254
|
-
end
|
255
|
-
end
|
256
|
-
alias_method :payment_method, :purchase_method
|
351
|
+
save!(validate: validate)
|
257
352
|
|
258
|
-
|
259
|
-
return 'None' unless purchased?
|
353
|
+
order_items.each { |item| (item.purchasable.declined!(self, item) rescue nil) }
|
260
354
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
elsif purchased?(:moneris)
|
266
|
-
payment[:card] || payment['card'] || 'Unknown'
|
267
|
-
elsif purchased?(:paypal)
|
268
|
-
payment[:payment_type] || payment['payment_type'] || 'Unknown'
|
269
|
-
else
|
270
|
-
'Online'
|
355
|
+
success = true
|
356
|
+
rescue => e
|
357
|
+
raise ::ActiveRecord::Rollback
|
358
|
+
end
|
271
359
|
end
|
360
|
+
|
361
|
+
raise "Failed to decline! Effective::Order: #{self.errors.full_messages.to_sentence}" unless success
|
362
|
+
success
|
272
363
|
end
|
273
|
-
alias_method :payment_card_type, :purchase_card_type
|
274
364
|
|
275
365
|
def purchased?(provider = nil)
|
276
366
|
return false if (purchase_state != EffectiveOrders::PURCHASED)
|
277
|
-
return true if provider
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
charge = (payment[:charge] || payment['charge'] || {})
|
282
|
-
charge['id'] && charge['customer'] && charge['application_fee'].present?
|
283
|
-
when :stripe
|
284
|
-
charge = (payment[:charge] || payment['charge'] || {})
|
285
|
-
charge['id'] && charge['customer']
|
286
|
-
when :moneris
|
287
|
-
(payment[:response_code] || payment['response_code']) &&
|
288
|
-
(payment[:transactionKey] || payment['transactionKey'])
|
289
|
-
when :paypal
|
290
|
-
(payment[:payer_email] || payment['payer_email'])
|
291
|
-
else
|
292
|
-
raise "Unknown provider #{provider} passed to Effective::Order.purchased?"
|
367
|
+
return true if provider.nil? || payment_provider == provider.to_s
|
368
|
+
|
369
|
+
unless EffectiveOrder.payment_providers.include?(provider.to_s)
|
370
|
+
raise "Unknown provider #{provider}. Known providers are #{EffectiveOrders.payment_providers}"
|
293
371
|
end
|
294
372
|
end
|
295
373
|
|
@@ -297,41 +375,74 @@ module Effective
|
|
297
375
|
purchase_state == EffectiveOrders::DECLINED
|
298
376
|
end
|
299
377
|
|
378
|
+
def pending?
|
379
|
+
purchase_state == EffectiveOrders::PENDING
|
380
|
+
end
|
381
|
+
|
300
382
|
def send_order_receipts!
|
301
|
-
send_order_receipt_to_admin!
|
302
|
-
send_order_receipt_to_buyer!
|
303
|
-
send_order_receipt_to_seller!
|
383
|
+
send_order_receipt_to_admin! if EffectiveOrders.mailer[:send_order_receipt_to_admin]
|
384
|
+
send_order_receipt_to_buyer! if EffectiveOrders.mailer[:send_order_receipt_to_buyer]
|
385
|
+
send_order_receipt_to_seller! if EffectiveOrders.mailer[:send_order_receipt_to_seller]
|
304
386
|
end
|
305
387
|
|
306
388
|
def send_order_receipt_to_admin!
|
307
|
-
|
308
|
-
send_email(:order_receipt_to_admin, self)
|
389
|
+
send_email(:order_receipt_to_admin, to_param) if purchased?
|
309
390
|
end
|
310
391
|
|
311
392
|
def send_order_receipt_to_buyer!
|
312
|
-
|
313
|
-
|
314
|
-
send_email(:order_receipt_to_buyer, self)
|
393
|
+
send_email(:order_receipt_to_buyer, to_param) if purchased?
|
315
394
|
end
|
316
395
|
|
317
396
|
def send_order_receipt_to_seller!
|
318
|
-
return false unless purchased?(:stripe_connect)
|
397
|
+
return false unless (EffectiveOrders.stripe_connect_enabled && purchased?(:stripe_connect))
|
319
398
|
|
320
399
|
order_items.group_by(&:seller).each do |seller, order_items|
|
321
|
-
send_email(:order_receipt_to_seller,
|
400
|
+
send_email(:order_receipt_to_seller, to_param, seller, order_items)
|
322
401
|
end
|
323
402
|
end
|
324
403
|
|
325
|
-
|
404
|
+
def send_payment_request_to_buyer!
|
405
|
+
send_email(:payment_request_to_buyer, to_param) if !purchased?
|
406
|
+
end
|
407
|
+
|
408
|
+
def send_pending_order_invoice_to_buyer!
|
409
|
+
send_email(:pending_order_invoice_to_buyer, to_param) if !purchased?
|
410
|
+
end
|
411
|
+
|
412
|
+
protected
|
413
|
+
|
414
|
+
def get_tax_rate
|
415
|
+
self.instance_exec(self, &EffectiveOrders.order_tax_rate_method).tap do |rate|
|
416
|
+
rate = rate.to_f
|
417
|
+
if (rate > 100.0 || (rate < 0.25 && rate > 0.0000))
|
418
|
+
raise "expected EffectiveOrders.order_tax_rate_method to return a value between 100.0 (100%) and 0.25 (0.25%) or 0 or nil. Received #{rate}. Please return 5.25 for 5.25% tax."
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def get_tax
|
424
|
+
return nil unless tax_rate.present?
|
425
|
+
amount = order_items.reject { |oi| oi.tax_exempt? }.map { |oi| (oi.subtotal * (tax_rate / 100.0)).round(0).to_i }.sum
|
426
|
+
[amount, 0].max
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
|
431
|
+
def assign_totals!
|
432
|
+
self.subtotal = order_items.map { |oi| oi.subtotal }.sum
|
433
|
+
self.tax_rate = get_tax_rate()
|
434
|
+
self.tax = get_tax()
|
435
|
+
self.total = [subtotal + (tax || 0), 0].max
|
436
|
+
end
|
326
437
|
|
327
438
|
def send_email(email, *mailer_args)
|
328
439
|
begin
|
329
440
|
if EffectiveOrders.mailer[:delayed_job_deliver] && EffectiveOrders.mailer[:deliver_method] == :deliver_later
|
330
|
-
|
441
|
+
Effective::OrdersMailer.delay.public_send(email, *mailer_args)
|
331
442
|
elsif EffectiveOrders.mailer[:deliver_method].present?
|
332
|
-
|
443
|
+
Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
|
333
444
|
else
|
334
|
-
|
445
|
+
Effective::OrdersMailer.public_send(email, *mailer_args).deliver_now
|
335
446
|
end
|
336
447
|
rescue => e
|
337
448
|
raise e unless Rails.env.production?
|