exact4r 0.5.2 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG +8 -0
  2. data/README +8 -8
  3. data/VERSION +1 -1
  4. data/doc/classes/EWS/Transaction/FakeResponse.html +451 -0
  5. data/doc/classes/EWS/Transaction/Request.html +54 -81
  6. data/doc/classes/EWS/Transaction/Response.html +182 -14
  7. data/doc/classes/EWS/Transaction/Validator.html +168 -0
  8. data/doc/classes/EWS/Transporter.html +271 -0
  9. data/doc/created.rid +1 -1
  10. data/doc/files/CHANGELOG.html +15 -2
  11. data/doc/files/README.html +17 -9
  12. data/doc/files/VERSION.html +2 -2
  13. data/doc/files/lib/ews/transaction/fake_response_rb.html +101 -0
  14. data/doc/files/lib/ews/transaction/mapping_rb.html +1 -1
  15. data/doc/files/lib/ews/transaction/request_rb.html +8 -1
  16. data/doc/files/lib/ews/transaction/response_rb.html +1 -1
  17. data/doc/files/lib/ews/transaction/validator_rb.html +101 -0
  18. data/doc/files/lib/ews/{transaction/transporter_rb.html → transporter_rb.html} +3 -3
  19. data/doc/files/lib/exact4r_rb.html +4 -2
  20. data/doc/fr_class_index.html +3 -1
  21. data/doc/fr_file_index.html +3 -1
  22. data/doc/fr_method_index.html +18 -7
  23. data/lib/ews/transaction/fake_response.rb +137 -0
  24. data/lib/ews/transaction/mapping.rb +38 -15
  25. data/lib/ews/transaction/request.rb +10 -58
  26. data/lib/ews/transaction/response.rb +3 -3
  27. data/lib/ews/transaction/validator.rb +230 -0
  28. data/lib/ews/transporter.rb +143 -0
  29. data/lib/exact4r.rb +4 -1
  30. data/spec/donncha_spec.rb +13 -0
  31. data/spec/mapping_spec.rb +45 -4
  32. data/spec/request_spec.rb +96 -69
  33. data/spec/spec_helper.rb +20 -8
  34. data/spec/transporter_spec.rb +26 -2
  35. data/spec/validator_spec.rb +145 -0
  36. metadata +16 -7
  37. data/doc/classes/EWS/Transaction/Transporter.html +0 -250
  38. data/lib/ews/transaction/transporter.rb +0 -120
  39. data/output.log +0 -368
