effective_orders 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +124 -84
  4. data/app/assets/javascripts/effective_orders/customers.js.coffee +39 -0
  5. data/app/assets/javascripts/effective_orders/providers/{stripe_charges.js.coffee → stripe.js.coffee} +15 -13
  6. data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +73 -0
  7. data/app/assets/stylesheets/effective_orders.scss +2 -1
  8. data/app/assets/stylesheets/effective_orders/_order.scss +16 -8
  9. data/app/assets/stylesheets/effective_orders/_subscriptions.scss +14 -0
  10. data/app/controllers/admin/customers_controller.rb +11 -8
  11. data/app/controllers/admin/order_items_controller.rb +4 -8
  12. data/app/controllers/admin/orders_controller.rb +133 -87
  13. data/app/controllers/effective/carts_controller.rb +18 -8
  14. data/app/controllers/effective/concerns/purchase.rb +39 -0
  15. data/app/controllers/effective/customers_controller.rb +43 -0
  16. data/app/controllers/effective/orders_controller.rb +73 -119
  17. data/app/controllers/effective/providers/app_checkout.rb +3 -1
  18. data/app/controllers/effective/providers/ccbill.rb +4 -6
  19. data/app/controllers/effective/providers/cheque.rb +20 -11
  20. data/app/controllers/effective/providers/free.rb +33 -0
  21. data/app/controllers/effective/providers/mark_as_paid.rb +33 -0
  22. data/app/controllers/effective/providers/moneris.rb +9 -17
  23. data/app/controllers/effective/providers/paypal.rb +4 -6
  24. data/app/controllers/effective/providers/pretend.rb +4 -4
  25. data/app/controllers/effective/providers/refund.rb +39 -0
  26. data/app/controllers/effective/providers/stripe.rb +19 -40
  27. data/app/controllers/effective/providers/stripe_connect.rb +2 -6
  28. data/app/controllers/effective/webhooks_controller.rb +44 -95
  29. data/app/datatables/effective_customers_datatable.rb +21 -29
  30. data/app/datatables/effective_order_items_datatable.rb +77 -79
  31. data/app/datatables/effective_orders_datatable.rb +67 -57
  32. data/app/helpers/effective_carts_helper.rb +17 -14
  33. data/app/helpers/effective_orders_helper.rb +40 -56
  34. data/app/helpers/effective_paypal_helper.rb +3 -3
  35. data/app/helpers/effective_stripe_helper.rb +47 -18
  36. data/app/helpers/effective_subscriptions_helper.rb +79 -0
  37. data/app/mailers/effective/orders_mailer.rb +125 -2
  38. data/app/models/concerns/acts_as_purchasable.rb +23 -33
  39. data/app/models/concerns/acts_as_subscribable.rb +68 -0
  40. data/app/models/concerns/acts_as_subscribable_buyer.rb +22 -0
  41. data/app/models/effective/cart.rb +53 -24
  42. data/app/models/effective/cart_item.rb +6 -12
  43. data/app/models/effective/customer.rb +51 -54
  44. data/app/models/effective/order.rb +160 -147
  45. data/app/models/effective/order_item.rb +18 -21
  46. data/app/models/effective/product.rb +7 -7
  47. data/app/models/effective/providers/ccbill_postback.rb +1 -1
  48. data/app/models/effective/providers/stripe_charge.rb +8 -19
  49. data/app/models/effective/subscripter.rb +230 -0
  50. data/app/models/effective/subscription.rb +27 -76
  51. data/app/models/effective/tax_rate_calculator.rb +10 -7
  52. data/app/views/admin/customers/_actions.html.haml +1 -2
  53. data/app/views/admin/customers/index.html.haml +1 -1
  54. data/app/views/admin/customers/show.html.haml +6 -0
  55. data/app/views/admin/orders/_actions.html.haml +9 -7
  56. data/app/views/admin/orders/_form.html.haml +11 -7
  57. data/app/views/admin/orders/_order_actions.html.haml +2 -1
  58. data/app/views/admin/orders/_order_item_fields.html.haml +1 -1
  59. data/app/views/admin/orders/edit.html.haml +4 -0
  60. data/app/views/admin/orders/index.html.haml +1 -4
  61. data/app/views/admin/orders/new.html.haml +1 -1
  62. data/app/views/admin/orders/show.html.haml +5 -6
  63. data/app/views/effective/carts/_cart.html.haml +2 -2
  64. data/app/views/effective/carts/show.html.haml +2 -2
  65. data/app/views/effective/customers/_customer.html.haml +152 -0
  66. data/app/views/effective/customers/_fields.html.haml +12 -0
  67. data/app/views/effective/customers/_form.html.haml +13 -0
  68. data/app/views/effective/customers/edit.html.haml +3 -0
  69. data/app/views/effective/orders/_checkout_step1.html.haml +8 -15
  70. data/app/views/effective/orders/_checkout_step2.html.haml +34 -21
  71. data/app/views/effective/orders/_order.html.haml +8 -9
  72. data/app/views/effective/orders/_order_actions.html.haml +7 -8
  73. data/app/views/effective/orders/_order_header.html.haml +1 -1
  74. data/app/views/effective/orders/_order_items.html.haml +11 -5
  75. data/app/views/effective/orders/_order_note.html.haml +4 -7
  76. data/app/views/effective/orders/_orders_table.html.haml +26 -26
  77. data/app/views/effective/orders/app_checkout/_form.html.haml +2 -2
  78. data/app/views/effective/orders/ccbill/_form.html.haml +1 -1
  79. data/app/views/effective/orders/cheque/_form.html.haml +3 -1
  80. data/app/views/effective/orders/declined.html.haml +1 -1
  81. data/app/views/effective/orders/{checkout_step1.html.haml → edit.html.haml} +0 -0
  82. data/app/views/effective/orders/free/_form.html.haml +4 -0
  83. data/app/views/effective/orders/index.html.haml +2 -4
  84. data/app/views/effective/orders/mark_as_paid/_form.html.haml +32 -0
  85. data/app/views/effective/orders/moneris/_form.html.haml +6 -6
  86. data/app/views/effective/orders/{checkout_step2.html.haml → new.html.haml} +1 -1
  87. data/app/views/effective/orders/paypal/_form.html.haml +2 -2
  88. data/app/views/effective/orders/pretend/_form.html.haml +2 -2
  89. data/app/views/effective/orders/purchased.html.haml +3 -0
  90. data/app/views/effective/orders/refund/_form.html.haml +32 -0
  91. data/app/views/effective/orders/show.html.haml +4 -1
  92. data/app/views/effective/orders/stripe/_form.html.haml +5 -5
  93. data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
  94. data/app/views/effective/orders_mailer/subscription_payment_failed.html.haml +9 -0
  95. data/app/views/effective/orders_mailer/subscription_payment_succeeded.html.haml +9 -0
  96. data/app/views/effective/orders_mailer/subscription_trial_expired.html.haml +5 -0
  97. data/app/views/effective/orders_mailer/subscription_trial_expiring.html.haml +7 -0
  98. data/app/views/effective/subscriptions/_fields.html.haml +16 -0
  99. data/app/views/effective/subscriptions/_plan.html.haml +21 -0
  100. data/app/views/layouts/effective_orders_mailer_layout.html.haml +6 -8
  101. data/config/effective_orders.rb +41 -20
  102. data/config/routes.rb +48 -48
  103. data/db/migrate/01_create_effective_orders.rb.erb +19 -5
  104. data/lib/effective_orders.rb +78 -42
  105. data/lib/effective_orders/engine.rb +36 -82
  106. data/lib/effective_orders/version.rb +1 -1
  107. data/lib/generators/effective_orders/install_generator.rb +2 -2
  108. data/lib/generators/templates/effective_orders_mailer_preview.rb +39 -4
  109. data/lib/tasks/effective_orders_tasks.rake +42 -0
  110. data/spec/controllers/carts_controller_spec.rb +1 -1
  111. data/spec/controllers/moneris_orders_controller_spec.rb +4 -4
  112. data/spec/controllers/orders_controller_spec.rb +4 -4
  113. data/spec/controllers/stripe_orders_controller_spec.rb +2 -2
  114. data/spec/controllers/webhooks_controller_spec.rb +1 -1
  115. data/spec/dummy/config/initializers/effective_orders.rb +1 -7
  116. data/spec/dummy/db/schema.rb +1 -0
  117. data/spec/dummy/db/test.sqlite3 +0 -0
  118. data/spec/dummy/log/test.log +3 -0
  119. data/spec/models/acts_as_purchasable_spec.rb +0 -56
  120. data/spec/models/customer_spec.rb +3 -3
  121. data/spec/models/order_spec.rb +2 -2
  122. data/spec/spec_helper.rb +1 -1
  123. data/spec/support/factories.rb +2 -1
  124. metadata +37 -49
  125. data/active_admin/effective_carts.rb +0 -14
  126. data/active_admin/effective_orders.rb +0 -112
  127. data/app/assets/javascripts/effective_orders/providers/stripe_subscriptions.js.coffee +0 -28
  128. data/app/controllers/concerns/acts_as_active_admin_controller.rb +0 -69
  129. data/app/controllers/effective/subscriptions_controller.rb +0 -126
  130. data/app/models/effective/datatables/customers.rb +0 -40
  131. data/app/models/effective/datatables/order_items.rb +0 -101
  132. data/app/models/effective/datatables/orders.rb +0 -91
  133. data/app/models/inputs/price_field.rb +0 -63
  134. data/app/models/inputs/price_form_input.rb +0 -7
  135. data/app/models/inputs/price_formtastic_input.rb +0 -9
  136. data/app/models/inputs/price_input.rb +0 -19
  137. data/app/models/inputs/price_simple_form_input.rb +0 -8
  138. data/app/views/admin/orders/_form_mark_as_paid.html.haml +0 -33
  139. data/app/views/admin/orders/_order_payment_details.html.haml +0 -5
  140. data/app/views/admin/orders/mark_as_paid.html.haml +0 -7
  141. data/app/views/effective/orders/stripe/_subscription_fields.html.haml +0 -7
  142. data/app/views/effective/subscriptions/index.html.haml +0 -22
  143. data/app/views/effective/subscriptions/new.html.haml +0 -9
  144. data/app/views/effective/subscriptions/show.html.haml +0 -49
  145. data/db/upgrade/02_upgrade_effective_orders_from03x.rb.erb +0 -29
  146. data/db/upgrade/03_upgrade_effective_orders_from1x.rb.erb +0 -98
  147. data/db/upgrade/upgrade_price_column_on_table.rb.erb +0 -17
  148. data/lib/generators/effective_orders/upgrade_from03x_generator.rb +0 -31
  149. data/lib/generators/effective_orders/upgrade_from1x_generator.rb +0 -27
  150. data/lib/generators/effective_orders/upgrade_price_column_generator.rb +0 -30
