Class: Edi::Amazon::Orchestrator

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

#config, #logger, #options

Class Method Summary collapse

Instance Method Summary collapse

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.

Parameters:

Options Hash (options):

  • logger (Logger)

    logger to use (defaults to Rails.logger)



390
391
392
# File 'app/services/edi/amazon/orchestrator.rb', line 390

def self.execute_feed_submission_result_processor(options = {})
  orchestrators(options).each { |o| o.feed_submission_result_processor.process }
end

.partnersHash{Symbol=>Hash}

Partner configuration hash for every Amazon marketplace we serve.

Returns:

  • (Hash{Symbol=>Hash})

    partner key → configuration options



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.

Parameters:

  • limit (Integer, nil) (defaults to: nil)

    how many records to process per partner

  • skus (Array<String>, String, nil) (defaults to: nil)

    specific SKUs to process

  • force (Boolean) (defaults to: false)

    pull even when information is already present

Returns:

  • (Hash{Symbol=>Array<Hash>})

    partner-keyed result rows



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_processorAPlusContentProcessor

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, options) if a_plus_content_remote_path.blank?

  APlusContentProcessor.new(self, options)
end

#a_plus_content_remote_pathString?

SP-API base URL for A+ content operations.

Returns:

  • (String, nil)


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_retrieverAPlusContentRetriever

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, options) if a_plus_content_remote_path.blank?

  APlusContentRetriever.new(self, options)
end

#acknowledge_message_enabled?Boolean

Whether this partner is configured to send acknowledgements.

Returns:

  • (Boolean)


447
448
449
# File 'app/services/edi/amazon/orchestrator.rb', line 447

def acknowledge_message_enabled?
  try(:order_message_remote_path).present?
end

#acknowledge_message_senderAcknowledgeMessageSender

Sender that posts order acknowledgements to Amazon.



454
455
456
457
458
# File 'app/services/edi/amazon/orchestrator.rb', line 454

def acknowledge_message_sender
  return Edi::NullProcessor.new(self, :acknowledge_message_sender, options) unless acknowledge_message_enabled?

  AcknowledgeMessageSender.new(self, options)
end

#buy_box_status_enabled?Boolean

Whether this partner can pull Buy Box status.

Returns:

  • (Boolean)


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_retrieverBuyBoxStatusRetriever

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, options) unless buy_box_status_enabled?

  BuyBoxStatusRetriever.new(self, options)
end

#buy_shipping_clientShipWithAmazon?

Client for purchasing Amazon shipping labels.

Returns:



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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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_processorCatalogItemInformationProcessor

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, options) unless catalog_item_information_enabled?

  CatalogItemInformationProcessor.new(self, options)
end

#catalog_item_information_retrieverCatalogItemInformationRetriever

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, options) unless catalog_item_information_enabled?

  CatalogItemInformationRetriever.new(self, options)
end

#confirm_message_enabled?Boolean

Whether this partner is configured to send ship confirmations.

Returns:

  • (Boolean)


422
423
424
# File 'app/services/edi/amazon/orchestrator.rb', line 422

def confirm_message_enabled?
  try(:order_message_remote_path).present?
end

#confirm_message_processorConfirmMessageProcessor

Processor that builds ship confirmations.



429
430
431
432
433
# File 'app/services/edi/amazon/orchestrator.rb', line 429

def confirm_message_processor
  return Edi::NullProcessor.new(self, :confirm_message_processor, options) unless confirm_message_enabled?

  ConfirmMessageProcessor.new(self, options)
end

#confirm_message_senderConfirmMessageSender

Sender that posts ship confirmations to Amazon.



438
439
440
441
442
# File 'app/services/edi/amazon/orchestrator.rb', line 438

def confirm_message_sender
  return Edi::NullProcessor.new(self, :confirm_message_sender, options) unless confirm_message_enabled?

  ConfirmMessageSender.new(self, options)
end

#confirm_outbound_processing?Boolean

Amazon uses a two-stage outbound processing model.

