Set up a pricing endpoint/API
Create an API that:
- uses a current customers location id and multiple productvariant id's as input.
- fetches the current customer's prices from the Shopify GraphQL Admin API, using the input
- returns the variantId and desired price as output.
The output would be in this format:
[
{
"id": "49328019669329",
"price": "712.0"
},
{
"id": "49328019997009",
"price": "729.95"
},
{
"id": "49328020324689",
"price": "749.95"
},
{
"id": "49328019538257",
"price": "712.0"
}
]Example API
An example API in node.js that retrieves customer specific product prices from Shopify:
import express from "express";
import cors from "cors";
import ShopifyService from "./service";
const app = express();
const port = 3000;
app.use(cors());
app.use(express.json());
app.post("/api/pricing", async (req, res, next) => {
const { locationId, variantIds } = req.body;
var service = new ShopifyService();
var data = await service.getForLocation(locationId, [], variantIds);
var mapped = data.nodes?.map((n: any) => {
return {
id: n.id.replace("gid://shopify/ProductVariant/", ""),
price: n.contextualPricing.price.amount,
};
});
res.send(mapped);
});
app.listen(port, () => {
console.log(`listening on port ${port}`);
});import axios from "axios";
class ShopifyService {
// Hardcoded credentials (matching your C# example)
// Note: In a production app, use process.env.SHOPIFY_CLIENT_ID, etc.
clientId = "";
secret = "";
shopUrl = "yourshop.myshopify.com";
token = "";
// GraphQL Queries
private VARIANT_QUERY = `
query getB2BVariantPrices($variantIds: [ID!]!, $locationId: ID!) {
nodes(ids: $variantIds) {
... on ProductVariant {
id
variantId: id
contextualPricing(context: { companyLocationId: $locationId }) {
price {
amount
currencyCode
}
}
}
}
}
`;
/**
* Executes the GraphQL query against Shopify
* @param {Object} queryPayload - The { query, variables } object
*/
private async executeQuery(queryPayload: any) {
// Check if token is missing, if so, fetch it
if (!this.token || this.token.trim() === "") {
try {
this.token = await this.getOauthToken(
this.shopUrl,
this.clientId,
this.secret
);
console.log("Token obtained: " + this.token);
} catch (err: any) {
console.error("Failed to obtain token:", err.message);
return null;
}
}
const url = `https://${this.shopUrl}/admin/api/2024-04/graphql.json`;
try {
const response = await axios.post(url, queryPayload, {
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": this.token,
},
});
// Axios throws on 4xx/5xx by default, but if Shopify returns 200 with errors in body:
if (response.data.errors) {
console.error("\x1b[31m%s\x1b[0m", "GraphQL Errors:"); // Red color
console.error(JSON.stringify(response.data.errors, null, 2));
}
return response.data;
} catch (error: any) {
console.error("\x1b[31m%s\x1b[0m", "HTTP Request Error:"); // Red color
if (error.response) {
// Server responded with a status other than 2xx
console.error(`Status: ${error.response.status}`);
console.error(JSON.stringify(error.response.data, null, 2));
} else {
// Network error or setup error
console.error(`Exception: ${error.message}`);
}
return null;
}
}
/**
* Main method to fetch prices
* @param {string} locationId - The raw ID (e.g., "123")
* @param {string[]} productIds - Array of raw product IDs
* @param {string[]} variantIds - Array of raw variant IDs
*/
public async getForLocation(
locationId: string,
productIds: string[],
variantIds: string[]
) {
// 1. Format IDs to Shopify GIDs
const formattedLocationId = `gid://shopify/CompanyLocation/${locationId}`;
// Note: In your C# code, you called .Select() but didn't assign the result back.
// Here we map properly to ensure the GIDs are sent to the API.
const formattedVariantIds = variantIds.map(
(id) => `gid://shopify/ProductVariant/${id}`
);
// 2. Construct Payload
const queryPayload = {
query: this.VARIANT_QUERY,
variables: {
locationId: formattedLocationId,
variantIds: formattedVariantIds,
},
};
try {
// Log the attempt (using JSON.stringify to mimic C# serialization logging)
console.log(
"\nFetching prices:",
JSON.stringify({
ids: formattedVariantIds,
locationId: formattedLocationId,
})
);
// 3. Execute
const result = await this.executeQuery(queryPayload);
return result?.data;
} catch (ex: any) {
console.error("\x1b[31m%s\x1b[0m", `Exception: ${ex.message}`);
}
}
/**
* Retrieves the OAuth token via Client Credentials flow
*/
private async getOauthToken(
shopifyDomain: string,
shopifyClientId: string,
shopifyClientSecret: string
) {
const tokenEndpoint = `https://${shopifyDomain}/admin/oauth/access_token`;
// URLSearchParams is the Node equivalent of FormUrlEncodedContent
const params = new URLSearchParams();
params.append("grant_type", "client_credentials");
params.append("client_id", shopifyClientId);
params.append("client_secret", shopifyClientSecret);
try {
const response = await axios.post(tokenEndpoint, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
const accessToken = response.data.access_token;
if (!accessToken) {
throw new Error(
"Shopify OAuth response did not contain an access_token."
);
}
return accessToken;
} catch (error: any) {
let errorMessage = "Shopify auth token request failed.";
if (error.response) {
errorMessage += ` (${error.response.status} ${error.response.statusText})`;
}
throw new Error(errorMessage);
}
}
}
export default ShopifyService;
Fetch the prices
In your theme, add a function that calls the API to fetch the prices and updates the Tweakwise JS tiles to display the correct price:
<script>
function tw__applyPricing(items){
const productids = items.map(o => o.itemno);
var locationId = {{ customer.current_location.id }};
var body = {
locationId: '{{customer.current_location.id}}',
variantIds: productids
};
console.log('tw | getting pricing for', body)
fetch('https://your.shop.com/api/pricing', {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body)
})
.then(response => response.json())
.then(function(result){
result.forEach(function(item){
var element = document.querySelector('[data-item-id="' + item.id + '"] [data-property="price"]');
if(element){
element.innerHTML = item.price;
}
});
});
}
</script>Display the prices
Hook the function into Tweakwise JS to make sure the prices get displayed:
<script>
tweakwiseListerPage({
//...
on: {
'twn.request.success': function (event) {
tw__applyPricing(event.data);
}
}
})
tweakwiseRecommendations({
//...
on: {
'twn.request.success': function (event) {
tw__applyPricing(event.data.items);
}
}
})
</script>