Skip to content
This repository
Browse code

Exported service_merchant from inner SVN repository

  • Loading branch information...
commit 1a8d719e692cc498deb2794ea932812bec1e1776 0 parents
Alex Lebedev authored

Showing 137 changed files with 13,162 additions and 0 deletions. Show diff stats Hide diff stats

  1. +20 0 MIT-LICENSE.txt
  2. +228 0 README.txt
  3. +121 0 Rakefile
  4. +69 0 demo.rb
  5. +1 0  recurring_billing/lib/am_extensions.rb
  6. +170 0 recurring_billing/lib/am_extensions/paypal_extension.rb
  7. +14 0 recurring_billing/lib/dependencies.rb
  8. +5 0 recurring_billing/lib/gateways.rb
  9. +103 0 recurring_billing/lib/gateways/authorize_net.rb
  10. +124 0 recurring_billing/lib/gateways/paypal.rb
  11. +130 0 recurring_billing/lib/recurring_billing.rb
  12. +87 0 recurring_billing/lib/recurring_billing.rdoc
  13. +81 0 recurring_billing/lib/utils.rb
  14. +33 0 recurring_billing/test/fixtures.yml
  15. +36 0 recurring_billing/test/remote/authorize_net_test.rb
  16. +46 0 recurring_billing/test/remote/paypal_test.rb
  17. +41 0 recurring_billing/test/remote/recurring_billing_test.rb
  18. +153 0 recurring_billing/test/test_helper.rb
  19. +42 0 recurring_billing/test/unit/authorize_net_gateway_class_test.rb
  20. +23 0 recurring_billing/test/unit/paypal_gateway_class_test.rb
  21. +35 0 recurring_billing/test/unit/recurring_billing_gateway_class_test.rb
  22. +17 0 recurring_billing/test/unit/utils_test.rb
  23. +9 0 sample_app/README
  24. +10 0 sample_app/Rakefile
  25. +28 0 sample_app/app/controllers/admin_controller.rb
  26. +36 0 sample_app/app/controllers/application.rb
  27. +121 0 sample_app/app/controllers/subscription_controller.rb
  28. +2 0  sample_app/app/helpers/admin_helper.rb
  29. +3 0  sample_app/app/helpers/application_helper.rb
  30. +2 0  sample_app/app/helpers/subscription_helper.rb
  31. +11 0 sample_app/app/views/admin/index.rhtml
  32. +9 0 sample_app/app/views/admin/problem_subscription.rhtml
  33. +7 0 sample_app/app/views/admin/problem_subscriptions.rhtml
  34. +37 0 sample_app/app/views/admin/tariff_plan.rhtml
  35. +31 0 sample_app/app/views/layouts/admin.rhtml
  36. +33 0 sample_app/app/views/layouts/default.rhtml
  37. +41 0 sample_app/app/views/subscription/_recurring_payment_profile.rhtml
  38. +38 0 sample_app/app/views/subscription/_subscription.rhtml
  39. +25 0 sample_app/app/views/subscription/_tariff_plan.rhtml
  40. +67 0 sample_app/app/views/subscription/index.rhtml
  41. +57 0 sample_app/app/views/subscription/invoice.rpdf
  42. +10 0 sample_app/app/views/subscription/show.rhtml
  43. +71 0 sample_app/app/views/subscription/subscribe.rhtml
  44. +12 0 sample_app/app/views/subscription/unsubscribe.rhtml
  45. +49 0 sample_app/app/views/subscription/update_card.rhtml
  46. +109 0 sample_app/config/boot.rb
  47. +36 0 sample_app/config/database.yml
  48. +96 0 sample_app/config/environment.rb
  49. +17 0 sample_app/config/environments/development.rb
  50. +22 0 sample_app/config/environments/production.rb
  51. +22 0 sample_app/config/environments/test.rb
  52. +10 0 sample_app/config/initializers/inflections.rb
  53. +5 0 sample_app/config/initializers/mime_types.rb
  54. +17 0 sample_app/config/initializers/new_rails_defaults.rb
  55. +44 0 sample_app/config/routes.rb
  56. +9 0 sample_app/lib/tasks/sample_app.rake
  57. 0  sample_app/log/production.log
  58. 0  sample_app/log/server.log
  59. 0  sample_app/log/test.log
  60. +30 0 sample_app/public/404.html
  61. +30 0 sample_app/public/422.html
  62. +30 0 sample_app/public/500.html
  63. +274 0 sample_app/public/_index.html
  64. +10 0 sample_app/public/dispatch.cgi
  65. +24 0 sample_app/public/dispatch.fcgi
  66. +10 0 sample_app/public/dispatch.rb
  67. 0  sample_app/public/favicon.ico
  68. BIN  sample_app/public/images/rails.png
  69. +2 0  sample_app/public/javascripts/application.js
  70. +963 0 sample_app/public/javascripts/controls.js
  71. +972 0 sample_app/public/javascripts/dragdrop.js
  72. +1,120 0 sample_app/public/javascripts/effects.js
  73. +4,225 0 sample_app/public/javascripts/prototype.js
  74. +5 0 sample_app/public/robots.txt
  75. +4 0 sample_app/script/about
  76. +3 0  sample_app/script/console
  77. +3 0  sample_app/script/dbconsole
  78. +3 0  sample_app/script/destroy
  79. +3 0  sample_app/script/generate
  80. +3 0  sample_app/script/performance/benchmarker
  81. +3 0  sample_app/script/performance/profiler
  82. +3 0  sample_app/script/performance/request
  83. +3 0  sample_app/script/plugin
  84. +3 0  sample_app/script/process/inspector
  85. +3 0  sample_app/script/process/reaper
  86. +3 0  sample_app/script/process/spawner
  87. +3 0  sample_app/script/runner
  88. +3 0  sample_app/script/server
  89. +8 0 sample_app/test/functional/admin_controller_test.rb
  90. +8 0 sample_app/test/functional/subscription_controller_test.rb
  91. +38 0 sample_app/test/test_helper.rb
  92. +29 0 subscription_management/Rakefile
  93. +9 0 subscription_management/lib/models/subscription.rb
  94. +4 0 subscription_management/lib/models/subscription_profile.rb
  95. +326 0 subscription_management/lib/subscription_management.rb
  96. +101 0 subscription_management/samples/backpack.yml
  97. +71 0 subscription_management/samples/basecamp.yml
  98. +90 0 subscription_management/samples/brainkeeper.yml
  99. +74 0 subscription_management/samples/campfire.yml
  100. +24 0 subscription_management/samples/clickandpledge.yml
  101. +19 0 subscription_management/samples/demo.rb
  102. +174 0 subscription_management/samples/elm.yml
  103. +78 0 subscription_management/samples/freshbooks.yml
  104. +100 0 subscription_management/samples/highrise.yml
  105. +10 0 subscription_management/samples/presets.yml
  106. 0  subscription_management/samples/tariff.outline.yml
  107. +21 0 subscription_management/samples/taxes.yml
  108. +7 0 subscription_management/subscription_management.rb
  109. +50 0 subscription_management/tasks/schema.rb
  110. +10 0 subscription_management/test/connection.rb
  111. +112 0 subscription_management/test/remote/subscription_management_test.rb
  112. +84 0 subscription_management/test/test_helper.rb
  113. +40 0 subscription_management/test/unit/subscription_management_test.rb
  114. +12 0 tracker/README
  115. +40 0 tracker/Rakefile
  116. 0  tracker/db/migrations/empty-directory
  117. +12 0 tracker/demo.rb
  118. +134 0 tracker/lib/models/recurring_payment_profile.rb
  119. +19 0 tracker/lib/models/transaction.rb
  120. +103 0 tracker/lib/recurring_billing_extension.rb
  121. +34 0 tracker/lib/recurring_billing_extension.rdoc
  122. +66 0 tracker/tasks/schema.rb
  123. +10 0 tracker/test/connection.rb
  124. +35 0 tracker/test/recurring_payment_profile.rb
  125. +68 0 tracker/test/remote/authorize_net_test.rb
  126. +115 0 tracker/test/remote/paypal_test.rb
  127. +87 0 tracker/test/test_helper.rb
  128. +62 0 tracker/test/unit/recurring_payment_profile_test.rb
  129. +10 0 tracker/tracker.rb
  130. +20 0 vendor/money-1.7.1/MIT-LICENSE
  131. +75 0 vendor/money-1.7.1/README
  132. +9 0 vendor/money-1.7.1/lib/bank/no_exchange_bank.rb
  133. +30 0 vendor/money-1.7.1/lib/bank/variable_exchange_bank.rb
  134. +29 0 vendor/money-1.7.1/lib/money.rb
  135. +26 0 vendor/money-1.7.1/lib/money/core_extensions.rb
  136. +209 0 vendor/money-1.7.1/lib/money/money.rb
  137. +57 0 vendor/money-1.7.1/lib/support/cattr_accessor.rb
