activemerchant 1.20.4 → 1.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data.tar.gz.sig +0 -0
  2. data/CHANGELOG +29 -0
  3. data/CONTRIBUTORS +13 -0
  4. data/lib/active_merchant/billing/credit_card_methods.rb +1 -1
  5. data/lib/active_merchant/billing/gateway.rb +1 -1
  6. data/lib/active_merchant/billing/gateways/authorize_net_cim.rb +104 -18
  7. data/lib/active_merchant/billing/gateways/beanstream.rb +29 -1
  8. data/lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb +105 -3
  9. data/lib/active_merchant/billing/gateways/braintree_orange.rb +4 -0
  10. data/lib/active_merchant/billing/gateways/certo_direct.rb +279 -0
  11. data/lib/active_merchant/billing/gateways/epay.rb +2 -2
  12. data/lib/active_merchant/billing/gateways/eway_managed.rb +1 -0
  13. data/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +1 -0
  14. data/lib/active_merchant/billing/gateways/nab_transact.rb +244 -0
  15. data/lib/active_merchant/billing/gateways/payflow.rb +10 -2
  16. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +1 -0
  17. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +1 -1
  18. data/lib/active_merchant/billing/gateways/paypal_digital_goods.rb +43 -0
  19. data/lib/active_merchant/billing/gateways/paypal_express.rb +36 -1
  20. data/lib/active_merchant/billing/gateways/paypal_express_common.rb +8 -3
  21. data/lib/active_merchant/billing/gateways/quickpay.rb +1 -0
  22. data/lib/active_merchant/billing/gateways/samurai.rb +1 -0
  23. data/lib/active_merchant/billing/gateways/secure_pay_au.rb +136 -49
  24. data/lib/active_merchant/billing/gateways/secure_pay_tech.rb +1 -1
  25. data/lib/active_merchant/billing/gateways/stripe.rb +23 -11
  26. data/lib/active_merchant/billing/gateways/verifi.rb +2 -2
  27. data/lib/active_merchant/billing/gateways/viaklix.rb +1 -1
  28. data/lib/active_merchant/billing/integrations/action_view_helper.rb +5 -1
  29. data/lib/active_merchant/billing/integrations/authorize_net_sim.rb +38 -0
  30. data/lib/active_merchant/billing/integrations/authorize_net_sim/helper.rb +228 -0
  31. data/lib/active_merchant/billing/integrations/authorize_net_sim/notification.rb +340 -0
  32. data/lib/active_merchant/billing/integrations/helper.rb +13 -1
  33. data/lib/active_merchant/billing/integrations/payflow_link/helper.rb +1 -1
  34. data/lib/active_merchant/version.rb +1 -1
  35. metadata +37 -31
  36. metadata.gz.sig +0 -0
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,5 +1,34 @@
1
1
  = ActiveMerchant CHANGELOG
2
2
 
