effective_orders 3.2.3 → 4.0.0beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +11 -99
  4. data/app/assets/stylesheets/effective_orders.scss +1 -0
  5. data/app/assets/stylesheets/effective_orders/_cart.scss +4 -0
  6. data/app/assets/stylesheets/effective_orders/_order.scss +6 -2
  7. data/app/controllers/admin/orders_controller.rb +17 -17
  8. data/app/controllers/effective/carts_controller.rb +4 -2
  9. data/app/controllers/effective/orders_controller.rb +50 -105
  10. data/app/controllers/effective/providers/cheque.rb +1 -1
  11. data/app/controllers/effective/providers/moneris.rb +17 -23
  12. data/app/controllers/effective/providers/stripe.rb +2 -37
  13. data/app/datatables/effective_customers_datatable.rb +1 -5
  14. data/app/datatables/effective_order_items_datatable.rb +10 -10
  15. data/app/datatables/effective_orders_datatable.rb +23 -46
  16. data/app/helpers/effective_carts_helper.rb +5 -25
  17. data/app/helpers/effective_orders_helper.rb +29 -61
  18. data/app/helpers/effective_paypal_helper.rb +10 -8
  19. data/app/helpers/effective_stripe_helper.rb +2 -33
  20. data/app/models/concerns/acts_as_purchasable.rb +26 -37
  21. data/app/models/concerns/acts_as_subscribable.rb +1 -1
  22. data/app/models/effective/cart.rb +4 -5
  23. data/app/models/effective/customer.rb +1 -4
  24. data/app/models/effective/order.rb +132 -163
  25. data/app/models/effective/order_item.rb +2 -21
  26. data/app/models/{effective → validators/effective}/sold_out_validator.rb +0 -0
  27. data/app/views/admin/customers/index.html.haml +1 -4
  28. data/app/views/admin/order_items/index.html.haml +1 -4
  29. data/app/views/admin/orders/_actions.html.haml +18 -9
  30. data/app/views/admin/orders/_form.html.haml +12 -14
  31. data/app/views/admin/orders/_form_note_internal.html.haml +2 -2
  32. data/app/views/admin/orders/_order_actions.html.haml +8 -5
  33. data/app/views/admin/orders/_order_item_fields.html.haml +9 -8
  34. data/app/views/admin/orders/checkout.html.haml +3 -0
  35. data/app/views/admin/orders/edit.html.haml +3 -1
  36. data/app/views/admin/orders/new.html.haml +2 -1
  37. data/app/views/admin/orders/show.html.haml +2 -5
  38. data/app/views/effective/carts/_cart.html.haml +1 -1
  39. data/app/views/effective/carts/_cart_actions.html.haml +3 -4
  40. data/app/views/effective/carts/show.html.haml +4 -4
  41. data/app/views/effective/orders/_checkout_actions.html.haml +3 -0
  42. data/app/views/effective/orders/_checkout_step1.html.haml +20 -27
  43. data/app/views/effective/orders/_checkout_step2.html.haml +26 -30
  44. data/app/views/effective/orders/_order.html.haml +1 -2
  45. data/app/views/effective/orders/_order_actions.html.haml +14 -6
  46. data/app/views/effective/orders/_order_header.html.haml +2 -2
  47. data/app/views/effective/orders/_order_note_fields.html.haml +6 -4
  48. data/app/views/effective/orders/_order_notes.html.haml +15 -0
  49. data/app/views/effective/orders/_order_payment_details.html.haml +1 -1
  50. data/app/views/effective/orders/_order_shipping.html.haml +6 -6
  51. data/app/views/effective/orders/_order_terms_and_conditions_fields.html.haml +5 -6
  52. data/app/views/effective/orders/_order_user_fields.html.haml +5 -11
  53. data/app/views/effective/orders/_orders_table.html.haml +2 -6
  54. data/app/views/effective/orders/index.html.haml +2 -1
  55. data/app/views/effective/orders/mark_as_paid/_form.html.haml +8 -14
  56. data/app/views/effective/orders/moneris/_form.html.haml +43 -4
  57. data/app/views/effective/orders/pretend/_form.html.haml +1 -3
  58. data/app/views/effective/orders/refund/_form.html.haml +8 -12
  59. data/app/views/effective/orders/show.html.haml +2 -2
  60. data/app/views/effective/orders/stripe/_form.html.haml +4 -4
  61. data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +1 -1
  62. data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +1 -1
  63. data/config/effective_orders.rb +116 -182
  64. data/config/routes.rb +16 -27
  65. data/db/migrate/01_create_effective_orders.rb.erb +6 -5
  66. data/lib/effective_orders.rb +72 -76
  67. data/lib/effective_orders/engine.rb +8 -80
  68. data/lib/effective_orders/version.rb +1 -1
  69. data/lib/generators/templates/effective_orders_mailer_preview.rb +4 -4
  70. metadata +26 -38
  71. data/Rakefile +0 -21
  72. data/app/assets/config/effective_orders_manifest.js +0 -3
  73. data/app/assets/images/effective_orders/stripe_connect.png +0 -0
  74. data/app/controllers/effective/providers/app_checkout.rb +0 -38
  75. data/app/controllers/effective/providers/ccbill.rb +0 -34
  76. data/app/controllers/effective/providers/stripe_connect.rb +0 -47
  77. data/app/helpers/effective_ccbill_helper.rb +0 -25
  78. data/app/models/effective/providers/ccbill_postback.rb +0 -85
  79. data/app/models/effective/providers/moneris_charge.rb +0 -115
  80. data/app/views/effective/orders/_order_note.html.haml +0 -5
  81. data/app/views/effective/orders/_order_note_to_buyer.html.haml +0 -9
  82. data/app/views/effective/orders/app_checkout/_form.html.haml +0 -2
  83. data/app/views/effective/orders/ccbill/_form.html.haml +0 -24
  84. data/app/views/effective/orders/my_purchases.html.haml +0 -3
  85. data/app/views/effective/orders/my_sales.html.haml +0 -25
  86. data/app/views/effective/orders_mailer/order_receipt_to_seller.html.haml +0 -25
  87. data/lib/effective_orders/app_checkout_service.rb +0 -26
