Class: Edi::Walmart::ReturnsReportRetriever
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Walmart::ReturnsReportRetriever
- Defined in:
- app/services/edi/walmart/returns_report_retriever.rb
Overview
Service object: returns report retriever.
Pulls Walmart Marketplace customer-return orders via the Returns API
(+GET /v3/returns+) for a creation-date window and lands the JSON on an
+EdiCommunicationLog+ (category +return_batch+) for
ReturnsReportProcessor to apply.
Unlike Amazon's asynchronous report flow (createReport → poll → document),
Walmart serves returns synchronously with cursor pagination
(+meta.nextCursor+), so a run walks every page of the window and stores
the concatenated +returnOrders+ as one log.
Constant Summary collapse
- PAGE_LIMIT =
Returns API page size (the endpoint's documented maximum is 200; its
default is 10, so we always pass it explicitly). 200- MAX_PAGES =
Safety bound on pagination: 50 x 200 = 10,000 returns per window is far
past any realistic daily volume; beyond it we fail (retryable) rather
than store a silently truncated window. 50
Constants included from RequestIdentifiable
RequestIdentifiable::REQUEST_ID_HEADERS
Constants included from AddressAbbreviator
AddressAbbreviator::MAX_LENGTH
Instance Attribute Summary
Attributes inherited from BaseEdiService
Attributes inherited from BaseService
Instance Method Summary collapse
-
#api_host ⇒ String
The Walmart API host from the orchestrator's transport profile.
-
#fetch_all_pages(transport, start_time, end_time) ⇒ Array(Array<Hash>, Integer)?
Walks every page of the window following +meta.nextCursor+.
-
#fetch_page(transport, url) ⇒ Array(Array<Hash>, String, nil), Symbol
+[return_orders, next_cursor]+ on success, +:failed+ on any failure.
-
#first_page_url(start_time, end_time) ⇒ String
The first-page URL with the creation-date filter.
- #instantiate_transporter(transporter, transporter_profile = nil) ⇒ Transport::HttpWalmartSellerApiConnection
-
#process(start_time: nil, end_time: nil) ⇒ Array<EdiCommunicationLog>?
Retrieves the returns for the window and stores them as an ECL.
-
#resolve_window(start_time, end_time) ⇒ Array(Time, Time)
Resolves the report window, defaulting to the previous day (UTC).
-
#returns_url ⇒ String
The Returns API endpoint from the orchestrator's transport profile host (same host the other Walmart v3 paths use).
-
#store_log(return_orders, file_name, start_time, end_time, pages) ⇒ EdiCommunicationLog
Lands the concatenated return orders on an +EdiCommunicationLog+ for the processor to pick up.
Methods inherited from BaseEdiService
#amazon_feed_product_type, #duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #onboard_ordered_catalog_items, #report_order_creation_issues, #safe_process_edi_communication_log
Methods included from RequestIdentifiable
Methods included from AddressAbbreviator
#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes
Methods inherited from BaseService
#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger
Constructor Details
This class inherits a constructor from Edi::BaseEdiService
Instance Method Details
#api_host ⇒ String
Returns the Walmart API host from the orchestrator's transport profile.
173 174 175 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 173 def api_host Heatwave::Configuration.fetch(orchestrator.transporter_profile&.to_sym, :api_host) end |
#fetch_all_pages(transport, start_time, end_time) ⇒ Array(Array<Hash>, Integer)?
Walks every page of the window following +meta.nextCursor+.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 85 def fetch_all_pages(transport, start_time, end_time) url = first_page_url(start_time, end_time) return_orders = [] MAX_PAGES.times do |page| result = fetch_page(transport, url) return nil if result == :failed page_orders, next_cursor = result return_orders.concat(page_orders) return [return_orders, page + 1] if next_cursor.blank? url = "#{returns_url}#{next_cursor.to_s.start_with?('?') ? next_cursor : "?#{next_cursor}"}" end logger.warn "Returns fetch for #{orchestrator.partner} exceeded #{MAX_PAGES} pages; giving up (next run will retry the window)" nil end |
#fetch_page(transport, url) ⇒ Array(Array<Hash>, String, nil), Symbol
Returns +[return_orders,
next_cursor]+ on success, +:failed+ on any failure.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 106 def fetch_page(transport, url) res = transport.send_data('', url, 'GET') unless res[:success] logger.warn "GET #{url} failed for #{orchestrator.partner}: #{res[:http_result]&.body.to_s[0, 500]}" return :failed end payload = JSON.parse(res[:http_result].body.to_s).with_indifferent_access [Array.wrap(payload[:returnOrders]), payload.dig(:meta, :nextCursor).presence] rescue JSON::ParserError => e logger.warn "GET #{url} returned unparseable JSON for #{orchestrator.partner}: #{e.}" :failed rescue HTTP::RateLimitExceededError => e logger.warn "Walmart rate limited fetching returns for #{orchestrator.partner}. Giving up this run; next run will retry the window. #{e.}" :failed end |
#first_page_url(start_time, end_time) ⇒ String
Returns the first-page URL with the creation-date filter.
157 158 159 160 161 162 163 164 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 157 def first_page_url(start_time, end_time) query = URI.encode_www_form( returnCreationStartDate: start_time.utc.iso8601, returnCreationEndDate: end_time.utc.iso8601, limit: PAGE_LIMIT ) "#{returns_url}?#{query}" end |
#instantiate_transporter(transporter, transporter_profile = nil) ⇒ Transport::HttpWalmartSellerApiConnection
181 182 183 184 185 186 187 188 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 181 def instantiate_transporter(transporter, transporter_profile = nil) case transporter when :http_walmart_seller_api Transport::HttpWalmartSellerApiConnection.new(profile: transporter_profile) else raise "Unknown transporter: #{transporter}" end end |
#process(start_time: nil, end_time: nil) ⇒ Array<EdiCommunicationLog>?
Retrieves the returns for the window and stores them as an ECL.
Idempotent per partner + window: the ECL file_name is deterministic
(+returns_YYYY-MM-DD_YYYY-MM-DD.json+), so a re-run over the same window
returns the already-stored log instead of duplicating it. The create is
additionally guarded by the partial unique index on
(partner, category, file_name) WHERE category='return_batch' — a
concurrent run that wins the race is reused, not duplicated.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 43 def process(start_time: nil, end_time: nil) start_time, end_time = resolve_window(start_time, end_time) file_name = "returns_#{start_time.to_date.iso8601}_#{end_time.to_date.iso8601}.json" if (existing = EdiCommunicationLog.find_by(partner: orchestrator.partner, category: 'return_batch', file_name: file_name)) logger.info "#{file_name} already stored as edi log #{existing.id}; skipping" return [existing] end transport = instantiate_transporter(orchestrator.transporter, orchestrator.transporter_profile) fetched = fetch_all_pages(transport, start_time, end_time) return nil if fetched.nil? # failure — leave the window retryable return_orders, pages = fetched return [] if return_orders.empty? # valid empty window [store_log(return_orders, file_name, start_time, end_time, pages)] rescue StandardError => e # Transport errors (timeouts, connection failures) and store failures # must also leave the window retryable, not escape as an unhandled # exception — the caller only pins/gates on a nil result. logger.warn "Returns retrieval failed for #{orchestrator.partner} (#{file_name}): #{e.class}: #{e.} — leaving the window retryable" nil end |
#resolve_window(start_time, end_time) ⇒ Array(Time, Time)
Resolves the report window, defaulting to the previous day (UTC).
72 73 74 75 76 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 72 def resolve_window(start_time, end_time) start_time = (start_time.presence || 1.day.ago.beginning_of_day).to_time end_time = (end_time.presence || start_time.end_of_day).to_time [start_time, end_time] end |
#returns_url ⇒ String
Returns the Returns API endpoint from the orchestrator's
transport profile host (same host the other Walmart v3 paths use).
168 169 170 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 168 def returns_url "https://#{api_host}/v3/returns" end |
#store_log(return_orders, file_name, start_time, end_time, pages) ⇒ EdiCommunicationLog
Lands the concatenated return orders on an +EdiCommunicationLog+ for the
processor to pick up.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'app/services/edi/walmart/returns_report_retriever.rb', line 131 def store_log(return_orders, file_name, start_time, end_time, pages) edi_log = EdiCommunicationLog.create!( partner: orchestrator.partner, category: 'return_batch', data: { meta: { totalCount: return_orders.size, limit: PAGE_LIMIT }, returnOrders: return_orders }.to_json, data_type: 'json', file_name: file_name, file_info: { window_start: start_time.utc.iso8601, window_end: end_time.utc.iso8601, return_orders_count: return_orders.size, pages: pages }, transmit_datetime: Time.current ) logger.info "#{file_name} saved to edi log #{edi_log.id}" edi_log rescue ActiveRecord::RecordNotUnique # A concurrent run stored the same window first (partial unique index on # (partner, category, file_name) WHERE category='return_batch') — reuse it. EdiCommunicationLog.find_by!(partner: orchestrator.partner, category: 'return_batch', file_name: file_name) end |