ruby_psigate 0.7

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 (47) hide show
  1. data/CHANGELOG +2 -0
  2. data/Gemfile +12 -0
  3. data/LICENSE +0 -0
  4. data/Manifest +45 -0
  5. data/README.markdown +99 -0
  6. data/Rakefile +28 -0
  7. data/lib/certs/cacert.pem +2633 -0
  8. data/lib/ruby_psigate/account.rb +152 -0
  9. data/lib/ruby_psigate/account_manager_api.rb +137 -0
  10. data/lib/ruby_psigate/account_methods.rb +5 -0
  11. data/lib/ruby_psigate/address.rb +54 -0
  12. data/lib/ruby_psigate/connection.rb +96 -0
  13. data/lib/ruby_psigate/credit_card.rb +104 -0
  14. data/lib/ruby_psigate/credit_card_methods.rb +12 -0
  15. data/lib/ruby_psigate/error.rb +33 -0
  16. data/lib/ruby_psigate/gateway.rb +126 -0
  17. data/lib/ruby_psigate/hash_variables.rb +58 -0
  18. data/lib/ruby_psigate/item.rb +65 -0
  19. data/lib/ruby_psigate/item_option.rb +10 -0
  20. data/lib/ruby_psigate/number_validation_methods.rb +12 -0
  21. data/lib/ruby_psigate/order.rb +120 -0
  22. data/lib/ruby_psigate/recurring_charge.rb +53 -0
  23. data/lib/ruby_psigate/recurring_item.rb +26 -0
  24. data/lib/ruby_psigate/response.rb +109 -0
  25. data/lib/ruby_psigate/serializer.rb +59 -0
  26. data/lib/ruby_psigate/transaction_methods.rb +21 -0
  27. data/lib/ruby_psigate/utils.rb +18 -0
  28. data/lib/ruby_psigate.rb +57 -0
  29. data/ruby_psigate.gemspec +31 -0
  30. data/test/remote/remote_account_test.rb +33 -0
  31. data/test/remote/remote_gateway_test.rb +32 -0
  32. data/test/test_helper.rb +144 -0
  33. data/test/unit/account_manager_api_test.rb +96 -0
  34. data/test/unit/account_test.rb +388 -0
  35. data/test/unit/address_test.rb +99 -0
  36. data/test/unit/connection_test.rb +153 -0
  37. data/test/unit/credit_card_test.rb +152 -0
  38. data/test/unit/gateway_test.rb +112 -0
  39. data/test/unit/item_option_test.rb +19 -0
  40. data/test/unit/item_test.rb +106 -0
  41. data/test/unit/order_test.rb +491 -0
  42. data/test/unit/recurring_charge_test.rb +89 -0
  43. data/test/unit/recurring_item_test.rb +62 -0
  44. data/test/unit/response_test.rb +110 -0
  45. data/test/unit/serializer_test.rb +89 -0
  46. data/test/unit/xml_api_test.rb +25 -0
  47. metadata +154 -0
