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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +150 -60
  4. data/Rakefile +0 -3
  5. data/active_admin/effective_orders.rb +14 -9
  6. data/app/assets/javascripts/effective_orders.js +2 -0
  7. data/app/assets/javascripts/effective_orders/{stripe_charges.js.coffee → providers/stripe_charges.js.coffee} +1 -1
  8. data/app/assets/javascripts/effective_orders/{stripe_subscriptions.js.coffee → providers/stripe_subscriptions.js.coffee} +1 -1
  9. data/app/assets/stylesheets/effective_orders/_order.scss +9 -3
  10. data/app/controllers/admin/orders_controller.rb +87 -3
  11. data/app/controllers/concerns/acts_as_active_admin_controller.rb +2 -1
  12. data/app/controllers/effective/carts_controller.rb +4 -2
  13. data/app/controllers/effective/orders_controller.rb +101 -60
  14. data/app/controllers/effective/providers/app_checkout.rb +10 -2
  15. data/app/controllers/effective/providers/ccbill.rb +34 -0
  16. data/app/controllers/effective/providers/cheque.rb +30 -0
  17. data/app/controllers/effective/providers/moneris.rb +7 -7
  18. data/app/controllers/effective/providers/paypal.rb +7 -5
  19. data/app/controllers/effective/providers/pretend.rb +22 -0
  20. data/app/controllers/effective/providers/stripe.rb +26 -24
  21. data/app/controllers/effective/webhooks_controller.rb +1 -1
  22. data/app/helpers/effective_carts_helper.rb +59 -29
  23. data/app/helpers/effective_ccbill_helper.rb +25 -0
  24. data/app/helpers/effective_orders_helper.rb +50 -41
  25. data/app/helpers/effective_paypal_helper.rb +11 -11
  26. data/app/mailers/effective/orders_mailer.rb +95 -20
  27. data/app/models/concerns/acts_as_purchasable.rb +14 -22
  28. data/app/models/effective/cart.rb +9 -15
  29. data/app/models/effective/cart_item.rb +15 -13
  30. data/app/models/effective/customer.rb +9 -9
  31. data/app/models/effective/datatables/order_items.rb +14 -8
  32. data/app/models/effective/datatables/orders.rb +56 -69
  33. data/app/models/effective/order.rb +250 -139
  34. data/app/models/effective/order_item.rb +23 -16
  35. data/app/models/effective/product.rb +23 -0
  36. data/app/models/effective/providers/ccbill_postback.rb +85 -0
  37. data/app/models/effective/{stripe_charge.rb → providers/stripe_charge.rb} +1 -1
  38. data/app/models/effective/subscription.rb +16 -12
  39. data/app/models/effective/tax_rate_calculator.rb +45 -0
  40. data/app/views/admin/customers/index.html.haml +2 -5
  41. data/app/views/admin/order_items/index.html.haml +2 -5
  42. data/app/views/admin/orders/_actions.html.haml +2 -0
  43. data/app/views/admin/orders/_form.html.haml +28 -0
  44. data/app/views/admin/orders/_order_item_fields.html.haml +9 -0
  45. data/app/views/admin/orders/index.html.haml +9 -5
  46. data/app/views/admin/orders/new.html.haml +3 -0
  47. data/app/views/effective/carts/_cart.html.haml +3 -12
  48. data/app/views/effective/carts/_cart_actions.html.haml +4 -0
  49. data/app/views/effective/carts/show.html.haml +10 -12
  50. data/app/views/effective/orders/_checkout_step1.html.haml +46 -0
  51. data/app/views/effective/orders/_checkout_step2.html.haml +33 -0
  52. data/app/views/effective/orders/_order.html.haml +2 -0
  53. data/app/views/effective/orders/_order_actions.html.haml +10 -5
  54. data/app/views/effective/orders/_order_footer.html.haml +1 -0
  55. data/app/views/effective/orders/_order_items.html.haml +11 -9
  56. data/app/views/effective/orders/_order_note.html.haml +8 -0
  57. data/app/views/effective/orders/_order_note_fields.html.haml +5 -0
  58. data/app/views/effective/orders/_order_shipping.html.haml +4 -4
  59. data/app/views/effective/orders/_order_user_fields.html.haml +1 -0
  60. data/app/views/effective/orders/_orders_table.html.haml +27 -0
  61. data/app/views/effective/orders/ccbill/_form.html.haml +24 -0
  62. data/app/views/effective/orders/checkout_step1.html.haml +3 -0
  63. data/app/views/effective/orders/checkout_step2.html.haml +3 -0
  64. data/app/views/effective/orders/cheque/_form.html.haml +2 -0
  65. data/app/views/effective/orders/cheque/pay_by_cheque.html.haml +9 -0
  66. data/app/views/effective/orders/declined.html.haml +4 -2
  67. data/app/views/effective/orders/my_purchases.html.haml +1 -1
  68. data/app/views/effective/orders/my_sales.html.haml +1 -1
  69. data/app/views/effective/orders/purchased.html.haml +3 -2
  70. data/app/views/effective/orders/show.html.haml +1 -1
  71. data/app/views/effective/orders/stripe/_form.html.haml +5 -5
  72. data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
  73. data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
  74. data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
  75. data/app/views/layouts/effective_orders_mailer_layout.html.haml +7 -7
  76. data/config/routes.rb +39 -24
  77. data/db/migrate/01_create_effective_orders.rb.erb +20 -1
  78. data/db/upgrade/03_upgrade_effective_orders_from1x.rb.erb +95 -0
  79. data/lib/effective_orders.rb +67 -9
  80. data/lib/effective_orders/app_checkout_service.rb +1 -2
  81. data/lib/effective_orders/engine.rb +46 -14
  82. data/lib/effective_orders/version.rb +1 -1
  83. data/lib/generators/effective_orders/install_generator.rb +1 -0
  84. data/lib/generators/effective_orders/upgrade_from03x_generator.rb +1 -0
  85. data/lib/generators/effective_orders/upgrade_from1x_generator.rb +31 -0
  86. data/lib/generators/templates/effective_orders.rb +131 -66
  87. data/lib/generators/templates/effective_orders_mailer_preview.rb +28 -13
  88. data/spec/controllers/admin/orders_controller_spec.rb +242 -0
  89. data/spec/controllers/ccbill_orders_controller_spec.rb +103 -0
  90. data/spec/controllers/moneris_orders_controller_spec.rb +23 -23
  91. data/spec/controllers/orders_controller_spec.rb +167 -79
  92. data/spec/controllers/stripe_orders_controller_spec.rb +7 -7
  93. data/spec/dummy/app/models/product.rb +0 -8
  94. data/spec/dummy/app/models/product_with_float_price.rb +0 -8
  95. data/spec/dummy/app/models/user.rb +2 -19
  96. data/spec/dummy/config/application.rb +2 -1
  97. data/spec/dummy/config/environments/test.rb +1 -0
  98. data/spec/dummy/config/initializers/effective_orders.rb +109 -64
  99. data/spec/dummy/db/schema.rb +15 -2
  100. data/spec/dummy/db/test.sqlite3 +0 -0
  101. data/spec/dummy/log/test.log +0 -258
  102. data/spec/models/acts_as_purchasable_spec.rb +8 -6
  103. data/spec/models/factories_spec.rb +7 -1
  104. data/spec/models/order_item_spec.rb +1 -1
  105. data/spec/models/order_spec.rb +165 -46
  106. data/spec/models/stripe_charge_spec.rb +5 -5
  107. data/spec/spec_helper.rb +2 -0
  108. data/spec/support/factories.rb +49 -33
  109. metadata +47 -64
  110. data/app/views/effective/orders/_checkout_step_1.html.haml +0 -43
  111. data/app/views/effective/orders/_checkout_step_2.html.haml +0 -25
  112. data/app/views/effective/orders/_my_purchases.html.haml +0 -17
  113. data/app/views/effective/orders/checkout.html.haml +0 -3
  114. data/app/views/effective/orders/new.html.haml +0 -4
  115. 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
