freemium 0.0.1

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