Class: Transport::HttpWalmartSellerApiConnection
- Inherits:
-
Object
- Object
- Transport::HttpWalmartSellerApiConnection
- Defined in:
- app/services/transport/http_walmart_seller_api_connection.rb
Overview
Service object: http walmart seller api connection.
Constant Summary collapse
- VALID_HTTP_METHODS =
Valid http methods.
%w[get put post patch delete head].freeze
Instance Attribute Summary collapse
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#profile ⇒ Object
readonly
Returns the value of attribute profile.
Instance Method Summary collapse
-
#fetch_access_token ⇒ String
Fetches a short-lived Walmart access token (client-credentials grant) for the +WM_SEC.ACCESS_TOKEN+ header.
-
#initialize(options = {}) ⇒ HttpWalmartSellerApiConnection
constructor
A new instance of HttpWalmartSellerApiConnection.
-
#send_authenticated_request(token, method, data, url, headers = {}) ⇒ Hash
Merges the Walmart authentication and QOS headers (access token, correlation id, service name, and the Canada-specific WM_MARKET / WM_CONSUMER.CHANNEL.TYPE) onto the caller's headers, then issues the request via #send_request.
-
#send_data(data, url, method, headers = {}) ⇒ Hash
Sends +data+ to a Walmart Marketplace endpoint, authenticating with a freshly fetched access token, and returns the parsed result envelope.
-
#send_feed_data(data, url, method = 'POST', headers = {}) ⇒ Hash
Uploads +data+ as a multipart feed file.
-
#send_request(method, url, data = '', headers = {}) ⇒ Hash
Issues a single request through the Faraday #connection, letting the +:retry+ middleware handle transient-failure and 429 retries (Retry-After aware).
-
#successful?(http_res) ⇒ Boolean
Whether +http_res+ is a successful Walmart response: a 2xx status with no +errors+ array in a JSON body.
Constructor Details
#initialize(options = {}) ⇒ HttpWalmartSellerApiConnection
Returns a new instance of HttpWalmartSellerApiConnection.
19 20 21 22 23 24 25 26 27 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 19 def initialize( = {}) @options = if [:profile_data] @profile = [:profile_data] elsif (profile = @options[:profile]) @profile = Heatwave::Configuration.fetch(profile&.to_sym) end @logger = [:logger] || Rails.logger end |
Instance Attribute Details
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
17 18 19 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 17 def logger @logger end |
#profile ⇒ Object (readonly)
Returns the value of attribute profile.
17 18 19 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 17 def profile @profile end |
Instance Method Details
#fetch_access_token ⇒ String
Fetches a short-lived Walmart access token (client-credentials grant) for the
+WM_SEC.ACCESS_TOKEN+ header. The detached block above shows a sample
request/response.
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 242 def fetch_access_token auth_payload = 'grant_type=client_credentials' headers = { 'accept' => 'application/json', 'Authorization' => ActionController::HttpAuthentication::Basic.encode_credentials(profile[:client_id], profile[:client_secret]), 'Content-Type' => 'application/x-www-form-urlencoded', 'WM_QOS.CORRELATION_ID' => SecureRandom.uuid, 'WM_SVC.NAME' => 'Walmart Marketplace' } # Add WM_MARKET header for non-US markets (e.g., 'ca' for Canada) # See: https://developer.walmart.com/ca-marketplace/docs/authentication headers['WM_MARKET'] = profile[:market] if profile[:market].present? # Add WM_CONSUMER.CHANNEL.TYPE header (required for Canada) headers['WM_CONSUMER.CHANNEL.TYPE'] = profile[:channel_type] if profile[:channel_type].present? res = send_request('POST', @profile[:auth_url], auth_payload, headers) response = res[:http_res] # Faraday::Response#to_s is NOT the body (http.rb's was) — read #body. JSON.parse(response.body)['access_token'] end |
#send_authenticated_request(token, method, data, url, headers = {}) ⇒ Hash
Merges the Walmart authentication and QOS headers (access token, correlation
id, service name, and the Canada-specific WM_MARKET / WM_CONSUMER.CHANNEL.TYPE)
onto the caller's headers, then issues the request via #send_request.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 142 def send_authenticated_request(token, method, data, url, headers = {}) # See: https://developer.walmart.com/us-marketplace/reference/tokenapi ac = headers.dig('accept') || 'application/json' # allow accept in headers to prevail ct = headers.dig('Content-Type') || 'application/json' # allow Content-Type in headers to prevail headers_for_request = headers.merge({ 'WM_SEC.ACCESS_TOKEN' => token, 'WM_QOS.CORRELATION_ID' => SecureRandom.uuid, 'WM_SVC.NAME' => 'Walmart Marketplace', 'accept' => ac, 'Content-Type' => ct }) # Add WM_MARKET header for non-US markets (e.g., 'ca' for Canada) # See: https://developer.walmart.com/ca-marketplace/docs/authentication headers_for_request['WM_MARKET'] = profile[:market] if profile[:market].present? # Add WM_CONSUMER.CHANNEL.TYPE header (required for Canada) headers_for_request['WM_CONSUMER.CHANNEL.TYPE'] = profile[:channel_type] if profile[:channel_type].present? send_request(method, url, data, headers_for_request) end |
#send_data(data, url, method, headers = {}) ⇒ Hash
Sends +data+ to a Walmart Marketplace endpoint, authenticating with a freshly
fetched access token, and returns the parsed result envelope.
38 39 40 41 42 43 44 45 46 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 38 def send_data(data, url, method, headers = {}) t = fetch_access_token URI.parse(url) res = send_authenticated_request(t, method, data, url, headers) http_res = res[:http_res] attempt_number_reached = res[:attempt_number_reached] logger.debug { "Walmart Seller API request complete url=#{url} method=#{method} attempts=#{attempt_number_reached} status=#{http_res&.status}" } { success: successful?(http_res), http_result: http_res, attempt_number_reached: attempt_number_reached } end |
#send_feed_data(data, url, method = 'POST', headers = {}) ⇒ Hash
Uploads +data+ as a multipart feed file. The Walmart bulk-feed endpoints
require +multipart/form-data+, so the payload is written to a Tempfile and
sent as a Faraday::Multipart::FilePart (which faraday-retry rewinds and
re-sends on retry).
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 59 def send_feed_data(data, url, method = 'POST', headers = {}) t = fetch_access_token URI.parse(url) headers_for_feed_file_post = headers.merge({ 'accept' => 'application/json', 'Content-Type' => 'multipart/form-data' }) # Build JSON file to send filename = "walmart_feed_#{Time.current.to_i}.json" # Create a temporary file with the JSON data tempfile = Tempfile.new(filename) begin tempfile.write(data) tempfile.rewind tempfile.flush tempfile.fsync # Faraday multipart part — 'application/octet-stream' matches what # HTTP::FormData::File produced for this .json feed file before the migration. file = Faraday::Multipart::FilePart.new(tempfile.path, 'application/octet-stream', filename) feed_data = { file: file } res = send_authenticated_request(t, method, feed_data, url, headers_for_feed_file_post) http_res = res[:http_res] attempt_number_reached = res[:attempt_number_reached] logger.debug { "Walmart Seller API request complete url=#{url} method=#{method} attempts=#{attempt_number_reached} status=#{http_res&.status}" } { success: successful?(http_res), http_result: http_res, attempt_number_reached: attempt_number_reached } ensure tempfile.close tempfile.unlink # Clean up the temporary file end end |
#send_request(method, url, data = '', headers = {}) ⇒ Hash
Issues a single request through the Faraday #connection, letting the +:retry+
middleware handle transient-failure and 429 retries (Retry-After aware). A 429
that survives the in-band retries is raised so the job reschedules.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 172 def send_request(method, url, data = '', headers = {}) # The :retry middleware on #connection now does what the hand-rolled rescue # loop did: it retries the timeout classes AND 429s in-band. Unlike the old # fixed 15+15·n sleep, faraday-retry honors the server's Retry-After header # (rate_limit_retry_header) for 429s. retry_block counts attempts. attempts = 1 http_res = connection(headers, retry_block: ->(**) { attempts += 1 }).run_request(method.downcase.to_sym, url, request_body(data), nil) # A 429 that survives the in-band retries still bubbles up so the job # reschedules — same contract as before (callers don't rescue it), now # Retry-After-aware. Throttling is per SP-API profile/credential and can # persist; better to fail and let the worker retry than spin here. if http_res.status == 429 retry_after = http_res.headers['Retry-After']&.to_i raise HTTP::RateLimitExceededError.new( status_code: 429, headers: http_res.headers, retry_after: retry_after, message: "429 returned with result headers: #{http_res.headers.inspect} after #{attempts} attempt(s), retry after #{retry_after}" ) end { http_res: http_res, attempt_number_reached: attempts } end |
#successful?(http_res) ⇒ Boolean
Whether +http_res+ is a successful Walmart response: a 2xx status with no
+errors+ array in a JSON body. A non-JSON 2xx body (e.g. a label PDF) is
treated as success on status alone.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'app/services/transport/http_walmart_seller_api_connection.rb', line 112 def successful?(http_res) res = false errs = [] # Only try to parse JSON if Content-Type indicates JSON # For binary responses (PDF, images, etc.), check HTTP status code only content_type = http_res.headers['Content-Type'].to_s if content_type.include?('application/json') && http_res.body.present? begin errs = JSON.parse(http_res.body.to_s).with_indifferent_access[:errors] || [] rescue JSON::ParserError # If JSON parsing fails, treat as non-JSON response (e.g., PDF) errs = [] end end code = http_res.status.to_i res = true if (code >= 200) && (code < 300) && errs.empty? res end |