Our Partner & Developer boards on the community are moving to a brand new home: the .dev community forums! While you can still access past discussions here, for all your future app and storefront building questions, head over to the new forums.

Setting up Product Option Stock Amount and Pricing

Setting up Product Option Stock Amount and Pricing

Demonix
Shopify Partner
6 0 1

I thought I might ask here as I wasn't getting any help in the Technical Forums.

Basically I have an API that gives me product data which I format and then use in the graphql to create a product. I need to create the product, add its variants and then the quantity available for each and its price.

I've set this up so far but I'm stuck at doing the last part of setting the price for the options and their qty's

//Preparing the Data

const prepareAmrodProducts = (products) => {
  return products.map((product) => {
    // Prepare color options
    const colorOptions = product.variants.reduce((acc, variant) => {
      if (variant.codeColourName && !acc.includes(variant.codeColourName)) {
        acc.push(variant.codeColourName);
      }
      return acc;
    }, []);

    const sizeOptions = product.variants.reduce((acc, variant) => {
      if (variant.codeSizeName && !acc.includes(variant.codeSizeName)) {
        acc.push(variant.codeSizeName);
      }
      return acc;
    }, []);

    const productOptions = [];

    if (colorOptions.length > 0) {
      productOptions.push({
        name: "Color",
        values: colorOptions.map((color) => ({ name: color })),
      });
    }

    if (sizeOptions.length > 0) {
      productOptions.push({
        name: "Size",
        values: sizeOptions.map((size) => ({ name: size })),
      });
    }

    return {
      title: product.productName,
      bodyHtml: product.description,
      vendor: "Amrod",
      productType: product.categories[0]?.name || "Default Category",
      images: product.images.map((image) => ({
        src: image.urls[0]?.url || "Default Image URL",
        width: image.urls[0]?.width || 1024,
        height: image.urls[0]?.height || 1024,
      })),
      productOptions: productOptions,
      variants: product.variants.map((variant) => ({
        price: variant.price || 0,
        inventoryManagement: "SHOPIFY",
        inventoryPolicy: "DENY",
        sku: variant.fullCode,
        inventoryQuantity: variant.stock || 0,
        options: [
          variant.codeColourName || "",
          variant.codeSizeName || "",
        ].filter(Boolean),
      })),
    };
  });
};

 

And here is how I attempt to create the product:

const createProductBatch = async (productsData) => {
  // Product creation mutation
  const productCreateMutation = `
  mutation CreateProduct($input: ProductInput!, $media: [CreateMediaInput!]) {
    productCreate(input: $input, media: $media) {
        product {
          id
          options {
            id
            name
            position
            values
            optionValues {
              id
              name
              hasVariants
            }
          }
          media(first: 10) {
            nodes {
              alt
              mediaContentType
              preview {
                status
              }
            }
          }
          variants(first: 5) {
            nodes {
              id
              title
              selectedOptions {
                name
                value
              }
            }
          }          
          metafields(first: 10) {
            edges {
              node {
                type
                namespace
                key
                value
              }
            }
          }      
        }    
        userErrors {
          field
          message
        }
      }
    }
  `;

  const batchSize = 5;
  const delay = 500;
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  let results = [];

  for (let i = 0; i < productsData.length; i += batchSize) {
    const batch = productsData.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(async (product) => {
        //Media
        const mediaInput = product.images.map((image) => ({
          originalSource: image.src,
          alt: image.altText || "",
          mediaContentType: "IMAGE",
        }));

        // Create the product
        const productResponse = await shopifyAPI.post(
          "",
          JSON.stringify({
            query: productCreateMutation,
            variables: {
              input: {
                title: product.title,
                bodyHtml: product.bodyHtml,
                vendor: product.vendor,
                productType: product.productType,
                metafields: [
                  {
                    namespace: "custom",
                    key: "sync_from_api",
                    type: "boolean",
                    value: "true",
                  },
                ],
                productOptions: product.productOptions,
              },
              media: mediaInput,
            },
          })
        );

        if (!productResponse.data || productResponse.data.errors) {
          console.error("GraphQL Error:", productResponse.data.errors);
          return null;
        }
        if (productResponse.data.data.productCreate.userErrors.length > 0) {
          console.error(
            "User Errors:",
            productResponse.data.data.productCreate.userErrors
          );
          return null;
        }

        // Product ID to use for creating variants
        const productId = productResponse.data.data.productCreate.product.id;

        return {
          productId: productId,
        };
      })
    );

    results = results.concat(batchResults);
    await sleep(delay);
  }
  return results.filter((result) => result !== null);
};

If anyone can give me some guidance on how to complete the last part or if I'm doing this correctly or wrong etc I'd really appreciate it. Even if you have some resource to teach me in an easy manor.