3
+ == Version 1.21.0 (March 7, 2012)
4
+
5
+ * Stripe: Add support for passing IP [collision]
6
+ * Merchant e-Solutions: pass expiration date when purchasing with a stored credit card [chrisyoung]
7
+ * Braintree: Fix passing custom processor ids to old accounts [maxsilver]
8
+ * Authorize.net CIM: Add validation mode option to create_customer_profile_request [jwood]
9
+ * eWay Managed: Include transaction number in response params [jamsi]
10
+ * Fix various hash ordering issues exposed by Ruby 1.8 [ntalbott]
11
+ * Authorize.Net CIM: Add WEB echeck type [deathbob]
12
+ * Move Braintree from the gemspec to the Gemfile [ntalbott]
13
+ * Add CertoDirect gateway [hron]
14
+ * Authorize.Net CIM: Add option for setting a custom delimiter [bmorton]
15
+ * Authorize.Net CIM: Add 3.1 response fields [bmorton]
16
+ * Authorize.Net CIM: Misc fixes and doc improvements [bmorton]
17
+ * Authorize.Net CIM: Fix error when order is blank [KeeperPat]
18
+ * Beanstream: Add recurring payments support [castiglione]
19
+ * Make ePay password optional [ePay]
20
+ * Quickpay: skip testmode if transaction provided [brentmc79]
21
+ * Payflow: add additional fields [thorstadt]
22
+ * Authorize.Net CIM: Add get_customer_profile_ids [howaboutwe]
23
+ * PayPal Express: Add support for BrandName and Custom fields [exviva]
24
+ * Payflow: Handle dates with leading zeros [jcoleman]
25
+ * Authorize.Net CIM: Add CCV code support & improve tests [tgarnett]
26
+ * Add Authorize.Net SIM integration [courtland & rdp]
27
+ * Secure Pay AU: Handle periodic payments [tommeier]
28
+ * Viaklix: Add discover as a supported card type [waelchatila]
29
+ * Improvements to testing infrastructure for integrations [jduff]
30
+ * Add NAB Transact (AU) Gateway [tommeier]
31
+
3
32
  == Version 1.20.4 (February 22, 2012)
4
33
 
5
34
  * Fix json dependency
data/CONTRIBUTORS CHANGED
@@ -259,3 +259,16 @@ Dwolla (September, 2011)
259
259
  Samurai (November, 2011)
260
260
 
261
261
  * Joshua Krall (jkrall)
262
+
263
+ CertoDirect Gateway (February, 2012)
264
+
265
+ * Aleksei Gusev (hron)
266
+
267
+ Authorize.Net SIM Integration (February, 2012)
268
+
269
+ * Roger Pack (rdp)
270
+ * Nick Rogers (courtland)
271
+
272
+ NAB Transact (AU) Gateway (February, 2012)
273
+
274
+ * Tom Meier (tommeier)
@@ -14,7 +14,7 @@ module ActiveMerchant #:nodoc:
14
14
  'dankort' => /^5019\d{12}$/,
15
15
  'maestro' => /^(5[06-8]|6\d)\d{10,17}$/,
16
16
  'forbrugsforeningen' => /^600722\d{10}$/,
17
- 'laser' => /^(6304|6706|6771|6709)\d{8}(\d{4}|\d{6,7})?$/
17
+ 'laser' => /^(6304|6706|6709|6771(?!89))\d{8}(\d{4}|\d{6,7})?$/
18
18
  }
19
19
 
20
20
  def self.included(base)
@@ -53,7 +53,7 @@ module ActiveMerchant #:nodoc:
53
53
  #
54
54
  # == Implmenting new gateways
55
55
  #
56
- # See the {ActiveMerchant Guide to Contributing}[http://code.google.com/p/activemerchant/wiki/Contributing]
56
+ # See the {ActiveMerchant Guide to Contributing}[https://github.com/Shopify/active_merchant/wiki/Contributing]
57
57
  #
58
58
  class Gateway
59
59
  include PostsData
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module ActiveMerchant #:nodoc:
2
3
  module Billing #:nodoc:
3
4
  # ==== Customer Information Manager (CIM)
@@ -39,6 +40,7 @@ module ActiveMerchant #:nodoc:
39
40
  :create_customer_payment_profile => 'createCustomerPaymentProfile',
40
41
  :create_customer_shipping_address => 'createCustomerShippingAddress',
41
42
  :get_customer_profile => 'getCustomerProfile',
43
+ :get_customer_profile_ids => 'getCustomerProfileIds',
42
44
  :get_customer_payment_profile => 'getCustomerPaymentProfile',
43
45
  :get_customer_shipping_address => 'getCustomerShippingAddress',
44
46
  :delete_customer_profile => 'deleteCustomerProfile',
@@ -75,7 +77,8 @@ module ActiveMerchant #:nodoc:
75
77
 
76
78
  ECHECK_TYPES = {
77
79
  :ccd => 'CCD',
78
- :ppd => 'PPD'
80
+ :ppd => 'PPD',
81
+ :web => 'WEB'
79
82
  }