20 MIT-LICENSE.txt
... ... @@ -0,0 +1,20 @@
  1 +Copyright (c) 2005 Tobias Luetke
  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 PURPOa AND
  17 +NONINFRINGEMENT. IN NO EVENT SaALL 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.
228 README.txt
... ... @@ -0,0 +1,228 @@
  1 +ServiceMerchant
  2 +===============
  3 +
  4 +*Ruby toolkit for recurring billing and subscription management*
  5 +
  6 +ServiceMerchant is an open source library for Software-as-a-Service
  7 +applications, based on subscription payments and various service plans.
  8 +The library consists of number of well-isolated and
  9 +well-defined components, so that you may re-use portions of the library,
  10 +should you find the full functionality not required for you. If you choose
  11 +to use the library as whole, it should be cover most of your payments
  12 +requirements, thus being billing module for your application.
  13 +
  14 +ServiceMerchant's main purpose is providing gateway-independent
  15 +support for recurring billing operations and powerful high-level tools
  16 +for builing subscription-based billing atop of it. It is based on
  17 +well-known [Active Merchant](http://www.activemerchant.org/) library.
  18 +
  19 +ServiceMerchant can be used both as a Rails plugin or standalone Ruby
  20 +library. It is also possible to integrate ServiceMerchant with
  21 +non-Ruby web applications via common GUI or REST interface.
  22 +
  23 +== Supported Gateways
  24 +
  25 +Currently [Authorize.Net](http://www.authorize.net/) and
  26 +[Paypal Website Payments Pro (US)](https://www.paypal.com/cgi-bin/webscr?cmd=_wp-pro-overview-outside)
  27 +are supported.
  28 +
  29 +Generally, if Active Merchant supports some gateway with recurring
  30 +billing features then it is easy to add ServiceMerhant support as
  31 +well. In this case you'll only need to add a few lines of proxy code
  32 +between Active Merchant and commont recurring billing API.
  33 +
  34 +== Components
  35 +
  36 +ServiceMerchant consists of three relatively independent components:
  37 +
  38 +=== Recurring Billing API
  39 +
  40 +Recurring Billing API is aimed at providing uniform interface for
  41 +recurring billing features of payment gateways and making switching
  42 +from one to another as painless as possible.
  43 +
  44 +=== Transaction Tracker
  45 +
  46 +Transactions Tracker stores local and readily available snapshots of
  47 +so-called "recurring billing profiles". With Tracker you can check
  48 +account status much faster than vie gateway query (which not every
  49 +gateway API includes). Transaction Tracker hooks automatically to
  50 +Recurring Billing API and updates your local copy of data according to
  51 +all ongoing operations.
  52 +
  53 +=== Subscription Manager
  54 +
  55 +Subscription Manager provides high-level logic for managic
  56 +subscriptionsm services, tariff plans, payment poliies and so on. You
  57 +can even use it to automatically adjust final price with the tax of
  58 +appropriate region!
  59 +
  60 +== Download
  61 +
  62 +Currently this library is available from:
  63 +
  64 +https://github.com/itteco/service_merchant
  65 +
  66 +== Installation
  67 +
  68 +0. Install Ruby, Rails and dependencies:
  69 +
  70 + In *nix software installation may require root privileges. Use "su"
  71 + or "sudo" in case of lack rights.
  72 +
  73 + 1) Install Ruby and Rails:
  74 +
  75 + please, refer to section #1 and #2 at
  76 + http://wiki.rubyonrails.com/rails/pages/GettingStartedWithRails
  77 +
  78 + 2) Install prerequisites:
  79 +
  80 + gem install activemerchant -v '1.3.2' --include-dependencies
  81 +
  82 + Test suite prerequisites:
  83 +
  84 + gem install mocha --include-dependencies
  85 + gem install rake
  86 +
  87 + You may also need to update rubygems package manager if your
  88 + version is too old
  89 +
  90 + gem install rubygems-update
  91 +
  92 + 3) Install SQLite3 library:
  93 +
  94 + 1. Install SQLite3:
  95 +
  96 + In *nix try your package manager. You'll also need header
  97 + files. On Ubuntu packages names are sqlite3 and sqlite3-dev
  98 + for library and header files respectively.
  99 +
  100 + Under Windows install it manually:
  101 +
  102 + a) download sqlitedll-*.zip from http://sqlite.org (for
  103 + example, http://sqlite.org/sqlitedll-3_6_3.zip)
  104 +
  105 + b) extract sqlite3.dll somewhere within PATH
  106 + (e.g. c:\ruby\bin, c:\windows\system32)
  107 +
  108 + 2. Install Ruby wrapper:
  109 +
  110 + gem install sqlite3-ruby --include-dependencies
  111 + (under Windows select <mswin32> option)
  112 +
  113 + In case of problems under Windows try to use older version:
  114 +
  115 + gem install --version 1.2.3 sqlite3-ruby
  116 +
  117 + 4) [optional] Install HTMLDOC library:
  118 +
  119 + To use invoice generation feature in sample Rails application
  120 +
  121 + 1. Install HTMLDOC:
  122 +
  123 + In *nix try your package manager. Package name is *htmldoc*.
  124 +
  125 + Under Windows download it from
  126 + http://www.easysw.com/htmldoc/software.php and install it
  127 + manually.
  128 +
  129 + 2. Intall Ruby wrapper:
  130 +
  131 + gem install htmldoc
  132 +
  133 +
  134 +1.1. GEM installation:
  135 +
  136 + #TODO#
  137 +
  138 +1.2. Manual installation:
  139 +
  140 + 1) Download and unpack source
  141 +
  142 + 2) [optional] Create ServiceMerchant database (will delete current
  143 + database):
  144 +
  145 + cd {unpack_dir}/trunk
  146 + rake create_all_tables
  147 +
  148 +2. Configuration:
  149 +
  150 + The distribution contains sample config for test usage. See
  151 + tracker/test/fixtures.yml for details. To run remote tests create
  152 +
  153 + !!! WARNING !!!
  154 +
  155 + Always use TEST accounts and TEST mode for your payment gateway
  156 + until you've verified everything works correctly.
  157 +
  158 +
  159 +3. Test suite
  160 +
  161 +Run unit tests:
  162 + rake test:unit
  163 +
  164 +Run remote tests (requires test accounts on gateways, see tracker/test/fixtures.yml):
  165 + rake test:remote
  166 +
  167 +Other test-related tasks:
  168 + rake -T test
  169 +
  170 +== Sample Usage
  171 +
  172 + 1) Simple command-line sample app:
  173 +
  174 + cd {unpack_dir}/trunk
  175 + ./demo.rb
  176 +
  177 + Please, refer to its source code for details.
  178 +
  179 + 2) Simple web site (Ruby on Rails application):
  180 +
  181 + cd {unpack_dir}/trunk/sample_app
  182 + ./script/server
  183 +
  184 + and open http://127.0.0.1:3000/ and http://127.0.0.1:3000/admin in
  185 + your browser. If both URLs does not work, try running
  186 +
  187 + ./script/server webrick
  188 + (instead of ./script/server)
  189 +
  190 + Please, refer to its source code (for example, config/environment.rb
  191 + and app/controllers/*) for details.
  192 +
  193 +Both of these demo applications use sample configs to work.
  194 +
  195 +== Known issues
  196 +
  197 +1. No expection handling is provided for sample applications.
  198 +
  199 +2. Not tested in LIVE payment gateway mode.
  200 +
  201 +3. Database is stored inside module, single ServiceMerchant database
  202 +for all projects.
  203 +
  204 +4. sqlite3-ruby 1.0.0 generates "Table not found" error on some Linux
  205 +machines.
  206 +
  207 +== Roadmap
  208 +
  209 +Add "sync payments status" feature - find out what accounts has
  210 +difficulties with payments - using
  211 +
  212 + 1. Online sync - ask for account status directly from payment
  213 + gateway
  214 +
  215 + 2. Offline sync - import payment gateway report manualy
  216 +
  217 +Pack source code as GEM and publish on rubyforge
  218 +
  219 +== Developers
  220 +
  221 + - Itteco Software (http://www.itteco.com)
  222 + - Alexander Lebedev <me@alexlebedev.com>
  223 + - Artyom Scorecky <tonn81@gmail.com>
  224 + - Anatoly Ivanov <xyzman@gmail.com>
  225 +
  226 +== Contributing
  227 +
  228 + #TODO#
121 Rakefile
... ... @@ -0,0 +1,121 @@
  1 +require 'rubygems'
  2 +require 'rake'
  3 +require 'rake/testtask'
  4 +require 'rake/rdoctask'
  5 +require 'rake/gempackagetask'
  6 +#require 'rake/contrib/rubyforgepublisher'
  7 +
  8 +PKG_VERSION = "0.1.0"
  9 +PKG_NAME = "service_merchant"
  10 +PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
  11 +
  12 +PKG_FILES = FileList[
  13 + "recurring_billing/**/*", "subscription_management/**/*", "tracker/**/*", "[a-zA-Z]*"
  14 +].exclude(/\.svn$/)
  15 +
  16 +task :default => 'dobuild'
  17 +
  18 +task :install => [:package] do
  19 + `gem install pkg/#{PKG_FILE_NAME}.gem`
  20 +end
  21 +
  22 +#TODO: use defaults in task
  23 +task :test => ["test:default"] do
  24 + puts 'All tests run is complete.'
  25 +end
  26 +
  27 +task :dobuild => [:test, :gem] do
  28 + puts 'Build complete'
  29 +end
  30 +
  31 +# Genereate the RDoc documentation
  32 +Rake::RDocTask.new do |rdoc|
  33 + rdoc.rdoc_dir = 'rdoc'
  34 + rdoc.title = "ServiceMerchant library"
  35 + rdoc.options << '--line-numbers' << '--inline-source' << '--main=README' << '--include=tracker/lib/'
  36 + rdoc.rdoc_files.include('README.txt')
  37 + rdoc.rdoc_files.include('recurring_billing/lib/**/*.rb')
  38 + rdoc.rdoc_files.include('subscription_management/lib/**/*.rb')
  39 + rdoc.rdoc_files.include('tracker/lib/**/*.rb')
  40 +end
  41 +
  42 +desc "Delete tar.gz / zip / rdoc"
  43 +task :cleanup => [ :clobber_package ]
  44 +
  45 +spec = Gem::Specification.new do |s|
  46 + s.name = PKG_NAME
  47 + s.version = PKG_VERSION
  48 + s.summary = "Provides UI and tools to add billing feature to web application"
  49 + s.has_rdoc = true
  50 +
  51 + s.files = PKG_FILES
  52 +
  53 + s.add_dependency('activemerchant', '= 1.3.2')
  54 +end
  55 +
  56 +Rake::GemPackageTask.new(spec) do |p|
  57 + p.gem_spec = spec
  58 + p.need_tar = true
  59 + p.need_zip = true
  60 +end
  61 +
  62 +#######################################################################
  63 +
  64 +require File.dirname(__FILE__)+'/tracker/tasks/schema'
  65 +require File.dirname(__FILE__)+'/subscription_management/tasks/schema'
  66 +
  67 +desc "Create all tables"
  68 +task :create_all_tables => ["tracker:create_tables", "subscription:create_tables"]
  69 +
  70 +desc "Drop all tables"
  71 +task :drop_all_tables => ["subscription:drop_tables", "tracker:drop_tables"]
  72 +
  73 +# Run the tests
  74 +namespace :test do
  75 + Rake::TestTask.new(:unit_recurring) do |t|
  76 + t.pattern = 'recurring_billing/test/unit/**/*_test.rb'
  77 + t.ruby_opts << '-rubygems'
  78 + t.verbose = true
  79 + end
  80 +
  81 + Rake::TestTask.new(:unit_tracker) do |t|
  82 + t.pattern = 'tracker/test/unit/**/*_test.rb'
  83 + t.ruby_opts << '-rubygems'
  84 + t.verbose = true
  85 + end
  86 +
  87 + Rake::TestTask.new(:unit_subscription) do |t|
  88 + t.pattern = 'subscription_management/test/unit/**/*_test.rb'
  89 + t.ruby_opts << '-rubygems'
  90 + t.verbose = true
  91 + end
  92 +
  93 + Rake::TestTask.new(:remote_recurring) do |t|
  94 + t.pattern = 'recurring_billing/test/remote/**/*_test.rb'
  95 + t.ruby_opts << '-rubygems'
  96 + t.verbose = true
  97 + end
  98 +
  99 + Rake::TestTask.new(:remote_tracker) do |t|
  100 + t.pattern = 'tracker/test/remote/**/*_test.rb'
  101 + t.ruby_opts << '-rubygems'
  102 + t.verbose = true
  103 + end
  104 +
  105 + Rake::TestTask.new(:remote_subscription) do |t|
  106 + t.pattern = 'subscription_management/test/remote/**/*_test.rb'
  107 + t.ruby_opts << '-rubygems'
  108 + t.verbose = true
  109 + end
  110 +
  111 + desc "Run all unit tests"
  112 + task :unit => [:unit_recurring, :unit_tracker, :unit_subscription]
  113 +
  114 + desc "Run all remote tests"
  115 + task :remote => [:remote_recurring, :remote_tracker, :remote_subscription]
  116 +
  117 + desc "Run both unit and remote tests"
  118 + task :all => [:unit, :remote]
  119 +
  120 + task :default => 'unit'
  121 +end