@@ -12,10 +12,14 @@ module ActsAsPurchasable
12
12
  end
13
13
 
14
14
  included do
15
- has_many :orders, through: :order_items, class_name: 'Effective::Order'
16
- has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
17
15
  has_many :cart_items, as: :purchasable, dependent: :delete_all, class_name: 'Effective::CartItem'
18
16
 
17
+ has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
18
+ has_many :orders, -> { order(:id) }, through: :order_items, class_name: 'Effective::Order'
19
+
20
+ has_many :purchased_orders, -> { where(purchase_state: EffectiveOrders::PURCHASED).order(:purchased_at) },
21
+ through: :order_items, class_name: 'Effective::Order', source: :order
22
+
19
23
  validates_with Effective::SoldOutValidator, on: :create
20
24
 
21
25
  # Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
@@ -24,8 +28,8 @@ module ActsAsPurchasable
24
28
  validates :tax_exempt, inclusion: { in: [true, false] }
25
29
 
26
30
  # These are breaking on the check for quanitty_enabled?. More research is due
27
- validates :quantity_purchased, numericality: {allow_nil: true}, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
28
- validates :quantity_max, numericality: {allow_nil: true}, if: proc { |purchasable| (purchasable.quantity_enabled? rescue false) }
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) }
29
33
 
30
34
  scope :purchased, -> { joins(order_items: :order).where(orders: {purchase_state: EffectiveOrders::PURCHASED}).distinct }
