activemerchant 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. data.tar.gz.sig +0 -0
  2. data/CHANGELOG +18 -0
  3. data/lib/active_merchant.rb +2 -0
  4. data/lib/active_merchant/billing/credit_card.rb +25 -113
  5. data/lib/active_merchant/billing/credit_card_formatting.rb +18 -0
  6. data/lib/active_merchant/billing/credit_card_methods.rb +83 -0
  7. data/lib/active_merchant/billing/gateway.rb +1 -0
  8. data/lib/active_merchant/billing/gateways.rb +2 -1
  9. data/lib/active_merchant/billing/gateways/data_cash.rb +475 -0
  10. data/lib/active_merchant/billing/gateways/linkpoint.rb +9 -24
  11. data/lib/active_merchant/billing/gateways/moneris.rb +1 -1
  12. data/lib/active_merchant/billing/gateways/payflow.rb +14 -0
  13. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +1 -14
  14. data/lib/active_merchant/billing/gateways/paypal.rb +11 -3
  15. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +47 -22
  16. data/lib/active_merchant/billing/gateways/paypal_express.rb +3 -3
  17. data/lib/active_merchant/billing/gateways/psigate.rb +4 -19
  18. data/lib/active_merchant/billing/gateways/usa_epay.rb +5 -6
  19. data/lib/active_merchant/billing/integrations.rb +1 -0
  20. data/lib/active_merchant/billing/integrations/helper.rb +13 -6
  21. data/lib/active_merchant/billing/integrations/notification.rb +2 -1
  22. data/lib/active_merchant/billing/integrations/paypal/helper.rb +2 -0
  23. data/lib/active_merchant/billing/integrations/two_checkout.rb +18 -0
  24. data/lib/active_merchant/billing/integrations/two_checkout/helper.rb +59 -0
  25. data/lib/active_merchant/billing/integrations/two_checkout/notification.rb +114 -0
  26. data/lib/active_merchant/lib/posts_data.rb +18 -9
  27. data/lib/active_merchant/lib/validateable.rb +2 -2
  28. data/lib/certs/cacert.pem +7815 -0
  29. metadata +11 -2
  30. metadata.gz.sig +0 -0
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,5 +1,23 @@
1
1
  # CHANGELOG
2
2
  # ---
3
+ # * Mark 1.1.0 release
4
+ #
5
+ # * Add unique_id option to PayPal mass payments [Haig]
6
+ #
7
+ # * Fix expiry date in USA ePay [cody]
8
+ #
9
+ # * Fix PayPal Payments Pro UK with Switch & Solo cards [cody]
10
+ #
11
+ # * Add reauthorization to PaypalGateway and PaypalExpressGateway [dorrenchen]
12
+ #
13
+ # * Update DataCash tests and format merchant reference number to meet DataCash's requirements [MoneySpyder, cody]
14
+ #
15
+ # * Add Datacash gateway [MoneySpyder, cody]
16
+ #
17
+ # * VERIFY_PEER on all SSL requests [cody]
18
+ #
19
+ # * Add support for 2Checkout [cody]
20
+ #
3
21
  # * Add support for PayPal mass payments to the PaypalGateway and the PaypalExpressGateway [Brandon Keepers]
4
22
  #
5
23
  # * Add a credit method to Authorize.net [cody]
@@ -48,6 +48,8 @@ require 'active_merchant/lib/requires_parameters'
48
48
  require 'active_merchant/lib/country'
49
49
 
50
50
  # CreditCard Utility class.
51
+ require 'active_merchant/billing/credit_card_methods'
52
+ require 'active_merchant/billing/credit_card_formatting'
51
53
  require 'active_merchant/billing/credit_card'
52
54
 
53
55
  require 'active_merchant/billing/base'
@@ -1,13 +1,13 @@
1
1
  require 'time'
2
- require 'delegate'
3
2
  require 'date'
4
3
 
5
4
  module ActiveMerchant #:nodoc:
6
5
  module Billing #:nodoc:
7
-
8
6
  # This credit card object can be used as a stand alone object. It acts just like a active record object
9
7
  # but doesn't support the .save method as its not backed by a database.
10
8
  class CreditCard