69 demo.rb
... ... @@ -0,0 +1,69 @@
  1 +#!/usr/bin/ruby
  2 +#
  3 +# Demo of subscription management
  4 +require File.dirname(__FILE__) + '/subscription_management/subscription_management'
  5 +
  6 +require 'rubygems'
  7 +require 'active_merchant'
  8 +
  9 +options = {
  10 + :account_id => 'Test',
  11 + :account_country => 'US',
  12 + :account_state => 'CA',
  13 + :tariff_plan => 'solo_monthly',
  14 + :start_date => (Date.today + 1),
  15 + :quantity => 1,
  16 + :end_date => DateTime.new(2010, 12, 11)
  17 + }
  18 +credit_card = ActiveMerchant::Billing::CreditCard.new({
  19 + :number => 4242424242424242,
  20 + :month => 9,
  21 + :year => Time.now.year + 1,
  22 + :first_name => 'John',
  23 + :last_name => 'Doe',
  24 + :verification_value => '123',
  25 + :type => 'visa'
  26 + })
  27 +
  28 +credit_card_2 = ActiveMerchant::Billing::CreditCard.new({
  29 + :number => 4929838635250031,
  30 + :month => 9,
  31 + :year => Time.now.year + 5,
  32 + :first_name => 'John',
  33 + :last_name => 'Doe',
  34 + :verification_value => '123',
  35 + :type => 'visa'
  36 + })
  37 +
  38 +credit_card_3 = ActiveMerchant::Billing::CreditCard.new({
  39 + :number => 4929273971564532,
  40 + :month => 12,
  41 + :year => Time.now.year + 3,
  42 + :first_name => 'John',
  43 + :last_name => 'Doe',
  44 + :verification_value => '123',
  45 + :type => 'visa'
  46 + })
  47 +
  48 +sm = SubscriptionManagement.new(
  49 + :tariff_plans_config => 'subscription_management/samples/backpack.yml',
  50 + :taxes_config => 'subscription_management/samples/taxes.yml',
  51 + :gateways_config => 'recurring_billing/test/fixtures.yml',
  52 + :gateway => :paypal
  53 + )
  54 +
  55 +subscription_id = sm.subscribe(options)
  56 +sm.pay_for_subscription(subscription_id, credit_card, {})
  57 +features = sm.get_features(subscription_id)
  58 +for feature in features
  59 + print "\n"+SubscriptionManagement.format_feature(feature)
  60 +end
  61 +
  62 +options_sets = [{:card=>credit_card_2}, {:card=>credit_card_3, :start_date => Date.today + 42}]
  63 +options_sets.each do |options|
  64 + print "\nTrying to update subscription using options: #{options.inspect}"
  65 + print "\nWarning: current billing profile on gateway will be canceled and re-created" unless sm.update_possible?(subscription_id, options)
  66 + sm.update_subscription(subscription_id, options)
  67 +end
  68 +
  69 +sm.unsubscribe(subscription_id)
