servicemerchant 0.1.0

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.
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