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
@@ -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