1  recurring_billing/lib/am_extensions.rb
... ... @@ -0,0 +1 @@
  1 +Dir[File.dirname(__FILE__) + '/am_extensions/*.rb'].each{|g| require g}
170 recurring_billing/lib/am_extensions/paypal_extension.rb
... ... @@ -0,0 +1,170 @@
  1 +module ActiveMerchant #:nodoc:
  2 + module Billing #:nodoc:
  3 + class PaypalGateway < Gateway#:nodoc:
  4 +
  5 + # this file was originally located in activemerchant-1.3.2/lib/active_merchant/billing/gateways
  6 + # MANUAL https://www.paypal.com/en_US/pdf/PP_APIReference.pdf
  7 + # See also:
  8 + # http://jadedpixel.lighthouseapp.com/projects/11599/tickets/17-patch-creating-paypal-recurring-payments-profile-with-activemerchant
  9 +
  10 + remove_const("RECURRING_ACTIONS") if defined? RECURRING_ACTIONS
  11 + RECURRING_ACTIONS = Set.new([:add, :modify, :cancel, :inquiry])
  12 +
  13 + # :interval - cannot exceed 1 year
  14 + # :interval[:unit] = :week | :semimonth | :month | :year
  15 +
  16 + def recurring(money, credit_card, options = {})
  17 + options[:name] = credit_card.name if options[:name].blank? && credit_card
  18 + request = build_recurring_request(options[:profile_id].nil? ? :add : :modify, money, options) do |xml|
  19 + add_credit_card(xml, credit_card, options[:billing_address], options) if credit_card
  20 + end
  21 + commit options[:profile_id].nil? ? 'CreateRecurringPaymentsProfile' : 'UpdateRecurringPaymentsProfile', request
  22 + end
  23 +
  24 + def cancel_recurring(profile_id, options)
  25 + request = build_recurring_request(:cancel, nil, options.update( :profile_id => profile_id ))
  26 + commit 'ManageRecurringPaymentsProfileStatus', request
  27 + end
  28 +
  29 + def inquiry_recurring(profile_id, options = {})
  30 + request = build_recurring_request(:inquiry, nil, options.update( :profile_id => profile_id ))
  31 + commit 'GetRecurringPaymentsProfileDetails', request
  32 + end
  33 +
  34 + private
  35 +
  36 + def build_recurring_request(action, money, options)
  37 + unless RECURRING_ACTIONS.include?(action)
  38 + raise StandardError, "Invalid Recurring Profile Action: #{action}"
  39 + end
  40 +
  41 + xml = Builder::XmlMarkup.new :indent => 2
  42 +
  43 + if action == :add
  44 + xml.tag! 'CreateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
  45 + xml.tag! 'CreateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  46 + xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
  47 + xml.tag! 'n2:CreateRecurringPaymentsProfileRequestDetails' do
  48 +
  49 + yield xml # put card information : CreditCardDetails
  50 +
  51 +
  52 + xml.tag! 'n2:RecurringPaymentsProfileDetails' do
  53 + xml.tag! 'n2:BillingStartDate', format_date(options[:starting_at])
  54 + # SubscriberName (optional)
  55 + # SubscriberShippingAddress (optional)
  56 + # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
  57 + end
  58 +
  59 + xml.tag! 'n2:ScheduleDetails' do
  60 + xml.tag! 'n2:Description', options[:description] # <= 127 single-byte alphanumeric characters!!!
  61 + # This field must match the corresponding billing agreement description included in the SetExpressCheckout reques
  62 + # ? MaxFailedPayments
  63 + # ? AutoBillOutstandingAmount = NoAutoBill / AddToNextBilling
  64 +
  65 + xml.tag! 'n2:PaymentPeriod' do
  66 + # if == :semimonth, then payed at 1 & 15 day of month
  67 + xml.tag! 'n2:BillingFrequency', options[:interval][:length]
  68 + xml.tag! 'n2:BillingPeriod', format_unit(options[:interval][:unit])
  69 + xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
  70 + # ShippingAmount (optional)
  71 + # TaxAmount (optional)
  72 + xml.tag! 'n2:TotalBillingCycles', options[:total_payments].to_s unless options[:total_payments].nil?
  73 + end
  74 +
  75 + # WARNING: Activation not tested
  76 + unless options[:activation].nil?
  77 + xml.tag! 'n2:ActivationDetails' do
  78 + xml.tag! 'n2:InitialAmount', amount(options[:activation][:amount]), 'currencyID' => options[:currency] || currency(options[:activation][:amount])
  79 + xml.tag! 'n2:FailedInitAmountAction', options[:activation][:failed_action] unless options[:activation][:failed_action] # 'ContinueOnFailure/CancelOnFailure'
  80 + xml.tag! 'n2:MaxFailedPayments', options[:activation][:max_failed_payments].to_s unless options[:activation][:max_failed_payments].nil?
  81 + end
  82 + end
  83 +
  84 + # WARNING: trial option not tested
  85 + unless options[:trial].nil?
  86 + xml.tag! 'n2:TrialPeriod' do
  87 + frequency, period = get_pay_period(options[:trial][:periodicity])
  88 + xml.tag! 'n2:BillingFrequency', frequency.to_s
  89 + xml.tag! 'n2:BillingPeriod', period
  90 + xml.tag! 'n2:Amount', amount(options[:trial][:amount]), 'currencyID' => options[:currency] || currency(options[:trial][:amount])
  91 + xml.tag! 'n2:TotalBillingCycles', options[:trial][:total_payments].to_s
  92 + end
  93 + end
  94 +
  95 + end
  96 + end
  97 + end
  98 + end
  99 +
  100 + elsif action == :modify
  101 + xml.tag! 'UpdateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
  102 + xml.tag! 'UpdateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  103 + xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
  104 + xml.tag! 'n2:UpdateRecurringPaymentsProfileRequestDetails' do
  105 +
  106 + xml.tag! 'n2:ProfileID', options[:profile_id]
  107 + xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
  108 + xml.tag! 'n2:Description', options[:description] unless options[:description].nil? # <= 127 single-byte alphanumeric characters!!!
  109 +
  110 + # SubscriberName (optional)
  111 + # SubscriberShippingAddress (optional)
  112 + # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
  113 + xml.tag! 'n2:AdditionalBillingCycles', options[:additional_payments].to_s unless options[:additional_payments].nil?
  114 + xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money) unless money.nil?
  115 + # ShippingAmount (optional)
  116 + # TaxAmount (optional)
  117 + # OutStandingBalance (optional)
  118 + # The current past due or outstanding amount for this profile. You can only
  119 + # decrease the outstanding amount—it cannot be increased.
  120 + # ? AutoBillOutstandingAmount (optional) = NoAutoBill / AddToNextBilling
  121 + # ? MaxFailedPayments (optional) = The number of failed payments allowed before the profile is automatically suspended.
  122 +
  123 + yield xml # put card information : CreditCardDetails
  124 + # Only enter credit card details for recurring payments with direct payments.
  125 + # Credit card billing address is optional, but if you update any of the address
  126 + # fields, you must enter all of them. For example, if you want to update the
  127 + # street address, you must specify all of the address fields listed in
  128 + # CreditCardDetailsType, not just the field for the street address.
  129 + end
  130 + end
  131 + end
  132 +
  133 + elsif action == :cancel
  134 + xml.tag! 'ManageRecurringPaymentsProfileStatusReq', 'xmlns' => PAYPAL_NAMESPACE do
  135 + xml.tag! 'ManageRecurringPaymentsProfileStatusRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  136 + xml.tag! 'n2:Version', 50.0
  137 + xml.tag! 'n2:ManageRecurringPaymentsProfileStatusRequestDetails' do
  138 + xml.tag! 'n2:ProfileID', options[:profile_id]
  139 + xml.tag! 'n2:Action', 'Cancel'
  140 + xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
  141 + end
  142 + end
  143 + end
  144 +
  145 + elsif action == :inquiry
  146 + xml.tag! 'GetRecurringPaymentsProfileDetailsReq', 'xmlns' => PAYPAL_NAMESPACE do
  147 + xml.tag! 'GetRecurringPaymentsProfileDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  148 + xml.tag! 'n2:Version', 50.0
  149 + xml.tag! 'ProfileID', options[:profile_id]
  150 + end
  151 + end
  152 + end
  153 + end
  154 +
  155 + def format_date(dat)
  156 + case dat.class.to_s
  157 + when 'Date' then return dat.strftime('%FT%T')
  158 + when 'Time' then return dat.getgm.strftime('%FT%T')
  159 + when 'String' then return dat
  160 + end
  161 + end
  162 +
  163 + def format_unit(unit)
  164 + requires!({:data => unit}, [:data, 'Week', 'SemiMonth', 'Month', 'Year'])
  165 + unit.to_s.downcase.capitalize
  166 + end
  167 +
  168 + end
  169 + end
  170 +end
