6. Service Bus Azure Function

Create our 2nd Azure Function to pick up Order Created messages from the Azure Service Bus and cascade to Marketplacer.

Build Outcome

By the end of this section we will have:

  • Created an Azure Service Bus Trigger Function
  • Tested the function locally to ensure it triggers
  • Deployed the function to Azure

Steps

Create an Azure Service Bus Trigger Function

Login to the Azure portal and:

1: Select Create a resource

Create a resource


2: Search for “Azure Function” and select the Function App by Microsoft:

Azure Function


3: Click Create:

Function App Selection


4. Complete the fields in the Create Function App form, ensuring that you select your own suitable values for the following fields:

  • Resource Group
  • Function App Name
  • Region

Creating a Function


5: You can choose to move through the rest of the function set up or select Review + create as the default values are sufficient for this tutorial.

Review and Create


6: When you’re happy with the configuration, click create and Azure will start to provision the service.

Create Function


7: When the deployment is complete, click Go to resource to ensure the service is running:

Go to resource


The resource should be in the running state:

Running resource


Create an Azure Function Project in VS Code

8: Open a new session of VS Code, 1. select the Azure Logo, 2. click the Create Function.. button:

Create the function

Navigate to a suitable project folder location for your function (you will need to create this manually if a suitable location does not exist), and select it.

9. Select C# as the language

C Sharp


10. Select the .NET 6 Runtime

.NET runtime


11. Select the Azure Service Bus Topic Trigger

Http Trigger


12: Keep the default name for the trigger function

Topic Trigger Name


13 Keep the default namespace

Namespace


14: Keep the default settings location

Setting Location


15: Select the correct service bus namespace

Service Bus Namespace


16: Enter the name of the topic, in this case it should be orders

Service Bus Topic


17: Enter the name of the subscription, in this case is should be S1

Service Bus Subscription


18: Open the project in the current window

Project Location


Update local project settings

19: Open the local.settings.json file and add the following configuration attributes to it:

  • MPOperatorAPIKey - The API Key of the Marketplacer Operator API
  • MPOperatorAPIEndPoint - The endpoint of the Graphql based Operator API
    • Ensure that you include the /graphql route
  • MPBasicAuth - [Optional] Only add this if you are using a Marketplacer instance that requires additional Basic Authentication. Refer to the instructions below on how to calculate this value. Otherwise you can ignore this.

Calculating the value of a Basic Auth Header

If your basic auth username was Admin and your password was abc123 then you calculate the value of a basic auth header in the following way:

  1. Form a single string with your username, a colon “:” and the password, e.g. Admin:abc123 (no spaces in between)
  2. Base 64 encode that string, e.g. QWRtaW46YWJjMTIz
  3. Form the value of the Auth header by specifying the auth-type (in this case Basic) and your Base 64 encoded username and password, e.g. Basic QWRtaW46YWJjMTIz (ensure there is a space between Basic and the encoded username and password)

So in this example that value you’d populate for MPBasicAuth would be: Basic QWRtaW46YWJjMTIz

Note: there are plenty of online tools that can do both Base 64 encoding as well as full Basic Auth header generation. Use your favorite search engine or generative AI for more detail.

Your file should look something like this (sensitive values have been redacted):

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "ctneworderbus_SERVICEBUS": "Endpoint=sb://<your service buss connection string>",
    "MPBasicAuth": "Basic QWRtaW46YWJjMTIz",
    "MPOperatorAPIKey": "<your operator API key>",
    "MPOperatorAPIEndPoint": "https://<your Marketplacer instance>.marketplacer.com/graphql"
  }
}

This file is used to store the local config elements that will allow us to make a call to the Marketplacer Operator API, some points to note:

  • The local.settings.json file is for local config only, and should not be exposed anywhere outside your local development environment, e.g. GitHub. If there isn’t already an entry for it in the project .gitignore file then you should add one.
  • We will need to replicate these configuration elements in our Azure function, we’ll do this later.

Create an Order Create Method

We want to create a method in our function that will accept the payload of the commercetools subscription, deserialize it, and then create an order in Marketplacer. The payload of the commercetools order created message is rather long so rather listing it in its entirety, we’ve outlined the components that we will be interested in below:

  • order.lineItems[0].variant.key
  • order.id

Some points to note:

  • The only elements of the commercetools order that we’ll cascade through when creating an order in Marketplacer are those listed above (the variant key = which will contain the Marketplacer VariantId, and the commercetools order id that we’ll add as an external id). Items such as customer name, address etc. will not cascade and will be hard coded - this is purely to keep our solution example simple.
  • We will only place an order for the first line item in the order, again for simplicity.
  • We will populate a suitable Marketplacer variant id into an existing fully defined commercetools product in a later section.

To create our order create method:

