diff --git a/.scratch/shopify-headless-commerce/assets/dev-store-seed.json b/.scratch/shopify-headless-commerce/assets/dev-store-seed.json new file mode 100644 index 0000000..8c983d5 --- /dev/null +++ b/.scratch/shopify-headless-commerce/assets/dev-store-seed.json @@ -0,0 +1,928 @@ +{ + "generatedAt": "2026-10-14T03:46:20.941Z", + "source": "e21i4d-0q.myshopify.com (production) via Storefront API 2026-10", + "productCount": 61, + "collectionCount": 16, + "membershipTotal": 100, + "productsWithoutWeight": 41, + "productsWithoutProductType": 61, + "metafieldDefinitions": [ + { + "namespace": "custom", + "key": "ingredients", + "type": "multi_line_text_field", + "populated": 20 + }, + { + "namespace": "custom", + "key": "energy_kcal", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "protein_g", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "total_fat_g", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "saturated_fat_g", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "carbohydrate_g", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "sodium_mg", + "type": "number_decimal", + "populated": 20 + }, + { + "namespace": "custom", + "key": "sugar_g", + "type": "number_decimal", + "populated": 18 + }, + { + "namespace": "custom", + "key": "added_sugar_g", + "type": "number_decimal", + "populated": 17 + }, + { + "namespace": "custom", + "key": "tagline", + "type": "single_line_text_field", + "populated": 1 + } + ], + "products": [ + { + "handle": "pistachio-dragees", + "title": "Pistachio Dragees", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/47801725092086", + "weight": 200, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-fruits-nuts-45", + "title": "Mozimo Fruits & Nuts 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946279670", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hazelnut-dragees-60", + "title": "Mozimo Hazelnut Dragees 60%", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946312438", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-pistachio-dragees", + "title": "Mozimo Pistachio Dragees", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946345206", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-whole-hazelnuts-bark-60", + "title": "Mozimo Whole Hazelnuts Bark 60%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946377974", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-whole-hazelnuts-bark-45", + "title": "Mozimo Whole Hazelnuts Bark 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946410742", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hazelnut-dragees-45", + "title": "Mozimo Hazelnut Dragees 45%", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946476278", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-gems-bark", + "title": "Mozimo Gems Bark", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946509046", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-crunchy-granola-bark-45", + "title": "Mozimo Crunchy Granola Bark 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946541814", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-bark-60", + "title": "Mozimo Almond Bark 60%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946574582", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-bark-45", + "title": "Mozimo Almond Bark 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946607350", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-dragees-45", + "title": "Mozimo Almond Dragees 45%", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946640118", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-slivers-rocks", + "title": "Mozimo Almond Slivers Rocks", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946672886", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-raspberry-bark", + "title": "Mozimo Raspberry Bark", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946705654", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-nutty-bar", + "title": "Mozimo Nutty Bar", + "optionName": "Title", + "optionValue": "1", + "variantGid": "gid://shopify/ProductVariant/47806946738422", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hazelnuts-spread", + "title": "Mozimo Single Origin 45% Hazelnut Spread", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946771190", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hot-chocolate-premix", + "title": "Mozimo Hot Chocolate Premix", + "optionName": "Title", + "optionValue": "250gm", + "variantGid": "gid://shopify/ProductVariant/47806946803958", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-dragees-60", + "title": "Mozimo Almond Dragees 60%", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806946869494", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-white-chocolate-tuilles", + "title": "Mozimo White Chocolate Tuilles", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946902262", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-milk-45-tuilles", + "title": "Mozimo Milk 45% Tuilles", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806946935030", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hazelnut-praline-bark", + "title": "Mozimo Hazelnut Praline Bark", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806947000566", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-pecan-bark", + "title": "Mozimo Pecan Bark", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806947033334", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-rice-crispy-bark", + "title": "Mozimo Rice crispy Bark", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806947066102", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-dark-chocolate-bar-72", + "title": "Mozimo Dark Chocolate Bar 72%", + "optionName": "Title", + "optionValue": "60gm", + "variantGid": "gid://shopify/ProductVariant/47806947098870", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-praline-panetela", + "title": "Mozimo Praline Panetela", + "optionName": "Title", + "optionValue": "12pc", + "variantGid": "gid://shopify/ProductVariant/47806947131638", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-honey-comb", + "title": "Mozimo Honey Comb", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947164406", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-almond-praline-bark", + "title": "Mozimo Almond Praline Bark", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806947197174", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-dark-chocolate-bar-60", + "title": "Mozimo Dark Chocolate Bar 60%", + "optionName": "Title", + "optionValue": "60gm", + "variantGid": "gid://shopify/ProductVariant/47806947295478", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-dark-chocolate-bar-85", + "title": "Mozimo Dark Chocolate Bar 85%", + "optionName": "Title", + "optionValue": "60gm", + "variantGid": "gid://shopify/ProductVariant/47806947328246", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-milk-chocolate-bar-45", + "title": "Mozimo Milk Chocolate Bar 45%", + "optionName": "Title", + "optionValue": "60gm", + "variantGid": "gid://shopify/ProductVariant/47806947361014", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-orangettes", + "title": "Mozimo Orangettes", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947393782", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "single-origin-coco-nibs", + "title": "Single Origin Coco Nibs", + "optionName": "Title", + "optionValue": "150gm", + "variantGid": "gid://shopify/ProductVariant/47806947426550", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "cocoa-bean-shell", + "title": "Cocoa Bean Shell", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947459318", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "4-pralines-box", + "title": "4 Pralines Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/47806947557622", + "weight": 80, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "9-pralines-box", + "title": "9 Pralines Box", + "optionName": "Title", + "optionValue": "1", + "variantGid": "gid://shopify/ProductVariant/47806947590390", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-assorted-bark-box", + "title": "Mozimo Assorted Bark Box", + "optionName": "Title", + "optionValue": "1", + "variantGid": "gid://shopify/ProductVariant/47806947623158", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-sugar-free-dark-72-chocolate-bar", + "title": "Mozimo Sugar Free Dark 72% Chocolate Bar", + "optionName": "Title", + "optionValue": "60gm", + "variantGid": "gid://shopify/ProductVariant/47806947655926", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-hazelnut-bark-sugar-free-45", + "title": "Mozimo Hazelnut Bark Sugar Free 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947721462", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "pistachio-spread-60", + "title": "Pistachio Spread 60%", + "optionName": "Title", + "optionValue": "200gm", + "variantGid": "gid://shopify/ProductVariant/47806947754230", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-salted-butter-crunch-bark-45", + "title": "Mozimo Salted Butter Crunch Bark 45%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947786998", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-fruits-and-nuts-bark-60", + "title": "Mozimo Fruits and Nuts Bark 60%", + "optionName": "Title", + "optionValue": "100gm", + "variantGid": "gid://shopify/ProductVariant/47806947852534", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "bars-box", + "title": "Bars Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49078921101558", + "weight": 450, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "spreads-box", + "title": "Spreads Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079091003638", + "weight": 1210, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-carnival-celebration-s", + "title": "Mozimo Carnival Celebration (S)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079145595126", + "weight": 1680, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-carnival-celebration-m", + "title": "Mozimo Carnival Celebration (M)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079147725046", + "weight": 1800, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-carnival-celebration-l", + "title": "Mozimo Carnival Celebration (L)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079154082038", + "weight": 2330, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-celestial-s", + "title": "Mozimo Celestial (S)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079192748278", + "weight": 2100, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-celestial-l", + "title": "Mozimo Celestial Hamper", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079213293814", + "weight": 5500, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "200-gas-bark-box", + "title": "200 gms Bark Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079244456182", + "weight": 380, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "kunafa-chocolate-bar", + "title": "Kunafa Chocolate Bar", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079263101174", + "weight": 370, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "1-kg-bark-box", + "title": "1 kg Bark Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079272046838", + "weight": 2100, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-praline-panetela-1", + "title": "Mozimo Praline Panetela (Box of 12)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079278239990", + "weight": 375, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-praline-box-of-25", + "title": "Mozimo Praline (Box of 25)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079282696438", + "weight": 550, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-praline-box-of-36", + "title": "Mozimo Praline (Box of 36)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49079289741558", + "weight": 770, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-sugar-free-milk-45-chocolate-bar", + "title": "Mozimo Sugar Free Milk 45% Chocolate Bar", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49080727372022", + "weight": 70, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-matcha-collection-box", + "title": "Mozimo Matcha Collection Box", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49947063976182", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "matcha-strawberry-praline", + "title": "Matcha Strawberry Praline (Box of 4)", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/49956310417654", + "weight": 0, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-matcha-bark", + "title": "Mozimo Matcha Bark Box 200gm", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/50112483590390", + "weight": 200, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "matcha-hazelnut-dragees-100gm", + "title": "Matcha Hazelnut Dragees 100gm", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/50112861831414", + "weight": 100, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-chocolate-muesli-200gm", + "title": "Mozimo Chocolate Muesli 200gm", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/50137431736566", + "weight": 200, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + }, + { + "handle": "mozimo-chocolate-muesli-400gm", + "title": "Mozimo Chocolate Muesli 400gm", + "optionName": "Title", + "optionValue": "Default Title", + "variantGid": "gid://shopify/ProductVariant/50137463685366", + "weight": 400, + "weightUnit": "GRAMS", + "productType": null, + "tags": [] + } + ], + "collections": [ + { + "handle": "frontpage", + "title": "Home page", + "members": [ + "mozimo-chocolate-muesli-400gm", + "mozimo-chocolate-muesli-200gm", + "mozimo-matcha-bark", + "mozimo-matcha-collection-box", + "mozimo-celestial-l" + ] + }, + { + "handle": "dragees", + "title": "Dragees", + "members": [ + "mozimo-almond-dragees-60", + "mozimo-almond-dragees-45", + "mozimo-hazelnut-dragees-60", + "mozimo-hazelnut-dragees-45", + "pistachio-dragees", + "matcha-hazelnut-dragees-100gm" + ] + }, + { + "handle": "bars", + "title": "Bars", + "members": [ + "mozimo-dark-chocolate-bar-85", + "mozimo-dark-chocolate-bar-72", + "mozimo-dark-chocolate-bar-60", + "mozimo-milk-chocolate-bar-45", + "mozimo-sugar-free-dark-72-chocolate-bar", + "mozimo-sugar-free-milk-45-chocolate-bar", + "mozimo-nutty-bar" + ] + }, + { + "handle": "spreads", + "title": "Spreads", + "members": ["mozimo-hazelnuts-spread", "pistachio-spread-60"] + }, + { + "handle": "best-sellers", + "title": "Best Sellers", + "members": [ + "mozimo-hot-chocolate-premix", + "200-gas-bark-box", + "kunafa-chocolate-bar", + "mozimo-celestial-l", + "mozimo-carnival-celebration-m", + "mozimo-dark-chocolate-bar-60", + "mozimo-sugar-free-dark-72-chocolate-bar", + "mozimo-hazelnut-dragees-45", + "mozimo-orangettes", + "mozimo-praline-box-of-36", + "mozimo-praline-panetela-1", + "mozimo-hazelnuts-spread", + "pistachio-spread-60", + "mozimo-matcha-collection-box", + "mozimo-matcha-bark", + "matcha-hazelnut-dragees-100gm", + "mozimo-chocolate-muesli-400gm" + ] + }, + { + "handle": "new-arrivals", + "title": "New Arrivals", + "members": [ + "mozimo-matcha-collection-box", + "mozimo-chocolate-muesli-200gm", + "mozimo-chocolate-muesli-400gm", + "mozimo-celestial-l" + ] + }, + { + "handle": "gift-collection", + "title": "Gift Collection", + "members": [ + "mozimo-celestial-l", + "1-kg-bark-box", + "mozimo-celestial-s", + "mozimo-carnival-celebration-l", + "mozimo-matcha-collection-box", + "mozimo-carnival-celebration-m", + "mozimo-carnival-celebration-s", + "200-gas-bark-box", + "bars-box", + "9-pralines-box" + ] + }, + { + "handle": "bark", + "title": "Barks", + "members": [ + "mozimo-almond-bark-45", + "mozimo-almond-bark-60", + "mozimo-whole-hazelnuts-bark-45", + "mozimo-whole-hazelnuts-bark-60", + "mozimo-fruits-nuts-45", + "mozimo-fruits-and-nuts-bark-60", + "mozimo-gems-bark", + "mozimo-rice-crispy-bark", + "mozimo-raspberry-bark", + "mozimo-pecan-bark", + "mozimo-crunchy-granola-bark-45", + "mozimo-salted-butter-crunch-bark-45", + "mozimo-hazelnut-bark-sugar-free-45", + "mozimo-assorted-bark-box", + "mozimo-almond-praline-bark" + ] + }, + { + "handle": "sugar-free-chocolate-bar", + "title": "Sugar Free Chocolate Bar", + "members": ["mozimo-sugar-free-dark-72-chocolate-bar"] + }, + { + "handle": "rocks-orangettes", + "title": "Rocks / Orangettes", + "members": ["mozimo-orangettes", "mozimo-almond-slivers-rocks"] + }, + { + "handle": "tuilles", + "title": "Tuilles", + "members": ["mozimo-milk-45-tuilles", "mozimo-white-chocolate-tuilles"] + }, + { + "handle": "pralines-bark", + "title": "Pralines Bark", + "members": ["mozimo-almond-praline-bark", "mozimo-hazelnut-praline-bark"] + }, + { + "handle": "pralines", + "title": "Pralines", + "members": [ + "9-pralines-box", + "matcha-strawberry-praline", + "4-pralines-box", + "mozimo-praline-panetela" + ] + }, + { + "handle": "mozimo-delights", + "title": "Mozimo Delights", + "members": [ + "mozimo-honey-comb", + "single-origin-coco-nibs", + "mozimo-nutty-bar", + "cocoa-bean-shell", + "mozimo-hot-chocolate-premix" + ] + }, + { + "handle": "diwali", + "title": "Diwali", + "members": [ + "bars-box", + "spreads-box", + "mozimo-carnival-celebration-s", + "mozimo-carnival-celebration-m", + "mozimo-carnival-celebration-l", + "mozimo-celestial-s", + "200-gas-bark-box", + "kunafa-chocolate-bar", + "1-kg-bark-box", + "mozimo-praline-panetela-1", + "mozimo-praline-box-of-25", + "mozimo-praline-box-of-36" + ] + }, + { + "handle": "hampers-boxes", + "title": "Hampers & Boxes", + "members": [ + "mozimo-celestial-s", + "mozimo-carnival-celebration-s", + "1-kg-bark-box", + "mozimo-celestial-l", + "mozimo-carnival-celebration-l", + "mozimo-carnival-celebration-m" + ] + } + ] +} diff --git a/.scratch/shopify-headless-commerce/issues/01-seed-dev-store.md b/.scratch/shopify-headless-commerce/issues/01-seed-dev-store.md index 2fdd284..cf87e4a 100644 --- a/.scratch/shopify-headless-commerce/issues/01-seed-dev-store.md +++ b/.scratch/shopify-headless-commerce/issues/01-seed-dev-store.md @@ -1,7 +1,7 @@ # Seed the dev store from the live catalogue `Type:` task (HITL) -`Status:` open +`Status:` claimed Part of [the map](../map.md). Empty the frontier from the top. @@ -42,3 +42,146 @@ Facts later tickets depend on: which products exist in the dev store, whether th ## Answer + +## Comments + +### AFK half worked — and the seeding decision re-opens further + +Progress against **To resolve**: #1 is settled without an export; #4's stated option is +dead and needs a decision the agent cannot make alone. + +#### 1. Variant option shape — settled (To resolve #1) ✅ + +The Storefront API exposes `Product.options`, so the export is not needed to read the +shape. All **61** products carry one option named `Title`. The *values* are not uniform: + +| `Option1 value` (= Variant title) | products | +| --- | --- | +| `Default Title` | 22 | +| `100gm` | 17 | +| `200gm` | 11 | +| `60gm` | 5 | +| `1` | 3 | +| `250gm`, `150gm`, `12pc` | 1 each | + +So the sample CSV's shape (name `Title`, a real value) covers 39 of 61 and the docs' +`Default Title` covers 22. The import must preserve each product's own value: changing +an option value deletes and recreates the Variant, and the resulting Variant GIDs are +what Phase 1 carts. **Do not normalise.** (`ADR-0002`'s claim that weight lives in the +Variant title holds for 39 products, not all 61; 22 are `Default Title`.) + +#### 2. The repo's credentials cannot drive an Admin API seed (To resolve #4) ❌ + +The production app's granted scopes are **Storefront-only**: + +``` +unauthenticated_read_product_listings unauthenticated_read_product_inventory +unauthenticated_read_product_tags unauthenticated_read_checkouts +unauthenticated_write_checkouts +``` + +`products`, `productsCount` and `collections` all return +`Access denied … Required access: 'read_products' access scope`. So +*"a small Admin API script using the credentials the repo already mints"* is **not +available as written**. Any Admin API mechanism needs a custom app installed in the +destination store with `write_products`. + +Two consequences beyond this ticket: + +- Production's collection *structure* (manual vs smart, and any smart rules) is also + unreadable with today's scopes, so a collection rebuild can only reproduce **today's + membership as manual collections** — a real fidelity gap, not a bug. +- The Phase 0 admin data fixes (61 × `productType`, 41 × weight) could be scripted + rather than hand-edited *if* an Admin-scoped app is installed on production. + +#### 3. Metafield definitions to create first (To resolve #2) — 10 keys + +The Storefront API cannot enumerate metafields (`metafields` demands explicit +`identifiers`), so this comes from `docs/shopify-catalog-inventory.md` §6, measured with +an explicit identifier list. These are the populated keys, i.e. the columns the CSV will +actually carry: + +| definition | type | populated | +| --- | --- | --- | +| `custom.ingredients` | `multi_line_text_field` | 20 | +| `custom.energy_kcal`, `custom.protein_g`, `custom.total_fat_g`, `custom.saturated_fat_g`, `custom.carbohydrate_g`, `custom.sodium_mg` | `number_decimal` | 20 each | +| `custom.sugar_g` | `number_decimal` | 18 | +| `custom.added_sugar_g` | `number_decimal` | 17 | +| `custom.tagline` | `single_line_text_field` | 1 | + +The `facts.*` namespace is empty catalogue-wide, so no `facts.*` definitions are needed +for a faithful seed (ticket 09 decides whether the compat branch survives at all). + +#### 4. Collection membership is 16 collections / 100 memberships + +Recorded with the full product list, option values, weights and product types in +[`assets/dev-store-seed.json`](../assets/dev-store-seed.json) — the baseline the seed is +checked against, and the membership map whichever mechanism is chosen will consume. + +Nothing in the CSV path can carry this. The docs confirm the `Collection` column takes +**one collection per product**, is never exported, and — per the overwrite rules — a +*blank* non-required column overwrites the existing value as blank. Repeated imports with +different `Collection` values would have to *accumulate* membership, which is +undocumented and whose failure mode is destroying membership. Not a sound mechanism. + +#### 5. The second mechanism, at 2026-10 + +- `collectionAddProducts` is **deprecated**; the current path is `collectionUpdate` with + `inclusion.selectionsToAdd`, or `collectionCreate` with `sources[].inclusion.selections`. + Both need `write_products`. +- **Trap:** `collectionCreate` leaves the collection **unpublished**, so without a + `publishablePublish` the membership work is invisible to the Storefront API — the + thing the dev store exists to serve. + +### Decision: CSV for products, a dev-store app for collections ✅ + +Products, metafields and images stay on CSV — it re-hosts images for free, whereas an API +seed would need staged uploads for 61 products. Collections go through a custom app +installed in the dev store, driven by +[`scripts/seed-dev-store.mjs`](../../../scripts/seed-dev-store.mjs), which creates each +collection with its members in one call and publishes it. Chosen over hand-building +because the membership map is a snapshot that will drift, and over an all-API seed because +images would then need staging. It also makes the collection step re-runnable whenever +production changes. + +**Fidelity gap, recorded rather than hidden:** production's collection *structure* is +unreadable with today's scopes, so the rebuild reproduces today's membership as **manual** +collections. If any of the 16 is a smart collection in production, the dev store will hold +a manual copy of it. + +### Runbook (HITL — the parts that need admin access) + +In the dev store `staging-iw0gviik.myshopify.com`: + +1. **Create the custom app.** Settings → Apps and sales channels → Develop apps → Create + an app → Configure Admin API scopes: `read_products`, `write_products`, + `read_publications`, `write_publications` → Install app → copy the **Admin API access + token** (`shpat_…`). `write_products` covers `collectionCreate`; `write_publications` + and `read_publications` cover `publishablePublish` and the `publications` query. +2. **Create the metafield definitions** (Settings → Custom data → Products) *before* + importing — the 10 keys and types in §3 above. Undefined metafields on import are + undocumented, which is why this comes first. +3. **Export from production, then import.** Products → Export → *Plain CSV* → Export + products (an *All products* export is emailed, not downloaded). Then Products → Import + → *Overwrite products with matching handles*, small batch first. Leave `Option1 + name`/`Option1 value` exactly as exported (§1). Images are URLs only: production's + products must stay publicly reachable until the import finishes. +4. **Add the dev-store credentials to `.env`** (gitignored): `SHOPIFY_DEV_STORE_DOMAIN`, + `SHOPIFY_DEV_ADMIN_TOKEN`. +5. **Run the collection seed** — dry run, then apply: + `npm run seed:dev-store`, then `npm run seed:dev-store -- --apply`. It creates the 16 + collections, adds the 100 memberships and publishes each one; existing handles are + skipped and any product handle it cannot find is reported, which is how a partial CSV + import shows up. +6. **Record what landed** (To resolve #5): product count, collection count and + membership, metafield coverage, anything rebuilt by hand — and whether the Variant GIDs + differ from [`assets/dev-store-seed.json`](../assets/dev-store-seed.json) (they will). + +**Unverified until step 5 runs:** the script's mutation shapes come from the 2026-10 Admin +reference (it uses the `CollectionCreateInput.sources` form because `collectionAddProducts` +is deprecated). Its auth, error and snapshot paths have been exercised against production; +the writes have not, because no dev-store app exists yet. That is why the dry run is the +default. + +Facts later tickets depend on once this is done: which products exist in the dev store, +the dev store's own Variant GIDs, and whether the metafield values survived the import. diff --git a/package.json b/package.json index da3a85d..cb77926 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "lint": "eslint . && prettier --check .", "format": "prettier --write .", "pb": "bash -c 'if [ ! -f .pb/pocketbase ]; then mkdir -p .pb && curl -sL -o .pb/pb.zip https://github.com/pocketbase/pocketbase/releases/download/v0.40.2/pocketbase_0.40.2_linux_amd64.zip && (cd .pb && unzip -o -q pb.zip pocketbase && rm pb.zip); fi; cd .pb && ./pocketbase serve --http=127.0.0.1:8090 --dir=./pb_data'", - "seed": "node --env-file-if-exists=.env scripts/pb-seed.mjs" + "seed": "node --env-file-if-exists=.env scripts/pb-seed.mjs", + "seed:dev-store": "node --env-file-if-exists=.env scripts/seed-dev-store.mjs" }, "dependencies": { "@sentry/tanstackstart-react": "^10.73.0", diff --git a/scripts/seed-dev-store.mjs b/scripts/seed-dev-store.mjs new file mode 100644 index 0000000..9cf7968 --- /dev/null +++ b/scripts/seed-dev-store.mjs @@ -0,0 +1,280 @@ +#!/usr/bin/env node +/** + * Seed a dev store's collections from the production membership snapshot. + * + * node --env-file=.env scripts/seed-dev-store.mjs # dry run + * node --env-file=.env scripts/seed-dev-store.mjs --apply # write + * + * Why a script at all: the product CSV round-trips products and product + * metafields, but it cannot carry collection membership — the `Collection` + * column is import-only, takes one collection per product, and this catalogue + * runs 1.6 memberships per product across 16 collections. + * + * Per collection in the snapshot it: + * 1. skips the collection when the handle already exists in the destination + * (and reports the destination's member count, so drift is visible); + * 2. otherwise creates it with its members in one call, using the current + * 2026-10 shape — `CollectionCreateInput.sources[].source.inclusion + * .selections` on the `collection` argument. The `input`/`ruleSet` form is + * deprecated, and so is `collectionAddProducts`; + * 3. publishes it, because `collectionCreate` leaves a collection + * unpublished and an unpublished collection is invisible to the + * Storefront API the dev store exists to serve. + * + * It never deletes anything, never touches products, and re-runs are safe. + * Products must already exist in the destination: run the CSV import first. + * + * Env: + * SHOPIFY_DEV_STORE_DOMAIN e.g. staging-iw0gviik.myshopify.com + * SHOPIFY_DEV_ADMIN_TOKEN Admin API access token (shpat_…) from a custom + * app installed in the dev store. Scopes: + * read_products, write_products, + * read_publications, write_publications. + * Production's app cannot be used — it is + * Storefront-scoped only, with no read_products. + * SHOPIFY_ADMIN_API_VERSION default 2026-10 + * + * Flags: + * --apply write. Without it the run is a dry run. + * --only=handle,handle limit to these collections + * --publication= publish target (default: Online Store) + * --snapshot= membership snapshot to seed from + */ + +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); + +const DOMAIN = process.env.SHOPIFY_DEV_STORE_DOMAIN ?? ""; +const TOKEN = process.env.SHOPIFY_DEV_ADMIN_TOKEN ?? ""; +const API_VERSION = process.env.SHOPIFY_ADMIN_API_VERSION ?? "2026-10"; + +const args = process.argv.slice(2); +const flag = (name, fallback) => { + const hit = args.find((a) => a === `--${name}` || a.startsWith(`--${name}=`)); + if (!hit) return fallback; + return hit.includes("=") ? hit.slice(hit.indexOf("=") + 1) : true; +}; + +const APPLY = Boolean(flag("apply", false)); +const ONLY = String(flag("only", "") ?? "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +const PUBLICATION = String(flag("publication", "Online Store")); +const SNAPSHOT = String( + flag( + "snapshot", + join(root, ".scratch/shopify-headless-commerce/assets/dev-store-seed.json"), + ), +); + +if (!DOMAIN || !TOKEN) { + console.error( + "Set SHOPIFY_DEV_STORE_DOMAIN and SHOPIFY_DEV_ADMIN_TOKEN — see the header of this file.", + ); + process.exit(2); +} + +/* ── Admin API ─────────────────────────────────────────────── */ + +const gql = async (query, variables) => { + const res = await fetch( + `https://${DOMAIN}/admin/api/${API_VERSION}/graphql.json`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Shopify-Access-Token": TOKEN, + }, + body: JSON.stringify({ query, variables }), + signal: AbortSignal.timeout(20_000), + }, + ); + const json = await res.json().catch(() => null); + if (!res.ok || !json) { + throw new Error(`Admin API HTTP ${res.status} ${res.statusText}`); + } + if (json.errors?.length) { + throw new Error(json.errors.map((e) => e.message).join("; ")); + } + return json.data; +}; + +const CREATE_COLLECTION = /* GraphQL */ ` + mutation SeedCollection($collection: CollectionCreateInput!) { + collectionCreate(collection: $collection) { + collection { + id + handle + } + userErrors { + field + message + } + } + } +`; + +const PUBLISH = /* GraphQL */ ` + mutation Publish($id: ID!, $input: [PublicationInput!]!) { + publishablePublish(id: $id, input: $input) { + userErrors { + field + message + } + } + } +`; + +/* ── the plan ──────────────────────────────────────────────── */ + +const snapshot = JSON.parse(readFileSync(SNAPSHOT, "utf8")); +const plan = snapshot.collections.filter( + (c) => !ONLY.length || ONLY.includes(c.handle), +); +if (!plan.length) { + console.error(`Nothing to seed: ${SNAPSHOT} has no matching collections.`); + process.exit(2); +} + +console.log( + `${APPLY ? "Seeding" : "Dry run against"} ${DOMAIN} (Admin API ${API_VERSION})`, +); +console.log( + `snapshot: ${SNAPSHOT}\n ${snapshot.productCount} products, ` + + `${snapshot.collectionCount} collections, ${snapshot.membershipTotal} memberships ` + + `(taken ${snapshot.generatedAt})\n`, +); + +let products; +let collections; +let publications; +try { + products = await gql( + `query { products(first: 250) { nodes { id handle } } }`, + ); + collections = await gql( + `query { collections(first: 250) { nodes { id handle productsCount { count } } } }`, + ); + publications = await gql( + `query { publications(first: 20) { nodes { id name } } }`, + ); +} catch (error) { + console.error(`Could not read the destination store: ${error.message}`); + console.error( + "The custom app needs read_products and read_publications, and must be installed.", + ); + process.exit(2); +} + +const productId = new Map(products.products.nodes.map((p) => [p.handle, p.id])); +const existing = new Map( + collections.collections.nodes.map((c) => [c.handle, c]), +); + +const pubs = publications.publications.nodes ?? []; +const chosen = pubs.find( + (p) => + p.name.toLowerCase() === PUBLICATION.toLowerCase() || p.id === PUBLICATION, +); +if (!chosen) { + console.error( + `No publication matches "${PUBLICATION}". Available: ${pubs.map((p) => p.name).join(", ") || "(none)"}`, + ); + process.exit(2); +} +console.log(`publish target: ${chosen.name} (${chosen.id})\n`); + +/* ── report and apply ──────────────────────────────────────── */ + +const tally = { created: 0, published: 0, existing: 0, empty: 0, errors: 0 }; + +for (const c of plan) { + const missing = c.members.filter((h) => !productId.has(h)); + const ids = c.members + .filter((h) => productId.has(h)) + .map((h) => productId.get(h)); + const label = c.handle.padEnd(24); + const note = `${String(c.members.length).padStart(3)} members`; + + if (missing.length) { + console.log( + `${label}${note} ${missing.length} not in the destination: ${missing.join(", ")}`, + ); + } + + const here = existing.get(c.handle); + if (here) { + tally.existing += 1; + const count = here.productsCount?.count ?? "?"; + const drift = + count === c.members.length ? "" : " ← differs from the snapshot"; + console.log(`${label}${note} exists with ${count} products${drift}`); + continue; + } + + if (!ids.length) { + tally.empty += 1; + console.log( + `${label}${note} skipped: none of its products exist here yet`, + ); + continue; + } + + console.log( + `${label}${note} ${APPLY ? "creating" : "would create"} and publish to ${chosen.name}`, + ); + if (!APPLY) continue; + + const created = await gql(CREATE_COLLECTION, { + collection: { + title: c.title, + handle: c.handle, + sources: [ + { + source: { + title: c.title, + inclusion: { selections: ids.map((productId) => ({ productId })) }, + }, + }, + ], + }, + }); + const userErrors = created.collectionCreate?.userErrors ?? []; + if (userErrors.length || !created.collectionCreate?.collection) { + tally.errors += 1; + console.error( + `${label} create failed: ${userErrors.map((e) => `${e.field?.join(".")} ${e.message}`).join("; ") || "no collection returned"}`, + ); + continue; + } + tally.created += 1; + const gid = created.collectionCreate.collection.id; + + const published = await gql(PUBLISH, { + id: gid, + input: [{ publicationId: chosen.id }], + }); + const pubErrors = published.publishablePublish?.userErrors ?? []; + if (pubErrors.length) { + tally.errors += 1; + console.error( + `${label} created but not published: ${pubErrors.map((e) => e.message).join("; ")}`, + ); + continue; + } + tally.published += 1; +} + +console.log( + `\n${APPLY ? "Done" : "Dry run complete"}: ` + + `${tally.created} created, ${tally.published} published, ` + + `${tally.existing} already present, ${tally.empty} skipped, ${tally.errors} errors.`, +); +if (!APPLY) { + console.log("Nothing was written. Re-run with --apply."); +} +process.exit(tally.errors ? 1 : 0);