@@ -2,22 +2,24 @@ module EffectivePaypalHelper
2
2
  class ConfigReader
3
3
  def self.cert_or_key(config)
4
4
  if File.exist?(EffectiveOrders.paypal[config])
5
- File.read(EffectiveOrders.paypal[config]) rescue {}
5
+ (File.read(EffectiveOrders.paypal[config]) rescue nil)
6
6
  else
7
- EffectiveOrders.paypal[config] || {}
7
+ EffectiveOrders.paypal[config]
8
8
  end
9
9
  end
10
10
  end
11
11
 
12
12
  # These're constants so they only get read once, not every order request
13
- PAYPAL_CERT_PEM = ConfigReader.cert_or_key(:paypal_cert)
14
- APP_CERT_PEM = ConfigReader.cert_or_key(:app_cert)
15
- APP_KEY_PEM = ConfigReader.cert_or_key(:app_key)
13
+ if EffectiveOrders.paypal?
14
+ PAYPAL_CERT_PEM = ConfigReader.cert_or_key(:paypal_cert)
15
+ APP_CERT_PEM = ConfigReader.cert_or_key(:app_cert)
16
+ APP_KEY_PEM = ConfigReader.cert_or_key(:app_key)
17
+ end
16
18
 
17
19
  def paypal_encrypted_payload(order)
18
- raise "unable to read EffectiveOrders PayPal paypal_cert #{EffectiveOrders.paypal[:paypal_cert]}" unless PAYPAL_CERT_PEM.present?
19
- raise "unable to read EffectiveOrders PayPal app_cert #{EffectiveOrders.paypal[:app_cert]}" unless APP_CERT_PEM.present?
20
- raise "unable to read EffectiveOrders PayPal app_key #{EffectiveOrders.paypal[:app_key]}" unless APP_KEY_PEM.present?
20
+ raise 'required paypal paypal_cert is missing' unless PAYPAL_CERT_PEM.present?
21
+ raise 'required paypal app_cert is missing' unless APP_CERT_PEM.present?
22
+ raise 'required paypal app_key is missing' unless APP_KEY_PEM.present?
21
23
 