Returns:

  • (Boolean)


931
932
933
# File 'app/services/edi/amazon/orchestrator.rb', line 931

def confirm_outbound_processing?
  true
end

#connection_infoHash

SP-API connection identifiers for this partner.

Returns:

  • (Hash)


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.

Parameters:

  • catalog_item (CatalogItem, nil) (defaults to: nil)

    the catalog item to delete (mutually exclusive with amazon_variation)

  • amazon_variation (VariantGroup, nil) (defaults to: nil)

    the Amazon parent-listing row to delete

  • http_method (String) (defaults to: 'DELETE')

    SP-API verb, defaults to 'DELETE'

  • attribute_actions (Hash, nil) (defaults to: nil)

    optional per-attribute patch actions

  • use_fba_sku (Boolean) (defaults to: false)

    target the FBA SKU variant

Returns:

  • (Array)

    sender results, or [false] when the processor yields a non-Result/failure

Raises:

  • (RuntimeError)

    if neither argument is given, or the catalog item is not an Amazon item



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 = delete_listing_message_processor.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 << listing_message_sender.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_processorDeleteListingMessageProcessor

Processor that builds listing delete messages.



796
797
798
799
800
# File 'app/services/edi/amazon/orchestrator.rb', line 796

def delete_listing_message_processor
  return Edi::NullProcessor.new(self, :delete_listing_message_processor, options) unless listing_message_enabled?

  DeleteListingMessageProcessor.new(self, options)
end

#discontinue_flow_processorDiscontinueFlowProcessor

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, options) unless listing_message_enabled?

  DiscontinueFlowProcessor.new(self, options)
end

#execute_discontinue_flowBoolean

Runs the discontinue-listing flow for this partner.

Returns:

  • (Boolean)

    false when the partner is inactive



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_flowvoid

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

  fba_order_message_retriever.process
  fba_order_message_processor.process
end

#execute_inventory_flow(use_delta_since_last_message: false) ⇒ Boolean

Generates and sends the daily inventory feed.

Parameters:

  • use_delta_since_last_message (Boolean) (defaults to: false)

    only include changes since the last feed

Returns:

  • (Boolean)

    false when the partner is inactive



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

  inventory_message_processor.process(use_delta_since_last_message:) # Generate an inventory feed message
  inventory_message_sender.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_flowBoolean

Generates and sends the daily listing message feed.

Returns:

  • (Boolean)

    false when the partner is inactive



912
913
914
915
916
917
# File 'app/services/edi/amazon/orchestrator.rb', line 912

def execute_listing_message_feed_flow
  return false unless active

  listing_message_feed_processor.process # Generate a listing message feed message
  listing_message_feed_sender.process # Send it
end

#execute_order_flowvoid

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

  order_message_retriever.process
  sleep(1)
  order_message_processor.process
  sleep(1)
  acknowledge_message_sender.process
  sleep(1)
  confirm_message_sender.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_verificationvoid

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.

Parameters:

  • use_delta_since_last_message (Boolean) (defaults to: false)

    only include changes since the last feed

Returns:

  • (Boolean)

    false when the partner is inactive



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

  price_message_processor.process(use_delta_since_last_message:) # Generate a price feed message
  price_message_sender.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+.

Parameters:

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

    explicit window start.

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

    explicit window end.



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.

Returns:

  • (Boolean)


1529
1530
1531
# File 'app/services/edi/amazon/orchestrator.rb', line 1529

def fba_order_message_enabled?
  try(:order_message_remote_path).present? && try(:fba_store_id).present?
end

#fba_order_message_processorFbaOrderMessageProcessor

Processor that imports fetched FBA order messages.



1545
1546
1547
1548
1549
# File 'app/services/edi/amazon/orchestrator.rb', line 1545

def fba_order_message_processor
  return Edi::NullProcessor.new(self, :fba_order_message_processor, options) unless fba_order_message_enabled?

  FbaOrderMessageProcessor.new(self, options)
end

#fba_order_message_retrieverFbaOrderMessageRetriever