- quantity :integer, :validates => [:presence]
10
- timestamps
11
- end
12
-
13
- validates_presence_of :purchasable
8
+ # structure do
9
+ # quantity :integer
10
+ #
11
+ # timestamps
12
+ # end
14
13
 
15
- delegate :title, :tax_exempt, :tax_rate, :to => :purchasable
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 subtotal
29
- price * quantity
28
+ def title
29
+ purchasable.try(:title) || 'New Cart Item'
30
30
  end
31
31
 
32
- def tax
33
- tax_exempt ? 0 : (subtotal * tax_rate).round(0).to_i
32
+ def tax_exempt
33
+ purchasable.try(:tax_exempt) || false
34
34
  end
35
35
 
36
- def total
37
- subtotal + tax
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
- 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
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
- timestamps
16
- end
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, :type => :datetime, :column => 'orders.purchased_at') do |order_item|
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, :visible => false
12
+ table_column :id, visible: false
13
13
 
14
- table_column(:order, :type => :obfuscated_id, :sortable => false) do |order_item|
15
- obfuscated_id = Effective::Order.obfuscate(order_item[:order_id])
16
- link_to(obfuscated_id, (datatables_admin_path? ? effective_orders.admin_order_path(obfuscated_id) : effective_orders.order_path(obfuscated_id)))
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, :visible => false
42
- table_column :updated_at, :visible => false
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(:order => :user)
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 :purchased_at, :desc
6
+ default_order :created_at, :desc
7
7
 