22
24
  values = {
23
25
  business: EffectiveOrders.paypal[:seller_email],
@@ -1,36 +1,5 @@
1
1
  module EffectiveStripeHelper
2
2
 
3
- STRIPE_CONNECT_AUTHORIZE_URL = 'https://connect.stripe.com/oauth/authorize'
4
- STRIPE_CONNECT_TOKEN_URL = 'https://connect.stripe.com/oauth/token'
5
-
6
- def is_stripe_connect_seller?(user)
7
- Effective::Customer.for_buyer(user).stripe_connect_access_token.present?
8
- end
9
-
10
- def link_to_new_stripe_connect_customer(opts = {})
11
- client_id = EffectiveOrders.stripe[:connect_client_id]
12
-
13
- raise 'effective_orders config: stripe.connect_client_id has not been set' unless client_id.present?
14
-
15
- authorize_params = {
16
- response_type: :code,
17
- client_id: client_id, # This is the Application's ClientID
18
- scope: :read_write,
19
- state: {
20
- form_authenticity_token: form_authenticity_token, # Rails standard CSRF
21
- redirect_to: URI.encode(request.original_url) # TODO: Allow this to be customized
22
- }.to_json
23
- }
24
-
25
- # Add the stripe_user parameter if it's possible
26
- stripe_user_params = opts.delete :stripe_user
27
- authorize_params.merge!({stripe_user: stripe_user_params}) if stripe_user_params.is_a?(Hash)
28
-
29
- authorize_url = STRIPE_CONNECT_AUTHORIZE_URL + '?' + authorize_params.to_query
30
- options = {}.merge(opts)
31
- link_to image_tag('/assets/effective_orders/stripe_connect.png'), authorize_url, options
32
- end
33
-
34
3
  def stripe_plan_description(obj)
35
4
  plan = (
36
5
  case obj
@@ -58,7 +27,7 @@ module EffectiveStripeHelper
58
27
  end
59
28
 
60
29
  def stripe_coupon_description(coupon)
61
- amount = coupon.amount_off.present? ? ActionController::Base.helpers.price_to_currency(coupon.amount_off) : "#{coupon.percent_off}%"
30
+ amount = coupon.amount_off.present? ? price_to_currency(coupon.amount_off) : "#{coupon.percent_off}%"
62
31
 
63
32
  if coupon.duration_in_months.present?
64
33
  "#{coupon.id} - #{amount} off for #{coupon.duration_in_months} months"
@@ -68,7 +37,7 @@ module EffectiveStripeHelper
68
37
  end
69
38
 
70
39
  def stripe_site_image_url
71
- return nil unless EffectiveOrders.stripe && (url = EffectiveOrders.stripe[:site_image].to_s).present?
40
+ return nil unless EffectiveOrders.stripe? && (url = EffectiveOrders.stripe[:site_image].to_s).present?
72
41
  url.start_with?('http') ? url : asset_url(url)
73
42
  end
74
43
 
@@ -7,36 +7,40 @@ module ActsAsPurchasable
7
7
  def acts_as_purchasable(*options)
8
8
  @acts_as_purchasable = options || []
9
9
  include ::ActsAsPurchasable
10
- (ActsAsPurchasable.descendants ||= []) << self
10
+
11
+ instance = new()
12
+ raise 'must respond_to price' unless instance.respond_to?(:price)
13
+ raise 'must respond_to purchased_order_id' unless instance.respond_to?(:purchased_order_id)
11
14
  end
12
15
  end
13
16
 
14
17
  included do
18
+ belongs_to :purchased_order, class_name: 'Effective::Order' # Set when purchased
19
+
15
20
  has_many :cart_items, as: :purchasable, dependent: :delete_all, class_name: 'Effective::CartItem'
16
21
 
17
22
  has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
18
23
  has_many :orders, -> { order(:id) }, through: :order_items, class_name: 'Effective::Order'
19
24
 
20
- has_many :purchased_orders, -> { where(purchase_state: EffectiveOrders::PURCHASED).order(:purchased_at) },
25
+ has_many :purchased_orders, -> { where(state: EffectiveOrders::PURCHASED).order(:purchased_at) },
21
26
  through: :order_items, class_name: 'Effective::Order', source: :order
22
27
 
23
- validates_with Effective::SoldOutValidator, on: :create
24
-
25
28
  # Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
26
29
  validates :price, presence: true, numericality: { less_than_or_equal_to: 2000000000, message: 'maximum price is $20,000,000' }
27
-
28
30
  validates :tax_exempt, inclusion: { in: [true, false] }
29
31
 
30
- # These are breaking on the check for quanitty_enabled?. More research is due
31
- validates :quantity_purchased, numericality: { allow_nil: true }, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
32
- validates :quantity_max, numericality: { allow_nil: true }, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
32
+ with_options(if: -> { quantity_enabled? }) do
33
+ validates :quantity_purchased, numericality: { allow_nil: true }
34
+ validates :quantity_max, numericality: { allow_nil: true }
35
+ validates_with Effective::SoldOutValidator, on: :create
36
+ end
33
37
 
34
- scope :purchased, -> { joins(order_items: :order).where(orders: {purchase_state: EffectiveOrders::PURCHASED}).distinct }
35
- scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: {user_id: user.try(:id), purchase_state: EffectiveOrders::PURCHASED}).distinct }
36
- scope :sold, -> { purchased() }
37
- scope :sold_by, lambda { |user| joins(order_items: :order).where(order_items: {seller_id: user.try(:id)}).where(orders: {purchase_state: EffectiveOrders::PURCHASED}).distinct }
38
+ scope :purchased, -> { where.not(purchased_order_id: nil) }
39
+ scope :not_purchased, -> { where(purchased_order_id: nil) }
38
40
 
39
- scope :not_purchased, -> { where('id NOT IN (?)', purchased.pluck(:id).presence || [0]) }
41
+ # scope :purchased, -> { joins(order_items: :order).where(orders: {state: EffectiveOrders::PURCHASED}).distinct }
42
+ # scope :not_purchased, -> { where('id NOT IN (?)', purchased.pluck(:id).presence || [0]) }
43
+ scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: { user_id: user.try(:id), state: EffectiveOrders::PURCHASED }).distinct }
40
44
  scope :not_purchased_by, lambda { |user| where('id NOT IN (?)', purchased_by(user).pluck(:id).presence || [0]) }
41
45
  end
42
46
 
@@ -56,10 +60,6 @@ module ActsAsPurchasable
56
60
 
57
61
  # Regular instance methods
58
62
 
59
- def price
60
- self[:price] || 0
61
- end
62
-
63
63
  # If I have a column type of Integer, and I'm passed a non-Integer, convert it here
64
64
  def price=(value)
65
65
  if value.kind_of?(Integer)
@@ -79,43 +79,32 @@ module ActsAsPurchasable
79
79
  self[:tax_exempt] || false
80
80
  end
81
81
 
82
- def seller
83
- if EffectiveOrders.stripe_connect_enabled
84
- raise 'acts_as_purchasable object requires the seller be defined to return the User selling this item. This is only a requirement when using StripeConnect.'
85
- end
86
- end
87
-
88
- def purchased_order
89
- @purchased_order ||= purchased_orders.first
82
+ def purchased?
83
+ purchased_order_id.present?
90
84
  end
91
85
 
