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.
@@ -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: []