saas 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: '0.1'
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-24 00:00:00.000000000Z
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: []