Lab - Postman Quote Workflow

Plan: B2B Developer

Lesson 12 of 18 · 45 min

Introduction

In this lab, you’ll explore the common storefront tasks involved with managing and submitting quotes. This will also give you an opportunity to observe the impact that different user roles have on GraphQL results.

Prerequisites

  • A BigCommerce sandbox store or trial store, or a full production store
  • B2B Edition enabled in your store
  • Postman or a similar API client
  • An existing Postman environment, collection, and headers preset as configured in previous labs
  • Minimal quote/payment settings as described below

In this lab, you will:

  • Create API requests to handle quote submission, management, and conversion
  • Implement Postman scripts to automate values used in API requests
  • Practice sending quote-related requests from the perspective of users with different roles/permissions

Required Quote/Payment Settings

The lab will involve logging into your storefront as a company user to place an order from a quote. The following minimal settings are required in the B2B Edition section of your control panel:

  • In Settings > General, “Quotes” must be checked under “Feature Management.”
  • In Settings > Quotes, “Allow Quote Request for” should have “Company user” checked, and “Enable add to quote button” should be checked.
  • In Settings > Checkout, the “Purchase Order Payment Method” should be enabled.
  • The Payments settings for the company you will use for the storefront must not have been customized to exclude “PO” as an available payment option.

Postman Recap

As a reminder, in previous labs you should have set up these collection variables:

  • admin_email
  • admin_password
  • junior_email
  • junior_password

Make sure these variables are set with valid credentials values. You’ll need the “Admin Login” and “Junior Login” requests that you previously created.

Step 1: Fetch Currency Details

  1. Create a new HTTP request and save it to your collection with the name “Get Currencies.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