14 recurring_billing/lib/dependencies.rb
... ... @@ -0,0 +1,14 @@
  1 +require 'rubygems'
  2 +
  3 +gem 'activemerchant'
  4 +require 'active_merchant'
  5 +
  6 +#TODO: Autodiscover new libs from vendor and add them to load path
  7 +$: << File.dirname(__FILE__) + "/../../vendor/money-1.7.1/lib"
  8 +require "money"
  9 +
  10 +gem 'activesupport'
  11 +require 'active_support/core_ext/string/inflections'
  12 +class String # :nodoc:
  13 + include ActiveSupport::CoreExtensions::String::Inflections
  14 +end
5 recurring_billing/lib/gateways.rb
... ... @@ -0,0 +1,5 @@
  1 +require File.dirname(__FILE__) + '/utils'
  2 +require File.dirname(__FILE__) + '/recurring_billing'
  3 +require File.dirname(__FILE__) + '/am_extensions'
  4 +
  5 +Dir[File.dirname(__FILE__) + '/gateways/*.rb'].each{|g| require g}
103 recurring_billing/lib/gateways/authorize_net.rb
... ... @@ -0,0 +1,103 @@
  1 +module RecurringBilling
  2 + class AuthorizeNetGateway < RecurringBillingGateway
  3 + include Utils
  4 +
  5 + def code
  6 + :authorize_net
  7 + end
  8 +
  9 + def name
  10 + 'Authorize.net'
  11 + end
  12 +
  13 + # Check if update is possible using specified arguments
  14 + def correct_update?(billing_id, amount, card, payment_options, recurring_options)
  15 + if !recurring_options.nil? && recurring_options.length > 0
  16 + raise StandardError, 'Cannot update recurring options at #{name} gateway'
  17 + end
  18 + return true
  19 + end
  20 +
  21 + # Make an update using gateway-specific actions
  22 + def update_specific(billing_id, amount, card, payment_options, recurring_options)
  23 + options = compile_options(amount, card, payment_options, recurring_options)
  24 + options[:amount] = amount
  25 + options[:subscription_id] = billing_id
  26 + (@last_response = @gateway.update_recurring(options)).success?
  27 + end
  28 +
  29 + # Create payment using gateway-specific actions
  30 + def create_specific(amount, card, payment_options, recurring_options)
  31 + @last_response = @gateway.recurring(amount, card, compile_options(amount, card, payment_options, recurring_options))
  32 + return @last_response.authorization if @last_response.success?
  33 + nil
  34 + end
  35 +
  36 + # Cancel the subscription
  37 + def delete_specific(billing_id)
  38 + (@last_response = @gateway.cancel_recurring(billing_id)).success?
  39 + end
  40 +
  41 + # Get ready-to-send options hash
  42 + def compile_options(amount, card, payment_options, recurring_options)
  43 + new_options = {}
  44 + if !recurring_options.nil? && !recurring_options.empty?
  45 + requires!(recurring_options, :start_date, :interval)
  46 + requires!(recurring_options, :occurrences) unless recurring_options.has_key?(:end_date)
  47 + requires!(recurring_options, :end_date) unless recurring_options.has_key?(:occurrences)
  48 + transformed_dates = (recurring_options[:occurrences]) ?
  49 + transform_dates(recurring_options[:start_date], recurring_options[:interval], recurring_options[:occurrences], nil) :
  50 + transform_dates(recurring_options[:start_date], recurring_options[:interval], nil, recurring_options[:end_date])
  51 +
  52 + new_options = {:interval => transformed_dates[:interval], :duration => transformed_dates[:duration]}
  53 + end
  54 +
  55 + billing_address = payment_options.has_key?(:billing_address) ? payment_options[:billing_address] : {}
  56 + if (!billing_address.has_key?(:last_name) || billing_address[:last_name].empty?) && card
  57 + billing_address[:last_name] = card.last_name
  58 + billing_address[:first_name] = card.first_name
  59 + end
  60 +
  61 +
  62 + new_options[:billing_address] = billing_address
  63 + new_options[:subscription_name] = payment_options[:subscription_name] if payment_options.has_key?(:subscription_name)
  64 + new_options[:order] = payment_options[:order] if payment_options.has_key?(:order)
  65 +
  66 + return new_options
  67 +
  68 + end
  69 +
  70 + #Transform dates to Authorize.net-recognizable format
  71 + def transform_dates(start_date, interval, occurrences, end_date)
  72 +
  73 + raise ArgumentError, 'Either number of occurences OR end date should be specified' if (!occurrences.nil? && !end_date.nil?) || ((occurrences.nil? && end_date.nil?))
  74 + raise ArgumentError, 'Payment cycle start date ({#start_date}) should be less than or equal to end date ({#end_date})' if !end_date.nil? && (start_date>end_date)
  75 + raise ArgumentError, 'Number of payment occurrences should be a positive integer)' if !occurrences.nil? && (occurrences <= 0)
  76 +
  77 + i_length, i_unit = parse_interval(interval)
  78 +
  79 + if i_length == 0.5 && (i_unit != :y)
  80 + raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
  81 + end
  82 +
  83 + new_interval = case i_unit
  84 + when :d then {:length=>i_length, :unit=>:days}
  85 + when :w then {:length=>i_length*7, :unit=>:days}
  86 + when :m then {:length=>i_length, :unit=>:months}
  87 + when :y then {:length=>i_length*12, :unit=>:months}
  88 + end
  89 + if !occurrences.nil?
  90 + return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>occurrences}}
  91 + else
  92 + if new_interval[:unit] == :days
  93 + new_occurrences = 1 + ((end_date - start_date)/new_interval[:length]).to_i
  94 + elsif new_interval[:unit] == :months
  95 + new_occurrences = 1 + (months_between(end_date, start_date)/new_interval[:length]).to_i
  96 + end
  97 + return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>new_occurrences}}
  98 + end
  99 + end
  100 +
  101 +
  102 + end
  103 +end