9
+ include CreditCardMethods
10
+
11
11
  cattr_accessor :require_verification_value
12
12
  self.require_verification_value = false
13
13
 
@@ -17,47 +17,11 @@ module ActiveMerchant #:nodoc:
17
17
 
18
18
  include Validateable
19
19
 
20
- class ExpiryMonth < DelegateClass(Fixnum)#:nodoc:
21
- def to_s(format = :default) #:nodoc:
22
- case format
23
- when :default
24
- __getobj__.to_s
25
- when :two_digit
26
- sprintf("%.2i", self)[-2..-1]
27
- else
28
- super
29
- end
30
- end
31
-
32
- def valid? #:nodoc:
33
- (1..12).include?(self)
34
- end
35
- end
36
-
37
- class ExpiryYear < DelegateClass(Fixnum)#:nodoc:
38
- def to_s(format = :default) #:nodoc:
39
- case format
40
- when :default
41
- __getobj__.to_s
42
- when :two_digit
43
- sprintf("%.2i", self)[-2..-1]
44
- when :four_digit
45
- sprintf("%.4i", self)
46
- else
47
- super
48
- end
49
- end
50
-
51
- def valid? #:nodoc:
52
- (Time.now.year..Time.now.year + 20).include?(self)
53
- end
54
- end
55
-
56
20
  class ExpiryDate #:nodoc:
57
21
  attr_reader :month, :year
58
22
  def initialize(month, year)
59
- @month = ExpiryMonth.new(month)
60
- @year = ExpiryYear.new(year)
23
+ @month = month
24
+ @year = year
61
25
  end
62
26
 
63
27
  def expired? #:nodoc:
@@ -78,6 +42,9 @@ module ActiveMerchant #:nodoc:
78
42
 
79
43
  # required
80
44
  attr_accessor :number, :month, :year, :type, :first_name, :last_name
45
+
46
+ # required for Switch / Solo
47
+ attr_accessor :start_month, :start_year, :issue_number
81
48
 
82
49
  # Optional verification_value (CVV, CVV2 etc)
83
50
  #
@@ -93,23 +60,31 @@ module ActiveMerchant #:nodoc:
93
60
  end
94
61
 
95
62
  def validate
96
- @errors.add "year", "expired" if expired?
97
-
98
- @errors.add "first_name", "cannot be empty" if @first_name.blank?
99
- @errors.add "last_name", "cannot be empty" if @last_name.blank?
100
- @errors.add "month", "cannot be empty" unless month.valid?
101
- @errors.add "year", "cannot be empty" unless year.valid?
63
+ errors.add "year", "expired" if expired?
64
+
65
+ errors.add "first_name", "cannot be empty" if @first_name.blank?
66
+ errors.add "last_name", "cannot be empty" if @last_name.blank?
67
+ errors.add "month", "cannot be empty" unless valid_month?(@month)
68
+ errors.add "year", "cannot be empty" unless valid_expiry_year?(@year)
102
69
 
103
70
  # Bogus card is pretty much for testing purposes. Lets just skip these extra tests if its used
104
71
 
105
72
  return if type == 'bogus'
106
73
 
107
- @errors.add "number", "is not a valid credit card number" unless CreditCard.valid_number?(number)
108
- @errors.add "type", "is invalid" unless CreditCard.card_companies.keys.include?(type)
109
- @errors.add "type", "is not the correct card type" unless CreditCard.type?(number) == type
74
+ errors.add "number", "is not a valid credit card number" unless CreditCard.valid_number?(number)
75
+ errors.add "type", "is invalid" unless CreditCard.card_companies.keys.include?(type)
76
+ errors.add "type", "is not the correct card type" unless CreditCard.type?(number) == type
110
77
 
111
78
  if CreditCard.requires_verification_value?
112
- @errors.add "verification_value", "is required" unless verification_value?
79
+ errors.add "verification_value", "is required" unless verification_value?
80
+ end
81
+
82
+ if [ 'switch', 'solo' ].include?(type)
83
+ unless valid_month?(@start_month) && valid_start_year?(@start_year) || valid_issue_number?(@issue_number)
84
+ errors.add "start_month", "is invalid" unless valid_month?(@start_month)
85
+ errors.add "start_year", "is invalid" unless valid_start_year?(@start_year)
86
+ errors.add "issue_number", "cannot be empty" unless valid_issue_number?(@issue_number)
87
+ end
113
88
  end