31
35
  scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: {user_id: user.try(:id), purchase_state: EffectiveOrders::PURCHASED}).distinct }
@@ -37,6 +41,10 @@ module ActsAsPurchasable
37
41
  end
38
42
 
39
43
  module ClassMethods
44
+ def before_purchase(&block)
45
+ send :define_method, :before_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
46
+ end
47
+
40
48
  def after_purchase(&block)
41
49
  send :define_method, :after_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
42
50
  end
@@ -54,16 +62,12 @@ module ActsAsPurchasable
54
62
 
55
63
  # If I have a column type of Integer, and I'm passed a non-Integer, convert it here
56
64
  def price=(value)
57
- integer_column = ((column_for_attribute('price').try(:type) rescue nil) == :integer) # Rails built in method to lookup datatype
58
-
59
- if integer_column == false
60
- super
61
- elsif value.kind_of?(Integer)
65
+ if value.kind_of?(Integer)
62
66
  super
63
67
  elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
64
68
  super
65
- else # Could be Float, BigDecimal, or String like 9.99
66
- super((value.to_f * 100.0).to_i)
69
+ else
70
+ raise 'expected price to be an Integer representing the number of cents.'
67
71
  end
68
72
  end
69
73
 
@@ -77,28 +81,24 @@ module ActsAsPurchasable
77
81
 