92
- def purchased?
93
- @is_purchased ||= purchased_order.present?
86
+ def purchased_at
87
+ purchased_order.try(:purchased_at)
94
88
  end
95
89
 
96
90
  def purchased_by?(user)
97
91
  purchased_orders.any? { |order| order.user_id == user.id }
98
92
  end
99
93
 
100
- def purchased_at
101
- purchased_order.try(:purchased_at)
94
+ def purchased_download_url # Override me if this is a digital purchase.
95
+ false
102
96
  end
103
97
 
104
98
  def quantity_enabled?
105
- self.respond_to?(:quantity_enabled) ? quantity_enabled == true : false
99
+ false
106
100
  end
107
101
 
108
102
  def quantity_remaining
109
- (quantity_max - quantity_purchased) rescue 0
103
+ quantity_max - quantity_purchased if quantity_enabled?
110
104
  end
111
105
 
112
106
  def sold_out?
113
- quantity_enabled? ? (quantity_remaining == 0) : false
114
- end
115
-
116
- # Override me if this is a digital purchase.
117
- def purchased_download_url
118
- false
107
+ quantity_enabled? ? (quantity_remaining <= 0) : false
119
108
  end
120
109
 
121
110
  end
@@ -57,7 +57,7 @@ module ActsAsSubscribable
57
57
 
58
58
  def trial_expires_at
59
59
  # The rake task send_trial_expiring_emails depends on this beginning_of_day
60
- ((created_at || Time.zone.now) + EffectiveOrders.subscription[:trial_period]).beginning_of_day
60
+ ((created_at || Time.zone.now) + EffectiveOrders.subscriptions[:trial_period]).beginning_of_day
61
61
  end
62
62
 
63
63
  def buyer
@@ -2,9 +2,8 @@ module Effective
2
2
  class Cart < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.carts_table_name.to_s
4
4
 
5
- # Optional. We want non-logged-in users to have carts too.
6
- belongs_to :user, optional: true
7
- has_many :cart_items, -> { includes(:purchasable).order(:updated_at) }, dependent: :delete_all, class_name: 'Effective::CartItem'
5
+ belongs_to :user # Optional. We want non-logged-in users to have carts too.
6
+ has_many :cart_items, -> { order(:id) }, dependent: :delete_all, class_name: 'Effective::CartItem'
8
7
 
9
8
  accepts_nested_attributes_for :cart_items
10
9
 
@@ -43,13 +42,13 @@ module Effective
43
42
  raise EffectiveOrders::SoldOutException, "#{item.title} is sold out"
44
43
  end
45
44
 
46
- existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s if unique.kind_of?(Symbol) || unique.kind_of?(String) || unique == true))
45
+ existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s unless unique.kind_of?(Proc)))
47
46
  save!
48
47
  end
49
48
 
50
49
  def clear!
51
50
  cart_items.each { |cart_item| cart_item.mark_for_destruction }
52
- save!
51
+ cart_items.present? ? save! : true
53
52
  end
54
53
 
55
54
  def remove(item)
@@ -2,7 +2,6 @@ module Effective
2
2
  class Customer < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.customers_table_name.to_s
4
4
 
5
- attr_accessor :stripe_token # This is a convenience method so we have a place to store StripeConnect temporary access tokens
6
5
  attr_accessor :stripe_customer, :stripe_subscription
7
6
 
8
7
  belongs_to :user
@@ -18,8 +17,6 @@ module Effective
18
17
  # stripe_subscription_id :string # Each user gets one stripe subscription object, which can contain many items
19
18
  # status :string
20
19
 
21
- # stripe_connect_access_token :string # If using StripeConnect and this user is a connected Seller
22
- #
23
20
  # timestamps
24
21
 
25
22
  scope :deep, -> { includes(subscriptions: :subscribable) }
@@ -32,7 +29,7 @@ module Effective
32
29
  validates :stripe_customer_id, presence: true
33
30
  validates :status, if: -> { stripe_subscription_id.present? }, inclusion: { in: %w(active past_due) }
34
31
 
35
- def self.for_buyer(user)
32
+ def self.for_user(user)
36
33
  Effective::Customer.where(user: user).first_or_initialize
37
34
  end
38
35
 
@@ -1,3 +1,12 @@
1
+ # When an Order is first initialized it is done in the pending state
2
+ # - when it's in the pending state, none of the buyer entered information is required
3
+ # - when a pending order is rendered:
4
+ # - if the user has a billing address, go to step 2
5
+ # - if the user has no billing address, go to step 1
6
+ #
7
+ # After Step1, we go to the confirmed state
8
+ # After Step2, we are in the purchased or declined state
9
+
1
10
  module Effective
2
11
  class Order < ActiveRecord::Base
3
12
  self.table_name = EffectiveOrders.orders_table_name.to_s
@@ -17,7 +26,6 @@ module Effective
17
26
  attr_accessor :send_payment_request_to_buyer # Set by Admin::Orders#new. Should the payment request email be sent after creating an order?
18
27
  attr_accessor :send_mark_as_paid_email_to_buyer # Set by Admin::Orders#mark_as_paid
19
28
  attr_accessor :skip_buyer_validations # Set by Admin::Orders#create
20
- attr_accessor :skip_minimum_charge_validation # Set by Admin::Orders#show
21
29
 