query GetCurrencies(
$storeHash: String!,
$channelId: String!
) {
currencies(
storeHash: $storeHash,
channelId: $channelId,
) {
currencies {
currency_code,
currency_exchange_rate,
token,
decimal_token,
decimal_places,
token_location,
thousands_token,
},
channelCurrencies
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"storeHash": "{{store_hash}}",
"channelId": "{{storefront_channel_id}}"
}
  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const currencies = pm.response.json().data?.currencies?.currencies ?? [];
pm.test(`Response includes at least one currency`, () => {
pm.expect(
currencies.length
).to.be.greaterThan(0);
});
const defaultCurrencyCode = pm.response.json().data?.currencies?.channelCurrencies?.default_currency;
pm.test(`Response includes default currency code`, () => {
pm.expect(
defaultCurrencyCode
).to.be.a('string');
});
const defaultCurrency = currencies.find(currency => {
return currency?.currency_code === defaultCurrencyCode;
});
pm.test(`Response includes default currency info`, () => {
pm.expect(
defaultCurrency?.currency_code
).to.be.a('string');
});

These tests verify not only that currency information was returned in the proper format, but also that the currency identified as the channel’s default is specifically present.

  1. Enter the following code immediately after the previous Post-response code.
pm.collectionVariables.unset('currency_code');
pm.collectionVariables.unset('currency_exchange_rate');
pm.collectionVariables.unset('currency_token');
pm.collectionVariables.unset('currency_decimal_token');
pm.collectionVariables.unset('currency_decimal_places');
pm.collectionVariables.unset('currency_token_location');
pm.collectionVariables.unset('currency_thousands_token');
pm.collectionVariables.set('currency_code', defaultCurrency.currency_code);
pm.collectionVariables.set('currency_exchange_rate', defaultCurrency.currency_exchange_rate);
pm.collectionVariables.set('currency_token', defaultCurrency.token);
pm.collectionVariables.set('currency_decimal_token', defaultCurrency.decimal_token);
pm.collectionVariables.set('currency_decimal_places', defaultCurrency.decimal_places);
pm.collectionVariables.set('currency_token_location', defaultCurrency.token_location);
pm.collectionVariables.set('currency_thousands_token', defaultCurrency.thousands_token);

There are quite a lot of values being captured in Postman collection variables here, but all will be needed when submitting a quote. This script ensures that the values matching the storefront’s default currency are automatically captured.

  1. Send the request and verify that all tests succeed.
  2. Verify that collection variables have been set for all currency details.

Step 2: Fetch Product Details

In order to submit a quote request for specific products from your storefront, you’ll need catalog details like product and variant IDs, as well as product prices. In this section, you’ll utilize the productsSearch query to capture these details. Automation utilizing Postman scripts will assist in translating the appropriate values from the product data into the eventual quote request.

You’ll perform this and subsequent requests from the context of your Junior Buyer user, as this role has permission to submit quotes.

  1. Re-run the “Junior Buyer Login” request to regenerate your GraphQL authentication token and other details.
  2. Verify that the b2b_logged_in_email variable on your environment reflects the Junior Buyer email address.
  3. If the b2b_logged_in_company_id variable is not set in your environment, or the company ID doesn’t match your Junior Buyer user, re-run the “User Company” request.

Now that you’ve authenticated as a Junior Buyer, let’s set up the request to fetch product details. For the workflow you’ll be setting up, you’ll choose two products from your store catalog to be part of the quote.

  1. Log in to your BigCommerce control panel and navigate to Products.
  2. Record the IDs of your two chosen products. The IDs can be found in the URL path of an individual product edit page, which is in the format /manage/products/edit/{product-id}.

If possible, include a product with variants as one of your IDs, as you’ll be able to observe the effects of the quote process with such a product.

  1. Create a new HTTP request and save it to your collection with the name “Search Products.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
query SearchProducts(
$productIds: [Int],
$companyId: String
) {
productsSearch(
productIds: $productIds,
companyId: $companyId
) {
id
name
sku
availability
variants
imageUrl
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"productIds": [<Product 1 ID>,<Product 2 ID>],
"companyId": "{{company_id}}"
}
  1. Replace <Product 1 ID> and <Product 2 ID> with the IDs of your chosen products. For example, [13, 27].
  2. In the Scripts tab, enter the following “Post-response” code.
const numberOfProducts = 2;
const productQty = 10;
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const products = pm.response.json().data?.productsSearch ?? [];
pm.test(`Response includes ${numberOfProducts} products`, () => {
pm.expect(
products.length
).to.be.greaterThan(numberOfProducts - 1);
});
let index = 0;
const productsData = products.map(product => {
index++;
const variants = product?.variants ?? [];
const firstVariant = variants[0] ?? {};
const productData = {
productId: product?.id,
sku: firstVariant?.sku,
price: firstVariant?.calculated_price,
variantId: firstVariant?.variant_id,
imageUrl: firstVariant?.image_url,
name: product?.name,
};
pm.test(`Product ${index} has ID`, () => {
pm.expect(productData.productId).to.be.a('number');
});
pm.test(`Product ${index} has a variant with SKU`, () => {
pm.expect(productData.sku).to.be.a('string');
});
pm.test(`Product ${index} has a variant with price`, () => {
pm.expect(productData.price).to.be.a('number');
});
pm.test(`Product ${index} has a variant with ID`, () => {
pm.expect(productData.variantId).to.be.a('number');
});
pm.test(`Product ${index} has image URL`, () => {
pm.expect(productData.imageUrl).to.be.a('string');
});
pm.test(`Product ${index} has name`, () => {
pm.expect(productData.name).to.be.a('string');
});
return productData;
});

Observe in the script that the numberOfProducts variable controls the test that verifies the proper number of products were returned (2). The productQty variable will also be used as a multiplier for product prices; we’ll eventually be including 10 of each product in the quote request.

The request submitting the quote will be specifically configured for two products and a quantity of 10 for each, but if you choose to change the details of that request, you can easily update the above variables to make sure productsSearch captures the right info.

The script tests that the values you intend to capture are present on each product and compiles that data into a new array. Notice that some values are captured from the product record, while others are captured from the product’s first available variant.

  1. Enter the following code immediately after the previous Post-response code.
index = 0;
let priceTotal = 0;
productsData.forEach(productData => {
index++;
pm.collectionVariables.unset(`product${index}_id`);
pm.collectionVariables.unset(`product${index}_sku`);
pm.collectionVariables.unset(`product${index}_price`);
pm.collectionVariables.unset(`product${index}_variant_id`);
pm.collectionVariables.unset(`product${index}_image_url`);
pm.collectionVariables.unset(`product${index}_name`);
pm.collectionVariables.set(
`product${index}_id`,
productData.productId
);
pm.collectionVariables.set(
`product${index}_sku`,
productData.sku
);
pm.collectionVariables.set(
`product${index}_price`,
productData.price
);
pm.collectionVariables.set(
`product${index}_variant_id`,
productData.variantId
);
pm.collectionVariables.set(
`product${index}_image_url`,
productData.imageUrl
);
pm.collectionVariables.set(
`product${index}_name`,
productData.name
);
priceTotal += (productData.price * productQty);
});
pm.collectionVariables.unset('products_price_total');
pm.collectionVariables.set('products_price_total', priceTotal);

The final portion of the post-response script captures the required values for each product in collection variables - for example, product1_id, product1_price, etc. The full expected price of the initial quote request (based on a quantity of 10 of each product) is also captured.

  1. Send the request and verify that all tests succeed.
  2. Verify that collection variables have been set for the product ID, SKU, price, variant ID, image URL, and name of each of your products.

The product details you’ve captured can now be fed directly into the request to submit the quote.

Step 3: Submit the Quote Request

  1. Create a new HTTP request and save it to your collection with the name “Create Quote.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
mutation CreateQuote(
$message: String,
$subtotal: Decimal!,
$discount: Decimal!,
$grandTotal: Decimal!,
$userEmail: String,
$quoteTitle: String,
$shippingAddress: ShippingAddressInputType!,
$billingAddress: BillingAddressInputType!,
$contactInfo: ContactInfoInputType!,
$companyId: Int,
$currency: CurrencyInputType!,
$storeHash: String!,
$productList: [ProductInputType]!,
$channelId: Int
) {
quoteCreate(
quoteData: {
message: $message,
subtotal: $subtotal,
discount: $discount,
grandTotal: $grandTotal,
userEmail: $userEmail,
quoteTitle: $quoteTitle,
shippingAddress: $shippingAddress,
billingAddress: $billingAddress,
contactInfo: $contactInfo,
companyId: $companyId,
currency: $currency,
storeHash: $storeHash,
productList: $productList,
channelId: $channelId
}
) {
quote {
id
createdAt
quoteNumber
quoteTitle
quoteUrl
}
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"message": "I need this ASAP.",
"subtotal": {{products_price_total}},
"discount": 0,
"grandTotal": {{products_price_total}},
"userEmail": "{{b2b_logged_in_email}}",
"quoteTitle": "Re-stock Quote",
"shippingAddress": {
"country": "United States",
"state": "Texas",
"city": "Austin",
"zipCode": "78726",
"address": "123 Park Central",
"firstName": "John",
"lastName": "Doe",
"addressLine1": "123 Park Central",
"phoneNumber": "444-555-6666"
},
"billingAddress": {
"country": "United States",
"state": "Texas",
"city": "Austin",
"zipCode": "78726",
"address": "123 Park Central",
"firstName": "John",
"lastName": "Doe",
"addressLine1": "123 Park Central",
"phoneNumber": "444-555-6666"
},
"contactInfo": {
"name": "John Doe",
"email": "{{b2b_logged_in_email}}",
"companyName": "John's Widgets",
"phoneNumber": "111-222-3333"
},
"companyId": {{b2b_logged_in_company_id}},
"currency": {
"token": "{{currency_token}}",
"location": "{{currency_token_location}}",
"currencyCode": "{{currency_code}}",
"decimalToken": "{{currency_decimal_token}}",
"decimalPlaces": {{currency_decimal_places}},
"thousandsToken": "{{currency_thousands_token}}",
"currencyExchangeRate": "{{currency_exchange_rate}}"
},
"storeHash": "{{store_hash}}",
"productList": [
{
"productId": {{product1_id}},
"sku": "{{product1_sku}}",
"basePrice": {{product1_price}},
"discount": 0,
"offeredPrice": {{product1_price}},
"quantity": 10,
"variantId": {{product1_variant_id}},
"imageUrl": "{{product1_image_url}}",
"productName": "{{product1_name}}",
"options": []
},
{
"productId": {{product2_id}},
"sku": "{{product2_sku}}",
"basePrice": {{product2_price}},
"discount": 0,
"offeredPrice": {{product2_price}},
"quantity": 10,
"variantId": {{product2_variant_id}},
"imageUrl": "{{product2_image_url}}",
"productName": "{{product2_name}}",
"options": []
}
],
"channelId": {{storefront_channel_id}}
}

Note that the collection variables you’ve captured with your previous requests are being used to populate the appropriate GraphQL variables related to currency, product details and pricing.

In this request, we’re providing a value for channelId, which is not required if your store does not support multiple storefronts. If multiple storefronts are supported, however, this field is necessary even if one storefront is designated as default.

  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const quote = pm.response.json().data?.quoteCreate?.quote;
pm.test(`Response includes quote with ID`, () => {
pm.expect(
quote?.id
).to.be.a('string');
});
pm.test('Quote includes created date', () => {
pm.expect(
quote?.createdAt
).to.be.a('number');
});
pm.collectionVariables.unset('quote_id');
pm.collectionVariables.unset('quote_created_at');
pm.collectionVariables.set('quote_id', parseInt(quote?.id));
pm.collectionVariables.set('quote_created_at', quote?.createdAt);

The ID of the newly created quote, as well as its “created at” timestamp, are captured in collection variables to automatically tie subsequent requests to the most recently created quote.

  1. Send the request, verify that all tests succeed, and verify that the quote_id and quote_created_at variables are set on your collection.

You should also be able to see your new quote in the B2B Edition admin, or in the storefront while logged in as one of the company’s users.

At this step, you may choose to edit the quote in the B2B Edition admin - for example, to set a discount on one or more items in the quote. (Be sure to “Preview” and then “Submit” the quote.) A typical workflow would involve a sales rep making such edits before the next steps in the quote process.

While editing the quote, also try including a message for the buyer.

Step 4: Get Quotes

Now you’ll make a storefront request to fetch all the quotes belonging to your company.

  1. Create a new HTTP request and save it to your collection with the name “Get Quotes.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
query GetQuotes(
$limit: Int,
$after: String,
$orderBy: String
) {
quotes(
first: $limit,
after: $after,
orderBy: $orderBy
) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
edges {
node {
id
createdAt
quoteNumber
quoteTitle
createdBy
createdByEmail
expiredAt
subtotal
discount
discountType
discountValue
shippingTotal
taxTotal
grandTotal
totalAmount
orderId
bcOrderId
contactInfo
trackingHistory
notes
legalTerms
shippingMethod
billingAddress
shippingAddress
salesRepInfo {
salesRepName
salesRepEmail
}
productsList {
productId
variantId
sku
basePrice
discount
offeredPrice
quantity
imageUrl
productName
options
notes
productUrl
type
}
quoteUrl
}
}
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"limit": 5,
"after": null,
"orderBy": "-createdAt"
}

Like in previous requests, the arguments and variables here are designed to easily facilitate pagination once the company has multiple quotes. The “-createdAt” value being passed in for orderBy ensures the quotes will be sorted in descending order by the creation date.

  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const result = pm.response.json().data?.quotes;
pm.test('Total count includes at least one quote', () => {
pm.expect(
result?.totalCount
).to.be.greaterThan(0);
});
let firstQuote = null;
if (Array.isArray(result?.edges)) {
firstQuote = result.edges[0]?.node;
}
pm.test('Response includes at least one quote with an ID', () => {
pm.expect(
firstQuote?.id
).to.be.a('string');
});
  1. Send the request and verify that all tests succeed.

As you’re sorting by creation date, the quote you previously created should be first in the results.

If you edited the quote from the perspective of a sales rep, observe the effects in the trackingHistory field and the various price fields that reflect any new discounts.

Step 5: Export a Quote PDF

  1. Create a new HTTP request and save it to your collection with the name “Export Quote PDF.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
mutation ExportQuotePdf(
$createdAt: Int!,
$lang: String!,
$quoteId: Int!,
$storeHash: String!
) {
quoteFrontendPdf(
createdAt: $createdAt,
lang: $lang,
quoteId: $quoteId,
storeHash: $storeHash
) {
url
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"createdAt": {{quote_created_at}},
"lang": "en",
"quoteId": {{quote_id}},
"storeHash": "{{store_hash}}"
}

Note that you’re passing in the collection variables that previously captured the quote ID and “created at” timestamp, meaning you’ll automatically be exporting a PDF for the most recently created quote. Feel free to manually change these values in the collection.

  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const quoteUrl = pm.response.json().data?.quoteFrontendPdf?.url;
pm.test('Response includes URL', () => {
pm.expect(
quoteUrl
).to.be.a('string');
});
  1. Send the request and verify that all tests succeed.
  2. Copy the URL value from the response and enter it in a browser to verify that the URL results in downloading a PDF with the details of the quote.

Step 6: Submit a Quote Message

  1. Create a new HTTP request and save it to your collection with the name “Add Message to Quote.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
mutation AddQuoteMessage(
$id: Int!,
$storeHash: String!,
$userEmail: String!,
$message: String
) {
quoteUpdate(
id: $id,
quoteData: {
storeHash: $storeHash,
userEmail: $userEmail,
message: $message
}
) {
quote {
createdAt
quoteNumber
quoteTitle
quoteUrl
}
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"id": {{quote_id}},
"storeHash": "{{store_hash}}",
"userEmail": "{{b2b_logged_in_email}}",
"message": "Can you give us your best quote on Express shipping?"
}
  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const quote = pm.response.json().data?.quoteUpdate?.quote;
pm.test('Response includes quote with quote number', () => {
pm.expect(
quote?.quoteNumber
).to.be.a('string');
});
  1. Send the request and verify that all tests succeed.

Step 7: Check Out from a Quote

The most critical step of the quoting process from the storefront perspective is actually placing an order from the quote details. Let’s practice generating the appropriate URL that will allow a storefront user to complete this process in the BigCommerce checkout.

  1. Create a new HTTP request and save it to your collection with the name “Get Quote Checkout.”
  2. Set the same configuration that was used for the previous requests, including the HTTP method, URL, Auth Type, Headers (using your previously created “GraphQL” preset), and Body type.
  3. Enter the following query.
mutation GetQuoteCheckout(
$id: Int!,
$storeHash: String!
) {
quoteCheckout(
id: $id,
storeHash: $storeHash
) {
quoteCheckout {
checkoutUrl
cartId
cartUrl
}
}
}
  1. Enter the following in the GraphQL Variables panel.
{
"id": {{quote_id}},
"storeHash": "{{store_hash}}"
}
  1. In the Scripts tab, enter the following “Post-response” code.
pm.test('Response is not an error', () => {
pm.response.to.not.be.error;
pm.response.to.not.have.jsonBody("errors");
});
pm.test('Response is JSON with data', () => {
pm.response.to.have.jsonBody("data");
});
const checkoutUrl = pm.response.json().data?.quoteCheckout?.quoteCheckout?.checkoutUrl;
pm.test(`Response includes checkout URL`, () => {
pm.expect(
checkoutUrl
).to.be.a('string');
});

Your request is fully configured. However, recall that you most recently used the “Junior Buyer Login” request to generate the GraphQL credentials you’ve been using in the quote creation process, and users with the Junior Buyer role do not have permission to place an order from a quote. Presuming your user’s role hasn’t been changed, you should be able to observe that the “Get Quote Checkout” requests fails with your current authentication.

  1. Send the request and verify that you receive a “Permission denied” error in the response.

Generating the checkout URL successfully should be as easy as re-authenticating as a user with sufficient permissions.

  1. Re-run the “Admin Login” request.
  2. Re-run the “Get Quote Checkout” request and verify that all tests succeed this time.
  3. Log in to your storefront as a Senior Buyer or Admin user with the company associated with the quote.
  4. Copy the checkoutUrl from the response and enter it in a browser to verify that the checkout is initiated.
  5. Complete the checkout and place an order from the quote, using the Purchase Order payment method.

The invoice workflow you’ll examine in the next lab is usually relevant for orders that have been paid with credit, so using the Purchase Order payment method will make sure you have an order that makes sense for that workflow.

Taking It Further

Try out implementing a “Get Quote” request to fetch the details of a single quote using the quote GraphQL query. Configure the request to utilize your quote_id and quote_created_at collection variables to automatically fetch the most recently created quote.