114
89
  end
115
90
 
@@ -137,61 +112,6 @@ module ActiveMerchant #:nodoc:
137
112
  !@verification_value.blank?
138
113
  end
139
114
 
140
- # Regular expressions for the known card companies
141
- # == Known card types
142
- # Card Type Prefix Length
143
- # --------------------------------------------------------------------------
144
- # master 51-55 16
145
- # visa 4 13, 16
146
- # american_express 34, 37 15
147
- # diners_club 300-305, 36, 38 14
148
- # discover 6011 16
149
- # jcb 3 16
150
- # jcb 2131, 1800 15
151
- # switch various 16,18,19
152
- # solo 63, 6767 16,18,19
153
- def self.card_companies
154
- {
155
- 'visa' => /^4\d{12}(\d{3})?$/,
156
- 'master' => /^5[1-5]\d{14}$/,
157
- 'discover' => /^6011\d{12}$/,
158
- 'american_express' => /^3[47]\d{13}$/,
159
- 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
160
- 'jcb' => /^(3\d{4}|2131|1800)\d{11}$/,
161
- 'switch' => [/^49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\d{10}(\d{2,3})?$/, /^564182\d{10}(\d{2,3})?$/, /^6(3(33[0-4][0-9])|759[0-9]{2})\d{10}(\d{2,3})?$/],
162
- 'solo' => /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/
163
- }
164
- end
165
-
166
- # Returns a string containing the type of card from the list of known information below.
167
- def self.type?(number)
168
- return 'visa' if Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s)
169
-
170
- card_companies.each do |company, patterns|
171
- return company if [patterns].flatten.any? { |pattern| number =~ pattern }
172
- end
173
-
174
- return nil
175
- end
176
-
177
- # Returns true if it validates. Optionally, you can pass a card type as an argument and make sure it is of the correct type.
178
- # == References
179
- # - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
180
- # - http://www.beachnet.com/~hstiles/cardtype.html
181
- def self.valid_number?(number)
182
- return true if Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s)
183
-
184
- return false unless number.to_s.length >= 13
185
-
186
- sum = 0
187
- for i in 0..number.length
188
- weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
189
- sum += (weight < 10) ? weight : weight - 9
190
- end
191
-
192
- (number[-1,1].to_i == (10 - sum % 10) % 10)
193
- end
194
-
195
115
  # Show the card number, with all but last 4 numbers replace with "X". (XXXX-XXXX-XXXX-4338)
196
116
  def display_number
197
117
  "XXXX-XXXX-XXXX-#{last_digits}"
@@ -201,14 +121,6 @@ module ActiveMerchant #:nodoc:
201
121
  number.nil? ? "" : number.last(4)
202
122
  end
203
123
 
204
- def month
205
- expiry_date.month
206
- end
207
-
208
- def year
209
- expiry_date.year
210
- end
211
-
212
124
  def expiry_date
213
125
  ExpiryDate.new(@month, @year)
214
126
  end