78
82
  def seller
79
83
  if EffectiveOrders.stripe_connect_enabled
80
- 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.'
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.'
81
85
  end
82
86
  end
83
87
 
84
- def purchased?
85
- @is_purchased ||= orders.any? { |order| order.purchased? }
88
+ def purchased_order
89
+ @purchased_order ||= purchased_orders.first
86
90
  end
87
91
 
88
- def purchased_at
89
- @purchased_at ||= orders.map { |order| order.purchased_at if order.purchased? }.compact.sort.first
92
+ def purchased?
93
+ @is_purchased ||= purchased_order.present?
90
94
  end
91
95
 
92
96
  def purchased_by?(user)
93
- orders.any? { |order| order.purchased? && order.user_id == user.id }
97
+ purchased_orders.any? { |order| order.user_id == user.id }
94
98
  end
95
99
 
96
- def purchased_orders
97
- orders.select { |order| order.purchased? }
98
- end
99
-
100
- def purchased_order
101
- purchased_orders.first
100
+ def purchased_at
101
+ purchased_order.try(:purchased_at)
102
102
  end
103
103
 
104
104
  def quantity_enabled?
@@ -113,16 +113,6 @@ module ActsAsPurchasable
113
113
  quantity_enabled? ? (quantity_remaining == 0) : false
114
114
  end
115
115
 
116
- def purchased!(order = nil, order_item = nil)
117
- after_purchase(order, order_item) if self.respond_to?(:after_purchase)
118
- save!
119
- end
120
-
121
- def declined!(order = nil, order_item = nil)
122
- after_decline(order, order_item) if self.respond_to?(:after_decline)
123
- save!
124
- end
125
-
126
116
  # Override me if this is a digital purchase.
127
117
  def purchased_download_url
128
118
  false
@@ -0,0 +1,68 @@
1
+ module ActsAsSubscribable
2
+ extend ActiveSupport::Concern
3
+
4
+ mattr_accessor :descendants
5
+
6
+ module ActiveRecord
7
+ def acts_as_subscribable(*options)
8
+ @acts_as_subscribable = options || []
9
+ include ::ActsAsSubscribable
10
+ (ActsAsSubscribable.descendants ||= []) << self
11
+ end
12
+ end
13
+
14
+ included do
15
+ has_one :subscription, as: :subscribable, class_name: 'Effective::Subscription'
16
+ has_one :customer, through: :subscription, class_name: 'Effective::Customer'
17
+
18
+ validates :subscripter, associated: true
19
+
20
+ scope :subscribed, -> { where(id: joins(:subscription)) } # All resources with a subscription
21
+ scope :trialing, -> { where.not(id: joins(:subscription)) } # All resources without a subscription
22
+ end
23
+
24
+ module ClassMethods
25
+ end
26
+
27
+ def subscripter
28
+ @_effective_subscripter ||= Effective::Subscripter.new(subscribable: self, user: buyer)
29
+ end
30
+
31
+ def subscripter=(atts)
32
+ subscripter.assign_attributes(atts)
33
+ end
34
+
35
+ def subscribed?(stripe_plan_id = nil)
36
+ case stripe_plan_id
37
+ when nil
38
+ subscription.present? # Subscribed to any subscription?
39
+ when (EffectiveOrders.stripe_plans['trial'] || {})[:id]
40
+ subscription.blank? || subscription.new_record? || subscription.stripe_plan_id == stripe_plan_id
41
+ else
42
+ subscription && subscription.persisted? && subscription.errors.blank? && subscription.stripe_plan_id == stripe_plan_id
43
+ end
44
+ end
45
+
46
+ def subscription_active?
47
+ (trialing? && !trial_expired?) || (subscribed? && subscription.active?)
48
+ end
49
+
50
+ def trialing?
51
+ !subscribed?
52
+ end
53
+
54
+ def trial_expired?
55
+ trialing? && Time.zone.now > trial_expires_at
56
+ end
57
+
58
+ def trial_expires_at
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
61
+ end
62
+
63
+ def buyer
64
+ raise 'acts_as_subscribable object requires the buyer be defined to return the User buying this item.'
65
+ end
66
+
67
+ end
68
+
@@ -0,0 +1,22 @@
1
+ module ActsAsSubscribableBuyer
2
+ extend ActiveSupport::Concern
3
+
4
+ module ActiveRecord
5
+ def acts_as_subscribable_buyer(*options)
6
+ include ::ActsAsSubscribableBuyer
7
+ end
8
+ end
9
+
10
+ included do
11
+ has_one :customer, class_name: 'Effective::Customer'
12
+
13
+ before_save(if: -> { persisted? && email_changed? && customer.present? }) do
14
+ Rails.logger.info "STRIPE CUSTOMER EMAIL UPDATE: #{customer.stripe_customer_id}"
15
+ customer.stripe_customer.email = email
16
+ customer.stripe_customer.description = to_s
17
+ throw :abort unless (customer.stripe_customer.save rescue false)
18
+ end
19
+ end
20
+
21
+ end
22
+
@@ -2,58 +2,87 @@ module Effective
2
2
  class Cart < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.carts_table_name.to_s