124 recurring_billing/lib/gateways/paypal.rb
... ... @@ -0,0 +1,124 @@
  1 +module RecurringBilling
  2 + class PaypalGateway < RecurringBillingGateway
  3 + include Utils
  4 +
  5 + # Returns :paypal
  6 + def code
  7 + :paypal
  8 + end
  9 +
  10 + # Returns 'PayPal Website Payments Pro (US)'
  11 + def name
  12 + 'PayPal Website Payments Pro (US)'
  13 + end
  14 +
  15 + # Checks whether passed parameters of requested recurring payment conform to specification
  16 + def correct_create?(amount, card, payment_options, recurring_options)
  17 + raise ArgumentError, 'Ammount must be defined and more than zero' if amount.nil? || amount.zero?
  18 + raise ArgumentError, 'Card is mandatory' if card.nil? # must be object of CreditCard class
  19 + raise ArgumentError, 'Subscription name is mandatory' if payment_options[:subscription_name].to_s.empty?
  20 + raise ArgumentError, 'Starting date is mandatory' if recurring_options[:start_date].to_s.empty?
  21 + raise ArgumentError, 'Interval is mandatory' if recurring_options[:interval].to_s.empty?
  22 + # end_date and occurrences - both can be ommited
  23 + return true
  24 + end
  25 +
  26 +
  27 + # Checks if update is possible using specified arguments
  28 + def correct_update?(billing_id, amount, card, payment_options, recurring_options)
  29 + raise ArgumentError, 'Billing ID is mandatory' if billing_id.to_s.empty?
  30 + raise ArgumentError, 'Starting date cannot be updated' if !recurring_options[:start_date].to_s.empty?
  31 + raise ArgumentError, 'Interval cannot be updated' if !recurring_options[:interval].to_s.empty?
  32 +
  33 + if !(recurring_options[:end_date].to_s.empty? && recurring_options[:occurrences].to_s.empty?)
  34 + raise NotImplementedError, 'Cannot shift the end of recurring payment'
  35 + # it is made via "AdditionalBillingCycles", so we have to know previous data
  36 + end
  37 + return true
  38 + end
  39 +
  40 + # Create payment using gateway-specific actions
  41 + def create_specific(amount, card, payment_options, recurring_options)
  42 + @last_response = @gateway.recurring(amount, card, convert_options(payment_options, recurring_options))
  43 + return @last_response.params['profile_id'] if @last_response.success?
  44 + nil
  45 + end
  46 +
  47 + # Make an update using gateway-specific actions
  48 + def update_specific(billing_id, amount, card, payment_options, recurring_options)
  49 + options = convert_options(payment_options, recurring_options)
  50 + options[:profile_id] = billing_id
  51 + (@last_response = @gateway.recurring(amount, card, options)).success?
  52 + end
  53 +
  54 + # Cancel the subscription
  55 + # TODO: Add :note parameter to API to enable it in update and cancel
  56 + def delete_specific(billing_id)
  57 + (@last_response = @gateway.cancel_recurring(billing_id, {})).success?
  58 + end
  59 +
  60 + # TODO: Unify result parameters names and values
  61 + def inquiry_specific(billing_id)
  62 + @last_response = @gateway.inquiry_recurring(billing_id)
  63 + result = @last_response.params.clone
  64 +
  65 + result.each do |k,v|
  66 + if k =~ /(^number_|_count$|_cycles(_|$)|_payments$|_frequency$|_month$|_year$)/
  67 + result[k] = v.to_i
  68 + elsif k =~ /(_date|^timestamp)$/
  69 + result[k] = DateTime.parse(v)
  70 + elsif (k =~ /(_|^)amount(_paid)?$/ && k != 'auto_bill_outstanding_amount') || k =~ /_balance$/
  71 + currency = result[k+'_currency_id']
  72 + result[k] = Money.new(v.to_f*100, currency=currency) # dollars => cents
  73 + elsif k =~ /_(status|period|card_type)$/
  74 + result[k] = v.downcase
  75 + end
  76 + end
  77 +
  78 + result['profile_status'] =~ /^(.*)Profile$/i
  79 + result['profile_status'] = $1 # active | pending | cancelled | suspended | expired
  80 +
  81 + return result.reject {|k,v| k =~ /_currency_id$/}
  82 + end
  83 +
  84 +
  85 + def convert_options(payment_options, recurring_options)
  86 + options = {}
  87 + options[:billing_address] = payment_options[:billing_address] if !payment_options[:billing_address].nil?
  88 + options[:description] = payment_options[:subscription_name]
  89 + options[:starting_at] = recurring_options[:start_date]
  90 + options[:total_payments] = recurring_options[:occurrences] if !recurring_options[:occurrences].nil?
  91 + options[:interval] = convert_interval(recurring_options[:interval]) if !recurring_options[:interval].nil? # absent for update
  92 + options[:currency] = payment_options[:currency] if !payment_options[:currency].nil?
  93 + #options[:note] = payment_options[:note] if !payment_options[:note].nil?
  94 + return options
  95 + end
  96 +
  97 +
  98 + def convert_interval(interval)
  99 + i_length, i_unit = parse_interval(interval)
  100 +
  101 + if i_length == 0.5 && ![:m,:y].include?(i_unit)
  102 + raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
  103 + end
  104 +
  105 + if [i_length, i_unit] == [0.5, :m]
  106 + return {:length => 1, :unit => 'SemiMonth'}
  107 + elsif [i_length, i_unit] == [0.5, :y]
  108 + i_length, i_unit = [6, :m]
  109 + end
  110 +
  111 + return {:length => i_length, :unit => convert_unit(i_unit)}
  112 + end
  113 +
  114 + def convert_unit(unit)
  115 + return case unit
  116 + when :d then 'Day'
  117 + when :w then 'Week'
  118 + when :m then 'Month'
  119 + when :y then 'Year'
  120 + end
  121 + end
  122 +
  123 + end
  124 +end
