@if ($this->platformKey() === 'shopify')
{{-- ── V2 Processes ── --}}
Sync Processes
Bulk · Update
ShopifyBatchSyncJob
Jobs/ShopifyBatchSyncJob.php
Triggered by "Update All / Push All" in V2. Updates products that already have a Shopify ID. Dispatched in chunks of 3,000.
Bulk · Create
ShopifyBatchCreateJob
Jobs/ShopifyBatchCreateJob.php
Triggered by "Create All" in V2, or the row "List" button (single product). Creates products with no Shopify ID. Parses result JSONL to obtain new IDs.
Bulk · Price + Cost
BulkPriceUpdateJob
Jobs/BulkPriceUpdateJob.php
Triggered by the repricing pipeline (fan-in after RecalculateProductChannelPrice). Pushes variant price and cost via productVariantsBulkUpdate in a Shopify bulk operation — fast and correct on both old and new product models.
{{-- ── Field Matrix ── --}}
Field Coverage Matrix
Covered
Gap — not pushed by this job
Partial / workaround
N/A — deliberately out of scope
Field BatchSync
"Update All"
BatchCreate
"Create All" / row "List"
BulkPriceUpdate
repricing pipeline
title ✓ JSONL bulk ✓ JSONL bulk —
descriptionHtml ✓ JSONL bulk ✓ JSONL bulk —
vendor ✓ JSONL bulk ✓ JSONL bulk —
handle ⚠ Phase 3.6 GQL direct
(JSONL ignored by Shopify)
✓ JSONL bulk
(productSet respects it)
—
tags ⚠ JSONL bulk
(overwrites 3rd-party tags)
⚠ JSONL bulk
(overwrites 3rd-party tags)
—
variant price ✓ JSONL bulk ✓ JSONL bulk ✓ JSONL bulk
variant cost price ✓ JSONL bulk ✓ JSONL bulk ✓ JSONL bulk
images ✓ REST diff (Phase 3.5) ✓ REST all (Phase 4.5) —
inventory qty ✓ GQL bulk
inventorySetQuantities
✓ JSONL bulk —
in_stock metafield ✓ JSONL bulk ✓ JSONL bulk —
category metafields ✓ JSONL bulk ✓ JSONL bulk —
vehicle_fitment ✓ GQL direct (Phase 5) ✓ GQL direct (Phase 6) —
published ✓ REST (new only, Phase 3.7) ✓ REST all (Phase 4.2) —
product_type — not pushed (ERP field, all products = "goods") — not pushed —
weight ✓ Phase 3.6b GQL direct
(productVariantsBulkUpdate)
✓ JSONL bulk
(inventoryItem.measurement)
—
barcode ✓ Phase 3.6b GQL direct
(productVariantsBulkUpdate)
✓ JSONL bulk
(variant.barcode)
—
inventoryPolicy ✗ not re-asserted ✓ JSONL bulk (DENY) —
{{-- ── Phase breakdown ── --}}
API Method per Phase

GraphQL Bulk Op  GraphQL Direct  REST

BulkPriceUpdateJob — price + cost repricing

PhaseWhat it doesAPIFields
1 — Pre-loadchannels_product_prices rows updated since repricing started. Load mappings and cost from products_products.—price per product, cost, shopify_variant_id
2 — Build JSONLOne productUpdate per Shopify product with variant price and cost (when cost > 0)Bulk JSONLvariants[].price, variants[].inventoryItem.cost
3 — Bulk opUpload → start mutation → poll to COMPLETEDBulk Op—
4 — DB writeUpsert channel_listings → listed. No SkuSyncStatus.Local—

ShopifyBatchSyncJob — update existing listings