@@ -0,0 +1,152 @@
1
+ module RubyPsigate
2
+
3
+ class InvalidRecurringCharge < RubyPsigateError; end
4
+
5
+ class Account
6
+
7
+ include AccountMethods
8
+ include CreditCardMethods
9
+
10
+ # List of actions you can perform on an account and their associated constant codes
11
+ ACTIONS = {
12
+ :account => {
13
+ :summary => "AMA00",
14
+ :details => "AMA05",
15
+ :register => "AMA01",
16
+ :update => "AMA02",
17
+ :enable => "AMA08",
18
+ :disable => "AMA09"
19
+ },
20
+ :credit_card => {
21
+ :add => "AMA11",
22
+ :delete => "AMA14",
23
+ :enable => "AMA18",
24
+ :disable => "AMA19"
25
+ },
26
+ :charges => {
27
+ :summary => "RBC00",
28
+ :add => "RBC01",
29
+ :update => "RBC02",
30
+ :delete => "RBC04",
31
+ :details => "RBC05",
32
+ :enable => "RBC08",
33
+ :disable => "RBC09",
34
+ :immediate => "RBC99"
35
+ }
36
+ }
37
+
38
+ def self.action_reverse_lookup(code)
39
+ result = nil
40
+ result_first = nil
41
+ result_second = nil
42
+ ACTIONS.each_pair do |section, actions|
43
+ break if result_second
44
+ result_second = actions.select { |k,v| v == code }
45
+ if result_second
46
+ result_first = section
47
+ result_second = result_second.keys[0]
48
+ end
49
+ end
50
+ result = "#{result_first}_#{result_second}".downcase.to_sym
51
+ end
52
+
53
+ # include RubyPsigate::HashVariables
54
+ # hashable :account_register, %w( AccountID )
55
+ # hashable :account_update, %w( Condition Update )
56
+
57
+ attr_writer :action
58
+ attr_reader :address, :rbcharge
59
+ attr_accessor :account_id, :comments, :email, :serial_no, :store_id
60
+
61
+ # For hasahable
62
+ alias_method :accountid, :account_id
63
+
64
+ def to_hash(type=nil)
65
+ type = self.class.action_reverse_lookup(type)
66
+ result = {}
67
+ result = process_hash(result, type)
68
+ result
69
+ end
70
+
71
+ def action
72
+ ACTIONS[@action.first[0]][@action.first[1]]
73
+ end
74
+
75
+ def process_hash(result, type)
76
+ result.merge!({:Action => action})
77
+
78
+ if type == :account_register
79
+ result[:Account] = Hash.new
80
+ result[:Account].merge!({:AccountID => nil})
81
+ result[:Account].merge!(address.to_hash(:billing)) unless address.nil?
82
+ result[:Account].merge!({ :CardInfo => cc.to_hash(:card_info) })
83
+ end
84
+
85
+ if type == :account_update
86
+ result.merge!({:Condition => { :AccountID => account_id }})
87
+ result[:Update] = {}
88
+ result[:Update].merge!(address.to_hash(:billing)) unless address.nil?
89
+ result[:Update].merge!(:Email => email) unless email.nil?
90
+ result[:Update].merge!(:Comments => comments) unless comments.nil?
91
+ end
92
+
93
+ if type == :account_details || type == :account_enable || type == :account_disable
94
+ result.merge!({:Condition => { :AccountID => account_id }})
95
+ end
96
+
97
+ if type == :credit_card_add
98
+ result[:Account] = Hash.new
99
+ result[:Account].merge!({ :AccountID => account_id })
100
+ result[:Account].merge!({:CardInfo => cc.to_hash(:card_info)})
101
+ end
102
+
103
+ if type == :credit_card_delete || type == :credit_card_enable || type == :credit_card_disable
104
+ result.merge!({:Condition => { :AccountID => account_id, :SerialNo => serial_no }})
105
+ end
106
+
107
+ if type == :charges_summary
108
+ result[:Condition] = Hash.new
109
+ result.merge!({:Condition => { :AccountID => account_id, :StoreID => store_id }})
110
+ result[:Condition].merge!(rbcharge.to_hash)
111
+ end
112
+
113
+ if type == :charges_add || type == :charges_immediate
114
+ result[:Charge] = Hash.new
115
+ result[:Charge].merge!({:AccountID => account_id, :SerialNo => serial_no})
116
+ result[:Charge].merge!(rbcharge.to_hash)
117
+ end
118
+
119
+ if type == :charges_update
120
+ result.merge!({:Condition => { :RBCID => rbcharge.rbcid }})
121
+ result.merge!({:Update => { :RBTrigger => rbcharge.rbtrigger }})
122
+ end
123
+
124
+ if type == :charges_delete || type == :charges_details || type == :charges_enable || type == :charges_disable
125
+ result.merge!({:Condition => { :RBCID => rbcharge.rbcid }})
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ # Method used to describe the query to cross reference the account on local and Psigate's servers
132
+ def condition
133
+ # AccountID, RBCID, RBTrigger, Type, InvoiceNo, Status, SerialNo
134
+ { :AccountID => "123" }
135
+ end
136
+
137
+ def update
138
+ { :Shit => "Fuck" }
139
+ end
140
+
141
+ def address=(input_address)
142
+ raise InvalidAddress unless input_address.is_a?(Address)
143
+ @address = input_address
144
+ end
145
+
146
+ def rbcharge=(charge)
147
+ raise InvalidRecurringCharge unless charge.is_a?(RecurringCharge)
148
+ @rbcharge = charge
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,137 @@
1
+ module RubyPsigate
2
+ class AccountManagerApi
3
+
4
+ include RubyPsigate::Utils
5
+
6
+ TEST_URL = 'https://dev.psigate.com:8645/Messenger/AMMessenger'
7
+ LIVE_URL = nil # TODO
8
+
9
+ attr_reader :action
10
+ attr_accessor :test
11
+
12
+ def initialize(options = {})
13
+ requires!(options, :cid, :login, :password)
14
+ @options = options
15
+ @live_url = @options[:live_url]
16
+ end
17
+
18
+ def test?
19
+ @live_url.blank?
20
+ end
21
+
22
+ # Registers a new account
23
+ #
24
+ # Options (* asterisk denotes required)
25
+ # => :name*
26
+ # => :company
27
+ # => :address1*
28
+ # => :address2
29
+ # => :city*
30
+ # => :province*
31
+ # => :postal_code*
32
+ # => :country*
33
+ # => :phone*
34
+ # => :fax
35
+ # => :email*
36
+ # => :comments
37
+ # => :card_holder*
38
+ # => :card_number*
39
+ # => :card_exp_month*
40
+ # => :card_exp_year*
41
+ #
42
+ def register(options = {})
43
+ requires!(options,
44
+ :name,
45
+ :address1,
46
+ :city,
47
+ :province,
48
+ :postal_code,
49
+ :country,
50
+ :phone,
51
+ :email,
52
+ :card_holder,
53
+ :card_number,
54
+ :card_exp_month,
55
+ :card_exp_year
56
+ )
57
+ @register_options = options
58
+ @action = "AMA01"
59
+ end
60
+
61
+ # Updates an already registered account
62
+ def update(options = {})
63
+ @action = "AMA02"
64
+ end
65
+
66
+ # Retrieves a summary of a registered account
67
+ def retrieve_summary(options = {})
68
+ requires!(options, :account_id)
69
+ @options.update(options)
70
+ @action = "AMA00"
71
+
72
+ @params = {
73
+ :Request => {
74
+ :CID => @options[:cid],
75
+ :UserID => @options[:login],
76
+ :Password => @options[:password],
77
+ :Action => @action,
78
+ :Condition => {
79
+ :AccountID => @options[:account_id]
80
+ }
81
+ }
82
+ }
83
+
84
+ @data = data_to_be_posted(@params)
85
+ raw_response = post_to_server(@data)
86
+ response = Response.new(successful?(raw_response), message_from(raw_response), raw_response, :test => test?)
87
+ end
88
+
89
+ def enable(options = {})
90
+ @action = "AMA08"
91
+ end
92
+
93
+ def disable(options = {})
94
+ @action = "AMA09"
95
+ end
96
+
97
+ def charge(options = {})
98
+
99
+ end
100
+
101
+ private
102
+
103
+ def data_to_be_posted(params)
104
+ data = Serializer.new(params)
105
+ data.to_xml
106
+ end
107
+
108
+ def post_to_server(data)
109
+ endpoint = test? ? TEST_URL : @live_url
110
+ psigate = ActiveMerchant::Connection.new(endpoint)
111
+
112
+ # Some configurations for ActiveMerchant::Connection instance object
113
+ psigate.verify_peer = true
114
+ psigate.retry_safe = false
115
+ psigate.open_timeout = 60
116
+ psigate.read_timeout = 60
117
+
118
+ # Since all of Psigate's requests are POST requests, we will use only the following
119
+ psigate.request(:post, data)
120
+ end
121
+
122
+ def successful?(response)
123
+ response[:approved] == "APPROVED"
124
+ end
125
+
126
+ def message_from(response)
127
+ if response[:approved] == "APPROVED"
128
+ return SUCCESS_MESSAGE
129
+ else
130
+ return FAILURE_MESSAGE if response[:errmsg].blank?
131
+ return response[:errmsg].gsub(/[^\w]/, ' ').split.join(" ").capitalize
132
+ end
133
+ end
134
+
135
+
136
+ end
137
+ end
@@ -0,0 +1,5 @@
1
+ module RubyPsigate
2
+ module AccountMethods
3
+
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ module RubyPsigate
2
+ # DOC - TODO
3
+ class Address
4
+
5
+ include RubyPsigate::HashVariables
6
+
7
+ hashable :billing, %w( Bname Bcompany Baddress1 Baddress2 Bcity Bprovince Bpostalcode Bcountry Phone Fax )
8
+ hashable :shipping, %w( Sname Scompany Saddress1 Saddress2 Scity Sprovince Spostalcode Scountry )
9
+
10
+ attr_accessor :firstname, :lastname, :line1, :line2, :city, :state, :country, :zipcode, :telephone, :fax, :company
11
+
12
+ alias_method :province, :state
13
+ alias_method :province=, :state=
14
+
15
+ alias_method :postalcode, :zipcode
16
+ alias_method :postalcode=, :zipcode=
17
+
18
+ alias_method :address1, :line1
19
+ alias_method :address2, :line2
20
+
21
+ alias_method :phone, :telephone
22
+
23
+ def name
24
+ "#{firstname} #{lastname}".strip
25
+ end
26
+
27
+ def to_hash(type = nil)
28
+ result = super
29
+ result = result.delete_if { |key, value| value.nil? } # Delete empty hash values
30
+ result
31
+ end
32
+
33
+ # For billing
34
+ alias_method :bname, :name
35
+ alias_method :bcompany, :company
36
+ alias_method :baddress1, :address1
37
+ alias_method :baddress2, :address2
38
+ alias_method :bcity, :city
39
+ alias_method :bprovince, :province
40
+ alias_method :bpostalcode, :postalcode
41
+ alias_method :bcountry, :country
42
+
43
+ # For shipping
44
+ alias_method :sname, :name
45
+ alias_method :scompany, :company
46
+ alias_method :saddress1, :address1
47
+ alias_method :saddress2, :address2
48
+ alias_method :scity, :city
49
+ alias_method :sprovince, :province
50
+ alias_method :spostalcode, :postalcode
51
+ alias_method :scountry, :country
52
+
53
+ end
54
+ end
@@ -0,0 +1,96 @@
1
+ module RubyPsigate
2
+
3
+ class Connection
4
+
5
+ MAX_RETRIES = 3
6
+ RETRY_SAFE = true
7
+
8
+ # => Values in seconds
9
+ READ_TIMEOUT = 60
10
+ OPEN_TIMEOUT = 30
11
+ VERIFY_PEER = true
12
+
13
+ def initialize(endpoint)
14
+ @endpoint = endpoint.is_a?(URI) ? endpoint : URI.parse(endpoint)
15
+ @retry_safe = RETRY_SAFE
16
+ @read_timeout = READ_TIMEOUT
17
+ @open_timeout = OPEN_TIMEOUT
18
+ @verify_peer = VERIFY_PEER
19
+ end
20
+
21
+ attr_reader :endpoint
22
+ attr_accessor :retry_safe
23
+ attr_accessor :read_timeout
24
+ attr_accessor :open_timeout
25
+ attr_accessor :verify_peer
26
+
27
+ def post(params)
28
+ retry_exceptions do
29
+ begin
30
+
31
+ uri = @endpoint
32
+ psigate = Net::HTTP.new(uri.host, uri.port)
33
+
34
+ # Configure Timeouts
35
+ psigate.open_timeout = open_timeout
36
+ psigate.read_timeout = read_timeout
37
+
38
+ # Configure SSL
39
+ psigate.use_ssl = true
40
+ if verify_peer
41
+ psigate.verify_mode = OpenSSL::SSL::VERIFY_PEER
42
+ psigate.ca_file = ca_file
43
+ else
44
+ psigate.verify_mode = OpenSSL::SSL::VERIFY_NONE
45
+ end
46
+
47
+ raw_response = psigate.post(uri.path, params)
48
+ response = handle_response(raw_response)
49
+ response
50
+ rescue EOFError => e
51
+ raise ConnectionError, "The remote server dropped the connection"
52
+ rescue Errno::ECONNRESET => e
53
+ raise ConnectionError, "The remote server reset the connection"
54
+ rescue Errno::ECONNREFUSED => e
55
+ raise RetriableConnectionError, "The remote server refused the connection"
56
+ rescue Timeout::Error, Errno::ETIMEDOUT => e
57
+ raise ConnectionError, "The connection to the remote server timed out"
58
+
59
+ end # begin
60
+ end # retry_exceptions
61
+ end
62
+
63
+ private
64
+
65
+ def ca_file
66
+ ca_file = File.dirname(__FILE__) + '/../certs/cacert.pem'
67
+ raise CertVerificationFileMissingError unless File.exists?(ca_file)
68
+ ca_file
69
+ end
70
+
71
+ def handle_response(raw_response)
72
+ case raw_response.code.to_i
73
+ when 200..300
74
+ raw_response.body
75
+ else
76
+ raise ResponseError.new(raw_response)
77
+ end
78
+ end
79
+
80
+ def retry_exceptions(&block)
81
+ retries = MAX_RETRIES
82
+ begin
83
+ yield
84
+ rescue RetriableConnectionError => e
85
+ retries -= 1
86
+ retry unless retries.zero?
87
+ raise ConnectionError, e.message
88
+ rescue ConnectionError
89
+ retries -= 1
90
+ retry if retry_safe && !retries.zero?
91
+ raise
92
+ end
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,104 @@
1
+ module RubyPsigate
2
+ # This class acts as a standalone object to verify that the credit card number is valid
3
+ # If the credit card number is not valid, RubyPsigate::CreditCardInvalidError will be raised
4
+ #
5
+ #
6
+
7
+ class CreditCardNumberInvalid < RubyPsigateError; end
8
+ class CreditCardExpired < RubyPsigateError; end
9
+ class CreditCardExpiryInvalid < RubyPsigateError; end
10
+ class CreditCardOwnerNameMissing < RubyPsigateError; end
11
+ class CreditCardNotSupported < RubyPsigateError; end
12
+
13
+ class CreditCard
14
+
15
+ include RubyPsigate::Utils
16
+ include RubyPsigate::HashVariables
17
+
18
+ hashable %w( PaymentType CardNumber CardExpMonth CardExpYear CardIDNumber )
19
+ hashable :card_info, %w( CardHolder CardNumber CardExpMonth CardExpYear )
20
+
21
+ attr_accessor :number, :month, :year, :verification_value, :name
22
+ attr_reader :type
23
+
24
+ # Psigate only supports these three types of cards
25
+ VALID_TYPES = %w( visa mastercard american_express )
26
+
27
+ def initialize(options={})
28
+ requires!(options, :number, :month, :year, :name)
29
+ @number = options[:number]
30
+ @month = options[:month]
31
+ @year = options[:year]
32
+ @verification_value = options[:verification_value]
33
+ @name = options[:name]
34
+
35
+ validate_credit_card!
36
+ end
37
+
38
+ # For hashable
39
+ def paymenttype
40
+ "CC"
41
+ end
42
+
43
+ # Returns the last 2 digits of the expiry year
44
+ def cardexpyear
45
+ year[2..3]
46
+ end
47
+
48
+ alias_method :cardnumber, :number
49
+ alias_method :cardexpmonth, :month
50
+ alias_method :cardidnumber, :verification_value
51
+ alias_method :cardholder, :name
52
+ # End for hashable
53
+
54
+ private
55
+
56
+ def validate_credit_card!
57
+ validate_number
58
+ validate_type
59
+ validate_name
60
+ validate_date
61
+ end
62
+
63
+ def validate_number
64
+ raise CreditCardNumberInvalid unless @number.to_s.creditcard?
65
+ end
66
+
67
+ def validate_type
68
+ type = @number.to_s.creditcard_type
69
+ raise CreditCardNotSupported unless VALID_TYPES.include?(type)
70
+ end
71
+
72
+ def validate_name
73
+ raise CreditCardOwnerNameMissing if @name.nil?
74
+ end
75
+
76
+ def validate_date
77
+ raise CreditCardExpiryInvalid unless (valid_month?(@month) && valid_expiry_year?(@year))
78
+ raise CreditCardExpired if expired?
79
+ end
80
+
81
+ def valid_month?(month)
82
+ (1..12).include?(month.to_i)
83
+ end
84
+
85
+ def valid_expiry_year?(year)
86
+ (Time.now.year..Time.now.year + 20).include?(year.to_i)
87
+ end
88
+
89
+ def expired?
90
+ today = Time.now
91
+ expiry = Time.new(@year, @month, days_in_a_month(@year, @month))
92
+ today > expiry
93
+ end
94
+
95
+ def days_in_a_month(year, month)
96
+ if month == 12
97
+ year += 1
98
+ next_month = 1
99
+ end
100
+ days = (Time.new(year, next_month)-1).day
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,12 @@
1
+ module RubyPsigate
2
+ module CreditCardMethods
3
+
4
+ attr_reader :cc
5
+
6
+ def cc=(credit_card)
7
+ raise InvalidCreditCard unless credit_card.is_a?(RubyPsigate::CreditCard)
8
+ @cc = credit_card
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ module RubyPsigate
2
+
3
+ class RubyPsigateError < StandardError
4
+ end
5
+
6
+ class ConnectionError < RubyPsigateError
7
+ end
8
+
9
+ class RetriableConnectionError < RubyPsigateError
10
+ end
11
+
12
+ class CertVerificationFileMissingError < RubyPsigateError
13
+ end
14
+
15
+ class ResponseError < RubyPsigateError
16
+ attr_reader :response
17
+
18
+ def initialize(response, message = nil)
19
+ @response = response
20
+ @message = message
21
+ end
22
+
23
+ def to_s
24
+ "Failed with #{response.code} #{response.message if response.respond_to?(:message)}"
25
+ end
26
+ end
27
+
28
+ class LiveURLMissingError < RubyPsigateError
29
+ end
30
+
31
+ class InvalidHashType < RubyPsigateError; end
32
+
33
+ end