Module: PayHyper

Defined in:
lib/payhyper.rb,
lib/payhyper/json.rb,
lib/payhyper/version.rb,
lib/payhyper/security.rb

Defined Under Namespace

Modules: JSON, Security Classes: CommunicationError, PayHyperError, ValidationError

Constant Summary collapse

BASE_URLS =
{
    :live => "http://api.local.local:3000",
    :sandbox => "http://sandbox-api.local.local:3001",
}
CURRENCIES =
%w{JOD USD BHD GBP EGP KWD OMR QAR SAR TRY AED PKR IQD}
COUNTRY_CODES =
{
  "JO" => "962",
  "SA" => "966",
  "IQ" => "964",
  "PK" => "92",
}
COUNTRIES =
COUNTRY_CODES.keys
VERSION =
'0.4.4'

Class Method Summary collapse

Class Method Details

.at_door!(name, phone, email, country, city, amount, currency, address = nil, invoice = nil, tag = nil, stickiness_id = nil, details = nil, location = nil) ⇒ Object

TODO: There is a lot of overlap with “in_store!”, refactor.

Raises:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/payhyper.rb', line 41

def self.at_door!(name, phone, email, country, city, amount, currency, address = nil, invoice = nil, tag = nil, stickiness_id = nil, details = nil, location = nil) # TODO: There is a lot of overlap with "in_store!", refactor.
  raise_if_not_setup!
  # == Clean-up fields ==
  currency = currency.upcase if currency && currency.is_a?(String)
  country = country.upcase if country && country.is_a?(String)
  city = city.upcase if city && city.is_a?(String)
  if phone && phone.is_a?(String)
    phone = phone.gsub(/[^0-9]/, "") # Remove non-numeric characters.
    phone = phone.gsub(/^0*/, "") # Remove leading zeros.
    if country && COUNTRY_CODES[country] && !phone.start_with?(COUNTRY_CODES[country])
      phone = COUNTRY_CODES[country] + phone # Add country code.
    end
  end
  # == Validate ==
  raise ValidationError, "Country specified is incorrect or not supported." unless COUNTRIES.include?(country)
  raise ValidationError, "Incorrect amount, must be positive." if amount.to_i <= 0
  raise ValidationError, "Currency is incorrect or not supported." unless CURRENCIES.include?(currency)
  raise ValidationError, "Incorrect phone, or not in a supported country." if phone.nil? || !phone.match(/\A#{COUNTRY_CODES[country]}[0-9]{8,#{15-COUNTRY_CODES[country].length}}\z/)
  raise ValidationError, "Invalid email." if email.nil? || !email.match(/\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/)
  raise ValidationError, "Name is mandatory." if name.nil? || name.strip.length == 0
  # == Do the request ==
  make_call("/v1/at-door", { :name => name, :phone => phone, :email => email, :country => country, :city => city, :amount => amount, :currency => currency, :address => address, :invoice => invoice, :tag => tag, :stickiness_id => stickiness_id, :details => details, :location => location })
end

.check_ani_authenticity(request) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/payhyper.rb', line 110

def self.check_ani_authenticity(request)
  authorization = request.env["HTTP_AUTHORIZATION"]
  return false unless authorization
  matches = authorization.match(/\A(.*) (.*):(.*)/)
  return false unless matches
  method, access_key_id, input_signature = matches.captures
  return false unless method == "Hyper" && access_key_id && input_signature && access_key_id == @access_key_id
  request.env["rack.input"].rewind # In case someone forgot to rewind the input.
  body = request.env["rack.input"].read
  request.env["rack.input"].rewind # Be nice to others.
  canonical_request_representation = [request.env["REQUEST_METHOD"], request.env["HTTP_HOST"], request.env["PATH_INFO"], request.env["CONTENT_TYPE"], body].join("\n") # TODO: security bug if the webserver doesn't check host headers.
  correct_signature = Security.sign(@access_key_secret, canonical_request_representation)
  return Security.secure_compare(correct_signature, input_signature)
end

.in_store!(name, phone, email, country, amount, currency, tag = nil) ⇒ Object

TODO: There is a lot of overlap with “at_door!”, refactor.

Raises:



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/payhyper.rb', line 65

def self.in_store!(name, phone, email, country, amount, currency, tag = nil) # TODO: There is a lot of overlap with "at_door!", refactor.
  raise_if_not_setup!
  # == Clean-up fields ==
  currency = currency.upcase if currency && currency.is_a?(String)
  country = country.upcase if country && country.is_a?(String)
  if phone && phone.is_a?(String)
    phone = phone.gsub(/[^0-9]/, "") # Remove non-numeric characters.
    phone = phone.gsub(/^0*/, "") # Remove leading zeros.
    if country && COUNTRY_CODES[country] && !phone.start_with?(COUNTRY_CODES[country])
      phone = COUNTRY_CODES[country] + phone # Add country code.
    end
  end
  # == Validate ==
  raise ValidationError, "Country specified is incorrect or not supported." unless COUNTRIES.include?(country)
  raise ValidationError, "Incorrect amount, must be positive." if amount.to_i <= 0
  raise ValidationError, "Currency is incorrect or not supported." unless CURRENCIES.include?(currency)
  raise ValidationError, "Incorrect phone, or not in a supported country." if phone.nil? || !phone.match(/\A962[0-9]{8,9}\z/)
  raise ValidationError, "Invalid email." if email.nil? || !email.match(/\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/)
  raise ValidationError, "Name is mandatory." if name.nil? || name.strip.length == 0
  # == Do the request ==
  make_call("/v1/in-store", { :name => name, :phone => phone, :email => email, :country => country, :amount => amount, :currency => currency, :tag => tag })
end

.make_call(endpoint, body) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/payhyper.rb', line 125

def self.make_call(endpoint, body)
  body = JSON.dump(body)
  failure = "Unknown error"
  begin
    uri = URI.parse(@base_url + endpoint)
    content_type = "application/json; charset=utf-8"
    signature = Security.sign(@access_key_secret, ["POST", uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}", uri.request_uri, content_type, body.encode("UTF-8")].join("\n")) # Would be great if this could read directly from the request's headers.
    res = RestClient.post(@base_url + endpoint, body, :content_type => content_type, :authorization => "Hyper #{@access_key_id}:#{signature}")
    failure = false
  rescue RestClient::Exception => e # HTTP status codes not in 200-207, 301-303 and 307 result in a RestClient::Exception.
    if e.http_code && e.http_code.to_i == 422 && e.http_body
      raise ValidationError, JSON.load(e.http_body)["error"]
    else
      failure = "Remote returned HTTP status code #{e.http_code} (#{e.http_body})"
    end
  rescue SocketError => e
    if e.message.match(/getaddrinfo: (.*?)/)
      failure = "Problem getting address information, possibly due to an incorrect domain name"
    else
      failure = "Unknown socket error"
    end
  rescue Errno::EINVAL => e
    failure = "Incorrect host address"
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
    failure = "Connection refused, or host unreachable"
  rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
    failure = "Problem parsing HTTP response"
  rescue EOFError, Errno::ECONNRESET => e
    failure = "Remote closed connection unexpectedly"
  end
  if failure
    raise CommunicationError, failure
  else
    JSON.load(res)
  end
end

.parse_notification(request) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/payhyper.rb', line 88

def self.parse_notification(request)
  raise_if_not_setup!
  if check_ani_authenticity(request)
    request.env["rack.input"].rewind # In case someone forgot to rewind the input.
    body = request.env["rack.input"].read
    request.env["rack.input"].rewind # Be nice to others.
    JSON.load(body)
  else
    nil
  end
end

.raise_if_not_setup!Object

Raises:



106
107
108
# File 'lib/payhyper.rb', line 106

def self.raise_if_not_setup!
  raise PayHyperError, "Must call setup() first" if @access_key_id.nil? || @access_key_secret.nil? || @base_url.nil?
end

.setup(access_key_id, access_key_secret, mode) ⇒ Object

Raises:



34
35
36
37
38
39
# File 'lib/payhyper.rb', line 34

def self.setup(access_key_id, access_key_secret, mode)
  raise PayHyperError, "Mode must be one of #{BASE_URLS.keys.map { |k| k.inspect }.join(" or ")}" unless BASE_URLS.keys.include?(mode)
  @access_key_id = access_key_id
  @access_key_secret = access_key_secret
  @base_url = BASE_URLS[mode]
end