8
8
  table_column :purchased_at
9
- table_column :id
10
9
 
11
- table_column :email, column: 'users.email', label: 'Buyer Email', if: proc { attributes[:user_id].blank? } do |order|
12
- link_to order[:email], (edit_admin_user_path(order.user_id) rescue admin_user_path(order.user_id) rescue '#')
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 EffectiveOrders.require_billing_address
16
- table_column :buyer_name, sortable: false, label: 'Buyer Name', if: proc { attributes[:user_id].blank? } do |order|
17
- (order[:buyer_name] || '').split('!!SEP!!').find(&:present?)
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
- table_column :purchase_state, filter: { type: :select, values: [%w(abandoned abandoned), [EffectiveOrders::PURCHASED, EffectiveOrders::PURCHASED], [EffectiveOrders::DECLINED, EffectiveOrders::DECLINED]], selected: EffectiveOrders::PURCHASED } do |order|
22
- order.purchase_state || 'abandoned'
23
- end
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
- table_column :items, label: 'Order Items', sortable: false, column: query_items_list do |order|
26
- content_tag(:ul) do
27
- (order[:items] || '').split('!!SEP!!').map { |oi| content_tag(:li, oi.to_s.html_safe) }.join.html_safe
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
- table_column(:total) { |order| price_to_currency(order[:total].to_i) }
32
- table_column :payment_method
32
+ if EffectiveOrders.require_billing_address
33
+ table_column :billing_address
34
+ end
33
35
 
34
- table_column :created_at, :visible => false
35
- table_column :updated_at, :visible => false
36
+ if EffectiveOrders.require_shipping_address
37
+ table_column :shipping_address
38
+ end
36
39
 
37
- table_column :actions, sortable: false, filter: false do |order|
38
- link_to('View', (datatables_admin_path? ? effective_orders.admin_order_path(order) : effective_orders.order_path(order)))
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
- def collection
43
- collection = Effective::Order.unscoped
44
- .joins(:user, :order_items)
45
- .group('users.email, orders.id')
46
- .select('orders.*, users.email AS email')
47
- .select("#{query_total} AS total")
48
- .select("#{query_items_list} AS items")
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
- attributes[:user_id].present? ? collection.where(user_id: attributes[:user_id]) : collection
61
- end
53
+ table_column :total, as: :price
62
54
 
63
- def query_subtotal
64
- 'SUM(order_items.price * order_items.quantity)'
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
- def query_tax
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
- def query_total
72
- 'SUM((order_items.price * order_items.quantity) + (CASE order_items.tax_exempt WHEN true THEN 0 ELSE ((order_items.price * order_items.quantity) * order_items.tax_rate) END))'
73
- end
60
+ table_column :created_at, visible: false
61
+ table_column :updated_at, visible: false
74
62
 
75
- def query_items_list
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 query_payment_method
80
- "COALESCE(SUBSTRING(payment FROM 'card: (\\w{1,2})\\n'), SUBSTRING(payment FROM 'action: (\\w+)_postback\\n'))"
81
- end
66
+ def collection
67
+ collection = Effective::Order.unscoped
68
+ .joins(:user)
69
+ .includes(:addresses)
70
+ .includes(:user)
71
+ .includes(:order_items)
82
72
 
83
- def search_column(collection, table_column, search_term)
84
- case table_column[:name]
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 :format => '###-####-###'
6
+ acts_as_obfuscated format: '###-####-###'
7
7
  end
8
8
 
9
- acts_as_addressable :billing => {:singular => true, :presence => EffectiveOrders.require_billing_address, :use_full_name => EffectiveOrders.use_address_full_name}, :shipping => {:singular => true, :presence => EffectiveOrders.require_shipping_address, :use_full_name => EffectiveOrders.use_address_full_name}
10
- attr_accessor :save_billing_address, :save_shipping_address, :shipping_address_same_as_billing # save these addresses to the user if selected
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
- belongs_to :user # This is the user who purchased the order
13
- has_many :order_items, :inverse_of => :order
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
- structure do
16
- payment :text # serialized hash, see below
17
- purchase_state :string, :validates => [:inclusion => {:in => [nil, EffectiveOrders::PURCHASED, EffectiveOrders::DECLINED]}]
18
- purchased_at :datetime, :validates => [:presence => {:if => Proc.new { |order| order.purchase_state == EffectiveOrders::PURCHASED}}]
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
- timestamps
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
- accepts_nested_attributes_for :order_items, :allow_destroy => false, :reject_if => :all_blank
24
- accepts_nested_attributes_for :user, :allow_destroy => false, :update_only => true
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
- unless EffectiveOrders.skip_user_validation
27
- validates_presence_of :user_id
28
- validates_associated :user
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
- validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :unless => Proc.new { |order| order.total == 0 }, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
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
- validates_numericality_of :total, :greater_than_or_equal_to => minimum_charge, :message => "A minimum order of #{EffectiveOrders.minimum_charge} is required. Please add additional items to your cart."
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
- validates_presence_of :order_items, :message => 'No items are present. Please add one or more item to your cart.'
40
- validates_associated :order_items
81
+ validates :purchase_state, inclusion: { in: [nil, EffectiveOrders::PURCHASED, EffectiveOrders::DECLINED, EffectiveOrders::PENDING] }
41
82
 