@@ -0,0 +1,137 @@
1
+ module EWS # :nodoc:
2
+ module Transaction # :nodoc:
3
+
4
+ # As its name suggests, this class allows you to generate fake responses,
5
+ # allowing you to stub out the web service in your testing.
6
+ #
7
+ # The most likely responses have been catered for here, but if you require a fake response
8
+ # which is not provided for here, the best approach would be to generate a fake valid response
9
+ # and then adjust its attributes (in consultation with E-xact's Web Service Programming Reference
10
+ # Guide) to match the particular response you want.
11
+ #
12
+ # Example:
13
+ #
14
+ # describe "Fake requests" do
15
+ # it "should stub sending" do
16
+ # request = {:nonsense => "this is nonsense"}
17
+ # fake_response = EWS::Transaction::FakeResponse.valid(request) # a fake valid response
18
+ # transporter = EWS::Transporter.new
19
+ # transporter.stubs(:submit).returns(fake_response)
20
+ #
21
+ # response = transporter.submit(request)
22
+ # response.should == fake_response
23
+ # response.should be_approved
24
+ # response.bank_message.should == "APPROVED"
25
+ # end
26
+ # end
27
+ class FakeResponse
28
+
29
+ # fake a valid response
30
+ def self.valid(request)
31
+ build_response(request)
32
+ end
33
+ # fake a declined response
34
+ def self.declined(request)
35
+ response = build_response(request, {:bank_resp_code => '200'})
36
+ end
37
+
38
+ # fake a response indicating an invalid credit card number
39
+ def self.invalid_cc_number(request)
40
+ build_response(request, {:exact_resp_code => '22'})
41
+ end
42
+ # fake a response indicating an invalid credit card expiry date
43
+ def self.invalid_cc_expiry(request)
44
+ build_response(request, {:exact_resp_code => '25'})
45
+ end
46
+ # fake a response indicating an invalid amount
47
+ def self.invalid_amount(request)
48
+ build_response(request, {:exact_resp_code => '26'})
49
+ end
50
+ # fake a response indicating an invalid credit card holder name
51
+ def self.invalid_cardholder_name(request)
52
+ build_response(request, {:exact_resp_code => '27'})
53
+ end
54
+ # fake a response indicating an invalid authorisation number
55
+ def self.invalid_auth_num(request)
56
+ build_response(request, {:exact_resp_code => '28'})
57
+ end
58
+ # fake a response indicating an invalid credit card verification string
59
+ def self.invalid_cc_verification_str(request)
60
+ build_response(request, {:exact_resp_code => '31'})
61
+ end
62
+ # fake a response indicating an invalid transaction code
63
+ def self.invalid_transaction_code(request)
64
+ build_response(request, {:exact_resp_code => '32'})
65
+ end
66
+ # fake a response indicating an invalid reference number
67
+ def self.invalid_reference_no(request)
68
+ build_response(request, {:exact_resp_code => '57'})
69
+ end
70
+ # fake a response indicating an invalid address verification string
71
+ def self.invalid_avs(request)
72
+ build_response(request, {:exact_resp_code => '58'})
73
+ end
74
+
75
+ private
76
+
77
+ def self.build_response(request, options = {})
78
+ # copy all the information from the request
79
+ exact_resp_code = options[:exact_resp_code] || '00'
80
+ bank_resp_code = options[:bank_resp_code] || '000'
81
+
82
+ response = EWS::Transaction::Mapping.json_to_response(EWS::Transaction::Mapping.request_to_json(request))
83
+ response.transaction_tag = rand(9000)
84
+
85
+ response.exact_resp_code = exact_resp_code
86
+ response.exact_message = @@exact_messages[exact_resp_code]
87
+ if (exact_resp_code == '00')
88
+ response.bank_resp_code = bank_resp_code
89
+ response.bank_resp_code_2 = '00'
90
+ response.bank_message = (bank_resp_code == '000') ? "APPROVED" : "Declined"
91
+ end
92
+
93
+ response.error_number = 0 # no http errors occurred
94
+ response.transaction_error = (exact_resp_code == '00') ? 0 : 1
95
+ response.transaction_approved = (exact_resp_code == '00' and bank_resp_code == '000') ? 1 : 0
96
+
97
+ response.authorization_num = "ET#{response.transaction_tag}" if response.approved?
98
+
99
+ response.sequence_no = "#{rand(100000)}"
100
+ response.avs = 'X' # exact match, 9-digit zip
101
+ response.cvv2 = 'M' # match
102
+
103
+ # great snowboarding ;-)
104
+ response.merchant_name = "Fernie Alpine Resort"
105
+ response.merchant_address = "5339 Fernie Ski Hill Rd."
106
+ response.merchant_city = "Fernie"
107
+ response.merchant_province = "BC"
108
+ response.merchant_country = "Canada"
109
+ response.merchant_postal = "V0B 1M6"
110
+ response.merchant_url = "http://skifernie.com/"
111
+
112
+ response.ctr = "Let's pretend this is a receipt"
113
+
114
+ response
115
+ end
116
+
117
+ @@exact_messages = {
118
+ '00' => 'Transaction Normal',
119
+ '08' => 'CVV2/CID/CVC2 Data not verified',
120
+ '22' => 'Invalid Credit Card Number',
121
+ '25' => 'Invalid Expiry Date',
122
+ '26' => 'Invalid Amount',
123
+ '27' => 'Invalid Card Holder',
124
+ '28' => 'Invalid Authorization Number',
125
+ '31' => 'Invalid Verification String',
126
+ '32' => 'Invalid Transaction Code',
127
+ '57' => 'Invalid Reference Number',
128
+ '58' => 'Invalid AVS String'
129
+ }
130
+
131
+ @@bank_messages = {
132
+ '000' => "APPROVED",
133
+ '200' => "Declined"
134
+ }
135
+ end
136
+ end
137
+ end
@@ -24,12 +24,13 @@ module EWS # :nodoc:
24
24
  :Track1 => :track1,
25
25
  :Track2 => :track2,
26
26
  :PAN => :pan,
27
- :Authorization_Num => :auth_number,
27
+ :Authorization_Num => :authorization_num,
28
28
  :Expiry_Date => :cc_expiry,
29
29
  :CardHoldersName => :cardholder_name,
30
30
  :VerificationStr1 => :cc_verification_str1,
