productVariantsBulkUpdate in a Shopify bulk operation — fast and correct on both old and new product models.| 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 bulkinventorySetQuantities |
✓ 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) | — |
GraphQL Bulk Op GraphQL Direct REST
BulkPriceUpdateJob — price + cost repricing
| Phase | What it does | API | Fields |
|---|---|---|---|
| 1 — Pre-load | channels_product_prices rows updated since repricing started. Load mappings and cost from products_products. | — | price per product, cost, shopify_variant_id |
| 2 — Build JSONL | One productUpdate per Shopify product with variant price and cost (when cost > 0) | Bulk JSONL | variants[].price, variants[].inventoryItem.cost |
| 3 — Bulk op | Upload → start mutation → poll to COMPLETED | Bulk Op | — |
| 4 — DB write | Upsert channel_listings → listed. No SkuSyncStatus. | Local | — |
ShopifyBatchSyncJob — update existing listings
| Phase | What it does | API | Fields |
|---|---|---|---|
| 1 — Pre-load | DB queries: mappings, prices, qty, product data, listing attrs, pre-existing listing status (for publish skip) | — | — |
| 2 — Build JSONL | One productUpdate per product. Pre-fetches image GIDs (250/batch). | Bulk JSONL + GQL | title, descriptionHtml, vendor, tags, price, cost, metafields |
| 3 — Bulk op | Upload JSONL → start mutation → poll to COMPLETED | Bulk Op | — |
| 3.6 — Handles | Per-product productUpdate mutation — Shopify ignores handles in bulk JSONL | GQL direct | handle, redirectNewHandle |
| 3.6b — Weight / Barcode | Per-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 direct | variants[].barcode, variants[].inventoryItem.measurement.weight |
| 3.5 — Images | Differential — only products whose image filenames differ from Shopify | REST | images[] (GID for existing, src for new) |
| 3.7 — Publish | Skip if already listed. REST publish for new/unlisted only. | REST | published |
| 4 — Inventory | inventorySetQuantities 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 direct | qty per location |
| 5 — Fitment | One mutation per product with fitment data | GQL direct | vehicle_fitment JSON metafield |
| 6 — DB + Activity | channel_listings → listed, SkuSyncStatus (assumed values), per-SKU console log entries | Local | — |
ShopifyBatchCreateJob — create new listings
| Phase | What it does | API | Fields |
|---|---|---|---|
| 1 — Pre-load | DB queries + builds sku→productId map for result correlation | — | — |
| 2 — Build JSONL | One productSet per product (no id → Shopify creates) | Bulk JSONL | title, handle, descriptionHtml, vendor, productType, status: ACTIVE, tags, variants (sku, price, barcode, cost, weight via inventoryItem.measurement, inventoryPolicy: DENY, inventoryQuantities), metafields |
| 3 — Bulk op | Upload → start → poll. Result URL contains new IDs. | Bulk Op | — |
| 4 — Parse results | Download result JSONL, extract new Shopify IDs, write channels_sku_mappings | Local | shopify_product_id, shopify_variant_id, shopify_inventory_item_id |
| 4.2 — Publish | productSet creates Active but unpublished — REST publish all | REST | published |
| 4.5 — Images | All images are new — no GID pre-fetch needed | REST | images[] with src |
| 5 — Inventory | Set via inventoryQuantities in productSet JSONL — no separate phase needed | JSONL bulk | qty per location |
| 6 — Fitment | One mutation per product | GQL direct | vehicle_fitment JSON metafield |
| 7 — DB | channel_listings → listed. No SkuSyncStatus written. (gap) | Local | — |
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.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.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).
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).
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.