42
- serialize :payment, Hash
83
+ validates :subtotal, presence: true
84
+ validates :total, presence: true
43
85
 
44
- default_scope -> { includes(:user).includes(:order_items => :purchasable).order('created_at DESC') }
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
- scope :purchased, -> { where(:purchase_state => EffectiveOrders::PURCHASED) }
47
- scope :purchased_by, lambda { |user| purchased.where(:user_id => user.try(:id)) }
48
- scope :declined, -> { where(:purchase_state => EffectiveOrders::DECLINED) }
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
- # Can be an Effective::Cart, a single acts_as_purchasable, or an array of acts_as_purchasables
51
- def initialize(items = {}, user = nil)
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.delete(:user) if items.kind_of?(Hash)) || user
60
- add_to_order(items) if items.present?
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
- def add(item, quantity = 1)
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
- if item.kind_of?(Effective::Cart)
68
- cart_items = item.cart_items
69
- else
70
- purchasables = [item].flatten
71
-
72
- if purchasables.any? { |p| !p.respond_to?(:is_effectively_purchasable?) }
73
- raise ArgumentError.new('Effective::Order.add() expects a single acts_as_purchasable item, or an array of acts_as_purchasable items')
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
- :title => item.title,
84
- :quantity => item.quantity,
85
- :price => item.price,
86
- :tax_exempt => item.tax_exempt,
87
- :tax_rate => item.tax_rate,
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) && !user.billing_address.nil?
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) && !user.shipping_address.nil?
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.nil? == false && billing_address.full_name.blank?
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.nil? == false && shipping_address.full_name.blank?
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 total
155
- [order_items.map(&:total).sum, 0].max
249
+ def purchasables
250
+ order_items.map { |order_item| order_item.purchasable }
156
251
  end
157
252
 
158
- def subtotal
159
- order_items.map(&:subtotal).sum
253
+ def tax_rate
254
+ self[:tax_rate] || get_tax_rate()
160
255
  end
161
256
 
162
257
  def tax
163
- [order_items.map(&:tax).sum, 0].max
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(&:quantity).sum
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
- if self.shipping_address_same_as_billing.nil?
180
- true # Default value
181
- else
182
- ::ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(self.shipping_address_same_as_billing)
183
- end
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
- # :validate => false, :email => false
197
- def purchase!(payment_details = nil, opts = {})
198
- opts = {validate: true, email: true}.merge(opts)
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
- save!(validate: opts[:validate])
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 && opts[:email]
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!(payment_details = nil)
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
- Order.transaction do
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
- save!
238
- end
239
- end
342
+ Effective::Order.transaction do
343
+ begin
344
+ self.purchase_state = EffectiveOrders::DECLINED
345
+ self.purchased_at = nil
240
346
 
241
- def purchase_method
242
- return 'None' unless purchased?
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
- if purchased?(:stripe_connect)
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
- def purchase_card_type
259
- return 'None' unless purchased?
353
+ order_items.each { |item| (item.purchasable.declined!(self, item) rescue nil) }
260
354
 
261
- if purchased?(:stripe_connect)
262
- ((payment[:charge] || payment['charge'])['card']['brand'] rescue 'Unknown')
263
- elsif purchased?(:stripe)
264
- ((payment[:charge] || payment['charge'])['card']['brand'] rescue 'Unknown')
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 == nil || payment.kind_of?(Hash) == false
278
-
279
- case provider.to_sym
280
- when :stripe_connect
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
- return false unless purchased? && EffectiveOrders.mailer[:send_order_receipt_to_admin]
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
- return false unless purchased? && EffectiveOrders.mailer[:send_order_receipt_to_buyer]
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) && EffectiveOrders.mailer[:send_order_receipt_to_seller]
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, self, seller, order_items)
400
+ send_email(:order_receipt_to_seller, to_param, seller, order_items)
322
401
  end
323
402
  end
324
403
 
325
- private
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
- (OrdersMailer.delay.public_send(email, *mailer_args) rescue false)
441
+ Effective::OrdersMailer.delay.public_send(email, *mailer_args)
331
442
  elsif EffectiveOrders.mailer[:deliver_method].present?
332
- (OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method]) rescue false)
443
+ Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
333
444
  else
334
- (OrdersMailer.public_send(email, *mailer_args).deliver_now) rescue false
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?