effective_orders 1.8.1 → 2.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 +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?
|