saasy 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 (146) hide show
  1. data/CHANGELOG.md +114 -0
  2. data/Gemfile +26 -0
  3. data/README.md +118 -0
  4. data/Rakefile +38 -0
  5. data/app/controllers/accounts_controller.rb +68 -0
  6. data/app/controllers/billings_controller.rb +25 -0
  7. data/app/controllers/invitations_controller.rb +65 -0
  8. data/app/controllers/memberships_controller.rb +45 -0
  9. data/app/controllers/plans_controller.rb +24 -0
  10. data/app/controllers/profiles_controller.rb +19 -0
  11. data/app/helpers/limits_helper.rb +13 -0
  12. data/app/mailers/billing_mailer.rb +53 -0
  13. data/app/mailers/invitation_mailer.rb +18 -0
  14. data/app/models/invitation.rb +113 -0
  15. data/app/models/limit.rb +49 -0
  16. data/app/models/membership.rb +26 -0
  17. data/app/models/permission.rb +19 -0
  18. data/app/models/signup.rb +163 -0
  19. data/app/views/accounts/_account.html.erb +9 -0
  20. data/app/views/accounts/_blank_slate.html.erb +6 -0
  21. data/app/views/accounts/_projects.html.erb +12 -0
  22. data/app/views/accounts/_subnav.html.erb +10 -0
  23. data/app/views/accounts/edit.html.erb +34 -0
  24. data/app/views/accounts/index.html.erb +9 -0
  25. data/app/views/accounts/new.html.erb +36 -0
  26. data/app/views/billing_mailer/completed_trial.text.erb +13 -0
  27. data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
  28. data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
  29. data/app/views/billing_mailer/problem.html.erb +13 -0
  30. data/app/views/billing_mailer/problem.text.erb +14 -0
  31. data/app/views/billing_mailer/receipt.html.erb +41 -0
  32. data/app/views/billing_mailer/receipt.text.erb +25 -0
  33. data/app/views/billings/_form.html.erb +8 -0
  34. data/app/views/billings/edit.html.erb +13 -0
  35. data/app/views/billings/show.html.erb +29 -0
  36. data/app/views/invitation_mailer/invitation.text.erb +6 -0
  37. data/app/views/invitations/new.html.erb +17 -0
  38. data/app/views/invitations/show.html.erb +22 -0
  39. data/app/views/layouts/saucy.html.erb +36 -0
  40. data/app/views/limits/_meter.html.erb +13 -0
  41. data/app/views/memberships/edit.html.erb +21 -0
  42. data/app/views/memberships/index.html.erb +17 -0
  43. data/app/views/plans/_plan.html.erb +32 -0
  44. data/app/views/plans/_terms.html.erb +15 -0
  45. data/app/views/plans/edit.html.erb +33 -0
  46. data/app/views/plans/index.html.erb +12 -0
  47. data/app/views/profiles/_inputs.html.erb +5 -0
  48. data/app/views/profiles/edit.html.erb +36 -0
  49. data/app/views/projects/_form.html.erb +36 -0
  50. data/app/views/projects/edit.html.erb +22 -0
  51. data/app/views/projects/index.html.erb +28 -0
  52. data/app/views/projects/new.html.erb +13 -0
  53. data/app/views/projects/show.html.erb +0 -0
  54. data/app/views/shared/_project_dropdown.html.erb +55 -0
  55. data/app/views/shared/_saucy_javascript.html.erb +33 -0
  56. data/config/locales/en.yml +37 -0
  57. data/config/routes.rb +19 -0
  58. data/features/run_features.feature +83 -0
  59. data/features/step_definitions/clearance_steps.rb +45 -0
  60. data/features/step_definitions/rails_steps.rb +73 -0
  61. data/features/step_definitions/saucy_steps.rb +8 -0
  62. data/features/support/env.rb +4 -0
  63. data/features/support/file.rb +11 -0
  64. data/lib/generators/saucy/base.rb +18 -0
  65. data/lib/generators/saucy/features/features_generator.rb +91 -0
  66. data/lib/generators/saucy/features/templates/README +3 -0
  67. data/lib/generators/saucy/features/templates/factories.rb +71 -0
  68. data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
  69. data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
  70. data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
  71. data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
  72. data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
  73. data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
  74. data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
  75. data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
  76. data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
  77. data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
  78. data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
  79. data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
  80. data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
  81. data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
  82. data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
  83. data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
  84. data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
  85. data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
  86. data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
  87. data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
  88. data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
  89. data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
  90. data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
  91. data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
  92. data/lib/generators/saucy/install/install_generator.rb +40 -0
  93. data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
  94. data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
  95. data/lib/generators/saucy/install/templates/models/account.rb +3 -0
  96. data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
  97. data/lib/generators/saucy/install/templates/models/project.rb +3 -0
  98. data/lib/generators/saucy/specs/specs_generator.rb +20 -0
  99. data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
  100. data/lib/generators/saucy/views/views_generator.rb +23 -0
  101. data/lib/saucy.rb +10 -0
  102. data/lib/saucy/account.rb +132 -0
  103. data/lib/saucy/account_authorization.rb +67 -0
  104. data/lib/saucy/configuration.rb +29 -0
  105. data/lib/saucy/engine.rb +35 -0
  106. data/lib/saucy/fake_braintree.rb +134 -0
  107. data/lib/saucy/layouts.rb +36 -0
  108. data/lib/saucy/plan.rb +54 -0
  109. data/lib/saucy/project.rb +125 -0
  110. data/lib/saucy/projects_controller.rb +94 -0
  111. data/lib/saucy/railties/tasks.rake +28 -0
  112. data/lib/saucy/routing_extensions.rb +121 -0
  113. data/lib/saucy/subscription.rb +237 -0
  114. data/lib/saucy/user.rb +30 -0
  115. data/spec/controllers/accounts_controller_spec.rb +228 -0
  116. data/spec/controllers/application_controller_spec.rb +32 -0
  117. data/spec/controllers/invitations_controller_spec.rb +215 -0
  118. data/spec/controllers/memberships_controller_spec.rb +117 -0
  119. data/spec/controllers/plans_controller_spec.rb +13 -0
  120. data/spec/controllers/profiles_controller_spec.rb +48 -0
  121. data/spec/controllers/projects_controller_spec.rb +216 -0
  122. data/spec/environment.rb +95 -0
  123. data/spec/layouts_spec.rb +21 -0
  124. data/spec/mailers/billing_mailer_spec.rb +68 -0
  125. data/spec/mailers/invitiation_mailer_spec.rb +19 -0
  126. data/spec/models/account_spec.rb +218 -0
  127. data/spec/models/invitation_spec.rb +320 -0
  128. data/spec/models/limit_spec.rb +70 -0
  129. data/spec/models/membership_spec.rb +37 -0
  130. data/spec/models/permission_spec.rb +30 -0
  131. data/spec/models/plan_spec.rb +81 -0
  132. data/spec/models/project_spec.rb +223 -0
  133. data/spec/models/signup_spec.rb +177 -0
  134. data/spec/models/subscription_spec.rb +481 -0
  135. data/spec/models/user_spec.rb +72 -0
  136. data/spec/route_extensions_spec.rb +51 -0
  137. data/spec/saucy_spec.rb +62 -0
  138. data/spec/scaffold/config/routes.rb +5 -0
  139. data/spec/spec_helper.rb +39 -0
  140. data/spec/support/authentication_helpers.rb +81 -0
  141. data/spec/support/authorization_helpers.rb +56 -0
  142. data/spec/support/braintree.rb +7 -0
  143. data/spec/support/clearance_matchers.rb +55 -0
  144. data/spec/support/notifications.rb +57 -0
  145. data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
  146. metadata +325 -0
