servicemerchant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/MIT-LICENSE.txt +20 -0
  2. data/README.txt +231 -0
  3. data/Rakefile +122 -0
  4. data/demo.rb +69 -0
  5. data/recurring_billing/lib/am_extensions.rb +1 -0
  6. data/recurring_billing/lib/am_extensions/paypal_extension.rb +170 -0
  7. data/recurring_billing/lib/dependencies.rb +14 -0
  8. data/recurring_billing/lib/gateways.rb +5 -0
  9. data/recurring_billing/lib/gateways/authorize_net.rb +103 -0
  10. data/recurring_billing/lib/gateways/paypal.rb +124 -0
  11. data/recurring_billing/lib/recurring_billing.rb +130 -0
  12. data/recurring_billing/lib/recurring_billing.rdoc +87 -0
  13. data/recurring_billing/lib/utils.rb +81 -0
  14. data/recurring_billing/test/fixtures.yml +33 -0
  15. data/recurring_billing/test/remote/authorize_net_test.rb +36 -0
  16. data/recurring_billing/test/remote/paypal_test.rb +46 -0
  17. data/recurring_billing/test/remote/recurring_billing_test.rb +41 -0
  18. data/recurring_billing/test/test_helper.rb +153 -0
  19. data/recurring_billing/test/unit/authorize_net_gateway_class_test.rb +42 -0
  20. data/recurring_billing/test/unit/paypal_gateway_class_test.rb +23 -0
  21. data/recurring_billing/test/unit/recurring_billing_gateway_class_test.rb +35 -0
  22. data/recurring_billing/test/unit/utils_test.rb +17 -0
  23. data/subscription_management/Rakefile +29 -0
  24. data/subscription_management/lib/models/subscription.rb +9 -0
  25. data/subscription_management/lib/models/subscription_profile.rb +4 -0
  26. data/subscription_management/lib/subscription_management.rb +326 -0
  27. data/subscription_management/samples/backpack.yml +101 -0
  28. data/subscription_management/samples/basecamp.yml +71 -0
  29. data/subscription_management/samples/brainkeeper.yml +90 -0
  30. data/subscription_management/samples/campfire.yml +74 -0
  31. data/subscription_management/samples/clickandpledge.yml +24 -0
  32. data/subscription_management/samples/demo.rb +19 -0
  33. data/subscription_management/samples/elm.yml +174 -0
  34. data/subscription_management/samples/freshbooks.yml +78 -0
  35. data/subscription_management/samples/highrise.yml +100 -0
  36. data/subscription_management/samples/presets.yml +10 -0
  37. data/subscription_management/samples/tariff.outline.yml +0 -0
  38. data/subscription_management/samples/taxes.yml +21 -0
  39. data/subscription_management/subscription_management.rb +7 -0
  40. data/subscription_management/tasks/schema.rb +50 -0
  41. data/subscription_management/test/connection.rb +10 -0
  42. data/subscription_management/test/remote/subscription_management_test.rb +112 -0
  43. data/subscription_management/test/test_helper.rb +84 -0
  44. data/subscription_management/test/unit/subscription_management_test.rb +40 -0
  45. data/tracker/README +12 -0
  46. data/tracker/Rakefile +40 -0
  47. data/tracker/db/migrations/empty-directory +0 -0
  48. data/tracker/demo.rb +12 -0
  49. data/tracker/lib/models/recurring_payment_profile.rb +134 -0
  50. data/tracker/lib/models/transaction.rb +19 -0
  51. data/tracker/lib/recurring_billing_extension.rb +103 -0
  52. data/tracker/lib/recurring_billing_extension.rdoc +34 -0
  53. data/tracker/tasks/schema.rb +66 -0
  54. data/tracker/test/connection.rb +10 -0
  55. data/tracker/test/recurring_payment_profile.rb +35 -0
  56. data/tracker/test/remote/authorize_net_test.rb +68 -0
  57. data/tracker/test/remote/paypal_test.rb +115 -0
  58. data/tracker/test/test_helper.rb +87 -0
  59. data/tracker/test/unit/recurring_payment_profile_test.rb +62 -0
  60. data/tracker/tracker.rb +10 -0
  61. data/vendor/money-1.7.1/MIT-LICENSE +20 -0
  62. data/vendor/money-1.7.1/README +75 -0
  63. data/vendor/money-1.7.1/lib/bank/no_exchange_bank.rb +9 -0
  64. data/vendor/money-1.7.1/lib/bank/variable_exchange_bank.rb +30 -0
  65. data/vendor/money-1.7.1/lib/money.rb +29 -0
  66. data/vendor/money-1.7.1/lib/money/core_extensions.rb +26 -0
  67. data/vendor/money-1.7.1/lib/money/money.rb +209 -0
  68. data/vendor/money-1.7.1/lib/support/cattr_accessor.rb +57 -0
  69. metadata +153 -0