80
83
 
81
84
  self.homepage_url = 'http://www.authorize.net/'
@@ -93,6 +96,7 @@ module ActiveMerchant #:nodoc:
93
96
  # * <tt>:login</tt> -- The Authorize.Net API Login ID (REQUIRED)
94
97
  # * <tt>:password</tt> -- The Authorize.Net Transaction Key. (REQUIRED)
95
98
  # * <tt>:test</tt> -- +true+ or +false+. If true, perform transactions against the test server.
99
+ # * <tt>:delimiter</tt> -- The delimiter used in the direct response. Default is ',' (comma).
96
100
  # Otherwise, perform transactions against the production server.
97
101
  def initialize(options = {})
98
102
  requires!(options, :login, :password)
@@ -107,11 +111,28 @@ module ActiveMerchant #:nodoc:
107
111
  # It is *CRITICAL* that you save this ID. There is no way to retrieve this through the API. You will not
108
112
  # be able to create another Customer Profile with the same information.
109
113
  #
114
+ #
115
+ #
110
116
  # ==== Options
117
+ #
118
+ # * <tt>:profile</tt> -- A hash containing at least one of the CONDITIONAL profile options below (REQUIRED)
119
+ #
120
+ # ==== Profile
111
121
  #
112
- # TODO
122
+ # * <tt>:email</tt> -- Email address associated with the customer profile (CONDITIONAL)
123
+ # * <tt>:description</tt> -- Description of the customer or customer profile (CONDITIONAL)
124
+ # * <tt>:merchant_customer_id</tt> -- Merchant assigned ID for the customer (CONDITIONAL)
125
+ # * <tt>:payment_profile</tt> -- A hash containing the elements of the new payment profile (optional)
126
+ #
127
+ # ==== Payment Profile
128
+ #
129
+ # * <tt>:payment</tt> -- A hash containing information on payment. Either :credit_card or :bank_account (optional)
113
130
  def create_customer_profile(options)
114
- # TODO Add requires
131
+ requires!(options, :profile)
132
+ requires!(options[:profile], :email) unless options[:profile][:merchant_customer_id] || options[:profile][:description]
133
+ requires!(options[:profile], :description) unless options[:profile][:email] || options[:profile][:merchant_customer_id]
134
+ requires!(options[:profile], :merchant_customer_id) unless options[:profile][:description] || options[:profile][:email]
135
+
115
136
  request = build_request(:create_customer_profile, options)
116
137
  commit(:create_customer_profile, request)
117
138
  end
@@ -203,6 +224,11 @@ module ActiveMerchant #:nodoc:
203
224
  commit(:get_customer_profile, request)
204
225
  end
205
226
 
227
+ def get_customer_profile_ids(options = {})
228
+ request = build_request(:get_customer_profile_ids, options)
229
+ commit(:get_customer_profile_ids, request)
230
+ end
231
+
206
232
  # Retrieve a customer payment profile for an existing customer profile.
207
233
  #
208
234
  # Returns a Response whose params hash contains all the payment profile information. Sensitive information such as credit card
@@ -332,7 +358,11 @@ module ActiveMerchant #:nodoc:
332
358
  # - :type = (:void, :refund, :prior_auth_capture) (REQUIRED)
333
359
  # - :type = (:auth_only, :capture_only, :auth_capture) (NOT USED)
334
360
  #
335
- # * <tt>customer_shipping_address_id</tt> -- Payment gateway assigned ID associated with the customer shipping address (CONDITIONAL)
361
+ # * <tt>:card_code</tt> -- CVV/CCV code (OPTIONAL)
362
+ # - :type = (:void, :refund, :prior_auth_capture) (NOT USED)
363
+ # - :type = (:auth_only, :capture_only, :auth_capture) (OPTIONAL)
364
+ #
365
+ # * <tt>:customer_shipping_address_id</tt> -- Payment gateway assigned ID associated with the customer shipping address (CONDITIONAL)
336
366
  # - :type = (:void, :refund) (OPTIONAL)
