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.
- data/CHANGELOG.md +114 -0
- data/Gemfile +26 -0
- data/README.md +118 -0
- data/Rakefile +38 -0
- data/app/controllers/accounts_controller.rb +68 -0
- data/app/controllers/billings_controller.rb +25 -0
- data/app/controllers/invitations_controller.rb +65 -0
- data/app/controllers/memberships_controller.rb +45 -0
- data/app/controllers/plans_controller.rb +24 -0
- data/app/controllers/profiles_controller.rb +19 -0
- data/app/helpers/limits_helper.rb +13 -0
- data/app/mailers/billing_mailer.rb +53 -0
- data/app/mailers/invitation_mailer.rb +18 -0
- data/app/models/invitation.rb +113 -0
- data/app/models/limit.rb +49 -0
- data/app/models/membership.rb +26 -0
- data/app/models/permission.rb +19 -0
- data/app/models/signup.rb +163 -0
- data/app/views/accounts/_account.html.erb +9 -0
- data/app/views/accounts/_blank_slate.html.erb +6 -0
- data/app/views/accounts/_projects.html.erb +12 -0
- data/app/views/accounts/_subnav.html.erb +10 -0
- data/app/views/accounts/edit.html.erb +34 -0
- data/app/views/accounts/index.html.erb +9 -0
- data/app/views/accounts/new.html.erb +36 -0
- data/app/views/billing_mailer/completed_trial.text.erb +13 -0
- data/app/views/billing_mailer/expiring_trial.text.erb +15 -0
- data/app/views/billing_mailer/new_unactivated.text.erb +1 -0
- data/app/views/billing_mailer/problem.html.erb +13 -0
- data/app/views/billing_mailer/problem.text.erb +14 -0
- data/app/views/billing_mailer/receipt.html.erb +41 -0
- data/app/views/billing_mailer/receipt.text.erb +25 -0
- data/app/views/billings/_form.html.erb +8 -0
- data/app/views/billings/edit.html.erb +13 -0
- data/app/views/billings/show.html.erb +29 -0
- data/app/views/invitation_mailer/invitation.text.erb +6 -0
- data/app/views/invitations/new.html.erb +17 -0
- data/app/views/invitations/show.html.erb +22 -0
- data/app/views/layouts/saucy.html.erb +36 -0
- data/app/views/limits/_meter.html.erb +13 -0
- data/app/views/memberships/edit.html.erb +21 -0
- data/app/views/memberships/index.html.erb +17 -0
- data/app/views/plans/_plan.html.erb +32 -0
- data/app/views/plans/_terms.html.erb +15 -0
- data/app/views/plans/edit.html.erb +33 -0
- data/app/views/plans/index.html.erb +12 -0
- data/app/views/profiles/_inputs.html.erb +5 -0
- data/app/views/profiles/edit.html.erb +36 -0
- data/app/views/projects/_form.html.erb +36 -0
- data/app/views/projects/edit.html.erb +22 -0
- data/app/views/projects/index.html.erb +28 -0
- data/app/views/projects/new.html.erb +13 -0
- data/app/views/projects/show.html.erb +0 -0
- data/app/views/shared/_project_dropdown.html.erb +55 -0
- data/app/views/shared/_saucy_javascript.html.erb +33 -0
- data/config/locales/en.yml +37 -0
- data/config/routes.rb +19 -0
- data/features/run_features.feature +83 -0
- data/features/step_definitions/clearance_steps.rb +45 -0
- data/features/step_definitions/rails_steps.rb +73 -0
- data/features/step_definitions/saucy_steps.rb +8 -0
- data/features/support/env.rb +4 -0
- data/features/support/file.rb +11 -0
- data/lib/generators/saucy/base.rb +18 -0
- data/lib/generators/saucy/features/features_generator.rb +91 -0
- data/lib/generators/saucy/features/templates/README +3 -0
- data/lib/generators/saucy/features/templates/factories.rb +71 -0
- data/lib/generators/saucy/features/templates/features/edit_profile.feature +9 -0
- data/lib/generators/saucy/features/templates/features/edit_project_permissions.feature +37 -0
- data/lib/generators/saucy/features/templates/features/edit_user_permissions.feature +47 -0
- data/lib/generators/saucy/features/templates/features/manage_account.feature +35 -0
- data/lib/generators/saucy/features/templates/features/manage_billing.feature +93 -0
- data/lib/generators/saucy/features/templates/features/manage_plan.feature +143 -0
- data/lib/generators/saucy/features/templates/features/manage_projects.feature +139 -0
- data/lib/generators/saucy/features/templates/features/manage_users.feature +142 -0
- data/lib/generators/saucy/features/templates/features/new_account.feature +33 -0
- data/lib/generators/saucy/features/templates/features/project_dropdown.feature +77 -0
- data/lib/generators/saucy/features/templates/features/sign_up.feature +32 -0
- data/lib/generators/saucy/features/templates/features/sign_up_paid.feature +65 -0
- data/lib/generators/saucy/features/templates/features/trial_plans.feature +82 -0
- data/lib/generators/saucy/features/templates/step_definitions/account_steps.rb +30 -0
- data/lib/generators/saucy/features/templates/step_definitions/braintree_steps.rb +25 -0
- data/lib/generators/saucy/features/templates/step_definitions/cron_steps.rb +23 -0
- data/lib/generators/saucy/features/templates/step_definitions/email_steps.rb +40 -0
- data/lib/generators/saucy/features/templates/step_definitions/factory_girl_steps.rb +1 -0
- data/lib/generators/saucy/features/templates/step_definitions/html_steps.rb +51 -0
- data/lib/generators/saucy/features/templates/step_definitions/plan_steps.rb +16 -0
- data/lib/generators/saucy/features/templates/step_definitions/project_steps.rb +4 -0
- data/lib/generators/saucy/features/templates/step_definitions/session_steps.rb +37 -0
- data/lib/generators/saucy/features/templates/step_definitions/user_steps.rb +100 -0
- data/lib/generators/saucy/features/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/install/install_generator.rb +40 -0
- data/lib/generators/saucy/install/templates/controllers/projects_controller.rb +3 -0
- data/lib/generators/saucy/install/templates/create_saucy_tables.rb +115 -0
- data/lib/generators/saucy/install/templates/models/account.rb +3 -0
- data/lib/generators/saucy/install/templates/models/plan.rb +3 -0
- data/lib/generators/saucy/install/templates/models/project.rb +3 -0
- data/lib/generators/saucy/specs/specs_generator.rb +20 -0
- data/lib/generators/saucy/specs/templates/support/braintree.rb +5 -0
- data/lib/generators/saucy/views/views_generator.rb +23 -0
- data/lib/saucy.rb +10 -0
- data/lib/saucy/account.rb +132 -0
- data/lib/saucy/account_authorization.rb +67 -0
- data/lib/saucy/configuration.rb +29 -0
- data/lib/saucy/engine.rb +35 -0
- data/lib/saucy/fake_braintree.rb +134 -0
- data/lib/saucy/layouts.rb +36 -0
- data/lib/saucy/plan.rb +54 -0
- data/lib/saucy/project.rb +125 -0
- data/lib/saucy/projects_controller.rb +94 -0
- data/lib/saucy/railties/tasks.rake +28 -0
- data/lib/saucy/routing_extensions.rb +121 -0
- data/lib/saucy/subscription.rb +237 -0
- data/lib/saucy/user.rb +30 -0
- data/spec/controllers/accounts_controller_spec.rb +228 -0
- data/spec/controllers/application_controller_spec.rb +32 -0
- data/spec/controllers/invitations_controller_spec.rb +215 -0
- data/spec/controllers/memberships_controller_spec.rb +117 -0
- data/spec/controllers/plans_controller_spec.rb +13 -0
- data/spec/controllers/profiles_controller_spec.rb +48 -0
- data/spec/controllers/projects_controller_spec.rb +216 -0
- data/spec/environment.rb +95 -0
- data/spec/layouts_spec.rb +21 -0
- data/spec/mailers/billing_mailer_spec.rb +68 -0
- data/spec/mailers/invitiation_mailer_spec.rb +19 -0
- data/spec/models/account_spec.rb +218 -0
- data/spec/models/invitation_spec.rb +320 -0
- data/spec/models/limit_spec.rb +70 -0
- data/spec/models/membership_spec.rb +37 -0
- data/spec/models/permission_spec.rb +30 -0
- data/spec/models/plan_spec.rb +81 -0
- data/spec/models/project_spec.rb +223 -0
- data/spec/models/signup_spec.rb +177 -0
- data/spec/models/subscription_spec.rb +481 -0
- data/spec/models/user_spec.rb +72 -0
- data/spec/route_extensions_spec.rb +51 -0
- data/spec/saucy_spec.rb +62 -0
- data/spec/scaffold/config/routes.rb +5 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/authentication_helpers.rb +81 -0
- data/spec/support/authorization_helpers.rb +56 -0
- data/spec/support/braintree.rb +7 -0
- data/spec/support/clearance_matchers.rb +55 -0
- data/spec/support/notifications.rb +57 -0
- data/spec/views/accounts/_account.html.erb_spec.rb +37 -0
- 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
|