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, "&quot;", "\""); // 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