337
367
  # - :type = (:auth_only, :capture_only, :auth_capture) (NOT USED)
338
368
  # - :type = (:prior_auth_capture) (OPTIONAL)
@@ -383,6 +413,10 @@ module ActiveMerchant #:nodoc:
383
413
  #
384
414
  # * <tt>:bank_routing_number_masked</tt> -- The last four gidits of the routing number to be refunded (CONDITIONAL - must be used with :bank_account_number_masked)
385
415
  # * <tt>:bank_account_number_masked</tt> -- The last four digis of the bank account number to be refunded, Ex. XXXX1234 (CONDITIONAL - must be used with :bank_routing_number_masked)
416
+ #
417
+ # * <tt>:tax</tt> - A hash containing tax information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
418
+ # * <tt>:duty</tt> - A hash containting duty information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
419
+ # * <tt>:shipping</tt> - A hash containing shipping information for the refund (OPTIONAL - <tt>:amount</tt>, <tt>:name</tt> (31 characters), <tt>:description</tt> (255 characters))
386
420
  def create_customer_profile_transaction_for_refund(options)
387
421
  requires!(options, :transaction)
388
422
  options[:transaction][:type] = :refund
@@ -424,8 +458,9 @@ module ActiveMerchant #:nodoc:
424
458
  #
425
459
  # * <tt>:customer_profile_id</tt> -- The Customer Profile ID of the customer to use in this transaction. (REQUIRED)
426
460
  # * <tt>:customer_payment_profile_id</tt> -- The Customer Payment Profile ID of the Customer Payment Profile to be verified. (REQUIRED)
427
- # * <tt>:customer_address_id</tt> -- The Customer Address ID of the Customer Shipping Address to be verified.
428
- # * <tt>:validation_mode</tt> -- <tt>:live</tt> or <tt>:test</tt> In Test Mode, only field validation is performed.
461
+ # * <tt>:customer_address_id</tt> -- The Customer Address ID of the Customer Shipping Address to be verified. (OPTIONAL)
462
+ # * <tt>:card_code</tt> -- If the payment profile is a credit card, the CCV/CVV code to validate with (OPTIONAL)
463
+ # * <tt>:validation_mode</tt> -- <tt>:live</tt> or <tt>:test</tt> In Test Mode, only field validation is performed. (REQUIRED
429
464
  # In Live Mode, a transaction is generated and submitted to the processor with the amount of $0.01. If successful, the transaction is immediately voided. (REQUIRED)
430
465
  def validate_customer_payment_profile(options)
431
466
  requires!(options, :customer_profile_id, :customer_payment_profile_id, :validation_mode)
@@ -466,6 +501,14 @@ module ActiveMerchant #:nodoc:
466
501
  def build_create_customer_profile_request(xml, options)
467
502
  add_profile(xml, options[:profile])
468
503
 
504
+ xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
505
+
506
+ if options.has_key?(:payment_profile)
507
+ xml.tag!('paymentProfile') do
508
+ add_payment_profile(xml, options[:payment_profile])
509
+ end
510
+ end
511
+
469
512
  xml.target!
470
513
  end
471
514
 
@@ -513,6 +556,10 @@ module ActiveMerchant #:nodoc:
513
556
  xml.target!
514
557
  end
515
558
 
559
+ def build_get_customer_profile_ids_request(xml, options)
560
+ xml.target!
561
+ end
562
+
516
563
  def build_get_customer_payment_profile_request(xml, options)
517
564
  xml.tag!('customerProfileId', options[:customer_profile_id])
518
565
  xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id])
@@ -564,6 +611,7 @@ module ActiveMerchant #:nodoc:
564
611
  xml.tag!('customerProfileId', options[:customer_profile_id])