22
30
  belongs_to :user, validate: false # This is the buyer/user of the order. We validate it below.
23
31
  has_many :order_items, -> { order(:id) }, inverse_of: :order, class_name: 'Effective::OrderItem'
@@ -26,13 +34,15 @@ module Effective
26
34
  accepts_nested_attributes_for :user, allow_destroy: false, update_only: true
27
35
 
28
36
  # Attributes
29
- # purchase_state :string
37
+ # state :string
30
38
  # purchased_at :datetime
31
39
  #
32
- # note_to_buyer :text
33
- # note_internal :text
40
+ # note :text # From buyer to admin
41
+ # note_to_buyer :text # From admin to buyer
42
+ # note_internal :text # Internal admin only
34
43
  #
35
- # payment :text # serialized hash containing all the payment details. see below.
44
+ # billing_name :string # name of buyer
45
+ # payment :text # serialized hash containing all the payment details.
36
46
  #
37
47
  # payment_provider :string
38
48
  # payment_card :string
@@ -47,38 +57,38 @@ module Effective
47
57
 
48
58
  serialize :payment, Hash
49
59
 
50
- before_validation { assign_totals! }
60
+ before_validation { assign_order_totals }
61
+ before_validation { assign_billing_name }
62
+ before_validation { assign_last_address }
51
63
 
52
64
  # Order validations
65
+ validates :user_id, presence: true
53
66
  validates :order_items, presence: { message: 'No items are present. Please add additional items.' }
54
- validates :purchase_state, inclusion: { in: EffectiveOrders::PURCHASE_STATES.keys }
67
+ validates :state, inclusion: { in: EffectiveOrders::STATES.keys }
55
68
  validates :subtotal, presence: true
56
69
 
57
70
  if EffectiveOrders.minimum_charge.to_i > 0
58
71
  validates :total, presence: true, numericality: {
59
72
  greater_than_or_equal_to: EffectiveOrders.minimum_charge.to_i,
60
73
  message: "must be $#{'%0.2f' % (EffectiveOrders.minimum_charge.to_i / 100.0)} or more. Please add additional items."
61
- }, unless: -> {
62
- (total == 0 && EffectiveOrders.allow_free_orders) ||
63
- (total < 0 && EffectiveOrders.allow_refunds && skip_minimum_charge_validation?)
64
- }
74
+ }, unless: -> { (free? && EffectiveOrders.free?) || (refund? && EffectiveOrders.refunds?) }
65
75
  end
66
76
 
67
77
  # User validations -- An admin skips these when working in the admin/ namespace
68
- with_options unless: -> { skip_buyer_validations? } do |order|
78
+ with_options unless: -> { pending? || skip_buyer_validations? } do |order|
69
79
  order.validates :tax_rate, presence: { message: "can't be determined based on billing address" }
70
80
  order.validates :tax, presence: true
71
81
 
72
82
  unless EffectiveOrders.skip_user_validation
73
- order.validates :user_id, presence: true
83
+ order.validates :billing_name, presence: true
74
84
  order.validates :user, associated: true
75
85
  end
76
86
 
77
- if EffectiveOrders.require_billing_address
87
+ if EffectiveOrders.billing_address
78
88
  order.validates :billing_address, presence: true
79
89
  end
80
90
 
81
- if EffectiveOrders.require_shipping_address
91
+ if EffectiveOrders.shipping_address
82
92
  order.validates :shipping_address, presence: true
83
93
  end
84
94
 
@@ -92,25 +102,20 @@ module Effective
92
102
  order.validates :purchased_at, presence: true
93
103
  order.validates :payment, presence: true
94
104
 
95
- order.validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.payment_providers + EffectiveOrders.other_payment_providers }
105
+ order.validates :payment_provider, presence: true, inclusion: { in: EffectiveOrders.payment_providers }
96
106
  order.validates :payment_card, presence: true
97
107
  end
98
108
 
99
- before_save { assign_totals! unless self[:total].present? } # Incase we save!(validate: false)
100
- before_save :save_addresses
101
-
102
- scope :deep, -> { includes(:user).includes(order_items: :purchasable) }
103
- scope :sorted, -> { order(created_at: :desc) }
109
+ scope :deep, -> { includes(:user, order_items: :purchasable) }
110
+ scope :sorted, -> { order(:id) }
104
111
 
105
- scope :purchased, -> { sorted.where(purchase_state: EffectiveOrders::PURCHASED) }
106
- scope :purchased_by, lambda { |user| purchased.where(user_id: user.try(:id)) }
107
- scope :declined, -> { where(purchase_state: EffectiveOrders::DECLINED) }
108
- scope :pending, -> { where(purchase_state: EffectiveOrders::PENDING) }
112
+ scope :purchased, -> { where(state: EffectiveOrders::PURCHASED) }
113
+ scope :purchased_by, lambda { |user| purchased.where(user: user) }
114
+ scope :not_purchased, -> { where.not(state: EffectiveOrders::PURCHASED) }
109
115
 