31
31
  :VerificationStr2 => :cc_verification_str2,
32
32
  :CVD_Presence_Ind => :cvd_presence_ind,
33
+ :ZipCode => :zip_code,
33
34
  :Tax1Amount => :tax1_amount,
34
35
  :Tax1Number => :tax1_number,
35
36
  :Tax2Amount => :tax2_amount,
@@ -45,8 +46,7 @@ module EWS # :nodoc:
45
46
  :Reference_3 => :reference_3,
46
47
  :Language => :language,
47
48
  :Client_IP => :client_ip,
48
- :Client_Email => :client_email,
49
- :User_Name => :user_name
49
+ :Client_Email => :client_email
50
50
  } unless defined?(XML_REQUEST_TAGS_TO_ATTRS)
51
51
 
52
52
  XML_RESPONSE_TAGS_TO_ATTRS = {
@@ -72,7 +72,7 @@ module EWS # :nodoc:
72
72
  :MerchantCountry => :merchant_country,
73
73
  :MerchantPostal => :merchant_postal,
74
74
  :MerchantURL => :merchant_url,
75
- :CTR => :CTR
75
+ :CTR => :ctr
76
76
  }.merge(XML_REQUEST_TAGS_TO_ATTRS) unless defined?(XML_RESPONSE_TAGS_TO_ATTRS)
77
77
 
78
78
  def self.request_to_json(request)
@@ -110,12 +110,38 @@ module EWS # :nodoc:
110
110
  end
111
111
  def self.rest_to_response(content)
112
112
  response = EWS::Transaction::Response.new
113
- response_xml_string_to_hash(response, content)
113
+ xml = REXML::Document.new(content)
114
+ root = REXML::XPath.first(xml, "//TransactionResult")
115
+ response_xml_string_to_hash(response, root) if root
114
116
  response
115
117
  end
116
118
  def self.soap_to_response(content)
117
119
  response = EWS::Transaction::Response.new
118
- response_xml_string_to_hash(response, content, xpath = "//types:TransactionResult")
120
+ xml = REXML::Document.new(content)
121
+ root = REXML::XPath.first(xml, "//types:TransactionResult")
122
+ if root
123
+ # we have a normal response
124
+ response_xml_string_to_hash(response, root)
125
+ else
126
+ # check if we have an error response
127
+ faultErrorRoot = REXML::XPath.first(xml, "//soap:Fault")
128
+ unless faultErrorRoot.nil?
129
+ # if we do, then see if we have a details section
130
+ detailRoot = REXML::XPath.first(faultErrorRoot, "detail")
131
+ if detailRoot.nil? or !detailRoot.has_elements?
132
+ # no details section, so we have an XML parsing error and should raise an exception
133
+ faultString = REXML::XPath.first(faultErrorRoot, "faultstring")
134
+ raise faultString.text
135
+ else
136
+ errorElem = REXML::XPath.first(detailRoot, "error")
137
+ # do have details, so figure out the error_number and error_description
138
+ errorNumElem = errorElem.attribute("number")
139
+ response.error_number = errorNumElem.value.to_i unless errorNumElem.nil?
140
+ errorDescElem = errorElem.attribute("description")
141
+ response.error_description = errorDescElem.value unless errorDescElem.nil?
142
+ end
143
+ end
144
+ end
119
145
  response
120
146
  end
121
147
 
@@ -128,15 +154,12 @@ module EWS # :nodoc:
128
154
  end
129
155
  end
130
156
 
131
- # parses xml response string into the response attributes. The XPath identifies the root element
132
- # of the payload. For REST (default) this is "//TransactionResult", for SOAP this is name space
133
- # prefixed "//types:TransactionResult".
134
- def self.response_xml_string_to_hash(response, xml_string, xpath = "//TransactionResult")
135
- xml = REXML::Document.new(xml_string)
136
- if root = REXML::XPath.first(xml, xpath)
137
- root.elements.to_a.each do |node|
138
- gwlib_prop_name = XML_RESPONSE_TAGS_TO_ATTRS[node.name.to_sym]
139
- logger.warn("No mapping for the tag #{node.name}.") if gwlib_prop_name.nil?
157
+ # parses xml response elements into the response attributes
158
+ def self.response_xml_string_to_hash(response, root)
159
+ root.elements.to_a.each do |node|
160
+ gwlib_prop_name = XML_RESPONSE_TAGS_TO_ATTRS[node.name.to_sym]
161
+ unless gwlib_prop_name.nil?
162
+ # todo: should notiy this somehow??
140
163
  value = (ATTR_FORMATS.include?(gwlib_prop_name)) ? node.text.send("to_#{ATTR_FORMATS[gwlib_prop_name]}") : node.text