4
4
 
5
- belongs_to :user # This is optional. We want to let non-logged-in people have carts too
6
- has_many :cart_items, :inverse_of => :cart, :dependent => :delete_all
5
+ belongs_to :user # Optional. We want non-logged-in users to have carts too.
6
+ has_many :cart_items, -> { includes(:purchasable).order(:updated_at) }, dependent: :delete_all, class_name: 'Effective::CartItem'
7
7
 
8
- # structure do
9
- # timestamps
10
- # end
8
+ accepts_nested_attributes_for :cart_items
11
9
 
12
- default_scope -> { includes(:cart_items => :purchasable) }
10
+ # Attributes
11
+ # cart_items_count :integer
12
+ # timestamps
13
13
 
14
- def add(item, quantity: 1)
14
+ scope :deep, -> { includes(cart_items: :purchasable) }
15
+
16
+ # cart.add(@product, unique: -> (a, b) { a.kind_of?(Product) && b.kind_of?(Product) && a.category == b.category })
17
+ # cart.add(@product, unique: :category)
18
+ # cart.add(@product, unique: false) # Add as many as you want
19
+ def add(item, quantity: 1, unique: true)
15
20
  raise 'expecting an acts_as_purchasable object' unless item.kind_of?(ActsAsPurchasable)
16
21
 
17
- existing_item = cart_items.where(purchasable_id: item.id, purchasable_type: item.class.name).first
22
+ existing = (
23
+ if unique.kind_of?(Proc)
24
+ cart_items.find { |cart_item| instance_exec(item, cart_item.purchasable, &unique) }
25
+ elsif unique.kind_of?(Symbol) || (unique.kind_of?(String) && unique != 'true')
26
+ raise "expected item to respond to unique #{unique}" unless item.respond_to?(unique)
27
+ cart_items.find { |cart_item| cart_item.purchasable.respond_to?(unique) && item.send(unique) == cart_item.purchasable.send(unique) }
28
+ elsif unique.present?
29
+ find(item)
30
+ end
31
+ )
18
32
 
19
- if item.quantity_enabled? && (quantity + (existing_item.quantity rescue 0)) > item.quantity_remaining
20
- raise EffectiveOrders::SoldOutException, "#{item.title} is sold out"
21
- return
33
+ if existing
34
+ if unique || (existing.unique.present?)
35
+ existing.assign_attributes(purchasable: item, quantity: quantity, unique: existing.unique)
36
+ else
37
+ existing.quantity = existing.quantity + quantity
38
+ end
22
39
  end
23
40
 
24
- if existing_item.present?
25
- existing_item.update_attributes(quantity: existing_item.quantity + quantity)
26
- else
27
- cart_items.create(cart: self, purchasable_id: item.id, purchasable_type: item.class.name, quantity: quantity)
41
+ if item.quantity_enabled? && (existing ? existing.quantity : quantity) > item.quantity_remaining
42
+ raise EffectiveOrders::SoldOutException, "#{item.title} is sold out"
28
43
  end