565
612
  xml.tag!('customerPaymentProfileId', options[:customer_payment_profile_id])
566
613
  xml.tag!('customerShippingAddressId', options[:customer_address_id]) if options[:customer_address_id]
614
+ tag_unless_blank(xml, 'cardCode', options[:card_code])
567
615
  xml.tag!('validationMode', CIM_VALIDATION_MODES[options[:validation_mode]]) if options[:validation_mode]
568
616
 
569
617
  xml.target!
@@ -606,7 +654,7 @@ module ActiveMerchant #:nodoc:
606
654
  tag_unless_blank(xml,'customerShippingAddressId', transaction[:customer_shipping_address_id])
607
655
  xml.tag!('transId', transaction[:trans_id])
608
656
  when :refund
609
- #TODO - add support for all the other options fields
657
+ #TODO - add lineItems and extraOptions fields
610
658
  xml.tag!('amount', transaction[:amount])
611
659
  tag_unless_blank(xml, 'customerProfileId', transaction[:customer_profile_id])
612
660
  tag_unless_blank(xml, 'customerPaymentProfileId', transaction[:customer_payment_profile_id])
@@ -615,6 +663,9 @@ module ActiveMerchant #:nodoc:
615
663
  tag_unless_blank(xml, 'bankRoutingNumberMasked', transaction[:bank_routing_number_masked])
616
664
  tag_unless_blank(xml, 'bankAccountNumberMasked', transaction[:bank_account_number_masked])
617
665
  xml.tag!('transId', transaction[:trans_id])
666
+ add_tax(xml, transaction[:tax]) if transaction[:tax]
667
+ add_duty(xml, transaction[:duty]) if transaction[:duty]
668
+ add_shipping(xml, transaction[:shipping]) if transaction[:shipping]
618
669
  when :prior_auth_capture
619
670
  xml.tag!('amount', transaction[:amount])
620
671
  xml.tag!('transId', transaction[:trans_id])
@@ -623,12 +674,37 @@ module ActiveMerchant #:nodoc:
623
674
  xml.tag!('customerProfileId', transaction[:customer_profile_id])
624
675
  xml.tag!('customerPaymentProfileId', transaction[:customer_payment_profile_id])
625
676
  xml.tag!('approvalCode', transaction[:approval_code]) if transaction[:type] == :capture_only
677
+ tag_unless_blank(xml, 'cardCode', transaction[:card_code])
626
678
  end
627
- add_order(xml, transaction[:order]) if transaction[:order]
679
+ add_order(xml, transaction[:order]) if transaction[:order].present?
628
680
  end
629
681
  end
630
682
  end
683
+
684
+ def add_tax(xml, tax)
685
+ xml.tag!('tax') do
686
+ xml.tag!('amount', tax[:amount]) if tax[:amount]
687
+ xml.tag!('name', tax[:name]) if tax[:name]
688
+ xml.tag!('description', tax[:description]) if tax[:description]
689
+ end
690
+ end
691
+
692
+ def add_duty(xml, duty)
693
+ xml.tag!('duty') do
694
+ xml.tag!('amount', duty[:amount]) if duty[:amount]
695
+ xml.tag!('name', duty[:name]) if duty[:name]
696
+ xml.tag!('description', duty[:description]) if duty[:description]
697
+ end
698
+ end
631
699
 
700
+ def add_shipping(xml, shipping)
701
+ xml.tag!('shipping') do
702
+ xml.tag!('amount', shipping[:amount]) if shipping[:amount]
703
+ xml.tag!('name', shipping[:name]) if shipping[:name]
704
+ xml.tag!('description', shipping[:description]) if shipping[:description]
705
+ end
706
+ end
707
+
632
708
  def add_order(xml, order)
633
709
  xml.tag!('order') do
634
710
  xml.tag!('invoiceNumber', order[:invoice_number]) if order[:invoice_number]
