Permalink
Browse files

Exported service_merchant from inner SVN repository

  • Loading branch information...
0 parents commit 1a8d719e692cc498deb2794ea932812bec1e1776 Alex Lebedev committed Oct 14, 2008
Showing with 13,162 additions and 0 deletions.
  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 @@
+Copyright (c) 2005 Tobias Luetke
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND
+NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
228 README.txt
@@ -0,0 +1,228 @@
+ServiceMerchant
+===============
+
+*Ruby toolkit for recurring billing and subscription management*
+
+ServiceMerchant is an open source library for Software-as-a-Service
+applications, based on subscription payments and various service plans.
+The library consists of number of well-isolated and
+well-defined components, so that you may re-use portions of the library,
+should you find the full functionality not required for you. If you choose
+to use the library as whole, it should be cover most of your payments
+requirements, thus being billing module for your application.
+
+ServiceMerchant's main purpose is providing gateway-independent
+support for recurring billing operations and powerful high-level tools
+for builing subscription-based billing atop of it. It is based on
+well-known [Active Merchant](http://www.activemerchant.org/) library.
+
+ServiceMerchant can be used both as a Rails plugin or standalone Ruby
+library. It is also possible to integrate ServiceMerchant with
+non-Ruby web applications via common GUI or REST interface.
+
+== Supported Gateways
+
+Currently [Authorize.Net](http://www.authorize.net/) and
+[Paypal Website Payments Pro (US)](https://www.paypal.com/cgi-bin/webscr?cmd=_wp-pro-overview-outside)
+are supported.
+
+Generally, if Active Merchant supports some gateway with recurring
+billing features then it is easy to add ServiceMerhant support as
+well. In this case you'll only need to add a few lines of proxy code
+between Active Merchant and commont recurring billing API.
+
+== Components
+
+ServiceMerchant consists of three relatively independent components:
+
+=== Recurring Billing API
+
+Recurring Billing API is aimed at providing uniform interface for
+recurring billing features of payment gateways and making switching
+from one to another as painless as possible.
+
+=== Transaction Tracker
+
+Transactions Tracker stores local and readily available snapshots of
+so-called "recurring billing profiles". With Tracker you can check
+account status much faster than vie gateway query (which not every
+gateway API includes). Transaction Tracker hooks automatically to
+Recurring Billing API and updates your local copy of data according to
+all ongoing operations.
+
+=== Subscription Manager
+
+Subscription Manager provides high-level logic for managic
+subscriptionsm services, tariff plans, payment poliies and so on. You
+can even use it to automatically adjust final price with the tax of
+appropriate region!
+
+== Download
+
+Currently this library is available from:
+
+https://github.com/itteco/service_merchant
+
+== Installation
+
+0. Install Ruby, Rails and dependencies:
+
+ In *nix software installation may require root privileges. Use "su"
+ or "sudo" in case of lack rights.
+
+ 1) Install Ruby and Rails:
+
+ please, refer to section #1 and #2 at
+ http://wiki.rubyonrails.com/rails/pages/GettingStartedWithRails
+
+ 2) Install prerequisites:
+
+ gem install activemerchant -v '1.3.2' --include-dependencies
+
+ Test suite prerequisites:
+
+ gem install mocha --include-dependencies
+ gem install rake
+
+ You may also need to update rubygems package manager if your
+ version is too old
+
+ gem install rubygems-update
+
+ 3) Install SQLite3 library:
+
+ 1. Install SQLite3:
+
+ In *nix try your package manager. You'll also need header
+ files. On Ubuntu packages names are sqlite3 and sqlite3-dev
+ for library and header files respectively.
+
+ Under Windows install it manually:
+
+ a) download sqlitedll-*.zip from http://sqlite.org (for
+ example, http://sqlite.org/sqlitedll-3_6_3.zip)
+
+ b) extract sqlite3.dll somewhere within PATH
+ (e.g. c:\ruby\bin, c:\windows\system32)
+
+ 2. Install Ruby wrapper:
+
+ gem install sqlite3-ruby --include-dependencies
+ (under Windows select <mswin32> option)
+
+ In case of problems under Windows try to use older version:
+
+ gem install --version 1.2.3 sqlite3-ruby
+
+ 4) [optional] Install HTMLDOC library:
+
+ To use invoice generation feature in sample Rails application
+
+ 1. Install HTMLDOC:
+
+ In *nix try your package manager. Package name is *htmldoc*.
+
+ Under Windows download it from
+ http://www.easysw.com/htmldoc/software.php and install it
+ manually.
+
+ 2. Intall Ruby wrapper:
+
+ gem install htmldoc
+
+
+1.1. GEM installation:
+
+ #TODO#
+
+1.2. Manual installation:
+
+ 1) Download and unpack source
+
+ 2) [optional] Create ServiceMerchant database (will delete current
+ database):
+
+ cd {unpack_dir}/trunk
+ rake create_all_tables
+
+2. Configuration:
+
+ The distribution contains sample config for test usage. See
+ tracker/test/fixtures.yml for details. To run remote tests create
+
+ !!! WARNING !!!
+
+ Always use TEST accounts and TEST mode for your payment gateway
+ until you've verified everything works correctly.
+
+
+3. Test suite
+
+Run unit tests:
+ rake test:unit
+
+Run remote tests (requires test accounts on gateways, see tracker/test/fixtures.yml):
+ rake test:remote
+
+Other test-related tasks:
+ rake -T test
+
+== Sample Usage
+
+ 1) Simple command-line sample app:
+
+ cd {unpack_dir}/trunk
+ ./demo.rb
+
+ Please, refer to its source code for details.
+
+ 2) Simple web site (Ruby on Rails application):
+
+ cd {unpack_dir}/trunk/sample_app
+ ./script/server
+
+ and open http://127.0.0.1:3000/ and http://127.0.0.1:3000/admin in
+ your browser. If both URLs does not work, try running
+
+ ./script/server webrick
+ (instead of ./script/server)
+
+ Please, refer to its source code (for example, config/environment.rb
+ and app/controllers/*) for details.
+
+Both of these demo applications use sample configs to work.
+
+== Known issues
+
+1. No expection handling is provided for sample applications.
+
+2. Not tested in LIVE payment gateway mode.
+
+3. Database is stored inside module, single ServiceMerchant database
+for all projects.
+
+4. sqlite3-ruby 1.0.0 generates "Table not found" error on some Linux
+machines.
+
+== Roadmap
+
+Add "sync payments status" feature - find out what accounts has
+difficulties with payments - using
+
+ 1. Online sync - ask for account status directly from payment
+ gateway
+
+ 2. Offline sync - import payment gateway report manualy
+
+Pack source code as GEM and publish on rubyforge
+
+== Developers
+
+ - Itteco Software (http://www.itteco.com)
+ - Alexander Lebedev <me@alexlebedev.com>
+ - Artyom Scorecky <tonn81@gmail.com>
+ - Anatoly Ivanov <xyzman@gmail.com>
+
+== Contributing
+
+ #TODO#
121 Rakefile
@@ -0,0 +1,121 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+#require 'rake/contrib/rubyforgepublisher'
+
+PKG_VERSION = "0.1.0"
+PKG_NAME = "service_merchant"
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+PKG_FILES = FileList[
+ "recurring_billing/**/*", "subscription_management/**/*", "tracker/**/*", "[a-zA-Z]*"
+].exclude(/\.svn$/)
+
+task :default => 'dobuild'
+
+task :install => [:package] do
+ `gem install pkg/#{PKG_FILE_NAME}.gem`
+end
+
+#TODO: use defaults in task
+task :test => ["test:default"] do
+ puts 'All tests run is complete.'
+end
+
+task :dobuild => [:test, :gem] do
+ puts 'Build complete'
+end
+
+# Genereate the RDoc documentation
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "ServiceMerchant library"
+ rdoc.options << '--line-numbers' << '--inline-source' << '--main=README' << '--include=tracker/lib/'
+ rdoc.rdoc_files.include('README.txt')
+ rdoc.rdoc_files.include('recurring_billing/lib/**/*.rb')
+ rdoc.rdoc_files.include('subscription_management/lib/**/*.rb')
+ rdoc.rdoc_files.include('tracker/lib/**/*.rb')
+end
+
+desc "Delete tar.gz / zip / rdoc"
+task :cleanup => [ :clobber_package ]
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Provides UI and tools to add billing feature to web application"
+ s.has_rdoc = true
+
+ s.files = PKG_FILES
+
+ s.add_dependency('activemerchant', '= 1.3.2')
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+#######################################################################
+
+require File.dirname(__FILE__)+'/tracker/tasks/schema'
+require File.dirname(__FILE__)+'/subscription_management/tasks/schema'
+
+desc "Create all tables"
+task :create_all_tables => ["tracker:create_tables", "subscription:create_tables"]
+
+desc "Drop all tables"
+task :drop_all_tables => ["subscription:drop_tables", "tracker:drop_tables"]
+
+# Run the tests
+namespace :test do
+ Rake::TestTask.new(:unit_recurring) do |t|
+ t.pattern = 'recurring_billing/test/unit/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:unit_tracker) do |t|
+ t.pattern = 'tracker/test/unit/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:unit_subscription) do |t|
+ t.pattern = 'subscription_management/test/unit/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:remote_recurring) do |t|
+ t.pattern = 'recurring_billing/test/remote/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:remote_tracker) do |t|
+ t.pattern = 'tracker/test/remote/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ Rake::TestTask.new(:remote_subscription) do |t|
+ t.pattern = 'subscription_management/test/remote/**/*_test.rb'
+ t.ruby_opts << '-rubygems'
+ t.verbose = true
+ end
+
+ desc "Run all unit tests"
+ task :unit => [:unit_recurring, :unit_tracker, :unit_subscription]
+
+ desc "Run all remote tests"
+ task :remote => [:remote_recurring, :remote_tracker, :remote_subscription]
+
+ desc "Run both unit and remote tests"
+ task :all => [:unit, :remote]
+
+ task :default => 'unit'
+end
69 demo.rb
@@ -0,0 +1,69 @@
+#!/usr/bin/ruby
+#
+# Demo of subscription management
+require File.dirname(__FILE__) + '/subscription_management/subscription_management'
+
+require 'rubygems'
+require 'active_merchant'
+
+options = {
+ :account_id => 'Test',
+ :account_country => 'US',
+ :account_state => 'CA',
+ :tariff_plan => 'solo_monthly',
+ :start_date => (Date.today + 1),
+ :quantity => 1,
+ :end_date => DateTime.new(2010, 12, 11)
+ }
+credit_card = ActiveMerchant::Billing::CreditCard.new({
+ :number => 4242424242424242,
+ :month => 9,
+ :year => Time.now.year + 1,
+ :first_name => 'John',
+ :last_name => 'Doe',
+ :verification_value => '123',
+ :type => 'visa'
+ })
+
+credit_card_2 = ActiveMerchant::Billing::CreditCard.new({
+ :number => 4929838635250031,
+ :month => 9,
+ :year => Time.now.year + 5,
+ :first_name => 'John',
+ :last_name => 'Doe',
+ :verification_value => '123',
+ :type => 'visa'
+ })
+
+credit_card_3 = ActiveMerchant::Billing::CreditCard.new({
+ :number => 4929273971564532,
+ :month => 12,
+ :year => Time.now.year + 3,
+ :first_name => 'John',
+ :last_name => 'Doe',
+ :verification_value => '123',
+ :type => 'visa'
+ })
+
+sm = SubscriptionManagement.new(
+ :tariff_plans_config => 'subscription_management/samples/backpack.yml',
+ :taxes_config => 'subscription_management/samples/taxes.yml',
+ :gateways_config => 'recurring_billing/test/fixtures.yml',
+ :gateway => :paypal
+ )
+
+subscription_id = sm.subscribe(options)
+sm.pay_for_subscription(subscription_id, credit_card, {})
+features = sm.get_features(subscription_id)
+for feature in features
+ print "\n"+SubscriptionManagement.format_feature(feature)
+end
+
+options_sets = [{:card=>credit_card_2}, {:card=>credit_card_3, :start_date => Date.today + 42}]
+options_sets.each do |options|
+ print "\nTrying to update subscription using options: #{options.inspect}"
+ print "\nWarning: current billing profile on gateway will be canceled and re-created" unless sm.update_possible?(subscription_id, options)
+ sm.update_subscription(subscription_id, options)
+end
+
+sm.unsubscribe(subscription_id)
1 recurring_billing/lib/am_extensions.rb
@@ -0,0 +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 @@
+module ActiveMerchant #:nodoc:
+ module Billing #:nodoc:
+ class PaypalGateway < Gateway#:nodoc:
+
+ # this file was originally located in activemerchant-1.3.2/lib/active_merchant/billing/gateways
+ # MANUAL https://www.paypal.com/en_US/pdf/PP_APIReference.pdf
+ # See also:
+ # http://jadedpixel.lighthouseapp.com/projects/11599/tickets/17-patch-creating-paypal-recurring-payments-profile-with-activemerchant
+
+ remove_const("RECURRING_ACTIONS") if defined? RECURRING_ACTIONS
+ RECURRING_ACTIONS = Set.new([:add, :modify, :cancel, :inquiry])
+
+ # :interval - cannot exceed 1 year
+ # :interval[:unit] = :week | :semimonth | :month | :year
+
+ def recurring(money, credit_card, options = {})
+ options[:name] = credit_card.name if options[:name].blank? && credit_card
+ request = build_recurring_request(options[:profile_id].nil? ? :add : :modify, money, options) do |xml|
+ add_credit_card(xml, credit_card, options[:billing_address], options) if credit_card
+ end
+ commit options[:profile_id].nil? ? 'CreateRecurringPaymentsProfile' : 'UpdateRecurringPaymentsProfile', request
+ end
+
+ def cancel_recurring(profile_id, options)
+ request = build_recurring_request(:cancel, nil, options.update( :profile_id => profile_id ))
+ commit 'ManageRecurringPaymentsProfileStatus', request
+ end
+
+ def inquiry_recurring(profile_id, options = {})
+ request = build_recurring_request(:inquiry, nil, options.update( :profile_id => profile_id ))
+ commit 'GetRecurringPaymentsProfileDetails', request
+ end
+
+ private
+
+ def build_recurring_request(action, money, options)
+ unless RECURRING_ACTIONS.include?(action)
+ raise StandardError, "Invalid Recurring Profile Action: #{action}"
+ end
+
+ xml = Builder::XmlMarkup.new :indent => 2
+
+ if action == :add
+ xml.tag! 'CreateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
+ xml.tag! 'CreateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
+ xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
+ xml.tag! 'n2:CreateRecurringPaymentsProfileRequestDetails' do
+
+ yield xml # put card information : CreditCardDetails
+
+
+ xml.tag! 'n2:RecurringPaymentsProfileDetails' do
+ xml.tag! 'n2:BillingStartDate', format_date(options[:starting_at])
+ # SubscriberName (optional)
+ # SubscriberShippingAddress (optional)
+ # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
+ end
+
+ xml.tag! 'n2:ScheduleDetails' do
+ xml.tag! 'n2:Description', options[:description] # <= 127 single-byte alphanumeric characters!!!
+ # This field must match the corresponding billing agreement description included in the SetExpressCheckout reques
+ # ? MaxFailedPayments
+ # ? AutoBillOutstandingAmount = NoAutoBill / AddToNextBilling
+
+ xml.tag! 'n2:PaymentPeriod' do
+ # if == :semimonth, then payed at 1 & 15 day of month
+ xml.tag! 'n2:BillingFrequency', options[:interval][:length]
+ xml.tag! 'n2:BillingPeriod', format_unit(options[:interval][:unit])
+ xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
+ # ShippingAmount (optional)
+ # TaxAmount (optional)
+ xml.tag! 'n2:TotalBillingCycles', options[:total_payments].to_s unless options[:total_payments].nil?
+ end
+
+ # WARNING: Activation not tested
+ unless options[:activation].nil?
+ xml.tag! 'n2:ActivationDetails' do
+ xml.tag! 'n2:InitialAmount', amount(options[:activation][:amount]), 'currencyID' => options[:currency] || currency(options[:activation][:amount])
+ xml.tag! 'n2:FailedInitAmountAction', options[:activation][:failed_action] unless options[:activation][:failed_action] # 'ContinueOnFailure/CancelOnFailure'
+ xml.tag! 'n2:MaxFailedPayments', options[:activation][:max_failed_payments].to_s unless options[:activation][:max_failed_payments].nil?
+ end
+ end
+
+ # WARNING: trial option not tested
+ unless options[:trial].nil?
+ xml.tag! 'n2:TrialPeriod' do
+ frequency, period = get_pay_period(options[:trial][:periodicity])
+ xml.tag! 'n2:BillingFrequency', frequency.to_s
+ xml.tag! 'n2:BillingPeriod', period
+ xml.tag! 'n2:Amount', amount(options[:trial][:amount]), 'currencyID' => options[:currency] || currency(options[:trial][:amount])
+ xml.tag! 'n2:TotalBillingCycles', options[:trial][:total_payments].to_s
+ end
+ end
+
+ end
+ end
+ end
+ end
+
+ elsif action == :modify
+ xml.tag! 'UpdateRecurringPaymentsProfileReq', 'xmlns' => PAYPAL_NAMESPACE do
+ xml.tag! 'UpdateRecurringPaymentsProfileRequest', 'xmlns:n2' => EBAY_NAMESPACE do
+ xml.tag! 'n2:Version', 50.0 # API_VERSION # must be >= 50.0
+ xml.tag! 'n2:UpdateRecurringPaymentsProfileRequestDetails' do
+
+ xml.tag! 'n2:ProfileID', options[:profile_id]
+ xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
+ xml.tag! 'n2:Description', options[:description] unless options[:description].nil? # <= 127 single-byte alphanumeric characters!!!
+
+ # SubscriberName (optional)
+ # SubscriberShippingAddress (optional)
+ # ProfileReference (optional) = The merchant’s own unique reference or invoice number.
+ xml.tag! 'n2:AdditionalBillingCycles', options[:additional_payments].to_s unless options[:additional_payments].nil?
+ xml.tag! 'n2:Amount', amount(money), 'currencyID' => options[:currency] || currency(money) unless money.nil?
+ # ShippingAmount (optional)
+ # TaxAmount (optional)
+ # OutStandingBalance (optional)
+ # The current past due or outstanding amount for this profile. You can only
+ # decrease the outstanding amount—it cannot be increased.
+ # ? AutoBillOutstandingAmount (optional) = NoAutoBill / AddToNextBilling
+ # ? MaxFailedPayments (optional) = The number of failed payments allowed before the profile is automatically suspended.
+
+ yield xml # put card information : CreditCardDetails
+ # Only enter credit card details for recurring payments with direct payments.
+ # Credit card billing address is optional, but if you update any of the address
+ # fields, you must enter all of them. For example, if you want to update the
+ # street address, you must specify all of the address fields listed in
+ # CreditCardDetailsType, not just the field for the street address.
+ end
+ end
+ end
+
+ elsif action == :cancel
+ xml.tag! 'ManageRecurringPaymentsProfileStatusReq', 'xmlns' => PAYPAL_NAMESPACE do
+ xml.tag! 'ManageRecurringPaymentsProfileStatusRequest', 'xmlns:n2' => EBAY_NAMESPACE do
+ xml.tag! 'n2:Version', 50.0
+ xml.tag! 'n2:ManageRecurringPaymentsProfileStatusRequestDetails' do
+ xml.tag! 'n2:ProfileID', options[:profile_id]
+ xml.tag! 'n2:Action', 'Cancel'
+ xml.tag! 'n2:Note', options[:note] unless options[:note].nil?
+ end
+ end
+ end
+
+ elsif action == :inquiry
+ xml.tag! 'GetRecurringPaymentsProfileDetailsReq', 'xmlns' => PAYPAL_NAMESPACE do
+ xml.tag! 'GetRecurringPaymentsProfileDetailsRequest', 'xmlns:n2' => EBAY_NAMESPACE do
+ xml.tag! 'n2:Version', 50.0
+ xml.tag! 'ProfileID', options[:profile_id]
+ end
+ end
+ end
+ end
+
+ def format_date(dat)
+ case dat.class.to_s
+ when 'Date' then return dat.strftime('%FT%T')
+ when 'Time' then return dat.getgm.strftime('%FT%T')
+ when 'String' then return dat
+ end
+ end
+
+ def format_unit(unit)
+ requires!({:data => unit}, [:data, 'Week', 'SemiMonth', 'Month', 'Year'])
+ unit.to_s.downcase.capitalize
+ end
+
+ end
+ end
+end
14 recurring_billing/lib/dependencies.rb
@@ -0,0 +1,14 @@
+require 'rubygems'
+
+gem 'activemerchant'
+require 'active_merchant'
+
+#TODO: Autodiscover new libs from vendor and add them to load path
+$: << File.dirname(__FILE__) + "/../../vendor/money-1.7.1/lib"
+require "money"
+
+gem 'activesupport'
+require 'active_support/core_ext/string/inflections'
+class String # :nodoc:
+ include ActiveSupport::CoreExtensions::String::Inflections
+end
5 recurring_billing/lib/gateways.rb
@@ -0,0 +1,5 @@
+require File.dirname(__FILE__) + '/utils'
+require File.dirname(__FILE__) + '/recurring_billing'
+require File.dirname(__FILE__) + '/am_extensions'
+
+Dir[File.dirname(__FILE__) + '/gateways/*.rb'].each{|g| require g}
103 recurring_billing/lib/gateways/authorize_net.rb
@@ -0,0 +1,103 @@
+module RecurringBilling
+ class AuthorizeNetGateway < RecurringBillingGateway
+ include Utils
+
+ def code
+ :authorize_net
+ end
+
+ def name
+ 'Authorize.net'
+ end
+
+ # Check if update is possible using specified arguments
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
+ if !recurring_options.nil? && recurring_options.length > 0
+ raise StandardError, 'Cannot update recurring options at #{name} gateway'
+ end
+ return true
+ end
+
+ # Make an update using gateway-specific actions
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
+ options = compile_options(amount, card, payment_options, recurring_options)
+ options[:amount] = amount
+ options[:subscription_id] = billing_id
+ (@last_response = @gateway.update_recurring(options)).success?
+ end
+
+ # Create payment using gateway-specific actions
+ def create_specific(amount, card, payment_options, recurring_options)
+ @last_response = @gateway.recurring(amount, card, compile_options(amount, card, payment_options, recurring_options))
+ return @last_response.authorization if @last_response.success?
+ nil
+ end
+
+ # Cancel the subscription
+ def delete_specific(billing_id)
+ (@last_response = @gateway.cancel_recurring(billing_id)).success?
+ end
+
+ # Get ready-to-send options hash
+ def compile_options(amount, card, payment_options, recurring_options)
+ new_options = {}
+ if !recurring_options.nil? && !recurring_options.empty?
+ requires!(recurring_options, :start_date, :interval)
+ requires!(recurring_options, :occurrences) unless recurring_options.has_key?(:end_date)
+ requires!(recurring_options, :end_date) unless recurring_options.has_key?(:occurrences)
+ transformed_dates = (recurring_options[:occurrences]) ?
+ transform_dates(recurring_options[:start_date], recurring_options[:interval], recurring_options[:occurrences], nil) :
+ transform_dates(recurring_options[:start_date], recurring_options[:interval], nil, recurring_options[:end_date])
+
+ new_options = {:interval => transformed_dates[:interval], :duration => transformed_dates[:duration]}
+ end
+
+ billing_address = payment_options.has_key?(:billing_address) ? payment_options[:billing_address] : {}
+ if (!billing_address.has_key?(:last_name) || billing_address[:last_name].empty?) && card
+ billing_address[:last_name] = card.last_name
+ billing_address[:first_name] = card.first_name
+ end
+
+
+ new_options[:billing_address] = billing_address
+ new_options[:subscription_name] = payment_options[:subscription_name] if payment_options.has_key?(:subscription_name)
+ new_options[:order] = payment_options[:order] if payment_options.has_key?(:order)
+
+ return new_options
+
+ end
+
+ #Transform dates to Authorize.net-recognizable format
+ def transform_dates(start_date, interval, occurrences, end_date)
+
+ raise ArgumentError, 'Either number of occurences OR end date should be specified' if (!occurrences.nil? && !end_date.nil?) || ((occurrences.nil? && end_date.nil?))
+ 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)
+ raise ArgumentError, 'Number of payment occurrences should be a positive integer)' if !occurrences.nil? && (occurrences <= 0)
+
+ i_length, i_unit = parse_interval(interval)
+
+ if i_length == 0.5 && (i_unit != :y)
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
+ end
+
+ new_interval = case i_unit
+ when :d then {:length=>i_length, :unit=>:days}
+ when :w then {:length=>i_length*7, :unit=>:days}
+ when :m then {:length=>i_length, :unit=>:months}
+ when :y then {:length=>i_length*12, :unit=>:months}
+ end
+ if !occurrences.nil?
+ return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>occurrences}}
+ else
+ if new_interval[:unit] == :days
+ new_occurrences = 1 + ((end_date - start_date)/new_interval[:length]).to_i
+ elsif new_interval[:unit] == :months
+ new_occurrences = 1 + (months_between(end_date, start_date)/new_interval[:length]).to_i
+ end
+ return {:interval=>new_interval, :duration=>{:start_date=>start_date, :occurrences=>new_occurrences}}
+ end
+ end
+
+
+ end
+end
124 recurring_billing/lib/gateways/paypal.rb
@@ -0,0 +1,124 @@
+module RecurringBilling
+ class PaypalGateway < RecurringBillingGateway
+ include Utils
+
+ # Returns :paypal
+ def code
+ :paypal
+ end
+
+ # Returns 'PayPal Website Payments Pro (US)'
+ def name
+ 'PayPal Website Payments Pro (US)'
+ end
+
+ # Checks whether passed parameters of requested recurring payment conform to specification
+ def correct_create?(amount, card, payment_options, recurring_options)
+ raise ArgumentError, 'Ammount must be defined and more than zero' if amount.nil? || amount.zero?
+ raise ArgumentError, 'Card is mandatory' if card.nil? # must be object of CreditCard class
+ raise ArgumentError, 'Subscription name is mandatory' if payment_options[:subscription_name].to_s.empty?
+ raise ArgumentError, 'Starting date is mandatory' if recurring_options[:start_date].to_s.empty?
+ raise ArgumentError, 'Interval is mandatory' if recurring_options[:interval].to_s.empty?
+ # end_date and occurrences - both can be ommited
+ return true
+ end
+
+
+ # Checks if update is possible using specified arguments
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
+ raise ArgumentError, 'Billing ID is mandatory' if billing_id.to_s.empty?
+ raise ArgumentError, 'Starting date cannot be updated' if !recurring_options[:start_date].to_s.empty?
+ raise ArgumentError, 'Interval cannot be updated' if !recurring_options[:interval].to_s.empty?
+
+ if !(recurring_options[:end_date].to_s.empty? && recurring_options[:occurrences].to_s.empty?)
+ raise NotImplementedError, 'Cannot shift the end of recurring payment'
+ # it is made via "AdditionalBillingCycles", so we have to know previous data
+ end
+ return true
+ end
+
+ # Create payment using gateway-specific actions
+ def create_specific(amount, card, payment_options, recurring_options)
+ @last_response = @gateway.recurring(amount, card, convert_options(payment_options, recurring_options))
+ return @last_response.params['profile_id'] if @last_response.success?
+ nil
+ end
+
+ # Make an update using gateway-specific actions
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
+ options = convert_options(payment_options, recurring_options)
+ options[:profile_id] = billing_id
+ (@last_response = @gateway.recurring(amount, card, options)).success?
+ end
+
+ # Cancel the subscription
+ # TODO: Add :note parameter to API to enable it in update and cancel
+ def delete_specific(billing_id)
+ (@last_response = @gateway.cancel_recurring(billing_id, {})).success?
+ end
+
+ # TODO: Unify result parameters names and values
+ def inquiry_specific(billing_id)
+ @last_response = @gateway.inquiry_recurring(billing_id)
+ result = @last_response.params.clone
+
+ result.each do |k,v|
+ if k =~ /(^number_|_count$|_cycles(_|$)|_payments$|_frequency$|_month$|_year$)/
+ result[k] = v.to_i
+ elsif k =~ /(_date|^timestamp)$/
+ result[k] = DateTime.parse(v)
+ elsif (k =~ /(_|^)amount(_paid)?$/ && k != 'auto_bill_outstanding_amount') || k =~ /_balance$/
+ currency = result[k+'_currency_id']
+ result[k] = Money.new(v.to_f*100, currency=currency) # dollars => cents
+ elsif k =~ /_(status|period|card_type)$/
+ result[k] = v.downcase
+ end
+ end
+
+ result['profile_status'] =~ /^(.*)Profile$/i
+ result['profile_status'] = $1 # active | pending | cancelled | suspended | expired
+
+ return result.reject {|k,v| k =~ /_currency_id$/}
+ end
+
+
+ def convert_options(payment_options, recurring_options)
+ options = {}
+ options[:billing_address] = payment_options[:billing_address] if !payment_options[:billing_address].nil?
+ options[:description] = payment_options[:subscription_name]
+ options[:starting_at] = recurring_options[:start_date]
+ options[:total_payments] = recurring_options[:occurrences] if !recurring_options[:occurrences].nil?
+ options[:interval] = convert_interval(recurring_options[:interval]) if !recurring_options[:interval].nil? # absent for update
+ options[:currency] = payment_options[:currency] if !payment_options[:currency].nil?
+ #options[:note] = payment_options[:note] if !payment_options[:note].nil?
+ return options
+ end
+
+
+ def convert_interval(interval)
+ i_length, i_unit = parse_interval(interval)
+
+ if i_length == 0.5 && ![:m,:y].include?(i_unit)
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
+ end
+
+ if [i_length, i_unit] == [0.5, :m]
+ return {:length => 1, :unit => 'SemiMonth'}
+ elsif [i_length, i_unit] == [0.5, :y]
+ i_length, i_unit = [6, :m]
+ end
+
+ return {:length => i_length, :unit => convert_unit(i_unit)}
+ end
+
+ def convert_unit(unit)
+ return case unit
+ when :d then 'Day'
+ when :w then 'Week'
+ when :m then 'Month'
+ when :y then 'Year'
+ end
+ end
+
+ end
+end
130 recurring_billing/lib/recurring_billing.rb
@@ -0,0 +1,130 @@
+require File.dirname(__FILE__) + '/dependencies'
+
+# RecurringBilling module provides common API for managing recurring billing operations
+# via remote gateway. All manipulations are done through instances of RecurringBillingGateway
+# and its descendants (though direct use of that class descendants is discouraged).
+#
+# Please see RecurringBillingGateway for more detailed reference.
+module RecurringBilling
+
+ #:include:recurring_billing.rdoc
+ #:include:recurring_billing_extension.rdoc
+ class RecurringBillingGateway
+ include ActiveMerchant::RequiresParameters
+ attr_reader :last_response
+
+ # Returns code that is used to identify the gateway
+ def code
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ # Returns gateway name
+ def name
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ # Creates a new recurring billing gateway
+ def initialize(options)#:nodoc:
+ @gateway = ::ActiveMerchant::Billing::Base.gateway(code).new(
+ :login => options[:login],
+ :password => options[:password],
+ :test => options[:is_test].nil? ? false : options[:is_test], # false by default
+ :signature => options[:signature]
+ )
+ @last_response = nil
+ end
+
+ # Creates a recurring payment
+ def create(amount, card, payment_options={}, recurring_options={})
+ if correct_create?(amount, card, payment_options, recurring_options)
+ create_specific(amount, card, payment_options, recurring_options)
+ end
+ end
+
+ # Updates a recurring payment
+ def update(billing_id, amount=nil, card=nil, payment_options={}, recurring_options={})
+ if correct_update?(billing_id, amount, card, payment_options, recurring_options)
+ update_specific(billing_id, amount, card, payment_options, recurring_options)
+ end
+ end
+
+ # Deletes a recurring payment
+ def delete(billing_id)
+ delete_specific(billing_id)
+ end
+
+ # Asks for status of recurring payment
+ def inquiry(billing_id)
+ inquiry_specific(billing_id)
+ end
+
+ class << self
+ # Converts single options hash into hash of parameters used by create|update methods
+ #
+ # :amount or :billing_amount => amount
+ # :card => card
+ # :subscription_name, :billing_address, :order, :taxes_amount_included => payment_options
+ # :start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences => recurring_options
+ def separate_create_update_params_from_options(options)
+ payment_options, recurring_options = {}, {}
+ amount = options[:billing_amount] unless amount = options[:amount]
+ card = options[:card]
+ options.each do |k,v|
+ payment_options[k] = v if [:subscription_name, :billing_address, :order, :taxes_amount_included].include?(k)
+ recurring_options[k] = v if [:start_date, :interval, :end_date, :trial_end, :occurrences, :trial_occurrences, :trial_days, :pay_on_day_x].include?(k)
+ end
+
+ return {:amount => amount, :card => card, :payment_options => payment_options, :recurring_options => recurring_options}
+ end
+
+
+ # Returns an instance of RecurringBillingGateway for selected gateway
+ #
+ # options <= hash of :gateway, :login, :password, :is_test(optional), :signature(optional)
+ def get_instance(options)
+ raise ArgumentError, ':gateway key is required' unless options.has_key?(:gateway)
+
+ gateway = RecurringBilling.const_get("#{options[:gateway].to_s.downcase}_gateway".camelize)
+ gateway.new(options)
+ end
+ end
+
+ ###
+ protected
+ # Checks whether requested change can be done via simple update (or recreate needed)
+ def correct_update?(billing_id, amount, card, payment_options, recurring_options)
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ # Make an update using gateway-specific actions
+ def update_specific(billing_id, amount, card, payment_options, recurring_options)
+ raise NotImplementedError, 'Method is virtual'
+ end
+ # Checks whether passed parameters of requested recurring payment conform to specification
+ def correct_create?(amount, card, payment_options, recurring_options)
+ raise ArgumentError, 'Card must be of ActiveMerchant::Billing::CreditCard' unless card.is_a?(ActiveMerchant::Billing::CreditCard)
+ if (!recurring_options) || !(recurring_options.has_key?(:end_date) || recurring_options.has_key?(:occurrences))
+ raise StandardError, 'Either payments'' end date or number of payment occurences should be set'
+ end
+ return true
+ end
+
+ # Creates a recurring payment using gateway-specific actions (virtual)
+ def create_specific(amount, card, payment_options, recurring_options)
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ # Deletes a recurring payment
+ def delete_specific(billing_id)
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ # Inquires status of given subscription profile on payment gateway.
+ def inquiry_specific(billing_id)
+ raise NotImplementedError, 'Method is virtual'
+ end
+
+ end
+end
+
+require File.dirname(__FILE__) + "/gateways"
87 recurring_billing/lib/recurring_billing.rdoc
@@ -0,0 +1,87 @@
+This class provides unified API to access remote payment gateways.
+
+== Quick overview
+=== Creating a gateway accessor
+There are two options to get an instance of an accessor for required gateway. First is to use get_instance method:
+ options = {:gateway => :authorize_net, :login => 'MyLogin', :password => 'MyPassword'}
+ gateway = RecurringBilling::RecurringBillingGateway.get_instance(options)
+which basically is an alias for the second method, directly calling
+the constructor of RecurringBilling::AuthorizeNetGateway (or any other RecurringBillingGateway descendant):
+ gateway = RecurringBilling::AuthorizeNetGateway({:login => 'MyLogin', :password => 'MyPassword'})
+First method is recommended to be used for more flexibility.
+
+=== Common options for RecurringBilling methods
+It should be self-explanatory that every recurring payment itself has many parameters - those parameters have to be specified
+when the payment is created and updated on remote gateway as well. Within the API, these are grouped for convenience into four groups
+that have become the input parameters for create and update methods:
+ - Amount
+ - Card
+ - Payment Options
+ - Recurring Options
+
+<b>Amount</b>(amount) is the object of Money class, which includes both amount and currency of the payment.
+
+<b>Card</b>(card) is the object of ActiveMerchant::Billing::CreditCard representing the credit card that is charged during recurring payments
+
+<b>Payment Options</b>(payment_options) are a hash of:
+- :subscription_name - +string+ containing the reference name for the subscription
+- :billing_address - +hash+ containing subscriber's billing address (see ActiveMerchant for more info on structure)
+- :order - +hash+ containing merchant's info about order, like invoice ID (see ActiveMerchant for more info on structure)
+- :taxes_amount_included - +boolean+ declaring if taxes are included in payment amount or not
+This set of options contains information of merchant.
+
+<b>Recurring Options</b>(payment_options) are a hash of:
+- :start_date - +Date+ object, date of the first billing occurrence
+- :interval - +string+ of <tt>(0.5|d+)\s*(d|w|m|y)/</tt> template that shows time between two consecutive billing occurrences
+- :end_date - +Date+ object representing date after which there are no more billing occurrences
+- :occurrences - positive +integer+ showing number of billing occurrences. Either this or :end_date should be specified
+- :trial_days - positive +integer+ showing number of days of free trial
+This set of options declares settings of payment recurring occurrences.
+
+=== Creating a remote payment
+Once gateway is created, payments could be made. The whole idea is very simple - prepare parameters and use the
+create method of an instance:
+ amount = Money.us_dollar(100) # $1 USD
+ card = ActiveMerchant::Billing::CreditCard.new({
+ :number => '4242424242424242',
+ :month => 9,
+ :year => Time.now.year + 1,
+ :first_name => 'Name',
+ :last_name => 'Lastname',
+ :verification_value => '123',
+ :type => 'visa'
+ }) # Some random credit card
+ payment_options = {
+ :subscription_name => 'Random subscription',
+ :order => {:invoice_number => 'ODMX31337'}
+ }
+ recurring_options = {
+ :start_date => Date.today,
+ :occurrences => 10,
+ :interval => '10d'
+ }
+ billing_id = gateway.create(amount, card, payment_options, recurring_options)
+You may then check the result of recurring payment creation by accessing last_response:
+ response = gateway.last_response
+ print response.inspect
+Exact structure of last_response return value is defined in respective ActiveMerchant module. Most common
+uses of last_response query are:
+ unless response.success? # succeed-failed check
+ print response.message # get message retrieved from remote gateway
+ ...
+Last response is changed whenever create, update, inquiry or delete methods are called.
+
+=== Updating and canceling a remote payment
+To update the settings of recurring payment on gateway, just call the update method of an instance.
+ new_amount = Money.us_dollar(150) # $1.5 USD
+ success = gateway.update(billing_id, new_amount, nil, {}, {}) # or just gateway.update(new_amount)
+Please note that acceptable update parameters vary with gateway. Correctness of parameters for update is
+checked via protected correct_update? method.
+
+Recurring payment may be cancelled via delete method:
+ success = gateway.delete(billing_id)
+
+=== Inquiring gateway on payment status
+In order to inquire remote gateway on status of given subscription, inquiry method of an instance should be used:
+ result = gateway.inquiry(billing_id)
+The result structure varies from gateway to gateway.
81 recurring_billing/lib/utils.rb
@@ -0,0 +1,81 @@
+# This module contains common functions that ServiceMerchant uses.
+module Utils
+ #Counts nubmer of months between two dates
+ #
+ # 2008/1/1, 2007/12/31 => 1
+ # 2008/9/10, 2009/10/20 => 13
+ # ETC
+ def months_between(date1, date2)
+ if date1 > date2
+ recent_date = date1.to_date
+ past_date = date2.to_date
+ else
+ recent_date = date2.to_date
+ past_date = date1.to_date
+ end
+ years_diff = recent_date.year - past_date.year
+ months_diff = recent_date.month - past_date.month + ((recent_date.day >= past_date.day) ? 0 : -1)
+ if months_diff < 0
+ months_diff = 12 + months_diff
+ years_diff -= 1
+ end
+ return years_diff*12 + months_diff
+ end
+
+ # Parses given INTERVAL string into hash.
+ #
+ # Sample use:
+ # "1w" => {:length => 1, :unit => :w }
+ # "0.5 m" => {:length => 0.5, :unit => :m }
+ # "10 d" => {:length => 10, :unit => :d }
+ # "3y" => {:length => 3, :unit => :y }
+ # "0.25 w" => ArgumentError
+ # "2 x" => ArgumentError
+ def parse_interval(interval)
+ if (interval =~ /^(\d+|0\.5)\s*(d|w|m|y)$/i)
+ return $1 == '0.5' ? 0.5 : $1.to_i, $2.downcase.to_sym
+ end
+ raise ArgumentError, "Invalid value format for payment interval: #{interval}"
+ end
+
+ # Returns number of payment occurrences between END_DATE and START_DATE with INTERVAL frequency.
+ #
+ # INTERVAL should be given in the same format parse_interval uses. END_DATE and START_DATE should be either Date or DateTime.
+ # Returned number includes initial payment.
+ #
+ # Sample use:
+ # [Date.today(), '1 w', Date.today()+18] => 3
+ # [DateTime.new(2008, 09, 20), '1y', DateTime.new(2009, 09, 19)] => 1
+ def get_occurrences(start_date, interval, end_date)
+
+ start_date = Date.new(start_date.year,start_date.month,start_date.mday)
+ end_date = Date.new(end_date.year,end_date.month,end_date.mday)
+ raise ArgumentError, "Start date (#{start_date}) should be less than or equal to end date (#{end_date})" if (start_date>end_date)
+
+ i_length, i_unit = parse_interval(interval)
+
+ if i_length == 0.5 && ![:m, :y].include?(i_unit)
+ raise ArgumentError, "Semi- interval is not supported to this units (#{i_unit.to_s})"
+ end
+
+ new_interval = case i_unit
+ when :d then {:length=>i_length, :unit=>:days}
+ when :w then {:length=>i_length*7, :unit=>:days}
+ when :m then {:length=>i_length, :unit=>:months}
+ when :y then {:length=>i_length*12, :unit=>:months}
+ end
+
+ if new_interval[:unit] == :days
+ new_occurrences = 1 + ((end_date - start_date)/new_interval[:length]).to_i
+ elsif new_interval[:unit] == :months
+ new_occurrences = 1 + (months_between(end_date, start_date)/new_interval[:length]).to_i
+ end
+ return new_occurrences
+ end
+
+ # Returns midnight of specified DateTime object (as DateTime).
+ def get_midnight(datetime_x)
+ DateTime.new(datetime_x.year,datetime_x.month,datetime_x.mday)
+ end
+
+end
33 recurring_billing/test/fixtures.yml
@@ -0,0 +1,33 @@
+# This file has all of the ActiveMerchant test account credentials.
+# Many gateways do not offer publicly available test accounts. In
+# order to make testing the gateways easy you can copy this file to
+# your home directory as the file ~/.active_merchant/fixtures.yml
+# You can then place your own test account credentials in your local
+# copy of the file.
+#
+# If the login is numeric, ensure that you place quotes around it.
+# Leading zeros will be lost when YAML parses the file if you don't.
+#
+# Paste any required PEM certificates after the pem key.
+#
+authorize_net:
+ gateway: authorize_net
+ login: 6zz6m5N4Et
+ password: 9V9wUv6Yd92t27t5
+ is_test: true
+
+# You can use either your API PEM file or API signature with PayPal.
+paypal_certificate:
+ gateway: paypal
+ login: LOGIN
+ password: PASSWORD
+ subject:
+ pem: |--
+ PASTE YOUR PEM FILE HERE
+
+paypal:
+ gateway: paypal
+ login: a.lebe_1220380924_biz_api1.list.ru
+ password: 1220380928
+ signature: An5ns1Kso7MWUdW4ErQKJJJ4qi4-ABavC-ayabELBOsjVjsDpfMCgJRN
+ is_test: true
36 recurring_billing/test/remote/authorize_net_test.rb
@@ -0,0 +1,36 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AuthorizeNetGatewayRemoteTest < Test::Unit::TestCase
+
+ def setup
+ credentials = fixtures(:authorize_net)
+ assert @gw = RecurringBilling::AuthorizeNetGateway.new(credentials.update({:is_test => true}))
+ assert_equal @gw.name, 'Authorize.net'
+ @card = credit_card()
+ end
+
+ def test_crud_recurring_payment
+ payment_options = {
+ :subscription_name => 'Test Subscription 1337',
+ :order => {:invoice_number => '407933'}
+ }
+ recurring_options = {
+ :start_date => Date.today + 1,
+ :end_date => Date.today + 290,
+ :interval => '1m'
+ }
+
+ billing_id = @gw.create(Money.us_dollar(15), @card, payment_options, recurring_options)
+ print "Create:\n"
+ print @gw.last_response.inspect
+ payment_options[:order] = {:invoice_number => '407934'}
+ @gw.update(billing_id, Money.us_dollar(16), @card, payment_options, nil)
+ print "\nUpdate:\n"
+ print @gw.last_response.inspect
+ @gw.delete(billing_id)
+ print "\nDelete:\n"
+ print @gw.last_response.inspect
+ assert true
+ end
+
+end
46 recurring_billing/test/remote/paypal_test.rb
@@ -0,0 +1,46 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PaypalGatewayRemoteTest < Test::Unit::TestCase
+
+ def setup
+ cred = fixtures(:paypal)
+ assert @gw = RecurringBilling::PaypalGateway.new(cred.update({:is_test=>true}))
+ assert_equal @gw.name, 'PayPal Website Payments Pro (US)'
+ @card = credit_card()
+ end
+
+ def test_crud_recurring_payment
+ payment_options = {
+ :subscription_name => 'Test Subscription 1337',
+ :order => {:invoice_number => '407933'}
+ }
+ recurring_options = {
+ :start_date => Date.today + 1,
+ :end_date => Date.today + 290,
+ :interval => '1m'
+ }
+
+ print "\nCreate:\n"
+ billing_id = @gw.create(Money.us_dollar(15), @card, payment_options=payment_options, recurring_options=recurring_options)
+ print @gw.last_response.inspect
+ payment_options[:order] = {:invoice_number => '407934'}
+ assert @gw.last_response.success?
+
+ print "\n\nUpdate:\n"
+ @gw.update(billing_id, Money.us_dollar(16), @card, payment_options=payment_options)
+ print @gw.last_response.inspect
+ assert @gw.last_response.success?
+
+ print "\n\nInquiry:\n"
+ result = @gw.inquiry(billing_id)
+ print @gw.last_response.inspect
+ print "\n\n", result.inspect
+ assert @gw.last_response.success?
+
+ print "\n\nDelete:\n"
+ @gw.delete(billing_id)
+ print @gw.last_response.inspect
+ assert @gw.last_response.success?
+ end
+
+end
41 recurring_billing/test/remote/recurring_billing_test.rb
@@ -0,0 +1,41 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RecurringBillingRemoteTest < Test::Unit::TestCase
+
+ def setup
+ @card = credit_card()
+ end
+
+ def perform_generic_test(gateway)
+ random_invoice = "%06d" % rand(999999)
+ payment_options = {
+ :subscription_name => 'Test Subscription 1337',
+ :order => {:invoice_number => random_invoice}
+ }
+ recurring_options = {
+ :start_date => Date.today + 1,
+ :end_date => Date.today + 290,
+ :interval => '1m'
+ }
+ credentials = fixtures(gateway)
+ assert gw = RecurringBilling::RecurringBillingGateway.get_instance(credentials)
+
+ billing_id = gw.create(Money.us_dollar(15), @card, payment_options, recurring_options)
+ assert gw.last_response.success?
+ new_random_invoice = "%06d" % rand(999999)
+ payment_options[:order] = {:invoice_number => new_random_invoice}
+ gw.update(billing_id, Money.us_dollar(16), @card, payment_options, {})
+ assert gw.last_response.success?
+ gw.delete(billing_id)
+ assert gw.last_response.success?
+ end
+
+ def test_paypal
+ perform_generic_test(:paypal)
+ end
+
+ def test_authorize_net
+ perform_generic_test(:authorize_net)
+ end
+
+end
153 recurring_billing/test/test_helper.rb
@@ -0,0 +1,153 @@
+#!/usr/bin/env ruby
+require 'rubygems'
+require 'test/unit'
+
+require 'active_merchant'
+
+require File.dirname(__FILE__) + '/../lib/gateways'
+
+# Turn off invalid certificate crashes
+require 'openssl'
+silence_warnings do
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
+end
+
+ActiveMerchant::Billing::Base.mode = :test
+
+module RecurringBilling
+ module Assertions
+ def assert_field(field, value)
+ clean_backtrace do
+ assert_equal value, @helper.fields[field]
+ end
+ end
+
+ # Allows the testing of you to check for negative assertions:
+ #
+ # # Instead of
+ # assert !something_that_is_false
+ #
+ # # Do this
+ # assert_false something_that_should_be_false
+ #
+ # An optional +msg+ parameter is available to help you debug.
+ def assert_false(boolean, message = nil)
+ message = build_message message, '<?> is not false or nil.', boolean
+
+ clean_backtrace do
+ assert_block message do
+ not boolean
+ end
+ end
+ end
+
+ # A handy little assertion to check for a successful response:
+ #
+ # # Instead of
+ # assert_success response
+ #
+ # # DRY that up with
+ # assert_success response
+ #
+ # A message will automatically show the inspection of the response
+ # object if things go wrong.
+ def assert_success(response)
+ clean_backtrace do
+ assert response.success?, "Response failed: #{response.inspect}"
+ end
+ end
+
+ # The negative of +assert_success+
+ def assert_failure(response)
+ clean_backtrace do
+ assert_false response.success?, "Response expected to fail: #{response.inspect}"
+ end
+ end
+
+ def assert_valid(validateable)
+ clean_backtrace do
+ assert validateable.valid?, "Expected to be valid"
+ end
+ end
+
+ def assert_not_valid(validateable)
+ clean_backtrace do
+ assert_false validateable.valid?, "Expected to not be valid"
+ end
+ end
+
+ private
+ def clean_backtrace(&block)
+ yield
+ rescue Test::Unit::AssertionFailedError => e
+ path = File.expand_path(__FILE__)
+ raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
+ end
+ end
+end
+
+module Test
+ module Unit
+ class TestCase
+
+ include RecurringBilling::Assertions
+ include Utils
+
+ DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml'
+
+ private
+ def credit_card(number = '4242424242424242', options = {})
+ defaults = {
+ :number => number,
+ :month => 9,
+ :year => Time.now.year + 1,
+ :first_name => 'John',
+ :last_name => 'Doe',
+ :verification_value => '123',
+ :type => 'visa'
+ }.update(options)
+
+ ActiveMerchant::Billing::CreditCard.new(defaults)
+ end
+
+ def address(options = {})
+ {
+ :name => 'John Doe',
+ :address1 => '1234 My Street',
+ :address2 => 'Apt 1',
+ :company => 'Widgets Inc',
+ :city => 'Ottawa',
+ :state => 'ON',
+ :zip => 'K1C2N6',
+ :country => 'CA',
+ :phone => '(555)555-5555'
+ }.update(options)
+ end
+
+ def all_fixtures
+ @@fixtures ||= load_fixtures
+ end
+
+ def fixtures(key)
+ data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
+
+ data.dup
+ end
+
+ def load_fixtures
+ file = DEFAULT_CREDENTIALS
+ yaml_data = YAML.load(File.read(file))
+ symbolize_keys(yaml_data)
+
+ yaml_data
+ end
+
+ def symbolize_keys(hash)
+ return unless hash.is_a?(Hash)
+
+ hash.symbolize_keys!
+ hash.each{|k,v| symbolize_keys(v)}
+ end
+ end
+ end
+end
42 recurring_billing/test/unit/authorize_net_gateway_class_test.rb
@@ -0,0 +1,42 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AuthorizeNetGatewayTest < Test::Unit::TestCase
+
+ def setup
+ credentials = fixtures(:authorize_net)
+ assert @gw = RecurringBilling::AuthorizeNetGateway.new(credentials.update({:is_test => true}))
+ assert_equal @gw.name, 'Authorize.net'
+ @card = credit_card()
+ end
+
+ def test_correct_update?
+ #def correct_update?(billing_id, amount, card, payment_options, recurring_options)
+ subscr_id = 'SOMERANDOMID'
+ assert_raise StandardError do; @gw.correct_update?(subscr_id, nil, @card, nil, {:start_date => Date.today + 1}); end
+ assert_nothing_thrown do;@gw.correct_update?(subscr_id, 1, @card, nil, nil);end
+ assert_nothing_thrown do;@gw.correct_update?(subscr_id, 100, @card, nil, {});end
+
+ end
+
+ def test_transform_dates
+ def d(string);return Date.parse(string);end
+ def h(a,b,c,d);return {:interval=>{:length=>a, :unit=>b}, :duration=>{:start_date=>c, :occurrences=>d}};end
+
+ #syntax is: transform_dates(start_date, interval, occurrences, end_date)
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '5m', nil, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '5m', 5, d('2009/01/01')); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), 'm', 1, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '0.5m', 1, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '1 year', 1, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '8', 1, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '5m', -1, nil); end
+ assert_raise ArgumentError do; @gw.transform_dates(d('2008/01/01'), '5m', nil, d('2007/12/31')); end
+
+ assert_equal @gw.transform_dates(d('2008/01/01'), '5m', 7, nil), h(5,:months,d('2008/01/01'),7)
+ assert_equal @gw.transform_dates(d('2008/01/01'), '3 d', nil, d('2008/01/19')), h(3,:days,d('2008/01/01'),7)
+ assert_equal @gw.transform_dates(d('2008/01/01'), '3d', nil, d('2008/01/21')), h(3,:days,d('2008/01/01'),7)
+ assert_equal @gw.transform_dates(d('2008/01/01'), '8w', 13, nil), h(56,:days,d('2008/01/01'),13)
+ assert_equal @gw.transform_dates(d('2008/01/01'), '1y', nil, d('2010/12/01')), h(12,:months,d('2008/01/01'),3)
+ end
+
+end
23 recurring_billing/test/unit/paypal_gateway_class_test.rb
@@ -0,0 +1,23 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class PaypalGatewayTest < Test::Unit::TestCase
+
+ def setup
+ cred = fixtures(:paypal)
+ assert @gw = RecurringBilling::PaypalGateway.new(cred.update({:is_test=>true}))
+ assert_equal @gw.name, 'PayPal Website Payments Pro (US)'
+ @card = credit_card()
+ end
+
+ def test_true
+ end
+
+# def test_correct_update?
+# #def correct_update?(billing_id, amount, card, payment_options, recurring_options)
+# subscr_id = 'SOMERANDOMID'
+# assert_raise StandardError do; @gw.correct_update?(subscr_id, nil, @card, nil, {:start_date => Date.today + 1}); end
+# assert_nothing_thrown do;@gw.correct_update?(subscr_id, 1, @card, nil, nil);end
+# assert_nothing_thrown do;@gw.correct_update?(subscr_id, 100, @card, nil, {});end
+# end
+
+end
35 recurring_billing/test/unit/recurring_billing_gateway_class_test.rb
@@ -0,0 +1,35 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RecurringBillingGatewayTest < Test::Unit::TestCase
+
+ def perform_generic_test(gateway)
+ credentials = fixtures(gateway)
+ assert gw = RecurringBilling::RecurringBillingGateway.get_instance(credentials)
+ end
+
+ # Checking separate_create_update_params_from_options method
+ def test_separate_create_update_params_from_options
+ cc = credit_card()
+ payment_options = {
+ :subscription_name => 'Test Subscription 1337',
+ :order => {:invoice_number => '000000'}
+ }
+ recurring_options = {
+ :start_date => Date.today + 1,
+ :end_date => Date.today + 290,
+ :interval => '1m'
+ }
+ all_options = {}.update({:card => cc}).update(payment_options).update(recurring_options)
+ separate_options = RecurringBilling::RecurringBillingGateway.separate_create_update_params_from_options(all_options)
+ assert_equal separate_options, {:amount => nil, :card => cc, :payment_options => payment_options, :recurring_options => recurring_options}
+ end
+
+ def test_paypal
+ perform_generic_test(:paypal)
+ end
+
+ def test_authorize_net
+ perform_generic_test(:authorize_net)
+ end
+
+end
17 recurring_billing/test/unit/utils_test.rb
@@ -0,0 +1,17 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class UtilsTest < Test::Unit::TestCase
+ def test_months_between
+ def d(string);return Date.parse(string);end
+
+ assert months_between(d("2008/10/01"), d("2008/10/01")), 0
+ assert months_between(d("2008/10/02"), d("2008/10/01")), 0
+ assert months_between(d("2008/11/01"), d("2008/10/02")), 0
+ assert months_between(d("2008/01/01"), d("2007/12/31")), 0
+ assert months_between(d("2008/11/01"), d("2008/10/01")), 1
+ assert months_between(d("2008/02/01"), d("2007/05/01")), 9
+ assert months_between(d("2008/02/14"), d("2007/05/01")), 9
+ assert months_between(d("2008/02/02"), d("2007/05/13")), 8
+ end
+
+end
9 sample_app/README
@@ -0,0 +1,9 @@
+== "Subscription Management" sample app
+
+#TODO#
+
+== Installation
+
+Requires Ruby on Rails 2.1+ See ../README.txt for service_merchant
+dependencies and installation instructions
+
10 sample_app/Rakefile
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
28 sample_app/app/controllers/admin_controller.rb
@@ -0,0 +1,28 @@
+class AdminController < ApplicationController
+ layout 'admin'
+
+ def initialize
+ super
+ @sm = SubscriptionManager
+ end
+
+
+ def index
+ end
+
+ def tariff_plan
+ @tariff_plan = @sm.all_tariff_plans[params[:id]]
+ @active_subscriptions = Subscription.find(:all, :conditions => ['tariff_plan_id = ? and status = ?', params[:id], 'ok'])
+ @inactive_subscriptions = Subscription.find(:all, :conditions => ['tariff_plan_id = ? and status not in (?)', params[:id], ['ok','pending']])
+ end
+
+ def problem_subscriptions
+ @subscriptions = Subscription.find(:all, :conditions => "0 < (select count(*) from subscription_profiles sp, tracker_recurring_payment_profiles tp where tp.problem_status is not null and tp.problem_status !='' and tp.id = sp.recurring_payment_profile_id and sp.subscription_id = subscriptions.id)")
+ end
+
+ def problem_subscription
+ @subscription = Subscription.find(params[:id])
+ @tariff_plan = @sm.all_tariff_plans[@subscription.tariff_plan_id]
+ end
+
+end
36 sample_app/app/controllers/application.rb
@@ -0,0 +1,36 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+
+ layout 'default'
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery # :secret => '9169b23d9e56ae529c8bf411f05601e8'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ # filter_parameter_logging :password
+
+ def render_to_pdf(options = nil)
+ pdf = PDF::HTMLDoc.new
+ pdf.set_option :bodycolor, :white
+ pdf.set_option :bodyfont, :helvetica # arial helvetica sans serif
+ pdf.set_option :footer, '.'
+ pdf.set_option :header, '...'
+ pdf.set_option :size, :universal
+ pdf.set_option :toc, false
+ pdf.set_option :portrait, true
+ pdf.set_option :links, false
+ pdf.set_option :webpage, true
+ pdf.set_option :left, '1cm'
+ pdf.set_option :right, '1cm'
+ pdf.set_option :bottom, '1cm'
+ pdf << render_to_string(options)
+ pdf.generate
+ end
+
+end
121 sample_app/app/controllers/subscription_controller.rb
@@ -0,0 +1,121 @@
+class SubscriptionController < ApplicationController
+
+ def initialize
+ super
+ # sample data, would be replaced with authentication system
+ @current_account = {:first_name => 'John', :last_name => 'Smith', :id => 'Test', :country => 'US', :state => 'CA'}
+ @sm = SubscriptionManager
+ end
+
+ def index
+ @active_subscriptions = Subscription.find(:all, :conditions => ['account_id = ? and status = ?', @current_account[:id], 'ok'])
+ @inactive_subscriptions = Subscription.find(:all, :conditions => ['account_id = ? and status not in (?)', @current_account[:id], ['ok','pending']])
+ end
+
+ def show
+ @subscription = Subscription.find(params[:id])
+ @tariff_plan = @sm.all_tariff_plans[@subscription.tariff_plan_id]
+ end
+
+ def subscribe
+ if !params[:submit].nil? && !params[:tariff_plan_id].nil?
+ subscription_options = {
+ :account_id => @current_account[:id],
+ :account_country => @current_account[:country],
+ :account_state => @current_account[:state],
+ :tariff_plan => params[:tariff_plan_id],
+ :quantity => params[:quantity].to_i,
+ :start_date => Date.parse(params[:start_date]),
+ :end_date => Date.parse(params[:end_date])
+ }
+ credit_card = ActiveMerchant::Billing::CreditCard.new({
+ :type => params[:card_type],
+ :number => params[:card_number].to_i,
+ :month => params[:card_expiration_month].to_i,
+ :year => params[:card_expiration_year].to_i,
+ :first_name => params[:card_first_name],
+ :last_name => params[:card_last_name],
+ :verification_value => params[:card_verification_value]
+ })
+ # subscribe
+ subscription_id = @sm.subscribe(subscription_options)
+
+ # this block should be different in multi-step subscription
+ begin
+ @sm.pay_for_subscription(subscription_id, credit_card, {})
+ rescue
+ Subscription.delete(subscription_id)
+ raise
+ end
+
+ redirect_to :action => nil
+ return
+ end
+ end
+
+ def unsubscribe
+ @subscription = Subscription.find(params[:id])
+ if !params[:submit].nil?
+ @sm.unsubscribe(@subscription.id)
+ redirect_to :action => nil
+ return
+ end
+ end
+
+ def update_card
+ @subscription = Subscription.find(params[:id])
+ if !params[:submit].nil?
+ credit_card = ActiveMerchant::Billing::CreditCard.new({
+ :type => params[:card_type],
+ :number => params[:card_number].to_i,
+ :month => params[:card_expiration_month].to_i,
+ :year => params[:card_expiration_year].to_i,
+ :first_name => params[:card_first_name],
+ :last_name => params[:card_last_name],
+ :verification_value => params[:card_verification_value]
+ })
+ @sm.update_subscription(@subscription.id, {:card => credit_card})
+ redirect_to :action => 'show', :id => @subscription.id
+ return
+ end
+ end
+
+ def invoice
+
+ if params[:id] == 'sample'
+ data = {
+ :billing_account => @current_account[:id],
+
+ # Subscription
+ :service_name => 'Basic (per month)',
+ :net_amount => '89.90 USD',
+ :taxes_amount => '10.00 USD',
+ :taxes_comment => 'resident', # nullable
+ :total_amount => '99.90 USD',
+
+ # Payment
+ :date => Date.today, # payment.created_at
+ :number => 9999, # payment.id
+ :transaction_gateway => 'PayPal',
+ :transaction_id => 'XYZ123456789CBA',
+ :transaction_amount => '99.90 USD'
+ }
+ else
+ data = @sm.get_invoice_data(params[:id])
+ end
+
+ # FILLED BY APPLICATION
+ data[:billing_address] = "44 Highway st., #33\nWashington, DC 99999-1111\nUSA"
+ data[:billing_name] = "%s %s" % [@current_account[:first_name], @current_account[:last_name]]
+
+ # APPLICATION preferences
+ data[:date_format] = '%Y-%m-%d' # nullable
+
+ # PDF generation
+ data[:date_format] = '%Y/%m/%d' if data[:date_format].nil?
+ data.each {|name, value| instance_variable_set('@invoice_'+name.to_s, value)}
+ #render :action => 'invoice.rpdf', :layout => false
+ send_data render_to_pdf({ :action => 'invoice.rpdf', :layout => false }), {:type => :pdf, :filename => "invoice_%s.pdf" % data[:number]}
+ end
+
+end
2 sample_app/app/helpers/admin_helper.rb
@@ -0,0 +1,2 @@
+module AdminHelper
+end
3 sample_app/app/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
2 sample_app/app/helpers/subscription_helper.rb
@@ -0,0 +1,2 @@
+module SubscriptionHelper
+end
11 sample_app/app/views/admin/index.rhtml
@@ -0,0 +1,11 @@
+<% @page_name = "Start page" %>
+
+<h3>All tarriff plans</h3>
+
+<ul>
+<% for k, tariff_plan in @sm.all_tariff_plans %>
+<li><%= tariff_plan['service']['name'] %> <%= link_to 'details', :action => :tariff_plan, :id => k %></li>
+<% end %>
+</ul>
+
+<p><%= link_to 'Problem subscriptions', :action => :problem_subscriptions %></p>
9 sample_app/app/views/admin/problem_subscription.rhtml
@@ -0,0 +1,9 @@
+<% @page_name = "Subscription with payment problems" %>
+
+<%= render :partial => 'subscription/subscription', :locals => {:subscription => @subscription, :tariff_plan => @tariff_plan} %>
+
+<%= render :partial => 'subscription/recurring_payment_profile', :locals => {:profiles => @subscription.recurring_payment_profiles} %>
+
+<p>
+<%= link_to '&larr; To problem subscriptions', :action => :problem_subscriptions %>
+</p>
7 sample_app/app/views/admin/problem_subscriptions.rhtml
@@ -0,0 +1,7 @@
+<% @page_name = "Subscriptions with payment problems" %>
+
+<ul>
+<% for subscription in @subscriptions %>
+<li><%= subscription.account_id %> - <%= subscription.tariff_plan_id %> <%= link_to 'more', :action => :problem_subscription, :id => subscription.id %></li>
+<% end %>
+</ul>
37 sample_app/app/views/admin/tariff_plan.rhtml
@@ -0,0 +1,37 @@
+<% @page_name = "Tariff plan" %>
+
+<h3>&quot;<%= @tariff_plan['service']['name'] %>&quot;</h3>
+
+<table><tbody>
+<%= render :partial => 'subscription/tariff_plan', :locals => {:tariff_plan => @tariff_plan} %>
+</td></tbody></table>
+
+<% if !@active_subscriptions.empty? %>
+<h3>Active subscriptions</h3>
+<ul>
+<% for subscription in @active_subscriptions %>
+<li>Account &quot;<%= subscription.account_id %>&quot;
+&mdash;
+from <%= subscription.starts_on %>
+<% if !subscription.ends_on.nil? %>
+to <%= subscription.ends_on %>
+<% end %>
+</li>
+<% end %>
+</ul>
+<% end %>
+
+<% if !@inactive_subscriptions.empty? %>
+<h3>Inactive subscriptions</h3>
+<ul>
+<% for subscription in @inactive_subscriptions %>
+<li>Account &quot;<%= subscription.account_id %>&quot; (<%= subscription.status %>)
+&mdash;
+from <%= subscription.starts_on %>
+<% if !subscription.ends_on.nil? %>
+to <%= subscription.ends_on %>
+<% end %>
+</li>
+<% end %>
+</ul>
+<% end %>
31 sample_app/app/views/layouts/admin.rhtml
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html>
+ <head>
+ <title><%= @page_title or @page_name or 'New page' %> : Admin : MySite.com</title>
+ <%= javascript_include_tag :defaults %>
+
+<style type='text/css'>
+HR {height: 1px; background-color: red; padding: 0; border: none;}
+TBODY TH {text-align: left;}
+
+</style>
+</head>
+
+<body>
+<h1>Admin MySite.com</h1>
+<hr />
+<h2><%= @page_name or @page_title or 'New page' %></h2>
+<div class='content'>
+<%= yield %>
+</div>
+
+<% if !current_page? :action => nil %>
+<p style='margin-top: 2em;'>&uarr; <%= link_to 'Home', :action => nil %></p>
+<% end %>
+
+<hr />
+
+MySite.com
+
+</body>
+</html>
33 sample_app/app/views/layouts/default.rhtml
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html>
+ <head>
+ <title><%= @page_title or @page_name or 'New page' %> : MySite.com</title>
+ <%= javascript_include_tag :defaults %>
+
+<style type='text/css'>
+HR {height: 1px; background-color: #CCC; padding: 0; border: none;}
+
+TBODY TH {text-align: left;}
+
+</style>
+</head>
+
+<body>
+<h1>MySite.com</h1>
+<hr />
+<h2><%= @page_name or @page_title or 'New page' %></h2>
+<div style='position: absolute; right: 0; top: 0; padding: 1em;'>Hello, <%= @current_account[:first_name] %> <%= @current_account[:last_name] %><br />from <%= @current_account[:state] %>, <%= @current_account[:country] %></div>
+<div class='content'>
+<%= yield %>
+</div>
+
+<% if !current_page? :action => nil %>
+<p style='margin-top: 2em;'>&uarr; <%= link_to 'Home', :action => nil %></p>
+<% end %>
+
+<hr />
+
+MySite.com
+
+</body>
+</html>
41 sample_app/app/views/subscription/_recurring_payment_profile.rhtml
@@ -0,0 +1,41 @@
+<table border="1" cellpadding="3">
+<tr>
+ <th rowspan="2">Reference #</th>
+ <th rowspan="2">Gateway</th>
+ <th rowspan="2">Balance</th>
+ <th colspan="4">Payments</th>
+ <th colspan="5">Billing Info</th>
+ <th rowspan="2">Status</th>
+</tr>
+<tr>
+ <th>Total</th>
+ <th>Complete</th>
+ <th>Failed</th>
+ <th>Remaining</th>
+ <th>Amount</th>
+ <th>Periodicity</th>
+ <th>Start Date</th>
+ <th>Trial Days</th>
+ <th>Credit Card</th>
+</tr>
+
+<% for profile in profiles %>
+ <td><%= profile.gateway_reference %></td>
+ <td><%= profile.gateway %></td>
+ <td><%= (profile.outstanding_balance) ? profile.outstanding_balance : "n/a" %></td>
+ <td><%= (profile.total_payments_count && profile.total_payments_count >= 0) ? profile.total_payments_count : "n/a" %></td>
+ <td><%= (profile.complete_payments_count && profile.complete_payments_count >= 0) ? profile.complete_payments_count : "n/a" %></td>
+ <td><%= (profile.failed_payments_count && profile.failed_payments_count >= 0) ? profile.failed_payments_count : "n/a" %></td>
+ <td><%= (profile.remaining_payments_count && profile.remaining_payments_count >= 0) ? profile.remaining_payments_count : "n/a" %></td>
+ <td><%= profile.money_formatted %></td>
+ <td><%= SubscriptionManagement.format_periodicity(profile.periodicity).capitalize %></td>