saas 0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/generators/saas_features/USAGE +41 -0
- data/generators/saas_features/saas_features_generator.rb +18 -0
- data/generators/saas_features/templates/subscriber_helpers.rb +32 -0
- data/generators/saas_features/templates/subscription.feature +89 -0
- data/generators/saas_features/templates/subscription_helpers.rb +65 -0
- data/generators/saas_features/templates/subscription_steps.rb +74 -0
- data/generators/saas_features/templates/user_steps.rb +33 -0
- data/generators/saas_migration/USAGE +1 -0
- data/generators/saas_migration/saas_migration_generator.rb +12 -0
- data/generators/saas_migration/templates/migration.rb +67 -0
- data/generators/saasramp/USAGE +12 -0
- data/generators/saasramp/lib/insert_commands.rb +54 -0
- data/generators/saasramp/saasramp_generator.rb +23 -0
- data/generators/saasramp/templates/active_merchant/authorizenetcim.rb +272 -0
- data/generators/saasramp/templates/active_merchant/bogus.rb +48 -0
- data/generators/saasramp/templates/active_merchant/braintree.rb +16 -0
- data/generators/saasramp/templates/subscription.rb +10 -0
- data/generators/saasramp/templates/subscription.yml +53 -0
- data/generators/saasramp/templates/subscription_plans.yml +33 -0
- data/lib/engine.rb +27 -0
- data/lib/saas.rb +88 -0
- metadata +24 -14
@@ -0,0 +1,41 @@
|
|
1
|
+
Description:
|
2
|
+
This generator add Cucumber features to your application
|
3
|
+
|
4
|
+
Example:
|
5
|
+
./script/generate saas_features
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
|
9
|
+
|
10
|
+
You will also have to manually do:
|
11
|
+
add to features/support/env.rb
|
12
|
+
# global subscription plans fixtures
|
13
|
+
# see http://wiki.github.com/aslakhellesoy/cucumber/fixtures
|
14
|
+
# and http://wiki.github.com/aslakhellesoy/cucumber/hooks
|
15
|
+
raw = File.read( RAILS_ROOT + '/db/subscription_plans.yml' )
|
16
|
+
data = YAML.load(raw)[RAILS_ENV].symbolize_keys
|
17
|
+
data[:plans].each {|params| SubscriptionPlan.create( params ) }
|
18
|
+
at_exit do
|
19
|
+
SubscriptionPlan.destroy_all
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
add to features/support/path.rb
|
24
|
+
when /my profile/
|
25
|
+
user_path(:current)
|
26
|
+
when /my subscription/
|
27
|
+
subscription_path(:current)
|
28
|
+
when /my credit card/
|
29
|
+
credit_card_subscription_path(:current)
|
30
|
+
|
31
|
+
also, check the user_steps.rb file, it may define steps and methods you already have, it contains
|
32
|
+
Given /^a user "(.*)"$/
|
33
|
+
Given /^a user is logged in as "(.*)"$/
|
34
|
+
When /^I log in as "(.*)"$/
|
35
|
+
def create_user( options = {} )
|
36
|
+
def log_in_as( username )
|
37
|
+
|
38
|
+
finally, the scenarios expect the homepage has a link to "Sign up" page
|
39
|
+
|
40
|
+
|
41
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class SaasFeaturesGenerator < Rails::Generator::Base
|
2
|
+
|
3
|
+
def manifest
|
4
|
+
record do |m|
|
5
|
+
m.directory 'features'
|
6
|
+
m.file 'subscription.feature', 'features/subscription.feature'
|
7
|
+
|
8
|
+
m.directory 'features/step_definitions'
|
9
|
+
m.file 'subscription_steps.rb', 'features/step_definitions/subscription_steps.rb'
|
10
|
+
|
11
|
+
m.directory 'features/support'
|
12
|
+
m.file 'subscription_helpers.rb', 'features/support/subscription_helpers.rb'
|
13
|
+
m.file 'subscriber_helpers.rb', 'features/support/subscriber_helpers.rb'
|
14
|
+
|
15
|
+
# cancel
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SubscriberHelpers
|
2
|
+
|
3
|
+
# change these as needed by your app
|
4
|
+
# e.g. i'm assuming here that User acts_as_subscriber
|
5
|
+
|
6
|
+
def create_subscriber( options = {} )
|
7
|
+
args = {
|
8
|
+
:username => 'subscriber',
|
9
|
+
:password => 'secret',
|
10
|
+
:password_confirmation => 'secret',
|
11
|
+
}.merge( options )
|
12
|
+
args[:email] ||= "#{args[:username]}@example.com"
|
13
|
+
|
14
|
+
subscriber = User.create!(args)
|
15
|
+
# :create syntax for restful_authentication w/ aasm. Tweak as needed.
|
16
|
+
# user.activate!
|
17
|
+
subscriber
|
18
|
+
end
|
19
|
+
|
20
|
+
def log_in_subscriber
|
21
|
+
visit "/login"
|
22
|
+
fill_in("user_session_username", :with => 'subscriber')
|
23
|
+
fill_in("password", :with => 'secret')
|
24
|
+
click_button("Log in")
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_subscriber
|
28
|
+
User.find_by_username('subscriber')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
World(SubscriberHelpers)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
Feature: Subscription
|
2
|
+
In order to use subscription based services
|
3
|
+
As a subscriber
|
4
|
+
I want to maintain my subscription
|
5
|
+
|
6
|
+
Scenario: As a new user, my subscription page says I have a free account
|
7
|
+
Given a "free" subscriber is logged in
|
8
|
+
When I go to my subscription page
|
9
|
+
Then I should see "Free"
|
10
|
+
And I should see "(no credit card on file)"
|
11
|
+
#And show me the page
|
12
|
+
|
13
|
+
Scenario: I have a free account and sign up for a paid plan
|
14
|
+
Given a "free" subscriber is logged in
|
15
|
+
When I go to my plan page
|
16
|
+
#And show me the page
|
17
|
+
And I select "basic" from "Plan"
|
18
|
+
And I press "Change Plan"
|
19
|
+
#And show me the page
|
20
|
+
Then I should be on my subscription page
|
21
|
+
And I should see "Basic ($10.00 per month)"
|
22
|
+
And I should see "Trial"
|
23
|
+
And the subscription should have next renewal in 30 days
|
24
|
+
|
25
|
+
Scenario: I add credit card info
|
26
|
+
Given a "basic" subscriber who is in trial, with next renewal in 3 days, and profile has no info
|
27
|
+
When the subscriber logs in
|
28
|
+
And I go to my subscription page
|
29
|
+
Then I should see "(no credit card on file)"
|
30
|
+
When I press "Update Credit Card"
|
31
|
+
And I fill out the credit card form correctly
|
32
|
+
And I press "Submit"
|
33
|
+
#And show me the page
|
34
|
+
Then I should be on my subscription page
|
35
|
+
And I should see "Credit card info successfully updated. No charges have been made at this time."
|
36
|
+
And I should see "Bogus XXXX-XXXX-XXXX-1 Expires: 2012-10-31"
|
37
|
+
And the profile should be "authorized"
|
38
|
+
|
39
|
+
Scenario: Subscription is past due, I update credit card info
|
40
|
+
Given a "basic" subscriber who is past due, with next renewal 3 days ago, and profile has an error
|
41
|
+
When the subscriber logs in
|
42
|
+
And I go to my subscription page
|
43
|
+
#And show me the page
|
44
|
+
Then I should see "There was an error processing your credit card"
|
45
|
+
When I press "Update Credit Card"
|
46
|
+
And I fill out the credit card form correctly
|
47
|
+
And I press "Submit"
|
48
|
+
#And show me the page
|
49
|
+
Then I should be on my subscription page
|
50
|
+
And I should see "Thank you for your payment. Your credit card has been charged $10.00"
|
51
|
+
And I should see "Bogus XXXX-XXXX-XXXX-1 Expires: 2012-10-31"
|
52
|
+
And the profile should be "authorized"
|
53
|
+
And a "charge" transaction should be created
|
54
|
+
And the next renewal should be set to "original renewal plus 1 month"
|
55
|
+
|
56
|
+
Scenario: I can see my transaction history
|
57
|
+
Given a "basic" subscriber who is in trial, with next renewal is today, and profile has no info
|
58
|
+
When the subscriber logs in
|
59
|
+
And I go to my credit card page
|
60
|
+
And I fill out the credit card form correctly
|
61
|
+
And I press "Submit"
|
62
|
+
Then I should be on my subscription page
|
63
|
+
When I follow "History"
|
64
|
+
#And show me the page
|
65
|
+
Then I should see "Charge $10.00"
|
66
|
+
And I should see "Store"
|
67
|
+
|
68
|
+
Scenario: I upgrade to a higher cost plan
|
69
|
+
Given a "basic" subscriber who is active, with next renewal in 15 days, and profile is authorized
|
70
|
+
When the subscriber logs in
|
71
|
+
And I go to my plan page
|
72
|
+
And I select "pro" from "Plan"
|
73
|
+
And I press "Change Plan"
|
74
|
+
#And show me the page
|
75
|
+
Then I should be on my subscription page
|
76
|
+
And I should see "Your credit card has been charged $295.00"
|
77
|
+
And I should see "Pro"
|
78
|
+
And I should see "Active"
|
79
|
+
And the subscription should have next renewal in 1 year
|
80
|
+
|
81
|
+
Scenario: I cancel my account
|
82
|
+
Given a "basic" subscriber who is active, with next renewal in 15 days, and profile is authorized
|
83
|
+
When the subscriber logs in
|
84
|
+
And I go to my plan page
|
85
|
+
And I follow "I want to cancel my subscription"
|
86
|
+
#And show me the page
|
87
|
+
Then I should see "Your subscription has been canceled"
|
88
|
+
And I should see "Free"
|
89
|
+
And I should see "$-5.00"
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module SubscriptionHelpers
|
2
|
+
|
3
|
+
def set_subscription_state( subscriber, text )
|
4
|
+
unless ['pending', 'free', 'trial', 'active', 'past due'].include? text
|
5
|
+
puts "bad case in set_current_subscription_text"
|
6
|
+
return
|
7
|
+
end
|
8
|
+
subscriber.subscription.state = text.gsub(' ','_')
|
9
|
+
# past due will have a balance due, so set it to the plan rate
|
10
|
+
subscriber.subscription.balance = subscriber.subscription.plan.rate if subscriber.subscription.past_due?
|
11
|
+
# assume active and past due were created more than 30 days ago
|
12
|
+
subscriber.subscription.created_at = Time.zone.now - 60.days
|
13
|
+
subscriber.subscription.save
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_renewal( subscriber, text )
|
17
|
+
subscriber.subscription.next_renewal_on = text_to_date(text)
|
18
|
+
subscriber.subscription.save
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_profile_state( subscriber, text )
|
22
|
+
profile = subscriber.subscription.profile || subscriber.subscription.build_profile
|
23
|
+
case text
|
24
|
+
when /no info/
|
25
|
+
profile.state = 'no_info'
|
26
|
+
when /authorized/, /error/
|
27
|
+
params = SubscriptionProfile.example_credit_card_params
|
28
|
+
profile.credit_card = params
|
29
|
+
#profile.request_ip = request.remote_ip
|
30
|
+
ok = profile.save
|
31
|
+
ok.should be_true
|
32
|
+
if text =~ /error/
|
33
|
+
profile.state = 'error'
|
34
|
+
profile.save
|
35
|
+
end
|
36
|
+
else
|
37
|
+
puts 'bad case in set_current_profile_state'
|
38
|
+
end
|
39
|
+
profile.save
|
40
|
+
end
|
41
|
+
|
42
|
+
def text_to_date(text)
|
43
|
+
today = Time.zone.today
|
44
|
+
case text
|
45
|
+
when /original renewal plus 1 month/
|
46
|
+
@original_renewal + 1.month
|
47
|
+
when /today/
|
48
|
+
today
|
49
|
+
when /in (\d+) days/
|
50
|
+
today + ($1).to_i.days
|
51
|
+
when /(\d+) days ago/
|
52
|
+
today - ($1).to_i.days
|
53
|
+
when /in 1 month/
|
54
|
+
today + 1.month
|
55
|
+
when /in 1 year/
|
56
|
+
today + 1.year
|
57
|
+
when "blank"
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
puts 'bad case in text_to_date'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
World(SubscriptionHelpers)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
Given /^a "(.*)" subscriber$/ do |plan|
|
2
|
+
create_subscriber( :username => 'subscriber', :subscription_plan => plan )
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^a "(.*)" subscriber is logged in$/ do |plan|
|
6
|
+
create_subscriber( :username => 'subscriber', :subscription_plan => plan )
|
7
|
+
log_in_subscriber
|
8
|
+
end
|
9
|
+
|
10
|
+
Given /^a "(.*)" subscriber who is (in trial|active|past due|expired), with next renewal (.*), and profile (has no info|is authorized|has an error)$/ do |plan, subs_state, date_text, profile_state |
|
11
|
+
#Example: Given a "basic" subscriber who is in trial, next renewal is in 3 days, and profile has no info
|
12
|
+
subscriber = create_subscriber( :username => 'subscriber', :subscription_plan => plan )
|
13
|
+
set_subscription_state( subscriber, subs_state )
|
14
|
+
set_renewal( subscriber, date_text )
|
15
|
+
set_profile_state( subscriber, profile_state )
|
16
|
+
# beware this is not safe in all scenarios
|
17
|
+
@original_renewal = subscriber.subscription.next_renewal_on
|
18
|
+
end
|
19
|
+
|
20
|
+
When /^the subscriber logs in$/ do
|
21
|
+
log_in_subscriber
|
22
|
+
end
|
23
|
+
|
24
|
+
When /^I fill out the credit card form (correctly|with errors|with invalid card)$/ do |what|
|
25
|
+
case what
|
26
|
+
when 'correctly'
|
27
|
+
params = SubscriptionProfile.example_credit_card_params
|
28
|
+
when 'with errors'
|
29
|
+
params = SubscriptionProfile.example_credit_card_params( :first_name => '')
|
30
|
+
when 'with invalid card'
|
31
|
+
params = SubscriptionProfile.example_credit_card_params( :number => '2')
|
32
|
+
else
|
33
|
+
puts 'step error: unknown "what"'
|
34
|
+
end
|
35
|
+
params.each do |field, value|
|
36
|
+
name = "profile_credit_card_#{field}" #assuming view as form_for :profile ... form.feldsfor :credit_card
|
37
|
+
begin
|
38
|
+
select value, :from => name
|
39
|
+
rescue
|
40
|
+
fill_in name, :with => value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Then /^the subscription should be a "(.*)" plan$/ do |plan|
|
46
|
+
subscriber = find_subscriber
|
47
|
+
subscriber.subscription_plan.name.should == plan
|
48
|
+
end
|
49
|
+
|
50
|
+
Then /^the subscription should be in a "(.*)" state$/ do |state|
|
51
|
+
subscriber = find_subscriber
|
52
|
+
subscriber.subscription.state.should == state
|
53
|
+
end
|
54
|
+
|
55
|
+
Then /^the subscription should have next renewal (.*)$/ do |value|
|
56
|
+
subscriber = find_subscriber
|
57
|
+
subscriber.subscription.next_renewal_on.should == text_to_date(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
Then /^the profile should be "(no info|authorized|error)"$/ do |state|
|
61
|
+
subscriber = find_subscriber
|
62
|
+
subscriber.subscription.profile.state.to_s.should == state.downcase
|
63
|
+
end
|
64
|
+
|
65
|
+
Then /^a "(validate|store|update|unstore|charge|credit|refund)" transaction should be created$/ do |action|
|
66
|
+
subscriber = find_subscriber
|
67
|
+
subscriber.subscription.latest_transaction.action.should == action
|
68
|
+
end
|
69
|
+
|
70
|
+
Then /^the next renewal should be set to "(.*)"$/ do |text|
|
71
|
+
subscriber = find_subscriber
|
72
|
+
subscriber.subscription.next_renewal_on.should == text_to_date(text)
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Given /^a user "(.*)"$/ do |username|
|
2
|
+
@current_user = create_user( :username => username )
|
3
|
+
end
|
4
|
+
|
5
|
+
When /^I log in as "(.*)"$/ do |username|
|
6
|
+
log_in_as username
|
7
|
+
end
|
8
|
+
|
9
|
+
Given /^a user is logged in as "(.*)"$/ do |username|
|
10
|
+
@current_user = create_user( :username => username )
|
11
|
+
log_in_as username
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def create_user( options = {} )
|
16
|
+
args = {
|
17
|
+
:username => 'subscriber',
|
18
|
+
:password => 'secret',
|
19
|
+
:password_confirmation => 'secret',
|
20
|
+
}.merge( options )
|
21
|
+
args[:email] ||= "#{args[:username]}@example.com"
|
22
|
+
user = User.create!(args)
|
23
|
+
# :create syntax for restful_authentication w/ aasm. Tweak as needed.
|
24
|
+
# user.activate!
|
25
|
+
user
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_in_as( username )
|
29
|
+
visit "/login"
|
30
|
+
fill_in("user_session_username", :with => username)
|
31
|
+
fill_in("password", :with => 'secret')
|
32
|
+
click_button("Log in")
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
./script/generate saas_migration
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class SaasMigrationGenerator < Rails::Generator::NamedBase
|
2
|
+
def initialize(runtime_args, runtime_options = {})
|
3
|
+
runtime_args.insert(0, 'migrations')
|
4
|
+
super
|
5
|
+
end
|
6
|
+
|
7
|
+
def manifest
|
8
|
+
record do |m|
|
9
|
+
m.migration_template "migration.rb", "db/migrate", :migration_file_name => "create_subscription_etc"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class CreateSubscriptionEtc < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :subscription_plans, :force => true do |t|
|
4
|
+
t.string :name, :null => false
|
5
|
+
t.integer :rate_cents, :default => 0
|
6
|
+
t.integer :interval, :default => 1
|
7
|
+
t.timestamps
|
8
|
+
|
9
|
+
# add app-specific resource limitations, e.g.
|
10
|
+
# t.integer :max_users
|
11
|
+
# t.integer :max_memory
|
12
|
+
# t.integer :telephone_support
|
13
|
+
# NOTE: Should also add to subcriptions table if you want to change settings per account
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
create_table :subscriptions, :force => true do |t|
|
18
|
+
t.integer :subscriber_id, :null => false
|
19
|
+
t.string :subscriber_type, :null => false
|
20
|
+
t.integer :plan_id
|
21
|
+
t.string :state
|
22
|
+
t.date :next_renewal_on
|
23
|
+
t.integer :warning_level
|
24
|
+
t.integer :balance_cents, :default => 0
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :subscription_profiles, :force => true do |t|
|
29
|
+
t.integer :subscription_id
|
30
|
+
t.string :state
|
31
|
+
t.string :profile_key, :null => true
|
32
|
+
t.string :card_first_name
|
33
|
+
t.string :card_last_name
|
34
|
+
t.string :card_type
|
35
|
+
t.string :card_display_number
|
36
|
+
t.date :card_expires_on
|
37
|
+
# could also add address columns if required by your gateway
|
38
|
+
t.timestamps
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :subscription_transactions, :force => true do |t|
|
42
|
+
t.integer :subscription_id, :null => false
|
43
|
+
t.integer :amount_cents
|
44
|
+
t.boolean :success
|
45
|
+
t.string :reference
|
46
|
+
t.string :message
|
47
|
+
t.string :action
|
48
|
+
t.text :params
|
49
|
+
t.boolean :test
|
50
|
+
t.timestamps
|
51
|
+
end
|
52
|
+
|
53
|
+
# indexes
|
54
|
+
add_index :subscriptions, :subscriber_id
|
55
|
+
add_index :subscriptions, :subscriber_type
|
56
|
+
add_index :subscriptions, :state
|
57
|
+
add_index :subscriptions, :next_renewal_on
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.down
|
62
|
+
drop_table :subscription_profiles
|
63
|
+
drop_table :subscription_transactions
|
64
|
+
drop_table :subscriptions
|
65
|
+
drop_table :subscription_plans
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Description:
|
2
|
+
This generator bootstraps a Rails project for use with SaasRamp
|
3
|
+
|
4
|
+
Example:
|
5
|
+
./script/generate saasramp
|
6
|
+
|
7
|
+
This will create:
|
8
|
+
config/subscription.yml editable configuration file
|
9
|
+
config/initializers/subscription.rb initializer
|
10
|
+
db/subscription_plans.yml
|
11
|
+
config/initializers/active_merchant/
|
12
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# adapted from Ryan Bates nifty generators
|
2
|
+
|
3
|
+
Rails::Generator::Commands::Create.class_eval do
|
4
|
+
def route_resource_x(resource, options)
|
5
|
+
resource_list = [resource.to_sym.inspect, options.inspect].join(', ')
|
6
|
+
sentinel = 'ActionController::Routing::Routes.draw do |map|'
|
7
|
+
|
8
|
+
logger.route "map.resource #{resource_list}"
|
9
|
+
unless options[:pretend]
|
10
|
+
gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
|
11
|
+
"#{match}\n map.resource #{resource_list}\n"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def insert_into(file, line)
|
17
|
+
logger.insert "#{line} into #{file}"
|
18
|
+
unless options[:pretend]
|
19
|
+
gsub_file file, /^(class|module) .+$/ do |match|
|
20
|
+
"#{match}\n #{line}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
Rails::Generator::Commands::Destroy.class_eval do
|
28
|
+
def route_resource_x(resource, options)
|
29
|
+
resource_list = [resource.to_sym.inspect, options.inspect].join(', ')
|
30
|
+
look_for = "\n map.resource #{resource_list}\n"
|
31
|
+
logger.route "map.resource #{resource_list}"
|
32
|
+
unless options[:pretend]
|
33
|
+
gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def insert_into(file, line)
|
38
|
+
logger.remove "#{line} from #{file}"
|
39
|
+
unless options[:pretend]
|
40
|
+
gsub_file file, "\n #{line}", ''
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Rails::Generator::Commands::List.class_eval do
|
46
|
+
def route_resource_x(resource, options)
|
47
|
+
resource_list = [resource.to_sym.inspect, options.inspect].join(', ')
|
48
|
+
logger.route "map.resource #{resource_list}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def insert_into(file, line)
|
52
|
+
logger.insert "#{line} into #{file}"
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# this generator bootstraps a Rails project for use with SaasRamp
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + "/lib/insert_commands.rb")
|
3
|
+
class SaasrampGenerator < Rails::Generator::Base
|
4
|
+
def manifest
|
5
|
+
record do |m|
|
6
|
+
m.directory 'config'
|
7
|
+
m.file 'subscription.yml', 'config/subscription.yml'
|
8
|
+
|
9
|
+
m.directory 'db'
|
10
|
+
m.file 'subscription_plans.yml', 'db/subscription_plans.yml'
|
11
|
+
|
12
|
+
m.directory 'config/initializers'
|
13
|
+
m.file 'subscription.rb', 'config/initializers/subscription.rb'
|
14
|
+
|
15
|
+
m.directory 'config/initializers/active_merchant'
|
16
|
+
m.file 'active_merchant/bogus.rb', 'config/initializers/active_merchant/bogus.rb'
|
17
|
+
m.file 'active_merchant/braintree.rb', 'config/initializers/active_merchant/braintree.rb'
|
18
|
+
m.file 'active_merchant/authorizenetcim.rb', 'config/initializers/active_merchant/authorizenetcim.rb'
|
19
|
+
|
20
|
+
m.insert_into 'app/controllers/application_controller.rb', 'filter_parameter_logging :credit_card'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# this file represents major hacking on the gateway, culled from various sources
|
2
|
+
# hopefully we'll integrate it into the Authorize.net gateway itself and eliminate this gorilla monkey patching
|
3
|
+
# references:
|
4
|
+
# http://gist.github.com/147194
|
5
|
+
# https://jadedpixel.lighthouseapp.com/projects/11599/tickets/111-void-refund-and-auth_capture-for-authorizenets-cim-gateway#ticket-111-1
|
6
|
+
|
7
|
+
include ActiveMerchant::Billing
|
8
|
+
require 'digest/sha1'
|
9
|
+
|
10
|
+
class AuthorizeNetCimResponse < ActiveMerchant::Billing::Response
|
11
|
+
def token
|
12
|
+
@authorization ||
|
13
|
+
(@params['direct_response']['transaction_id'] if @params && @params['direct_response'])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveMerchant::Billing::AuthorizeNetCimGateway::Response = AuthorizeNetCimResponse
|
18
|
+
|
19
|
+
class ActiveMerchant::Billing::AuthorizeNetCimGateway
|
20
|
+
# module ActiveMerchant #:nodoc:
|
21
|
+
# module Billing #:nodoc:
|
22
|
+
# class AuthorizeNetCimGateway < Gateway
|
23
|
+
|
24
|
+
# redefine constant (bleh)
|
25
|
+
silence_warnings do
|
26
|
+
CIM_TRANSACTION_TYPES = {
|
27
|
+
:auth_capture => 'profileTransAuthCapture',
|
28
|
+
:auth_only => 'profileTransAuthOnly',
|
29
|
+
:capture_only => 'profileTransCaptureOnly',
|
30
|
+
# adding this
|
31
|
+
:void => 'profileTransVoid',
|
32
|
+
:refund => 'profileTransRefund'
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# NOTE TO SELF: this gateway has a validate_customer_payment_profile action
|
37
|
+
# It requires the profile id (and billing id) exists, so its for validating an existing profile
|
38
|
+
# Thus, cannot be used to validate a card -before- storing it (as saasramp expects in SubscriptionTransaction#validate_card)
|
39
|
+
|
40
|
+
# Create a payment profile
|
41
|
+
def store(creditcard, options = {})
|
42
|
+
profile = {
|
43
|
+
:payment_profiles => {
|
44
|
+
:payment => { :credit_card => creditcard }
|
45
|
+
}
|
46
|
+
}
|
47
|
+
profile[:payment_profiles][:bill_to] = options[:billing_address] if options[:billing_address]
|
48
|
+
profile[:ship_to_list] = options[:shipping_address] if options[:shipping_address]
|
49
|
+
|
50
|
+
# CIM actually does require a unique ID to be passed in,
|
51
|
+
# either merchant_customer_id or email, so generate it, if necessary
|
52
|
+
if options[:billing_id]
|
53
|
+
profile[:merchant_customer_id] = options[:billing_id]
|
54
|
+
elsif options[:email]
|
55
|
+
profile[:email] = options[:email]
|
56
|
+
else
|
57
|
+
profile[:merchant_customer_id] = Digest::SHA1.hexdigest("#{creditcard.number}#{Time.now.to_i}").first(20)
|
58
|
+
end
|
59
|
+
|
60
|
+
#create_customer_profile(:profile => profile)
|
61
|
+
create_customer_profile( {
|
62
|
+
:ref_id => rand(1_000_000),
|
63
|
+
:profile => profile
|
64
|
+
})
|
65
|
+
end
|
66
|
+
|
67
|
+
# Update an existing payment profile
|
68
|
+
def update(billing_id, creditcard, options = {})
|
69
|
+
if (response = get_customer_profile(:customer_profile_id => billing_id)).success?
|
70
|
+
update_customer_payment_profile(
|
71
|
+
:customer_profile_id => billing_id,
|
72
|
+
:payment_profile => {
|
73
|
+
:customer_payment_profile_id => response.params['profile']['payment_profiles']['customer_payment_profile_id'],
|
74
|
+
:payment => {
|
75
|
+
:credit_card => creditcard
|
76
|
+
}
|
77
|
+
}.merge(options[:billing_address] ? {:bill_to => options[:billing_address]} : {})
|
78
|
+
)
|
79
|
+
else
|
80
|
+
response
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Run an auth and capture transaction against the stored CC
|
85
|
+
def purchase(money, billing_id, options = {})
|
86
|
+
if (response = get_customer_profile(:customer_profile_id => billing_id)).success?
|
87
|
+
create_customer_profile_transaction( options.merge(
|
88
|
+
:transaction => {
|
89
|
+
:customer_profile_id => billing_id,
|
90
|
+
:customer_payment_profile_id => response.params['profile']['payment_profiles']['customer_payment_profile_id'],
|
91
|
+
:type => :auth_capture, :amount => amount(money)
|
92
|
+
}
|
93
|
+
))
|
94
|
+
else
|
95
|
+
response
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# authorize
|
100
|
+
def authorize(money, billing_id, options = {})
|
101
|
+
if (response = get_customer_profile(:customer_profile_id => billing_id)).success?
|
102
|
+
create_customer_profile_transaction( options.merge(
|
103
|
+
:transaction => {
|
104
|
+
:customer_profile_id => billing_id,
|
105
|
+
:customer_payment_profile_id => response.params['profile']['payment_profiles']['customer_payment_profile_id'],
|
106
|
+
:type => :auth_only, :amount => amount(money)
|
107
|
+
}
|
108
|
+
))
|
109
|
+
else
|
110
|
+
response
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# void
|
115
|
+
def void(money, trans_id, options = {})
|
116
|
+
create_customer_profile_transaction(
|
117
|
+
:transaction => {
|
118
|
+
:type => :void,
|
119
|
+
:trans_id => trans_id
|
120
|
+
}
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
# refund (against a previous transaction) (options { :amount => money })
|
125
|
+
def refund(trans_id, options)
|
126
|
+
money = options.delete(:amount)
|
127
|
+
billing_id = options.delete(:billing_id)
|
128
|
+
if (response = get_customer_profile(:customer_profile_id => billing_id)).success?
|
129
|
+
create_customer_profile_transaction(
|
130
|
+
:transaction => {
|
131
|
+
:customer_profile_id => billing_id,
|
132
|
+
:customer_payment_profile_id => response.params['profile']['payment_profiles']['customer_payment_profile_id'],
|
133
|
+
|
134
|
+
:type => :refund,
|
135
|
+
:trans_id => trans_id,
|
136
|
+
:amount => amount(money)
|
137
|
+
}
|
138
|
+
)
|
139
|
+
else
|
140
|
+
response
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# credit (is a refund without the trans_id)
|
145
|
+
# Requires Special Permission, Is not recommended by Authorize.net
|
146
|
+
# def credit(money, billing_id)
|
147
|
+
# if (response = get_customer_profile(:customer_profile_id => billing_id)).success?
|
148
|
+
# create_customer_profile_transaction(
|
149
|
+
# :transaction => {
|
150
|
+
# :customer_profile_id => billing_id,
|
151
|
+
# :customer_payment_profile_id => response.params['profile']['payment_profiles']['customer_payment_profile_id'],
|
152
|
+
# :type => :refund, :amount => amount(money)
|
153
|
+
# }
|
154
|
+
# )
|
155
|
+
# else
|
156
|
+
# response
|
157
|
+
# end
|
158
|
+
# end
|
159
|
+
|
160
|
+
# Destroy a customer profile
|
161
|
+
def unstore(billing_id, options = {})
|
162
|
+
delete_customer_profile(:customer_profile_id => billing_id)
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_customer_profile_transaction(options)
|
166
|
+
requires!(options, :transaction)
|
167
|
+
requires!(options[:transaction], :type)
|
168
|
+
case options[:transaction][:type]
|
169
|
+
when :void
|
170
|
+
requires!(options[:transaction], :trans_id)
|
171
|
+
when :refund
|
172
|
+
requires!(options[:transaction], :trans_id) &&
|
173
|
+
(
|
174
|
+
(options[:transaction][:customer_profile_id] && options[:transaction][:customer_payment_profile_id]) ||
|
175
|
+
options[:transaction][:credit_card_number_masked] ||
|
176
|
+
(options[:transaction][:bank_routing_number_masked] && options[:transaction][:bank_account_number_masked])
|
177
|
+
)
|
178
|
+
when :prior_auth_capture
|
179
|
+
requires!(options[:transaction], :amount, :trans_id)
|
180
|
+
else
|
181
|
+
requires!(options[:transaction], :amount, :customer_profile_id, :customer_payment_profile_id)
|
182
|
+
end
|
183
|
+
request = build_request(:create_customer_profile_transaction, options)
|
184
|
+
commit(:create_customer_profile_transaction, request)
|
185
|
+
end
|
186
|
+
|
187
|
+
def create_customer_profile_transaction_for_refund(options)
|
188
|
+
requires!(options, :transaction)
|
189
|
+
options[:transaction][:type] = :refund
|
190
|
+
requires!(options[:transaction], :trans_id)
|
191
|
+
requires!(options[:transaction], :amount)
|
192
|
+
|
193
|
+
request = build_request(:create_customer_profile_transaction, options)
|
194
|
+
commit(:create_customer_profile_transaction, request)
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
def tag_unless_blank(xml, tag_name, data)
|
199
|
+
xml.tag!(tag_name, data) unless data.blank? || data.nil?
|
200
|
+
end
|
201
|
+
|
202
|
+
def add_transaction(xml, transaction)
|
203
|
+
unless CIM_TRANSACTION_TYPES.include?(transaction[:type])
|
204
|
+
raise StandardError, "Invalid Customer Information Manager Transaction Type: #{transaction[:type]}"
|
205
|
+
end
|
206
|
+
|
207
|
+
xml.tag!('transaction') do
|
208
|
+
xml.tag!(CIM_TRANSACTION_TYPES[transaction[:type]]) do
|
209
|
+
# The amount to be billed to the customer
|
210
|
+
case transaction[:type]
|
211
|
+
when :void
|
212
|
+
tag_unless_blank(xml,'customerProfileId', transaction[:customer_profile_id])
|
213
|
+
tag_unless_blank(xml,'customerPaymentProfileId', transaction[:customer_payment_profile_id])
|
214
|
+
tag_unless_blank(xml,'customerShippingAddressId', transaction[:customer_shipping_address_id])
|
215
|
+
xml.tag!('transId', transaction[:trans_id])
|
216
|
+
when :refund
|
217
|
+
#TODO - add support for all the other options fields
|
218
|
+
xml.tag!('amount', transaction[:amount])
|
219
|
+
tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id])
|
220
|
+
tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id])
|
221
|
+
tag_unless_blank(xml, 'customerShippingAddressId', transaction[:customer_shipping_address_id])
|
222
|
+
tag_unless_blank(xml, 'creditCardNumberMasked', transaction[:credit_card_number_masked])
|
223
|
+
tag_unless_blank(xml, 'bankRoutingNumberMasked', transaction[:bank_routing_number_masked])
|
224
|
+
tag_unless_blank(xml, 'bankAccountNumberMasked', transaction[:bank_account_number_masked])
|
225
|
+
xml.tag!('transId', transaction[:trans_id])
|
226
|
+
when :prior_auth_capture
|
227
|
+
xml.tag!('amount', transaction[:amount])
|
228
|
+
xml.tag!('transId', transaction[:trans_id])
|
229
|
+
else
|
230
|
+
xml.tag!('amount', transaction[:amount])
|
231
|
+
xml.tag!('customerProfileId', transaction[:customer_profile_id])
|
232
|
+
xml.tag!('customerPaymentProfileId', transaction[:customer_payment_profile_id])
|
233
|
+
xml.tag!('approvalCode', transaction[:approval_code]) if transaction[:type] == :capture_only
|
234
|
+
end
|
235
|
+
add_order(xml, transaction[:order]) if transaction[:order]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def parse_direct_response(response)
|
241
|
+
direct_response = {'raw' => response.params['direct_response']}
|
242
|
+
direct_response_fields = response.params['direct_response'].split(',')
|
243
|
+
|
244
|
+
#keep this backwards compatible but add new direct response fields using
|
245
|
+
#field names from the AIM guide spec http://www.authorize.net/support/AIM_guide (around page 29)
|
246
|
+
dr_hash = {
|
247
|
+
'message' => direct_response_fields[3],
|
248
|
+
'approval_code' => direct_response_fields[4],
|
249
|
+
'invoice_number' => direct_response_fields[7],
|
250
|
+
'order_description' => direct_response_fields[8],
|
251
|
+
'amount' => direct_response_fields[9],
|
252
|
+
'transaction_type' => direct_response_fields[11],
|
253
|
+
'purchase_order_number' => direct_response_fields[36]
|
254
|
+
}
|
255
|
+
|
256
|
+
dr_arr = %w(response_code response_subcode response_reason_code response_reason_text
|
257
|
+
authorization_code avs_response transaction_id invoice_number description amount
|
258
|
+
method transaction_type customer_id first_name last_name company address
|
259
|
+
city state zip_code country phone fax email-address ship_to_first_name
|
260
|
+
ship_to_last_name ship_to_company ship_to_address ship_to_city ship_to_state
|
261
|
+
ship_to_zip_code ship_to_country tax duty freight tax_exempt purchase_order_number
|
262
|
+
md5_hash card_code_response cardholder_authentication_verification_response
|
263
|
+
)
|
264
|
+
dr_arr.each do |f|
|
265
|
+
dr_hash[f] = direct_response_fields[dr_arr.index(f)]
|
266
|
+
end
|
267
|
+
direct_response.merge(dr_hash)
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# monkeypatch the gateway
|
2
|
+
include ActiveMerchant::Billing
|
3
|
+
|
4
|
+
class ActiveMerchant::Billing::Response
|
5
|
+
#class BogusResponse < ActiveMerchant::Billing::Response
|
6
|
+
def token
|
7
|
+
@params.stringify_keys['billingid']
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ActiveMerchant::Billing::BogusGateway
|
12
|
+
# module ActiveMerchant #:nodoc:
|
13
|
+
# module Billing #:nodoc:
|
14
|
+
# class Bogus < Gateway
|
15
|
+
|
16
|
+
# handle billingid in addition to credit card
|
17
|
+
def purchase(money, ident, options = {})
|
18
|
+
number = ident.is_a?(ActiveMerchant::Billing::CreditCard) ? ident.number : ident
|
19
|
+
case number
|
20
|
+
when '1'
|
21
|
+
ActiveMerchant::Billing::Response.new(true, SUCCESS_MESSAGE,
|
22
|
+
{:authorized_amount => money.to_s}, :test => true, :authorization => AUTHORIZATION )
|
23
|
+
when '2'
|
24
|
+
ActiveMerchant::Billing::Response.new(false, FAILURE_MESSAGE,
|
25
|
+
{:authorized_amount => money.to_s, :error => FAILURE_MESSAGE }, :test => true)
|
26
|
+
else
|
27
|
+
raise Error, ERROR_MESSAGE
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# fix apparent blantant bug in bogus.rb
|
32
|
+
def credit(money, ident, options = {})
|
33
|
+
case ident
|
34
|
+
when '1'
|
35
|
+
Response.new(true, SUCCESS_MESSAGE,
|
36
|
+
{:paid_amount => money.to_s}, :test => true)
|
37
|
+
when '2'
|
38
|
+
Response.new(false, FAILURE_MESSAGE,
|
39
|
+
{:paid_amount => money.to_s, :error => FAILURE_MESSAGE }, :test => true)
|
40
|
+
else
|
41
|
+
raise Error, ERROR_MESSAGE
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# monkeypatch the gateway
|
2
|
+
include ActiveMerchant::Billing
|
3
|
+
|
4
|
+
class BraintreeResponse < ActiveMerchant::Billing::Response
|
5
|
+
def token
|
6
|
+
@params["customer_vault_id"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
# HEAD
|
12
|
+
ActiveMerchant::Billing::SmartPs::Response = BraintreeResponse
|
13
|
+
rescue NameError
|
14
|
+
# 1.4.2
|
15
|
+
ActiveMerchant::Billing::BraintreeGateway::Response = BraintreeResponse
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
ActiveMerchant::Billing::Base.mode = :test if SubscriptionConfig.test
|
2
|
+
|
3
|
+
gateway_args = {
|
4
|
+
:login => SubscriptionConfig.login,
|
5
|
+
:password => SubscriptionConfig.password
|
6
|
+
}.merge( (SubscriptionConfig.gateway_options rescue {}) )
|
7
|
+
|
8
|
+
SubscriptionConfig.gateway = ActiveMerchant::Billing::Base.gateway(SubscriptionConfig.gateway_name).new( gateway_args )
|
9
|
+
|
10
|
+
SubscriptionConfig.validate_via_transaction = false if SubscriptionConfig.bogus?
|
@@ -0,0 +1,53 @@
|
|
1
|
+
defaults: &defaults
|
2
|
+
# default plan (name) when user has no subscription. Defaults to the first plan with rate==0. Must be a plan with rate==0 since no billing occurs.
|
3
|
+
default_plan: free
|
4
|
+
|
5
|
+
# what plan (name) to assign to subscriptions that have expired (may be nil) (defaults to default_plan). Must be a plan with rate==0 since no billing occurs.
|
6
|
+
expired_plan: free
|
7
|
+
|
8
|
+
# trial period length (days) before first billing (can be 0 for no trial) (default = 30)
|
9
|
+
trial_period: 30
|
10
|
+
|
11
|
+
# grace period length (days) after subscription is past due before it is expired (closed down)
|
12
|
+
grace_period: 7
|
13
|
+
|
14
|
+
# define the mailer class name to use (default SubscriptionMailer provided with plugin)
|
15
|
+
mailer_class: SubscriptionMailer
|
16
|
+
|
17
|
+
# where to send admin reports (nil for no emails)
|
18
|
+
admin_report_recipients: jonathan@parkerhill.com
|
19
|
+
|
20
|
+
# shortcut configuration of the ActiveMerchant gateway
|
21
|
+
# test sets the Billing mode
|
22
|
+
test: true
|
23
|
+
|
24
|
+
# gateway is the name of the gateway, passed to ActiveMerchant::Billing::Base.gateway(name)
|
25
|
+
gateway_name: braintree
|
26
|
+
|
27
|
+
# login credentials to the gateway
|
28
|
+
login: testapi
|
29
|
+
password: password1
|
30
|
+
|
31
|
+
# other options passed to #new when initializing the gateway
|
32
|
+
# gateway_options:
|
33
|
+
# foo: bar
|
34
|
+
# baz: bam
|
35
|
+
|
36
|
+
# when true then card is validated by authorizing for $1 (and then void) [always false for bogus gateway]
|
37
|
+
validate_via_transaction: true
|
38
|
+
|
39
|
+
development:
|
40
|
+
<<: *defaults
|
41
|
+
gateway_name: bogus
|
42
|
+
|
43
|
+
test:
|
44
|
+
<<: *defaults
|
45
|
+
gateway_name: bogus
|
46
|
+
|
47
|
+
cucumber:
|
48
|
+
<<: *defaults
|
49
|
+
gateway_name: bogus
|
50
|
+
|
51
|
+
production:
|
52
|
+
<<: *defaults
|
53
|
+
test: false
|
@@ -0,0 +1,33 @@
|
|
1
|
+
defaults: &defaults
|
2
|
+
# Note, this is seed data, changes here are not used at runtime,
|
3
|
+
# must run "rake subscription:plans" to load them into the database.
|
4
|
+
# But you can change this and re-run the rake task to update the database.
|
5
|
+
# If you've added any app-specific limits to plans you can add them here
|
6
|
+
# name: name of the plan (required)
|
7
|
+
# rate_cents: cost of the plan in cents (0 or nil is free)
|
8
|
+
# interval: rate is for number of months (default 1). For example, an annual fee @ $25/month would have rate_cents: 30000 and interval: 12
|
9
|
+
# other attributes: are app-specific, be sure you've added them to the SubscriptionPlan migration
|
10
|
+
plans:
|
11
|
+
- name: free
|
12
|
+
rate_cents: 0
|
13
|
+
|
14
|
+
- name: basic
|
15
|
+
rate_cents: 1000
|
16
|
+
#foo: bar
|
17
|
+
|
18
|
+
- name: pro
|
19
|
+
rate_cents: 30000
|
20
|
+
interval: 12
|
21
|
+
#foo: baz
|
22
|
+
|
23
|
+
development:
|
24
|
+
<<: *defaults
|
25
|
+
|
26
|
+
test:
|
27
|
+
<<: *defaults
|
28
|
+
|
29
|
+
cucumber:
|
30
|
+
<<: *defaults
|
31
|
+
|
32
|
+
production:
|
33
|
+
<<: *defaults
|
data/lib/engine.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'saas'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module Saas
|
5
|
+
class Engine < Rails::Engine
|
6
|
+
|
7
|
+
# Config defaults
|
8
|
+
config.mount_at = '/'
|
9
|
+
|
10
|
+
# Load rake tasks
|
11
|
+
#rake_tasks do
|
12
|
+
# load File.join(File.dirname(__FILE__), 'rails/railties/tasks.rake')
|
13
|
+
#end
|
14
|
+
|
15
|
+
# Check the gem config
|
16
|
+
initializer "check config" do |app|
|
17
|
+
|
18
|
+
# make sure mount_at ends with trailing slash
|
19
|
+
config.mount_at += '/' unless config.mount_at.last == '/'
|
20
|
+
end
|
21
|
+
|
22
|
+
#initializer "static assets" do |app|
|
23
|
+
# app.middleware.use ::ActionDispatch::Static, "#{root}/public"
|
24
|
+
#end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/saas.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# acts_as_subscriber
|
2
|
+
module Saas #:nodoc:
|
3
|
+
require 'engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
4
|
+
|
5
|
+
module Acts #:nodoc:
|
6
|
+
module Subscriber #:nodoc:
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def acts_as_subscriber(options = {})
|
13
|
+
# when subscriber is using acts_as_paranoid, we'll destroy subscription (and its children) only when really really destroyed
|
14
|
+
if self.respond_to?(:paranoid?) && self.paranoid?
|
15
|
+
has_one :subscription, :as => :subscriber
|
16
|
+
else
|
17
|
+
has_one :subscription, :as => :subscriber, :dependent => :destroy
|
18
|
+
end
|
19
|
+
validates_associated :subscription
|
20
|
+
|
21
|
+
after_save :control_subscription
|
22
|
+
|
23
|
+
include Saas::Acts::Subscriber::InstanceMethods
|
24
|
+
extend Saas::Acts::Subscriber::SingletonMethods
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module SingletonMethods
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
# delegate for easier user forms
|
33
|
+
# for example, to sign up params[:user] => { :username => 'foo', :subscription_plan => '2', etc. }
|
34
|
+
attr_accessor :subscription_plan
|
35
|
+
|
36
|
+
def subscription_plan=(plan)
|
37
|
+
# arg can be object or id or name
|
38
|
+
@newplan = case
|
39
|
+
when plan.is_a?(SubscriptionPlan) then plan
|
40
|
+
when plan.to_i > 0 then SubscriptionPlan.find_by_id(plan)
|
41
|
+
else SubscriptionPlan.find_by_name(plan)
|
42
|
+
end
|
43
|
+
# not just change the attribute, really switch plans
|
44
|
+
subscription.change_plan @newplan unless subscription.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def subscription_plan
|
48
|
+
subscription.plan if subscription
|
49
|
+
end
|
50
|
+
|
51
|
+
# overwrite this method
|
52
|
+
# compare subscriber to the plan's limits
|
53
|
+
# return a blank value if ok (nil, false, [], {}), anything else means subscriber has exceeded limits
|
54
|
+
# maybe should make this a callback option to acts_as_subscriber
|
55
|
+
def subscription_plan_check(plan)
|
56
|
+
# example:
|
57
|
+
# exceeded = {}
|
58
|
+
# exceeded[:memory_used] = plan.max_memory if subscriber.memory_used > plan.max_memory
|
59
|
+
# exceeded[:file_count] = plan.max_files if subscriber.file_count > plan.max_files
|
60
|
+
# exceeded
|
61
|
+
end
|
62
|
+
|
63
|
+
# when acts_as_paranoid, only destroy dependents when i'm really getting destroyed
|
64
|
+
# (this way we don't have to also make the dependents acts_as_paranoid)
|
65
|
+
def destroy!
|
66
|
+
self.subscription.destroy if self.class.respond_to?(:paranoid?) && self.class.paranoid? && self.subscription
|
67
|
+
super
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def control_subscription
|
73
|
+
# this is the best time to create the subscription
|
74
|
+
# because cannot build_subscription while self.id is still nil
|
75
|
+
if subscription.nil?
|
76
|
+
self.create_subscription
|
77
|
+
self.subscription.change_plan @newplan if @newplan
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
ActiveRecord::Base.class_eval do
|
87
|
+
include Saas::Acts::Subscriber
|
88
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saas
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,19 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
13
|
-
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: rspec
|
16
|
-
requirement: &83443350 !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
|
-
requirements:
|
19
|
-
- - ! '>='
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '0'
|
22
|
-
type: :development
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: *83443350
|
12
|
+
date: 2012-02-25 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
25
14
|
description: SaaS gem
|
26
15
|
email:
|
27
16
|
- info@luisperichon.com.ar
|
@@ -48,6 +37,27 @@ files:
|
|
48
37
|
- app/models/subscription_config.rb
|
49
38
|
- app/models/subscription_profile.rb
|
50
39
|
- app/models/subscription_observer.rb
|
40
|
+
- lib/engine.rb
|
41
|
+
- lib/saas.rb
|
42
|
+
- generators/saas_migration/saas_migration_generator.rb
|
43
|
+
- generators/saas_migration/USAGE
|
44
|
+
- generators/saas_migration/templates/migration.rb
|
45
|
+
- generators/saasramp/saasramp_generator.rb
|
46
|
+
- generators/saasramp/USAGE
|
47
|
+
- generators/saasramp/templates/subscription.yml
|
48
|
+
- generators/saasramp/templates/subscription.rb
|
49
|
+
- generators/saasramp/templates/subscription_plans.yml
|
50
|
+
- generators/saasramp/templates/active_merchant/bogus.rb
|
51
|
+
- generators/saasramp/templates/active_merchant/braintree.rb
|
52
|
+
- generators/saasramp/templates/active_merchant/authorizenetcim.rb
|
53
|
+
- generators/saasramp/lib/insert_commands.rb
|
54
|
+
- generators/saas_features/USAGE
|
55
|
+
- generators/saas_features/templates/subscriber_helpers.rb
|
56
|
+
- generators/saas_features/templates/subscription_steps.rb
|
57
|
+
- generators/saas_features/templates/subscription_helpers.rb
|
58
|
+
- generators/saas_features/templates/user_steps.rb
|
59
|
+
- generators/saas_features/templates/subscription.feature
|
60
|
+
- generators/saas_features/saas_features_generator.rb
|
51
61
|
- README.rdoc
|
52
62
|
homepage: http://luisperichon.com.ar
|
53
63
|
licenses: []
|