Update pricing and availability for a batch of products

This desktop console example shows how to use the Inventory resource to update pricing and availability for a batch of products. For information about working with the Inventory resource, see Manage product pricing.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading.Tasks;
using System.Net;
using Content.OAuth;             // Reference to CodeGrantFlow DLL simple OAuth example
using Newtonsoft.Json;           // NuGet Json.NET
using System.Globalization;

namespace Catalogs
{
    class Program
    {
        // The application ID  that you were given when you
        // registered your application.

        private static string clientId = "<APPLICATIONIDGOESHERE>";
        private static string storedRefreshToken = "<REFRESHTOKENGOESHERE>";
        private static CodeGrantOauth tokens = null;
        private static DateTime tokenExpiration;

        private static string devToken = "<DEVELOPERTOKENGOESHERE>";

        private static string storeCode = "online";
        private static string[] productIds = new[] {
            "<PRODUCTIDGOESHERE>",
            "<PRODUCTIDGOESHERE>" };

        // URI templates used to update product pricing.

        public const string BaseUri = "https://content.api.bingads.microsoft.com/shopping/v9.1";
        public static string BmcUri = BaseUri + "/bmc/{0}";
        public static string InventoryBatchUri = BmcUri + "/inventory/batch";

        // Your store ID.

        public static ulong merchantId = <STOREIDGOESHERE>;

        // Suppress NULL when serializing objects.

        public static JsonSerializerSettings jsonSettings = new JsonSerializerSettings()
        { NullValueHandling = NullValueHandling.Ignore };