110
- scope :for_users, -> (users) { # Expects a Users relation, an Array of ids, or Array of users
111
- users = users.kind_of?(::ActiveRecord::Relation) ? users.pluck(:id) : Array(users)
112
- where(user_id: (users.first.kind_of?(Integer) ? users : users.map { |user| user.id }))
113
- }
116
+ scope :pending, -> { where(state: EffectiveOrders::PENDING) }
117
+ scope :confirmed, -> { where(state: EffectiveOrders::CONFIRMED) }
118
+ scope :declined, -> { where(state: EffectiveOrders::DECLINED) }
114
119
 
115
120
  # Effective::Order.new()
116
121
  # Effective::Order.new(Product.first)
@@ -122,27 +127,29 @@ module Effective
122
127
  # Effective::Order.new(items: Product.first, user: User.first, billing_address: Effective::Address.new, shipping_address: Effective::Address.new)
123
128
 
124
129
  def initialize(atts = nil, &block)
125
- super(purchase_state: EffectiveOrders::PENDING) # Initialize with state: PENDING
130
+ super(state: EffectiveOrders::PENDING) # Initialize with state: PENDING
126
131
 
127
132
  return unless atts.present?
128
133
 
129
134
  if atts.kind_of?(Hash)
130
- items = Array(atts.delete(:item)) + Array(atts.delete(:items))
135
+ if (keywords = (atts.keys - [:item, :items, :user, :billing_address, :shipping_address])).present?
136
+ raise ArgumentError.new("unknown keyword: #{keywords.join(' ')}")
137
+ end
138
+
139
+ items = Array(atts[:item]) + Array(atts[:items])
131
140
 
132
- self.user = atts.delete(:user) || (items.first.user if items.first.respond_to?(:user))
141
+ self.user = atts[:user] || (items.first.user if items.first.respond_to?(:user))
133
142
 
134
- if (address = atts.delete(:billing_address)).present?
135
- self.billing_address = address
143
+ if atts.key?(:billing_address)
144
+ self.billing_address = atts[:billing_address]
136
145
  self.billing_address.full_name ||= user.to_s.presence
137
146
  end
138
147
 
139
- if (address = atts.delete(:shipping_address)).present?
140
- self.shipping_address = address
148
+ if atts.key?(:shipping_address)
149
+ self.shipping_address = atts[:shipping_address]
141
150
  self.shipping_address.full_name ||= user.to_s.presence
142
151
  end
143
152
 
144
- atts.each { |key, value| self.send("#{key}=", value) }
145
-
146
153
  add(items) if items.present?
147
154
  else # Attributes are not a Hash
148
155
  self.user = atts.user if atts.respond_to?(:user)
@@ -150,6 +157,7 @@ module Effective
150
157
  end
151
158
  end
152
159
 
160
+ # Items can be an Effective::Cart, an Effective::order, a single acts_as_purchasable, or multiple acts_as_purchasables
153
161
  # add(Product.first) => returns an Effective::OrderItem
154
162
  # add(Product.first, current_cart) => returns an array of Effective::OrderItems
155
163
  def add(*items, quantity: 1)
@@ -192,41 +200,6 @@ module Effective
192
200
  retval.size == 1 ? retval.first : retval
193
201
  end
194
202
 
195
- def user=(user)
196
- super
197
-
198
- return unless user.present?
199
-
200
- # Copy user addresses into this order if they are present
201
- if user.respond_to?(:billing_address) && user.billing_address.present?
202
- self.billing_address = user.billing_address
203
- end
204
-
205
- if user.respond_to?(:shipping_address) && user.shipping_address.present?
206
- self.shipping_address = user.shipping_address
207
- end
208
-
209
- user
210
- end
211
-
212
- # This is called from admin/orders#create
213
- # This is intended for use as an admin action only
214
- # It skips any address or bad user validations
215
- def create_as_pending!
216
- return false unless new_record?
217
-
218
- self.purchase_state = EffectiveOrders::PENDING
219
-
220
- self.skip_buyer_validations = true
221
- self.addresses.clear if addresses.any? { |address| address.valid? == false }
222
-
223
- save!
224
-
225
- send_payment_request_to_buyer! if send_payment_request_to_buyer?
226
-
227
- true
228
- end
229
-
230
203
  def to_s
231
204
  if refund?
232
205
  "Refund ##{to_param}"
@@ -239,18 +212,32 @@ module Effective
239
212
  end
240
213
  end
241
214
 
242
- def free?
243
- total == 0
215
+ def pending?
216
+ state == EffectiveOrders::PENDING
244
217
  end
245
218
 
246
- def refund?
247
- total.to_i < 0
219
+ def confirmed?
220
+ state == EffectiveOrders::CONFIRMED
221
+ end
222
+
223
+ def purchased?(provider = nil)
224
+ return false if (state != EffectiveOrders::PURCHASED)
225
+ return true if provider.nil? || payment_provider == provider.to_s
226
+ false
227
+ end
228
+
229
+ def declined?
230
+ state == EffectiveOrders::DECLINED
248
231
  end
249
232
 
250
233
  def purchasables
251
234
  order_items.map { |order_item| order_item.purchasable }
252
235
  end
253
236
 
237
+ def subtotal
238
+ self[:subtotal] || order_items.map { |oi| oi.subtotal }.sum
239
+ end
240
+
254
241
  def tax_rate
255
242
  self[:tax_rate] || get_tax_rate()
256
243
  end