Retriever that fetches FBA order messages.



1536
1537
1538
1539
1540
# File 'app/services/edi/amazon/orchestrator.rb', line 1536

def fba_order_message_retriever
  return Edi::NullProcessor.new(self, :fba_order_message_retriever, options) unless fba_order_message_enabled?

  FbaOrderMessageRetriever.new(self, options)
end

#fba_pull_orders_by_po_numbers(po_numbers) ⇒ Object

Pulls FBA orders for the given PO numbers.

Parameters:

  • po_numbers (Array<String>)

    Amazon PO numbers to fetch

Returns:

  • (Object)

    retriever result



1565
1566
1567
# File 'app/services/edi/amazon/orchestrator.rb', line 1565

def fba_pull_orders_by_po_numbers(po_numbers)
  fba_order_message_retriever.process(po_numbers:)
end

#feed_submission_result_processorFeedSubmissionResultProcessor

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, options)
end

#inconclusive_buy_box_key(catalog_item) ⇒ String

Parameters:

Returns:

  • (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.

Returns:

  • (Boolean)


556
557
558
559
560
561
562
# File 'app/services/edi/amazon/orchestrator.rb', line 556

def inventory_message_enabled?
  # `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_processorInventoryMessageProcessor

Processor that builds the inventory feed message.



567
568
569
570
571
# File 'app/services/edi/amazon/orchestrator.rb', line 567

def inventory_message_processor
  return Edi::NullProcessor.new(self, :inventory_message_processor, options) unless inventory_message_enabled?

  InventoryMessageProcessor.new(self, options)
end

#inventory_message_senderInventoryMessageSender

Sender that posts the inventory feed to Amazon.



576
577
578
579
580
# File 'app/services/edi/amazon/orchestrator.rb', line 576

def inventory_message_sender
  return Edi::NullProcessor.new(self, :inventory_message_sender, options) unless inventory_message_enabled?

  InventoryMessageSender.new(self, options)
end

#invoice_message_enabled?Boolean

Invoices are not sent to Amazon.

Returns:

  • (Boolean)


463
464
465
# File 'app/services/edi/amazon/orchestrator.rb', line 463

def invoice_message_enabled?
  false
end

#invoice_message_processorEdi::NullProcessor

Invoice processing is disabled for Amazon.

Returns:



470
471
472
# File 'app/services/edi/amazon/orchestrator.rb', line 470

def invoice_message_processor
  Edi::NullProcessor.new(self, :invoice_message_processor, options)
end

#listing_item_information_processorListingItemInformationProcessor

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, options) unless listing_message_enabled?

  ListingItemInformationProcessor.new(self, options)
end

#listing_item_information_retrieverListingItemInformationRetriever

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, options) unless listing_message_enabled?

  ListingItemInformationRetriever.new(self, options)
end

#listing_item_schema_processorListingItemSchemaProcessor

Processor that persists fetched listing JSON schemas.

Returns:

  • (ListingItemSchemaProcessor)


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, options) unless listing_schema_message_enabled?

  ListingItemSchemaProcessor.new(self, options)
end

#listing_item_schema_retrieverListingItemSchemaRetriever

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, options) unless listing_schema_message_enabled?

  ListingItemSchemaRetriever.new(self, options)
end

#listing_message_enabled?Boolean

Whether this partner is configured to push listing items.

Returns:

  • (Boolean)


739
740
741
# File 'app/services/edi/amazon/orchestrator.rb', line 739

def listing_message_enabled?
  try(:listing_message_remote_path).present?
end

#listing_message_feed_enabled?Boolean

Whether this partner is configured to push listing message feeds.

Returns:

  • (Boolean)


614
615
616
617
618
619
# File 'app/services/edi/amazon/orchestrator.rb', line 614

def listing_message_feed_enabled?
  # 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_processorListingMessageFeedProcessor

Processor that builds the listing message feed.



633
634
635
636
637
# File 'app/services/edi/amazon/orchestrator.rb', line 633

