freemium 0.0.1

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 (90) hide show
  1. data/.gitignore +53 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +121 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.rdoc +67 -0
  6. data/Rakefile +23 -0
  7. data/autotest/discover.rb +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/freemium.gemspec +28 -0
  10. data/lib/freemium/address.rb +18 -0
  11. data/lib/freemium/coupon.rb +38 -0
  12. data/lib/freemium/coupon_redemption.rb +48 -0
  13. data/lib/freemium/credit_card.rb +273 -0
  14. data/lib/freemium/feature_set.rb +45 -0
  15. data/lib/freemium/gateways/base.rb +65 -0
  16. data/lib/freemium/gateways/brain_tree.rb +175 -0
  17. data/lib/freemium/gateways/test.rb +34 -0
  18. data/lib/freemium/manual_billing.rb +73 -0
  19. data/lib/freemium/railtie.tb +7 -0
  20. data/lib/freemium/rates.rb +33 -0
  21. data/lib/freemium/recurring_billing.rb +59 -0
  22. data/lib/freemium/response.rb +24 -0
  23. data/lib/freemium/subscription.rb +350 -0
  24. data/lib/freemium/subscription_change.rb +20 -0
  25. data/lib/freemium/subscription_mailer/admin_report.rhtml +4 -0
  26. data/lib/freemium/subscription_mailer/expiration_notice.rhtml +1 -0
  27. data/lib/freemium/subscription_mailer/expiration_warning.rhtml +1 -0
  28. data/lib/freemium/subscription_mailer/invoice.text.plain.erb +5 -0
  29. data/lib/freemium/subscription_mailer.rb +36 -0
  30. data/lib/freemium/subscription_plan.rb +32 -0
  31. data/lib/freemium/transaction.rb +15 -0
  32. data/lib/freemium/version.rb +3 -0
  33. data/lib/freemium.rb +75 -0
  34. data/lib/generators/active_record/freemium_generator.rb +28 -0
  35. data/lib/generators/active_record/templates/migrations/account_transactions.rb +17 -0
  36. data/lib/generators/active_record/templates/migrations/coupon_redemptions.rb +18 -0
  37. data/lib/generators/active_record/templates/migrations/coupons.rb +28 -0
  38. data/lib/generators/active_record/templates/migrations/credit_cards.rb +14 -0
  39. data/lib/generators/active_record/templates/migrations/subscription_changes.rb +18 -0
  40. data/lib/generators/active_record/templates/migrations/subscription_plans.rb +14 -0
  41. data/lib/generators/active_record/templates/migrations/subscriptions.rb +30 -0
  42. data/lib/generators/active_record/templates/models/account_transaction.rb +3 -0
  43. data/lib/generators/active_record/templates/models/coupon.rb +3 -0
  44. data/lib/generators/active_record/templates/models/coupon_redemption.rb +3 -0
  45. data/lib/generators/active_record/templates/models/credit_card.rb +3 -0
  46. data/lib/generators/active_record/templates/models/subscription.rb +3 -0
  47. data/lib/generators/active_record/templates/models/subscription_change.rb +3 -0
  48. data/lib/generators/active_record/templates/models/subscription_plan.rb +3 -0
  49. data/lib/generators/freemium/freemium_generator.rb +15 -0
  50. data/lib/generators/freemium/install_generator.rb +28 -0
  51. data/lib/generators/freemium/orm_helpers.rb +27 -0
  52. data/lib/generators/templates/freemium.rb +43 -0
  53. data/lib/generators/templates/freemium_feature_sets.yml +5 -0
  54. data/spec/dummy/Rakefile +7 -0
  55. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  56. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  57. data/spec/dummy/app/models/models.rb +32 -0
  58. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  59. data/spec/dummy/config/application.rb +45 -0
  60. data/spec/dummy/config/boot.rb +10 -0
  61. data/spec/dummy/config/database.yml +22 -0
  62. data/spec/dummy/config/environment.rb +5 -0
  63. data/spec/dummy/config/environments/development.rb +26 -0
  64. data/spec/dummy/config/environments/production.rb +49 -0
  65. data/spec/dummy/config/environments/test.rb +35 -0
  66. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/spec/dummy/config/initializers/inflections.rb +10 -0
  68. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  69. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  70. data/spec/dummy/config/initializers/session_store.rb +8 -0
  71. data/spec/dummy/config/locales/en.yml +5 -0
  72. data/spec/dummy/config/routes.rb +58 -0
  73. data/spec/dummy/config.ru +4 -0
  74. data/spec/dummy/db/schema.rb +92 -0
  75. data/spec/dummy/script/rails +6 -0
  76. data/spec/fixtures/credit_cards.yml +11 -0
  77. data/spec/fixtures/subscription_plans.yml +18 -0
  78. data/spec/fixtures/subscriptions.yml +29 -0
  79. data/spec/fixtures/users.yml +16 -0
  80. data/spec/freemium_feature_sets.yml +9 -0
  81. data/spec/freemium_spec.rb +4 -0
  82. data/spec/models/coupon_redemption_spec.rb +235 -0
  83. data/spec/models/credit_card_spec.rb +114 -0
  84. data/spec/models/manual_billing_spec.rb +174 -0
  85. data/spec/models/recurring_billing_spec.rb +92 -0
  86. data/spec/models/subscription_plan_spec.rb +44 -0
  87. data/spec/models/subscription_spec.rb +386 -0
  88. data/spec/spec_helper.rb +38 -0
  89. data/spec/support/helpers.rb +21 -0
  90. metadata +298 -0