@@ -259,20 +246,24 @@ module Effective
259
246
  self[:tax] || get_tax()
260
247
  end
261
248
 
262
- def subtotal
263
- self[:subtotal] || order_items.map { |oi| oi.subtotal }.sum
264
- end
265
-
266
249
  def total
267
250
  (self[:total] || (subtotal + tax.to_i)).to_i
268
251
  end
269
252
 
253
+ def free?
254
+ total == 0
255
+ end
256
+
257
+ def refund?
258
+ total.to_i < 0
259
+ end
260
+
270
261
  def num_items
271
262
  order_items.map { |oi| oi.quantity }.sum
272
263
  end
273
264
 
274
265
  def send_payment_request_to_buyer?
275
- truthy?(send_payment_request_to_buyer)
266
+ truthy?(send_payment_request_to_buyer) && !free? && !refund?
276
267
  end
277
268
 
278
269
  def send_mark_as_paid_email_to_buyer?
@@ -283,18 +274,34 @@ module Effective
283
274
  truthy?(skip_buyer_validations)
284
275
  end
285
276
 
286
- def skip_minimum_charge_validation?
287
- truthy?(skip_minimum_charge_validation) || skip_buyer_validations?
277
+ # This is called from admin/orders#create
278
+ # This is intended for use as an admin action only
279
+ # It skips any address or bad user validations
280
+ # It's basically the same as save! on a new order, except it might send the payment request to buyer
281
+ def pending!
282
+ self.state = EffectiveOrders::PENDING
283
+ self.addresses.clear if addresses.any? { |address| address.valid? == false }
284
+ save!
285
+
286
+ send_payment_request_to_buyer! if send_payment_request_to_buyer?
287
+ true
288
288
  end
289
289
 
290
- def billing_name
291
- name ||= billing_address.try(:full_name).presence
292
- name ||= user.try(:full_name).presence
293
- name ||= (user.try(:first_name).to_s + ' ' + user.try(:last_name).to_s).presence
294
- name ||= user.try(:email).presence
295
- name ||= user.to_s
296
- name ||= "User #{user.try(:id)}"
297
- name
290
+ def confirm!
291
+ self.state = EffectiveOrders::CONFIRMED
292
+ save!
293
+ end
294
+
295
+ # This lets us skip to the confirmed workflow for an admin...
296
+ def assign_confirmed_if_valid!
297
+ return unless pending?
298
+
299
+ self.state = EffectiveOrders::CONFIRMED
300
+ return true if valid?
301
+
302
+ self.errors.clear
303
+ self.state = EffectiveOrders::PENDING
304
+ false
298
305
  end
299
306
 
300
307
  # Effective::Order.new(Product.first, user: User.first).purchase!(details: 'manual purchase')
@@ -307,31 +314,24 @@ module Effective
307
314
 
308
315
  Effective::Order.transaction do
309
316
  begin
310
- self.purchase_state = EffectiveOrders::PURCHASED
317
+ self.state = EffectiveOrders::PURCHASED
311
318
  self.purchased_at ||= Time.zone.now
312
319
 
313
- self.payment = (
314
- if details.kind_of?(Hash)
315
- details
316
- elsif details.respond_to?(:to_unsafe_h)
317
- details.to_unsafe_h
318
- else
319
- { details: details.to_s }
320
- end
321
- )
322
-
320
+ self.payment = details.kind_of?(Hash) ? details : { details: details.to_s }
323
321
  self.payment_provider = provider.to_s
324
322
  self.payment_card = card.to_s.presence || 'none'
325
323
 
326
324
  self.skip_buyer_validations = skip_buyer_validations
327
325
 
326
+ assign_purchased_order_to_purchasables
327
+
328
328
  run_purchasable_callbacks(:before_purchase)
329
329
 
330
330
  save!(validate: validate)
331
331
 
332
332
  success = true
333
333
  rescue => e
334
- self.purchase_state = purchase_state_was
334
+ self.state = state_was
335
335
  self.purchased_at = purchased_at_was
336
336
 
337
337
  error = e.message
@@ -339,7 +339,7 @@ module Effective
339
339
  end
340
340
  end
341
341
 
342
- raise "Failed to purchase Effective::Order: #{error || errors.full_messages.to_sentence}" unless success
342
+ raise "Failed to purchase order: #{error || errors.full_messages.to_sentence}" unless success
343
343
 
344
344
  send_order_receipts! if email
345
345
 
@@ -358,7 +358,7 @@ module Effective
358
358
 
359
359
  Effective::Order.transaction do
360
360
  begin
361
- self.purchase_state = EffectiveOrders::DECLINED
361
+ self.state = EffectiveOrders::DECLINED
362
362
  self.purchased_at = nil
363
363
 
364
364
  self.payment = details.kind_of?(Hash) ? details : { details: details.to_s }
@@ -369,7 +369,7 @@ module Effective
369
369
 
370
370
  success = true
371
371
  rescue => e
372
- self.purchase_state = purchase_state_was
372
+ self.state = state_was
373
373
  self.purchased_at = purchased_at_was
374
374
 
375
375
  error = e.message
@@ -377,38 +377,16 @@ module Effective
377
377
  end
378
378
  end
379
379
 
