Class: Edi::Wayfair::CatalogReadV2Retriever
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Wayfair::CatalogReadV2Retriever
- Includes:
- CatalogApiTransport
- Defined in:
- app/services/edi/wayfair/catalog_read_v2_retriever.rb
Overview
Pulls Wayfair's Catalog Read v2 supplierCatalogItems query and persists each
item's insights (Wayfair's own problems / warnings / opportunities) and its
live status (catalogItemStatus → wayfair_status) onto the matching
CatalogItem, so ListingIssues::WayfairAdapter can map them to listing_issues
and our DB stays in sync with what Wayfair actually has on-site. Reads only —
never mutates the Wayfair catalog.
Why v2: unlike the v1 supplier-catalog query, v2 exposes insights (with stable
insightTypeId + a resolution.url) and catalogItemStatus — Wayfair computing
the missing-attribute / quality anomalies for us. Production access is per
region and gated behind the sandbox test (see
doc/tasks/202606281210_WAYFAIR_API_V2_ACCESS.md).
Also persists the listing's populated taxonomy attributes
(attributes.chosenAttributeValues) — the listing's real spec coverage, which
ListingIssues::WayfairAdapter diffs against WayfairSchema to report specs
Wayfair genuinely has no value for.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- CATALOG_API_URL =
Product Catalog API endpoint (v2 read shares the product-catalog-api host).
'https://api.wayfair.io/v1/product-catalog-api/graphql'- SANDBOX_CATALOG_API_URL =
Sandbox endpoint.
'https://api.wayfair.io/sandbox/v1/product-catalog-api/graphql'- MAX_PAGE_SIZE =
Hard page-size cap from the API (30 catalog items per response).
30- DEFAULT_PAGE_SIZE =
SKUs per request. Well under MAX_PAGE_SIZE:
attributesmakes each
response heavy enough that 30-item batches read-time out against the live
catalog (~2.5s per 10-SKU batch, measured 2026-07-25). 10- QUERY =
Only the fields we persist — insights + status + populated attribute ids.
Union must branch both ways. <<~GRAPHQL.squish query SupplierCatalogItems($input: SupplierCatalogItemsInput!) { supplierCatalogItems(input: $input) { ... on SupplierCatalogItems { paginationInfo { page hasNextPage } catalogItems { supplierPartNumber catalogItemStatus class { classId className } attributes { attribute { attributeId } chosenAttributeValues { value } } insights { problems { insightId title explanation insightTypeId resolution { url } } warnings { insightId title explanation insightTypeId resolution { url } } opportunities { insightId title explanation insightTypeId resolution { url } } } } } ... on SupplierCatalogItemsError { httpError { code message } internalError { code message } } } } GRAPHQL
Constants included from CatalogApiTransport
Edi::Wayfair::CatalogApiTransport::CATALOG_API_BASE, Edi::Wayfair::CatalogApiTransport::CATALOG_AUTH_URL
Constants included from RequestIdentifiable
RequestIdentifiable::REQUEST_ID_HEADERS
Constants included from AddressAbbreviator
AddressAbbreviator::MAX_LENGTH
Instance Attribute Summary
Attributes inherited from BaseEdiService
Attributes inherited from BaseService
Instance Method Summary collapse
-
#pull(batch_size: DEFAULT_PAGE_SIZE, max_batches: nil) ⇒ Result
Ask Wayfair about OUR active SKUs in batches and persist what comes back.
Methods inherited from BaseEdiService
#amazon_feed_product_type, #duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #onboard_ordered_catalog_items, #report_order_creation_issues, #safe_process_edi_communication_log
Methods included from RequestIdentifiable
Methods included from AddressAbbreviator
#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes
Methods inherited from BaseService
#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #process, #tagged_logger
Constructor Details
This class inherits a constructor from Edi::BaseEdiService
Instance Method Details
#pull(batch_size: DEFAULT_PAGE_SIZE, max_batches: nil) ⇒ Result
Ask Wayfair about OUR active SKUs in batches and persist what comes back.
Deliberately NOT a paginationOptions walk of the whole supplier catalog:
measured 2026-07-25, unfiltered pagination returned 360 CA catalog entries
and silently omitted 56 of our active CA SKUs that the very same API
returns when asked for them by supplierPartNumbers. Driving off our own
catalog also halves the request count (407 active items vs 953 catalog
entries) and makes "Wayfair has no such SKU" an answerable question.
⚠️ The filter drops items nondeterministically. A 10-SKU batch can come
back with 6, and the 4 missing ones return fine when asked again (measured
2026-07-25: 10 of 14 "absent" CA SKUs were present on a single-SKU re-ask).
So anything unreturned is re-asked once, one SKU per request, before we call
it absent. Never treat a first-pass miss as a catalog gap.
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 |
# File 'app/services/edi/wayfair/catalog_read_v2_retriever.rb', line 99 def pull(batch_size: DEFAULT_PAGE_SIZE, max_batches: nil) supplier_id = orchestrator.try(:warehouse_code).to_s catalog = orchestrator.customer&.catalog return Result.new(errors: ['No catalog for orchestrator']) unless catalog items_by_sku = active_items_by_sku(catalog) return Result.new(errors: ['No active catalog items']) if items_by_sku.empty? state = { batches: 0, requested: 0, matched: 0, updated: 0, errors: [], seen: Set.new } slices = items_by_sku.keys.each_slice(batch_size.clamp(1, MAX_PAGE_SIZE)).to_a slices = slices.first(max_batches) if max_batches slices.each { |skus| consume(supplier_id, skus, items_by_sku, state) } # Retry pass: one identifier per request, so a dropped item can't hide behind # a neighbour. Scoped to what we actually asked about — under `max_batches` # the un-asked remainder is out of scope, and retrying it would turn the cap # into one request per SKU for the whole rest of the catalog. Identifiers # whose item was already found under its *other* identifier are skipped. asked = slices.flatten pending = asked.reject { |sku| state[:seen].include?(items_by_sku[sku]&.id) } pending.each { |sku| consume(supplier_id, [sku], items_by_sku, state) } Result.new(batches: state[:batches], requested: state[:requested], matched: state[:matched], updated: state[:updated], errors: state[:errors], unreturned: asked.reject { |sku| state[:seen].include?(items_by_sku[sku]&.id) }) end |