Class: Edi::Amazon::FeedMessageSender
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Amazon::FeedMessageSender
- Defined in:
- app/services/edi/amazon/feed_message_sender.rb
Overview
Service object: feed message sender.
Direct Known Subclasses
InventoryMessageSender, ListingMessageFeedSender, ListingMessageSender, PriceMessageSender
Constant Summary
Constants included from RequestIdentifiable
RequestIdentifiable::REQUEST_ID_HEADERS
Constants included from Edi::AddressAbbreviator
Edi::AddressAbbreviator::MAX_LENGTH
Instance Attribute Summary
Attributes inherited from BaseEdiService
Attributes inherited from BaseService
Instance Method Summary collapse
-
#claim_budget(ecl) ⇒ Boolean
False when this account has spent its window's budget.
-
#create_feed(ecl, feed_transport, feed_document_id) ⇒ void
STEP 4: create the feed itself, binding the uploaded document to the marketplace.
-
#defer(ecl, seconds, reason) ⇒ Object
Reschedules an unsent ECL and RECORDS why.
- #ecl_in_queue ⇒ Object
-
#full_snapshot_feed? ⇒ Boolean
Whether every message of this feed category carries the COMPLETE state, so an older unsent one is superseded rather than owed.
-
#instantiate_transporter(transporter, transporter_profile = nil, options = {}) ⇒ Transport::HttpSellerApiConnection, Transport::HttpApiUploadConnection
Builds the transport connection for the given transporter type.
- #process(edi_communication_logs = nil) ⇒ Object
-
#submit_feed(ecl, feed_transport) ⇒ void
The four-step SP-API feed dance, extracted from #process so the queue loop reads as a queue loop.
-
#supersede_stale(ecls) ⇒ Array<EdiCommunicationLog>
Each full-snapshot message carries the complete state, so an older unsent one is stale data, not a backlog item — and replaying it is actively harmful: nine EU marketplaces sharing one SP-API account replayed 90+ superseded snapshots an hour, spent the account's whole feed allowance on data from July, and 429'd the snapshot that mattered.
-
#upload_feed_document(ecl, url, content_type) ⇒ Boolean
STEP 2 & 3: construct and upload the feed document to the signed URL returned by step 1 — no auth needed on that URL.
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 Edi::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
#claim_budget(ecl) ⇒ Boolean
Returns false when this account has spent its window's budget.
182 183 184 185 186 187 188 189 190 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 182 def claim_budget(ecl) return true if FeedSubmissionBudget.claim!(orchestrator.transporter_profile) defer(ecl, FeedSubmissionBudget::PERIOD.to_i, "Deferred: #{orchestrator.transporter_profile} feed budget spent for this window") logger.info "Deferring ECL #{ecl.id} (#{feed_category}/#{orchestrator.partner}) — " \ "#{orchestrator.transporter_profile} budget spent" false end |
#create_feed(ecl, feed_transport, feed_document_id) ⇒ void
This method returns an undefined value.
STEP 4: create the feed itself, binding the uploaded document to the
marketplace. On success the returned feedId is the handle
Edi::Amazon::FeedSubmissionResultProcessor later polls.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 141 def create_feed(ecl, feed_transport, feed_document_id) data = %( { "feedType":"#{feed_type}", "marketplaceIds": ["#{orchestrator.marketplace}"], "inputFeedDocumentId": "#{feed_document_id}" } ) res = feed_transport.send_data(data, "#{orchestrator.}/feeds", 'POST') ecl.notes += " | HTTP CODE: #{res[:http_result]&.status}, HTTP BODY: #{res[:http_result]&.body}, REQUEST ID: #{partner_request_id(res[:http_result])}, Timestamp: #{Time.current.to_datetime.to_fs(:crm_default)}" logger.info "Result: HTTP CODE: #{res[:http_result]&.status}, HTTP BODY: #{res[:http_result]&.body}" return ecl.error unless res[:success] && (body = res[:http_result]&.body.to_s).present? ecl.transaction_id = JSON.parse(body).with_indifferent_access[:feedId] ecl.transmit_datetime = Time.current ecl.start_process! end |
#defer(ecl, seconds, reason) ⇒ Object
Reschedules an unsent ECL and RECORDS why. It stays ready on purpose —
requiring_processing honours transmit_after — but a row that changes
nothing is unreadable: a deferred feed used to look exactly like a sender
that hung, which is how the EU stall hid for seven weeks. update_columns
keeps the state machine out of it, so updated_at is set explicitly.
197 198 199 200 201 202 203 204 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 197 def defer(ecl, seconds, reason) ecl.update_columns( transmit_after: Time.current + seconds, process_attempts: ecl.process_attempts.to_i + 1, notes: "#{reason} — Timestamp: #{Time.current.to_datetime.to_fs(:crm_default)}", updated_at: Time.current ) end |
#ecl_in_queue ⇒ Object
206 207 208 209 210 211 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 206 def ecl_in_queue EdiCommunicationLog.requiring_processing .where(partner: orchestrator.partner) .where(category: feed_category) .order(:created_at) end |
#full_snapshot_feed? ⇒ Boolean
Whether every message of this feed category carries the COMPLETE state, so
an older unsent one is superseded rather than owed. False by default —
ListingMessageSender patches a single item per message, and dropping one
of those loses the edit. Overridden true by the inventory and price senders.
73 74 75 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 73 def full_snapshot_feed? false end |
#instantiate_transporter(transporter, transporter_profile = nil, options = {}) ⇒ Transport::HttpSellerApiConnection, Transport::HttpApiUploadConnection
Builds the transport connection for the given transporter type.
222 223 224 225 226 227 228 229 230 231 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 222 def instantiate_transporter(transporter, transporter_profile = nil, = {}) case transporter when :http_seller_api Transport::HttpSellerApiConnection.new({ profile: transporter_profile }.merge()) when :http_api_upload Transport::HttpApiUploadConnection.new({ profile: transporter_profile }.merge()) else raise "Unknown transporter: #{transporter}" end end |
#process(edi_communication_logs = nil) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 77 def process(edi_communication_logs = nil) ecls = edi_communication_logs ? [edi_communication_logs].flatten : supersede_stale(ecl_in_queue.to_a) return ecls if ecls.empty? feed_transport = instantiate_transporter(orchestrator.transporter, orchestrator.transporter_profile) ecls.each do |ecl| next unless claim_budget(ecl) submit_feed(ecl, feed_transport) rescue HTTP::RateLimitExceededError => e retry_after_seconds = e.retry_after.to_i.positive? ? e.retry_after.to_i : 1.hour.to_i defer(ecl, retry_after_seconds, "Amazon SP-API rate limited (429). #{e.}") logger.warn "Amazon SP-API rate limited for ECL #{ecl.id} (#{feed_category}/#{orchestrator.partner}). " \ "Scheduled retry via transmit_after: #{ecl.transmit_after&.iso8601} " \ "(#{retry_after_seconds}s from now). #{e.}" end ecls end |
#submit_feed(ecl, feed_transport) ⇒ void
This method returns an undefined value.
The four-step SP-API feed dance, extracted from #process so the queue
loop reads as a queue loop. Each step's HTTP result is appended to
+ecl.notes+ and any failure short-circuits to +ecl.error+.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 104 def submit_feed(ecl, feed_transport) logger.info "Sending feed data to #{orchestrator.partner}" # STEP 1 Create Feed Document # VERY IMPORTANT contentType must match Content-Type header in the subsequent upload PUT request exactly! ct = ecl.data_type == 'xml' ? 'text/xml; charset=UTF-8' : 'application/json' res = feed_transport.send_data(%({'contentType':'#{ct}'}), "#{orchestrator.}/documents", 'POST') ecl.notes = "HTTP CODE: #{res[:http_result]&.status}, HTTP BODY: #{res[:http_result]&.body}, HTTP METHOD: 'POST', REQUEST ID: #{partner_request_id(res[:http_result])}, Timestamp: #{Time.current.to_datetime.to_fs(:crm_default)}" logger.info "Result: HTTP CODE: #{res[:http_result]&.status}, HTTP BODY: #{res[:http_result]&.body}" return ecl.error unless res[:success] && (body = res[:http_result]&.body.to_s).present? json_hash = JSON.parse(body).with_indifferent_access return ecl.error unless upload_feed_document(ecl, json_hash[:url], ct) create_feed(ecl, feed_transport, json_hash[:feedDocumentId]) end |
#supersede_stale(ecls) ⇒ Array<EdiCommunicationLog>
Each full-snapshot message carries the complete state, so an older unsent
one is stale data, not a backlog item — and replaying it is actively
harmful: nine EU marketplaces sharing one SP-API account replayed 90+
superseded snapshots an hour, spent the account's whole feed allowance on
data from July, and 429'd the snapshot that mattered. Every one of those
ECLs sat in ready with NULL notes, indistinguishable from a hang.
168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 168 def supersede_stale(ecls) return ecls unless full_snapshot_feed? return ecls if ecls.size <= 1 *stale, current = ecls stale.each do |ecl| ecl.update(notes: "Superseded by ECL #{current.id} — snapshot never sent") ecl.archive end logger.info "Superseded #{stale.size} stale #{feed_category} snapshot(s) for #{orchestrator.partner}" [current] end |
#upload_feed_document(ecl, url, content_type) ⇒ Boolean
STEP 2 & 3: construct and upload the feed document to the signed URL
returned by step 1 — no auth needed on that URL.
VERY IMPORTANT Content-Type header must match contentType in the previous
Create Feed Document request exactly!
126 127 128 129 130 131 132 133 134 |
# File 'app/services/edi/amazon/feed_message_sender.rb', line 126 def upload_feed_document(ecl, url, content_type) # Both transports are now Faraday-backed and expose #status: the # feed_document_uploader (HttpApiUploadConnection) and feed_transport # (HttpSellerApiConnection, migrated to faraday-retry). uploader = instantiate_transporter(:http_api_upload, nil, { headers: { 'Content-Type': content_type } }) res = uploader.send_data(ecl.data, url, 'POST') ecl.notes += " | HTTP CODE: #{res[:http_result]&.status}, HTTP BODY: #{res[:http_result]&.body}, REQUEST ID: #{partner_request_id(res[:http_result])}, Timestamp: #{Time.current.to_datetime.to_fs(:crm_default)}" res[:success] end |