141
164
  response.send "#{gwlib_prop_name}=", value
142
165
  end
@@ -1,3 +1,4 @@
1
+ require 'ews/transaction/validator'
1
2
  module EWS # :nodoc:
2
3
  module Transaction # :nodoc:
3
4
 
@@ -18,7 +19,7 @@ module EWS # :nodoc:
18
19
  # Reference Guide, v8.5 be consulted. This document is contained in the Webservice Plugin ZIP file:
19
20
  # http://www.e-xact.com/wp-content/uploads/2007/06/E-xact_Payment_Webservice_Plug-In.zip
20
21
  #
21
- # Please note that, if your chosen transaction requires it, credit card expiry dates *must* be entered in YYMM format.
22
+ # Please note that, if your chosen transaction requires it, credit card expiry dates *must* be entered in MMYY format.
22
23
  #
23
24
  # =Allowable transaction types
24
25
  # :purchase
@@ -44,11 +45,13 @@ module EWS # :nodoc:
44
45
  # :secure_storage_eft
45
46
  # :transaction_details
46
47
  class Request
48
+ include Validator
47
49
 
50
+ # yeah, it's ugly, but otherwise RDoc won't pick them up
48
51
  attr_accessor :errors
49
- attr_accessor :gateway_id, :password, :transaction_type, :amount, :surcharge_amount, :cc_number, :transaction_tag, :track1, :track2, :pan, :auth_number, :cc_expiry, :cardholder_name
52
+ attr_accessor :gateway_id, :password, :transaction_type, :amount, :surcharge_amount, :cc_number, :transaction_tag, :track1, :track2, :pan, :authorization_num, :cc_expiry, :cardholder_name
50
53
  attr_accessor :cc_verification_str1, :cc_verification_str2, :cvd_presence_ind, :tax1_amount, :tax1_number, :tax2_amount, :tax2_number, :secure_auth_required, :secure_auth_result
51
- attr_accessor :ecommerce_flag, :xid, :cavv, :cavv_algorithm, :reference_no, :customer_ref, :reference_3, :language, :client_ip, :client_email, :user_name
54
+ attr_accessor :ecommerce_flag, :xid, :cavv, :cavv_algorithm, :reference_no, :customer_ref, :reference_3, :language, :client_ip, :client_email, :zip_code
52
55
 
53
56
  # Initialize a Request with a hash of values
54
57
  def initialize(hash = {})
@@ -62,7 +65,10 @@ module EWS # :nodoc:
62
65
  # assume we're given a symbol, so look up the code
63
66
  value = @@transaction_codes[type_sym]
64
67
  # if nothing found, then maybe we were given an actual code?
65
- value = type_sym if value.nil? and @@transaction_codes.values.include?(type_sym)
68
+ if(value.nil?)
69
+ raise "invalid transaction_type supplied #{type_sym}" unless @@transaction_codes.values.include?(type_sym)
70
+ value = type_sym
71
+ end
66
72
 
67
73
  @transaction_type = value
68
74
  end
@@ -72,61 +78,7 @@ module EWS # :nodoc:
72
78
  self.transaction_type == "CR"
73
79
  end
74
80
 
75
- def valid?
76
- errors[:transaction_type] = "transaction_type must be supplied" if self.transaction_type.blank?
77
- errors[:transaction_type] = "invalid transaction_type supplied" unless @@transaction_codes.values.include? self.transaction_type
78
-
79
- # need to authenticate
80
- errors[:gateway_id] = "gateway_id must be supplied" if self.gateway_id.blank?
81
- errors[:password] = "password must be supplied" if self.password.blank?
82
-
83
- # ensure we've been given valid amounts
84
- errors[:amount] = "invalid amount supplied" unless valid_amount?(self.amount)
85
- errors[:surcharge_amount] = "invalid surcharge_amount supplied" unless valid_amount?(self.surcharge_amount)
86
- errors[:tax1_amount] = "invalid tax1_amount supplied" unless valid_amount?(self.tax1_amount)
87
- errors[:tax2_amount] = "invalid tax2_amount supplied" unless valid_amount?(self.tax2_amount)
88
-
89
- errors[:cc_number] = "invalid cc_number supplied" unless valid_card_number?
90
- errors[:cc_expiry] = "invalid cc_expiry supplied" unless valid_expiry_date?
91
-
92
- errors.empty?
93
- end
94
-
95
81
  private