Replies 4 (4)

ShopifyDevSup
Shopify Staff
1453 238 519

Hi @Demonix,

 

Using the new Product API Model, once you create the Product with multiple Product Options, as you are doing here, you will then need to create the variants in a second API call. This is since the productCreate mutation when created with multiple Product Options, will create the Product Options, but doesn't actually create variants for them yet, however it does create a single default variant using the first listed product option values, and any additional variants will need to be created with a productVariantsBulkCreate call.

For example if you are running the productCreate mutation with the following input:

 

{
 "productInput": {
   "title": "My cool socks",
   "productOptions": [
     {
       "name": "Color",
       "values": [
         {
           "name": "Red"
         },
         {
           "name": "Green"
         },
         {
           "name": "Blue"
         }
       ]
     },
     {
       "name": "Size",
       "values": [
         {
           "name": "Small"
         },
         {
           "name": "Medium"
         },
         {
           "name": "Large"
         }
       ]
     }
   ]
 }
}

This will create a Product called My cool socks, this Product will have Product Options: Color and Size, with Option Values: Red, Green, Blue, and Small, Medium, Large. This product will also only have a single default variant created with Red/Small product option values.

Once you have created the product, you will need to create the Product Variant, with the productVariantsBulkCreate call. In the input of the productVariantsBulkCreate call you can set the prices and inventory values for each variant you are creating.

Additionally since you already have the Red/Small variant, instead of having to create new variants, then having to update the Red/Small variant with prices and inventory, you can set  strategy: REMOVE_STANDALONE_VARIANT in the initial productVariantsBulkCreate call and include the Red/Small variant in the productVariantsBulkCreate input. Here's an example for reference:


Call Body:

mutation productVariantsBulkCreate($productId: ID!, $strategy: ProductVariantsBulkCreateStrategy, $variants: [ProductVariantsBulkInput!]!) {
   productVariantsBulkCreate(productId: $productId, strategy: $strategy, variants: $variants) {

       productVariants{
           id
           title
           price
           inventoryPolicy
           inventoryQuantity
           .... etc ....
       }
   }
}

Call Variables:

{
   "productId": "gid://shopify/Product/1234567890",
   "strategy": "REMOVE_STANDALONE_VARIANT",
   "variants": [
       {
           "price": 4.99,
           "inventoryPolicy": "DENY",
           "inventoryQuantities": [
               {
                   "availableQuantity": 10,
                   "locationId": "gid://shopify/Location/1234567890"
               }
           ],
           "inventoryItem": {
               "cost":1.99,
               "tracked": true,
               "requiresShipping": true
           },        
           "optionValues": [
               {
                   "name": "Red",
                   "optionName": "Color"
               },
               {
                   "name": "Small",
                   "optionName": "Size"
               }
           ]
       },{
           "price": 4.99,
           "inventoryPolicy": "DENY",
           "inventoryQuantities": [
               {
                   "availableQuantity": 10,
                   "locationId": "gid://shopify/Location/1234567890"
               }
           ],
           "inventoryItem": {
               "cost":1.99,
               "tracked": true,
               "requiresShipping": true
           },        
           "optionValues": [
               {
                   "name": "Red",
                   "optionName": "Color"
               },
               {
                   "name": "Medium",
                   "optionName": "Size"
               }
           ]
       },{
           "price": 4.99,
           "inventoryPolicy": "DENY",
           "inventoryQuantities": [
               {
                   "availableQuantity": 10,
                   "locationId": "gid://shopify/Location/1234567890"
               }
           ],
           "inventoryItem": {
               "cost":1.99,
               "tracked": true,
               "requiresShipping": true
           },        
           "optionValues": [
               {
                   "name": "Red",
                   "optionName": "Color"
               },
               {
                   "name": "Large",
                   "optionName": "Size"
               }
           ]
       }
   ]
}

We do have some resources in our Shopify.dev documentation as well that does explain this in further detail, as well as providing guidance on using our new Product API Models. 
 

I hope this helps, and I hope you have a great day 🙂

Developer Support @ Shopify
- Was this reply helpful? Click Like to let us know!
- Was your question answered? Mark it as an Accepted Solution
- To learn more visit Shopify.dev or the Shopify Web Design and Development Blog

Demonix
Shopify Partner
6 0 1

 

 

Thanks for the breakdown. I've managed to setup an example for now to test and broke it up like this:

 

