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.
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?