saas 0.1 → 0.1.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/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: []
|