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,273 @@
1
+ module Freemium
2
+ module CreditCard
3
+
4
+ CARD_COMPANIES = {
5
+ 'visa' => /^4\d{12}(\d{3})?$/,
6
+ 'master' => /^(5[1-5]\d{4}|677189)\d{10}$/,
7
+ 'discover' => /^(6011|65\d{2})\d{12}$/,
8
+ 'american_express' => /^3[47]\d{13}$/,
9
+ 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
10
+ 'jcb' => /^3528\d{12}$/,
11
+ 'switch' => /^6759\d{12}(\d{2,3})?$/,
12
+ 'solo' => /^6767\d{12}(\d{2,3})?$/,
13
+ 'dankort' => /^5019\d{12}$/,
14
+ 'maestro' => /^(5[06-8]|6\d)\d{10,17}$/,
15
+ 'forbrugsforeningen' => /^600722\d{10}$/,
16
+ 'laser' => /^(6304[89]\d{11}(\d{2,3})?|670695\d{13})$/
17
+ }
18
+
19
+ def self.included(base)
20
+ base.class_eval do
21
+ # Essential attributes for a valid, non-bogus creditcards
22
+ attr_accessor :number, :month, :year, :first_name, :last_name
23
+
24
+ # Required for Switch / Solo cards
25
+ attr_accessor :start_month, :start_year, :issue_number
26
+
27
+ # Optional verification_value (CVV, CVV2 etc). Gateways will try their best to
28
+ # run validation on the passed in value if it is supplied
29
+ attr_accessor :verification_value
30
+
31
+ attr_accessible :number, :month, :year, :first_name, :last_name, :start_month, :start_year, :issue_number, :verification_value, :card_type, :zip_code
32
+
33
+ has_one :subscription, :class_name => "Subscription"
34
+
35
+ before_validation :sanitize_data, :if => :changed?
36
+
37
+ validate :validate_card
38
+ end
39
+
40
+ base.extend(ClassMethods)
41
+ end
42
+
43
+ ##
44
+ ## Callbacks
45
+ ##
46
+
47
+ protected
48
+
49
+ def sanitize_data #:nodoc:
50
+ self.month = month.to_i
51
+ self.year = year.to_i
52
+ self.number = number.to_s.gsub(/[^\d]/, "")
53
+ self.card_type.downcase! if card_type.respond_to?(:downcase)
54
+ self.card_type = self.class.card_type?(number) if card_type.blank?
55
+ self.display_number = display_number
56
+ end
57
+
58
+ public
59
+
60
+ ##
61
+ ## Class Methods
62
+ ##
63
+
64
+ module ClassMethods
65
+ # Returns true if it validates. Optionally, you can pass a card type as an argument and
66
+ # make sure it is of the correct type.
67
+ #
68
+ # References:
69
+ # - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
70
+ # - http://www.beachnet.com/~hstiles/cardtype.html
71
+ def valid_number?(number)
72
+ valid_card_number_length?(number) &&
73
+ valid_checksum?(number)
74
+ end
75
+
76
+ # Regular expressions for the known card companies.
77
+ #
78
+ # References:
79
+ # - http://en.wikipedia.org/wiki/Credit_card_number
80
+ # - http://www.barclaycardbusiness.co.uk/information_zone/processing/bin_rules.html
81
+ def card_companies
82
+ CARD_COMPANIES
83
+ end
84
+
85
+ # Returns a string containing the type of card from the list of known information below.
86
+ # Need to check the cards in a particular order, as there is some overlap of the allowable ranges
87
+ #--
88
+ # TODO Refactor this method. We basically need to tighten up the Maestro Regexp.
89
+ #
90
+ # Right now the Maestro regexp overlaps with the MasterCard regexp (IIRC). If we can tighten
91
+ # things up, we can boil this whole thing down to something like...
92
+ #
93
+ # def type?(number)
94
+ # return 'visa' if valid_test_mode_card_number?(number)
95
+ # card_companies.find([nil]) { |type, regexp| number =~ regexp }.first.dup
96
+ # end
97
+ #
98
+ def card_type?(number)
99
+ card_companies.reject { |c,p| c == 'maestro' }.each do |company, pattern|
100
+ return company.dup if number =~ pattern
101
+ end
102
+
103
+ return 'maestro' if number =~ card_companies['maestro']
104
+
105
+ return nil
106
+ end
107
+
108
+ def last_digits(number)
109
+ number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
110
+ end
111
+
112
+ def mask(number)
113
+ "XXXX-XXXX-XXXX-#{last_digits(number)}"
114
+ end
115
+
116
+ # Checks to see if the calculated type matches the specified type
117
+ def matching_card_type?(number, card_type)
118
+ card_type?(number) == card_type
119
+ end
120
+
121
+ private
122
+
123
+ def valid_card_number_length?(number) #:nodoc:
124
+ number.to_s.length >= 12
125
+ end
126
+
127
+ # Checks the validity of a card number by use of the the Luhn Algorithm.
128
+ # Please see http://en.wikipedia.org/wiki/Luhn_algorithm for details.
129
+ def valid_checksum?(number) #:nodoc:
130
+ sum = 0
131
+ for i in 0..number.length
132
+ weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
133
+ sum += (weight < 10) ? weight : weight - 9
134
+ end
135
+
136
+ (number[-1,1].to_i == (10 - sum % 10) % 10)
137
+ end
138
+
139
+ end
140
+
141
+ ##
142
+ ## From ActiveMerchant::Billing::CreditCard
143
+ ##
144
+
145
+ # Provides proxy access to an expiry date object
146
+ def expiration_date
147
+ unless self['expiration_date']
148
+ month_days = [nil,31,28,31,30,31,30,31,31,30,31,30,31]
149
+ begin
150
+ month_days[2] = 29 if Date.leap?(@year)
151
+ str = "#{month_days[@month]}/#{@month}/#{@year} 23:59:59"
152
+ self['expiration_date'] = Time.parse(str)
153
+ end
154
+ end
155
+ self['expiration_date']
156
+ end
157
+
158
+ def expired?
159
+ return false unless expiration_date
160
+ Time.now > expiration_date
161
+ end
162
+
163
+ def name?
164
+ first_name? && last_name?
165
+ end
166
+
167
+ def first_name?
168
+ !@first_name.blank?
169
+ end
170
+
171
+ def last_name?
172
+ !@last_name.blank?
173
+ end
174
+
175
+ def name
176
+ "#{@first_name} #{@last_name}"
177
+ end
178
+
179
+ # Show the card number, with all but last 4 numbers replace with "X". (XXXX-XXXX-XXXX-4338)
180
+ def display_number
181
+ self['display_number'] ||= self.class.mask(number)
182
+ self['display_number']
183
+ end
184
+
185
+ def last_digits
186
+ self.class.last_digits(number)
187
+ end
188
+
189
+ def address
190
+ unless @address
191
+ @address = Address.new
192
+ @address.zip = self.zip_code
193
+ end
194
+ @address
195
+ end
196
+
197
+ ##
198
+ ## Overrides
199
+ ##
200
+
201
+ # We're overriding AR#changed? to include instance vars that aren't persisted to see if a new card is being set
202
+ def changed?
203
+ card_type_changed? || [:number, :month, :year, :first_name, :last_name, :start_month, :start_year, :issue_number, :verification_value].any? {|attr| !self.send(attr).nil?}
204
+ end
205
+
206
+ ##
207
+ ## Validation
208
+ ##
209
+
210
+ def validate_card
211
+ # We don't need to run validations unless it's a new record or the
212
+ # record has changed
213
+ return unless new_record? || changed?
214
+
215
+ validate_essential_attributes
216
+
217
+ # Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used
218
+ return if card_type == 'bogus'
219
+
220
+ validate_card_type
221
+ validate_card_number
222
+ validate_switch_or_solo_attributes
223
+ end
224
+
225
+ private
226
+
227
+ def validate_card_number #:nodoc:
228
+ errors.add :number, "is not a valid credit card number" unless self.class.valid_number?(number)
229
+ unless errors[:number] || errors[:type]
230
+ errors.add :card_type, "is not the correct card type" unless self.class.matching_card_type?(number, card_type)
231
+ end
232
+ end
233
+
234
+ def validate_card_type #:nodoc:
235
+ errors.add :card_type, "is required" if card_type.blank?
236
+ errors.add :card_type, "is invalid" unless self.class.card_companies.keys.include?(card_type)
237
+ end
238
+
239
+ def validate_essential_attributes #:nodoc:
240
+ errors.add :first_name, "cannot be empty" if @first_name.blank?
241
+ errors.add :last_name, "cannot be empty" if @last_name.blank?
242
+ errors.add :month, "is not a valid month" unless valid_month?(@month)
243
+ errors.add :year, "expired" if expired?
244
+ errors.add :year, "is not a valid year" unless valid_expiration_year?(@year)
245
+ end
246
+
247
+ def validate_switch_or_solo_attributes #:nodoc:
248
+ if %w[switch solo].include?(card_type)
249
+ unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number)
250
+ errors.add :start_month, "is invalid" unless valid_month?(@start_month)
251
+ errors.add :start_year, "is invalid" unless valid_start_year?(@start_year)
252
+ errors.add :issue_number, "cannot be empty" unless valid_issue_number?(@issue_number)
253
+ end
254
+ end
255
+ end
256
+
257
+ def valid_month?(month)
258
+ (1..12).include?(month)
259
+ end
260
+
261
+ def valid_expiration_year?(year)
262
+ (Time.now.year..Time.now.year + 20).include?(year)
263
+ end
264
+
265
+ def valid_start_year?(year)
266
+ year.to_s =~ /^\d{4}$/ && year.to_i > 1987
267
+ end
268
+
269
+ def valid_issue_number?(number)
270
+ number.to_s =~ /^\d{1,2}$/
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,45 @@
1
+ module Freemium
2
+ class FeatureSet
3
+
4
+ def initialize(hash = {})
5
+ hash.each do |key, value|
6
+ self.class.class_eval { attr_accessor key.intern }
7
+ self.send("#{key}=", value)
8
+ end
9
+ end
10
+
11
+ def method_missing(method, *args, &block)
12
+ # forward named routes
13
+ if method.to_s.include? '?'
14
+ send(method.to_s[0..-2], *args, &block)
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def self.find(id)
21
+ self.feature_sets[id.to_s]
22
+ end
23
+
24
+ protected
25
+
26
+ cattr_accessor :config_file
27
+ def self.config_file
28
+ @@config_file ||= File.join(RAILS_ROOT, 'config', 'freemium_feature_sets.yml')
29
+ end
30
+
31
+ cattr_accessor :feature_sets
32
+ self.feature_sets = nil
33
+
34
+ def self.feature_sets
35
+ if @@feature_sets.nil?
36
+ @@feature_sets = {}
37
+ YAML::load(File.read(self.config_file)).each do |features|
38
+ feature_set = FeatureSet.new(features)
39
+ @@feature_sets[feature_set.id] = feature_set
40
+ end
41
+ end
42
+ @@feature_sets
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ module Freemium
2
+ module Gateways
3
+ class Base #:nodoc:
4
+ superclass_delegating_accessor :open_timeout
5
+ self.open_timeout = 60
6
+
7
+ superclass_delegating_accessor :read_timeout
8
+ self.read_timeout = 60
9
+
10
+ # cancels the subscription identified by the given billing key.
11
+ # this might mean removing it from the remote system, or halting the remote
12
+ # recurring billing.
13
+ #
14
+ # should return a Freemium::Response
15
+ def cancel(billing_key)
16
+ raise MethodNotImplemented
17
+ end
18
+
19
+ # stores a credit card with the gateway.
20
+ # should return a Freemium::Response
21
+ def store(credit_card, address = nil)
22
+ raise MethodNotImplemented
23
+ end
24
+
25
+ # updates a credit card in the gateway.
26
+ # should return a Freemium::Response
27
+ def update(billing_key, credit_card = nil, address = nil)
28
+ raise MethodNotImplemented
29
+ end
30
+
31
+ # validates a credit card with the gateway.
32
+ # should return a Freemium::Response
33
+ def validate(credit_card, address = nil)
34
+ raise MethodNotImplemented
35
+ end
36
+
37
+ ##
38
+ ## Only needed to support Freemium.billing_handler = :gateway
39
+ ##
40
+
41
+ # only needed to support an ARB module. otherwise, the manual billing process will
42
+ # take care of processing transaction information as it happens.
43
+ #
44
+ # concrete classes need to support these options:
45
+ # :billing_key : - only retrieve transactions for this specific billing key
46
+ # :after : - only retrieve transactions after this datetime (non-inclusive)
47
+ # :before : - only retrieve transactions before this datetime (non-inclusive)
48
+ #
49
+ # return value should be a collection of Freemium::Transaction objects.
50
+ def transactions(options = {})
51
+ raise MethodNotImplemented
52
+ end
53
+
54
+ ##
55
+ ## Only needed to support Freemium.billing_handler = :manual
56
+ ##
57
+
58
+ # charges money against the given billing key.
59
+ # should return a Freemium::Transaction
60
+ def charge(billing_key, amount)
61
+ raise MethodNotImplemented
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,175 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ module Freemium
4
+ module Gateways
5
+ # = Setup and Configuration
6
+ # In your config/initializers/freemium.rb, configure Freemium to use BrainTree:
7
+ #
8
+ # Freemium.gateway = Freemium::Gateways::BrainTree.new
9
+ # Freemium.gateway.username = "my_username"
10
+ # Freemium.gateway.password = "my_password"
11
+ #
12
+ # Note that if you want to use demo/password credentials when not in production mode, this is the place.
13
+ #
14
+ # = Data Structures
15
+ # All amounts should use the Money class (from eponymous gem).
16
+ # All credit cards should use Freemium::CreditCard class (currently just an alias for ActiveMerchant::Billing::CreditCard).
17
+ # All addresses should use Freemium::Address class.
18
+ #
19
+ # = For Testing
20
+ # The URL does not change. If your account is in test mode, no charges will be processed. Otherwise,
21
+ # configure the username and password to be "demo" and "password", respectively.
22
+ class BrainTree < Base
23
+ URL = 'https://secure.braintreepaymentgateway.com/api/transact.php'
24
+ attr_accessor :username, :password
25
+
26
+ # using BrainTree's recurring billing is not possible until I have their reporting API
27
+ #def transactions(options = {}); end
28
+
29
+ # Stores a card in SecureVault.
30
+ def store(credit_card, address = nil)
31
+ p = Post.new(URL, {
32
+ :username => self.username,
33
+ :password => self.password,
34
+ :customer_vault => "add_customer"
35
+ })
36
+ p.params.merge! params_for_credit_card(credit_card)
37
+ p.params.merge! params_for_address(address) if address
38
+ p.commit(open_timeout, read_timeout)
39
+ return p.response
40
+ end
41
+
42
+ # Updates a card in SecureVault.
43
+ def update(vault_id, credit_card = nil, address = nil)
44
+ p = Post.new(URL, {
45
+ :username => self.username,
46
+ :password => self.password,
47
+ :customer_vault => "update_customer",
48
+ :customer_vault_id => vault_id
49
+ })
50
+ p.params.merge! params_for_credit_card(credit_card) if credit_card
51
+ p.params.merge! params_for_address(address) if address
52
+ p.commit(open_timeout, read_timeout)
53
+ return p.response
54
+ end
55
+
56
+ # Manually charges a card in SecureVault. Called automatically as part of manual billing process.
57
+ def charge(vault_id, amount)
58
+ p = Post.new(URL, {
59
+ :username => self.username,
60
+ :password => self.password,
61
+ :customer_vault_id => vault_id,
62
+ :amount => sprintf("%.2f", amount.cents.to_f / 100)
63
+ })
64
+ p.commit(open_timeout, read_timeout)
65
+ transaction = AccountTransaction.new(:billing_key => vault_id, :amount => amount, :success => p.response.success?)
66
+ transaction.response = p.response if transaction.respond_to?(:response=)
67
+ return transaction
68
+ end
69
+
70
+ # Removes a card from SecureVault. Called automatically when the subscription expires.
71
+ def cancel(vault_id)
72
+ p = Post.new(URL, {
73
+ :username => self.username,
74
+ :password => self.password,
75
+ :customer_vault => 'delete_customer',
76
+ :customer_vault_id => vault_id
77
+ })
78
+ p.commit(open_timeout, read_timeout)
79
+ return p.response
80
+ end
81
+
82
+ # Validates the card.
83
+ def validate(credit_card, address = nil)
84
+ # Assume we validated if we're using a demo account
85
+ return Freemium::Response.new(true) if self.username == 'demo'
86
+
87
+ p = Post.new(URL, {
88
+ :username => self.username,
89
+ :password => self.password,
90
+ :type => 'validate'
91
+ })
92
+ p.params.merge! params_for_credit_card(credit_card)
93
+ if address
94
+ p.params.merge! params_for_address(address)
95
+ end
96
+
97
+ p.commit(open_timeout, read_timeout)
98
+ return p.response
99
+ end
100
+
101
+ protected
102
+ def params_for_credit_card(card)
103
+ params = {
104
+ :payment => 'creditcard',
105
+ :firstname => card.first_name,
106
+ :lastname => card.last_name,
107
+ :ccnumber => card.number,
108
+ :ccv => card.verification_value,
109
+ :ccexp => ["%.2i" % card.month, ("%.4i" % card.year)[-2..-1]].join # MMYY
110
+ }
111
+ end
112
+
113
+ def params_for_address(address)
114
+ params = {
115
+ :email => address.email,
116
+ :address1 => address.address1,
117
+ :address2 => address.address2,
118
+ :city => address.city,
119
+ :state => address.state, # TODO: two-digit code!
120
+ :zip => address.zip,
121
+ :country => address.country, # TODO: two digit code! (ISO-3166)
122
+ :phone => address.phone_number,
123
+ :ipaddress => address.ip_address
124
+ }
125
+ end
126
+
127
+ class Post
128
+ attr_accessor :url
129
+ attr_accessor :params
130
+ attr_reader :response
131
+
132
+ def initialize(url, params = {})
133
+ self.url = url
134
+ self.params = params
135
+ end
136
+
137
+ def commit(open_timeout, read_timeout)
138
+ data = parse(post(open_timeout, read_timeout))
139
+ # from BT API: 1 means approved, 2 means declined, 3 means error
140
+ success = data['response'].to_i == 1
141
+ @response = Freemium::Response.new(success, data)
142
+ @response.billing_key = data['customer_vault_id']
143
+ @response.message = data['responsetext']
144
+ return self
145
+ end
146
+
147
+ protected
148
+
149
+ # BrainTree returns a body of parameters in GET query format, so convert that into a simple hash.
150
+ def parse(data)
151
+ {}.tap do |results|
152
+ data.split('&').each do |pair|
153
+ key, value = pair.split('=', 2).collect { |v| CGI::unescape(v) }
154
+ results[key] = value
155
+ end
156
+ end
157
+ end
158
+
159
+ # cf. ActiveMerchant's PostsData module.
160
+ def post(open_timeout, read_timeout)
161
+ uri = URI.parse(self.url)
162
+
163
+ http = Net::HTTP.new(uri.host, uri.port)
164
+ http.open_timeout = open_timeout
165
+ http.read_timeout = read_timeout
166
+ http.use_ssl = true
167
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
168
+
169
+ data = self.params.collect { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join("&")
170
+ http.post(uri.request_uri, data).body
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,34 @@
1
+ module Freemium
2
+ module Gateways
3
+ class Test < Base
4
+ def transactions(options = {})
5
+ options
6
+ end
7
+
8
+ def charge(*args)
9
+ args
10
+ end
11
+
12
+ def store(*args)
13
+ response = Freemium::Response.new(true)
14
+ response.billing_key = Time.now.to_i.to_s
15
+ response
16
+ end
17
+
18
+ def update(billing_key, *args)
19
+ response = Freemium::Response.new(true)
20
+ response.billing_key = billing_key
21
+ response
22
+ end
23
+
24
+ def cancel(*args)
25
+ args
26
+ end
27
+
28
+ def validate(*args)
29
+ response = Freemium::Response.new(true)
30
+ response
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,73 @@
1
+ module Freemium
2
+ # adds manual billing functionality to the Subscription class
3
+ module ManualBilling
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # Override if you need to charge something different than the rate (ex: yearly billing option)
9
+ def installment_amount(options = {})
10
+ self.rate(options)
11
+ end
12
+
13
+ # charges this subscription.
14
+ # assumes, of course, that this module is mixed in to the Subscription model
15
+ def charge!
16
+ # Save the transaction immediately
17
+
18
+ @transaction = gateway.charge(billing_key, self.installment_amount)
19
+ self.transactions << @transaction
20
+ self.last_transaction_at = Time.now # TODO this could probably now be inferred from the list of transactions
21
+ self.last_transaction_success = @transaction.success?
22
+
23
+ self.save(:validate => false)
24
+
25
+ begin
26
+ if @transaction.success?
27
+ receive_payment!(@transaction)
28
+ elsif !@transaction.subscription.in_grace?
29
+ expire_after_grace!(@transaction)
30
+ end
31
+ rescue
32
+ end
33
+
34
+ @transaction
35
+ end
36
+
37
+ def store_credit_card_offsite
38
+ if credit_card && credit_card.changed? && credit_card.valid?
39
+ response = billing_key ? gateway.update(billing_key, credit_card, credit_card.address) : gateway.store(credit_card, credit_card.address)
40
+ raise Freemium::CreditCardStorageError.new(response.message) unless response.success?
41
+ self.billing_key = response.billing_key
42
+ self.expire_on = nil if last_transaction_success
43
+ self.credit_card.reload # to prevent needless subsequent store() calls
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+ # the process you should run periodically
49
+ def run_billing
50
+ # charge all billable subscriptions
51
+ @transactions = find_billable.collect{|b| b.charge!}
52
+ # actually expire any subscriptions whose time has come
53
+ expire
54
+
55
+ # send the activity report
56
+ Freemium.mailer.deliver_admin_report(
57
+ @transactions # Add in transactions
58
+ ) if Freemium.admin_report_recipients && !@transactions.empty?
59
+
60
+ @transactions
61
+ end
62
+
63
+ protected
64
+
65
+ # a subscription is due on the last day it's paid through. so this finds all
66
+ # subscriptions that expire the day *after* the given date.
67
+ # because of coupons we can't trust rate_cents alone and need to verify that the account is indeed paid?
68
+ def find_billable
69
+ self.paid.due.select{|s| s.paid?}
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,7 @@
1
+ require 'freemium'
2
+ require 'rails'
3
+
4
+ module Freemium
5
+ class Railtie < Rails::Railtie
6
+ end
7
+ end