@@ -702,6 +778,10 @@ module ActiveMerchant #:nodoc:
702
778
  xml.tag!('cardNumber', credit_card.number)
703
779
  # The expiration date of the credit card used for the subscription
704
780
  xml.tag!('expirationDate', expdate(credit_card))
781
+ # Note that Authorize.net does not save CVV codes as part of the
782
+ # payment profile. Any transactions/validations after the payment
783
+ # profile is created that wish to use CVV verification must pass
784
+ # the CVV code to authorize.net again.
705
785
  xml.tag!('cardCode', credit_card.verification_value) if credit_card.verification_value?
706
786
  end
707
787
  end
@@ -756,24 +836,23 @@ module ActiveMerchant #:nodoc:
756
836
  message = response_params['messages']['message']['text']
757
837
  test_mode = test? || message =~ /Test Mode/
758
838
  success = response_params['messages']['result_code'] == 'Ok'
839
+ response_params['direct_response'] = parse_direct_response(response_params['direct_response']) if response_params['direct_response']
840
+ transaction_id = response_params['direct_response']['transaction_id'] if response_params['direct_response']
759
841
 
760
- response = Response.new(success, message, response_params,
842
+ Response.new(success, message, response_params,
761
843
  :test => test_mode,
762
- :authorization => response_params['customer_profile_id'] || (response_params['profile'] ? response_params['profile']['customer_profile_id'] : nil)
844
+ :authorization => transaction_id || response_params['customer_profile_id'] || (response_params['profile'] ? response_params['profile']['customer_profile_id'] : nil)
763
845
  )
764
-
765
- response.params['direct_response'] = parse_direct_response(response) if response.params['direct_response']
766
- response
767
846
  end
768
847
 
769
848
  def tag_unless_blank(xml, tag_name, data)
770
849
  xml.tag!(tag_name, data) unless data.blank? || data.nil?
771
850
  end
772
851
 
773
- def parse_direct_response(response)
774
- direct_response = {'raw' => response.params['direct_response']}
775
- direct_response_fields = response.params['direct_response'].split(',')
776
-
852
+ def parse_direct_response(params)
853
+ delimiter = @options[:delimiter] || ','
854
+ direct_response = {'raw' => params}
855
+ direct_response_fields = params.split(delimiter)
777
856
  direct_response.merge(
778
857
  {
779
858
  'response_code' => direct_response_fields[0],
@@ -815,7 +894,14 @@ module ActiveMerchant #:nodoc:
815
894
  'purchase_order_number' => direct_response_fields[36],
816
895
  'md5_hash' => direct_response_fields[37],
817
896
  'card_code' => direct_response_fields[38],
818
- 'cardholder_authentication_verification_response' => direct_response_fields[39]
897
+ 'cardholder_authentication_verification_response' => direct_response_fields[39],
898
+ # The following direct response fields are only available in version 3.1 of the
899
+ # transaction response. Check your merchant account settings for details.
900
+ 'account_number' => direct_response_fields[50] || '',
901
+ 'card_type' => direct_response_fields[51] || '',
902
+ 'split_tender_id' => direct_response_fields[52] || '',
903
+ 'requested_amount' => direct_response_fields[53] || '',
904
+ 'balance_on_card' => direct_response_fields[54] || '',
819
905
  }
820
906
  )
821
907
  end
@@ -19,7 +19,6 @@ module ActiveMerchant #:nodoc:
19
19
  # To learn more about storing credit cards with the Beanstream gateway, please read the BEAN_Payment_Profiles.pdf (I had to phone BeanStream to request it.)
20
20
  #
21
21
  # == Notes
22
- # * Recurring billing is not yet implemented.
23
22
  # * Adding of order products information is not implemented.
24
23
  # * Ensure that country and province data is provided as a code such as "CA", "US", "QC".
25
24
  # * login is the Beanstream merchant ID, username and password should be enabled in your Beanstream account and passed in using the <tt>:user</tt> and <tt>:password</tt> options.
@@ -95,6 +94,35 @@ module ActiveMerchant #:nodoc:
95
94
  commit(post)
96
95
  end
97
96
 
97
+ def recurring(money, source, options = {})
98
+ post = {}
99
+ add_amount(post, money)
100
+ add_invoice(post, options)
101
+ add_credit_card(post, source)
102
+ add_address(post, options)
103
+ add_transaction_type(post, purchase_action(source))
104
+ add_recurring_type(post, options)
105
+ commit(post)
106
+ end
107
+
108
+ def update_recurring(amount, source, options = {})
109
+ post = {}
110
+ add_recurring_amount(post, amount)
111
+ add_recurring_invoice(post, options)
112
+ add_credit_card(post, source)
113
+ add_address(post, options)
114
+ add_recurring_operation_type(post, :update)
115
+ add_recurring_service(post, options)
116
+ recurring_commit(post)
117
+ end
118
+
119
+ def cancel_recurring(options = {})
120
+ post = {}
121
+ add_recurring_operation_type(post, :cancel)
122
+ add_recurring_service(post, options)
123
+ recurring_commit(post)
124
+ end
125
+
98
126
  def interac
99
127
  @interac ||= BeanstreamInteracGateway.new(@options)
100
128
  end
@@ -2,6 +2,7 @@ module ActiveMerchant #:nodoc:
2
2
  module Billing #:nodoc:
3
3
  module BeanstreamCore
4
4
  URL = 'https://www.beanstream.com/scripts/process_transaction.asp'
5
+ RECURRING_URL = 'https://www.beanstream.com/scripts/recurring_billing.asp'
5
6
  SECURE_PROFILE_URL = 'https://www.beanstream.com/scripts/payment_profile.asp'
6
7
  SP_SERVICE_VERSION = '1.1'
7
8
 
@@ -37,6 +38,27 @@ module ActiveMerchant #:nodoc:
37
38
  '9' => 'I'
38
39
  }