20: In the ServiceBusTopicTrigger.cs file add a new method to the ServiceBusTopicTrigger1 class:

public async Task<bool> CreateMarketplacerOrderAsync(string body)
{
  string variantId = string.Empty;
  string commercetoolsOrderId = string.Empty;

  var payload = JsonConvert.DeserializeObject<dynamic>(body);

  try
  {
    variantId = payload!.order.lineItems[0].variant.key;
    commercetoolsOrderId = payload!.order.id;
    Console.WriteLine($"---> The variant id is: {variantId}");
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"---> The Commercetools order id is: {commercetoolsOrderId}");
    Console.ResetColor();
  }
  catch(Exception ex)
  {
    Console.WriteLine($"---> Exception: {ex.Message}");
    return false;
  }

  if (variantId == string.Empty || commercetoolsOrderId == string.Empty)
    return false;

  var mpMutationObject = new
  {
    query = "mutation{orderCreate(input:{order:{firstName:\"Jane\" surname:\"Doe\" phone:\"0405555555\" emailAddress:\"jane.doe@email.com\" address:{address:\"146 Buckhurst Street\" city:\"Melbourne\" country:{code:\"AU\"}postcode:\"3000\" state:{name:\"Victoria\"}}externalIds:[{key:\"Commercetools_Order_Id\" value:\"" + commercetoolsOrderId + "\"}]}lineItems:[{variantId:\"" + variantId + "\",quantity:1,cost:{amount:999}}]}){order{id legacyId totalCents invoices{nodes{id legacyId}}}errors{field messages}}}",
    variables = new { }
  };

  var httpContent = new StringContent(
    JsonConvert.SerializeObject(mpMutationObject),
    Encoding.UTF8,
    "application/json");

  HttpClient client = new HttpClient();

  //This header is optional if you are not using basic authentication
  client.DefaultRequestHeaders.Add("Authorization", Environment.GetEnvironmentVariable("MPBasicAuth"));
            
  client.DefaultRequestHeaders.Add("marketplacer-api-key", Environment.GetEnvironmentVariable("MPOperatorAPIKey"));

  var response = await client.PostAsync(Environment.GetEnvironmentVariable("MPOperatorAPIEndPoint"), httpContent);

  if (response.IsSuccessStatusCode)
    {
      var content = await response.Content.ReadAsStringAsync();
      var contentObject = JsonConvert.DeserializeObject<dynamic>(content);
      Console.WriteLine($"---> Response Contents: {content}");
      if (contentObject!.data.orderCreate.order != null)
        {
          //Get the Marketplacer Order ID
          Console.WriteLine($"----> Marketplacer OrderId: {contentObject!.data.orderCreate.order.id}");
          return true;
        }
      else
        {
          Console.ForegroundColor = ConsoleColor.Red;
          Console.WriteLine($"----> Order Create Error: {contentObject!.data.orderCreate.errors}");
          Console.ResetColor();
          return false;
        }
      }
  return false;
}

You will also need to include the following namespaces at the top of the ServiceBusTopicTrigger.cs file:

  • using System.Threading.Tasks;
  • using Newtonsoft.Json;
  • using System.Net.Http;
  • using System.Text;

The CreateMarketplacerOrderAsync method accepts the string that is received from the Azure Message Bus and:

  • Deserializes the string
  • Attempts to extract the key value for the first Line Items’ variant on the order
  • Attempts to extract the commercetools order id
  • Will return false if either of those extracted values are null
  • Create orderCreate mutation call
  • Make request to the Marketplacer API

Update the Run method

21: Remaining in the ServiceBusTopicTrigger1 class update the Run method as follows

public async Task Run([ServiceBusTrigger("orders", "S1", Connection = "ctneworderbus_SERVICEBUS")]string mySbMsg)
{
  Console.WriteLine($"C# ServiceBus topic trigger function processed message: {mySbMsg}");

  if (await CreateMarketplacerOrderAsync(mySbMsg))
  {
    Console.WriteLine("---> Order created ok");
  }
  else
  {
    Console.WriteLine("---> Order not created");
  }
}

Make sure to change the method signature of Run from returning void to returning async Task. Also note we’re using Console.Writeline to print the message.

Run a local instance of the function

Casting your mind back to when we created a subscription in commerce tools, as part of the creation process commercetools places a test message onto the Azure Service Bus.

This is not a order created event, but it should still be enough for us to test whether the function will trigger, so we should see:

  • The message payload printed to the console
  • A binding exception will be thrown (and caught) - this is not a order payload
  • The error message: Order not created

To run up a local instance of our project, ensure all your changes are saved and:

22: In VS Code with our function project open press F5 to run up a local instance of our function, give it a minute or two and you should see something similar to the following:

Message Triggered


This of course has not created an order, but it does prove out that our function can read from the service bus.