96
- def valid_amount?(amount)
97
- return true if amount.blank?
98
-
99
- (amount.class == Float) or (amount.class == Fixnum) or !amount.match(/[^0-9.]/)
100
- end
101
-
102
- def valid_card_number?
103
- return true if self.cc_number.blank?
104
-
105
- # do a mod10 check
106
- weight = 1
107
- card_number = self.cc_number.scan(/./).map(&:to_i)
108
- result = card_number.reverse[1..-1].inject(0) do |sum, num|
109
- weight = 1 + weight%2
110
- digit = num * weight
111
- sum += (digit / 10) + (digit % 10)
112
- end
113
- card_number.last == (10 - result % 10 ) % 10
114
- end
115
-
116
- def valid_expiry_date?
117
- return true if self.cc_expiry.blank?
118
-
119
- # check format
120
- return false unless self.cc_expiry.match(/^\d{4}$/)
121
-
122
- # check date is not in past
123
- year, month = 2000 + self.cc_expiry[0..1].to_i, self.cc_expiry[2..3].to_i
124
-
125
- # CC is still considered valid during the month of expiry,
126
- # so just compare year and month, ignoring the rest.
127
- now = DateTime.now
128
- return ((1..12) === month) && DateTime.new(year, month) >= DateTime.new(now.year, now.month)
129
- end
130
82
 