@@ -0,0 +1,18 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ module CreditCardFormatting
4
+ def format(number, format)
5
+ return '' if number.blank?
6
+
7
+ case format
8
+ when :two_digits
9
+ sprintf("%.2i", number)[-2..-1]
10
+ when :four_digits
11
+ sprintf("%.4i", number)[-4..-1]
12
+ else
13
+ number
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,83 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ # Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object.
4
+ module CreditCardMethods
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ def valid_month?(month)
10
+ (1..12).include?(month)
11
+ end
12
+
13
+ def valid_expiry_year?(year)
14
+ (Time.now.year..Time.now.year + 20).include?(year)
15
+ end
16
+
17
+ def valid_start_year?(year)
18
+ year.to_s =~ /^\d{4}$/ && year.to_i > 1987
19
+ end
20
+
21
+ def valid_issue_number?(number)
22
+ number.to_s =~ /^\d{1,2}$/
23
+ end
24
+
25
+ module ClassMethods
26
+ # Returns true if it validates. Optionally, you can pass a card type as an argument and make sure it is of the correct type.
27
+ # == References
28
+ # - http://perl.about.com/compute/perl/library/nosearch/P073000.htm
29
+ # - http://www.beachnet.com/~hstiles/cardtype.html
30
+ def valid_number?(number)
31
+ return true if ActiveMerchant::Billing::Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s)
32
+
33
+ return false unless number.to_s.length >= 13
34
+
35
+ sum = 0
36
+ for i in 0..number.length
37
+ weight = number[-1 * (i + 2), 1].to_i * (2 - (i % 2))
38
+ sum += (weight < 10) ? weight : weight - 9
39
+ end
40
+
41
+ (number[-1,1].to_i == (10 - sum % 10) % 10)
42
+ end
43
+
44
+ # Regular expressions for the known card companies
45
+ # == Known card types
46
+ # Card Type Prefix Length
47
+ # --------------------------------------------------------------------------
48
+ # master 51-55 16
49
+ # visa 4 13, 16
50
+ # american_express 34, 37 15
51
+ # diners_club 300-305, 36, 38 14
52
+ # discover 6011 16
53
+ # jcb 3 16
54
+ # jcb 2131, 1800 15
55
+ # switch various 16,18,19
56
+ # solo 63, 6767 16,18,19
57
+ def card_companies
58
+ {
59
+ 'visa' => /^4\d{12}(\d{3})?$/,
60
+ 'master' => /^5[1-5]\d{14}$/,
61
+ 'discover' => /^6011\d{12}$/,
62
+ 'american_express' => /^3[47]\d{13}$/,
63
+ 'diners_club' => /^3(0[0-5]|[68]\d)\d{11}$/,
64
+ 'jcb' => /^(3\d{4}|2131|1800)\d{11}$/,
65
+ 'switch' => [/^49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})\d{10}(\d{2,3})?$/, /^564182\d{10}(\d{2,3})?$/, /^6(3(33[0-4][0-9])|759[0-9]{2})\d{10}(\d{2,3})?$/],
66
+ 'solo' => /^6(3(34[5-9][0-9])|767[0-9]{2})\d{10}(\d{2,3})?$/
67
+ }
68
+ end
69
+
70
+ # Returns a string containing the type of card from the list of known information below.
71
+ def type?(number)
72
+ return 'visa' if ActiveMerchant::Billing::Base.gateway_mode == :test and ['1','2','3','success','failure','error'].include?(number.to_s)
73
+
74
+ card_companies.each do |company, patterns|
75
+ return company if [patterns].flatten.any? { |pattern| number =~ pattern }
76
+ end
77
+
78
+ return nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -17,6 +17,7 @@ module ActiveMerchant #:nodoc:
17
17
  class Gateway
18
18
  include PostsData
19
19
  include RequiresParameters
20
+ include CreditCardFormatting
20
21
 
21
22
  # The format of the amounts used by the gateway
22
23
  # :dollars => '12.50'
@@ -12,4 +12,5 @@ require 'active_merchant/billing/gateways/usa_epay'
12
12
  require 'active_merchant/billing/gateways/payflow'
13
13
  require 'active_merchant/billing/gateways/payflow_express'
14
14
  require 'active_merchant/billing/gateways/payflow_uk'
