effective_orders 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1004 -0
  4. data/app/assets/images/effective_orders/stripe.png +0 -0
  5. data/app/assets/javascripts/effective_orders.js +6 -0
  6. data/app/assets/javascripts/effective_orders/customers.js.coffee +32 -0
  7. data/app/assets/javascripts/effective_orders/providers/stripe.js.coffee +77 -0
  8. data/app/assets/javascripts/effective_orders/subscriptions.js.coffee +81 -0
  9. data/app/assets/stylesheets/effective_orders.scss +2 -0
  10. data/app/assets/stylesheets/effective_orders/_cart.scss +4 -0
  11. data/app/assets/stylesheets/effective_orders/_order.scss +58 -0
  12. data/app/controllers/admin/customers_controller.rb +24 -0
  13. data/app/controllers/admin/order_items_controller.rb +16 -0
  14. data/app/controllers/admin/orders_controller.rb +223 -0
  15. data/app/controllers/effective/carts_controller.rb +85 -0
  16. data/app/controllers/effective/concerns/purchase.rb +62 -0
  17. data/app/controllers/effective/customers_controller.rb +20 -0
  18. data/app/controllers/effective/orders_controller.rb +162 -0
  19. data/app/controllers/effective/providers/cheque.rb +22 -0
  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 +60 -0
  23. data/app/controllers/effective/providers/paypal.rb +33 -0
  24. data/app/controllers/effective/providers/phone.rb +22 -0
  25. data/app/controllers/effective/providers/pretend.rb +26 -0
  26. data/app/controllers/effective/providers/refund.rb +33 -0
  27. data/app/controllers/effective/providers/stripe.rb +72 -0
  28. data/app/controllers/effective/subscripter_controller.rb +18 -0
  29. data/app/controllers/effective/webhooks_controller.rb +109 -0
  30. data/app/datatables/admin/effective_customers_datatable.rb +22 -0
  31. data/app/datatables/admin/effective_orders_datatable.rb +100 -0
  32. data/app/datatables/effective_orders_datatable.rb +79 -0
  33. data/app/helpers/effective_carts_helper.rb +113 -0
  34. data/app/helpers/effective_orders_helper.rb +143 -0
  35. data/app/helpers/effective_paypal_helper.rb +49 -0
  36. data/app/helpers/effective_stripe_helper.rb +85 -0
  37. data/app/helpers/effective_subscriptions_helper.rb +34 -0
  38. data/app/mailers/effective/orders_mailer.rb +196 -0
  39. data/app/models/concerns/acts_as_purchasable.rb +118 -0
  40. data/app/models/concerns/acts_as_subscribable.rb +90 -0
  41. data/app/models/concerns/acts_as_subscribable_buyer.rb +49 -0
  42. data/app/models/effective/access_denied.rb +17 -0
  43. data/app/models/effective/cart.rb +88 -0
  44. data/app/models/effective/cart_item.rb +40 -0
  45. data/app/models/effective/customer.rb +92 -0
  46. data/app/models/effective/order.rb +541 -0
  47. data/app/models/effective/order_item.rb +63 -0
  48. data/app/models/effective/product.rb +23 -0
  49. data/app/models/effective/sold_out_validator.rb +7 -0
  50. data/app/models/effective/subscripter.rb +185 -0
  51. data/app/models/effective/subscription.rb +95 -0
  52. data/app/models/effective/tax_rate_calculator.rb +48 -0
  53. data/app/views/admin/customers/_actions.html.haml +2 -0
  54. data/app/views/admin/customers/index.html.haml +6 -0
  55. data/app/views/admin/customers/show.html.haml +6 -0
  56. data/app/views/admin/order_items/index.html.haml +3 -0
  57. data/app/views/admin/orders/_datatable_actions.html.haml +18 -0
  58. data/app/views/admin/orders/_form.html.haml +35 -0
  59. data/app/views/admin/orders/_form_note_internal.html.haml +7 -0
  60. data/app/views/admin/orders/_order_actions.html.haml +9 -0
  61. data/app/views/admin/orders/_order_item_fields.html.haml +14 -0
  62. data/app/views/admin/orders/checkout.html.haml +3 -0
  63. data/app/views/admin/orders/edit.html.haml +6 -0
  64. data/app/views/admin/orders/index.html.haml +6 -0
  65. data/app/views/admin/orders/new.html.haml +4 -0
  66. data/app/views/admin/orders/show.html.haml +4 -0
  67. data/app/views/effective/carts/_cart.html.haml +28 -0
  68. data/app/views/effective/carts/_cart_actions.html.haml +3 -0
  69. data/app/views/effective/carts/show.html.haml +17 -0
  70. data/app/views/effective/customers/_customer.html.haml +72 -0
  71. data/app/views/effective/customers/_form.html.haml +21 -0
  72. data/app/views/effective/customers/edit.html.haml +4 -0
  73. data/app/views/effective/customers/update.js.erb +5 -0
  74. data/app/views/effective/orders/_checkout_actions.html.haml +3 -0
  75. data/app/views/effective/orders/_checkout_step1.html.haml +4 -0
  76. data/app/views/effective/orders/_checkout_step2.html.haml +37 -0
  77. data/app/views/effective/orders/_datatable_actions.html.haml +2 -0
  78. data/app/views/effective/orders/_fields.html.haml +31 -0
  79. data/app/views/effective/orders/_fields_note.html.haml +7 -0
  80. data/app/views/effective/orders/_fields_terms.html.haml +8 -0
  81. data/app/views/effective/orders/_order.html.haml +11 -0
  82. data/app/views/effective/orders/_order_actions.html.haml +18 -0
  83. data/app/views/effective/orders/_order_deferred.html.haml +9 -0
  84. data/app/views/effective/orders/_order_footer.html.haml +1 -0
  85. data/app/views/effective/orders/_order_header.html.haml +23 -0
  86. data/app/views/effective/orders/_order_items.html.haml +72 -0
  87. data/app/views/effective/orders/_order_notes.html.haml +17 -0
  88. data/app/views/effective/orders/_order_payment.html.haml +24 -0
  89. data/app/views/effective/orders/_order_shipping.html.haml +30 -0
  90. data/app/views/effective/orders/_orders_table.html.haml +23 -0
  91. data/app/views/effective/orders/cheque/_form.html.haml +4 -0
  92. data/app/views/effective/orders/declined.html.haml +12 -0
  93. data/app/views/effective/orders/deferred.html.haml +13 -0
  94. data/app/views/effective/orders/deferred/_form.html.haml +16 -0
  95. data/app/views/effective/orders/edit.html.haml +3 -0
  96. data/app/views/effective/orders/free/_form.html.haml +5 -0
  97. data/app/views/effective/orders/index.html.haml +3 -0
  98. data/app/views/effective/orders/mark_as_paid/_form.html.haml +23 -0
  99. data/app/views/effective/orders/moneris/_form.html.haml +47 -0
  100. data/app/views/effective/orders/new.html.haml +3 -0
  101. data/app/views/effective/orders/paypal/_form.html.haml +5 -0
  102. data/app/views/effective/orders/phone/_form.html.haml +4 -0
  103. data/app/views/effective/orders/pretend/_form.html.haml +8 -0
  104. data/app/views/effective/orders/purchased.html.haml +11 -0
  105. data/app/views/effective/orders/refund/_form.html.haml +5 -0
  106. data/app/views/effective/orders/show.html.haml +6 -0
  107. data/app/views/effective/orders/stripe/_element.html.haml +8 -0
  108. data/app/views/effective/orders/stripe/_form.html.haml +31 -0
  109. data/app/views/effective/orders_mailer/order_error.html.haml +11 -0
  110. data/app/views/effective/orders_mailer/order_receipt_to_admin.html.haml +2 -0
  111. data/app/views/effective/orders_mailer/order_receipt_to_buyer.html.haml +2 -0
  112. data/app/views/effective/orders_mailer/payment_request_to_buyer.html.haml +13 -0
  113. data/app/views/effective/orders_mailer/pending_order_invoice_to_buyer.html.haml +13 -0
  114. data/app/views/effective/orders_mailer/refund_notification_to_admin.html.haml +15 -0
  115. data/app/views/effective/orders_mailer/subscription_canceled.html.haml +9 -0
  116. data/app/views/effective/orders_mailer/subscription_created.html.haml +13 -0
  117. data/app/views/effective/orders_mailer/subscription_event_to_admin.html.haml +13 -0
  118. data/app/views/effective/orders_mailer/subscription_payment_failed.html.haml +9 -0
  119. data/app/views/effective/orders_mailer/subscription_payment_succeeded.html.haml +9 -0
  120. data/app/views/effective/orders_mailer/subscription_trial_expired.html.haml +5 -0
  121. data/app/views/effective/orders_mailer/subscription_trialing.html.haml +7 -0
  122. data/app/views/effective/orders_mailer/subscription_updated.html.haml +13 -0
  123. data/app/views/effective/subscripter/_form.html.haml +60 -0
  124. data/app/views/effective/subscripter/_plan.html.haml +23 -0
  125. data/app/views/layouts/effective_orders_mailer_layout.html.haml +25 -0
  126. data/config/effective_orders.rb +279 -0
  127. data/config/routes.rb +70 -0
  128. data/db/migrate/01_create_effective_orders.rb.erb +137 -0
  129. data/lib/effective_orders.rb +243 -0
  130. data/lib/effective_orders/engine.rb +60 -0
  131. data/lib/effective_orders/version.rb +3 -0
  132. data/lib/generators/effective_orders/install_generator.rb +63 -0
  133. data/lib/generators/templates/effective_orders_mailer_preview.rb +120 -0
  134. data/lib/tasks/effective_orders_tasks.rake +69 -0
  135. metadata +276 -0