@@ -0,0 +1,33 @@
1
+ module Freemium
2
+ module Rates
3
+
4
+ # returns the daily cost of this plan.
5
+ def daily_rate(options = {})
6
+ yearly_rate(options) / 365
7
+ end
8
+
9
+ # returns the yearly cost of this plan.
10
+ def yearly_rate(options = {})
11
+ begin
12
+ rate(options) * 12
13
+ rescue
14
+ rate * 12
15
+ end
16
+ end
17
+
18
+ # returns the monthly cost of this plan.
19
+ def monthly_rate(options = {})
20
+ begin
21
+ rate(options)
22
+ rescue
23
+ rate
24
+ end
25
+ end
26
+
27
+ def paid?
28
+ return false unless rate
29
+ rate.cents > 0
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ module Freemium
2
+ # adds recurring billing functionality to the Subscription class
3
+ module RecurringBilling
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ # the process you should run periodically
10
+ def run_billing
11
+ # first, synchronize transactions
12
+ transactions = process_transactions
13
+
14
+ # then, set expiration for any subscriptions that didn't process
15
+ find_expirable.each(&:expire_after_grace!)
16
+ # then, actually expire any subscriptions whose time has come
17
+ expire
18
+
19
+ # send the activity report
20
+ Freemium.mailer.deliver_admin_report(
21
+ transactions
22
+ ) if Freemium.admin_report_recipients && !new_transactions.empty?
23
+ end
24
+
25
+ protected
26
+
27
+ # retrieves all transactions posted after the last known transaction
28
+ #
29
+ # please note how this works: it calculates the maximum last_transaction_at
30
+ # value and only retrieves transactions after that. so be careful that you
31
+ # don't accidentally update the last_transaction_at field for some subset
32
+ # of subscriptions, and leave the others behind!
33
+ def new_transactions
34
+ Freemium.gateway.transactions(:after => self.maximum(:last_transaction_at))
35
+ end
36
+
37
+ # updates all subscriptions with any new transactions
38
+ def process_transactions(transactions = new_transactions)
39
+ transaction do
40
+ transactions.each do |t|
41
+ subscription = ::Subscription.find_by_billing_key(t.billing_key)
42
+ subscription.transactions << t
43
+ t.success? ? subscription.receive_payment!(t) : subscription.expire_after_grace!(t)
44
+ end
45
+ end
46
+ transactions
47
+ end
48
+
49
+ # finds all subscriptions that should have paid but didn't and need to be expired
50
+ # because of coupons we can't trust rate_cents alone and need to verify that the account is indeed paid?
51
+ def find_expirable
52
+ paid.
53
+ where(['paid_through < ?', Date.today]).
54
+ where('(expire_on IS NULL OR expire_on < paid_through)').
55
+ select { |s| s.paid? }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,24 @@
1
+ module Freemium
2
+ # used to encapsulate the success/failure/details of a response from some gateway.
3
+ # intended to be independent of the details of communication (e.g. Freemium::Gateways::BrainTree::Post).
4
+ class Response
5
+ # a gateway-specific hash of raw data related to the request.
6
+ attr_reader :raw_data
7
+ # may contain a description of the response. should contain an explanation if the response was not a success.
8
+ attr_accessor :message
9
+ # the related billing key, if appropriate
10
+ attr_accessor :billing_key
11
+
12
+ def initialize(success, raw_data = {})
13
+ @success, @raw_data = success, raw_data
14
+ end
15
+
16
+ def success?
17
+ @success
18
+ end
19
+
20
+ def [](key)
21
+ raw_data[key]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,350 @@
1
+ # == Attributes
2
+ # subscribable: the model in your system that has the subscription. probably a User.
3
+ # subscription_plan: which service plan this subscription is for. affects how payment is interpreted.
4
+ # paid_through: when the subscription currently expires, assuming no further payment. for manual billing, this also determines when the next payment is due.
5
+ # billing_key: the id for this user in the remote billing gateway. may not exist if user is on a free plan.
6
+ # last_transaction_at: when the last gateway transaction was for this account. this is used by your gateway to find "new" transactions.
7
+ #
8
+ module Freemium
9
+ module Subscription
10
+ include Rates
11
+
12
+ def self.included(base)
13
+ base.class_eval do
14
+ belongs_to :subscription_plan, :class_name => "SubscriptionPlan"
15
+ belongs_to :subscribable, :polymorphic => true
16
+ belongs_to :credit_card, :dependent => :destroy, :class_name => "CreditCard"
17
+ has_many :coupon_redemptions, :conditions => "coupon_redemptions.expired_on IS NULL", :class_name => "CouponRedemption", :foreign_key => :subscription_id, :dependent => :destroy
18
+ has_many :coupons, :through => :coupon_redemptions, :conditions => "coupon_redemptions.expired_on IS NULL"
19
+
20
+ # Auditing
21
+ has_many :transactions, :class_name => "AccountTransaction", :foreign_key => :subscription_id
22
+
23
+ scope :paid, includes(:subscription_plan).where("subscription_plans.rate_cents > 0")
24
+ scope :due, lambda {
25
+ where(['paid_through <= ?', Date.today]) # could use the concept of a next retry date
26
+ }
27
+ scope :expired, lambda {
28
+ where(['expire_on >= paid_through AND expire_on <= ?', Date.today])
29
+ }
30
+
31
+ before_validation :set_paid_through
32
+ before_validation :set_started_on
33
+ before_save :store_credit_card_offsite
34
+ before_save :discard_credit_card_unless_paid
35
+ before_destroy :cancel_in_remote_system
36
+
37
+ after_create :audit_create
38
+ after_update :audit_update
39
+ after_destroy :audit_destroy
40
+
41
+ validates_presence_of :subscribable
42
+ validates_associated :subscribable
43
+ validates_presence_of :subscription_plan
44
+ validates_presence_of :paid_through, :if => :paid?
45
+ validates_presence_of :started_on
46
+ validates_presence_of :credit_card, :if => :store_credit_card?
47
+ validates_associated :credit_card#, :if => :store_credit_card?
48
+
49
+ validate :gateway_validates_credit_card
50
+ validate :coupon_exist
51
+ end
52
+ base.extend ClassMethods
53
+ end
54
+
55
+ def original_plan
56
+ @original_plan ||= ::SubscriptionPlan.find_by_id(subscription_plan_id_was) unless subscription_plan_id_was.nil?
57
+ end
58
+
59
+ def gateway
60
+ Freemium.gateway
61
+ end
62
+
63
+
64
+ protected
65
+
66
+ ##
67
+ ## Validations
68
+ ##
69
+
70
+ def gateway_validates_credit_card
71
+ if credit_card && credit_card.changed? && credit_card.valid?
72
+ response = gateway.validate(credit_card, credit_card.address)
73
+ unless response.success?
74
+ errors.add(:base, "Credit card could not be validated: #{response.message}")
75
+ end
76
+ end
77
+ end
78
+
79
+ ##
80
+ ## Callbacks
81
+ ##
82
+
83
+ def set_paid_through
84
+ if subscription_plan_id_changed? && !paid_through_changed?
85
+ if paid?
86
+ if new_record?
87
+ # paid + new subscription = in free trial
88
+ self.paid_through = Date.today + Freemium.days_free_trial
89
+ self.in_trial = true
90
+ elsif !self.in_trial? && self.original_plan && self.original_plan.paid?
91
+ # paid + not in trial + not new subscription + original sub was paid = calculate and credit for remaining value
92
+ value = self.remaining_value(original_plan)
93
+ self.paid_through = Date.today
94
+ self.credit(value)
95
+ else
96
+ # otherwise payment is due today
97
+ self.paid_through = Date.today
98
+ self.in_trial = false
99
+ end
100
+ else
101
+ # free plans don't pay
102
+ self.paid_through = nil
103
+ end
104
+ end
105
+ true
106
+ end
107
+
108
+ def set_started_on
109
+ self.started_on = Date.today if subscription_plan_id_changed?
110
+ end
111
+
112
+ # Simple assignment of a credit card. Note that this may not be
113
+ # useful for your particular situation, especially if you need
114
+ # to simultaneously set up automated recurrences.
115
+ #
116
+ # Because of the third-party interaction with the gateway, you
117
+ # need to be careful to only use this method when you expect to
118
+ # be able to save the record successfully. Otherwise you may end
119
+ # up storing a credit card in the gateway and then losing the key.
120
+ #
121
+ # NOTE: Support for updating an address could easily be added
122
+ # with an "address" property on the credit card.
123
+ def store_credit_card_offsite
124
+ if credit_card && credit_card.changed? && credit_card.valid?
125
+ response = billing_key ? gateway.update(billing_key, credit_card, credit_card.address) : gateway.store(credit_card, credit_card.address)
126
+ raise Freemium::CreditCardStorageError.new(response.message) unless response.success?
127
+ self.billing_key = response.billing_key
128
+ self.expire_on = nil
129
+ self.credit_card.reload # to prevent needless subsequent store() calls
130
+ end
131
+ end
132
+
133
+ def discard_credit_card_unless_paid
134
+ unless store_credit_card?
135
+ destroy_credit_card
136
+ end
137
+ end
138
+
139
+ def destroy_credit_card
140
+ credit_card.destroy if credit_card
141
+ cancel_in_remote_system
142
+ end
143
+
144
+ def cancel_in_remote_system
145
+ if billing_key
146
+ gateway.cancel(self.billing_key)
147
+ self.billing_key = nil
148
+ end
149
+ end
150
+
151
+ ##
152
+ ## Callbacks :: Auditing
153
+ ##
154
+
155
+ def audit_create
156
+ ::SubscriptionChange.create(:reason => "new",
157
+ :subscribable => self.subscribable,
158
+ :new_subscription_plan_id => self.subscription_plan_id,
159
+ :new_rate => self.rate,
160
+ :original_rate => Money.empty)
161
+ end
162
+
163
+ def audit_update
164
+ if self.subscription_plan_id_changed?
165
+ return if self.original_plan.nil?
166
+ reason = self.original_plan.rate > self.subscription_plan.rate ? (self.expired? ? "expiration" : "downgrade") : "upgrade"
167
+ ::SubscriptionChange.create(:reason => reason,
168
+ :subscribable => self.subscribable,
169
+ :original_subscription_plan_id => self.original_plan.id,
170
+ :original_rate => self.rate(:plan => self.original_plan),
171
+ :new_subscription_plan_id => self.subscription_plan.id,
172
+ :new_rate => self.rate)
173
+ end
174
+ end
175
+
176
+ def audit_destroy
177
+ ::SubscriptionChange.create(:reason => "cancellation",
178
+ :subscribable => self.subscribable,
179
+ :original_subscription_plan_id => self.subscription_plan_id,
180
+ :original_rate => self.rate,
181
+ :new_rate => Money.empty)
182
+ end
183
+
184
+ public
185
+
186
+ ##
187
+ ## Class Methods
188
+ ##
189
+
190
+ module ClassMethods
191
+ # expires all subscriptions that have been pastdue for too long (accounting for grace)
192
+ def expire
193
+ self.expired.select{|s| s.paid?}.each(&:expire!)
194
+ end
195
+ end
196
+
197
+ ##
198
+ ## Rate
199
+ ##
200
+
201
+ def rate(options = {})
202
+ options = {:date => Date.today, :plan => self.subscription_plan}.merge(options)
203
+
204
+ return nil unless options[:plan]
205
+ value = options[:plan].rate
206
+ value = self.coupon(options[:date]).discount(value) if self.coupon(options[:date])
207
+ value
208
+ end
209
+
210
+ def paid?
211
+ return false unless rate
212
+ rate.cents > 0
213
+ end
214
+
215
+ # Allow for more complex logic to decide if a card should be stored
216
+ def store_credit_card?
217
+ paid?
218
+ end
219
+
220
+ ##
221
+ ## Coupon Redemption
222
+ ##
223
+
224
+ def coupon_key=(coupon_key)
225
+ @coupon_key = coupon_key ? coupon_key.downcase : nil
226
+ self.coupon = ::Coupon.find_by_redemption_key(@coupon_key) unless @coupon_key.blank?
227
+ end
228
+
229
+ def coupon_exist
230
+ self.errors.add :coupon, "could not be found for '#{@coupon_key}'" if !@coupon_key.blank? && ::Coupon.find_by_redemption_key(@coupon_key).nil?
231
+ end
232
+
233
+ def coupon=(coupon)
234
+ if coupon
235
+ s = ::CouponRedemption.new(:subscription => self, :coupon => coupon)
236
+ coupon_redemptions << s
237
+ end
238
+ end
239
+
240
+ def coupon(date = Date.today)
241
+ coupon_redemption(date).coupon rescue nil
242
+ end
243
+
244
+ def coupon_redemption(date = Date.today)
245
+ return nil if coupon_redemptions.empty?
246
+ active_coupons = coupon_redemptions.select{|c| c.active?(date)}
247
+ return nil if active_coupons.empty?
248
+ active_coupons.sort_by{|c| c.coupon.discount_percentage }.reverse.first
249
+ end
250
+
251
+ ##
252
+ ## Remaining Time
253
+ ##
254
+
255
+ # returns the value of the time between now and paid_through.
256
+ # will optionally interpret the time according to a certain subscription plan.
257
+ def remaining_value(plan = self.subscription_plan)
258
+ self.daily_rate(:plan => plan) * remaining_days
259
+ end
260
+
261
+ # if paid through today, returns zero
262
+ def remaining_days
263
+ (self.paid_through - Date.today)
264
+ end
265
+
266
+ ##
267
+ ## Grace Period
268
+ ##
269
+
270
+ # if under grace through today, returns zero
271
+ def remaining_days_of_grace
272
+ (self.expire_on - Date.today - 1).to_i
273
+ end
274
+
275
+ def in_grace?
276
+ remaining_days < 0 and not expired?
277
+ end
278
+
279
+ ##
280
+ ## Expiration
281
+ ##
282
+
283
+ # sets the expiration for the subscription based on today and the configured grace period.
284
+ def expire_after_grace!(transaction = nil)
285
+ return unless self.expire_on.nil? # You only set this once subsequent failed transactions shouldn't affect expiration
286
+ self.expire_on = [Date.today, paid_through].max + Freemium.days_grace
287
+ transaction.message = "now set to expire on #{self.expire_on}" if transaction
288
+ Freemium.mailer.expiration_warning(self).deliver
289
+ transaction.save! if transaction
290
+ save!
291
+ end
292
+
293
+ # sends an expiration email, then downgrades to a free plan
294
+ def expire!
295
+ Freemium.mailer.expiration_notice(self).deliver
296
+ # downgrade to a free plan
297
+ self.expire_on = Date.today
298
+ self.subscription_plan = Freemium.expired_plan if Freemium.expired_plan
299
+ self.destroy_credit_card
300
+ self.save!
301
+ end
302
+
303
+ def expired?
304
+ expire_on and expire_on <= Date.today
305
+ end
306
+
307
+ ##
308
+ ## Receiving More Money
309
+ ##
310
+
311
+ # receives payment and saves the record
312
+ def receive_payment!(transaction)
313
+ receive_payment(transaction)
314
+ transaction.save!
315
+ self.save!
316
+ end
317
+
318
+ # extends the paid_through period according to how much money was received.
319
+ # when possible, avoids the days-per-month problem by checking if the money
320
+ # received is a multiple of the plan's rate.
321
+ #
322
+ # really, i expect the case where the received payment does not match the
323
+ # subscription plan's rate to be very much an edge case.
324
+ def receive_payment(transaction)
325
+ self.credit(transaction.amount)
326
+ self.save!
327
+ transaction.subscription.reload # reloaded to that the paid_through date is correct
328
+ transaction.message = "now paid through #{self.paid_through}"
329
+
330
+ begin
331
+ Freemium.mailer.invoice(transaction).deliver
332
+ rescue => e
333
+ transaction.message = "error sending invoice: #{e}"
334
+ end
335
+ end
336
+
337
+ def credit(amount)
338
+ self.paid_through = if amount.cents % rate.cents == 0
339
+ self.paid_through + (amount.cents / rate.cents).months
340
+ else
341
+ self.paid_through + (amount.cents / daily_rate.cents).days
342
+ end
343
+
344
+ # if they've paid again, then reset expiration
345
+ self.expire_on = nil
346
+ self.in_trial = false
347
+ end
348
+
349
+ end
350
+ end
@@ -0,0 +1,20 @@
1
+ module Freemium
2
+ module SubscriptionChange
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ belongs_to :subscribable, :polymorphic => true
7
+
8
+ belongs_to :original_subscription_plan, :class_name => "SubscriptionPlan"
9
+ belongs_to :new_subscription_plan, :class_name => "SubscriptionPlan"
10
+
11
+ composed_of :new_rate, :class_name => 'Money', :mapping => [ %w(new_rate_cents cents) ], :allow_nil => true
12
+ composed_of :original_rate, :class_name => 'Money', :mapping => [ %w(original_rate_cents cents) ], :allow_nil => true
13
+
14
+ validates_presence_of :reason
15
+ validates_inclusion_of :reason, :in => %w(new upgrade downgrade expiration cancellation)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ <% @transactions.each do |subscription, events| %>
2
+ subscription #<%= subscription.id %> (billing key <%= subscription.billing_key %>)
3
+ <%= events.collect{|e| "- #{e.to_s}"}.join("\n") %>
4
+ <% end %>
@@ -0,0 +1 @@
1
+ Your subscription has expired.
@@ -0,0 +1 @@
1
+ We were unable to process your payment, and your subscription is set to expire in <%= @subscription.remaining_days_of_grace %> days. Please contact us to correct this.
@@ -0,0 +1,5 @@
1
+ Thanks for paying!
2
+
3
+ Your Plan: <%= @subscription.subscription_plan.name %> (<%= @subscription.subscription_plan.rate %> / month)
4
+ Paid: <%= @amount %>
5
+ Paid Through: <%= @subscription.paid_through.to_s %>
@@ -0,0 +1,36 @@
1
+ class SubscriptionMailer < ActionMailer::Base
2
+ prepend_view_path(File.dirname(__FILE__))
3
+
4
+ default :from => 'billing@example.com',
5
+ :return_path => 'no-reply@example.com'
6
+
7
+ def invoice(transaction)
8
+ @amount = transaction.amount
9
+ @subscription = transaction.subscription
10
+ mail(:to => transaction.subscription.subscribable.email,
11
+ :bcc => Freemium.admin_report_recipients,
12
+ :subject => "Your invoice")
13
+ end
14
+
15
+ def expiration_warning(subscription)
16
+ @subscription = subscription
17
+ mail(:to => subscription.subscribable.email,
18
+ :bcc => Freemium.admin_report_recipients,
19
+ :subject => "Your subscription is set to expire")
20
+ end
21
+
22
+ def expiration_notice(subscription)
23
+ @subscription = subscription
24
+ mail(:to => subscription.subscribable.email,
25
+ :bcc => Freemium.admin_report_recipients,
26
+ :subject => "Your subscription has expired")
27
+ end
28
+
29
+ def admin_report(transactions)
30
+ @amount_charged = transactions.select{|t| t.success?}.collect{|t| t.amount}.sum
31
+ @transactions = transactions
32
+ @amount_charged = @amount_charged
33
+ mail(:to => Freemium.admin_report_recipients,
34
+ :subject => "Billing report (#{@amount_charged} charged)")
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # == Attributes
2
+ # subscriptions: all subscriptions for the plan
3
+ # rate_cents: how much this plan costs, in cents
4
+ # rate: how much this plan costs, in Money
5
+ # yearly: whether this plan cycles yearly or monthly
6
+ #
7
+ module Freemium
8
+ module SubscriptionPlan
9
+ include Rates
10
+
11
+ def self.included(base)
12
+ base.class_eval do
13
+ # yes, subscriptions.subscription_plan_id may not be null, but
14
+ # this at least makes the delete not happen if there are any active.
15
+ has_many :subscriptions, :dependent => :nullify, :class_name => "Subscription", :foreign_key => :subscription_plan_id
16
+ has_and_belongs_to_many :coupons, :class_name => "SubscriptionPlan",
17
+ :join_table => :coupons_subscription_plans, :foreign_key => :subscription_plan_id, :association_foreign_key => :coupon_id
18
+
19
+ composed_of :rate, :class_name => 'Money', :mapping => [ %w(rate_cents cents) ], :allow_nil => true
20
+
21
+ validates_uniqueness_of :redemption_key, :allow_nil => true, :allow_blank => true
22
+ validates_presence_of :name
23
+ validates_presence_of :rate_cents
24
+ end
25
+ end
26
+
27
+ def features
28
+ Freemium::FeatureSet.find(self.feature_set_id)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ module Freemium
2
+ module Transaction
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ scope :since, lambda { |time| where(["created_at >= ?", time]) }
7
+
8
+ belongs_to :subscription, :class_name => "Subscription"
9
+
10
+ composed_of :amount, :class_name => 'Money', :mapping => [ %w(amount_cents cents) ], :allow_nil => true
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Freemium
2
+ VERSION = "0.0.1"
3
+ end
data/lib/freemium.rb ADDED
@@ -0,0 +1,75 @@
1
+ require 'rails'
2
+
3
+ require 'money'
4
+ require "freemium/version"
5
+ require 'freemium/address'
6
+ require 'freemium/coupon'
7
+ require 'freemium/coupon_redemption'
8
+ require 'freemium/credit_card'
9
+ require 'freemium/feature_set'
10
+ require 'freemium/manual_billing'
11
+ require 'freemium/rates'
12
+ require 'freemium/recurring_billing'
13
+ require 'freemium/response'
14
+ require 'freemium/subscription'
15
+ require 'freemium/subscription_change'
16
+ require 'freemium/subscription_plan'
17
+ require 'freemium/transaction'
18
+ require 'freemium/gateways/base'
19
+ require 'freemium/gateways/brain_tree'
20
+ require 'freemium/gateways/test'
21
+ require 'freemium/subscription_mailer'
22
+
23
+ module Freemium
24
+ class CreditCardStorageError < RuntimeError; end
25
+
26
+ # Lets you configure which ActionMailer class contains appropriate
27
+ # mailings for invoices, expiration warnings, and expiration notices.
28
+ # You'll probably want to create your own, based on lib/subscription_mailer.rb.
29
+ mattr_accessor :mailer
30
+ @@mailer = SubscriptionMailer
31
+
32
+ # The gateway of choice. Default gateway is a stubbed testing gateway.
33
+ mattr_accessor :gateway
34
+ @@gateway = Freemium::Gateways::Test.new
35
+
36
+ # You need to specify whether Freemium or your gateway's ARB module will control
37
+ # the billing process. If your gateway's ARB controls the billing process, then
38
+ # Freemium will simply try and keep up-to-date on transactions.
39
+ def self.billing_handler=(val)
40
+ case val
41
+ when :manual then ::Subscription.send(:include, Freemium::ManualBilling)
42
+ when :gateway then ::Subscription.send(:include, Freemium::RecurringBilling)
43
+ else
44
+ raise "unknown billing_handler: #{val}"
45
+ end
46
+ end
47
+
48
+ # How many days to keep an account active after it fails to pay.
49
+ mattr_accessor :days_grace
50
+ @@days_grace = 3
51
+
52
+ # How many days in an initial free trial?
53
+ mattr_accessor :days_free_trial
54
+ @@days_free_trial = 0
55
+
56
+ # What plan to assign to subscriptions that have expired. May be nil.
57
+ mattr_writer :expired_plan
58
+ def self.expired_plan
59
+ @@expired_plan ||= (::SubscriptionPlan.find_by_redemption_key(expired_plan_key.to_s) if expired_plan_key)
60
+ end
61
+
62
+ # It's easier to assign a plan by it's key (so you don't get errors before you run migrations)
63
+ # we will reset subscription_plan when we change the key
64
+ mattr_reader :expired_plan_key
65
+ def self.expired_plan_key=(key)
66
+ @@expired_plan_key = key
67
+ @@expired_plan = nil
68
+ end
69
+
70
+ # If you want to receive admin reports, enter an email (or list of emails) here.
71
+ # These will be bcc'd on all SubscriptionMailer emails, and will also receive the
72
+ # admin activity report.
73
+ mattr_accessor :admin_report_recipients
74
+
75
+ end