Class: Edi::Walmart::ReturnsReportRetriever

Inherits:
BaseEdiService show all
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

#orchestrator

Attributes inherited from BaseService

#options

Instance Method Summary collapse

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

#partner_request_id

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_hostString

Returns the Walmart API host from the orchestrator's transport profile.

Returns:

  • (String)

    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+.

Parameters:

Returns:

  • (Array(Array<Hash>, Integer), nil)

    +[return_orders, page_count]+,
    or nil on any page failure (retryable — never store a partial window).



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.

Parameters:

Returns:

  • (Array(Array<Hash>, String, nil), Symbol)

    +[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.message}"
  :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.message}"
  :failed
end

#first_page_url(start_time, end_time) ⇒ String

Returns the first-page URL with the creation-date filter.

Parameters:

  • start_time (Time)

    window start.

  • end_time (Time)

    window end.

Returns:

  • (String)

    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

Parameters:

  • transporter (Symbol)
  • transporter_profile (Symbol, nil) (defaults to: nil)

    the partner's transport profile.

Returns:

Raises:

  • (RuntimeError)

    on an unknown transporter key.



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.

Parameters:

  • start_time (Time, Date, String, nil) (defaults to: nil)

    window start (default:
    previous day, beginning of day); parameterizable for backfills.

  • end_time (Time, Date, String, nil) (defaults to: nil)

    window end (default: end of
    the start day).

Returns:

  • (Array<EdiCommunicationLog>, nil)

    the stored (or pre-existing)
    log; empty for a valid empty window; nil on failure (API error,
    unparseable payload, rate limit, page bound exceeded) so the caller
    can leave the window retryable instead of gating it.



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.message} — 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).

Parameters:

  • start_time (Time, Date, String, nil)

    explicit window start.

  • end_time (Time, Date, String, nil)

    explicit window end.

Returns:

  • (Array(Time, Time))

    the resolved [start, end] window.



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_urlString

Returns the Returns API endpoint from the orchestrator's
transport profile host (same host the other Walmart v3 paths use).

Returns:

  • (String)

    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.

Parameters:

  • return_orders (Array<Hash>)

    every returnOrder across the window's pages.

  • file_name (String)

    deterministic per-window file name.

  • start_time (Time)

    window start.

  • end_time (Time)

    window end.

  • pages (Integer)

    how many API pages the window spanned.

Returns:



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