@@ -0,0 +1,118 @@
1
+ module ActsAsPurchasable
2
+ extend ActiveSupport::Concern
3
+
4
+ module Base
5
+ def acts_as_purchasable(*options)
6
+ @acts_as_purchasable = options || []
7
+
8
+ # if table_exists?
9
+ # instance = new()
10
+ # raise 'must respond_to price' unless instance.respond_to?(:price)
11
+ # raise 'must respond_to purchased_order_id' unless instance.respond_to?(:purchased_order_id)
12
+
13
+ # if defined?(EffectiveQbSync)
14
+ # raise 'must respond to qb_item_name' unless instance.respond_to?(:qb_item_name)
15
+ # end
16
+ # end
17
+
18
+ include ::ActsAsPurchasable
19
+ end
20
+ end
21
+
22
+ included do
23
+ belongs_to :purchased_order, class_name: 'Effective::Order', optional: true # Set when purchased
24
+
25
+ has_many :cart_items, as: :purchasable, dependent: :delete_all, class_name: 'Effective::CartItem'
26
+
27
+ has_many :order_items, as: :purchasable, class_name: 'Effective::OrderItem'
28
+ has_many :orders, -> { order(:id) }, through: :order_items, class_name: 'Effective::Order'
29
+
30
+ has_many :purchased_orders, -> { where(state: EffectiveOrders::PURCHASED).order(:purchased_at) },
31
+ through: :order_items, class_name: 'Effective::Order', source: :order
32
+
33
+ # Database max integer value is 2147483647. So let's round that down and use a max/min of $20 million (2000000000)
34
+ validates :price, presence: true
35
+ validates :price, numericality: { less_than_or_equal_to: 2000000000, message: 'maximum price is $20,000,000' }
36
+ validates :price, numericality: { greater_than_or_equal_to: -2000000000, message: 'minimum price is -$20,000,000' }
37
+
38
+ validates :tax_exempt, inclusion: { in: [true, false] }
39
+
40
+ with_options(if: -> { quantity_enabled? }) do
41
+ validates :quantity_purchased, numericality: { allow_nil: true }
42
+ validates :quantity_max, numericality: { allow_nil: true }
43
+ validates_with Effective::SoldOutValidator, on: :create
44
+ end
45
+
46
+ scope :purchased, -> { where.not(purchased_order_id: nil) }
47
+ scope :not_purchased, -> { where(purchased_order_id: nil) }
48
+
49
+ # scope :purchased, -> { joins(order_items: :order).where(orders: {state: EffectiveOrders::PURCHASED}).distinct }
50
+ # scope :not_purchased, -> { where('id NOT IN (?)', purchased.pluck(:id).presence || [0]) }
51
+ scope :purchased_by, lambda { |user| joins(order_items: :order).where(orders: { user_id: user.try(:id), state: EffectiveOrders::PURCHASED }).distinct }
52
+ scope :not_purchased_by, lambda { |user| where('id NOT IN (?)', purchased_by(user).pluck(:id).presence || [0]) }
53
+ end
54
+
55
+ module ClassMethods
56
+ def before_purchase(&block)
57
+ send :define_method, :before_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
58
+ end
59
+
60
+ def after_purchase(&block)
61
+ send :define_method, :after_purchase do |order, order_item| self.instance_exec(order, order_item, &block) end
62
+ end
63
+
64
+ def after_decline(&block)
65
+ send :define_method, :after_decline do |order, order_item| self.instance_exec(order, order_item, &block) end
66
+ end
67
+ end
68
+
69
+ # Regular instance methods
70
+
71
+ # If I have a column type of Integer, and I'm passed a non-Integer, convert it here
72
+ def price=(value)
73
+ if value.kind_of?(Integer)
74
+ super
75
+ elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
76
+ super
77
+ else
78
+ raise 'expected price to be an Integer representing the number of cents.'
79
+ end
80
+ end
81
+
82
+ def purchasable_name
83
+ to_s
84
+ end
85
+
86
+ def tax_exempt
87
+ self[:tax_exempt] || false
88
+ end
89
+
90
+ def purchased?
91
+ purchased_order_id.present?
92
+ end
93
+
94
+ def purchased_at
95
+ purchased_order.try(:purchased_at)
96
+ end
97
+
98
+ def purchased_by?(user)
99
+ purchased_orders.any? { |order| order.user_id == user.id }
100
+ end
101
+
102
+ def purchased_download_url # Override me if this is a digital purchase.
103
+ false
104
+ end
105
+
106
+ def quantity_enabled?
107
+ false
108
+ end
109
+
110
+ def quantity_remaining
111
+ quantity_max - quantity_purchased if quantity_enabled?
112
+ end
113
+
114
+ def sold_out?
115
+ quantity_enabled? ? (quantity_remaining <= 0) : false
116
+ end
117
+
118
+ end
@@ -0,0 +1,90 @@
1
+ module ActsAsSubscribable
2
+ extend ActiveSupport::Concern
3
+
4
+ mattr_accessor :descendants
5
+
6
+ module Base
7
+ def acts_as_subscribable(*options)
8
+ @acts_as_subscribable = options || []
9
+
10
+ # if table_exists?
11
+ # instance = new()
12
+ # raise 'must respond to trialing_until' unless instance.respond_to?(:trialing_until) || !EffectiveOrders.trial?
13
+ # raise 'must respond to subscription_status' unless instance.respond_to?(:subscription_status)
14
+ # end
15
+
16
+ include ::ActsAsSubscribable
17
+ (ActsAsSubscribable.descendants ||= []) << self
18
+ end
19
+ end
20
+
21
+ included do
22
+ has_one :subscription, as: :subscribable, class_name: 'Effective::Subscription', inverse_of: :subscribable
23
+ has_one :customer, through: :subscription, class_name: 'Effective::Customer'
24
+
25
+ before_validation(if: -> { EffectiveOrders.trial? && trialing_until.blank? }) do
26
+ self.trialing_until = (Time.zone.now + EffectiveOrders.trial.fetch(:length)).beginning_of_day
27
+ end
28
+
29
+ before_destroy(if: -> { subscribed? }) do
30
+ raise :abort unless (subscripter.destroy! rescue false)
31
+ end
32
+
33
+ if EffectiveOrders.trial?
34
+ validates :trialing_until, presence: true
35
+ end
36
+
37
+ validates :subscription_status, inclusion: { allow_nil: true, in: EffectiveOrders::STATUSES.keys }
38
+
39
+ scope :trialing, -> { where(subscription_status: nil).where('trialing_until > ?', Time.zone.now) }
40
+ scope :trial_past_due, -> { where(subscription_status: nil).where('trialing_until < ?', Time.zone.now) }
41
+ scope :not_trialing, -> { where.not(subscription_status: nil) }
42
+
43
+ scope :subscribed, -> { where(subscription_status: EffectiveOrders::ACTIVE) }
44
+ scope :subscription_past_due, -> { where(subscription_status: EffectiveOrders::PAST_DUE) }
45
+ scope :not_subscribed, -> { where(subscription_status: nil) }
46
+ end
47
+
48
+ module ClassMethods
49
+ end
50
+
51
+ def subscripter
52
+ @_effective_subscripter ||= begin
53
+ Effective::Subscripter.new(subscribable: self, user: subscribable_buyer, quantity: subscription&.quantity, stripe_plan_id: subscription&.stripe_plan_id)
54
+ end
55
+ end
56
+
57
+ def subscribed?(stripe_plan_id = nil)
58
+ return false if subscription_status.blank?
59
+ stripe_plan_id ? (subscription&.stripe_plan_id == stripe_plan_id) : true
60
+ end
61
+
62
+ def subscription_active?
63
+ subscribed? && subscription_status == EffectiveOrders::ACTIVE
64
+ end
65
+
66
+ def subscription_past_due?
67
+ subscribed? && subscription_status == EffectiveOrders::PAST_DUE
68
+ end
69
+
70
+ def trialing?
71
+ subscription_status.blank?
72
+ end
73
+
74
+ def trial_active?
75
+ trialing? && trialing_until > Time.zone.now
76
+ end
77
+
78
+ def trial_past_due?
79
+ trialing? && trialing_until < Time.zone.now
80
+ end
81
+
82
+ def subscribable_buyer
83
+ raise 'acts_as_subscribable object requires the subscribable_buyer method be defined to return the User buying this item.'
84
+ end
85
+
86
+ def subscribable_quantity_used
87
+ raise 'acts_as_subscribable object requires the subscribable_quantity_used method be defined to determine how many are in use.'
88
+ end
89
+
90
+ end
@@ -0,0 +1,49 @@
1
+ module ActsAsSubscribableBuyer
2
+ extend ActiveSupport::Concern
3
+
4
+ module Base
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] update customer: #{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
+ module ClassMethods
22
+ def after_invoice_payment_succeeded(&block)
23
+ send :define_method, :after_invoice_payment_succeeded do |event| self.instance_exec(event, &block) end
24
+ end
25
+
26
+ def after_invoice_payment_failed(&block)
27
+ send :define_method, :after_invoice_payment_failed do |event| self.instance_exec(event, &block) end
28
+ end
29
+
30
+ def after_customer_subscription_created(&block)
31
+ send :define_method, :after_customer_subscription_created do |event| self.instance_exec(event, &block) end
32
+ end
33
+
34
+ def after_customer_subscription_updated(&block)
35
+ send :define_method, :after_customer_subscription_updated do |event| self.instance_exec(event, &block) end
36
+ end
37
+
38
+ def after_customer_subscription_deleted(&block)
39
+ send :define_method, :after_customer_subscription_deleted do |event| self.instance_exec(event, &block) end
40
+ end
41
+
42
+ def after_customer_updated(&block)
43
+ send :define_method, :after_customer_updated do |event| self.instance_exec(event, &block) end
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
@@ -0,0 +1,17 @@
1
+ unless defined?(Effective::AccessDenied)
2
+ module Effective
3
+ class AccessDenied < StandardError
4
+ attr_reader :action, :subject
5
+
6
+ def initialize(message = nil, action = nil, subject = nil)
7
+ @message = message
8
+ @action = action
9
+ @subject = subject
10
+ end
11
+
12
+ def to_s
13
+ @message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,88 @@
1
+ module Effective
2
+ class Cart < ActiveRecord::Base
3
+ self.table_name = EffectiveOrders.carts_table_name.to_s
4
+
5
+ belongs_to :user, optional: true # 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'
7
+
8
+ accepts_nested_attributes_for :cart_items
9
+
10
+ # Attributes
11
+ # cart_items_count :integer
12
+ # timestamps
13
+
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)
20
+ raise 'expecting an acts_as_purchasable object' unless item.kind_of?(ActsAsPurchasable)
21
+
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
+ )
32
+
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
39
+ end
40
+
41
+ if item.quantity_enabled? && (existing ? existing.quantity : quantity) > item.quantity_remaining
42
+ raise EffectiveOrders::SoldOutException, "#{item.purchasable_name} is sold out"
43
+ end
44
+
45
+ existing ||= cart_items.build(purchasable: item, quantity: quantity, unique: (unique.to_s unless unique.kind_of?(Proc)))
46
+ save!
47
+ end
48
+
49
+ def clear!
50
+ cart_items.each { |cart_item| cart_item.mark_for_destruction }
51
+ cart_items.present? ? save! : true
52
+ end
53
+
54
+ def remove(item)
55
+ find(item).try(:mark_for_destruction)
56
+ save!
57
+ end
58
+
59
+ def includes?(item)
60
+ find(item).present?
61
+ end
62
+
63
+ def find(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 }
69
+ end
70
+
71
+ def size
72
+ cart_items_count || cart_items.length
73
+ end
74
+
75
+ def present?
76
+ size > 0
77
+ end
78
+
79
+ def blank?
80
+ size <= 0
81
+ end
82
+
83
+ def subtotal
84
+ cart_items.map { |ci| ci.subtotal }.sum
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,40 @@
1
+ module Effective
2
+ class CartItem < ActiveRecord::Base
3
+ self.table_name = EffectiveOrders.cart_items_table_name.to_s
4
+
5
+ belongs_to :cart, counter_cache: true, class_name: 'Effective::Cart'
6
+ belongs_to :purchasable, polymorphic: true
7
+
8
+ # Attributes
9
+ # quantity :integer
10
+ # timestamps
11
+
12
+ validates :purchasable, presence: true
13
+ validates :quantity, presence: true
14
+
15
+ def to_s
16
+ name || 'New Cart Item'
17
+ end
18
+
19
+ def name
20
+ purchasable&.purchasable_name
21
+ end
22
+
23
+ def price
24
+ if (purchasable.price || 0).kind_of?(Integer)
25
+ purchasable.price || 0
26
+ else
27
+ raise 'expected price to be an Integer representing the number of cents.'
28
+ end
29
+ end
30
+
31
+ def tax_exempt
32
+ purchasable&.tax_exempt || false
33
+ end
34
+
35
+ def subtotal
36
+ price * quantity
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,92 @@
1
+ module Effective
2
+ class Customer < ActiveRecord::Base
3
+ self.table_name = EffectiveOrders.customers_table_name.to_s
4
+
5
+ attr_accessor :stripe_customer
6
+
7
+ belongs_to :user
8
+ has_many :subscriptions, -> { includes(:subscribable) }, class_name: 'Effective::Subscription', foreign_key: 'customer_id'
9
+ accepts_nested_attributes_for :subscriptions
10
+
11
+ # Attributes
12
+ # stripe_customer_id :string # cus_xja7acoa03
13
+ # payment_method_id :string # Last payment method used
14
+ # active_card :string # **** **** **** 4242 Visa 05/12
15
+
16
+ # timestamps
17
+
18
+ scope :deep, -> { includes(subscriptions: :subscribable) }
19
+
20
+ validates :user, presence: true
21
+ validates :stripe_customer_id, presence: true
22
+
23
+ def self.for_user(user)
24
+ Effective::Customer.where(user: user).first_or_initialize
25
+ end
26
+
27
+ def to_s
28
+ user.to_s.presence || 'New Customer'
29
+ end
30
+
31
+ def email
32
+ user.email if user
33
+ end
34
+
35
+ def create_stripe_customer!
36
+ return if stripe_customer.present?
37
+ raise('expected a user') unless user.present?
38
+
39
+ Rails.logger.info "[STRIPE] create customer: #{user.email}"
40
+
41
+ self.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
42
+ self.stripe_customer_id = stripe_customer.id
43
+
44
+ save!
45
+ end
46
+
47
+ def stripe_customer
48
+ @stripe_customer ||= if stripe_customer_id.present?
49
+ Rails.logger.info "[STRIPE] get customer: #{stripe_customer_id}"
50
+ ::Stripe::Customer.retrieve(stripe_customer_id)
51
+ end
52
+ end
53
+
54
+ def invoices
55
+ @invoices ||= if stripe_customer_id.present?
56
+ Rails.logger.info "[STRIPE] list invoices: #{stripe_customer_id}"
57
+ ::Stripe::Invoice.list(customer: stripe_customer_id) rescue nil
58
+ end
59
+ end
60
+
61
+ def upcoming_invoice
62
+ @upcoming_invoice ||= if stripe_customer_id.present?
63
+ Rails.logger.info "[STRIPE] get upcoming invoice: #{stripe_customer_id}"
64
+ ::Stripe::Invoice.upcoming(customer: stripe_customer_id) rescue nil
65
+ end
66
+ end
67
+
68
+ def token_required?
69
+ active_card.blank? || past_due?
70
+ end
71
+
72
+ def past_due?
73
+ subscriptions.any? { |subscription| subscription.past_due? }
74
+ end
75
+
76
+ def active?
77
+ subscriptions.present? && subscriptions.all? { |subscription| subscription.active? }
78
+ end
79
+
80
+ def payment_status
81
+ if past_due?
82
+ 'We ran into an error processing your last payment. Please update or confirm your card details to continue.'
83
+ elsif active?
84
+ "Your payment is in good standing. Thanks so much for your support!"
85
+ elsif active_card.blank?
86
+ 'No credit card on file. Please add a card.'
87
+ else
88
+ 'Please update or confirm your card details to continue.'
89
+ end.html_safe
90
+ end
91
+ end
92
+ end