39
40
 
41
+ PERIODS = {
42
+ :days => 'D',
43
+ :weeks => 'W',
44
+ :months => 'M',
45
+ :years => 'Y'
46
+ }
47
+
48
+ PERIODICITIES = {
49
+ :daily => [:days, 1],
50
+ :weekly => [:weeks, 1],
51
+ :biweekly => [:weeks, 2],
52
+ :monthly => [:months, 1],
53
+ :bimonthly => [:months, 2],
54
+ :yearly => [:years, 1]
55
+ }
56
+
57
+ RECURRING_OPERATION = {
58
+ :update => 'M',
59
+ :cancel => 'C'
60
+ }
61
+
40
62
  def self.included(base)
41
63
  base.default_currency = 'CAD'
42
64
 
@@ -207,11 +229,66 @@ module ActiveMerchant #:nodoc:
207
229
  post[:status] = options[:status]
208
230
  end
209
231
 
232
+ def add_recurring_amount(post, money)
233
+ post[:amount] = amount(money)
234
+ end
235
+
236
+ def add_recurring_invoice(post, options)
237
+ post[:rbApplyTax1] = options[:apply_tax1]
238
+ end
239
+
240
+ def add_recurring_operation_type(post, operation)
241
+ post[:operationType] = RECURRING_OPERATION[operation]
242
+ end
243
+
244
+ def add_recurring_service(post, options)
245
+ post[:serviceVersion] = '1.0'
246
+ post[:merchantId] = @options[:login]
247
+ post[:passCode] = @options[:recurring_api_key]
248
+ post[:rbAccountId] = options[:account_id]
249
+ end
250
+
251
+ def add_recurring_type(post, options)
252
+ # XXX requires!
253
+ post[:trnRecurring] = 1
254
+ period, increment = interval(options)
255
+ post[:rbBillingPeriod] = PERIODS[period]
256
+ post[:rbBillingIncrement] = increment
257
+
258
+ if options.include? :start_date
259
+ post[:rbCharge] = 0
260
+ post[:rbFirstBilling] = options[:start_date].strftime('%m%d%Y')
261
+ end
262
+
263
+ if count = options[:occurrences] || options[:payments]
264
+ post[:rbExpiry] = (options[:start_date] || Date.current).advance(period => count).strftime('%m%d%Y')
265
+ end
266
+ end
267
+
268
+ def interval(options)
269
+ if options.include? :periodicity
270
+ requires!(options, [:periodicity, *PERIODICITIES.keys])
271
+ PERIODICITIES[options[:periodicity]]
272
+ elsif options.include? :interval
273
+ interval = options[:interval]
274
+ if interval.respond_to? :parts
275
+ parts = interval.parts
276
+ raise ArgumentError.new("Cannot recur with mixed interval (#{interval}). Use only one of: days, weeks, months or years") if parts.length > 1
277
+ parts.first
278
+ elsif interval.kind_of? Hash
279
+ requires!(interval, :unit)
280
+ unit, length = interval.values_at(:unit, :length)
281
+ length ||= 1
282
+ [unit, length]
283
+ end
284
+ end
285
+ end
286
+
210
287
  def parse(body)