130 recurring_billing/lib/recurring_billing.rb
... ... @@ -0,0 +1,130 @@
  1 +require File.dirname(__FILE__) + '/dependencies'
  2 +
  3 +# RecurringBilling module provides common API for managing recurring billing operations
  4 +# via remote gateway. All manipulations are done through instances of RecurringBillingGateway
  5 +# and its descendants (though direct use of that class descendants is discouraged).
  6 +#
  7 +# Please see RecurringBillingGateway for more detailed reference.
  8 +module RecurringBilling
  9 +
  10 + #:include:recurring_billing.rdoc
  11 + #:include:recurring_billing_extension.rdoc
  12 + class RecurringBillingGateway
  13 + include ActiveMerchant::RequiresParameters
  14 + attr_reader :last_response
  15 +
  16 + # Returns code that is used to identify the gateway
  17 + def code
  18 + raise NotImplementedError, 'Method is virtual'
  19 + end
  20 +
  21 + # Returns gateway name
  22 + def name
  23 + raise NotImplementedError, 'Method is virtual'
  24 + end
  25 +
  26 + # Creates a new recurring billing gateway
  27 + def initialize(options)#:nodoc:
  28 + @gateway = ::ActiveMerchant::Billing::Base.gateway(code).new(
  29 + :login => options[:login],
  30 + :password => options[:password],
  31 + :test => options[:is_test].nil? ? false : options[:is_test], # false by default
  32 + :signature => options[:signature]
  33 + )
  34 + @last_response = nil
  35 + end
  36 +
  37 + # Creates a recurring payment
  38 + def create(amount, card, payment_options={}, recurring_options={})
  39 + if correct_create?(amount, card, payment_options, recurring_options)
  40 + create_specific(amount, card, payment_options, recurring_options)
  41 + end
  42 + end
  43 +
  44 + # Updates a recurring payment
  45 + def update(billing_id, amount=nil, card=nil, payment_options={}, recurring_options={})
  46 + if correct_update?(billing_id, amount, card, payment_options, recurring_options)
  47 + update_specific(billing_id, amount, card, payment_options, recurring_options)
  48 + end
  49 + end
  50 +
  51 + # Deletes a recurring payment
  52 + def delete(billing_id)
  53 + delete_specific(billing_id)
  54 + end
  55 +
  56 + # Asks for status of recurring payment
  57 + def inquiry(billing_id)
  58 + inquiry_specific(billing_id)
  59 + end
  60 +
  61 + class << self
  62 + # Converts single options hash into hash of parameters used by create|update methods
  63 + #
  64 + # :amount or :billing_amount => amount
  65 + # :card => card
  66 + # :subscription_name, :billing_address, :order, :taxes_amount_included => payment_options
  67 + # :start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences => recurring_options
  68 + def separate_create_update_params_from_options(options)
  69 + payment_options, recurring_options = {}, {}
  70 + amount = options[:billing_amount] unless amount = options[:amount]
  71 + card = options[:card]
  72 + options.each do |k,v|
  73 + payment_options[k] = v if [:subscription_name, :billing_address, :order, :taxes_amount_included].include?(k)
  74 + recurring_options[k] = v if [:start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences, :trial_days, :pay_on_day_x].include?(k)
  75 + end
  76 +
  77 + return {:amount => amount, :card => card, :payment_options => payment_options, :recurring_options => recurring_options}
  78 + end
  79 +
  80 +
  81 + # Returns an instance of RecurringBillingGateway for selected gateway
  82 + #
  83 + # options <= hash of :gateway, :login, :password, :is_test(optional), :signature(optional)
  84 + def get_instance(options)
  85 + raise ArgumentError, ':gateway key is required' unless options.has_key?(:gateway)
  86 +
  87 + gateway = RecurringBilling.const_get("#{options[:gateway].to_s.downcase}_gateway".camelize)
  88 + gateway.new(options)
  89 + end
  90 + end
  91 +
  92 + ###
  93 + protected
  94 + # Checks whether requested change can be done via simple update (or recreate needed)
  95 + def correct_update?(billing_id, amount, card, payment_options, recurring_options)
  96 + raise NotImplementedError, 'Method is virtual'
  97 + end
  98 +
  99 + # Make an update using gateway-specific actions
  100 + def update_specific(billing_id, amount, card, payment_options, recurring_options)
  101 + raise NotImplementedError, 'Method is virtual'
  102 + end
  103 + # Checks whether passed parameters of requested recurring payment conform to specification
  104 + def correct_create?(amount, card, payment_options, recurring_options)
  105 + raise ArgumentError, 'Card must be of ActiveMerchant::Billing::CreditCard' unless card.is_a?(ActiveMerchant::Billing::CreditCard)
  106 + if (!recurring_options) || !(recurring_options.has_key?(:end_date) || recurring_options.has_key?(:occurrences))
  107 + raise StandardError, 'Either payments'' end date or number of payment occurences should be set'
  108 + end
  109 + return true
  110 + end
  111 +
  112 + # Creates a recurring payment using gateway-specific actions (virtual)
  113 + def create_specific(amount, card, payment_options, recurring_options)
  114 + raise NotImplementedError, 'Method is virtual'
  115 + end
  116 +
  117 + # Deletes a recurring payment
  118 + def delete_specific(billing_id)
  119 + raise NotImplementedError, 'Method is virtual'
  120 + end
  121 +
  122 + # Inquires status of given subscription profile on payment gateway.
  123 + def inquiry_specific(billing_id)
  124 + raise NotImplementedError, 'Method is virtual'
  125 + end
  126 +
  127 + end
  128 +end
  129 +
  130 +require File.dirname(__FILE__) + "/gateways"
87 recurring_billing/lib/recurring_billing.rdoc
Source Rendered
... ... @@ -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'}