44
+
45
+ existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s if unique.kind_of?(Symbol) || unique.kind_of?(String) || unique == true))
46
+ save!
47
+ end
48
+
49
+ def clear!
50
+ cart_items.each { |cart_item| cart_item.mark_for_destruction }
51
+ save!
29
52
  end
30
- alias_method :add_to_cart, :add
31
53
 
32
- def remove(obj)
33
- (cart_items.find(cart_item) || cart_item).try(:destroy)
54
+ def remove(item)
55
+ find(item).try(:mark_for_destruction)
56
+ save!
34
57
  end
35
- alias_method :remove_from_cart, :remove
36
58
 
37
59
  def includes?(item)
38
60
  find(item).present?
39
61
  end
40
62
 
41
63
  def find(item)
42
- cart_items.to_a.find { |cart_item| cart_item == item || cart_item.purchasable == item }
64
+ cart_items.find { |cart_item| cart_item == item || cart_item.purchasable == item }
65
+ end
66
+
67
+ def purchasables
68
+ cart_items.map { |cart_item| cart_item.purchasable }
43
69
  end
44
70
 
45
71
  def size
46
- cart_items.size
72
+ cart_items_count || cart_items.length
73
+ end
74
+
75
+ def present?
76
+ size > 0
47
77
  end
48
78
 
49
- def empty?
50
- size == 0
79
+ def blank?
80
+ size <= 0
51
81
  end
52
82
 
53
83
  def subtotal
54
84
  cart_items.map { |ci| ci.subtotal }.sum
55
85
  end
56
- alias_method :total, :subtotal
57
86
 
58
87
  end
59
88
  end
@@ -2,26 +2,21 @@ module Effective
2
2
  class CartItem < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.cart_items_table_name.to_s
4
4
 
5
- belongs_to :cart
6
- belongs_to :purchasable, :polymorphic => true
5
+ belongs_to :cart, counter_cache: true, class_name: 'Effective::Cart'
6
+ belongs_to :purchasable, polymorphic: true
7
7
 
8
- # structure do
9
- # quantity :integer
10
- #
11
- # timestamps
12
- # end
8
+ # Attributes
9
+ # quantity :integer
10
+ # timestamps
13
11
 
14
12
  validates :purchasable, presence: true
15
13
  validates :quantity, presence: true
16
14
 
17
- default_scope -> { order(:updated_at) }
18
-
19
15
  def price
20
16
  if (purchasable.price || 0).kind_of?(Integer)
21
17
  purchasable.price || 0
22
18
  else
23
- ActiveSupport::Deprecation.warn('price is a non-integer. It should be an Integer representing the number of cents. Continuing with (price * 100.0).round(0).to_i conversion') unless EffectiveOrders.silence_deprecation_warnings
24
- (purchasable.price * 100.0).round(0).to_i rescue 0
19
+ raise 'expected price to be an Integer representing the number of cents.'
25
20
  end
26
21
  end
27
22
 
@@ -36,7 +31,6 @@ module Effective
36
31
  def subtotal
37
32
  price * quantity
38
33
  end
39
- alias_method :total, :subtotal
40
34
 
41
35
  end
42
36
  end
@@ -2,83 +2,80 @@ module Effective
2
2
  class Customer < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.customers_table_name.to_s
4
4
 
5
- attr_accessor :token # This is a convenience method so we have a place to store StripeConnect temporary access tokens
5
+ attr_accessor :stripe_token # This is a convenience method so we have a place to store StripeConnect temporary access tokens
6
+ attr_accessor :stripe_customer, :stripe_subscription
6
7
 
7
8
  belongs_to :user
8
- has_many :subscriptions, :inverse_of => :customer
9
+ has_many :subscriptions, class_name: 'Effective::Subscription', foreign_key: 'customer_id'
10
+ has_many :subscribables, through: :subscriptions, source: :subscribable
9
11
 
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
12
+ accepts_nested_attributes_for :subscriptions
13
+
14
+ # Attributes
15
+ # stripe_customer_id :string # cus_xja7acoa03
16
+ # active_card :string # **** **** **** 4242 Visa 05/12
17
+
18
+ # stripe_subscription_id :string # Each user gets one stripe subscription object, which can contain many items
19
+ # status :string
20
+
21
+ # stripe_connect_access_token :string # If using StripeConnect and this user is a connected Seller
14
22
  #
