effective_orders 2.2.4 → 3.0.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 (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
@@ -2,34 +2,32 @@ module Effective
2
2
  class OrderItem < ActiveRecord::Base
3
3
  self.table_name = EffectiveOrders.order_items_table_name.to_s
4
4
 
5
- belongs_to :order
6
- belongs_to :purchasable, :polymorphic => true
7
- belongs_to :seller, :class_name => 'User'
5
+ belongs_to :order, class_name: 'Effective::Order'
6
+ belongs_to :purchasable, polymorphic: true
7
+ belongs_to :seller, class_name: 'User'
8
8
 
9
- delegate :purchased_download_url, :to => :purchasable
10
- delegate :purchased?, :declined?, :to => :order
9
+ delegate :purchased_download_url, to: :purchasable
10
+ delegate :purchased?, :declined?, to: :order
11
11
 
12
- # structure do
13
- # title :string
14
- # quantity :integer
15
- # price :integer, default: 0
16
- # tax_exempt :boolean
17
- #
18
- # timestamps
19
- # end
12
+ # Attributes
13
+ # title :string
14
+ # quantity :integer
15
+ # price :integer, default: 0
16
+ # tax_exempt :boolean
17
+ # timestamps
20
18
 
21
19
  validates :purchasable, associated: true, presence: true
22
20
  accepts_nested_attributes_for :purchasable, allow_destroy: false, reject_if: :all_blank, update_only: true
23
21
 
24
22
  validates :title, presence: true
25
23
  validates :quantity, presence: true, numericality: { greater_than: 0 }
26
- validates :price, numericality: true
24
+ validates :price, presence: true
27
25
  validates :tax_exempt, inclusion: { in: [true, false] }
28
26
 
29
- validates :seller_id, presence: true, if: Proc.new { |order_item| EffectiveOrders.stripe_connect_enabled }
27
+ validates :seller_id, presence: true, if: -> { EffectiveOrders.stripe_connect_enabled }
30
28
 
31
- scope :sold, -> { joins(:order).where(:orders => {:purchase_state => EffectiveOrders::PURCHASED}) }
32
- scope :sold_by, lambda { |user| sold().where(:seller_id => user.try(:id)) }
29
+ scope :sold, -> { joins(:order).where(orders: { purchase_state: EffectiveOrders::PURCHASED }) }
30
+ scope :sold_by, lambda { |user| sold().where(seller_id: user.id) }
33
31
 
34
32
  def to_s
35
33
  (quantity || 0) > 1 ? "#{quantity}x #{title}" : title
@@ -57,8 +55,7 @@ module Effective
57
55
  elsif value.kind_of?(String) && !value.include?('.') # Looks like an integer
58
56
  super
59
57
  else # Could be Float, BigDecimal, or String like 9.99
60
- ActiveSupport::Deprecation.warn('order_item.price= was passed a non-integer. Expecting an Integer representing the number of cents. Continuing with (price * 100.0).round(0).to_i conversion') unless EffectiveOrders.silence_deprecation_warnings
61
- super((value.to_f * 100.0).to_i)
58
+ raise 'expected price to be an Integer representing the number of cents.'
62
59
  end
63
60
  end
64
61
 
@@ -66,13 +63,13 @@ module Effective
66
63
  # And is the Customer representing who is selling the product
67
64
  # This is really only used for StripeConnect
68
65
  def seller
69
- @seller ||= Effective::Customer.for_user(purchasable.try(:seller))
66
+ @seller ||= Effective::Customer.for_buyer(purchasable.seller)
70
67
  end
71
68
 
72
69
  def stripe_connect_application_fee
73
70
  @stripe_connect_application_fee ||= (
74
71
  self.instance_exec(self, &EffectiveOrders.stripe_connect_application_fee_method).to_i.tap do |fee|
75
- raise ArgumentError.new("expected EffectiveOrders.stripe_connect_application_fee_method to return a value between 0 and the order_item total (#{self.total}). Received #{fee}.") if (fee > total || fee < 0)
72
+ raise "expected EffectiveOrders.stripe_connect_application_fee_method to return a value between 0 and the order_item total (#{self.total}). Received #{fee}." if (fee > total || fee < 0)
76
73
  end
77
74
  )
78
75
  end
@@ -4,16 +4,16 @@ module Effective
4
4
 
5
5
  acts_as_purchasable
6
6
 
7
- # structure do
8
- # title :string
9
- # price :integer, default: 0
10
- # tax_exempt :boolean, default: false
7
+ # Attributes
8
+ # title :string
9
+ # price :integer, default: 0
10
+ # tax_exempt :boolean, default: false
11
11
  #
12
- # timestamps
13
- # end
12
+ # timestamps
14
13
 
15
14
  validates :title, presence: true
16
- validates :price, numericality: { greater_than: 0 }
15
+ validates :price, presence: true
16
+ validates :tax_exempt, inclusion: { in: [true, false] }
17
17
 
18
18
  def to_s
19
19
  self[:title] || 'New Product'
@@ -20,7 +20,7 @@ class Effective::Providers::CcbillPostback
20
20
 
21
21
  def denial?
22
22
  # denialId is for subscriptions only
23
- [:reasonForDeclineCode, :reasonForDecline, :denialId].any? {|key| params[key].present?}
23
+ [:reasonForDeclineCode, :reasonForDecline, :denialId].any? { |key| params[key].present? }
24
24
  end
25
25
 
26
26
  def order_details
@@ -1,34 +1,23 @@
1
1
  # This is an object for the stripe charge form
2
2
  module Effective::Providers
3
3
  class StripeCharge
4
- extend ActiveModel::Naming
5
- include ActiveModel::Conversion
6
- include ActiveModel::Validations
7
- include ActiveRecord::Reflection
4
+ include ActiveModel::Model
8
5
 
9
- attr_accessor :effective_order_id, :order, :token # For our form
6
+ attr_accessor :effective_order_id, :order, :stripe_token # For our form
10
7
 
11
- validates_presence_of :effective_order_id, :token
12
-
13
- def initialize(params = {})
14
- if params.kind_of?(Effective::Order)
15
- @order = params
16
- @effective_order_id = params.to_param
17
- else
18
- params.each { |k, v| self.send("#{k}=", v) if self.respond_to?("#{k}=") }
19
- end
20
- end
8
+ validates :effective_order_id, presence: true
9
+ validates :stripe_token, presence: true
21
10
 
22
11
  def persisted?
23
12
  false
24
13
  end
25
14
 
26
- def order_items
27
- order.order_items.reject { |order_item| order_item.purchasable.kind_of?(Effective::Subscription) }
15
+ def effective_order_id
16
+ @effective_order_id || (order.to_param if order)
28
17
  end
29
18
 
30
- def subscriptions
31
- order.order_items.select { |order_item| order_item.purchasable.kind_of?(Effective::Subscription) }.map(&:purchasable)
19
+ def order_items
20
+ order.order_items if order
32
21
  end
33
22
 
34
23
  end
@@ -0,0 +1,230 @@
1
+ # Form object to select a plan, and build the correct subscription and customer
2
+
3
+ module Effective
4
+ class Subscripter
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor :user, :subscribable, :customer
8
+ attr_accessor :stripe_plan_id, :stripe_token
9
+
10
+ validates :user, presence: true
11
+ validates :subscribable, presence: true, if: -> { stripe_plan_id }
12
+ validates :stripe_plan_id, inclusion: { allow_blank: true, in: EffectiveOrders.stripe_plans.keys, message: 'unknown plan' }
13
+
14
+ validate(if: -> { stripe_plan_id && plan && subscribable }) do
15
+ if plan[:amount] > 0 && stripe_token.blank? && token_required?
16
+ self.errors.add(:stripe_token, 'updated payment card required')
17
+ customer.errors.add(:stripe_token, 'updated payment card required')
18
+ end
19
+ end
20
+
21
+ validate(if: -> { subscribable }) do
22
+ subscribable.errors.add(:subscripter, errors.messages.values.flatten.to_sentence) if self.errors.present?
23
+ end
24
+
25
+ def customer
26
+ @customer ||= Effective::Customer.deep.where(user: user).first_or_initialize
27
+ end
28
+
29
+ def current_plan
30
+ return nil unless subscribable
31
+ subscribable.subscription.blank? ? EffectiveOrders.stripe_plans['trial'] : subscribable.subscription.plan
32
+ end
33
+
34
+ def plan
35
+ EffectiveOrders.stripe_plans[stripe_plan_id]
36
+ end
37
+
38
+ def token_required?
39
+ customer.token_required?
40
+ end
41
+
42
+ def save!
43
+ return true if (plan == current_plan) && stripe_token.blank? # No work to do
44
+
45
+ raise 'is invalid' unless valid?
46
+
47
+ begin
48
+ build! && sync! && customer.save!
49
+ rescue => e
50
+ reload!
51
+
52
+ self.errors.add(:base, e.message)
53
+ raise e
54
+ end
55
+ end
56
+
57
+ def subscribe!(stripe_plan_id)
58
+ self.stripe_plan_id = stripe_plan_id
59
+ save!
60
+ end
61
+
62
+ def destroy!
63
+ return true unless subscription && subscription.persisted? && customer.stripe_subscription.present?
64
+
65
+ raise 'is invalid' unless valid?
66
+
67
+ subscription.destroy!
68
+ customer.subscriptions.reload
69
+
70
+ sync! && customer.save!
71
+ end
72
+
73
+ def reload!
74
+ @stripe_token = nil
75
+ @stripe_plan_id = nil
76
+ @customer = nil
77
+ @subscription = nil
78
+ end
79
+
80
+ private
81
+
82
+ def subscription
83
+ return nil unless subscribable
84
+ @subscription ||= (
85
+ customer.subscriptions.find { |sub| sub.subscribable == subscribable } ||
86
+ customer.subscriptions.build(subscribable: subscribable, customer: customer)
87
+ )
88
+ end
89
+
90
+ def build!
91
+ # Check for an existing customer created within the last hour with this email
92
+ # This catches an edge case in which a stripe customer is created twice when a first time customer is using a declined card
93
+ if customer.stripe_customer.blank?
94
+ Rails.logger.info "STRIPE CUSTOMER CHECK FOR EXISTING: #{user.email}"
95
+
96
+ customers = Stripe::Customer.list(created: { gt: (user.created_at - 1.hour).to_i } ).data
97
+
98
+ if (existing = customers.find { |cus| cus.email == user.email && (cus.metadata || {})[:user_id] == user.id.to_s })
99
+ customer.stripe_customer = existing
100
+ customer.stripe_customer_id = existing.id
101
+ end
102
+ end
103
+
104
+ # Create customer
105
+ if customer.stripe_customer.blank?
106
+ Rails.logger.info "STRIPE CUSTOMER CREATE: #{user.email}"
107
+ customer.stripe_customer = Stripe::Customer.create(email: user.email, description: user.to_s, metadata: { user_id: user.id })
108
+ customer.stripe_customer_id = customer.stripe_customer.id
109
+ end
110
+
111
+ # Update stripe customer card
112
+ if stripe_token.present?
113
+ Rails.logger.info "STRIPE CUSTOMER SOURCE UPDATE #{stripe_token}"
114
+ customer.stripe_customer.source = stripe_token
115
+ customer.stripe_customer.save
116
+
117
+ if customer.stripe_customer.default_source.present?
118
+ card = customer.stripe_customer.sources.retrieve(customer.stripe_customer.default_source)
119
+ customer.active_card = "**** **** **** #{card.last4} #{card.brand} #{card.exp_month}/#{card.exp_year}"
120
+ end
121
+ end
122
+
123
+ # Assign stripe plan
124
+ if plan
125
+ subscription.stripe_plan_id = plan[:id]
126
+
127
+ # Ensure stripe subscription exists
128
+ if customer.stripe_subscription.blank?
129
+ Rails.logger.info "STRIPE SUBSCRIPTION CREATE: #{items(metadata: false)}"
130
+ customer.stripe_subscription = Stripe::Subscription.create(customer: customer.stripe_customer_id, items: items(metadata: false), metadata: metadata)
131
+ customer.stripe_subscription_id = customer.stripe_subscription.id
132
+ end
133
+ end
134
+
135
+ true
136
+ end
137
+
138
+ def sync!
139
+ return true unless plan && customer.stripe_subscription
140
+
141
+ Rails.logger.info "STRIPE SUBSCRIPTION SYNC: #{customer.stripe_subscription_id} #{items}"
142
+
143
+ if items.length == 0
144
+ customer.stripe_subscription.delete
145
+ customer.stripe_subscription_id = nil
146
+ customer.status = nil
147
+ return true
148
+ end
149
+
150
+ # Update stripe subscription items
151
+ customer.stripe_subscription.items.each do |stripe_item|
152
+ item = items.find { |item| item[:plan] == stripe_item['plan']['id'] }
153
+
154
+ next if item.blank? || item[:quantity] == stripe_item['quantity']
155
+
156
+ stripe_item.quantity = item[:quantity]
157
+ stripe_item.metadata = item[:metadata]
158
+
159
+ Rails.logger.info " -> UPDATE: #{item[:plan]}"
160
+ stripe_item.save
161
+ end
162
+
163
+ # Create stripe subscription items
164
+ items.each do |item|
165
+ next if customer.stripe_subscription.items.find { |stripe_item| item[:plan] == stripe_item['plan']['id'] }
166
+
167
+ Rails.logger.info " -> CREATE: #{item[:plan]}"
168
+ customer.stripe_subscription.items.create(plan: item[:plan], quantity: item[:quantity], metadata: item[:metadata])
169
+ end
170
+
171
+ # Delete stripe subscription items
172
+ customer.stripe_subscription.items.each do |stripe_item|
173
+ next if items.find { |item| item[:plan] == stripe_item['plan']['id'] }
174
+
175
+ Rails.logger.info " -> DELETE: #{stripe_item['plan']['id']}"
176
+ stripe_item.delete
177
+ end
178
+
179
+ # Update metadata
180
+ if customer.stripe_subscription.metadata.to_h != metadata
181
+ Rails.logger.info " -> METATADA: #{metadata}"
182
+ customer.stripe_subscription.metadata = metadata
183
+ customer.stripe_subscription.save
184
+ end
185
+
186
+ # When upgrading a plan, invoice immediately.
187
+ if current_plan && current_plan[:id] != 'trial' && plan[:amount] > current_plan[:amount]
188
+ Rails.logger.info " -> INVOICE GENERATED"
189
+ Stripe::Invoice.create(customer: customer.stripe_customer_id).pay rescue false
190
+ end
191
+
192
+ # Sync status
193
+ customer.status = customer.stripe_subscription.status
194
+
195
+ true
196
+ end
197
+
198
+ private
199
+
200
+ def items(metadata: true)
201
+ customer.subscriptions.group_by { |sub| sub.stripe_plan_id }.map do |plan, subscriptions|
202
+ if metadata
203
+ { plan: plan, quantity: subscriptions.length, metadata: metadata(subscriptions: subscriptions) }
204
+ else
205
+ { plan: plan, quantity: subscriptions.length }
206
+ end
207
+ end
208
+ end
209
+
210
+ # The stripe metadata limit is 500 characters
211
+ def metadata(subscriptions: nil)
212
+ retval = { user_id: user.id.to_s, user: user.to_s.truncate(500) }
213
+
214
+ (subscriptions || customer.subscriptions).group_by { |sub| sub.subscribable_type }.each do |subscribable_type, subs|
215
+ subs = subs.sort
216
+
217
+ if subs.length == 1
218
+ retval[subscribable_type.downcase + '_id'] = subs.map { |sub| sub.subscribable.id }.join(',')
219
+ retval[subscribable_type.downcase] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
220
+ else
221
+ retval[subscribable_type.downcase + '_ids'] = subs.map { |sub| sub.subscribable.id }.join(',')
222
+ retval[subscribable_type.downcase.pluralize] = subs.map { |sub| sub.subscribable.to_s }.join(',').truncate(500)
223
+ end
224
+ end
225
+
226
+ retval
227
+ end
228
+
229
+ end
230
+ end
@@ -1,98 +1,49 @@
1
+ # This links the acts_as_subscribable_buyer (customer) to the acts_as_subscribable (subscribable)
2
+
1
3
  module Effective
2
4
  class Subscription < ActiveRecord::Base
3
- include EffectiveStripeHelper
4
-
5
5
  self.table_name = EffectiveOrders.subscriptions_table_name.to_s
6
6
 
7
- acts_as_purchasable
7
+ belongs_to :customer, class_name: 'Effective::Customer', counter_cache: true
8
+ belongs_to :subscribable, polymorphic: true
8
9
 
9
- belongs_to :customer
10
-
11
- # structure do
12
- # stripe_plan_id :string # This will be 'Weekly' or something like that
13
- # stripe_subscription_id :string
14
- # stripe_coupon_id :string
10
+ # Attributes
11
+ # stripe_plan_id :string # This will be 'bronze' or something like that
12
+ # status :string
15
13
  #
16
- # title :string
17
- # price :integer, default: 0
14
+ # name :string
15
+ # price :integer, default: 0
18
16
  #
19
- # timestamps
20
- # end
21
-
22
- delegate :user, :user_id, :to => :customer
23
-
24
- validates :stripe_plan_id, presence: true
25
- validates :title, presence: true
26
- validates :price, numericality: { greater_than: 0 }
27
-
28
- validates :customer, presence: true
29
- validates :customer_id, uniqueness: { scope: [:stripe_plan_id] } # Can only be on each plan once.
30
-
31
- before_validation do
32
- self.errors.add(:stripe_plan_id, "is an invalid Plan") if stripe_plan_id.present? && stripe_plan.blank?
33
- self.errors.add(:stripe_coupon_id, "is an invalid Coupon") if stripe_coupon_id.present? && stripe_coupon.blank?
34
- end
35
-
36
- def tax_exempt
37
- true
38
- end
39
-
40
- def stripe_plan_id=(plan_id)
41
- unless self[:stripe_plan_id] == plan_id
42
- self[:stripe_plan_id] = plan_id
43
- @stripe_plan = nil # Remove any memoization
17
+ # timestamps
44
18
 
45
- assign_price_and_title()
46
- end
19
+ before_validation(if: -> { plan && (stripe_plan_id_changed? || new_record?) }) do
20
+ self.name = "#{plan[:name]} #{plan[:description]}"
21
+ self.price = plan[:amount]
47
22
  end
48
23
 
49
- def stripe_coupon_id=(coupon_id)
50
- unless self[:stripe_coupon_id] == coupon_id
51
- self[:stripe_coupon_id] = coupon_id
52
- @stripe_coupon = nil # Remove any memoization
24
+ validates :customer, presence: true
25
+ validates :subscribable, presence: true
53
26
 
54
- assign_price_and_title()
55
- end
56
- end
27
+ validates :stripe_plan_id, presence: true, inclusion: { in: EffectiveOrders.stripe_plans.except('trial').keys }
28
+ validates :status, presence: true, inclusion: { in: %w(active past_due) }
57
29
 
58
- def stripe_plan
59
- if stripe_plan_id.present?
60
- @stripe_plan ||= (Stripe::Plan.retrieve(stripe_plan_id) rescue nil)
61
- end
62
- end
30
+ validates :name, presence: true
31
+ validates :price, numericality: { greater_than_or_equal_to: 0, only_integer: true }
63
32
 
64
- def stripe_coupon
65
- if stripe_coupon_id.present?
66
- @stripe_coupon ||= (Stripe::Coupon.retrieve(stripe_coupon_id) rescue nil)
67
- end
33
+ def to_s
34
+ name.presence || 'New Subscription'
68
35
  end
69
36
 
70
- def stripe_subscription
71
- if stripe_subscription_id.present?
72
- @stripe_subscription ||= (customer.stripe_customer.subscriptions.retrieve(stripe_subscription_id) rescue nil)
73
- end
37
+ def plan
38
+ EffectiveOrders.stripe_plans[stripe_plan_id]
74
39
  end
75
40
 
76
- private
77
-
78
- def assign_price_and_title
79
- if stripe_plan
80
- if stripe_coupon
81
- self.price = price_with_coupon(stripe_plan.amount, stripe_coupon)
82
- self.title = stripe_plan_description(stripe_plan) + '<br>Coupon Code: ' + stripe_coupon_description(stripe_coupon)
83
- else
84
- self.title = stripe_plan_description(stripe_plan)
85
- self.price = stripe_plan.amount
86
- end
87
- end
41
+ def active?
42
+ status == 'active'
88
43
  end
89
44
 
90
- def price_with_coupon(amount, coupon)
91
- if coupon.percent_off.present?
92
- (amount * (coupon.percent_off.to_i / 100.0)).round(0).to_i
93
- else
94
- [0, amount - coupon.amount_off].max
95
- end
45
+ def <=>(other)
46
+ (name || '') <=> (other.try(:name) || '')
96
47
  end
97
48
 
98
49
  end