acts_as_subscription 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,168 @@
1
+ require 'acts_as_subscription/error'
2
+
3
+ module ActsAsSubscription
4
+ module Subscription
5
+
6
+ # Provides a consistent interface to the various recurring billing services.
7
+ #
8
+ # There are three ways to initialize the backend :
9
+ # 1. Pass the parameters to the _acts_as_subscription_ method inside the model :
10
+ #
11
+ # class Subscription < ActiveRecord::Base
12
+ # acts_as_subscription :backend => :cheddar_getter,
13
+ # :user => 'my_user',
14
+ # :password => 'my_pass',
15
+ # :product_code => 'my_product'
16
+ # end
17
+ #
18
+ # 2. Set the params in rails via <tt>config/initializers/acts_as_subscription.rb</tt> :
19
+ #
20
+ # ActsAsSubscription::Subscription::Backend.config = {
21
+ # :backend => :cheddar_getter,
22
+ # :user => 'my_user',
23
+ # :password => 'my_pass',
24
+ # :product_code => 'my_product'
25
+ # }
26
+ #
27
+ # 3. Add a <tt>config/subscription.yml</tt> file to your rails project :
28
+ #
29
+ # development:
30
+ # backend: 'cheddar_getter'
31
+ # user: 'my_user'
32
+ # password: 'my_pass'
33
+ # product_code: 'my_product'
34
+
35
+ module Backend
36
+
37
+ # Stores the backend configuration data.
38
+ mattr_accessor :config
39
+
40
+ # Stores an instance of the backend for a particular recurring billing service.
41
+ mattr_accessor :instance
42
+
43
+ # Sets the configuration hash for the backend.
44
+ #
45
+ # _args_ should be a hash containing the subscription backend account and product information :
46
+ # * <tt>backend</tt> : Name of the recurring billing service being used. Current choices are
47
+ # <tt>:chargify</tt>, <tt>:cheddar_getter</tt> and <tt>:recurly</tt>.
48
+ # * <tt>user</tt> : Username used to log into the billling service API.
49
+ # * <tt>password</tt> : Password used to log into the billling service API.
50
+ # * <tt>product_code</tt> : Code of your product on the backend service.
51
+ #
52
+ # If the backend configuration has already been set, _args_ will be merged with the current settings.
53
+ def self.config=(*args)
54
+ args = args.extract_options!
55
+
56
+ if @@config
57
+ @@config = self.config.merge(args)
58
+ else
59
+ @@config = args
60
+ end
61
+ end
62
+
63
+ # Validates the configuration options, and creates an instance of the requested <tt>:backend</tt>.
64
+ #
65
+ # _args_ should be a hash containing the subscription backend account and product information.
66
+ def self.initialize(*args)
67
+ # Don't do anything if the backend has already been loaded.
68
+ return if self.instance
69
+
70
+ self.read_yaml_options
71
+ self.config = *args
72
+ self.validate_options!
73
+
74
+ # Make sure the backend is a valid class.
75
+ begin
76
+ require "acts_as_subscription/backend/#{self.config[:backend]}_client"
77
+ class_name = "#{self.config[:backend].to_s}_client".camelize
78
+ klass = ActsAsSubscription::Subscription::Backend.const_get(class_name)
79
+ self.instance = klass.new(self.config[:user], self.config[:password], self.config[:product_code])
80
+ rescue LoadError
81
+ raise ActsAsSubscription::Subscription::Error::BackendError.new("Backend '#{self.config[:backend]}' could not be found")
82
+ rescue NameError
83
+ raise ActsAsSubscription::Subscription::Error::BackendError.new("Backend '#{self.config[:backend]}' does not appear to implement class '#{class_name}'")
84
+ end
85
+ end
86
+
87
+ # Creates a customer on the backend subscription service, using the settings from the given
88
+ # <tt>subscription</tt> instance.
89
+ #
90
+ # <tt>subscription</tt> should be a subclass of <tt>ActiveRecord::Base</tt> that implements
91
+ # <tt>acts_as_subscription</tt> :
92
+ #
93
+ # class Subscription < ActiveRecord::Base
94
+ # acts_as_subscription
95
+ # end
96
+ #
97
+ # Returns true if the operation was successful, otherwise an error message.
98
+ def self.create_subscription(subscription)
99
+ self.instance.create_subscription(subscription)
100
+ end
101
+
102
+ # Updates a customer on the backend subscription service, using the settings from the given
103
+ # <tt>subscription</tt> instance.
104
+ #
105
+ # <tt>subscription</tt> should be a subclass of <tt>ActiveRecord::Base</tt> that implements
106
+ # <tt>acts_as_subscription</tt> :
107
+ #
108
+ # class Subscription < ActiveRecord::Base
109
+ # acts_as_subscription
110
+ # end
111
+ #
112
+ # Returns true if the update was successful, otherwise an error message.
113
+ def self.update_subscription(subscription)
114
+ self.instance.update_subscription(subscription)
115
+ end
116
+
117
+ # Cancels the customer with the given <tt>customer_code</tt> on the backend subscription service.
118
+ #
119
+ # Returns true if the cancellation was successful, otherwise an error message.
120
+ def self.cancel_subscription!(customer_code)
121
+ self.instance.cancel_subscription!(customer_code)
122
+ end
123
+
124
+ # Returns a list of subscription plans registered with the backend subscription service.
125
+ def self.plans
126
+ self.instance.plans
127
+ end
128
+
129
+
130
+ protected
131
+
132
+ # Tries to read configuration options from the subscription.yml configuration file.
133
+ def self.read_yaml_options
134
+ # Try to get initial config from a YAML file if possible.
135
+ if Rails.root
136
+ config_path = File.join(Rails.root, 'config/subscription.yml')
137
+ if FileTest.exists?(config_path)
138
+ config = YAML::load(ERB.new(IO.read(config_path)).result)[Rails.env]
139
+ # Convert string keys to symbols.
140
+ config = config.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
141
+ self.config = config
142
+ end
143
+ end
144
+ end
145
+
146
+ # Validates the configuration options, and makes sure all required parameters are present.
147
+ def self.validate_options!
148
+ unless self.config[:backend]
149
+ raise ActsAsSubscription::Subscription::Error::ArgumentError.new(':backend parameter must be provided')
150
+ end
151
+
152
+ unless self.config[:product_code]
153
+ raise ActsAsSubscription::Subscription::Error::ArgumentError.new(':product_code parameter must be provided')
154
+ end
155
+
156
+ unless self.config[:user]
157
+ raise ActsAsSubscription::Subscription::Error::ArgumentError.new(':user parameter must be provided')
158
+ end
159
+
160
+ unless self.config[:password]
161
+ raise ActsAsSubscription::Subscription::Error::ArgumentError.new(':password parameter must be provided')
162
+ end
163
+
164
+ end
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,15 @@
1
+ module ActsAsSubscription
2
+ module Subscription
3
+ module Error # :nodoc:
4
+
5
+ # Raised when invalid arguments are passed.
6
+ class ArgumentError < Exception
7
+ end
8
+
9
+ # Raised if there is a problem with the backend configuration.
10
+ class BackendError < Exception
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails'
2
+ require 'acts_as_subscription'
3
+
4
+ module ActsAsSubscription
5
+ class Railtie < Rails::Railtie # :nodoc:
6
+ initializer :'acts_as_subscription.hook' do
7
+ ActiveSupport.on_load(:active_record) do
8
+ ActiveRecord::Base.send(:include, ActsAsSubscription::Subscription)
9
+ ActiveRecord::Base.send(:include, ActsAsSubscription::SubscriptionPlan)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,258 @@
1
+ require 'acts_as_subscription/backend'
2
+
3
+ module ActsAsSubscription
4
+ module Subscription # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ # Class methods for an ActsAsSubscription::Subscription instance.
8
+ #
9
+ # Handles validation of all subscription-related information, such as :
10
+ #
11
+ # * customer_code
12
+ # * email
13
+ # * first_name
14
+ # * last_name
15
+ # * zip_code
16
+ # * cc_expiration_month
17
+ # * cc_expiration_year
18
+ # * cc_verification_value
19
+ # * cc_number
20
+ #
21
+ # These class methods should be included in ActiveRecord::Base to provide
22
+ # subscription validation support.
23
+ module ClassMethods
24
+
25
+ # Main entry point - any subclass of ActiveRecord::Base can include the
26
+ # _acts_as_subscription_ method to activate subscription functionality.
27
+ #
28
+ # _args_ should be a hash containing the subscription backend account and product information :
29
+ # * <tt>backend</tt> : Name of the recurring billing service being used. Current choices are
30
+ # <tt>:chargify</tt>, <tt>:cheddar_getter</tt> and <tt>:recurly</tt>.
31
+ # * <tt>user</tt> : Username used to log into the billling service API.
32
+ # * <tt>password</tt> : Password used to log into the billling service API.
33
+ # * <tt>product_code</tt> : Code of your product on the backend service.
34
+ #
35
+ # There are three ways to initialize the backend :
36
+ # 1. Pass the parameters to the _acts_as_subscription_ method from within a model :
37
+ #
38
+ # class Subscription < ActiveRecord::Base
39
+ # acts_as_subscription :backend => :cheddar_getter,
40
+ # :user => 'my_user',
41
+ # :password => 'my_pass',
42
+ # :product_code => 'my_product'
43
+ # end
44
+ #
45
+ # 2. Set the params directly via the Backend module :
46
+ # ActsAsSubscription::Subscription::Backend.config = {
47
+ # :backend => :cheddar_getter,
48
+ # :user => 'my_user',
49
+ # :password => 'my_pass',
50
+ # :product_code => 'my_product'
51
+ # }
52
+ #
53
+ # 3. Add a <tt>config/subscription.yml</tt> file to your rails project :
54
+ #
55
+ # development:
56
+ # backend: 'cheddar_getter'
57
+ # user: 'my_user'
58
+ # password: 'my_pass'
59
+ # product_code: 'my_product'
60
+ def acts_as_subscription(*args)
61
+
62
+ ActsAsSubscription::Subscription::Backend.config = *args
63
+ ActsAsSubscription::Subscription::Backend.initialize
64
+
65
+ validates :customer_code,
66
+ :presence => true
67
+
68
+ validates :email,
69
+ :presence => true
70
+
71
+ validates :first_name,
72
+ :presence => true
73
+
74
+ validates :last_name,
75
+ :presence => true
76
+
77
+ validates :plan_code,
78
+ :presence => true
79
+
80
+ validates :zip_code,
81
+ :presence => true,
82
+ :if => :require_zip_code?
83
+
84
+ validates :cc_expiration_month,
85
+ :presence => true,
86
+ :unless => :is_free_plan?
87
+
88
+ validates :cc_expiration_year,
89
+ :presence => true,
90
+ :unless => :is_free_plan?
91
+
92
+ validates :cc_verification_value,
93
+ :presence => true,
94
+ :if => :require_verification_value?
95
+
96
+ validates :cc_number,
97
+ :presence => true,
98
+ :unless => :is_free_plan?
99
+
100
+ validates_associated :credit_card,
101
+ :unless => :is_free_plan?
102
+
103
+ before_validation :update_credit_card
104
+
105
+ before_validation :assign_customer_code,
106
+ :on => :create
107
+
108
+ validate :validate_credit_card
109
+
110
+ attr_accessible :cc_expiration_month,
111
+ :cc_expiration_year,
112
+ :cc_number,
113
+ :cc_verification_value,
114
+ :customer_code,
115
+ :email,
116
+ :first_name,
117
+ :last_name,
118
+ :plan_code,
119
+ :zip_code
120
+
121
+ # Removed this for the time being, since we often want to perform this before_save
122
+ # on a different model (such as User), if the forms are nested. Having before_save
123
+ # on both models causes quite a few problems. This before_save must be specified
124
+ # by hand in the implementing model.
125
+ #before_save :backend_save
126
+
127
+ self.send(:include, ActsAsSubscription::Subscription::SubscriptionInstanceMethods)
128
+
129
+ end
130
+ end
131
+
132
+ # Instance methods for an ActsAsSubscription::Subscription instance, which use ActiveMerchant
133
+ # to validate credit card information.
134
+ #
135
+ # These instance methods should be included in ActiveRecord::Base to provide
136
+ # subscription validation support.
137
+ module SubscriptionInstanceMethods
138
+
139
+ # Used as an interface to ActiveMerchant for credit card validation.
140
+ attr_reader :credit_card
141
+
142
+ # Allows entry of a complete credit card number, even though we only store the last 4
143
+ # digits internally.
144
+ attr_accessor :cc_number
145
+
146
+ # Returns true if the current plan is free, false otherwise.
147
+ def is_free_plan?
148
+ # If no plan-code is given, assume this is a paid plan to be on the safe side.
149
+ return false unless self.plan_code
150
+
151
+ return (self.plan_code =~ /free/i) != nil
152
+ end
153
+
154
+ # Instantiates an instance of ActiveMerchant::Billing::CreditCard if one hasn't already
155
+ # been created.
156
+ def credit_card
157
+ self[:credit_card] ||= instantiate_credit_card
158
+ end
159
+
160
+ def backend_save
161
+ p "+++++++++++++++++++++++ saving +++++++++++++++++++++++"
162
+ if self.new_record?
163
+ result = ActsAsSubscription::Subscription::Backend.instance.create_subscription(self)
164
+ else
165
+ result = ActsAsSubscription::Subscription::Backend.instance.update_subscription(self)
166
+ end
167
+
168
+ # No way of knowing what fields the errors are for for some backends - just add a top-level error.
169
+ unless result == true
170
+ self.errors.add :base, result
171
+ return false
172
+ end
173
+
174
+ return true
175
+ end
176
+
177
+ # This can be overridden in the implementing model, to turn off zip-code validation.
178
+ def require_zip_code?
179
+ return self.is_free_plan? == false
180
+ end
181
+
182
+ # This can be overridden in the implementing model, to turn off verification-value validation.
183
+ def require_verification_value?
184
+ return self.is_free_plan? == false
185
+ end
186
+
187
+ protected
188
+
189
+ # Assigns a unique UUID for the customer.
190
+ def assign_customer_code
191
+ self.customer_code ||= UUIDTools::UUID.random_create.to_s
192
+ end
193
+
194
+ # Updates the ActiveMerchant::Billing::CreditCard instance with the information
195
+ # provided by the user, prior to validation.
196
+ def update_credit_card
197
+ self.credit_card.first_name = self.first_name
198
+ self.credit_card.last_name = self.last_name
199
+ self.credit_card.month = self.cc_expiration_month
200
+ self.credit_card.year = self.cc_expiration_year
201
+ self.credit_card.verification_value = self.cc_verification_value
202
+ self.credit_card.number = self.cc_number
203
+ self.cc_last_digits = self.credit_card.last_digits
204
+ end
205
+
206
+ # Creates an instance of ActiveMerchant::Billing::CreditCard, and adds a couple
207
+ # of utility methods to make it compatible with ActiveRecord.
208
+ def instantiate_credit_card(attributes = {})
209
+ ActiveMerchant::Billing::CreditCard.require_verification_value = require_verification_value?
210
+
211
+ credit_card = ActiveMerchant::Billing::CreditCard.new(attributes)
212
+
213
+ def credit_card.new_record?
214
+ true
215
+ end
216
+
217
+ def credit_card.persisted?
218
+ false
219
+ end
220
+
221
+ credit_card
222
+ end
223
+
224
+ # Returns true if the credit card information provided is valid, or false otherwise.
225
+ def validate_credit_card
226
+ return if is_free_plan?
227
+
228
+ unless credit_card.valid?
229
+ credit_card.errors.each do |field, messages|
230
+ messages.each do |message|
231
+ # Only add the error if one doesn't already exist - don't need to report
232
+ # 'credit card is invalid' etc if it is already blank.
233
+ errors.add("cc_#{field}".to_sym, message) unless errors["cc_#{field}".to_sym].length > 0
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+ end
242
+ end
243
+
244
+
245
+
246
+ # Need to monkey-patch this since validation complains and errors that it is returning nil.
247
+ module ActionView # :nodoc:
248
+ module Helpers # :nodoc:
249
+ module ActiveModelInstanceTag # :nodoc:
250
+ def error_message
251
+ # Original :
252
+ # object.errors[@method_name]
253
+ # Patched :
254
+ object.errors[@method_name] || []
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,93 @@
1
+ require 'acts_as_subscription/backend'
2
+
3
+ module ActsAsSubscription
4
+ module SubscriptionPlan # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ # Class methods for an ActsAsSubscription::SubscriptionPlan instance.
8
+ #
9
+ # Handles validation of all subscription_plan-related information, such as :
10
+ #
11
+ # * code
12
+ # * name
13
+ # * description
14
+ # * price
15
+ #
16
+ # These class methods should be included in ActiveRecord::Base to provide
17
+ # subscription_plan validation support.
18
+ module ClassMethods
19
+
20
+ # Main entry point - any subclass of ActiveRecord::Base can include the
21
+ # _acts_as_subscription_plan_ method to activate subscription_plan
22
+ # functionality.
23
+ def acts_as_subscription_plan
24
+
25
+ validates :code,
26
+ :presence => true,
27
+ :uniqueness => {:case_sensitive => false}
28
+
29
+ validates :name,
30
+ :presence => true
31
+
32
+ validates :billing_frequency,
33
+ :presence => true
34
+
35
+ validates :recurring_charge,
36
+ :presence => true,
37
+ :format => { :with => /^\d+??(?:\.\d{0,2})?$/ },
38
+ :numericality => {:greater_than_or_equal_to => 0}
39
+
40
+ attr_accessible :code,
41
+ :name,
42
+ :description,
43
+ :billing_frequency,
44
+ :recurring_charge,
45
+ :active
46
+
47
+ # Synchronizes the local SubscriptionPlan database with the remote backend. This method
48
+ # will try to update existing plans with new data if possible (based on the :code of
49
+ # the plan).
50
+ #
51
+ # It will *NOT* however delete any plans locally that have been removed from
52
+ # the reote system. It is the responsibilty of the developer to remove local copies
53
+ # themselves if a plan is removed from the remote subscription backend. For safety
54
+ # it seems more sensible not to automate the removal of plans.
55
+ def self.sync!
56
+ plans = ActsAsSubscription::Subscription::Backend.plans
57
+ plans.each do |plan|
58
+ # Try to find an existing plan with this code.
59
+ existing = self.find_by_code(plan[:code])
60
+
61
+ if existing
62
+ existing.update_attributes(plan)
63
+ else
64
+ existing = self.new(plan)
65
+ end
66
+
67
+ existing.save
68
+ end
69
+ end
70
+
71
+ # Returns an array of the current plans, suitable for using with a select
72
+ # or radio input within a form. This is provided more as an example, and
73
+ # should be over-ridden in your implementing class if you want to do
74
+ # currency formatting etc.
75
+ def form_options
76
+ plans = self.find_all_by_active(true)
77
+ result = []
78
+ plans.each do |plan|
79
+ label = plan.name
80
+ if plan.recurring_charge > 0
81
+ label = "#{plan.name} ($#{plan.recurring_charge})"
82
+ end
83
+ result << [label, plan.code]
84
+ end
85
+
86
+ return result
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), 'acts_as_subscription/railtie.rb')
2
+
3
+ module ActsAsSubscription # :nodoc:
4
+ autoload :Subscription, File.join(File.dirname(__FILE__), 'acts_as_subscription/subscription')
5
+ autoload :SubscriptionPlan, File.join(File.dirname(__FILE__), 'acts_as_subscription/subscription_plan')
6
+ end
7
+