15
- # timestamps
16
- # end
23
+ # timestamps
24
+
25
+ scope :deep, -> { includes(subscriptions: :subscribable) }
26
+
27
+ before_validation do
28
+ subscriptions.each { |subscription| subscription.status = status }
29
+ end
17
30
 
18
31
  validates :user, presence: true
19
- validates :user_id, uniqueness: true
32
+ validates :stripe_customer_id, presence: true
33
+ validates :status, if: -> { stripe_subscription_id.present? }, inclusion: { in: %w(active past_due) }
20
34
 
21
- scope :customers, -> { where("#{EffectiveOrders.customers_table_name.to_s}.stripe_customer_id IS NOT NULL") }
35
+ def self.for_user(user)
36
+ Effective::Customer.where(user: user).first_or_initialize
37
+ end
22
38
 
23
- class << self
24
- def for_user(user)
25
- if user.present?
26
- Effective::Customer.where(:user_id => (user.try(:id) rescue user.to_i)).first_or_create
27
- end
28
- end
39
+ def to_s
40
+ user.to_s.presence || 'New Customer'
29
41
  end
30
42
 
31
43
  def stripe_customer
32
44
  @stripe_customer ||= if stripe_customer_id.present?
45
+ Rails.logger.info "STRIPE CUSTOMER RETRIEVE: #{stripe_customer_id}"
33
46
  ::Stripe::Customer.retrieve(stripe_customer_id)
34
- else
35
- ::Stripe::Customer.create(:email => user.email, :description => user.id.to_s).tap do |stripe_customer|
36
- self.update_attributes(:stripe_customer_id => stripe_customer.id)
37
- end
38
47
  end
39
48
  end
40
49
 
41
- def update_card!(token)
42
- if token.present? # Oh, so they want to use a new credit card...
43
- if stripe_customer.respond_to?(:cards)
44
- stripe_customer.card = token # This sets the default_card to the new card
45
- elsif stripe_customer.respond_to?(:sources)
46
- stripe_customer.source = token
47
- end
48
-
49
- if stripe_customer.save && default_card.present?
50
- card = cards.retrieve(default_card)
51
-
52
- self.stripe_active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
53
- self.save!
54
- else
55
- raise Exception.new('unable to update stripe customer with new card')
56
- end
50
+ def stripe_subscription
51
+ @stripe_subscription ||= if stripe_subscription_id.present?
52
+ Rails.logger.info "STRIPE SUBSCRIPTION RETRIEVE: #{stripe_subscription_id}"
53
+ ::Stripe::Subscription.retrieve(stripe_subscription_id)
57
54
  end
58
55
  end
59
56
 
60
- def is_stripe_connect_seller?
61
- stripe_connect_access_token.present?
57
+ def upcoming_invoice
58
+ @upcoming_invoice ||= if stripe_customer_id.present? && stripe_subscription_id.present?
59
+ Rails.logger.info "STRIPE UPCOMING INVOICE RETRIEVE: #{stripe_customer_id}"
60
+ ::Stripe::Invoice.upcoming(customer: stripe_customer_id) rescue nil
61
+ end
62
62
  end
63
63
 
64
- private
65
-
66
- def default_card
67
- case
68
- when stripe_customer.respond_to?(:default_card)
69
- stripe_customer.default_card
70
- when stripe_customer.respond_to?(:default_source)
71
- stripe_customer.default_source
72
- end
64
+ def token_required?
65
+ active_card.blank? || (active_card.present? && stripe_subscription_id.present? && status != 'active')
73
66
  end
74
67
 
75
- def cards
76
- case
77
- when stripe_customer.respond_to?(:cards)
78
- stripe_customer.cards
79
- when stripe_customer.respond_to?(:sources)
80
- stripe_customer.sources
81
- end
68
+ def payment_status
69
+ if status == 'past_due'
70
+ 'We ran into an error processing your last payment. Please update or confirm your card details to continue.'
71
+ elsif active_card.present? && token_required?
72
+ 'Please update or confirm your card details to continue.'
73
+ elsif active_card.present?
74
+ 'Thanks for your support! The card we have on file is'
75
+ else
76
+ 'No credit card on file. Please add a card.'
77
+ end.html_safe
82
78
  end
79
+
83
80
  end
84
81
  end