Deploy function code to Azure

23: Back in VS Code, select the Azure icon, then select Deploy to function app…:

Deploy to function


24: Select the Azure Function name for the order create function:

Select function


Follow the remaining on-screen prompts to complete the deployment.

25: When the deployment completes, you should see the following message, click on Upload settings to push our local config up to Azure:

Upload Config


26: Back over in our Azure function, click Configuration and ensure that the highlighted configuration elements have been pushed to Azure:

Check Config


Although not necessary, you can check the values that have been populated by clicking on the appropriate value.

Update commercetools product

Before we can place an order in commercetools that will flow all the way through to Marketplacer, we want to attach a variant Id to one of the well-formed products in commercetools.

First up, let’s obtain a suitable Marketplacer Variant Id.

27: Using the API client of your choice (as mentioned at the start we’ll use Insomnia) run the following advertSearch query on your Marketplacer instance:

query {
	advertSearch {
		adverts {
			nodes {
				id
				title
				seller {
					businessName
				}
				variants {
					nodes {
						id
						label
						countOnHand
					}
				}
			}
		}
	}
}

Running this query will return purchasable products (adverts) along with their variants, a sample abbreviated (1 product) response is shown below:

{
  "data": {
    "advertSearch": {
      "adverts": {
        "nodes": [
          {
            "id": "QWR2ZXJ0LTEwMDAwMjU2Mw==",
            "title": "T-Shirt",
            "seller": {
              "businessName": "ACME Corp."
            },
            "variants": {
              "nodes": [
                {
                  "id": "VmFyaWFudC0zNjE2",
                  "label": "Red / L",
                  "countOnHand": 1000
                }
              ]
            }
          }
        ]
      }
    }
  }
}

We can see this product has 1 variant with a countOnHand position of 1000 units, we’ll pick this id (VmFyaWFudC0zNjE2) and use it in the next step.

Note: you will have to pick an appropriate variant id returned from your own query

Next up we want to update a published product in commercetools with this id, you could of course use the commercetools API to do this, we’re just going to use the commercetool Merchant Center.

28: Login to the commercetools Merchant Center, select Products, then Product list and identify a published that you want to work with:

Product List


29: Select the variant tab, and pick a variant that you want to update:

Select Variant


30: Select the General & Attributes tab, and update the Variant Key with the Marketplacer variant Id:

Update Variant


Save your changes to return to the Product Screen, you will see that the product is in the Modified state:

Modified


31: Select Published to place the product back into the Published state:

Published

Create and Order

We are now going to simulate an order being placed in commercetools, to do so:

32: Remaining in the commercetools Merchant Center, select Home then Add order:

Add Order


33: Move through the order create flow until you come to the Items section, populate the Item Search Box with the Marketplacer Variant Id and hit Enter:

Variant Search


34: Move through the rest of the order create flow until you come to the Place order screen and place the order:

Place Order


35: Moving back to the Marketplacer Operator Portal, select Orders then All sales and you will see an order has been placed for the variant with an id of VmFyaWFudC0zNjE2. Make a note of the Order # as we’ll be using that next (in this example it’s 10375).

Order in Marketplacer

You’ll note that the product in commercetools and Marketplacer don’t actually match, that’s simply because we have assigned a “random” Marketplacer variant to a Product Variant in commercetools…


36: Move back to your favorite API client and run the following query, this just takes the orderId and return the ExternalIds for that order:

query {
	invoices(filters: { orderId: 10375 }) {
		nodes {
			order {
				externalIds {
					key
					value
				}
			}
		}
	}
}

In this case we can see that we have added the commercetools order Id as an externalId in Marketplacer.

{
	"data": {
		"invoices": {
			"nodes": [
				{
					"order": {
						"externalIds": [
							{
								"key": "Commercetools_Order_Id",
								"value": "7977951b-f351-47df-9377-07c6a1f5e5b7"
							}
						]
					}
				}
			]
		}
	}
}

37: You can then use the external id to search for the corresponding object in Marketplacer:

query {
	externalIds(
		ownerType: "order"
		key: "Commercetools_Order_Id"
		value: "7977951b-f351-47df-9377-07c6a1f5e5b7"
	) {
		owner {
			id
			__typename
			... on Order {
				id
				legacyId
				lineItems {
					nodes {
						variantId
					}
				}
			}
		}
	}
}

Final Thoughts

This was iteration #1 of this tutorial, the primary aim of which was to illustrate how both the Product and Order flows could work between Marketplacer and commercetools. Future iterations are likely to include the following elements:

  • Session Management (for the commercetools access_token)
  • Product Creation that results in a orderable product in commercetools
  • Mapping of all the relevant commercetools order fields through onto the Marketplacer order object
  • Product Updates
  • Order Retries
  • Line Item Dispatch