@@ -0,0 +1,94 @@
1
+ module Saucy
2
+ module ProjectsController
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_project?
7
+ before_filter :authorize
8
+ before_filter :authorize_member, :only => :show
9
+ before_filter :authorize_admin, :except => [:show]
10
+ before_filter :ensure_active_account, :only => [:show, :destroy, :index]
11
+ before_filter :ensure_account_within_projects_limit, :only => [:new, :create]
12
+ before_filter :ensure_admin_for_project_account, :only => [:edit, :create, :update]
13
+ layout Saucy::Layouts.to_proc
14
+ end
15
+
16
+ module InstanceMethods
17
+ def new
18
+ @project = current_account.projects.build_with_default_permissions
19
+ if @project.keyword.blank?
20
+ @project.keyword = 'keyword'
21
+ end
22
+ end
23
+
24
+ def create
25
+ @project = current_account.projects.build(params[:project])
26
+ if @project.save
27
+ flash[:notice] = "Project successfully created"
28
+ redirect_to project_url(@project)
29
+ else
30
+ render :action => :new
31
+ end
32
+ end
33
+
34
+ def edit
35
+ @project = current_project
36
+ set_project_account_if_moving
37
+ end
38
+
39
+ def update
40
+ @project = current_project
41
+ set_project_account_if_moving
42
+ if @project.update_attributes params[:project]
43
+ flash[:success] = 'Project was updated.'
44
+ redirect_to account_projects_url(@project.account)
45
+ else
46
+ render :action => :edit
47
+ end
48
+ end
49
+
50
+ def show
51
+ current_project
52
+ end
53
+
54
+ def destroy
55
+ current_project.destroy
56
+ flash[:success] = "Project has been deleted"
57
+ redirect_to account_projects_url(current_project.account)
58
+ end
59
+
60
+ def index
61
+ @active_projects = current_account.projects.active
62
+ @archived_projects = current_account.projects.archived
63
+ end
64
+
65
+ private
66
+
67
+ def set_project_account_if_moving
68
+ if params[:project] && params[:project][:account_id]
69
+ @project.account_id = params[:project][:account_id]
70
+ end
71
+ end
72
+
73
+ def current_project
74
+ @project ||= current_account.projects.find_by_keyword!(params[:id])
75
+ end
76
+
77
+ def current_project?
78
+ params[:id].present?
79
+ end
80
+
81
+ def ensure_account_within_projects_limit
82
+ ensure_account_within_limit("projects")
83
+ end
84
+
85
+ def ensure_admin_for_project_account
86
+ if params[:project] && params[:project][:account_id]
87
+ if !current_user.admin_of?(::Account.find(params[:project][:account_id]))
88
+ redirect_to account_projects_url(current_account), :alert => t('account.permission_denied', :default => "You do not have permission to this account.")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,28 @@
1
+ namespace :saucy do
2
+ desc "Updates subscription status and delivers receipt/problem notices"
3
+ task :update_subscriptions => :environment do
4
+ Account.update_subscriptions!
5
+ end
6
+
7
+ desc "Deliver notifications for users that have signed up and aren't activated"
8
+ task :ask_users_to_activate => :environment do
9
+ Account.deliver_new_unactivated_notifications
10
+ end
11
+
12
+ desc "Deliver notifications to users with expiring trial accounts"
13
+ task :deliver_expiring_trial_notifications => :environment do
14
+ Account.deliver_expiring_trial_notifications
15
+ end
16
+
17
+ desc "Deliver notifications to users with completed trial accounts"
18
+ task :deliver_completed_trial_notifications => :environment do
19
+ Account.deliver_completed_trial_notifications
20
+ end
21
+
22
+ desc "Run all daily tasks"
23
+ task :daily => [:update_subscriptions,
24
+ :ask_users_to_activate,
25
+ :deliver_expiring_trial_notifications,
26
+ :deliver_completed_trial_notifications]
27
+ end
28
+
@@ -0,0 +1,121 @@
1
+ # This set of hacks extends Rails routing so that we can define pretty, unique,
2
+ # urls for account resources without having to specify every nested resource
3
+ # every time we generate a url.
4
+ #
5
+ # For example, you can generate /accounts/thoughtbot/projects/hoptoad from
6
+ # project_path(@project), because the account can be inferred from the project.
7
+ module Saucy
8
+ module MapperExtensions
9
+ def initialize(*args)
10
+ @through_scope = []
11
+ super
12
+ end
13
+
14
+ def through(parent, &block)
15
+ @through_scope << parent
16
+ resources(parent, :only => [], &block)
17
+ @through_scope.pop
18
+ end
19
+
20
+ end
21
+
22
+ class ThroughAlias
23
+ attr_reader :route, :through
24
+
25
+ def initialize(route, through)
26
+ @route = route
27
+ @through = through
28
+ end
29
+
30
+ def to_method(kind)
31
+ return <<-RUBY
32
+ def #{alias_name}_#{kind}(#{arguments.last}, options = {})
33
+ #{route_name}_#{kind}(#{arguments.join(', ')}, options)
34
+ end
35
+ RUBY
36
+ end
37
+
38
+ private
39
+
40
+ def alias_name
41
+ @alias_name ||= through.inject(route_name) do |name, through|
42
+ prefix = "#{through.to_s.singularize}_"
43
+ name.sub(/^(new_|edit_|)#{Regexp.escape(prefix)}/, '\1')
44
+ end
45
+ end
46
+
47
+ def arguments
48
+ @arguments ||= build_arguments
49
+ end
50
+
51
+ def build_arguments
52
+ other_segments = segments.dup
53
+ first_segment = other_segments.shift
54
+ other_segments.inject([first_segment]) { |result, member|
55
+ result << "#{result.last}.#{member}"
56
+ }.reverse
57
+ end
58
+
59
+ def segments
60
+ parent_segments = through.map { |parent| parent.to_s.singularize }.reverse
61
+ if include_self?
62
+ [alias_name] + parent_segments
63
+ else
64
+ parent_segments
65
+ end
66
+ end
67
+
68
+ def route_name
69
+ route.name
70
+ end
71
+
72
+ def include_self?
73
+ route.segment_keys.include?(:id)
74
+ end
75
+ end
76
+ end
77
+
78
+ ActionDispatch::Routing::Mapper.class_eval do
79
+ include Saucy::MapperExtensions
80
+ end
81
+
82
+ ActionDispatch::Routing::Mapper::Base.class_eval do
83
+ def match_with_through(path, options=nil)
84
+ match_without_through(path, options)
85
+ unless @through_scope.empty?
86
+ route = @set.routes.last
87
+ @set.named_routes.add_through_alias(route, @through_scope) if route.name
88
+ end
89
+ self
90
+ end
91
+
92
+ alias_method_chain :match, :through
93
+ end
94
+
95
+ ActionDispatch::Routing::RouteSet::NamedRouteCollection.class_eval do
96
+ attr_reader :through_aliases
97
+
98
+ def clear_with_through_aliases!
99
+ clear_without_through_aliases!
100
+ @through_aliases = []
101
+ end
102
+ alias_method_chain :clear!, :through_aliases
103
+
104
+ def reset_with_through_aliases!
105
+ old_through_aliases = through_aliases.dup
106
+ reset_without_through_aliases!
107
+ old_through_aliases.each do |through_alias|
108
+ add_through_alias through_alias.route, through_alias.through
109
+ end
110
+ end
111
+ alias_method_chain :reset!, :through_aliases
112
+
113
+ def add_through_alias(route, through)
114
+ @through_aliases ||= []
115
+ through_alias = Saucy::ThroughAlias.new(route, through)
116
+ @through_aliases << through_alias
117
+ @module.module_eval through_alias.to_method('path')
118
+ @module.module_eval through_alias.to_method('url')
119
+ end
120
+ end
121
+
@@ -0,0 +1,237 @@
1
+ module Saucy
2
+ module Subscription
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ require "rubygems"
7
+ require "braintree"
8
+
9
+ extend ActiveSupport::Memoizable
10
+
11
+ CUSTOMER_ATTRIBUTES = { :cardholder_name => :cardholder_name,
12
+ :billing_email => :email,
13
+ :card_number => :number,
14
+ :expiration_month => :expiration_month,
15
+ :expiration_year => :expiration_year,
16
+ :verification_code => :cvv }
17
+
18
+ attr_accessor *CUSTOMER_ATTRIBUTES.keys
19
+
20
+ CUSTOMER_ATTRIBUTES.keys.each do |attribute|
21
+ validates_presence_of attribute, :if => :switching_to_billed?
22
+ end
23
+ before_create :create_customer
24
+ before_create :create_subscription, :if => :billed?
25
+ after_destroy :destroy_customer
26
+
27
+ memoize :customer
28
+ memoize :subscription
29
+ end
30
+
31
+ module InstanceMethods
32
+ def customer
33
+ Braintree::Customer.find(customer_token) if customer_token
34
+ end
35
+
36
+ def credit_card
37
+ customer.credit_cards[0] if customer && customer.credit_cards.any?
38
+ end
39
+
40
+ def subscription
41
+ Braintree::Subscription.find(subscription_token) if subscription_token
42
+ end
43
+
44
+ def save_customer_and_subscription!(attributes)
45
+ successful = true
46
+ self.plan = ::Plan.find(attributes[:plan_id]) if changing_plan?(attributes)
47
+ if changing_customer_attributes?(attributes)
48
+ successful = update_customer(attributes)
49
+ end
50
+ if successful && past_due?
51
+ successful = retry_subscription_charge!
52
+ end
53
+ if successful && changing_plan?(attributes)
54
+ save_subscription
55
+ flush_cache :subscription
56
+ end
57
+ successful && save
58
+ end
59
+
60
+ def can_change_plan_to?(new_plan)
61
+ within_limits_for?(new_plan) && !past_trial_for?(new_plan)
62
+ end
63
+
64
+ def past_due?
65
+ subscription_status == Braintree::Subscription::Status::PastDue
66
+ end
67
+
68
+ private
69
+
70
+ def within_limits_for?(new_plan)
71
+ new_plan.limits.where(:value_type => :number).all? do |limit|
72
+ new_plan.limit(limit.name).value >= send(:"#{limit.name}_count")
73
+ end
74
+ end
75
+
76
+ def past_trial_for?(new_plan)
77
+ new_plan.trial? && past_trial?
78
+ end
79
+
80
+ def retry_subscription_charge!
81
+ authorized_transaction = Braintree::Subscription.retry_charge(subscription.id).transaction
82
+ result = Braintree::Transaction.submit_for_settlement(authorized_transaction.id)
83
+ handle_errors(authorized_transaction, result.errors) if !result.success?
84
+ update_subscription_cache!
85
+ result.success?
86
+ end
87
+
88
+ def update_subscription_cache!
89
+ flush_cache :subscription
90
+ update_attribute(:subscription_status, subscription.status)
91
+ update_attribute(:next_billing_date, subscription.next_billing_date)
92
+ end
93
+
94
+ def changing_plan?(attributes)
95
+ attributes[:plan_id].present?
96
+ end
97
+
98
+ def changing_customer_attributes?(attributes)
99
+ CUSTOMER_ATTRIBUTES.keys.any? { |attribute| attributes[attribute].present? }
100
+ end
101
+
102
+ def set_customer_attributes(attributes)
103
+ CUSTOMER_ATTRIBUTES.keys.each do |attribute|
104
+ send("#{attribute}=", attributes[attribute]) if attributes[attribute].present?
105
+ end
106
+ end
107
+
108
+ def update_customer(attributes)
109
+ set_customer_attributes(attributes)
110
+ if valid?
111
+ result = Braintree::Customer.update(customer_token, customer_attributes)
112
+ handle_customer_result(result)
113
+ result.success?
114
+ end
115
+ end
116
+
117
+ def save_subscription
118
+ if subscription
119
+ Braintree::Subscription.update(subscription_token, :plan_id => plan_id)
120
+ elsif plan.billed?
121
+ valid? && create_subscription
122
+ end
123
+ end
124
+
125
+ def customer_attributes
126
+ {
127
+ :email => billing_email,
128
+ :credit_card => credit_card_attributes
129
+ }
130
+ end
131
+
132
+ def credit_card_attributes
133
+ if plan.billed?
134
+ card_attributes = { :cardholder_name => cardholder_name,
135
+ :number => card_number,
136
+ :expiration_month => expiration_month,
137
+ :expiration_year => expiration_year,
138
+ :cvv => verification_code }
139
+ if credit_card
140
+ card_attributes.merge!(:options => credit_card_options)
141
+ end
142
+ card_attributes
143
+ else
144
+ {}
145
+ end
146
+ end
147
+
148
+ def credit_card_options
149
+ if customer && customer.credit_cards.any?
150
+ { :update_existing_token => credit_card.token }
151
+ else
152
+ {}
153
+ end
154
+ end
155
+
156
+ def create_customer
157
+ result = Braintree::Customer.create(customer_attributes)
158
+ handle_customer_result(result)
159
+ end
160
+
161
+ def destroy_customer
162
+ Braintree::Customer.delete(customer_token)
163
+ end
164
+
165
+ def handle_customer_result(result)
166
+ if result.success?
167
+ self.customer_token = result.customer.id
168
+ flush_cache :customer
169
+ else
170
+ handle_errors(result.credit_card_verification, result.errors)
171
+ end
172
+ result.success?
173
+ end
174
+
175
+ def handle_errors(result, remote_errors)
176
+ if result && result.status == "processor_declined"
177
+ errors[:card_number] << "was denied by the payment processor with the message: #{result.processor_response_text}"
178
+ elsif result && result.status == "gateway_rejected"
179
+ errors[:verification_code] << "did not match"
180
+ elsif remote_errors.any?
181
+ remote_errors.each do |error|
182
+ if error.attribute == "number"
183
+ errors[:card_number] << error.message.gsub("Credit card number ", "")
184
+ elsif error.attribute == "CVV"
185
+ errors[:verification_code] << error.message.gsub("CVV ", "")
186
+ elsif error.attribute == "expiration_month"
187
+ errors[:expiration_month] << error.message.gsub("Expiration month ", "")
188
+ elsif error.attribute == "expiration_year"
189
+ errors[:expiration_year] << error.message.gsub("Expiration year ", "")
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def create_subscription
196
+ result = Braintree::Subscription.create(subscription_attributes)
197
+ if result.success?
198
+ self.subscription_token = result.subscription.id
199
+ self.next_billing_date = result.subscription.next_billing_date
200
+ self.subscription_status = result.subscription.status
201
+ else
202
+ false
203
+ end
204
+ end
205
+
206
+ def subscription_attributes
207
+ {
208
+ :payment_method_token => credit_card.token,
209
+ :plan_id => plan_id,
210
+ :merchant_account_id => Saucy::Configuration.merchant_account_id
211
+ }.tap do |attributes|
212
+ attributes.reject! { |key, value| value.nil? }
213
+ end
214
+ end
215
+
216
+ def switching_to_billed?
217
+ plan_id && plan.billed? && subscription_token.blank?
218
+ end
219
+ end
220
+
221
+ module ClassMethods
222
+ def update_subscriptions!
223
+ recently_billed = where("next_billing_date <= ?", Time.now)
224
+ recently_billed.each do |account|
225
+ account.subscription_status = account.subscription.status
226
+ account.next_billing_date = account.subscription.next_billing_date
227
+ account.save!
228
+ if account.past_due?
229
+ BillingMailer.problem(account, account.subscription.transactions.last).deliver!
230
+ else
231
+ BillingMailer.receipt(account, account.subscription.transactions.last).deliver!
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end