@@ -0,0 +1,87 @@
1
+ This class provides unified API to access remote payment gateways.
2
+
3
+ == Quick overview
4
+ === Creating a gateway accessor
5
+ There are two options to get an instance of an accessor for required gateway. First is to use get_instance method:
6
+ options = {:gateway => :authorize_net, :login => 'MyLogin', :password => 'MyPassword'}
7
+ gateway = RecurringBilling::RecurringBillingGateway.get_instance(options)
8
+ which basically is an alias for the second method, directly calling
9
+ the constructor of RecurringBilling::AuthorizeNetGateway (or any other RecurringBillingGateway descendant):
10
+ gateway = RecurringBilling::AuthorizeNetGateway({:login => 'MyLogin', :password => 'MyPassword'})
11
+ First method is recommended to be used for more flexibility.
12
+
13
+ === Common options for RecurringBilling methods
14
+ It should be self-explanatory that every recurring payment itself has many parameters - those parameters have to be specified
15
+ when the payment is created and updated on remote gateway as well. Within the API, these are grouped for convenience into four groups
16
+ that have become the input parameters for create and update methods:
17
+ - Amount
18
+ - Card
19
+ - Payment Options
20
+ - Recurring Options
21
+
22
+ <b>Amount</b>(amount) is the object of Money class, which includes both amount and currency of the payment.
23
+
24
+ <b>Card</b>(card) is the object of ActiveMerchant::Billing::CreditCard representing the credit card that is charged during recurring payments
25
+
26
+ <b>Payment Options</b>(payment_options) are a hash of:
27
+ - :subscription_name - +string+ containing the reference name for the subscription
28
+ - :billing_address - +hash+ containing subscriber's billing address (see ActiveMerchant for more info on structure)
29
+ - :order - +hash+ containing merchant's info about order, like invoice ID (see ActiveMerchant for more info on structure)
30
+ - :taxes_amount_included - +boolean+ declaring if taxes are included in payment amount or not
31
+ This set of options contains information of merchant.
32
+
33
+ <b>Recurring Options</b>(payment_options) are a hash of:
34
+ - :start_date - +Date+ object, date of the first billing occurrence
35
+ - :interval - +string+ of <tt>(0.5|d+)\s*(d|w|m|y)/</tt> template that shows time between two consecutive billing occurrences
36
+ - :end_date - +Date+ object representing date after which there are no more billing occurrences
37
+ - :occurrences - positive +integer+ showing number of billing occurrences. Either this or :end_date should be specified
38
+ - :trial_days - positive +integer+ showing number of days of free trial
39
+ This set of options declares settings of payment recurring occurrences.
40
+
41
+ === Creating a remote payment
42
+ Once gateway is created, payments could be made. The whole idea is very simple - prepare parameters and use the
43
+ create method of an instance:
44
+ amount = Money.us_dollar(100) # $1 USD
45
+ card = ActiveMerchant::Billing::CreditCard.new({
46
+ :number => '4242424242424242',
47
+ :month => 9,
48
+ :year => Time.now.year + 1,
49
+ :first_name => 'Name',
50
+ :last_name => 'Lastname',
51
+ :verification_value => '123',
52
+ :type => 'visa'
53
+ }) # Some random credit card
54
+ payment_options = {
55
+ :subscription_name => 'Random subscription',
56
+ :order => {:invoice_number => 'ODMX31337'}
57
+ }
58
+ recurring_options = {
59
+ :start_date => Date.today,
60
+ :occurrences => 10,
61
+ :interval => '10d'
62
+ }
63
+ billing_id = gateway.create(amount, card, payment_options, recurring_options)
64
+ You may then check the result of recurring payment creation by accessing last_response:
65
+ response = gateway.last_response
66
+ print response.inspect
67
+ Exact structure of last_response return value is defined in respective ActiveMerchant module. Most common
68
+ uses of last_response query are:
69
+ unless response.success? # succeed-failed check
70
+ print response.message # get message retrieved from remote gateway
71
+ ...
72
+ Last response is changed whenever create, update, inquiry or delete methods are called.
73
+
74
+ === Updating and canceling a remote payment
75
+ To update the settings of recurring payment on gateway, just call the update method of an instance.
76
+ new_amount = Money.us_dollar(150) # $1.5 USD
77
+ success = gateway.update(billing_id, new_amount, nil, {}, {}) # or just gateway.update(new_amount)
78
+ Please note that acceptable update parameters vary with gateway. Correctness of parameters for update is
79
+ checked via protected correct_update? method.
80
+
81
+ Recurring payment may be cancelled via delete method:
82
+ success = gateway.delete(billing_id)
83
+
84
+ === Inquiring gateway on payment status
85
+ In order to inquire remote gateway on status of given subscription, inquiry method of an instance should be used:
86
+ result = gateway.inquiry(billing_id)
87
+ The result structure varies from gateway to gateway.
@@ -0,0 +1,81 @@
1
+ # This module contains common functions that ServiceMerchant uses.
2
+ module Utils
3
+ #Counts nubmer of months between two dates
4
+ #
5
+ # 2008/1/1, 2007/12/31 => 1
6
+ # 2008/9/10, 2009/10/20 => 13
7
+ # ETC
8
+ def months_between(date1, date2)
9
+ if date1 > date2
10
+ recent_date = date1.to_date
11
+ past_date = date2.to_date
12
+ else
13
+ recent_date = date2.to_date
14
+ past_date = date1.to_date
15
+ end
16
+ years_diff = recent_date.year - past_date.year
17
+ months_diff = recent_date.month - past_date.month + ((recent_date.day >= past_date.day) ? 0 : -1)
18
+ if months_diff < 0
19
+ months_diff = 12 + months_diff
20
+ years_diff -= 1
21
+ end
22
+ return years_diff*12 + months_diff
23
+ end
24
+
25
+ # Parses given INTERVAL string into hash.
26
+ #
27
+ # Sample use:
28
+ # "1w" => {:length => 1, :unit => :w }
29
+ # "0.5 m" => {:length => 0.5, :unit => :m }
30
+ # "10 d" => {:length => 10, :unit => :d }
31
+ # "3y" => {:length => 3, :unit => :y }
32
+ # "0.25 w" => ArgumentError
33
+ # "2 x" => ArgumentError
34
+ def parse_interval(interval)
35
+ if (interval =~ /^(\d+|0\.5)\s*(d|w|m|y)$/i)
36
+ return $1 == '0.5' ? 0.5 : $1.to_i, $2.downcase.to_sym
37
+ end
38
+ raise ArgumentError, "Invalid value format for payment interval: #{interval}"
39
+ end
40
+
41
+ # Returns number of payment occurrences between END_DATE and START_DATE with INTERVAL frequency.
42
+ #
43
+ # INTERVAL should be given in the same format parse_interval uses. END_DATE and START_DATE should be either Date or DateTime.
44
+ # Returned number includes initial payment.
45
+ #
46
+ # Sample use:
47
+ # [Date.today(), '1 w', Date.today()+18] => 3
48
+ # [DateTime.new(2008, 09, 20), '1y', DateTime.new(2009, 09, 19)] => 1
49
+ def get_occurrences(start_date, interval, end_date)
50
+
51
+ start_date = Date.new(start_date.year,start_date.month,start_date.mday)
52
+ end_date = Date.new(end_date.year,end_date.month,end_date.mday)
53
+ raise ArgumentError, "Start date (#{start_date}) should be less than or equal to end date (#{end_date})" if (start_date>end_date)
54
+
55
+ i_length, i_unit = parse_interval(interval)
56
+
57
+ if i_length == 0.5 && ![:m, :y].include?(i_unit)
58
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
59
+ end
60
+
61
+ new_interval = case i_unit
62
+ when :d then {:length=>i_length, :unit=>:days}
63
+ when :w then {:length=>i_length*7, :unit=>:days}
64
+ when :m then {:length=>i_length, :unit=>:months}
65
+ when :y then {:length=>i_length*12, :unit=>:months}
66
+ end
67
+
68
+ if new_interval[:unit] == :days
69
+ new_occurrences = 1 + ((end_date - start_date)/new_interval[:length]).to_i
70
+ elsif new_interval[:unit] == :months
71
+ new_occurrences = 1 + (months_between(end_date, start_date)/new_interval[:length]).to_i
72
+ end
73
+ return new_occurrences
74
+ end
75
+
76
+ # Returns midnight of specified DateTime object (as DateTime).
77
+ def get_midnight(datetime_x)
78
+ DateTime.new(datetime_x.year,datetime_x.month,datetime_x.mday)
79
+ end
80
+
81
+ end
@@ -0,0 +1,33 @@
1
+ # This file has all of the ActiveMerchant test account credentials.
2
+ # Many gateways do not offer publicly available test accounts. In
3
+ # order to make testing the gateways easy you can copy this file to
4
+ # your home directory as the file ~/.active_merchant/fixtures.yml
5
+ # You can then place your own test account credentials in your local
6
+ # copy of the file.
7
+ #
8
+ # If the login is numeric, ensure that you place quotes around it.
9
+ # Leading zeros will be lost when YAML parses the file if you don't.
10
+ #
11
+ # Paste any required PEM certificates after the pem key.
12
+ #
13
+ authorize_net:
14
+ gateway: authorize_net
15
+ login: 6zz6m5N4Et
16
+ password: 9V9wUv6Yd92t27t5
17
+ is_test: true
18
+
19
+ # You can use either your API PEM file or API signature with PayPal.
20
+ paypal_certificate:
21
+ gateway: paypal
22
+ login: LOGIN
23
+ password: PASSWORD
24
+ subject:
25
+ pem: |--
26
+ PASTE YOUR PEM FILE HERE
27
+
28
+ paypal:
29
+ gateway: paypal
30
+ login: a.lebe_1220380924_biz_api1.list.ru
31
+ password: 1220380928
32
+ signature: An5ns1Kso7MWUdW4ErQKJJJ4qi4-ABavC-ayabELBOsjVjsDpfMCgJRN
33
+ is_test: true
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class AuthorizeNetGatewayRemoteTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ credentials = fixtures(:authorize_net)
7
+ assert @gw = RecurringBilling::AuthorizeNetGateway.new(credentials.update({:is_test => true}))
8
+ assert_equal @gw.name, 'Authorize.net'
9
+ @card = credit_card()
10
+ end
11
+
12
+ def test_crud_recurring_payment
13
+ payment_options = {
14
+ :subscription_name => 'Test Subscription 1337',
15
+ :order => {:invoice_number => '407933'}
16
+ }
17
+ recurring_options = {
18
+ :start_date => Date.today + 1,
19
+ :end_date => Date.today + 290,
20
+ :interval => '1m'
21
+ }
22
+
23
+ billing_id = @gw.create(Money.us_dollar(15), @card, payment_options, recurring_options)
24
+ print "Create:\n"
25
+ print @gw.last_response.inspect
26
+ payment_options[:order] = {:invoice_number => '407934'}
27
+ @gw.update(billing_id, Money.us_dollar(16), @card, payment_options, nil)
28
+ print "\nUpdate:\n"
29
+ print @gw.last_response.inspect
30
+ @gw.delete(billing_id)
31
+ print "\nDelete:\n"
32
+ print @gw.last_response.inspect
33
+ assert true
34
+ end
35
+
36
+ end
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class PaypalGatewayRemoteTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ cred = fixtures(:paypal)
7
+ assert @gw = RecurringBilling::PaypalGateway.new(cred.update({:is_test=>true}))
8
+ assert_equal @gw.name, 'PayPal Website Payments Pro (US)'
9
+ @card = credit_card()
10
+ end
11
+
12
+ def test_crud_recurring_payment
13
+ payment_options = {
14
+ :subscription_name => 'Test Subscription 1337',
15
+ :order => {:invoice_number => '407933'}
16
+ }
17
+ recurring_options = {
18
+ :start_date => Date.today + 1,
19
+ :end_date => Date.today + 290,
20
+ :interval => '1m'
21
+ }
22
+
23
+ print "\nCreate:\n"
24
+ billing_id = @gw.create(Money.us_dollar(15), @card, payment_options=payment_options, recurring_options=recurring_options)
25
+ print @gw.last_response.inspect
26
+ payment_options[:order] = {:invoice_number => '407934'}
27
+ assert @gw.last_response.success?
28
+
29
+ print "\n\nUpdate:\n"
30
+ @gw.update(billing_id, Money.us_dollar(16), @card, payment_options=payment_options)
31
+ print @gw.last_response.inspect
32
+ assert @gw.last_response.success?
33
+
34
+ print "\n\nInquiry:\n"
35
+ result = @gw.inquiry(billing_id)
36
+ print @gw.last_response.inspect
37
+ print "\n\n", result.inspect
38
+ assert @gw.last_response.success?
39
+
40
+ print "\n\nDelete:\n"
41
+ @gw.delete(billing_id)
42
+ print @gw.last_response.inspect
43
+ assert @gw.last_response.success?
44
+ end
45
+
46
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class RecurringBillingRemoteTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @card = credit_card()
7
+ end
8
+
9
+ def perform_generic_test(gateway)
10
+ random_invoice = "%06d" % rand(999999)
11
+ payment_options = {
12
+ :subscription_name => 'Test Subscription 1337',
13
+ :order => {:invoice_number => random_invoice}
14
+ }
15
+ recurring_options = {
16
+ :start_date => Date.today + 1,
17
+ :end_date => Date.today + 290,
18
+ :interval => '1m'
19
+ }
20
+ credentials = fixtures(gateway)
21
+ assert gw = RecurringBilling::RecurringBillingGateway.get_instance(credentials)
22
+
23
+ billing_id = gw.create(Money.us_dollar(15), @card, payment_options, recurring_options)
24
+ assert gw.last_response.success?
25
+ new_random_invoice = "%06d" % rand(999999)
26
+ payment_options[:order] = {:invoice_number => new_random_invoice}
27
+ gw.update(billing_id, Money.us_dollar(16), @card, payment_options, {})
28
+ assert gw.last_response.success?
29
+ gw.delete(billing_id)
30
+ assert gw.last_response.success?
31
+ end
32
+
33
+ def test_paypal
34
+ perform_generic_test(:paypal)
35
+ end
36
+
37
+ def test_authorize_net
38
+ perform_generic_test(:authorize_net)
39
+ end
40
+
41
+ end
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'test/unit'
4
+
5
+ require 'active_merchant'
6
+
7
+ require File.dirname(__FILE__) + '/../lib/gateways'
8
+
9
+ # Turn off invalid certificate crashes
10
+ require 'openssl'
11
+ silence_warnings do
12
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
13
+ end
14
+
15
+ ActiveMerchant::Billing::Base.mode = :test
16
+
17
+ module RecurringBilling
18
+ module Assertions
19
+ def assert_field(field, value)
20
+ clean_backtrace do
21
+ assert_equal value, @helper.fields[field]
22
+ end
23
+ end
24
+
25
+ # Allows the testing of you to check for negative assertions:
26
+ #
27
+ # # Instead of
28
+ # assert !something_that_is_false
29
+ #
30
+ # # Do this
31
+ # assert_false something_that_should_be_false
32
+ #
33
+ # An optional +msg+ parameter is available to help you debug.
34
+ def assert_false(boolean, message = nil)
35
+ message = build_message message, '<?> is not false or nil.', boolean
36
+
37
+ clean_backtrace do
38
+ assert_block message do
39
+ not boolean
40
+ end
41
+ end
42
+ end
43
+
44
+ # A handy little assertion to check for a successful response:
45
+ #
46
+ # # Instead of
47
+ # assert_success response
48
+ #
49
+ # # DRY that up with
50
+ # assert_success response
51
+ #
52
+ # A message will automatically show the inspection of the response
53
+ # object if things go wrong.
54
+ def assert_success(response)
55
+ clean_backtrace do
56
+ assert response.success?, "Response failed: #{response.inspect}"
57
+ end
58
+ end
59
+
60
+ # The negative of +assert_success+
61
+ def assert_failure(response)
62
+ clean_backtrace do
63
+ assert_false response.success?, "Response expected to fail: #{response.inspect}"
64
+ end
65
+ end
66
+
67
+ def assert_valid(validateable)
68
+ clean_backtrace do
69
+ assert validateable.valid?, "Expected to be valid"
70
+ end
71
+ end
72
+
73
+ def assert_not_valid(validateable)
74
+ clean_backtrace do
75
+ assert_false validateable.valid?, "Expected to not be valid"
76
+ end
77
+ end
78
+
79
+ private
80
+ def clean_backtrace(&block)
81
+ yield
82
+ rescue Test::Unit::AssertionFailedError => e
83
+ path = File.expand_path(__FILE__)
84
+ raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
85
+ end
86
+ end
87
+ end
88
+
89
+ module Test
90
+ module Unit
91
+ class TestCase
92
+
93
+ include RecurringBilling::Assertions
94
+ include Utils
95
+
96
+ DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml'
97
+
98
+ private
99
+ def credit_card(number = '4242424242424242', options = {})
100
+ defaults = {
101
+ :number => number,
102
+ :month => 9,
103
+ :year => Time.now.year + 1,
104
+ :first_name => 'John',
105
+ :last_name => 'Doe',
106
+ :verification_value => '123',
107
+ :type => 'visa'
108
+ }.update(options)
109
+
110
+ ActiveMerchant::Billing::CreditCard.new(defaults)
111
+ end
112
+
113
+ def address(options = {})
114
+ {
115
+ :name => 'John Doe',
116
+ :address1 => '1234 My Street',
117
+ :address2 => 'Apt 1',
118
+ :company => 'Widgets Inc',
119
+ :city => 'Ottawa',
120
+ :state => 'ON',
121
+ :zip => 'K1C2N6',
122
+ :country => 'CA',
123
+ :phone => '(555)555-5555'
124
+ }.update(options)
125
+ end
126
+
127
+ def all_fixtures
128
+ @@fixtures ||= load_fixtures
129
+ end
130
+
131
+ def fixtures(key)
132
+ data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
133
+
134
+ data.dup
135
+ end
136
+
137
+ def load_fixtures
138
+ file = DEFAULT_CREDENTIALS
139
+ yaml_data = YAML.load(File.read(file))
140
+ symbolize_keys(yaml_data)
141
+
142
+ yaml_data
143
+ end
144
+
145
+ def symbolize_keys(hash)
146
+ return unless hash.is_a?(Hash)
147
+
148
+ hash.symbolize_keys!
149
+ hash.each{|k,v| symbolize_keys(v)}
150
+ end
151
+ end
152
+ end
153
+ end