effective_orders 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
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