380
- raise "Failed to decline! Effective::Order: #{error || errors.full_messages.to_sentence}" unless success
380
+ raise "Failed to decline order: #{error || errors.full_messages.to_sentence}" unless success
381
381
 
382
382
  run_purchasable_callbacks(:after_decline)
383
383
 
384
384
  true
385
385
  end
386
386
 
387
- def abandoned?
388
- purchase_state == EffectiveOrders::ABANDONED
389
- end
390
-
391
- def purchased?(provider = nil)
392
- return false if (purchase_state != EffectiveOrders::PURCHASED)
393
- return true if provider.nil? || payment_provider == provider.to_s
394
-
395
- unless EffectiveOrder.payment_providers.include?(provider.to_s)
396
- raise "Unknown provider #{provider}. Known providers are #{EffectiveOrders.payment_providers}"
397
- end
398
- end
399
-
400
- def declined?
401
- purchase_state == EffectiveOrders::DECLINED
402
- end
403
-
404
- def pending?
405
- purchase_state == EffectiveOrders::PENDING
406
- end
407
-
408
387
  def send_order_receipts!
409
388
  send_order_receipt_to_admin! if EffectiveOrders.mailer[:send_order_receipt_to_admin]
410
389
  send_order_receipt_to_buyer! if EffectiveOrders.mailer[:send_order_receipt_to_buyer]
411
- send_order_receipt_to_seller! if EffectiveOrders.mailer[:send_order_receipt_to_seller]
412
390
  end
413
391
 
414
392
  def send_order_receipt_to_admin!
@@ -419,14 +397,6 @@ module Effective
419
397
  send_email(:order_receipt_to_buyer, to_param) if purchased?
420
398
  end
421
399
 
422
- def send_order_receipt_to_seller!
423
- return false unless (EffectiveOrders.stripe_connect_enabled && purchased?(:stripe_connect))
424
-
425
- order_items.group_by { |oi| oi.seller }.each do |seller, order_items|
426
- send_email(:order_receipt_to_seller, to_param, seller, order_items)
427
- end
428
- end
429
-
430
400
  def send_payment_request_to_buyer!
431
401
  send_email(:payment_request_to_buyer, to_param) unless purchased?
432
402
  end
@@ -453,46 +423,45 @@ module Effective
453
423
 
454
424
  private
455
425
 
456
- def assign_totals!
426
+ def assign_order_totals
457
427
  self.subtotal = order_items.map { |oi| oi.subtotal }.sum
458
428
  self.tax_rate = get_tax_rate()
459
429
  self.tax = get_tax()
460
430
  self.total = subtotal + (tax || 0)
461
431
  end
462
432
 
463
- def save_addresses
464
- if user.respond_to?(:billing_address=) && billing_address.present?
465
- user.billing_address = billing_address
466
- user.billing_address.save
433
+ def assign_billing_name
434
+ self.billing_name = [(billing_address.full_name.presence if billing_address.present?), (user.to_s.presence)].compact.first
435
+ end
436
+
437
+ def assign_last_address
438
+ return unless user.present?
439
+ return unless (EffectiveOrders.billing_address || EffectiveOrders.shipping_address)
440
+ return if EffectiveOrders.billing_address && billing_address.present?
441
+ return if EffectiveOrders.shipping_address && shipping_address.present?
442
+
443
+ last_order = Effective::Order.sorted.where(user: user).last
444
+ return unless last_order.present?
445
+
446
+ if EffectiveOrders.billing_address && last_order.billing_address.present?
447
+ self.billing_address = last_order.billing_address
467
448
  end
468
449
 
469
- if user.respond_to?(:shipping_address=) && shipping_address.present?
470
- user.shipping_address = shipping_address
471
- user.shipping_address.save
450
+ if EffectiveOrders.shipping_address && last_order.shipping_address.present?
451
+ self.shipping_address = last_order.shipping_address
472
452
  end
473
453
  end
474
454
 
455
+ def assign_purchased_order_to_purchasables
456
+ order_items.each { |oi| oi.purchasable.purchased_order = self }
457
+ end
458
+
475
459
  def run_purchasable_callbacks(name)
476
- begin
477
- order_items.each { |oi| oi.purchasable.public_send(name, self, oi) if oi.purchasable.respond_to?(name) }
478
- rescue => e
479
- raise e unless Rails.env.production?
480
- end
460
+ order_items.each { |oi| oi.purchasable.public_send(name, self, oi) if oi.purchasable.respond_to?(name) }
481
461
  end
482
462
 
483
463
  def send_email(email, *mailer_args)
484
- begin
485
- if EffectiveOrders.mailer[:delayed_job_deliver] && EffectiveOrders.mailer[:deliver_method] == :deliver_later
486
- Effective::OrdersMailer.delay.public_send(email, *mailer_args)
487
- elsif EffectiveOrders.mailer[:deliver_method].present?
488
- Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
489
- else
490
- Effective::OrdersMailer.public_send(email, *mailer_args).deliver_now
491
- end
492
- rescue => e
493
- raise e unless Rails.env.production?
494
- return false
495
- end
464
+ Effective::OrdersMailer.public_send(email, *mailer_args).public_send(EffectiveOrders.mailer[:deliver_method])
496
465
  end
497
466
 
498
467
  def truthy?(value)