effective_orders 3.2.3 → 4.0.0beta1

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 (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)