def listing_message_feed_processor
  return Edi::NullProcessor.new(self, :listing_message_feed_processor, options) unless listing_message_feed_enabled?

  ListingMessageFeedProcessor.new(self, options)
end

#listing_message_feed_senderListingMessageFeedSender

Sender that posts the listing message feed to Amazon.



624
625
626
627
628
# File 'app/services/edi/amazon/orchestrator.rb', line 624

def listing_message_feed_sender
  return Edi::NullProcessor.new(self, :listing_message_feed_sender, options) unless listing_message_feed_enabled?

  ListingMessageFeedSender.new(self, options)
end

#listing_message_processorListingMessageProcessor

Processor that builds listing item messages.



787
788
789
790
791
# File 'app/services/edi/amazon/orchestrator.rb', line 787

def listing_message_processor
  return Edi::NullProcessor.new(self, :listing_message_processor, options) unless listing_message_enabled?

  ListingMessageProcessor.new(self, options)
end

#listing_message_senderListingMessageSender

Sender that posts listing item messages to Amazon.



778
779
780
781
782
# File 'app/services/edi/amazon/orchestrator.rb', line 778

def listing_message_sender
  return Edi::NullProcessor.new(self, :listing_message_sender, options) unless listing_message_enabled?

  ListingMessageSender.new(self, options)
end

#listing_schema_message_enabled?Boolean

Whether this partner is configured to pull listing schemas.

Returns:

  • (Boolean)


746
747
748
# File 'app/services/edi/amazon/orchestrator.rb', line 746

def listing_schema_message_enabled?
  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.

Parameters:



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.

Returns:

  • (Boolean)


397
398
399
# File 'app/services/edi/amazon/orchestrator.rb', line 397

def order_message_enabled?
  try(:order_message_remote_path).present?
end

#order_message_processorOrderMessageProcessor

Processor that imports fetched Amazon orders.



413
414
415
416
417
# File 'app/services/edi/amazon/orchestrator.rb', line 413

def order_message_processor
  return Edi::NullProcessor.new(self, :order_message_processor, options) unless order_message_enabled?

  OrderMessageProcessor.new(self, options)
end

#order_message_retrieverOrderMessageRetriever

Retriever that fetches new Amazon orders.



404
405
406
407
408
# File 'app/services/edi/amazon/orchestrator.rb', line 404

def order_message_retriever
  return Edi::NullProcessor.new(self, :order_message_retriever, options) unless order_message_enabled?

  OrderMessageRetriever.new(self, options)
end

#order_status_verification_enabled?Boolean

Whether this partner is configured to verify order statuses.

Returns:

  • (Boolean)


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_verifierOrderStatusVerifier

Retriever that verifies pending order statuses with Amazon.

Returns:



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, options) unless order_status_verification_enabled?

  OrderStatusVerifier.new(self, options)
end

#orders_with_amazon_id_po(order_id) ⇒ Array<Order>

Finds local orders matching an Amazon order ID or PO number.

Parameters:

  • order_id (String)

    the Amazon order ID or PO number

Returns:

  • (Array<Order>)

    matching orders



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.

Parameters:

  • limit (Integer, nil) (defaults to: nil)

    how many records to process

  • skus (Array<String>, String, nil) (defaults to: nil)

    specific SKUs to process

  • force (Boolean) (defaults to: false)

    pull even when information is already present

Returns:

  • (Array<Hash>)

    result rows with name, catalog_item_id, and pull results



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.

Returns:

  • (Boolean)


585
586
587
588
589
590
591
# File 'app/services/edi/amazon/orchestrator.rb', line 585

def price_message_enabled?
  # 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_processorPriceMessageProcessor

Processor that builds the price feed message.



605
606
607
608
609
# File 'app/services/edi/amazon/orchestrator.rb', line 605

def price_message_processor
  return Edi::NullProcessor.new(self, :price_message_processor, options) unless price_message_enabled?

  PriceMessageProcessor.new(self, options)
end

#price_message_senderPriceMessageSender