        static void Main(string[] args)
        {
            try
            {
                var headers = GetCredentialHeaders();

                // Build the inventory endpoint.

                var url = string.Format(InventoryBatchUri, merchantId);

                // Create a list of products to update. This example shows one
                // product in the batch. 
                // The product must specify both price and availability.
                // Sale price and effective date are optional. If missing,
                // sale price and effective data are removed from the product.

                var batch = new Batch()
                {
                    Entries = new List<Entry>()
                    {
                        new Entry()
                        {
                            BatchId = 1,
                            StoreCode = storeCode,
                            ProductId = productIds[0],
                            Inventory = new Product()
                            {
                                Price = new Price()
                                {
                                    Value = 4567,
                                    Currency = "USD"
                                },
                                Availability = "in stock"
                            }
                        },
                        new Entry()
                        {
                            BatchId = 2,
                            StoreCode = storeCode,
                            ProductId = productIds[1],
                            Inventory = new Product()
                            {
                                Price = new Price()
                                {
                                    Value = 7654,
                                    Currency = "USD"
                                },
                                Availability = "bad in stock"
                            }
                        }
                    }
                };

                // Update the product's price and availability. The response
                // body contains the Batch object. If a product in the batch 
                // successfully updated, the entry contains the batch ID. If 
                // the product failed to update, the entry contains the batch 
                // ID and error details.  

                var response = AddResource(url, headers, batch);

                // Check which updates succeeded and which failed.

                foreach (var entry in response.Entries)
                {
                    if (entry.Errors == null)
                    {
                        Console.WriteLine("{0} update succeeded\n", batch.Entries[(int)entry.BatchId - 1].ProductId);
                    }
                    else
                    {
                        foreach (var error in entry.Errors.Errors)
                        {
                            Console.WriteLine("{0} update failed", batch.Entries[(int)entry.BatchId - 1].ProductId);
                            Console.WriteLine("Reason: {0}\nMessage: {1}\n", error.Reason, error.Message);
                        }
                    }
                }

            }
            catch (WebException e)
            {
                Console.WriteLine("\n" + e.Message);

                HttpWebResponse response = (HttpWebResponse)e.Response;

                // If the request is bad, the API returns the errors in the 
                // body of the request. For cases where the path may be valid, but
                // the resource does not belong to the user, the API returns not found.

                if (HttpStatusCode.BadRequest == response.StatusCode ||
                    HttpStatusCode.NotFound == response.StatusCode ||
                    HttpStatusCode.InternalServerError == response.StatusCode)
                {
                    using (Stream stream = response.GetResponseStream())
                    {
                        StreamReader reader = new StreamReader(stream);
                        string json = reader.ReadToEnd();
                        reader.Close();

                        // Deserialize error string into errors object.

                        try
                        {
                            var batch = JsonConvert.DeserializeObject<Batch>(json);

                            PrintBatchErrors(batch.Entries);
                        }
                        catch (Exception deserializeError)
                        {
                            // This case occurs when the path is not valid.

                            if (HttpStatusCode.NotFound == response.StatusCode)
                            {
                                Console.WriteLine("Path not found: " + response.ResponseUri);
                            }
                            else
                            {
                                Console.WriteLine(deserializeError.Message);
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("\n" + e.Message);
            }
        }


        // Update a product's pricing and availability.

        private static Batch AddResource(string uri, WebHeaderCollection headers, Batch batch)
        {
            var request = (HttpWebRequest)WebRequest.Create(uri);
            request.Method = "POST";
            request.Headers = headers;
            request.ContentType = "application/json";

            var json = JsonConvert.SerializeObject(batch, jsonSettings);
            request.ContentLength = json.Length;
            using (Stream requestStream = request.GetRequestStream())
            {
                StreamWriter writer = new StreamWriter(requestStream);
                writer.Write(json);
                writer.Close();
            }

            var response = (HttpWebResponse)request.GetResponse();

            Batch batchOut = null;

            using (Stream responseStream = response.GetResponseStream())
            {
                var reader = new StreamReader(responseStream);
                var jsonOut = reader.ReadToEnd();
                reader.Close();
                batchOut = JsonConvert.DeserializeObject<Batch>(jsonOut);
            }

            return batchOut;
        }


        // Print errors.

        private static void PrintBatchErrors(List<Entry> entries)
        {
            foreach (Entry entry in entries)
            {
                PrintErrors(entry.Errors);
            }
        }

        private static void PrintErrors(BatchEntryError batchErrors)
        {
            foreach (Error error in batchErrors.Errors)
            {
                Console.WriteLine("reason: {0}\nmessage: {1}\n",
                    error.Reason, error.Message);
            }
        }


        // Gets the AuthenticationToken and DeveloperToken headers 
        // that are required to call the API. 

        private static WebHeaderCollection GetCredentialHeaders()
        {
            // TODO: Add logic to get the logged on user's refresh token 
            // from secured storage. 

            tokens = GetOauthTokens(storedRefreshToken, clientId);

            var headers = new WebHeaderCollection();
            headers.Add("AuthenticationToken", tokens.AccessToken);
            headers.Add("DeveloperToken", devToken);

            return headers;
        }

        // Gets the OAuth tokens. If the refresh token doesn't exist, get 
        // the user's consent and a new access and refresh token.

        private static CodeGrantOauth GetOauthTokens(string refreshToken, string clientId)
        {
            CodeGrantOauth auth = new CodeGrantOauth(clientId);

            if (string.IsNullOrEmpty(refreshToken))
            {
                auth.GetAccessToken();
            }
            else
            {
                auth.RefreshAccessToken(refreshToken);

                // Refresh tokens can become invalid for several reasons
                // such as the user's password changed.

                if (!string.IsNullOrEmpty(auth.Error))
                {
                    auth = GetOauthTokens(null, clientId);
                }
            }

            // TODO: Store the new refresh token in secured storage
            // for the logged on user.

            if (!string.IsNullOrEmpty(auth.Error))
            {
                throw new Exception(auth.Error);
            }
            else
            {
                storedRefreshToken = auth.RefreshToken;
                tokenExpiration = DateTime.Now.AddSeconds(auth.Expiration);
            }

            return auth;
        }
    }
}


// Classes used to update a product's price and availability.

public class Batch
{
    [JsonProperty("entries")]
    public List<Entry> Entries { get; set; }
}

// Batch ID is user-defined. Store code must be online.
// Errors is set in the response if an error occurs.
public class Entry
{
    [JsonProperty("batchId")]
    public UInt32 BatchId { get; set; }

    [JsonProperty("storeCode")]
    public string StoreCode { get; set; }

    [JsonProperty("productId")]
    public string ProductId { get; set; }

    [JsonProperty("inventory")]
    public Product Inventory { get; set; }
    
    [JsonProperty("errors")]
    public BatchEntryError Errors { get; set; }
}


public class Price
{
    [JsonProperty("currency")]
    public string Currency { get; set; }

    [JsonProperty("value")]
    public decimal? Value { get; set; }
}

// The product fields you may update. Price and
// availability are required and sale price and
// effective date are optional.

public class Product
{
    public Product() { }

    [JsonProperty("availability")]
    public string Availability { get; set; }

    [JsonProperty("price")]
    public Price Price { get; set; }

    [JsonProperty("salePrice")]
    public Price SalePrice { get; set; }

    [JsonProperty("salePriceEffectiveDate")]
    public string SalePriceEffectiveDate { get; set; }
}


// Classes used to handle errors.

public class Error
{
    [JsonProperty("domain")]
    public string Domain { get; set; }

    [JsonProperty("message")]
    public string Message { get; set; }

    [JsonProperty("reason")]
    public string Reason { get; set; }
}


public class BatchEntryError
{
    [JsonProperty("errors")]
    public List<Error> Errors { get; set; }
}