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,10 @@
1
+ # As we're not using Rails in tests, we have to establish DB conneciton ourselves
2
+ require 'rubygems'
3
+ gem 'activerecord'
4
+ require 'activerecord'
5
+ gem 'sqlite3-ruby'
6
+
7
+ ActiveRecord::Base.establish_connection(
8
+ :adapter => "sqlite3",
9
+ :database => (File.dirname(__FILE__) + "/../db/test.db")
10
+ )
@@ -0,0 +1,35 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class RandomRecurringProfileTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ cred = fixtures(:paypal)
7
+ assert @gw = RecurringBilling::PaypalGateway.new(cred)
8
+ @card = credit_card(number='4532130086825928')
9
+ end
10
+
11
+ def test_crud_recurring_payment
12
+ payment_options = {
13
+ :subscription_name => 'Test Subscription 1337',
14
+ :order => {:invoice_number => '407933'}
15
+ }
16
+ recurring_options = {
17
+ :start_date => Date.today + 1,
18
+ :end_date => Date.today + 290,
19
+ :interval => '1m'
20
+ }
21
+
22
+ billing_id = @gw.create_with_persist(Money.us_dollar(15), @card, payment_options=payment_options, recurring_options=recurring_options)
23
+ print "\n\nCreate:\n"
24
+ print @gw.last_response.inspect
25
+ payment_options[:order] = {:invoice_number => '407934'}
26
+ @gw.update_with_persist(billing_id, Money.us_dollar(16), @card, payment_options=payment_options)
27
+ print "\n\nUpdate:\n"
28
+ print @gw.last_response.inspect
29
+ @gw.delete_with_persist(billing_id)
30
+ print "\n\nDelete:\n"
31
+ print @gw.last_response.inspect
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,68 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require "tracker"
3
+
4
+ class AuthorizeNetGatewayRemoteTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ credentials = fixtures(:authorize_net)
8
+ assert @gw = RecurringBilling::AuthorizeNetGateway.new(credentials)
9
+ @card = credit_card()
10
+ end
11
+
12
+ def test_crud_recurring_payment
13
+ payment_options = {
14
+ :subscription_name => 'Random subscription',
15
+ :order => {:invoice_number => 'ODMX31337'}
16
+ }
17
+ recurring_options = {
18
+ :start_date => Date.today,
19
+ :occurrences => 10,
20
+ :interval => '10d'
21
+ }
22
+
23
+ sum = rand(50000)+120
24
+ billing_id = @gw.create(Money.us_dollar(sum), @card, payment_options, recurring_options)
25
+ assert @gw.last_response.success?
26
+
27
+ payment_options[:order] = {:invoice_number => 'ODMX17532'}
28
+
29
+ another_sum = rand(50000)+120
30
+ @gw.update(billing_id, Money.us_dollar(another_sum), @card, payment_options, {})
31
+ assert @gw.last_response.success?
32
+
33
+ assert_raise NotImplementedError do; @gw.inquiry(billing_id); end
34
+
35
+ @gw.delete(billing_id)
36
+ assert @gw.last_response.success?
37
+ profile = RecurringPaymentProfile.find_by_gateway_reference(billing_id)
38
+ assert_equal 'deleted', profile.status
39
+
40
+ assert_raise StandardError do;@gw.update(billing_id, Money.us_dollar(another_sum), @card, payment_options, {});end;
41
+ end
42
+
43
+ def test_update_or_recreate
44
+ payment_options = {
45
+ :subscription_name => 'Random subscription',
46
+ :order => {:invoice_number => 'ODMX31337'}
47
+ }
48
+ recurring_options = {
49
+ :start_date => Date.today,
50
+ :occurrences => 10,
51
+ :interval => '10d'
52
+ }
53
+
54
+ sum = rand(50000)+120
55
+ billing_id = @gw.create(Money.us_dollar(sum), @card, payment_options, recurring_options)
56
+ assert @gw.last_response.success?
57
+
58
+ payment_options[:order] = {:invoice_number => 'ODMX17532'}
59
+
60
+ another_sum = rand(50000)+120
61
+ @gw.update_or_recreate(billing_id, {:amount => Money.us_dollar(another_sum)})
62
+ assert @gw.last_response.success?
63
+
64
+ @gw.update_or_recreate(billing_id, {:card => @card, :start_date => Date.new(2009, 7, 8)})
65
+ assert @gw.last_response.success?
66
+ end
67
+
68
+ end
@@ -0,0 +1,115 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ require "mocha"
4
+ require "tracker"
5
+
6
+ class PaypalRemoteTest < Test::Unit::TestCase
7
+
8
+ def setup
9
+ cred = fixtures(:paypal)
10
+ assert @gw = RecurringBilling::PaypalGateway.new(cred)
11
+ @card = credit_card()
12
+ @card2 = credit_card('4929838635250031', {:first_name => 'James', :last_name => 'Lueser'})
13
+ @card_bogus = credit_card("ISMELLLIKEBOGUS")
14
+ end
15
+
16
+ def test_inquiry_updates_tracker
17
+ payment_options = {
18
+ :subscription_name => 'Test Subscription 1337',
19
+ :order => {:invoice_number => '407933'}
20
+ }
21
+ recurring_options = {
22
+ :start_date => Date.today + 1,
23
+ :end_date => Date.today + 290,
24
+ :interval => '1m'
25
+ }
26
+
27
+ billing_id = @gw.create(Money.us_dollar(15), @card, payment_options=payment_options, recurring_options=recurring_options)
28
+ assert @gw.last_response.success?
29
+
30
+ profile = RecurringPaymentProfile.find_by_gateway_reference(billing_id)
31
+ assert_equal 'active', profile.status
32
+ assert_not_equal -1, profile.outstanding_balance
33
+ assert_not_equal -1, profile.complete_payments_count
34
+ assert_not_equal -1, profile.failed_payments_count
35
+ assert_not_equal -1, profile.remaining_payments_count
36
+
37
+ # Mock result of original inquire methods
38
+ result_mock = {'profile_status' => 'verified',
39
+ 'outstanding_balance' => ::Money.new(-100),
40
+ 'number_cycles_completed'=> -1,
41
+ 'failed_payment_count' => -1,
42
+ 'number_cycles_remaining'=> -1}
43
+
44
+ @gw.methods.include? :inquiry_without_persist
45
+ @gw.expects(:inquiry_without_persist).returns(result_mock)
46
+ result = @gw.inquiry(billing_id)
47
+ assert @gw.last_response.success?
48
+
49
+ assert_equal result_mock, result
50
+
51
+ # Get profile and check whether data is updated
52
+ profile = RecurringPaymentProfile.find_by_gateway_reference(billing_id)
53
+ assert_equal 'verified', profile.status
54
+ assert_equal -100, profile.outstanding_balance
55
+ assert_equal -1, profile.complete_payments_count
56
+ assert_equal -1, profile.failed_payments_count
57
+ assert_equal -1, profile.remaining_payments_count
58
+ end
59
+
60
+ def test_create_update_failure
61
+ payment_options = {
62
+ :subscription_name => 'Unsuccessful payment',
63
+ :order => {:invoice_number => '032895'}
64
+ }
65
+ recurring_options = {
66
+ :start_date => Date.today + 1,
67
+ :occurrences => 402,
68
+ :interval => '1w'
69
+ }
70
+
71
+ new_recurring_options = {
72
+ :pay_on_day_x => 3
73
+ }
74
+
75
+ token_sum = rand(50000)+120
76
+ @gw.create(Money.us_dollar(token_sum), @card_bogus, payment_options=payment_options, recurring_options=recurring_options)
77
+ assert !@gw.last_response.success?
78
+ assert RecurringPaymentProfile.find_by_net_amount(Money.us_dollar(token_sum).cents).nil?
79
+
80
+ new_token_sum = rand(50000)+120
81
+ billing_id = @gw.create(Money.us_dollar(new_token_sum), @card, payment_options=payment_options, recurring_options=recurring_options)
82
+ assert @gw.last_response.success?
83
+ assert_equal profile = RecurringPaymentProfile.find_by_net_amount(Money.us_dollar(new_token_sum).cents), RecurringPaymentProfile.find_by_gateway_reference(billing_id)
84
+
85
+ another_token_sum = rand(50000)+120
86
+ @gw.update(billing_id, Money.us_dollar(another_token_sum), @card, {}, new_recurring_options)
87
+ assert_not_equal profile.updated_at, (profile2 = RecurringPaymentProfile.find_by_gateway_reference(billing_id)).updated_at #meaning the profile hadn't been updated
88
+ assert_equal profile2.amount, Money.us_dollar(another_token_sum).cents
89
+
90
+
91
+ @gw.update(billing_id, Money.us_dollar(new_token_sum), @card_bogus, {}, {})
92
+ assert_equal profile2.updated_at, RecurringPaymentProfile.find_by_gateway_reference(billing_id).updated_at #meaning the profile hadn't been updated
93
+ end
94
+
95
+ def test_update_or_create
96
+ payment_options = {
97
+ :subscription_name => 'Random subscription',
98
+ :order => {:invoice_number => 'ODMX31337'}
99
+ }
100
+ recurring_options = {
101
+ :start_date => Date.today,
102
+ :occurrences => 10,
103
+ :interval => '10 w'
104
+ }
105
+
106
+ sum = rand(50000)+120
107
+ billing_id = @gw.create(Money.us_dollar(sum), @card, payment_options, recurring_options)
108
+ assert @gw.last_response.success?
109
+
110
+ another_sum = rand(50000)+120
111
+ @gw.update_or_recreate(billing_id, {:card => @card2, :occurrences => 20})
112
+ assert @gw.last_response.success?
113
+ end
114
+
115
+ end
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'activesupport'
6
+
7
+ silence_warnings do
8
+ require 'active_merchant'
9
+ end
10
+
11
+ require File.dirname(__FILE__) + "/connection"
12
+
13
+ $: << File.dirname(__FILE__) + "/../" # Tracker root
14
+
15
+ require 'tracker'
16
+
17
+ # Turn off invalid certificate crashes
18
+ require 'openssl'
19
+ silence_warnings do
20
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
21
+ end
22
+
23
+ ActiveMerchant::Billing::Base.mode = :test
24
+
25
+ module Test
26
+ module Unit
27
+ class TestCase
28
+
29
+
30
+ DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml'
31
+
32
+ private
33
+ def credit_card(number = '4242424242424242', options = {})
34
+ defaults = {
35
+ :number => number,
36
+ :month => 9,
37
+ :year => Time.now.year + 1,
38
+ :first_name => 'John',
39
+ :last_name => 'Doe',
40
+ :verification_value => '123',
41
+ :type => 'visa'
42
+ }.update(options)
43
+
44
+ ActiveMerchant::Billing::CreditCard.new(defaults)
45
+ end
46
+
47
+ def address(options = {})
48
+ {
49
+ :name => 'John Doe',
50
+ :address1 => '1234 My Street',
51
+ :address2 => 'Apt 1',
52
+ :company => 'Widgets Inc',
53
+ :city => 'Ottawa',
54
+ :state => 'ON',
55
+ :zip => 'K1C2N6',
56
+ :country => 'CA',
57
+ :phone => '(555)555-5555'
58
+ }.update(options)
59
+ end
60
+
61
+ def all_fixtures
62
+ @@fixtures ||= load_fixtures
63
+ end
64
+
65
+ def fixtures(key)
66
+ data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
67
+
68
+ data.dup
69
+ end
70
+
71
+ def load_fixtures
72
+ file = DEFAULT_CREDENTIALS
73
+ yaml_data = YAML.load(File.read(file))
74
+ symbolize_keys(yaml_data)
75
+
76
+ yaml_data
77
+ end
78
+
79
+ def symbolize_keys(hash)
80
+ return unless hash.is_a?(Hash)
81
+
82
+ hash.symbolize_keys!
83
+ hash.each{|k,v| symbolize_keys(v)}
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ require 'money'
4
+
5
+ class RecurringPaymentProfileTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ @profile = ::RecurringPaymentProfile.new
9
+ end
10
+
11
+ def test_net_money_assigns
12
+ @profile.net_money=Money.new(333, currency='CAD')
13
+ assert_equal 333, @profile.net_amount
14
+ assert_equal 'CAD', @profile.currency
15
+ end
16
+
17
+ def test_taxes_money_assigns
18
+ @profile.taxes_money=Money.new(444, currency='RUR')
19
+ assert_equal 444, @profile.taxes_amount
20
+ assert_equal 'RUR', @profile.currency
21
+ end
22
+
23
+ def test_net_money_fails
24
+ @profile.taxes_money=Money.new(333, currency='CAD')
25
+ assert_raise ArgumentError do; @profile.net_money=Money.new(444, currency='RUR'); end
26
+ end
27
+
28
+ def test_taxes_money_fails
29
+ @profile.net_money=Money.new(333, currency='CAD')
30
+ assert_raise ArgumentError do; @profile.taxes_money=Money.new(444, currency='RUR'); end
31
+ end
32
+
33
+ def test_mask_card_number
34
+ assert_equal 'abba', @profile.mask_card_number('abba')
35
+ assert_equal 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXched', @profile.mask_card_number('whatisthethingthatcannotbetouched')
36
+ end
37
+
38
+ def test_parse_and_set_card
39
+ card = credit_card(number='1111111111', options={:type => 'master', :year => 2003, :month => 3, :first_name => 'Name', :last_name => 'Surname'})
40
+ @profile.parse_and_set_card(card)
41
+ assert_equal 'master', @profile.card_type
42
+ assert_equal 'Name Surname, MASTER, XXXXXX1111, exp. 2003-03', @profile.card_owner_memo
43
+ end
44
+
45
+ def test_parse_and_set_card_with_hint
46
+ card = credit_card(number='1111111111', options={:type => 'master'})
47
+ @profile.parse_and_set_card(card, 'this is my hint')
48
+ assert_equal 'master', @profile.card_type
49
+ assert_equal 'this is my hint', @profile.card_owner_memo
50
+ end
51
+
52
+ def test_card_exp_date
53
+ card = credit_card(number='4242424242424242', options={:year => 2001, :month => 1})
54
+ assert_equal '2001-01', @profile.card_exp_date(card)
55
+ end
56
+
57
+ def test_no_obsolete_fields
58
+ assert_raise NoMethodError do; @profile.payment_offset = 0; end
59
+ assert_raise NoMethodError do; @profile.payments_start_on = Date.today; end
60
+ end
61
+
62
+ end
@@ -0,0 +1,10 @@
1
+ #TODO: Use auto-loading from activesupport
2
+
3
+ require 'rubygems'
4
+ require 'activerecord'
5
+
6
+ $: << File.dirname(__FILE__) + "/../vendor/money-1.7.1/lib"
7
+
8
+ require File.dirname(__FILE__) + '/lib/models/recurring_payment_profile'
9
+ require File.dirname(__FILE__) + '/lib/models/transaction'
10
+ require File.dirname(__FILE__) + '/lib/recurring_billing_extension'
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005 Tobias Lutke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ == Money class
2
+
3
+ This money class is based on the example from the ActiveRecord doc:
4
+ http://api.rubyonrails.org/classes/ActiveRecord/Aggregations/ClassMethods.html
5
+
6
+ Its in production use at http://www.snowdevil.ca and I haven't found any major issues
7
+ so far.
8
+ The main reason to open source it is because It might be useful to other people and
9
+ I hope i'll get some feedback on how to improve the class.
10
+
11
+ I bundled the exporter with the money class since some tests depend on it and I figured
12
+ that most applications which need to deal with Money also need to deal with proper
13
+ exporting.
14
+
15
+ == Download
16
+
17
+ Preferred method of installation is gem:
18
+
19
+ gem install --source http://dist.leetsoft.com money
20
+
21
+ Alternatively you can get the library packed
22
+
23
+ http://dist.leetsoft.com/pkg/
24
+
25
+ == Usage
26
+
27
+ Use the compose_of helper to let active record deal with embedding the money
28
+ object in your models. The following example requires a cents and a currency field.
29
+
30
+ class ProductUnit < ActiveRecord::Base
31
+ belongs_to :product
32
+ composed_of :price, :class_name => "Money", :mapping => [%w(cents cents) %(currency currency)]
33
+
34
+ private
35
+ validate :cents_not_zero
36
+
37
+ def cents_not_zero
38
+ errors.add("cents", "cannot be zero or less") unless cents > 0
39
+ end
40
+
41
+ validates_presence_of :sku, :currency
42
+ validates_uniqueness_of :sku
43
+ end
44
+
45
+ == Class configuration
46
+
47
+ Two const class variables are available to tailor Money to your needs.
48
+ If you don't need currency exchange at all, just ignore those.
49
+
50
+ === Default Currency
51
+
52
+ By default Money defaults to USD as its currency. This can be overwritten using
53
+
54
+ Money.default_currency = "CAD"
55
+
56
+ If you use rails, the environment.rb is a very good place to put this.
57
+
58
+ === Currency Exchange
59
+
60
+ The second parameter is a bit more complex. It lets you provide your own implementation of the
61
+ currency exchange service. By default Money throws an exception when trying to call .exchange_to.
62
+
63
+ A second minimalist implementation is provided which lets you supply custom exchange rates:
64
+
65
+ Money.bank = VariableExchangeBank.new
66
+ Money.bank.add_rate("USD", "CAD", 1.24515)
67
+ Money.bank.add_rate("CAD", "USD", 0.803115)
68
+ Money.us_dollar(100).exchange_to("CAD") => Money.ca_dollar(124)
69
+ Money.ca_dollar(100).exchange_to("USD") => Money.us_dollar(80)
70
+
71
+ There is nothing stopping you from creating bank objects which scrape www.xe.com for the current rates or just return rand(2)
72
+
73
+ == Code
74
+
75
+ If you have any improvements please email them to tobi [at] leetsoft.com