211
288
  results = {}
212
289
  if !body.nil?
213
290
  body.split(/&/).each do |pair|
214
- key,val = pair.split(/=/)
291
+ key, val = pair.split(/=/)
215
292
  results[key.to_sym] = val.nil? ? nil : CGI.unescape(val)
216
293
  end
217
294
  end
@@ -225,11 +302,22 @@ module ActiveMerchant #:nodoc:
225
302
 
226
303
  results
227
304
  end
228
-
305
+
306
+ def recurring_parse(data)
307
+ REXML::Document.new(data).root.elements.to_a.inject({}) do |response, element|
308
+ response[element.name.to_sym] = element.text
309
+ response
310
+ end
311
+ end
312
+
229
313
  def commit(params, use_profile_api = false)
230
314
  post(post_data(params,use_profile_api),use_profile_api)
231
315
  end
232
316
 
317
+ def recurring_commit(params)
318
+ recurring_post(post_data(params, false))
319
+ end
320
+
233
321
  def post(data, use_profile_api=nil)
234
322
  response = parse(ssl_post((use_profile_api ? SECURE_PROFILE_URL : URL), data))
235
323
  response[:customer_vault_id] = response[:customerCode] if response[:customerCode]
@@ -240,7 +328,12 @@ module ActiveMerchant #:nodoc:
240
328
  :avs_result => { :code => (AVS_CODES.include? response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] }
241
329
  )
242
330
  end
243
-
331
+
332
+ def recurring_post(data)
333
+ response = recurring_parse(ssl_post(RECURRING_URL, data))
334
+ build_response(recurring_success?(response), recurring_message_from(response), response)
335
+ end
336
+
244
337
  def authorization_from(response)
245
338
  "#{response[:trnId]};#{response[:trnAmount]};#{response[:trnType]}"
246
339
  end
@@ -249,10 +342,18 @@ module ActiveMerchant #:nodoc:
249
342
  response[:messageText] || response[:responseMessage]
250
343
  end
251
344
 
345
+ def recurring_message_from(response)
346
+ response[:message]
347
+ end
348
+
252
349
  def success?(response)
253
350
  response[:responseType] == 'R' || response[:trnApproved] == '1' || response[:responseCode] == '1'
254
351
  end
255
352
 
353
+ def recurring_success?(response)
354
+ response[:code] == '1'
355
+ end
356
+
256
357
  def add_source(post, source)
257
358
  if source.is_a?(String) or source.is_a?(Integer)
258
359
  post[:customerCode] = source
@@ -280,6 +381,7 @@ module ActiveMerchant #:nodoc:
280
381
 
281
382
  params.reject{|k, v| v.blank?}.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
282
383
  end
384
+
283
385
  end
284
386
  end
285
387
  end