15
- require 'active_merchant/billing/gateways/payflow_express_uk'
15
+ require 'active_merchant/billing/gateways/payflow_express_uk'
16
+ require 'active_merchant/billing/gateways/data_cash'
@@ -0,0 +1,475 @@
1
+ # Author:: MoneySpyder, http://moneyspyder.co.uk
2
+
3
+ module ActiveMerchant
4
+ module Billing
5
+ # ActiveMerchant Datacash Gateway
6
+ #
7
+ # Datacash allows a policy for CV2 checks. There is currently no way
8
+ # to modify this programatically. The policy may be changed in the
9
+ # add_credit_card method.
10
+ #
11
+ class DataCashGateway < Gateway
12
+
13
+ # Datacash server URLs
14
+ TEST_URL = 'https://testserver.datacash.com/Transaction'
15
+ LIVE_URL = 'https://mars.transaction.datacash.com/Transaction'
16
+
17
+ # Different Card Transaction Types
18
+ AUTH_TYPE = 'auth'
19
+ CANCEL_TYPE = 'cancel'
20
+ FULFILL_TYPE = 'fulfill'
21
+ PRE_TYPE = 'pre'
22
+
23
+ # Constant strings for use in the ExtendedPolicy complex element for
24
+ # CV2 checks
25
+ POLICY_ACCEPT = 'accept'
26
+ POLICY_REJECT = 'reject'
27
+
28
+ #Datacash success code
29
+ DATACASH_SUCCESS = '1'
30
+
31
+ # Class attributes
32
+ attr_reader :url
33
+ attr_reader :response
34
+ attr_reader :options
35
+
36
+ # Create a new DataCashGateway
37
+ #
38
+ # The gateway requires that a valid :login and :password be passed
39
+ # in the options hash
40
+ #
41
+ # Parameters:
42
+ # -options:
43
+ # :login - the Datacash account login
44
+ # :password - the Datacash account password
45
+ # :test - boolean, use the test or live Datacash url
46
+ #
47
+ def initialize(options = {})
48
+ requires!(options, :login, :password)
49
+ @options = options
50
+ super
51
+ end
52
+
53
+ # Purchase the item straight away
54
+ #
55
+ # Parameters:
56
+ # -money: Money object for the total to be charged
57
+ # -credit_card: ActiveMerchant::Billing::CreditCard details for the transaction
58
+ # -options:
59
+ #
60
+ # Returns:
61
+ # -ActiveRecord::Billing::Response object
62
+ #
63
+ def purchase(money, credit_card, options = {})
64
+ if result = test_result_from_cc_number(credit_card.number)
65
+ return result
66
+ end
67
+
68
+ request = build_purchase_or_authorization_request(AUTH_TYPE, money, credit_card, options)
69
+
70
+ commit(request)
71
+ end
72
+
73
+ # Authorize the transaction
74
+ #
75
+ # Reserves the funds on the customer's credit card, but does not
76
+ # charge the card.
77
+ #
78
+ # Parameters:
79
+ # -money: Money object for the total to be charged
80
+ # -credit_card: ActiveMerchant::Billing::CreditCard details for the transaction
81
+ # -options:
82
+ #
83
+ # Returns:
84
+ # -ActiveRecord::Billing::Response object
85
+ #
86
+ def authorize(money, credit_card, options = {})
87
+ if result = test_result_from_cc_number(credit_card.number)
88
+ return result
89
+ end
90
+
91
+ request = build_purchase_or_authorization_request(PRE_TYPE, money, credit_card, options)
92
+
93
+ commit(request)
94
+ end
95
+
96
+ # Datacash requires both the reference and the authcode of the original
97
+ # authorization. To maintain the same interface as the other
98
+ # gateways the two numbers are concatenated together with an ; separator as
99
+ # the authorization number returned by authorization
100
+
101
+ # Captures the funds from an authorized transaction.
102
+ # authorization must be a valid Datacash reference and :authcode must be
103
+ # a valid Datacash authcode from a prior authorized transaction.
104
+ #
105
+ # This needs to create a 'historic txn' to fulfill
106
+ #
107
+ # Parameters:
108
+ # -money: Money object for the total to be charged
109
+ # -authorization: the Datacash reference and authcode from the previous authorization
110
+ #
111
+ # Returns:
112
+ # -ActiveRecord::Billing::Response object
113
+ #
114
+ def capture(money, authorization, options = {})
115
+ request = build_void_or_capture_request(FULFILL_TYPE, money, authorization, options)
116
+
117
+ commit(request)
118
+ end
119
+
120
+ # Void a previous transaction
121
+ #
122
+ # This needs to create a 'historic txn' to fulfil
123
+ #
124
+ # Parameters:
125
+ # -authorization: the Datacash reference from the previous authorization
126
+ #
127
+ # Returns:
128
+ # -ActiveRecord::Billing::Response object
129
+ #
130
+ def void(authorization, options = {})
131
+ request = build_void_or_capture_request(CANCEL_TYPE, nil, authorization, options)
132
+
133
+ commit(request)
134
+ end
135
+
136
+ # From the DataCash docs; Page 13, the following cards are
137
+ # usable
138
+ #
139
+ # American Express, ATM, Carte Blanche, Diners Club, Discover,
140
+ # EnRoute, GE Capital, JCB, Laser, Maestro, Mastercard, Solo,
141
+ # Switch, Unknown, Visa, Visa Delta, VISA Electron, Visa Purchasing
142
+ #
143
+ # Parameters:
144
+ # -none
145
+ #
146
+ # Returns:
147
+ # -the list of all supported cards
148
+ #
149
+ def self.supported_cardtypes
150
+ [ :visa, :master, :american_express, :discover, :diners_club, :jcb,
151
+ :switch, :solo ]
152
+ end
153
+
154
+ # Return whether or not the gateway is in test mode
155
+ #
156
+ # Parameters:
157
+ # -none
158
+ #
159
+ # Returns:
160
+ # -boolean
161
+ #
162
+ def test?
163
+ @options[:test] || Base.gateway_mode == :test
164
+ end
165
+
166
+ private
167
+ # Create the xml document for a 'cancel' or 'fulfill' transaction.
168
+ #
169
+ # Final XML should look like:
170
+ # <Request>
171
+ # <Authentication>
172
+ # <client>99000001</client>
173
+ # <password>******</password>
174
+ # </Authentication>
175
+ # <Transaction>
176
+ # <TxnDetails>
177
+ # <amount>25.00</amount>
178
+ # </TxnDetails>
179
+ # <HistoricTxn>
180
+ # <reference>4900200000000001</reference>
181
+ # <authcode>A6</authcode>
182
+ # <method>fulfill</method>
183
+ # </HistoricTxn>
184
+ # </Transaction>
185
+ # </Request>
186
+ #
187
+ # Parameters:
188
+ # -type must be FULFILL_TYPE or CANCEL_TYPE
189
+ # -money - optional - A money object with the price and currency
190
+ # -authorization - the Datacash reference number from a previous
191
+ # succesful authorize transaction
192
+ # -authcode - the Datacash authcode
193
+ # -order_id - The merchants reference
194
+ #
195
+ # Returns:
196
+ # -Builder xml document
197
+ #
198
+ def build_void_or_capture_request(type, money, authorization, options)
199
+ reference, auth_code = authorization.to_s.split(';')
200
+
201
+ xml = Builder::XmlMarkup.new :indent => 2
202
+ xml.instruct!
203
+ xml.tag! :Request do
204
+ add_authentication(xml)
205
+
206
+ xml.tag! :Transaction do
207
+ xml.tag! :HistoricTxn do
208
+ xml.tag! :reference, reference
209
+ xml.tag! :authcode, auth_code
210
+ xml.tag! :method, type
211
+ end
212
+
213
+ if money
214
+ xml.tag! :TxnDetails do
215
+ xml.tag! :merchantreference, format_reference_number(options[:order_id])
216
+ xml.tag! :amount, amount(money), :currency => currency(money)
217
+ end
218
+ end
219
+ end
220
+ end
221
+ xml.target!
222
+ end
223
+
224
+ # Create the xml document for an 'auth' or 'pre' transaction.
225
+ #
226
+ # Final XML should look like:
227
+ #
228
+ # <Request>
229
+ # <Authentication>
230
+ # <client>99000000</client>
231
+ # <password>*******</password>
232
+ # </Authentication>
233
+ # <Transaction>
234
+ # <TxnDetails>
235
+ # <merchantreference>123456</merchantreference>
236
+ # <amount currency="EUR">10.00</amount>
237
+ # </TxnDetails>
238
+ # <CardTxn>
239
+ # <Card>
240
+ # <pan>4444********1111</pan>
241
+ # <expirydate>03/04</expirydate>
242
+ # <Cv2Avs>
243
+ # <street_address1>Flat 7</street_address1>
244
+ # <street_address2>89 Jumble
245
+ # Street</street_address2>
246
+ # <street_address3>Mytown</street_address3>
247
+ # <postcode>AV12FR</postcode>
248
+ # <cv2>123</cv2>
249
+ # <ExtendedPolicy>
250
+ # <cv2_policy notprovided="reject"
251
+ # notchecked="accept"
252
+ # matched="accept"
253
+ # notmatched="reject"
254
+ # partialmatch="reject"/>
255
+ # <postcode_policy notprovided="reject"
256
+ # notchecked="accept"
257
+ # matched="accept"
258
+ # notmatched="reject"
259
+ # partialmatch="accept"/>
260
+ # <address_policy notprovided="reject"
261
+ # notchecked="accept"
262
+ # matched="accept"
263
+ # notmatched="reject"
264
+ # partialmatch="accept"/>
265
+ # </ExtendedPolicy>
266
+ # </Cv2Avs>
267
+ # </Card>
268
+ # <method>auth, </method>
269
+ # </CardTxn>
270
+ # </Transaction>
271
+ # </Request>
272
+ #
273
+ # Parameters:
274
+ # -type must be 'auth' or 'pre'
275
+ # -money - A money object with the price and currency
276
+ # -credit_card - The credit_card details to use
277
+ # -options:
278
+ # :order_id is the merchant reference number
279
+ # :billing_address is the billing address for the cc
280
+ # :address is the delivery address
281
+ #
282
+ # Returns:
283
+ # -xml: Builder document containing the markup
284
+ #
285
+ def build_purchase_or_authorization_request(type, money, credit_card, options)
286
+ xml = Builder::XmlMarkup.new :indent => 2
287
+ xml.instruct!
288
+ xml.tag! :Request do
289
+ add_authentication(xml)
290
+
291
+ xml.tag! :Transaction do
292
+ xml.tag! :CardTxn do
293
+ xml.tag! :method, type
294
+
295
+ add_credit_card(xml, credit_card, options[:billing_address] || options[:address])
296
+ end
297
+ xml.tag! :TxnDetails do
298
+ xml.tag! :merchantreference, format_reference_number(options[:order_id])
299
+ xml.tag! :amount, amount(money), :currency => currency(money)
300
+ end
301
+ end
302
+ end
303
+ xml.target!
304
+ end
305
+
306
+ # Adds the authentication element to the passed builder xml doc
307
+ #
308
+ # Parameters:
309
+ # -xml: Builder document that is being built up
310
+ #
311
+ # Returns:
312
+ # -none: The results is stored in the passed xml document
313
+ #
314
+ def add_authentication(xml)
315
+ xml.tag! :Authentication do
316
+ xml.tag! :client, @options[:login]
317
+ xml.tag! :password, @options[:password]
318
+ end
319
+ end
320
+
321
+ # Add credit_card detals to the passed XML Builder doc
322
+ #
323
+ # Parameters:
324
+ # -xml: Builder document that is being built up
325
+ # -credit_card: ActiveMerchant::Billing::CreditCard object
326
+ # -billing_address: Hash containing all of the billing address details
327
+ #
328
+ # Returns:
329
+ # -none: The results is stored in the passed xml document
330
+ #
331
+ def add_credit_card(xml, credit_card, address)
332
+ xml.tag! :Card do
333
+
334
+ # DataCash calls the CC number 'pan'
335
+ xml.tag! :pan, credit_card.number
336
+ xml.tag! :expirydate, format_date(credit_card.month, credit_card.year)
337
+
338
+ # optional values - for Solo etc
339
+ if [ 'switch', 'solo' ].include?(credit_card.type.to_s)
340
+
341
+ xml.tag! :issuenumber, credit_card.issue_number unless credit_card.issue_number.blank?
342
+
343
+ if !credit_card.start_month.blank? && !credit_card.start_year.blank?
344
+ xml.tag! :startdate, format_date(credit_card.start_month, credit_card.start_year)
345
+ end
346
+ end
347
+
348
+ xml.tag! :Cv2Avs do
349
+ xml.tag! :cv2, credit_card.verification_value if credit_card.verification_value?
350
+ xml.tag! :street_address1, address[:address1] unless address[:address1].blank?
351
+ xml.tag! :street_address2, address[:address2] unless address[:address2].blank?
352
+ xml.tag! :street_address3, address[:address3] unless address[:address3].blank?
353
+ xml.tag! :street_address4, address[:address4] unless address[:address4].blank?
354
+ xml.tag! :postcode, address[:zip] unless address[:zip].blank?
355
+
356
+ # The ExtendedPolicy defines what to do when the passed data
357
+ # matches, or not...
358
+ #
359
+ # All of the following elements MUST be present for the
360
+ # xml to be valid (or can drop the ExtendedPolicy and use
361
+ # a predefined one
362
+ xml.tag! :ExtendedPolicy do
363
+ xml.tag! :cv2_policy,
364
+ :notprovided => POLICY_REJECT,
365
+ :notchecked => POLICY_REJECT,
366
+ :matched => POLICY_ACCEPT,
367
+ :notmatched => POLICY_REJECT,
368
+ :partialmatch => POLICY_REJECT
369
+ xml.tag! :postcode_policy,
370
+ :notprovided => POLICY_ACCEPT,
371
+ :notchecked => POLICY_ACCEPT,
372
+ :matched => POLICY_ACCEPT,
373
+ :notmatched => POLICY_REJECT,
374
+ :partialmatch => POLICY_ACCEPT
375
+ xml.tag! :address_policy,
376
+ :notprovided => POLICY_ACCEPT,
377
+ :notchecked => POLICY_ACCEPT,
378
+ :matched => POLICY_ACCEPT,
379
+ :notmatched => POLICY_REJECT,
380
+ :partialmatch => POLICY_ACCEPT
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ # Send the passed data to DataCash for processing
387
+ #
388
+ # Parameters:
389
+ # -request: The XML data that is to be sent to Datacash
390
+ #
391
+ # Returns:
392
+ # - ActiveMerchant::Billing::Response object
393
+ #
394
+ def commit(request)
395
+ url = test? ? TEST_URL : LIVE_URL
396
+
397
+ @response = parse(ssl_post(url, request))
398
+ success = @response[:status] == DATACASH_SUCCESS
399
+ message = @response[:reason]
400
+
401
+ Response.new(success, message, @response,
402
+ :test => test?,
403
+ :authorization => "#{@response[:datacash_reference]};#{@response[:authcode]}"
404
+ )
405
+ end
406
+
407
+ # Find the currency of the Money object passed
408
+ #
409
+ # Parameters:
410
+ # -money: The money object that we are looking at
411
+ #
412
+ # Returns:
413
+ # -string: The three digit currency code (These are
414
+ # ISO 4217 codes)
415
+ #
416
+ def currency(money)
417
+ money.respond_to?(:currency) ? money.currency : 'GBP'
418
+ end
419
+
420
+ # Returns a date string in the format Datacash expects
421
+ #
422
+ # Parameters:
423
+ # -month: integer, the month
424
+ # -year: integer, the year
425
+ #
426
+ # Returns:
427
+ # -String: date in MM/YY format
428
+ #
429
+ def format_date(month, year)
430
+ "#{format(month,:two_digits)}/#{format(year, :two_digits)}"
431
+ end
432
+
433
+ # Parse the datacash response and create a Response object
434
+ #
435
+ # Parameters:
436
+ # -body: The XML returned from Datacash
437
+ #
438
+ # Returns:
439
+ # -a hash with all of the values returned in the Datacash XML response
440
+ #
441
+ def parse(body)
442
+
443
+ response = {}
444
+ xml = REXML::Document.new(body)
445
+ root = REXML::XPath.first(xml, "//Response")
446
+
447
+ root.elements.to_a.each do |node|
448
+ parse_element(response, node)
449
+ end
450
+
451
+ response
452
+ end
453
+
454
+ # Parse an xml element
455
+ #
456
+ # Parameters:
457
+ # -response: The hash that the values are being returned in
458
+ # -node: The node that is currently being read
459
+ #
460
+ # Returns:
461
+ # - none (results are stored in the passed hash)
462
+ def parse_element(response, node)
463
+ if node.has_elements?
464
+ node.elements.each{|e| parse_element(response, e) }
465
+ else
466
+ response[node.name.underscore.to_sym] = node.text
467
+ end
468
+ end
469
+
470
+ def format_reference_number(number)
471
+ number.to_s.gsub(/[^A-Za-z0-9]/, '').rjust(6, "0")
472
+ end
473
+ end
474
+ end
475
+ end