Sender that posts the price feed to Amazon.

Returns:



596
597
598
599
600
# File 'app/services/edi/amazon/orchestrator.rb', line 596

def price_message_sender
  return Edi::NullProcessor.new(self, :price_message_sender, options) unless price_message_enabled?

  PriceMessageSender.new(self, options)
end

#pull_amazon_variation_listing_information(amazon_variation) ⇒ Array<Hash>

Pulls listing item details for an Amazon variation parent.

Parameters:

  • amazon_variation (VariantGroup)

    the variation to query

Returns:

  • (Array<Hash>)

    locale-keyed results



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.

Parameters:

  • catalog_item (CatalogItem)

    the Amazon catalog item to check

Returns:

  • (Array<Hash>)

    locale-keyed boolean results



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.message}" }
      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.

Parameters:

  • catalog_item (CatalogItem)

    the Amazon catalog item to enrich

Returns:

  • (Array<Hash>)

    locale-keyed results



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.

Parameters:

  • catalog_item (CatalogItem)

    the Amazon catalog item to query

Returns:

  • (Array<Hash>)

    locale-keyed results



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.

Parameters:

  • catalog_item (CatalogItem)

    the catalog item whose product type defines the schema

  • product_type (String, nil) (defaults to: nil)

    override product type; defaults to the catalog item's

Returns:

  • (Array<Hash>)

    locale-keyed schema results



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.

Parameters:

  • product_type (String)

    the Amazon product type identifier

Returns:

  • (Array<Hash>)

    locale-keyed AmazonSchema records



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.

Parameters:

  • product_type (String)

    the Amazon product type identifier

  • locale (String)

    the locale to fetch

  • amazon_schema (AmazonSchema, nil) (defaults to: nil)

    optional existing record to update

Returns:

  • (AmazonSchema, nil)

    the persisted schema, or nil when skipped



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.

Parameters:

  • amazon_variation (VariantGroup)

    the Amazon parent-listing row to push

Returns:

  • (Array)

    sender results, or [false] when the processor yields a non-Result/failure



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 = listing_message_processor.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 << listing_message_sender.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.

Parameters:

  • catalog_item (CatalogItem)

    the Amazon catalog item to push

  • http_method (String) (defaults to: 'PATCH')

    SP-API verb ('PUT' to create/replace, 'PATCH' to update)

  • attribute_actions (Hash, nil) (defaults to: nil)

    optional per-attribute patch actions

  • use_fba_sku (Boolean) (defaults to: false)

    push the FBA SKU variant instead of FBM

Returns:

  • (Array)

    sender results, or [{ error: true, message: ... }] on failure

Raises:

  • (RuntimeError)

    if the catalog item is not an Amazon item in an api-ready state



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 = listing_message_processor.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 << listing_message_sender.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.

Returns:

  • (Boolean)


477
478
479
# File 'app/services/edi/amazon/orchestrator.rb', line 477

def return_notification_message_enabled?
  false
end

#return_notification_message_processorEdi::NullProcessor

Return notification processing is disabled for Amazon.

Returns:



484
485
486
# File 'app/services/edi/amazon/orchestrator.rb', line 484

def return_notification_message_processor
  Edi::NullProcessor.new(self, :return_notification_message_processor, options)
end

#returns_report_enabled?Boolean

Whether this partner is configured to fetch the returns report.

Returns:

  • (Boolean)


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_processorReturnsReportProcessor

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, options) unless returns_report_enabled?

  ReturnsReportProcessor.new(self, options)
end

#returns_report_retrieverReturnsReportRetriever

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, options) unless returns_report_enabled?

  ReturnsReportRetriever.new(self, options)
end

#ship_code_mapperShipCodeMapper

Mapper that converts our carrier codes to Amazon ship codes.

Returns:



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.

Parameters:

  • catalog_item (CatalogItem)

    the catalog item being checked

  • offers (Array<Hash>)

    Buy Box offers payload

  • our_merchant_id (String)

    our Amazon merchant identifier



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