PhaseWhat it doesAPIFields
1 — Pre-loadDB queries: mappings, prices, qty, product data, listing attrs, pre-existing listing status (for publish skip)——
2 — Build JSONLOne productUpdate per product. Pre-fetches image GIDs (250/batch).Bulk JSONL + GQLtitle, descriptionHtml, vendor, tags, price, cost, metafields
3 — Bulk opUpload JSONL → start mutation → poll to COMPLETEDBulk Op—
3.6 — HandlesPer-product productUpdate mutation — Shopify ignores handles in bulk JSONLGQL directhandle, redirectNewHandle
3.6b — Weight / BarcodePer-product productVariantsBulkUpdate — productUpdate silently ignores weight/barcode on stores using Shopify's new product model (2024+). Skipped when product has no weight or barcode.GQL directvariants[].barcode, variants[].inventoryItem.measurement.weight
3.5 — ImagesDifferential — only products whose image filenames differ from ShopifyRESTimages[] (GID for existing, src for new)
3.7 — PublishSkip if already listed. REST publish for new/unlisted only.RESTpublished
4 — InventoryinventorySetQuantities batched 250/call. If any item returns "not stocked at location", those items are activated via inventoryActivate then the batch is retried (mutation is atomic — one failure rolls back the whole chunk).GQL directqty per location
5 — FitmentOne mutation per product with fitment dataGQL directvehicle_fitment JSON metafield
6 — DB + Activitychannel_listings → listed, SkuSyncStatus (assumed values), per-SKU console log entriesLocal—

ShopifyBatchCreateJob — create new listings

PhaseWhat it doesAPIFields
1 — Pre-loadDB queries + builds sku→productId map for result correlation——
2 — Build JSONLOne productSet per product (no id → Shopify creates)Bulk JSONLtitle, handle, descriptionHtml, vendor, productType, status: ACTIVE, tags, variants (sku, price, barcode, cost, weight via inventoryItem.measurement, inventoryPolicy: DENY, inventoryQuantities), metafields
3 — Bulk opUpload → start → poll. Result URL contains new IDs.Bulk Op—
4 — Parse resultsDownload result JSONL, extract new Shopify IDs, write channels_sku_mappingsLocalshopify_product_id, shopify_variant_id, shopify_inventory_item_id
4.2 — PublishproductSet creates Active but unpublished — REST publish allRESTpublished
4.5 — ImagesAll images are new — no GID pre-fetch neededRESTimages[] with src
5 — InventorySet via inventoryQuantities in productSet JSONL — no separate phase neededJSONL bulkqty per location
6 — FitmentOne mutation per productGQL directvehicle_fitment JSON metafield
7 — DBchannel_listings → listed. No SkuSyncStatus written. (gap)Local—
{{-- ── Gaps ── --}}
Known Gaps
Handles ignored in BatchSync JSONL — workaround adds ~3,000 GQL calls High
Shopify silently ignores handle and redirectNewHandle in productUpdate bulk mutations. Phase 3.6 was added as a workaround — one individual productUpdate GQL call per product after the bulk op completes.
Optimisation: Add shopify_handle column to channels_sku_mappings and skip Phase 3.6 for products whose handle hasn't changed. Turns ~3,000 calls per batch into near-zero after the first run.
Tags overwrite 3rd-party Shopify tags in bulk jobs Medium
Both bulk jobs compute tags entirely from local Cydekick data and overwrite whatever is currently in Shopify. Tags added by Shopify apps (loyalty, reviews, etc.) are lost after every bulk sync.
Fix: Batch-fetch current Shopify tags before building the JSONL (same approach as the existing image GID pre-fetch — ~12 extra GQL calls per 3,000 products) and merge rather than replace.
{{-- ── Alignment plan ── --}}
Alignment Plan — Recommended Changes
1
Store last-pushed handle to skip unchanged handles in Phase 3.6 High — performance
Add shopify_handle column to channels_sku_mappings. In Phase 3.6, skip the GQL call when the computed handle matches the stored value. Turns ~3,000 mutations per batch into near-zero after the first run. Also remove handle + redirectNewHandle from the JSONL (Shopify ignores them anyway).
2
Batch-fetch current tags before bulk sync to enable safe merge
Add getProductTagsByShopifyIds(array $ids, int $batchSize = 250) to ShopifyClient. Use fetched tags to merge rather than replace in ShopifyBatchSyncJob Phase 2. Same overhead as the existing image GID pre-fetch (~12 GQL calls per 3,000-product batch).
3
Replace row "Sync Now" with BatchSyncJob Cleanup
pushNow() in ManageListingsV2 still dispatches the old per-product V1 sync. Replace with ShopifyBatchSyncJob::dispatch([$productId], $channelId)->onQueue('high'). Search TODO(V1-cleanup) in the codebase to find all remaining call sites.
@else @include('channels::filament.pages.partials.sync-docs-marketplace', ['reference' => $this->marketplaceReference()]) @endif