Class: Edi::Wayfair::ProductAdditionSender
- Inherits:
-
BaseEdiService
- Object
- BaseService
- BaseEdiService
- Edi::Wayfair::ProductAdditionSender
- Includes:
- CatalogApiTransport
- Defined in:
- app/services/edi/wayfair/product_addition_sender.rb
Overview
Programmatic new-product onboarding via Wayfair's Product Addition API
(productAddition { submitV2 }) — the API replacement for the formula-locked
Quick-Upload workbook. Builds each product as a flat list of attribute
key→value pairs — every REQUIRED attribute for the class, resolved by
ProductAdditionAttributes (namespaced core:: / media:: /
variantGrouping:: / shippingAndFulfillment:: / price:: / propSixtyFive::
plus the numeric class attributes) — submits a batch, and polls submissions
for the async two-loop validation result.
Wire format reverse-engineered + verified in sandbox 2026-06-29 (schema
introspection + a successful submitV2 call returning a productAdditionRequestId).
IMPORTANT CONSTRAINTS:
- Product Addition is US / UK only — Wayfair rejects CANADA with
"The market is not supported. Supported markets are Wayfair US and Wayfair
UK". CA new products still go through the Quick-Upload workbook. - US production Submit scope is enabled (self-served 2026-06-29: a
sandboxsubmitV2whose payload passes first-loop validation unlocks the
production scope). A complete ProductAdditionAttributes payload is what
made that pass — an incomplete one fails first-loop and never unlocks. submitV2supportsvalidateOnly(underoptions), so it can be dry-run
server-side without creating anything. Default tovalidate_only: true
until a human has curated the Edi::Wayfair::ProductAdditionAttributes#defaulted
attributes for the batch.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- CATALOG_API_URL =
Production Product Catalog API endpoint.
'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_BATCH =
Max products per submitV2 request (Wayfair cap; >500 needs multi-batch jobContext).
500- SUBMIT_MUTATION =
submitV2 mutation — return selection verified against the live schema
(ProductResult{productId,status,validationFlaws},
AttributeValidationFlaw{attributeId,flawType,flaw}). <<~GRAPHQL.squish mutation SubmitV2($request: SubmitProductAdditionsRequestV2!) { productAddition { submitV2(request: $request) { productAdditionRequestId batchId status processedProducts productResults { productId status validationFlaws { attributeId flawType flaw } } } } } GRAPHQL
- SUBMISSIONS_QUERY =
Poll the async submission status by productAdditionRequestId.
NB:submissionsreturnsProductAddition.validationFlaws: ValidationFlaw
(questionId), NOT the submit mutation'sAttributeValidationFlaw
(attributeId). Verified via live schema introspection 2026-06-29. <<~GRAPHQL.squish query Submissions($request: GetProductAdditionsRequest!) { submissions(request: $request) { requestId supplierPartNumber classId status validationStatus submissionStatus validationFlaws { questionId flawType flaw } submissionError } } GRAPHQL
Constants included from CatalogApiTransport
CatalogApiTransport::CATALOG_API_BASE, 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
-
#build_proposed_product(catalog_item, variant_grouping: nil) ⇒ Hash?
Build the { productId, classId, attributes } payload for one item: core identity + media + variant role + the schema-driven class attributes.
-
#poll(request_id) ⇒ Array<Hash>
Poll submission status.
-
#process(catalog_items:, validate_only: true, ignore_warnings: true, variant_grouping_for: nil) ⇒ Result
Submit a batch of catalog items as new products.
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, #tagged_logger
Constructor Details
This class inherits a constructor from Edi::BaseEdiService
Instance Method Details
#build_proposed_product(catalog_item, variant_grouping: nil) ⇒ Hash?
Build the { productId, classId, attributes } payload for one item:
core identity + media + variant role + the schema-driven class attributes.
Returns nil when the item has no Wayfair class, or when attribute generation
fails/yields nothing — so a build failure drops the product rather than
submitting an empty, invalid payload.
133 134 135 136 137 138 139 140 141 142 |
# File 'app/services/edi/wayfair/product_addition_sender.rb', line 133 def build_proposed_product(catalog_item, variant_grouping: nil) class_id = catalog_item.wayfair_effective_taxonomy_category_id sku = catalog_item.sku.presence || catalog_item.store_item&.item&.sku.presence return nil if class_id.blank? || sku.blank? attributes = attributes_for(catalog_item, sku, variant_grouping:) return nil if attributes.blank? { productId: sku, classId: class_id.to_s, attributes: } end |
#poll(request_id) ⇒ Array<Hash>
Poll submission status.
147 148 149 150 151 152 153 154 155 |
# File 'app/services/edi/wayfair/product_addition_sender.rb', line 147 def poll(request_id) transport = build_catalog_transport vars = { request: { supplierId: orchestrator.try(:warehouse_code).to_s, ids: [request_id] } } payload = { query: SUBMISSIONS_QUERY, variables: vars, operationName: 'Submissions' }.to_json http = transport.send_request('POST', catalog_api_url, payload, request_headers(transport))[:http_res] JSON.parse(http.body.to_s).dig('data', 'submissions') || [] rescue JSON::ParserError, NoMethodError [] end |
#process(catalog_items:, validate_only: true, ignore_warnings: true, variant_grouping_for: nil) ⇒ Result
Submit a batch of catalog items as new products.
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 |
# File 'app/services/edi/wayfair/product_addition_sender.rb', line 97 def process(catalog_items:, validate_only: true, ignore_warnings: true, variant_grouping_for: nil) items = Array(catalog_items) return Result.new(success: false, errors: ["Batch exceeds #{MAX_BATCH}"]) if items.size > MAX_BATCH built = items.index_with do |item| # A failing grouping resolver must not abort the batch — and must not # silently submit a family member ungrouped (the exact listing debt this # Phase-0 path removes) — so the item is dropped into Result#skipped. build_proposed_product(item, variant_grouping: variant_grouping_for&.call(item)) rescue StandardError => e logger.warn "Wayfair ProductAddition: variant_grouping_for failed for #{item.try(:sku) || item.try(:id)}: #{e.}" nil end products = built.values.compact skipped = built.select { |_item, product| product.nil? }.keys.map { |item| item.try(:sku).presence || "catalog_item ##{item.try(:id)}" } return Result.new(success: false, errors: ['No products could be built'], skipped:) if products.empty? request = { marketContext: market_context, jobContext: { productAdditionRequestId: nil, hasMoreProducts: false }, options: { validateOnly: validate_only, ignoreWarnings: ignore_warnings, rejectAllOnErrors: false }, proposedProductAdditions: products } result = send_submit(request) skipped.any? ? result.with(skipped:) : result end |