Appendix H: Understand the Customized Integrations and Support for Recurring Pricing (25D and later)
Users with customized integrations (25D and later) who have previously customized their Add to Cart BML need to modify and update their BML to add support for recurring pricing for configurable products.
The following provides the Add to Cart BML for Support for Recurring Pricing (25D and later):
// ===============================
// Define Constants & Global Variables
// ===============================
MODEL_PATH = "/configuration/configureResponse/item/model"; // This path is used for main model extraction, not spares
CONFIG_ID_PATH = "/configuration/configureResponse/item/@configurationId";
CURRENCY_CODE_PATH = "/configuration/configureResponse/attributes/attribute[@_variableName='currencyCode']/value";
TOTAL_PRICE_PATH = "/configuration/configureResponse/price/totalPrice";
BOM_ITEM_PATH = "/configuration/configureResponse/bomItem";
CART_TEMPLATE_LOCATION = "$BASE_PATH$/CommerceCloud/AddToCartPayload-Cloud.txt";
// Define individual XPaths for the values we want from each spare item
SPARE_PART_XPATH = "/configuration/configureResponse/spare/rule/item/part";
SPARE_ITEM_MODEL_XPATH = "/configuration/configureResponse/spare/rule/item/model"; // We will read this to identify models
SPARE_QUANTITY_XPATH = "/configuration/configureResponse/spare/rule/item/quantity";
SPARE_SELECTED_XPATH = "/configuration/configureResponse/spare/rule/item/selected";
SPARE_MANDATORY_XPATH = "/configuration/configureResponse/spare/rule/item/mandatory";
payload = "";
sparesList = "";
priceTotal = 0.0;
baseModelPrice = 0.0;
recurringTotal = 0.0;
oneTimeTotal = 0.0;
unitPrice = 0.0;
configDict = dict("string");
partNumber = "";
bomItemVarName = "";
bomJson = json();
// ===============================
// Extract Data from Configuration XML
// ===============================
singleValuePaths = string[]{MODEL_PATH, CONFIG_ID_PATH, CURRENCY_CODE_PATH, TOTAL_PRICE_PATH, BOM_ITEM_PATH};
if(isnull(configXML) OR len(configXML) < 1){
return "Empty Payload";
}
configXmlData = readxmlsingle(configXML, singleValuePaths);
model = get(configXmlData, MODEL_PATH);
configId = get(configXmlData, CONFIG_ID_PATH);
currency = get(configXmlData, CURRENCY_CODE_PATH);
totalPrice = get(configXmlData, TOTAL_PRICE_PATH);
bomItem = get(configXmlData, BOM_ITEM_PATH);
// --- Core Spare Item Extraction with readxmlmultiple and Tracing ---
// Create an array of XPaths for all relevant elements (part, model, quantity, selected, mandatory)
// We need both part and model XPaths to distinguish between them
spareItemXPaths = string[]{
SPARE_PART_XPATH,
SPARE_ITEM_MODEL_XPATH,
SPARE_QUANTITY_XPATH,
SPARE_SELECTED_XPATH,
SPARE_MANDATORY_XPATH
};
// Use readxmlmultiple to get arrays of values for each XPath
multipleXmlData = readxmlmultiple(configXML, spareItemXPaths);
// Get the arrays of results for each XPath
partNumbers = get(multipleXmlData, SPARE_PART_XPATH);
itemModels = get(multipleXmlData, SPARE_ITEM_MODEL_XPATH);
quantities = get(multipleXmlData, SPARE_QUANTITY_XPATH);
selectedStatuses = get(multipleXmlData, SPARE_SELECTED_XPATH);
mandatoryStatuses = get(multipleXmlData, SPARE_MANDATORY_XPATH);
spareItemsArray = jsonarray();
modelIndex = 0;
partIndex = 0;
quantityIndex = 0;
totalQuantities = 0;
if(isnull(quantities)){
print("DEBUG: No recommended item quantity found in configXML");
}else{
totalQuantities = sizeofarray(quantities);
}
if(isnull(itemModels)){
print("DEBUG: No recommended models found in configXML");
}else{
totalModels = sizeofarray(itemModels);
spareModelRange = range(totalModels);
// --- First map models ---
for idx in spareModelRange {
if (modelIndex >= totalModels OR quantityIndex >= totalQuantities) {
break;
}
modelValue = itemModels[modelIndex];
quantityValue = quantities[quantityIndex];
item = json();
jsonput(item, "model", modelValue);
jsonput(item, "quantity", quantityValue);
// Optionally get selected / mandatory if needed
if (quantityIndex < sizeofarray(selectedStatuses)) {
jsonput(item, "selected", selectedStatuses[quantityIndex]);
}
if (quantityIndex < sizeofarray(mandatoryStatuses)) {
jsonput(item, "mandatory", mandatoryStatuses[quantityIndex]);
}
jsonarrayappend(spareItemsArray, item);
modelIndex = modelIndex + 1;
quantityIndex = quantityIndex + 1;
}
}
if(isnull(partNumbers)){
print("DEBUG: No recommended parts found in configXML");
}else{
totalParts = sizeofarray(partNumbers);
sparePartsRange = range(totalParts);
// --- Then map parts ---
for idx in sparePartsRange{
if (partIndex >= totalParts OR quantityIndex >= totalQuantities) {
break;
}
partValue = partNumbers[partIndex];
quantityValue = quantities[quantityIndex];
item = json();
jsonput(item, "part", partValue);
jsonput(item, "quantity", quantityValue);
// Optionally get selected / mandatory if needed
if (quantityIndex < sizeofarray(selectedStatuses)) {
jsonput(item, "selected", selectedStatuses[quantityIndex]);
}
if (quantityIndex < sizeofarray(mandatoryStatuses)) {
jsonput(item, "mandatory", mandatoryStatuses[quantityIndex]);
}
jsonarrayappend(spareItemsArray, item);
partIndex = partIndex + 1;
quantityIndex = quantityIndex + 1;
}
}
// ===============================
// Prepare Line Items for Pricing API
// ===============================
linesArray = jsonarray();
lineItemIndex = 1;
spareItemsRange = range(jsonarraysize(spareItemsArray));
for idx in spareItemsRange {
item = jsonarrayget(spareItemsArray, idx, "json");
selected = jsonget(item, "selected");
mandatory = jsonget(item, "mandatory");
part = jsonget(item, "part");
model = jsonget(item, "model");
quantity = jsonget(item, "quantity");
// Skip items without part or model
if (isnull(part) AND isnull(model)) {
continue;
}
// Only include if selected or mandatory
if (selected == "true" OR mandatory == "true") {
currentLineItem = json();
jsonput(currentLineItem, "_itemIdentifier", string(lineItemIndex));
if (NOT(isnull(part)) AND len(part) > 0) {
jsonput(currentLineItem, "_partNumber", part);
jsonput(currentLineItem, "_quantity", quantity);
jsonarrayappend(linesArray, currentLineItem);
}
// Models not supported by pricing engine as of today so not adding them as of now.
// elif (NOT(isnull(model)) AND len(model) > 0) {
// jsonput(currentLineItem, "_modelNumber", model);
// }
lineItemIndex = lineItemIndex + 1;
}
}
// --- Add BOM Items to Lines Array ---
// Check if bomItem string is not null and not empty
if (NOT(isnull(bomItem)) AND bomItem <> "") {
bomJson = json(bomItem); // Convert BOM XML string to JSON object
print("DEBUG: Initial bomJson from XML: " + jsontostr(bomJson));
// Get variable names and quantities for all BOM parts (flat list using JSONPath)
bomVariableNames = jsonpathgetmultiple(bomJson, "$..variableName");
bomQuantities = jsonpathgetmultiple(bomJson, "$..quantity");
// Check if bomVariableNames array is not null and has elements
if (NOT(isnull(bomVariableNames)) AND jsonarraysize(bomVariableNames) > 0) {
// Store range in a variable for clarity and consistency
numBomVariableNames = range(jsonarraysize(bomVariableNames));
for i in numBomVariableNames {
bomVarName = jsonarrayget(bomVariableNames, i, "string"); // Get variableName string, explicitly as string
bomQty = "1"; // Default quantity if not found or invalid
// Check if quantity exists for the current index and is not null
if (NOT(isnull(bomQuantities)) AND i < jsonarraysize(bomQuantities)) {
qtyRaw = jsonarrayget(bomQuantities, i, "string"); // Explicitly get as string
// Clean up quantity string by removing quotes and spaces
bomQty = replace(replace(qtyRaw, "\"", ""), " ", "");
}
// Further clean bomVarName if it comes with quotes/spaces
cleanedBomVarName = replace(replace(bomVarName, "\"", ""), " ", "");
currentLineItem = json();
// Populate line item details for the pricing API request using jsonput for top-level keys
jsonput(currentLineItem, "_itemIdentifier", string(lineItemIndex));
jsonput(currentLineItem, "_bomItemVariableName", cleanedBomVarName);
jsonput(currentLineItem, "_quantity", bomQty);
jsonarrayappend(linesArray, currentLineItem);
lineItemIndex = lineItemIndex + 1; // Increment for the next item
}
}
}
// ===============================
// Build & Call Pricing API
// ===============================
pricingHeaders = dict("string");
put(pricingHeaders, "Accept", "application/json");
put(pricingHeaders, "Content-Type", "application/json");
pricingPayload = json();
// Populate pricing payload using jsonput for top-level keys
if (isnull(currency) OR len(currency) < 1) { //if currency not present setting to default 'USD'
currency = "USD";
}
jsonput(pricingPayload, "_currencyCode", currency);
jsonput(pricingPayload, "priceBookVarName", "_default_price_book");
jsonput(pricingPayload, "returnAvailableRatePlans", true);
jsonput(pricingPayload, "enableWaterfall", true);
jsonput(pricingPayload, "lines", linesArray);
print("DEBUG: Pricing API Payload: " + jsontostr(pricingPayload));
// Make the API call to the pricing service
pricingApiResponse = urldatabypost(
"https://cpqqa210.cpq.us-phoenix-1.ocs.oc-test.com/rest/v17/pricing/actions/calculatePrice",
jsontostr(pricingPayload),
"", // Authentication string (empty as per original script)
pricingHeaders,
true // Indicates response should be treated as text (true for JSON response)
);
responseJson = json(pricingApiResponse); // Parse the JSON response
print("DEBUG: Pricing API Response: " + jsontostr(responseJson));
// ===============================
// Process Pricing Response and Update BOM JSON
// ===============================
priceLines = jsonget(responseJson, "lines", "jsonarray"); // Get all price lines from API response
// Check if priceLines array is not null and has elements
if (NOT(isnull(priceLines)) AND jsonarraysize(priceLines) > 0) {
// Store range in a variable for clarity and consistency
numPriceLines = range(jsonarraysize(priceLines));
for priceLineIndex in numPriceLines {
priceLineData = jsonarrayget(priceLines, priceLineIndex, "json"); // Get individual price line object
partNumber = jsonget(priceLineData, "_partNumber", "string"); // For spare parts, explicitly as string
bomItemVarName = jsonget(priceLineData, "_bomItemVariableName", "string"); // For BOM items, explicitly as string
chargesJsonArray = jsonget(priceLineData, "charges", "jsonarray"); // Array of charges for the item, explicitly as jsonarray
unitPrice = 0.0;
// Extract unit price from the first charge if available
if (NOT(isnull(chargesJsonArray)) AND jsonarraysize(chargesJsonArray) > 0) {
firstCharge = jsonarrayget(chargesJsonArray, 0, "json");
unitPriceStr = jsonget(firstCharge, "unitPrice", "string"); // Explicitly get as string
if (isnumber(unitPriceStr)) {
unitPrice = atof(unitPriceStr);
}
}
print("DEBUG: Processing priceLine for bomItemVarName: " + bomItemVarName + " | partNumber: " + partNumber);
// Handle Spare Parts pricing update
if (isnull(bomItemVarName) OR bomItemVarName == "") {
print("DEBUG: Attempting to update Part for sparesList for variableName: " + partNumber);
itemPayload = json();
jsonput(itemPayload, "quantity", "1"); // Assuming quantity of 1 for spare parts in this specific payload structure
jsonput(itemPayload, "price", string(unitPrice));
jsonput(itemPayload, "charges", chargesJsonArray);
jsonput(itemPayload, "catalogRefId", partNumber);
// Concatenate the JSON string of the current spare part to sparesList
if (sparesList == "") {
sparesList = jsontostr(itemPayload);
} else {
sparesList = sparesList + "," + jsontostr(itemPayload);
}
}
// Handle BOM Item pricing update
elif (NOT(isnull(bomItemVarName)) AND bomItemVarName <> "") {
// First, check if it's the main model (root of bomJson)
if (jsonget(bomJson, "variableName", "string") == bomItemVarName) {
print("DEBUG: Updating ROOT BOM item: " + bomItemVarName);
// Directly use jsonput on the root bomJson object to add/update fields
jsonput(bomJson, "price", string(unitPrice));
jsonput(bomJson, "charges", chargesJsonArray);
//replace "partNumber" with "catalogRefId" - OCC requirement
partNumberValue = jsonget(bomJson, "partNumber");
jsonput(bomJson, "catalogRefId", partNumberValue);
jsonremove(bomJson, "partNumber");
//Replace "On Request" or "Not Defined:" with '0.0'
fieldsJson = jsonget(bomJson, "fields", "json");
priceField = jsonget(fieldsJson, "_price_unit_price_each");
if (NOT(isnull(priceField)) AND NOT(isnumber(priceField))) {
jsonput(fieldsJson, "_price_unit_price_each", "0.0");
jsonput(bomJson, "fields", fieldsJson);
}
} else {
// It's a child BOM item – iterate through the 'children' array to find the correct index
childrenArray = jsonget(bomJson, "children", "jsonarray");
if (NOT(isnull(childrenArray)) AND jsonarraysize(childrenArray) > 0) {
childFound = false;
// Store range in a variable for clarity and consistency
numChildrenArray = range(jsonarraysize(childrenArray));
for childIndex in numChildrenArray {
// Get a copy of the child object to modify
childItemToModify = jsonarrayget(childrenArray, childIndex, "json");
// Check if this child's variableName matches the current bomItemVarName from pricing response
if (NOT(isnull(childItemToModify)) AND jsonget(childItemToModify, "variableName", "string") == bomItemVarName) {
print("DEBUG: Found child item at index " + string(childIndex) + " with matching variableName: " + bomItemVarName);
// Add/update price and charges using jsonput on the retrieved child object
jsonput(childItemToModify, "price", string(unitPrice));
jsonput(childItemToModify, "charges", chargesJsonArray);
//replace "partNumber" with "catalogRefId" - OCC requirement
partNumberValueChild = jsonget(childItemToModify, "partNumber");
jsonput(childItemToModify, "catalogRefId", partNumberValueChild);
jsonremove(childItemToModify, "partNumber");
//Replace "On Request" or "Not Defined:" with '0.0'
fieldsJsonChild = jsonget(childItemToModify, "fields", "json");
print("DEBUG: Fields Json " + jsontostr(fieldsJsonChild));
priceFieldChild = jsonget(fieldsJsonChild, "_price_unit_price_each");
if (NOT(isnull(priceFieldChild)) AND NOT(isnumber(priceFieldChild))) {
jsonput(fieldsJsonChild, "_price_unit_price_each", "0.0");
jsonput(childItemToModify, "fields", fieldsJsonChild);
}
// Replace the old child object in the bomJson with the newly modified one
jsonpathset(bomJson, "$.children[" + string(childIndex) + "]", childItemToModify);
print("DEBUG: Child item at index " + string(childIndex) + " updated and re-inserted into bomJson.");
childFound = true;
break; // Exit loop once child is found and updated
}
}
if (NOT(childFound)) {
print("DEBUG: Child BOM item " + bomItemVarName + " not found in the 'children' array.");
}
} else {
print("DEBUG: 'children' array not found or is empty in bomJson for child: " + bomItemVarName);
}
}
}
}
}
// ===============================
// Calculate Final Total Price
// ===============================
// Recalculate total price based on the processed pricing API response
if (NOT(isnull(priceLines)) AND jsonarraysize(priceLines) > 0) {
// Store range in a variable for clarity and consistency
numPriceLines = range(jsonarraysize(priceLines));
for priceLineIndex in numPriceLines {
priceLineItem = jsonarrayget(priceLines, priceLineIndex, "json");
charges = jsonget(priceLineItem, "charges", "jsonarray");
if (NOT(isnull(charges)) AND jsonarraysize(charges) > 0) {
// Store range in a variable for clarity and consistency
numCharges = range(jsonarraysize(charges));
for chargeIndex in numCharges {
eachCharge = jsonarrayget(charges, chargeIndex, "json");
extendedPriceStr = jsonget(eachCharge, "extendedAmount", "string"); // Explicitly get as string
priceType = jsonget(eachCharge, "priceType", "string"); // Explicitly get as string
if (isnumber(extendedPriceStr)) {
currentExtendedPrice = atof(extendedPriceStr);
if (priceType == "Recurring") {
recurringTotal = recurringTotal + currentExtendedPrice;
} else {
oneTimeTotal = oneTimeTotal + currentExtendedPrice;
}
}
}
}
}
}
baseModelPrice = oneTimeTotal; // Assuming base model price is the sum of one-time charges
priceTotal = oneTimeTotal + recurringTotal; // Total price is sum of one-time and recurring charges
bomJsonStr = jsontostr(bomJson); // Convert the final updated BOM JSON object to a string
print("DEBUG: Final bomJsonStr after all updates: " + bomJsonStr);
// ===============================
// Build Final Payload for Add To Cart (or similar)
// ===============================
// Populate the configDict with all the extracted and calculated data
put(configDict, "commerceItemId", ""); // Placeholder, often filled later in the commerce system
put(configDict, "model", model);
put(configDict, "ConfigId", configId);
put(configDict, "currency", currency);
put(configDict, "totalPrice", string(priceTotal));
put(configDict, "basePrice", string(baseModelPrice));
put(configDict, "ChildItems", sparesList); // This now contains the comma-separated JSON strings for spare parts
put(configDict, "BomItems", bomJsonStr); // This is your fully updated BOM JSON string
// Apply the data to the cart template
payload = applytemplate(CART_TEMPLATE_LOCATION, configDict);
// Clean up common escape characters that might be introduced during templating or JSON stringification
payload = replace(payload, """, "\""); // Replace HTML entity for double quote
payload = replace(payload, "\\\"", "\""); // Replace escaped double quotes
payload = replace(payload, "\n", ""); // Remove newline characters
payload = replace(payload, "\r", ""); // Remove carriage return characters
return payload; // Return the final prepared payload