Class: Edi::Amazon::Orchestrator
- Inherits:
-
BaseOrchestrator
- Object
- BaseOrchestrator
- Edi::Amazon::Orchestrator
- Defined in:
- app/services/edi/amazon/orchestrator.rb
Overview
The core of our Amazon SP-API integration: the factory for all Amazon EDI
processors (listing, order, inventory, pricing, schema, …).
Marketplace IDs reference:
https://docs.developer.amazonservices.com/en_US/dev_guide/DG_Endpoints.html
Constant Summary collapse
- GOLIVE_DATE_STR =
Golive date str.
'October 11, 2024'- INCONCLUSIVE_BUY_BOX_ALERT_AFTER =
Consecutive no-offer buy-box pulls on one listing before we stop treating
it as a blip and surface it. Three daily pulls is ~72h of stale flags. 3
Constants inherited from BaseOrchestrator
BaseOrchestrator::DEFAULT_PENDING_DISCONTINUE_LIFETIME, BaseOrchestrator::ORCHESTRATORS, BaseOrchestrator::RECOMMENDED_EXECUTE_FLOW_EVERY_X_HOUR
Instance Attribute Summary
Attributes inherited from BaseOrchestrator
Class Method Summary collapse
- .execute_feed_submission_result_processor(options = {}) ⇒ void
-
.partners ⇒ Hash{Symbol=>Hash}
Partner configuration hash for every Amazon marketplace we serve.
-
.populate_items_from_all_partners(limit: nil, skus: nil, force: false) ⇒ Hash{Symbol=>Array<Hash>}
Populates catalog data across all active Amazon partners.
Instance Method Summary collapse
-
#a_plus_content_processor ⇒ APlusContentProcessor
Processor that applies fetched A+ content details.
-
#a_plus_content_remote_path ⇒ String?
SP-API base URL for A+ content operations.
-
#a_plus_content_retriever ⇒ APlusContentRetriever
Retriever that fetches A+ content summaries.
-
#acknowledge_message_enabled? ⇒ Boolean
Whether this partner is configured to send acknowledgements.
-
#acknowledge_message_sender ⇒ AcknowledgeMessageSender
Sender that posts order acknowledgements to Amazon.
-
#buy_box_status_enabled? ⇒ Boolean
Whether this partner can pull Buy Box status.
-
#buy_box_status_retriever ⇒ BuyBoxStatusRetriever
Retriever that fetches Buy Box status for a catalog item.
-
#buy_shipping_client ⇒ ShipWithAmazon?
Client for purchasing Amazon shipping labels.
-
#buy_shipping_enabled? ⇒ Boolean
Whether this partner can purchase Amazon shipping labels.
-
#catalog_item_information_enabled? ⇒ Boolean
Whether this partner can pull catalog item details.
-
#catalog_item_information_processor ⇒ CatalogItemInformationProcessor
Processor that applies fetched catalog details to a catalog item.
-
#catalog_item_information_retriever ⇒ CatalogItemInformationRetriever
Retriever that fetches catalog item details from Amazon.
-
#confirm_message_enabled? ⇒ Boolean
Whether this partner is configured to send ship confirmations.
-
#confirm_message_processor ⇒ ConfirmMessageProcessor
Processor that builds ship confirmations.
-
#confirm_message_sender ⇒ ConfirmMessageSender
Sender that posts ship confirmations to Amazon.
-
#confirm_outbound_processing? ⇒ Boolean
Amazon uses a two-stage outbound processing model.
-
#connection_info ⇒ Hash
SP-API connection identifiers for this partner.
-
#delete_listing_from_catalog_item(catalog_item: nil, amazon_variation: nil, http_method: 'DELETE', attribute_actions: nil, use_fba_sku: false) ⇒ Array
Builds and sends an Amazon listing delete message for a catalog item or variation.
-
#delete_listing_message_processor ⇒ DeleteListingMessageProcessor
Processor that builds listing delete messages.
-
#discontinue_flow_processor ⇒ DiscontinueFlowProcessor
Processor that builds discontinue-listing messages.
-
#execute_discontinue_flow ⇒ Boolean
Runs the discontinue-listing flow for this partner.
-
#execute_fba_order_flow ⇒ void
Runs the FBA order flow for this partner.
-
#execute_inventory_flow(use_delta_since_last_message: false) ⇒ Boolean
Generates and sends the daily inventory feed.
-
#execute_listing_message_feed_flow ⇒ Boolean
Generates and sends the daily listing message feed.
-
#execute_order_flow ⇒ void
Runs the order/acknowledgement/confirmation flow for this partner.
-
#execute_order_status_verification ⇒ void
Runs the order-status verification flow for this partner.
-
#execute_price_flow(use_delta_since_last_message: false) ⇒ Boolean
Generates and sends the daily price feed.
-
#execute_returns_report_flow(start_time: nil, end_time: nil) ⇒ void
Pulls the Seller Central returns report (previous day's window) and applies it to our RMAs.
-
#fba_order_message_enabled? ⇒ Boolean
Whether this partner is configured to receive FBA orders.
-
#fba_order_message_processor ⇒ FbaOrderMessageProcessor
Processor that imports fetched FBA order messages.
-
#fba_order_message_retriever ⇒ FbaOrderMessageRetriever
Retriever that fetches FBA order messages.
-
#fba_pull_orders_by_po_numbers(po_numbers) ⇒ Object
Pulls FBA orders for the given PO numbers.
-
#feed_submission_result_processor ⇒ FeedSubmissionResultProcessor
Processor that checks feed submission results.
- #inconclusive_buy_box_key(catalog_item) ⇒ String
-
#inventory_message_enabled? ⇒ Boolean
Whether this partner is configured to push inventory feeds.
-
#inventory_message_processor ⇒ InventoryMessageProcessor
Processor that builds the inventory feed message.
-
#inventory_message_sender ⇒ InventoryMessageSender
Sender that posts the inventory feed to Amazon.
-
#invoice_message_enabled? ⇒ Boolean
Invoices are not sent to Amazon.
-
#invoice_message_processor ⇒ Edi::NullProcessor
Invoice processing is disabled for Amazon.
-
#listing_item_information_processor ⇒ ListingItemInformationProcessor
Processor that normalizes fetched listing item details.
-
#listing_item_information_retriever ⇒ ListingItemInformationRetriever
Retriever that fetches listing item details from Amazon.
-
#listing_item_schema_processor ⇒ ListingItemSchemaProcessor
Processor that persists fetched listing JSON schemas.
-
#listing_item_schema_retriever ⇒ ListingItemSchemaRetriever
Retriever that fetches the JSON schema for a product type.
-
#listing_message_enabled? ⇒ Boolean
Whether this partner is configured to push listing items.
-
#listing_message_feed_enabled? ⇒ Boolean
Whether this partner is configured to push listing message feeds.
-
#listing_message_feed_processor ⇒ ListingMessageFeedProcessor
Processor that builds the listing message feed.
-
#listing_message_feed_sender ⇒ ListingMessageFeedSender
Sender that posts the listing message feed to Amazon.
-
#listing_message_processor ⇒ ListingMessageProcessor
Processor that builds listing item messages.
-
#listing_message_sender ⇒ ListingMessageSender
Sender that posts listing item messages to Amazon.
-
#listing_schema_message_enabled? ⇒ Boolean
Whether this partner is configured to pull listing schemas.
-
#note_inconclusive_buy_box_pull(catalog_item, asin) ⇒ void
A buy-box pull that comes back with no offers at all is inconclusive, so #pull_buy_box_status leaves the flags alone.
-
#order_message_enabled? ⇒ Boolean
Whether this partner is configured to receive order messages.
-
#order_message_processor ⇒ OrderMessageProcessor
Processor that imports fetched Amazon orders.
-
#order_message_retriever ⇒ OrderMessageRetriever
Retriever that fetches new Amazon orders.
-
#order_status_verification_enabled? ⇒ Boolean
Whether this partner is configured to verify order statuses.
-
#order_status_verifier ⇒ OrderStatusVerifier
Retriever that verifies pending order statuses with Amazon.
-
#orders_with_amazon_id_po(order_id) ⇒ Array<Order>
Finds local orders matching an Amazon order ID or PO number.
-
#populate_items_from_catalog(limit: nil, skus: nil, force: false) ⇒ Array<Hash>
Populates our item data with Amazon's catalog data.
-
#price_message_enabled? ⇒ Boolean
Whether this partner is configured to push price feeds.
-
#price_message_processor ⇒ PriceMessageProcessor
Processor that builds the price feed message.
-
#price_message_sender ⇒ PriceMessageSender
Sender that posts the price feed to Amazon.
-
#pull_amazon_variation_listing_information(amazon_variation) ⇒ Array<Hash>
Pulls listing item details for an Amazon variation parent.
-
#pull_buy_box_status(catalog_item) ⇒ Array<Hash>
Pulls Buy Box status for a catalog item and updates tracking flags.
-
#pull_catalog_information(catalog_item) ⇒ Array<Hash>
Pulls catalog details from Amazon for every configured locale.
-
#pull_listing_information(catalog_item) ⇒ Array<Hash>
Pulls listing item details for a catalog item.
-
#pull_listing_schema(catalog_item, product_type = nil) ⇒ Array<Hash>
Pulls the listing schema for a catalog item's product type.
-
#pull_schema(product_type) ⇒ Array<Hash>
Pulls the JSON schema for a product type across all locales.
-
#pull_schema_for_locale(product_type, locale, amazon_schema: nil) ⇒ AmazonSchema?
Pulls and persists the JSON schema for a product type in one locale.
-
#push_listing_from_amazon_variation(amazon_variation) ⇒ Array
Builds and sends an Amazon listing PUT message for a single variation.
-
#push_listing_from_catalog_item(catalog_item, http_method: 'PATCH', attribute_actions: nil, use_fba_sku: false) ⇒ Array
Builds and sends an Amazon listing-items message for a catalog item.
-
#return_notification_message_enabled? ⇒ Boolean
Return notifications are not sent to Amazon.
-
#return_notification_message_processor ⇒ Edi::NullProcessor
Return notification processing is disabled for Amazon.
-
#returns_report_enabled? ⇒ Boolean
Whether this partner is configured to fetch the returns report.
-
#returns_report_processor ⇒ ReturnsReportProcessor
Processor that applies the returns report to RMAs.
-
#returns_report_retriever ⇒ ReturnsReportRetriever
Retriever that fetches the Seller Central returns report.
-
#ship_code_mapper ⇒ ShipCodeMapper
Mapper that converts our carrier codes to Amazon ship codes.
-
#store_competitor_seller_ids(catalog_item, offers, our_merchant_id) ⇒ void
Stores competitor seller IDs discovered from Buy Box status.
Methods inherited from BaseOrchestrator
all_orchestrators_class, build, build_customer_id_to_partner_key_map, cached_build, cached_orchestrators, catalog_id_to_pending_discontinue_lifetime, catalog_ids_edi_enabled, #customer, #customer_catalog, customer_id_to_partner_key_map, #customer_ids, customer_ids_edi_enabled, customer_ids_with_invoice_message_enabled, #customers, execute_discontinue_flow, execute_flow, execute_inventory_flow, execute_listing_message_feed_flow, execute_order_flow, execute_price_flow, #execute_product_data_flow, execute_product_data_flow, #ignore_back_orders, #initialize, orchestrator_for_customer_id, orchestrators, #pending_discontinue_lifetime, #product_data_enabled?, #should_execute_flow?, #should_execute_order_flow?, #should_execute_product_data_flow?, #supports_flow?, #test_mode?
Constructor Details
This class inherits a constructor from Edi::BaseOrchestrator
Class Method Details
.execute_feed_submission_result_processor(options = {}) ⇒ void
This method returns an undefined value.
390 391 392 |
# File 'app/services/edi/amazon/orchestrator.rb', line 390 def self.execute_feed_submission_result_processor( = {}) orchestrators().each { |o| o.feed_submission_result_processor.process } end |
.partners ⇒ Hash{Symbol=>Hash}
Partner configuration hash for every Amazon marketplace we serve.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 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 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'app/services/edi/amazon/orchestrator.rb', line 23 def self.partners { amazon_seller_central_us: { active: true, partner: :amazon_seller_central_us, customer_id: 5_322_434, fba_store_id: 3, marketplace: 'ATVPDKIKX0DER', merchant_id: 'A1ALB70A8YMUCH', locales: ['en_US'], # aws_access_key_id: Heatwave::Configuration.fetch(:amazon_mws, :aws_access_key_id), # aws_secret_access_key: Heatwave::Configuration.fetch(:amazon_mws, :aws_secret_access_key), order_status_verification_enabled: true, inventory_message_enabled: true, returns_report_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: true, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/catalog/2022-04-01/items/", product_pricing_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/products/pricing/v0/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/aplus/2020-11-01", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/feeds/2021-06-30", buy_shipping_enabled: true, buy_shipping_business_id: 'AmazonShipping_US', failure_timeout_in_minutes: 1440, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'USD' }, amazon_seller_central_ca: { active: true, partner: :amazon_seller_central_ca, customer_id: 5_768_174, fba_store_id: 5, marketplace: 'A2EUQ1WTGCTBG2', merchant_id: 'A1ALB70A8YMUCH', locales: %w[en_CA fr_CA], # aws_access_key_id: Heatwave::Configuration.fetch(:amazon_mws, :aws_access_key_id), # aws_secret_access_key: Heatwave::Configuration.fetch(:amazon_mws, :aws_secret_access_key), order_status_verification_enabled: true, inventory_message_enabled: true, returns_report_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: true, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/catalog/2022-04-01/items/", product_pricing_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/products/pricing/v0/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/aplus/2020-11-01", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/feeds/2021-06-30", buy_shipping_enabled: false, failure_timeout_in_minutes: 1440, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'CAD' }, # North-American (Mexico) — amazon.com.mx. Order-pull only (FBM/MFN): # products list via Build International Listings (US FBA / Remote # Fulfillment), so no product/price/inventory push here. Orders arrive # in MXN; customer_id 26215516 points at catalog 76 (US), so # `order.currency` resolves to USD and OrderMessageProcessor converts # the MXN amounts at the order-date FX rate. First pull validated # 2026-05-29 (ECL 3510368); flipped active here so the scheduled # order flow picks up subsequent MX orders. amazon_seller_central_mx: { active: true, partner: :amazon_seller_central_mx, customer_id: 26_215_516, marketplace: 'A1AM78C64UM0Y8', merchant_id: 'A1ALB70A8YMUCH', locales: ['es_MX'], order_status_verification_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_api, :api_host)}/orders/v0/orders", failure_timeout_in_minutes: 1440, currency: 'USD', # marketplace transacts in MXN; orders book USD via catalog 76, processor converts MXN→USD inventory_message_enabled: false, # MX orders book through catalog 76 (US); no separate MX inventory feed price_message_enabled: false, # ditto — no MX-specific price feed listing_message_feed_enabled: false # ditto — listings live under the US channel via Build International Listings }, amazon_seller_central_fr: { active: true, partner: :amazon_seller_central_fr, customer_id: 22_447_665, marketplace: 'A13V1IB3VIYZZH', merchant_id: 'A1U07I6I50BN2R', locales: ['fr'], language_tag: 'fr_FR', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_be: { active: true, partner: :amazon_seller_central_be, customer_id: 23_325_148, marketplace: 'AMEN7PMS3EDWL', merchant_id: 'A1U07I6I50BN2R', locales: ['fr'], language_tag: 'fr_FR', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: false, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_de: { active: true, partner: :amazon_seller_central_de, customer_id: 18_007_732, marketplace: 'A1PA6795UKMFR9', merchant_id: 'A1U07I6I50BN2R', locales: ['de'], language_tag: 'de_DE', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_es: { active: true, partner: :amazon_seller_central_es, customer_id: 22_308_935, marketplace: 'A1RKKUPIHCS9HS', merchant_id: 'A1U07I6I50BN2R', locales: ['es'], language_tag: 'es_ES', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_it: { active: true, partner: :amazon_seller_central_it, customer_id: 22_788_055, marketplace: 'APJ6JRA9NG5V4', merchant_id: 'A1U07I6I50BN2R', locales: ['it'], language_tag: 'it_IT', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_nl: { active: true, partner: :amazon_seller_central_nl, customer_id: 23_325_157, marketplace: 'A1805IZSGTT6HS', merchant_id: 'A1U07I6I50BN2R', locales: ['de'], language_tag: 'nl_NL', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: false, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'EUR' }, amazon_seller_central_pl: { active: true, partner: :amazon_seller_central_pl, customer_id: 23_325_160, marketplace: 'A1C3SOZRARQ6R3', merchant_id: 'A1U07I6I50BN2R', locales: ['pl'], language_tag: 'pl_PL', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: false, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'PLN' }, amazon_seller_central_se: { active: true, partner: :amazon_seller_central_se, customer_id: 23_325_161, marketplace: 'A2NODRKZP88ZB9', merchant_id: 'A1U07I6I50BN2R', locales: ['se'], language_tag: 'sv_SE', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: false, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'SEK' }, amazon_seller_central_uk: { active: false, partner: :amazon_seller_central_uk, customer_id: 23_325_163, marketplace: 'A1F83G8C2ARO7P', merchant_id: 'A1U07I6I50BN2R', locales: ['en-UK'], language_tag: 'en_UK', inventory_message_enabled: true, support_contact: 'https://sellercentral.amazon.com/gp/mws/contactus.html', price_message_enabled: true, business_price_available: true, listing_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/listings/2021-08-01/items/", listing_message_feed_enabled: false, product_type_schema_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/definitions/2020-09-01/productTypes/", listing_schema_message_enabled: true, transporter: :http_seller_api, transporter_profile: :amazon_sc_seller_eu_api, acknowledge_only_on_pending_ship_confirm: true, order_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/orders/v0/orders", catalog_item_information_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/catalog/2022-04-01/items/", a_plus_content_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/aplus/2020-11-01/", feed_message_remote_path: "https://#{Heatwave::Configuration.fetch(:amazon_sc_seller_eu_api, :api_host)}/feeds/2021-06-30", failure_timeout_in_minutes: 480, execute_inventory_flow_every_x_hour: 24, # once a day for full inventory feed execute_price_flow_every_x_hour: 24, # once a day for full price feed execute_listing_message_feed_flow_every_x_hour: 24, # once a day for full feed currency: 'GBP' } } end |
.populate_items_from_all_partners(limit: nil, skus: nil, force: false) ⇒ Hash{Symbol=>Array<Hash>}
Populates catalog data across all active Amazon partners.
1484 1485 1486 1487 1488 1489 1490 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1484 def self.populate_items_from_all_partners(limit: nil, skus: nil, force: false) res = {} orchestrators.each do |orchestrator| res[orchestrator.partner] = orchestrator.populate_items_from_catalog(limit:, skus:, force:) end res end |
Instance Method Details
#a_plus_content_processor ⇒ APlusContentProcessor
Processor that applies fetched A+ content details.
769 770 771 772 773 |
# File 'app/services/edi/amazon/orchestrator.rb', line 769 def a_plus_content_processor return Edi::NullProcessor.new(self, :a_plus_content_processor, ) if a_plus_content_remote_path.blank? APlusContentProcessor.new(self, ) end |
#a_plus_content_remote_path ⇒ String?
SP-API base URL for A+ content operations.
753 754 755 |
# File 'app/services/edi/amazon/orchestrator.rb', line 753 def a_plus_content_remote_path try(:a_plus_content_remote_path) end |
#a_plus_content_retriever ⇒ APlusContentRetriever
Retriever that fetches A+ content summaries.
760 761 762 763 764 |
# File 'app/services/edi/amazon/orchestrator.rb', line 760 def a_plus_content_retriever return Edi::NullProcessor.new(self, :a_plus_content_retriever, ) if a_plus_content_remote_path.blank? APlusContentRetriever.new(self, ) end |
#acknowledge_message_enabled? ⇒ Boolean
Whether this partner is configured to send acknowledgements.
447 448 449 |
# File 'app/services/edi/amazon/orchestrator.rb', line 447 def try(:order_message_remote_path).present? end |
#acknowledge_message_sender ⇒ AcknowledgeMessageSender
Sender that posts order acknowledgements to Amazon.
454 455 456 457 458 |
# File 'app/services/edi/amazon/orchestrator.rb', line 454 def return Edi::NullProcessor.new(self, :acknowledge_message_sender, ) unless AcknowledgeMessageSender.new(self, ) end |
#buy_box_status_enabled? ⇒ Boolean
Whether this partner can pull Buy Box status.
678 679 680 |
# File 'app/services/edi/amazon/orchestrator.rb', line 678 def buy_box_status_enabled? try(:product_pricing_remote_path).present? end |
#buy_box_status_retriever ⇒ BuyBoxStatusRetriever
Retriever that fetches Buy Box status for a catalog item.
685 686 687 688 689 |
# File 'app/services/edi/amazon/orchestrator.rb', line 685 def buy_box_status_retriever return Edi::NullProcessor.new(self, :buy_box_status_retriever, ) unless buy_box_status_enabled? BuyBoxStatusRetriever.new(self, ) end |
#buy_shipping_client ⇒ ShipWithAmazon?
Client for purchasing Amazon shipping labels.
1579 1580 1581 1582 1583 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1579 def buy_shipping_client return nil unless buy_shipping_enabled? ShipWithAmazon.new(self) end |
#buy_shipping_enabled? ⇒ Boolean
Whether this partner can purchase Amazon shipping labels.
1572 1573 1574 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1572 def buy_shipping_enabled? try(:buy_shipping_enabled) == true end |
#catalog_item_information_enabled? ⇒ Boolean
Whether this partner can pull catalog item details.
662 663 664 |
# File 'app/services/edi/amazon/orchestrator.rb', line 662 def catalog_item_information_enabled? try(:catalog_item_information_remote_path).present? end |
#catalog_item_information_processor ⇒ CatalogItemInformationProcessor
Processor that applies fetched catalog details to a catalog item.
730 731 732 733 734 |
# File 'app/services/edi/amazon/orchestrator.rb', line 730 def catalog_item_information_processor return Edi::NullProcessor.new(self, :catalog_item_information_processor, ) unless catalog_item_information_enabled? CatalogItemInformationProcessor.new(self, ) end |
#catalog_item_information_retriever ⇒ CatalogItemInformationRetriever
Retriever that fetches catalog item details from Amazon.
669 670 671 672 673 |
# File 'app/services/edi/amazon/orchestrator.rb', line 669 def catalog_item_information_retriever return Edi::NullProcessor.new(self, :catalog_item_information_retriever, ) unless catalog_item_information_enabled? CatalogItemInformationRetriever.new(self, ) end |
#confirm_message_enabled? ⇒ Boolean
Whether this partner is configured to send ship confirmations.
422 423 424 |
# File 'app/services/edi/amazon/orchestrator.rb', line 422 def try(:order_message_remote_path).present? end |
#confirm_message_processor ⇒ ConfirmMessageProcessor
Processor that builds ship confirmations.
429 430 431 432 433 |
# File 'app/services/edi/amazon/orchestrator.rb', line 429 def return Edi::NullProcessor.new(self, :confirm_message_processor, ) unless ConfirmMessageProcessor.new(self, ) end |
#confirm_message_sender ⇒ ConfirmMessageSender
Sender that posts ship confirmations to Amazon.
438 439 440 441 442 |
# File 'app/services/edi/amazon/orchestrator.rb', line 438 def return Edi::NullProcessor.new(self, :confirm_message_sender, ) unless ConfirmMessageSender.new(self, ) end |
#confirm_outbound_processing? ⇒ Boolean
Amazon uses a two-stage outbound processing model.
931 932 933 |
# File 'app/services/edi/amazon/orchestrator.rb', line 931 def confirm_outbound_processing? true end |
#connection_info ⇒ Hash
SP-API connection identifiers for this partner.
650 651 652 653 654 655 656 657 |
# File 'app/services/edi/amazon/orchestrator.rb', line 650 def connection_info { marketplace:, merchant_id:, aws_access_key_id:, aws_secret_access_key: } end |
#delete_listing_from_catalog_item(catalog_item: nil, amazon_variation: nil, http_method: 'DELETE', attribute_actions: nil, use_fba_sku: false) ⇒ Array
Builds and sends an Amazon listing delete message for a catalog item or variation.
1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1407 def delete_listing_from_catalog_item(catalog_item: nil, amazon_variation: nil, http_method: 'DELETE', attribute_actions: nil, use_fba_sku: false) raise 'Must provide either a catalog_item or an amazon_variation' unless catalog_item || amazon_variation raise 'Catalog Item not an amazon item or not in the right state' if catalog_item && !catalog_item.amazon_catalog_item? listing = catalog_item || amazon_variation res = [] result = .process(listing, http_method:, attribute_actions:, use_fba_sku:) if result.try(:success?) Rails.logger.debug { "orchestrator#delete_listing_from_catalog_item for #{listing}, ecl: #{result.data}" } res << .process(result.data, catalog_item_or_variation: listing, http_method:, use_fba_sku:) else Rails.logger.error "orchestrator#delete_listing_from_catalog_item for #{listing} failed: #{result.try(:error) || 'listing messages not enabled for this partner'}" res << false end res end |
#delete_listing_message_processor ⇒ DeleteListingMessageProcessor
Processor that builds listing delete messages.
796 797 798 799 800 |
# File 'app/services/edi/amazon/orchestrator.rb', line 796 def return Edi::NullProcessor.new(self, :delete_listing_message_processor, ) unless DeleteListingMessageProcessor.new(self, ) end |
#discontinue_flow_processor ⇒ DiscontinueFlowProcessor
Processor that builds discontinue-listing messages.
805 806 807 808 809 |
# File 'app/services/edi/amazon/orchestrator.rb', line 805 def discontinue_flow_processor return Edi::NullProcessor.new(self, :discontinue_flow_processor, ) unless DiscontinueFlowProcessor.new(self, ) end |
#execute_discontinue_flow ⇒ Boolean
Runs the discontinue-listing flow for this partner.
922 923 924 925 926 |
# File 'app/services/edi/amazon/orchestrator.rb', line 922 def execute_discontinue_flow return false unless active discontinue_flow_processor.process end |
#execute_fba_order_flow ⇒ void
This method returns an undefined value.
Runs the FBA order flow for this partner.
1554 1555 1556 1557 1558 1559 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1554 def execute_fba_order_flow return unless active .process .process end |
#execute_inventory_flow(use_delta_since_last_message: false) ⇒ Boolean
Generates and sends the daily inventory feed.
889 890 891 892 893 894 895 896 |
# File 'app/services/edi/amazon/orchestrator.rb', line 889 def execute_inventory_flow(use_delta_since_last_message: false) return false unless active .process(use_delta_since_last_message:) # Generate an inventory feed message .process # Send it sleep(1) execute_fba_order_flow # put it here for daily processing for now: need this to run less frequently or to remove ECLs where we skip all the processed fba_orders end |
#execute_listing_message_feed_flow ⇒ Boolean
Generates and sends the daily listing message feed.
912 913 914 915 916 917 |
# File 'app/services/edi/amazon/orchestrator.rb', line 912 def return false unless active .process # Generate a listing message feed message .process # Send it end |
#execute_order_flow ⇒ void
This method returns an undefined value.
Runs the order/acknowledgement/confirmation flow for this partner.
814 815 816 817 818 819 820 821 822 823 824 825 826 827 |
# File 'app/services/edi/amazon/orchestrator.rb', line 814 def execute_order_flow return unless active .process sleep(1) .process sleep(1) .process sleep(1) .process execute_returns_report_flow # sleep(1) # execute_fba_order_flow # need this to run less frequently or to remove ECLs where we skip all the processed fba_orders end |
#execute_order_status_verification ⇒ void
This method returns an undefined value.
Runs the order-status verification flow for this partner.
1520 1521 1522 1523 1524 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1520 def execute_order_status_verification return unless active && order_status_verification_enabled? order_status_verifier.process end |
#execute_price_flow(use_delta_since_last_message: false) ⇒ Boolean
Generates and sends the daily price feed.
902 903 904 905 906 907 |
# File 'app/services/edi/amazon/orchestrator.rb', line 902 def execute_price_flow(use_delta_since_last_message: false) return false unless active .process(use_delta_since_last_message:) # Generate a price feed message .process # Send it end |
#execute_returns_report_flow(start_time: nil, end_time: nil) ⇒ void
This method returns an undefined value.
Pulls the Seller Central returns report (previous day's window) and
applies it to our RMAs. The order flow worker runs every 15 minutes
(config/sidekiq_production_schedule.yml +edi_order_flow+) but the returns
report is a daily batch, so runs are limited by three +Rails.cache+ keys:
a 30-minute in-flight lock (+unless_exist+ — first caller wins, so
overlapping 15-minute cycles don't double-fetch while a report
generates), a 24-hour completion gate written ONLY after a window was
actually retrieved, and a 7-day pending-window marker recorded on
failure so a window that keeps failing past midnight is resumed (oldest
first) instead of silently dropped. Pass an explicit window (backfills)
to bypass all three. Dark unless the partner config carries
+returns_report_enabled: true+.
845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 |
# File 'app/services/edi/amazon/orchestrator.rb', line 845 def execute_returns_report_flow(start_time: nil, end_time: nil) return unless returns_report_enabled? explicit_window = start_time.present? || end_time.present? done_key = "edi/amazon/returns_report_flow/#{partner}/#{Date.current.iso8601}" return if !explicit_window && Rails.cache.read(done_key) lock_key = "edi/amazon/returns_report_flow/lock/#{partner}" return unless explicit_window || Rails.cache.write(lock_key, Time.current.iso8601, expires_in: 30.minutes, unless_exist: true) pending_key = "edi/amazon/returns_report_flow/pending/#{partner}" pending = !explicit_window && Rails.cache.read(pending_key) window = if explicit_window { start_time:, end_time: } elsif pending JSON.parse(pending, symbolize_names: true) else {} end result = returns_report_retriever.process(**window) if result.nil? # failed — leave ungated and pin the window for a later cycle unless explicit_window # A window already pending and failing again is stuck — surface it # on AppSignal before the pending key quietly expires (7-day TTL). ErrorReporting.error('Amazon returns report window stuck retrying', partner:, window:) if pending ws, we = returns_report_retriever.resolve_window(window[:start_time], window[:end_time]) Rails.cache.write(pending_key, { start_time: ws, end_time: we }.to_json, expires_in: 7.days, unless_exist: true) end return end Rails.cache.delete(pending_key) # A resumed pending window isn't today's pull — leave today ungated so # the current window is attempted on the next cycle. Rails.cache.write(done_key, Time.current.iso8601, expires_in: 24.hours) unless explicit_window || pending sleep(1) returns_report_processor.process end |
#fba_order_message_enabled? ⇒ Boolean
Whether this partner is configured to receive FBA orders.
1529 1530 1531 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1529 def try(:order_message_remote_path).present? && try(:fba_store_id).present? end |
#fba_order_message_processor ⇒ FbaOrderMessageProcessor
Processor that imports fetched FBA order messages.
1545 1546 1547 1548 1549 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1545 def return Edi::NullProcessor.new(self, :fba_order_message_processor, ) unless FbaOrderMessageProcessor.new(self, ) end |
#fba_order_message_retriever ⇒ FbaOrderMessageRetriever
Retriever that fetches FBA order messages.
1536 1537 1538 1539 1540 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1536 def return Edi::NullProcessor.new(self, :fba_order_message_retriever, ) unless FbaOrderMessageRetriever.new(self, ) end |
#fba_pull_orders_by_po_numbers(po_numbers) ⇒ Object
Pulls FBA orders for the given PO numbers.
1565 1566 1567 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1565 def fba_pull_orders_by_po_numbers(po_numbers) .process(po_numbers:) end |
#feed_submission_result_processor ⇒ FeedSubmissionResultProcessor
Processor that checks feed submission results.
642 643 644 645 |
# File 'app/services/edi/amazon/orchestrator.rb', line 642 def feed_submission_result_processor # Its amazon its always enabled FeedSubmissionResultProcessor.new(self, ) end |
#inconclusive_buy_box_key(catalog_item) ⇒ String
540 541 542 |
# File 'app/services/edi/amazon/orchestrator.rb', line 540 def inconclusive_buy_box_key(catalog_item) "amazon:buy_box:inconclusive:#{partner}:#{catalog_item.id}" end |
#inventory_message_enabled? ⇒ Boolean
Whether this partner is configured to push inventory feeds.
556 557 558 559 560 561 562 |
# File 'app/services/edi/amazon/orchestrator.rb', line 556 def # `try` (not bare call) because per-partner attr_accessors are only # defined for keys actually present in the partner config hash — # a partner added without an explicit `inventory_message_enabled:` # key would otherwise raise NameError here (AppSignal #5595). try(:inventory_message_enabled).present? end |
#inventory_message_processor ⇒ InventoryMessageProcessor
Processor that builds the inventory feed message.
567 568 569 570 571 |
# File 'app/services/edi/amazon/orchestrator.rb', line 567 def return Edi::NullProcessor.new(self, :inventory_message_processor, ) unless InventoryMessageProcessor.new(self, ) end |
#inventory_message_sender ⇒ InventoryMessageSender
Sender that posts the inventory feed to Amazon.
576 577 578 579 580 |
# File 'app/services/edi/amazon/orchestrator.rb', line 576 def return Edi::NullProcessor.new(self, :inventory_message_sender, ) unless InventoryMessageSender.new(self, ) end |
#invoice_message_enabled? ⇒ Boolean
Invoices are not sent to Amazon.
463 464 465 |
# File 'app/services/edi/amazon/orchestrator.rb', line 463 def false end |
#invoice_message_processor ⇒ Edi::NullProcessor
Invoice processing is disabled for Amazon.
470 471 472 |
# File 'app/services/edi/amazon/orchestrator.rb', line 470 def Edi::NullProcessor.new(self, :invoice_message_processor, ) end |
#listing_item_information_processor ⇒ ListingItemInformationProcessor
Processor that normalizes fetched listing item details.
703 704 705 706 707 |
# File 'app/services/edi/amazon/orchestrator.rb', line 703 def listing_item_information_processor return Edi::NullProcessor.new(self, :listing_item_information_processor, ) unless ListingItemInformationProcessor.new(self, ) end |
#listing_item_information_retriever ⇒ ListingItemInformationRetriever
Retriever that fetches listing item details from Amazon.
694 695 696 697 698 |
# File 'app/services/edi/amazon/orchestrator.rb', line 694 def listing_item_information_retriever return Edi::NullProcessor.new(self, :listing_item_information_retriever, ) unless ListingItemInformationRetriever.new(self, ) end |
#listing_item_schema_processor ⇒ ListingItemSchemaProcessor
Processor that persists fetched listing JSON schemas.
721 722 723 724 725 |
# File 'app/services/edi/amazon/orchestrator.rb', line 721 def listing_item_schema_processor return Edi::NullProcessor.new(self, :listing_item_schema_processor, ) unless ListingItemSchemaProcessor.new(self, ) end |
#listing_item_schema_retriever ⇒ ListingItemSchemaRetriever
Retriever that fetches the JSON schema for a product type.
712 713 714 715 716 |
# File 'app/services/edi/amazon/orchestrator.rb', line 712 def listing_item_schema_retriever return Edi::NullProcessor.new(self, :listing_item_schema_retriever, ) unless ListingItemSchemaRetriever.new(self, ) end |
#listing_message_enabled? ⇒ Boolean
Whether this partner is configured to push listing items.
739 740 741 |
# File 'app/services/edi/amazon/orchestrator.rb', line 739 def try(:listing_message_remote_path).present? end |
#listing_message_feed_enabled? ⇒ Boolean
Whether this partner is configured to push listing message feeds.
614 615 616 617 618 619 |
# File 'app/services/edi/amazon/orchestrator.rb', line 614 def # Same NameError-safety pattern as `inventory_message_enabled?` / # `price_message_enabled?`. Partners without the key in their config # hash return false instead of raising. AppSignal #5867. try(:listing_message_feed_enabled).present? end |
#listing_message_feed_processor ⇒ ListingMessageFeedProcessor
Processor that builds the listing message feed.
633 634 635 636 637 |
# File 'app/services/edi/amazon/orchestrator.rb', line 633 def return Edi::NullProcessor.new(self, :listing_message_feed_processor, ) unless ListingMessageFeedProcessor.new(self, ) end |
#listing_message_feed_sender ⇒ ListingMessageFeedSender
Sender that posts the listing message feed to Amazon.
624 625 626 627 628 |
# File 'app/services/edi/amazon/orchestrator.rb', line 624 def return Edi::NullProcessor.new(self, :listing_message_feed_sender, ) unless ListingMessageFeedSender.new(self, ) end |
#listing_message_processor ⇒ ListingMessageProcessor
Processor that builds listing item messages.
787 788 789 790 791 |
# File 'app/services/edi/amazon/orchestrator.rb', line 787 def return Edi::NullProcessor.new(self, :listing_message_processor, ) unless ListingMessageProcessor.new(self, ) end |
#listing_message_sender ⇒ ListingMessageSender
Sender that posts listing item messages to Amazon.
778 779 780 781 782 |
# File 'app/services/edi/amazon/orchestrator.rb', line 778 def return Edi::NullProcessor.new(self, :listing_message_sender, ) unless ListingMessageSender.new(self, ) end |
#listing_schema_message_enabled? ⇒ Boolean
Whether this partner is configured to pull listing schemas.
746 747 748 |
# File 'app/services/edi/amazon/orchestrator.rb', line 746 def try(:listing_schema_message_enabled).present? end |
#note_inconclusive_buy_box_pull(catalog_item, asin) ⇒ void
This method returns an undefined value.
A buy-box pull that comes back with no offers at all is inconclusive, so
#pull_buy_box_status leaves the flags alone. That is the safe response to
one bad response, but a listing whose feed is persistently degraded would
then just sit on stale flags forever — the hourly recovery sweep that used
to paper over it is gone. Count the streak and escalate once it stops
looking like a blip.
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 |
# File 'app/services/edi/amazon/orchestrator.rb', line 518 def note_inconclusive_buy_box_pull(catalog_item, asin) key = inconclusive_buy_box_key(catalog_item) streak = Rails.cache.read(key).to_i + 1 Rails.cache.write(key, streak, expires_in: 7.days) Rails.logger.warn do "Catalog item #{catalog_item.id}: buy-box pull returned NO offers at all for ASIN #{asin} — " \ "leaving Buy Box flags untouched (inconclusive, not a loss). Streak: #{streak}." end return if streak < INCONCLUSIVE_BUY_BOX_ALERT_AFTER # Fires once per streak crossing, not on every pull past the threshold. return unless streak == INCONCLUSIVE_BUY_BOX_ALERT_AFTER ErrorReporting.error( 'Amazon buy-box pull inconclusive repeatedly', partner:, asin:, catalog_item_id: catalog_item.id, streak: ) end |
#order_message_enabled? ⇒ Boolean
Whether this partner is configured to receive order messages.
397 398 399 |
# File 'app/services/edi/amazon/orchestrator.rb', line 397 def try(:order_message_remote_path).present? end |
#order_message_processor ⇒ OrderMessageProcessor
Processor that imports fetched Amazon orders.
413 414 415 416 417 |
# File 'app/services/edi/amazon/orchestrator.rb', line 413 def return Edi::NullProcessor.new(self, :order_message_processor, ) unless OrderMessageProcessor.new(self, ) end |
#order_message_retriever ⇒ OrderMessageRetriever
Retriever that fetches new Amazon orders.
404 405 406 407 408 |
# File 'app/services/edi/amazon/orchestrator.rb', line 404 def return Edi::NullProcessor.new(self, :order_message_retriever, ) unless OrderMessageRetriever.new(self, ) end |
#order_status_verification_enabled? ⇒ Boolean
Whether this partner is configured to verify order statuses.
1504 1505 1506 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1504 def order_status_verification_enabled? !!try(:order_status_verification_enabled) && try(:order_message_remote_path).present? end |
#order_status_verifier ⇒ OrderStatusVerifier
Retriever that verifies pending order statuses with Amazon.
1511 1512 1513 1514 1515 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1511 def order_status_verifier return Edi::NullProcessor.new(self, :order_status_verifier, ) unless order_status_verification_enabled? OrderStatusVerifier.new(self, ) end |
#orders_with_amazon_id_po(order_id) ⇒ Array<Order>
Finds local orders matching an Amazon order ID or PO number.
1496 1497 1498 1499 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1496 def orders_with_amazon_id_po(order_id) # Here we look for either EDI orders with edi_po_number matching order_id or manual orders with payments with the po_number matching order_id (customer.orders.where(edi_po_number: order_id).to_ary + customer.orders.with_payments.where(payments: { po_number: order_id }).to_ary).uniq end |
#populate_items_from_catalog(limit: nil, skus: nil, force: false) ⇒ Array<Hash>
Populates our item data with Amazon's catalog data.
1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1466 def populate_items_from_catalog(limit: nil, skus: nil, force: false) catalog = customer_catalog catalog_items = catalog.catalog_items.amazons_with_asins.available_for_edi_feeds # Note that this should probably be specific to the locale so leverage mobility TODO catalog_items = catalog_items.where(items: { amazon_title: [nil, ''] }) unless force catalog_items = catalog_items.limit(limit) if limit catalog_items = catalog_items.where(items: { sku: skus }) if skus.present? catalog_items.map do |catalog_item| { name: catalog_item.to_s, catalog_item_id: catalog_item.id, res: pull_catalog_information(catalog_item) } end end |
#price_message_enabled? ⇒ Boolean
Whether this partner is configured to push price feeds.
585 586 587 588 589 590 591 |
# File 'app/services/edi/amazon/orchestrator.rb', line 585 def # Use `try(:price_message_enabled).present?` so partners whose config # hash does not carry the key (no attr_accessor for `price_message_enabled` # exists on that instance) return false instead of raising NameError. # Same pattern as `inventory_message_enabled?` after AppSignal #5595. try(:price_message_enabled).present? end |
#price_message_processor ⇒ PriceMessageProcessor
Processor that builds the price feed message.
605 606 607 608 609 |
# File 'app/services/edi/amazon/orchestrator.rb', line 605 def return Edi::NullProcessor.new(self, :price_message_processor, ) unless PriceMessageProcessor.new(self, ) end |
#price_message_sender ⇒ PriceMessageSender
Sender that posts the price feed to Amazon.
596 597 598 599 600 |
# File 'app/services/edi/amazon/orchestrator.rb', line 596 def return Edi::NullProcessor.new(self, :price_message_sender, ) unless PriceMessageSender.new(self, ) end |
#pull_amazon_variation_listing_information(amazon_variation) ⇒ Array<Hash>
Pulls listing item details for an Amazon variation parent.
1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1267 def pull_amazon_variation_listing_information(amazon_variation) sku = amazon_variation.reported_vendor_sku(partner) # Listing info is marketplace-specific, not locale-specific. # The API call doesn't use the locale parameter, so we only need one call per marketplace. primary_locale = locales.first ecl = listing_item_information_retriever.process( sku: sku, locale: primary_locale, file_info: { sku: sku, locale: primary_locale } ) if ecl Rails.logger.debug { "orchestrator#pull_amazon_variation_listing_information, ecl: #{ecl}" } [{ primary_locale => listing_item_information_processor.process(ecl, catalog_item_or_amazon_variation: amazon_variation, locale: primary_locale) }] else Rails.logger.error "orchestrator#pull_amazon_variation_listing_information for amazon variation #{amazon_variation.id} yielded no ecl (sku #{sku} not found?)" [{ primary_locale => false }] end end |
#pull_buy_box_status(catalog_item) ⇒ Array<Hash>
Pulls Buy Box status for a catalog item and updates tracking flags.
971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 |
# File 'app/services/edi/amazon/orchestrator.rb', line 971 def pull_buy_box_status(catalog_item) asin = catalog_item.amazon_asin raise "No ASIN for catalog_item #{catalog_item.id}" if asin.blank? locale = 'en_US' # just put something in here item = catalog_item.item sku = catalog_item.reported_vendor_sku res = [] ecl = buy_box_status_retriever.process( asin: asin, file_info: { sku: sku, item_id: item.id, catalog_item_id: catalog_item.id } ) if ecl&.try(:data).present? && (json_hash = JSON.parse(ecl.data).with_indifferent_access).present? Rails.logger.debug { "orchestrator#pull_buy_box_status, catalog_item.id: #{catalog_item.id}, ecl: #{ecl}" } Rails.logger.debug { "orchestrator#pull_buy_box_status, json_hash: #{json_hash}" } ecl.edi_documents.create(catalog_item: catalog_item) Amazon::RecordSalesRanks.call( catalog_item:, payload: json_hash, source: 'product_pricing', observed_at: ecl.created_at ) status = json_hash.dig(:payload, :status).to_s.downcase if status == 'success' offers = json_hash.dig(:payload, :Offers) || [] # Convert each offer to indifferent access for consistent key handling # (nested hashes in arrays from JSON.parse don't inherit indifferent access) offers = offers.map(&:with_indifferent_access) inconclusive_offers = false # Find ALL offers from our merchant (there may be multiple: FBA vs MFN/FBM) our_offers = offers.select { |o| o[:SellerId] == merchant_id } if our_offers.any? # Separate FBA and MFN offers our_fba_offers = our_offers.select { |o| o[:IsFulfilledByAmazon] } our_mfn_offers = our_offers.reject { |o| o[:IsFulfilledByAmazon] } # Check if ANY of our offers is the buy box winner (FBA offer may win while MFN doesn't) is_buy_box_winner = our_offers.any? { |o| o[:IsBuyBoxWinner] } is_featured_merchant = our_offers.any? { |o| o[:IsFeaturedMerchant] } # Detect which fulfillment type is winning the buy box fba_winning = our_fba_offers.any? { |o| o[:IsBuyBoxWinner] } mfn_winning = our_mfn_offers.any? { |o| o[:IsBuyBoxWinner] } # Log the situation for visibility if our_fba_offers.any? && our_mfn_offers.any? Rails.logger.info { "Catalog item #{catalog_item.id}: Multiple offer types detected - FBA: #{our_fba_offers.count}, MFN: #{our_mfn_offers.count}" } if fba_winning Rails.logger.info { "Catalog item #{catalog_item.id}: FBA offer is the Buy Box winner" } elsif mfn_winning Rails.logger.info { "Catalog item #{catalog_item.id}: MFN offer is the Buy Box winner" } end end Rails.logger.debug { "Found #{our_offers.count} offer(s) for merchant #{merchant_id}: IsBuyBoxWinner=#{is_buy_box_winner}, IsFeaturedMerchant=#{is_featured_merchant}, FBA_winning=#{fba_winning}, MFN_winning=#{mfn_winning}" } elsif offers.any? # Offers exist and none are ours: conclusive. We are not on this # listing, so recording the loss (and the out_of_stock handling # below) is right. is_buy_box_winner = false is_featured_merchant = false Rails.logger.debug { "No offer of ours among #{offers.count} offers for merchant #{merchant_id}: IsBuyBoxWinner=false, IsFeaturedMerchant=false" } else # EMPTY Offers array under a success status. That is not evidence we # lost the Buy Box, it is the absence of evidence — Amazon returns # one on a throttled or partial getItemOffers response. Writing # false here flipped winning listings to false/false on the daily # pull for weeks (TWS5-IBZ06PP: 07-17, 07-25, 07-27, 08-02, each # time reverting a true/true). The hourly recovery sweep happened to # correct it a few hours later; that sweep is gone, so a bad value # now sticks until the next successful pull and the repricer spends # the day competing for a Buy Box it already holds. # # Leave every flag exactly as it was and say so loudly. inconclusive_offers = true note_inconclusive_buy_box_pull(catalog_item, asin) end unless inconclusive_offers Rails.cache.delete(inconclusive_buy_box_key(catalog_item)) # Track buy box winner changes previous_buy_box_winner = catalog_item.is_amz_buy_box_winner new_buy_box_winner = is_buy_box_winner == true # Ensure we never set nil values to prevent database constraint violations catalog_item.is_amz_buy_box_winner = new_buy_box_winner catalog_item.is_amz_featured_merchant = is_featured_merchant == true # Update change timestamp if buy box status changed catalog_item.amz_last_buy_box_winner_change = Time.current if previous_buy_box_winner != new_buy_box_winner end # Check if we have an active offer on Amazon. we_have_offer = our_offers.present? if inconclusive_offers # Nothing to conclude about stock either. Resolving the stock flags # would be the same unfounded write in the opposite direction, and # creating one would invent an outage from a truncated response. Rails.logger.debug { "Catalog item #{catalog_item.id}: skipping stock/offer flag changes on an inconclusive payload" } elsif we_have_offer # We have an offer - resolve any out_of_stock/no_buyable_offers flags catalog_item.amazon_catalog_item_flags .active .where(flag_type: %w[no_buyable_offers out_of_stock]) .find_each { |flag| flag.update!(resolved_at: Time.current) } # Auto-resolve "cannot_automatically_win_buy_box" flags when we become the Buy Box winner if is_buy_box_winner catalog_item.amazon_catalog_item_flags .active .where(flag_type: 'cannot_automatically_win_buy_box') .find_each { |flag| flag.update!(resolved_at: Time.current) } end else # We don't have an offer on Amazon despite successful status check # This likely means we're out of stock - create appropriate flags Rails.logger.info { "Catalog item #{catalog_item.id}: No offer found for merchant #{merchant_id} despite successful status. Creating out_of_stock flag." } # Create out_of_stock flag if not already present unless catalog_item.amazon_catalog_item_flags.active.by_flag_type('out_of_stock').exists? catalog_item.amazon_catalog_item_flags.create!( flag_type: 'out_of_stock', issues: 'Out of stock! No active offer found on Amazon for this listing.', our_price_at_flagging: catalog_item.amazon_price_with_tax ) end # Also create no_buyable_offers flag (we have no buyable offer) unless catalog_item.amazon_catalog_item_flags.active.by_flag_type('no_buyable_offers').exists? catalog_item.amazon_catalog_item_flags.create!( flag_type: 'no_buyable_offers', issues: 'No Buyable Offers from us! We have no active offer on Amazon.', our_price_at_flagging: catalog_item.amazon_price_with_tax ) end # Resolve any "cannot_automatically_win_buy_box" flags - the issue isn't pricing catalog_item.amazon_catalog_item_flags .active .where(flag_type: 'cannot_automatically_win_buy_box') .find_each { |flag| flag.update!(resolved_at: Time.current) } end # Store all competitor seller IDs for discovery (excluding our own offers) store_competitor_seller_ids(catalog_item, offers, merchant_id) Rails.logger.debug { "Setting catalog_item #{catalog_item.id}: is_amz_buy_box_winner=#{catalog_item.is_amz_buy_box_winner}, is_amz_featured_merchant=#{catalog_item.is_amz_featured_merchant}" } catalog_item.save! ecl.complete! if ecl.can_complete? elsif status == 'nobuyableoffers' # just mark it as not buy box because there are no offers previous_buy_box_winner = catalog_item.is_amz_buy_box_winner is_buy_box_winner = false is_featured_merchant = false # Ensure we never set nil values to prevent database constraint violations catalog_item.is_amz_buy_box_winner = is_buy_box_winner.nil? ? false : !!is_buy_box_winner catalog_item.is_amz_featured_merchant = is_featured_merchant.nil? ? false : !!is_featured_merchant # Update change timestamp if buy box status changed catalog_item.amz_last_buy_box_winner_change = Time.current if previous_buy_box_winner != catalog_item.is_amz_buy_box_winner # Check if this is an out-of-stock situation vs a real "no buyable offers" issue # Look at the stored retailer_information for fulfillmentAvailability quantity is_out_of_stock = false begin amazon_info = catalog_item.retailer_information&.with_indifferent_access || {} # Check en_US listing first, then try other locales listing_info = amazon_info.dig(:en_US, :listing, :payload) || amazon_info.dig('en_US', 'listing', 'payload') || amazon_info.values.first&.dig(:listing, :payload) || amazon_info.values.first&.dig('listing', 'payload') if listing_info.present? fulfillment_availability = listing_info[:fulfillmentAvailability] || listing_info['fulfillmentAvailability'] || [] # Check if all fulfillment channels report 0 quantity if fulfillment_availability.any? quantities = fulfillment_availability.map { |fa| fa[:quantity] || fa['quantity'] || 0 } is_out_of_stock = quantities.all?(&:zero?) end end rescue StandardError => e Rails.logger.warn { "Error checking fulfillment availability for catalog_item #{catalog_item.id}: #{e.}" } end # Always create no_buyable_offers flag when status is NoBuyableOffers # This ensures the item appears in "No Buyable Offers" filter unless catalog_item.amazon_catalog_item_flags.active.by_flag_type('no_buyable_offers').exists? catalog_item.amazon_catalog_item_flags.create!( flag_type: 'no_buyable_offers', issues: 'No Buyable Offers! This listing has no active offers on Amazon.', our_price_at_flagging: catalog_item.amazon_price_with_tax ) Rails.logger.info { "Created no_buyable_offers flag for catalog_item #{catalog_item.id}" } end if is_out_of_stock # Also create out_of_stock flag to indicate WHY there are no buyable offers # This allows the item to appear in both "No Buyable Offers" AND "Out of Stock" filters unless catalog_item.amazon_catalog_item_flags.active.by_flag_type('out_of_stock').exists? catalog_item.amazon_catalog_item_flags.create!( flag_type: 'out_of_stock', issues: 'Out of stock! This listing has no inventory reported to Amazon.', our_price_at_flagging: catalog_item.amazon_price_with_tax ) Rails.logger.info { "Created out_of_stock flag for catalog_item #{catalog_item.id}" } end else # Not out of stock - resolve any stale out_of_stock flags catalog_item.amazon_catalog_item_flags .active .by_flag_type('out_of_stock') .find_each { |flag| flag.update!(resolved_at: Time.current) } end Rails.logger.debug { "Setting default values for catalog_item #{catalog_item.id} due to no buyable offers" } catalog_item.save! ecl.complete! if ecl.can_complete? else Rails.logger.error "Failed to retrieve buy box status! Status: #{status}" ecl.notes = "Failed to retrieve buy box status! Status: #{status}" # Set default values to prevent database constraint violations previous_buy_box_winner = catalog_item.is_amz_buy_box_winner catalog_item.is_amz_buy_box_winner = false catalog_item.is_amz_featured_merchant = false # Update change timestamp if buy box status changed catalog_item.amz_last_buy_box_winner_change = Time.current if previous_buy_box_winner != catalog_item.is_amz_buy_box_winner Rails.logger.debug { "Setting default values for catalog_item #{catalog_item.id} due to unexpected status: #{status}" } catalog_item.save! if ecl.can_error? ecl.error! else ecl.save end res << { locale => false } end else Rails.logger.error "orchestrator#pull_buy_box_status for catalog_item.id: #{catalog_item.id} yielded no ecl!" # Set default values to prevent database constraint violations previous_buy_box_winner = catalog_item.is_amz_buy_box_winner catalog_item.is_amz_buy_box_winner = false catalog_item.is_amz_featured_merchant = false # Update change timestamp if buy box status changed catalog_item.amz_last_buy_box_winner_change = Time.current if previous_buy_box_winner != catalog_item.is_amz_buy_box_winner Rails.logger.debug { "Setting default values for catalog_item #{catalog_item.id} due to no ECL data" } catalog_item.save! res << { locale => false } end res << { locale => true } end |
#pull_catalog_information(catalog_item) ⇒ Array<Hash>
Pulls catalog details from Amazon for every configured locale.
939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 |
# File 'app/services/edi/amazon/orchestrator.rb', line 939 def pull_catalog_information(catalog_item) item = catalog_item.item asin = catalog_item.amazon_asin raise "No ASIN for catalog_item #{catalog_item.id}" if asin.blank? res = [] locales.each do |locale| # en_US en_CA fr_CA ecl = catalog_item_information_retriever.process( asin: asin, locale:, file_info: { asin: asin, item_id: item.id, locale: } ) if ecl Rails.logger.debug { "orchestrator#pull_catalog_information, ecl: #{ecl}" } res << { locale => catalog_item_information_processor.process(ecl, catalog_item:, locale:) } else Rails.logger.error "orchestrator#pull_catalog_information for catalog item #{catalog_item.id} yielded no ecl (asin #{asin} not found?)" res << { locale => false } end end res end |
#pull_listing_information(catalog_item) ⇒ Array<Hash>
Pulls listing item details for a catalog item.
1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1237 def pull_listing_information(catalog_item) item = catalog_item.item sku = catalog_item.reported_vendor_sku # Listing info is marketplace-specific, not locale-specific. # The API call doesn't use the locale parameter, so we only need one call per marketplace. primary_locale = locales.first ecl = listing_item_information_retriever.process( sku: sku, locale: primary_locale, file_info: { sku: sku, item_id: item.id, catalog_item_id: catalog_item.id, locale: primary_locale } ) if ecl Rails.logger.debug { "orchestrator#pull_listing_information, ecl: #{ecl}" } [{ primary_locale => listing_item_information_processor.process(ecl, catalog_item_or_amazon_variation: catalog_item, locale: primary_locale) }] else Rails.logger.error "orchestrator#pull_listing_information for catalog item #{catalog_item.id} yielded no ecl (sku #{sku} not found?)" [{ primary_locale => false }] end end |
#pull_listing_schema(catalog_item, product_type = nil) ⇒ Array<Hash>
Pulls the listing schema for a catalog item's product type.
1357 1358 1359 1360 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1357 def pull_listing_schema(catalog_item, product_type = nil) product_type ||= catalog_item.amazon_effective_desired_product_type pull_schema(product_type) end |
#pull_schema(product_type) ⇒ Array<Hash>
Pulls the JSON schema for a product type across all locales.
1294 1295 1296 1297 1298 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1294 def pull_schema(product_type) locales.map do |locale| # en_US en_CA fr_CA { locale => pull_schema_for_locale(product_type, locale) } end end |
#pull_schema_for_locale(product_type, locale, amazon_schema: nil) ⇒ AmazonSchema?
Pulls and persists the JSON schema for a product type in one locale.
1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1306 def pull_schema_for_locale(product_type, locale, amazon_schema: nil) amazon_schema = nil if product_type.blank? Rails.logger.error "orchestrator#pull_schema_for_locale called with blank product_type for partner #{partner}, locale #{locale} — skipping" return amazon_schema end ErrorReporting.scoped(partner: partner, product_type: product_type, locale: locale) do ecl = listing_item_schema_retriever.process( product_type: product_type, locale: locale, file_info: { product_type: product_type, locale: locale } ) if ecl ErrorReporting.scoped(edi_commmunication_log_id: ecl.id) do Rails.logger.debug { "orchestrator#pull_listing_schema, ecl: #{ecl}" } json_hash = JSON.parse(ecl.data).with_indifferent_access schema_authenticated_url = json_hash.dig(:schema, :link, :resource) if schema_authenticated_url && (response = HTTP.get(schema_authenticated_url))&.status&.success? json_schema = response.body.to_s catalog = customer_catalog amazon_marketplace = catalog.amazon_marketplace amazon_schema ||= AmazonSchema.amazon_channel_seller.where(amazon_marketplace: amazon_marketplace.id, locale: locale, product_type: product_type).first_or_initialize amazon_schema.schema = json_schema amazon_schema.save! ecl.complete! if ecl.can_complete? else Rails.logger.error "Failed to retrieve data from #{schema_authenticated_url}: #{response&.status}" ecl.notes = "Failed to retrieve data from #{schema_authenticated_url}: #{response&.status}" if ecl.can_error? ecl.error! else ecl.save end end end else Rails.logger.error "orchestrator#pull_listing_schema for product_type #{product_type} yielded no ecl (schema #{product_type} not found?)" end end amazon_schema end |
#push_listing_from_amazon_variation(amazon_variation) ⇒ Array
Builds and sends an Amazon listing PUT message for a single variation.
1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1428 def push_listing_from_amazon_variation(amazon_variation) res = [] http_method = 'PUT' result = .process(amazon_variation, http_method:) if result.try(:success?) Rails.logger.debug { "orchestrator#push_listing_from_amazon_variation for amazon variation #{amazon_variation.id}, ecl: #{result.data}" } res << .process(result.data, catalog_item_or_variation: amazon_variation, http_method:) else Rails.logger.error "orchestrator#push_listing_from_amazon_variation for amazon variation #{amazon_variation.id} failed: #{result.try(:error) || 'listing messages not enabled for this partner'}" res << false end res end |
#push_listing_from_catalog_item(catalog_item, http_method: 'PATCH', attribute_actions: nil, use_fba_sku: false) ⇒ Array
Builds and sends an Amazon listing-items message for a catalog item.
Runs the listing message through the processor, then hands the resulting
ECL to the sender. When the partner has no listing-message path configured
the processor is an NullProcessor whose #process returns a falsy
non-Result, so the success check is guarded with #try.
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1375 def push_listing_from_catalog_item(catalog_item, http_method: 'PATCH', attribute_actions: nil, use_fba_sku: false) item = catalog_item.item raise 'Catalog item not an amazon item or not in the right state' unless catalog_item.amazon_catalog_item? && catalog_item.api_ready_state? res = [] result = .process(catalog_item, http_method:, attribute_actions:, use_fba_sku:) if result.try(:success?) Rails.logger.debug { "orchestrator#push_listing_from_catalog_item for catalog item #{catalog_item.id}, ecl: #{result.data}" } res << .process(result.data, catalog_item_or_variation: catalog_item, http_method:, use_fba_sku:) else # result is a non-Result (NullProcessor#process returns logger.debug => false) when listing # messages aren't configured for the partner; #try keeps that from raising NoMethodError. fba_label = use_fba_sku ? ' (FBA)' : ' (FBM)' error = result.try(:error) || 'listing messages not enabled for this partner' Rails.logger.error "orchestrator#push_listing_from_catalog_item for catalog item #{catalog_item.id}#{fba_label} failed: #{error}. " \ "ASIN: #{item.amazon_asin}, SKU: #{catalog_item.sku}, Partner: #{partner}." res << { error: true, message: error } end res end |
#return_notification_message_enabled? ⇒ Boolean
Return notifications are not sent to Amazon.
477 478 479 |
# File 'app/services/edi/amazon/orchestrator.rb', line 477 def false end |
#return_notification_message_processor ⇒ Edi::NullProcessor
Return notification processing is disabled for Amazon.
484 485 486 |
# File 'app/services/edi/amazon/orchestrator.rb', line 484 def Edi::NullProcessor.new(self, :return_notification_message_processor, ) end |
#returns_report_enabled? ⇒ Boolean
Whether this partner is configured to fetch the returns report.
491 492 493 494 495 496 497 |
# File 'app/services/edi/amazon/orchestrator.rb', line 491 def returns_report_enabled? # `try` (not bare call) because per-partner attr_accessors only exist for # keys present in the partner config hash — partners without an explicit # `returns_report_enabled:` key stay dark instead of raising NameError. # Same pattern as `inventory_message_enabled?` after AppSignal #5595. try(:returns_report_enabled).present? end |
#returns_report_processor ⇒ ReturnsReportProcessor
Processor that applies the returns report to RMAs.
547 548 549 550 551 |
# File 'app/services/edi/amazon/orchestrator.rb', line 547 def returns_report_processor return Edi::NullProcessor.new(self, :returns_report_processor, ) unless returns_report_enabled? ReturnsReportProcessor.new(self, ) end |
#returns_report_retriever ⇒ ReturnsReportRetriever
Retriever that fetches the Seller Central returns report.
502 503 504 505 506 |
# File 'app/services/edi/amazon/orchestrator.rb', line 502 def returns_report_retriever return Edi::NullProcessor.new(self, :returns_report_retriever, ) unless returns_report_enabled? ReturnsReportRetriever.new(self, ) end |
#ship_code_mapper ⇒ ShipCodeMapper
Mapper that converts our carrier codes to Amazon ship codes.
1588 1589 1590 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1588 def ship_code_mapper ShipCodeMapper.new(self) end |
#store_competitor_seller_ids(catalog_item, offers, our_merchant_id) ⇒ void
This method returns an undefined value.
Stores competitor seller IDs discovered from Buy Box status.
Creates AmazonCompetitor records without names for any new seller IDs.
1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 |
# File 'app/services/edi/amazon/orchestrator.rb', line 1600 def store_competitor_seller_ids(catalog_item, offers, our_merchant_id) return if offers.blank? competitor_offers = offers.reject { |o| o[:SellerId] == our_merchant_id } return if competitor_offers.empty? competitor_offers.each do |offer| seller_id = offer[:SellerId] next if seller_id.blank? # Create a placeholder record if this seller doesn't exist yet # Name will be nil until someone identifies them AmazonCompetitor.find_or_create_by(seller_id: seller_id) do |competitor| competitor.notes = "Discovered on #{Time.current.strftime('%Y-%m-%d')} from ASIN #{catalog_item.amazon_asin}" end rescue ActiveRecord::RecordNotUnique # Race condition - record already exists, which is fine end end |