131
83
  @@transaction_codes = {
132
84
  :purchase => '00',
@@ -7,11 +7,11 @@ module EWS # :nodoc:
7
7
  attr_accessor :logon_message, :error_number, :error_description, :transaction_error, :transaction_approved
8
8
  attr_accessor :exact_resp_code, :exact_message, :bank_resp_code, :bank_message, :bank_resp_code_2
9
9
  attr_accessor :sequence_no, :avs, :cvv2, :retrieval_ref_no, :cavv_response
10
- attr_accessor :merchant_name, :merchant_address, :merchant_city, :merchant_province, :merchant_country, :merchant_postal, :merchant_url, :CTR
10
+ attr_accessor :merchant_name, :merchant_address, :merchant_city, :merchant_province, :merchant_country, :merchant_postal, :merchant_url, :ctr
11
11
 
12
- attr_accessor :gateway_id, :password, :transaction_type, :amount, :surcharge_amount, :cc_number, :transaction_tag, :track1, :track2, :pan, :auth_number, :cc_expiry, :cardholder_name
12
+ attr_accessor :gateway_id, :password, :transaction_type, :amount, :surcharge_amount, :cc_number, :transaction_tag, :track1, :track2, :pan, :authorization_num, :cc_expiry, :cardholder_name
13
13
  attr_accessor :cc_verification_str1, :cc_verification_str2, :cvd_presence_ind, :tax1_amount, :tax1_number, :tax2_amount, :tax2_number, :secure_auth_required, :secure_auth_result
14
- attr_accessor :ecommerce_flag, :xid, :cavv, :cavv_algorithm, :reference_no, :customer_ref, :reference_3, :language, :client_ip, :client_email, :user_name
14
+ attr_accessor :ecommerce_flag, :xid, :cavv, :cavv_algorithm, :reference_no, :customer_ref, :reference_3, :language, :client_ip, :client_email, :user_name, :zip_code
15
15
 
16
16
  # Indicates whether or not the transaction was approved
17
17
  def approved?
@@ -0,0 +1,230 @@
1
+ module EWS # :nodoc:
2
+ module Transaction # :nodoc:
3
+ module Validator
4
+
5
+ @@valid_lengths = {
6
+ :authorization_num => 8,
7
+ :cardholder_name => 30,
8
+ :cc_number => 19,
9
+ :cc_expiry => 4,
10
+ :cavv => 40,
11
+ :client_email => 30,
12
+ :client_ip => 15,
13
+ :customer_ref => 20,
14
+ :gateway_id => 10,
15
+ :pan => 39,
16
+ :password => 30,
17
+ :reference_3 => 30,
18
+ :reference_no => 20,
19
+ :tax1_number => 20,
20
+ :tax2_number => 20,
21
+ :track1 => 79,
22
+ :track2 => 40,
23
+ :transaction_type => 2,
24
+ :cc_verification_str1 => 40,
25
+ :cc_verification_str2 => 4,
26
+ :xid => 40,
27
+ :zip_code => 10
28
+ }.freeze unless defined?(@@valid_lengths)
29
+
30
+ def valid?
31
+ @errors = {}
32
+
33
+ validate_lengths
34
+
35
+ validate_mandatory_fields
36
+
37
+ append_error(:transaction_type, "transaction_type must be supplied") if self.transaction_type.blank?
38
+
39
+ # need to authenticate
40
+ append_error(:gateway_id, "gateway_id must be supplied") if self.gateway_id.blank?
41
+ append_error(:password, "password must be supplied") if self.password.blank?
42
+
43
+ # ensure we've been given valid amounts
44
+ append_error(:amount, "invalid amount supplied") unless valid_amount?(self.amount)
45
+ append_error(:surcharge_amount, "invalid surcharge_amount supplied") unless valid_amount?(self.surcharge_amount)
46
+ append_error(:tax1_amount, "invalid tax1_amount supplied") unless valid_amount?(self.tax1_amount)
47
+ append_error(:tax2_amount, "invalid tax2_amount supplied") unless valid_amount?(self.tax2_amount)
48
+
49
+ # ensure our amounts are within range
50
+ append_error(:amount, "amount must be between 0.00 and 99999.99") unless amount_too_big?(self.amount)
51
+ append_error(:surcharge_amount, "amount must be between 0.00 and 99999.99") unless amount_too_big?(self.surcharge_amount)
52
+ append_error(:tax1_amount, "amount must be between 0.00 and 99999.99") unless amount_too_big?(self.tax1_amount)
53
+ append_error(:tax2_amount, "amount must be between 0.00 and 99999.99") unless amount_too_big?(self.tax2_amount)
54
+
55
+ # ensure our credit card information is valid
56
+ append_error(:cc_number, "invalid cc_number supplied") unless valid_card_number?
57
+ append_error(:cc_expiry, "invalid cc_expiry supplied") unless valid_expiry_date?
58
+
59
+ @errors.empty?
60
+ end
61
+
62
+ private
63
+ def validate_lengths
64
+ @@valid_lengths.each do |k,len|
65
+ value = self.send k
66
+ append_error(k, "#{k.to_s} is too long. maximum allowed length is #{len} characters") unless value.nil? or (value.length <= len)
67
+ end
68
+ end
69
+
70
+ # which fields are mandatory and which optional depends on the transaction_type and
71
+ # also how the credit card information is supplied.
72
+ #
73
+ # it can be supplied either
74
+ # a) via the cc_number field
75
+ # b) via a tagged transaction
76
+ # c) encoded in a track1 value, or
77
+ # d) encoded in a track2 value
78
+ def validate_mandatory_fields
79
+ if !self.cc_number.blank?
80
+ validate_for_card
81
+ elsif !self.transaction_tag.blank?
82
+ validate_for_transaction_tag
83
+ elsif !self.track1.blank?
84
+ validate_for_track1
85
+ elsif !self.track2.blank?
86
+ validate_for_track2
87
+ end
88
+ end
89
+
90
+ def valid_amount?(amount)
91
+ return true if amount.blank?
92
+
93
+ ((amount.class == Float) or (amount.class == Fixnum) or !amount.match(/[^0-9.]/))
94
+ end
95
+ def amount_too_big?(amount)
96
+ return true if amount.blank?
97
+
98
+ return ((amount.to_f <= 99999.99) and (amount.to_f >= 0.0))
99
+ end
100
+
101
+ def valid_card_number?
102
+ return true if self.cc_number.blank?
103
+
104
+ # do a mod10 check
105
+ weight = 1
106
+ card_number = self.cc_number.scan(/./).map(&:to_i)
107
+ result = card_number.reverse[1..-1].inject(0) do |sum, num|
108
+ weight = 1 + weight%2
109
+ digit = num * weight
110
+ sum += (digit / 10) + (digit % 10)
111
+ end
112
+ card_number.last == (10 - result % 10 ) % 10
113
+ end
114
+
115
+ # date should be...
116
+ # - not blank
117
+ # - 4 digits
118
+ # - MMYY format
119
+ # - not in the past
120
+ def valid_expiry_date?
121
+ return true if self.cc_expiry.blank?
122
+
123
+ # check format
124
+ return false unless self.cc_expiry.match(/^\d{4}$/)
125
+
126
+ # check date is not in past
127
+ year, month = 2000 + self.cc_expiry[2..3].to_i, self.cc_expiry[0..1].to_i
128
+
129
+ # CC is still considered valid during the month of expiry,
130
+ # so just compare year and month, ignoring the rest.
131
+ now = DateTime.now
132
+ return ((1..12) === month) && DateTime.new(year, month) >= DateTime.new(now.year, now.month)
133
+ end
134
+
135
+ def append_error(key, message)
136
+ if @errors[key].nil?
137
+ @errors[key] = message
138
+ else
139
+ # otherwise convert into an array of errors
140
+ current_val = @errors[key]
141
+ if current_val.is_a?(String)
142
+ @errors[key] = [current_val, message]
143
+ else
144
+ @errors[key] << message
145
+ end
146
+ end
147
+ end
148
+
149
+ # validate presence of mandatort fields when cc_number present
150
+ def validate_for_card
151
+ tt = self.transaction_type.to_i
152
+
153
+ # mandatory: transaction_type must != (30, 31, 32, 34, 35)
154
+ append_error(:cc_number, "cc_number must not be set for tagged transactions") if [30,31,32,34,35].include?(tt)
155
+
156
+ # amount, cardholder_name always mandaory
157
+ mandatory = [:amount, :cardholder_name]
158
+
159
+ # card_number & expiry_date mandatory for all except 50, 54
160
+ # pan mandatory for only 50, 54
161
+ mandatory << ([50,54].include?(tt) ? :pan : [:cc_number, :cc_expiry])
162
+ mandatory.flatten!
163
+
164
+ # reference_no mandatory for 60
165
+ mandatory << :reference_no if tt == 60
166
+
167
+ # auth_number mandatory for (02, 03, 11, 12, 13)
168
+ mandatory << :authorization_num if [02, 03, 11, 12, 13].include?(tt)
169
+
170
+ check_mandatory(mandatory)
171
+ end
172
+
173
+ def validate_for_transaction_tag
174
+ tt = self.transaction_type
175
+
176
+ # mandatory: transaction_type must == (30, 31, 32, 34, 35)
177
+ append_error(:transaction_tag, "transaction_tag must only be set for tagged transactions") unless ['30','31','32','34','35','CR'].include?(tt)
178
+
179
+ # transaction_tag, auth_num & amount mandatory
180
+ mandatory = [:transaction_tag]
181
+ mandatory << [:authorization_num, :amount] unless tt == 'CR'
182
+
183
+ check_mandatory(mandatory.flatten)
184
+ end
185
+
186
+ def validate_for_track1
187
+ tt = self.transaction_type.to_i
188
+
189
+ # mandatory: transaction_type must != (30, 31, 32, 34, 35)
190
+ append_error(:track1, "track1 must not be set for tagged transactions") if [30,31,32,34,35].include?(tt)
191
+
192
+ # amount mandatory for all
193
+ mandatory = [:amount]
194
+
195
+ # track1 mandatory, except 50,54
196
+ # pan mandatory 50,54 only
197
+ mandatory << ([50,54].include?(tt) ? :pan : :track1)
198
+
199
+ # reference_no mandatory for 60
200
+ mandatory << :reference_no if tt == 60
201
+ # auth_number mandatory for (02, 03, 11, 12, 13)
202
+ mandatory << :authorization_num if [02, 03, 11, 12, 13].include?(tt)
203
+
204
+ check_mandatory(mandatory)
205
+ end
206
+
207
+ def validate_for_track2
208
+ tt = self.transaction_type.to_i
209
+
210
+ # mandatory: transaction_type must != (30, 31, 32, 34, 35, 50, 54)
211
+ append_error(:track1, "track1 must not be set for tagged transactions") if [30,31,32,34,35,50,54].include?(tt)
212
+
213
+ # track2, expiry_date, cardholder_name, amount mandatory
214
+ mandatory = [:track2, :cc_expiry, :cardholder_name, :amount]
215
+
216
+ # auth_number mandatory for (02, 03, 11, 12, 13)
217
+ mandatory << :authorization_num if [02, 03, 11, 12, 13].include?(tt)
218
+
219
+ check_mandatory(mandatory)
220
+ end
221
+
222
+ def check_mandatory(mandatory)
223
+ mandatory.each do |key|
224
+ append_error(key, "#{key} is required") if self.send(key).blank?
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+