const exampleProduct = {
  title: "Example Product",
  bodyHtml: "Description of the example product.",
  vendor: "Test Vendor",
  productType: "ProductType",
  images: [
    { src: "https://i.imgur.com/ynSF4sO.png", altText: "Image 1" },
    { src: "https://i.imgur.com/T0Q5Q5H.png", altText: "Image 2" },
  ],
  variants: [
    {
      price: "19.99",
      sku: "EXAMPLE-SKU-1",
      inventoryQuantity: 100,
      options: ["Red"],
    },
    {
      price: "24.99",
      sku: "EXAMPLE-SKU-2",
      inventoryQuantity: 50,
      options: ["Blue"],
    },
  ],
};

const createProduct = async (product) => {
  const productCreateMutation = `
  mutation CreateProduct($input: ProductInput!, $media: [CreateMediaInput!]) {
    productCreate(input: $input, media: $media) {
      product {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
  `;

  const mediaInput = product.images.map((image) => ({
    originalSource: image.src,
    alt: image.altText || "",
    mediaContentType: "IMAGE",
  }));

  try {
    const productResponse = await shopifyAPI.post(
      "",
      JSON.stringify({
        query: productCreateMutation,
        variables: {
          input: {
            title: product.title,
            bodyHtml: product.bodyHtml,
            vendor: product.vendor,
            productType: product.productType,
            metafields: [
              {
                namespace: "custom",
                key: "sync_from_api",
                type: "boolean",
                value: "true",
              },
            ],
          },
          media: mediaInput,
        },
      })
    );

    if (
      productResponse.data.errors ||
      productResponse.data.data.productCreate.userErrors.length > 0
    ) {
      console.error(
        "Error creating product:",
        productResponse.data.errors ||
          productResponse.data.data.productCreate.userErrors
      );
      return null;
    }

    return productResponse.data.data.productCreate.product.id;
  } catch (error) {
    console.error("Network or Server Error:", error);
    return null;
  }
};

const createVariants = async (productId, variants) => {
  const variantCreateMutation = `
  mutation CreateVariant($input: ProductVariantInput!) {
    productVariantCreate(input: $input) {
      productVariant {
        id
        inventoryItem {
          id
        }
      }
      userErrors {
        field
        message
      }
    }
  }
  `;

  const createdVariants = [];

  for (const variant of variants) {
    try {
      const variantResponse = await shopifyAPI.post(
        "",
        JSON.stringify({
          query: variantCreateMutation,
          variables: {
            input: {
              productId,
              price: variant.price,
              sku: variant.sku,
              options: variant.options,
            },
          },
        })
      );

      if (
        variantResponse.data.errors ||
        variantResponse.data.data.productVariantCreate.userErrors.length > 0
      ) {
        console.error(
          "Error creating variant:",
          variantResponse.data.errors ||
            variantResponse.data.data.productVariantCreate.userErrors
        );
      } else {
        console.log(`Variant created: ${variant.sku}`);
        const inventoryItemId =
          variantResponse.data.data.productVariantCreate.productVariant.inventoryItem.id
            .split("/")
            .pop();
        createdVariants.push({
          inventoryItemId,
          inventoryQuantity: variant.inventoryQuantity,
        });
      }
    } catch (error) {
      console.error("Network or Server Error:", error);
    }
  }

  return createdVariants;
};

const adjustInventory = async (inventoryItemId, inventoryQuantity) => {
  const restAPI = axios.create({
    baseURL: `https://${process.env.SHOPIFY_SHOP}/admin/api/2024-04`,
    headers: {
      "Content-Type": "application/json",
      "X-Shopify-Access-Token": process.env.SHOPIFY_ACCESS_TOKEN,
    },
  });

  const locationId = process.env.SHOPIFY_LOCATION_ID;

  try {
    await restAPI.post(`/inventory_levels/adjust.json`, {
      location_id: locationId,
      inventory_item_id: inventoryItemId, // Ensure inventoryItemId is correct
      available_adjustment: inventoryQuantity,
    });

    console.log(`Inventory adjusted for item: ${inventoryItemId}`);
  } catch (error) {
    console.error("Error adjusting inventory:", error.response.data);
  }
};

const HandleShopProductCreation = async () => {
  const productId = await createProduct(exampleProduct);
  if (productId) {
    console.log(`Successfully created product: ${productId}`);
    const createdVariants = await createVariants(
      productId,
      exampleProduct.variants
    );
    for (const variant of createdVariants) {
      await adjustInventory(variant.inventoryItemId, variant.inventoryQuantity); // Ensure inventoryItemId is correct
    }
  } else {
    console.error("Failed to create product");
  }
};

 

 

I see the only problem now is we are getting a default variant with a default title and the sales channel which seems to be needed isn't being added. I assume I need another mutation to do the sales channel?

Best

Demonix
Shopify Partner
6 0 1

I've got the sales channel sorted now its just that default title as a option/variant I'm getting.

LaytonBerth
Tourist
8 1 5

I also use the strategy to remove the standalone variant but still get the default variant, any solution..?