Punchout integration often requires customization to accommodate different procurement systems and business requirements. Kodaris Commerce provides a powerful interceptor framework that allows you to customize various aspects of the punchout flow without modifying core code.
Interceptors are JavaScript functions that are executed at specific points in the punchout process. They allow you to:
Kodaris Commerce provides the following interceptor points for customizing the punchout process:
Interceptor Name | Execution Point | Available Variables | Purpose |
---|---|---|---|
punchOutAuthentication.js | During authentication of PunchOutSetupRequest | integrationCustomer, xmlRequest, header, contact, newIntegrationUserName | Customize user authentication and selection |
punchOutSetupRequestSuccessResponse.js | Before sending PunchOutSetupResponse | xmlRequest, xmlResponse | Modify PunchOutSetupResponse XML |
cartPunchOutOrderMessage.js | During generation of PunchOutOrderMessage | punchOutOrderMessage, punchOutOrderMessageCXML, punchOutSetupRequest, punchOutSetupRequestCXML, order | Customize cart data before transmission |
punchOutOrderMessageSuccessResponse.js | After generating PunchOutOrderMessage | xmlRequest, xmlResponse | Modify final PunchOutOrderMessage XML |
punchOutOrderRequestSuccessResponse.js | Before sending response to PunchOutOrderRequest | xmlRequest, xmlResponse | Modify PunchOutOrderRequest response XML |
punchoutMapAddressIdToERP.js | During order processing | erpAddressCode, punchoutAddressCode, isShipping, isBilling, customer, companyCode, punchoutSystem | Map punchout address IDs to ERP address codes |
One common customization is to dynamically authenticate users based on information in the PunchOutSetupRequest, such as a ShipTo address ID or other attributes.
// Script designed to run on PunchOutSetupRequest right before sending back PunchOutSetupResponse
// Vars in scope:
// - integrationCustomer - Object - Authenticated Customer
// - xmlRequest - String - Original incoming SetupRequest XML
// - header - Object - Header element from PunchOut request
// - contact - Object - Contact element from Request > PunchOutSetupRequest
// - newIntegrationUserName - String - Can be set to change the authenticated customer
// Check the parent company authentication to know when to switch accounts
if (integrationCustomer && integrationCustomer.userName === 'ParentCompany@example.com') {
// Extract ShipTo addressID from XML request
var shiptoAddressID;
var regex = /ShipTo>\s*/g;
var match;
while ((match = regex.exec(xmlRequest)) !== null) {
shiptoAddressID = match[1];
console.log('Found ShipTo > Address.addressID = ' + shiptoAddressID);
}
// Determine if we're handling a setup request or an order
var isPunchOutSetupRequest = xmlRequest.indexOf('
This example shows how to authenticate different users based on the ShipTo address ID in the PunchOutSetupRequest. This is particularly useful for organizations with multiple locations or departments that need to be treated as separate accounts.
Some procurement systems require specific formats or values in the PunchOutSetupResponse. You can customize the response XML to meet these requirements.
// Script designed to run on PunchOutSetupRequest right before sending back PunchOutSetupResponse
// Vars in scope:
// - xmlRequest - PunchOutSetupRequest XML string
// - xmlResponse - PunchOutSetupResponse XML string
// For Ariba PunchOut provider requests
if (xmlRequest.match("ariba\\.com")) {
// Remove standalone attribute from xml header
xmlResponse = xmlResponse.replace(" standalone=\"yes\"", "");
// Change success text status code from OK to Accepted (Ariba preference)
xmlResponse = xmlResponse.replace("text=\"OK\"", "text=\"Accepted\"");
// Get custom redirect URL from company settings
var redirectUrl = "";
try {
if (integrationCustomer.companyID) {
var result = scriptServiceUtils.runAPIMethod(
'GET',
'/api/system/company/{companyID}/setting/{code}',
{"companyID": integrationCustomer.companyID, "code": "punchoutRedirectUrl"},
{},
{});
if (result && result.value) {
redirectUrl = result.value;
}
}
} catch(ex) {
console.log("Exception finding redirectUrl: " + ex);
}
// Modify the URL in the response to include custom redirect
var newUrl = "example.com/api/user/customer/access/login/PUNCHOUT?";
if (redirectUrl) {
newUrl = newUrl + "redirectUri=" + redirectUrl + "&";
}
xmlResponse = xmlResponse.replace("shop.example.com/api/user/customer/access/login/PUNCHOUT?", newUrl);
}
Ariba Specific Requirements: Ariba often has specific requirements for XML responses, such as:
When users check out and return to the procurement system, you can customize the PunchOutOrderMessage XML that contains the cart data.
// Script designed to run at the end of OrderMessage (Cart) cXML generation
// Vars in scope:
// - punchOutOrderMessage - PunchOutOrderMessage object
// - punchOutOrderMessageCXML - Full CXML for PunchOutOrderMessage
// - punchOutSetupRequest - PunchOutSetupRequest object that originated the session
// - punchOutSetupRequestCXML - Full CXML for PunchOutSetupRequest
// - order - Order object (Cart)
// Example: Map handling fee to shipping cost for a specific provider
if (punchOutSetupRequestCXML.payloadID.indexOf("specificProvider") !== -1) {
for (var i = 0; i < order.orderDiscounts.length; i++) {
if ("handling fee" === order.orderDiscounts[i].name.toLowerCase()) {
var handlingFeeAmount = order.orderDiscounts[i].reward;
console.log("Handling Fee found, applying as Shipping, amount = " + handlingFeeAmount);
punchOutOrderMessage.punchOutOrderMessageHeader.shipping.money.value = handlingFeeAmount;
break;
}
}
}
// Example: Customize classification for products
if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
// Get the product code
var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
// Find additional product information
var pimProduct = findProductByCode(productCode);
if (pimProduct) {
// Set UNSPSC classification if available
if (punchOutOrderMessage.itemIn[i].itemDetail.classification &&
punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
if (pimProduct.unspscCode) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = pimProduct.unspscCode;
}
}
// Add additional product attributes as Extrinsic elements
if (pimProduct.attributes) {
for (var attr in pimProduct.attributes) {
var extrinsic = new Extrinsic();
extrinsic.name = attr;
extrinsic.value = pimProduct.attributes[attr];
punchOutOrderMessage.itemIn[i].itemDetail.extrinsic.add(extrinsic);
}
}
}
}
}
// Helper function to find product information
function findProductByCode(code) {
var params = {
"filterFields": [
{
"name": "code",
"operation": "IS",
"value": code
}
],
page: 0,
size: 1
};
var result = scriptServiceUtils.runAPIMethod('POST', '/api/system/product/search', null, null, params);
if (result && result.content && result.content.length > 0) {
return result.content[0];
}
return null;
}
Different procurement systems often have specific requirements for cart data. Here are examples of customizations for common providers:
// Coupa-specific customizations
if (punchOutSetupRequestCXML.payloadID.indexOf("coupa") !== -1) {
for (var i=0; i < punchOutOrderMessage.itemIn.length; i++) {
// Normalize unit of measure to EA (Each) if it's "each"
if ("each" === punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure.toLowerCase()) {
punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure = "EA";
}
// Add UNSPSC classification
var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
var searchParams = {
"filterFields": [
{
"name": "code",
"operation": "IS",
"value": productCode
}
]
};
try {
var result = scriptServiceUtils.runAPIMethod('POST', '/api/system/product/search', null, null, searchParams);
if(result && result.totalElements >= 1 && result.content[0].unspscCode) {
var unspscCode = result.content[0].unspscCode;
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = "UNSPSC";
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = unspscCode;
}
} catch(err) {
console.log("Exception calling Product Search: " + err);
}
}
}
// Ariba-specific customizations
if (punchOutSetupRequestCXML.payloadID.indexOf("ariba\\.com") !== -1) {
if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
// Add UNSPSC classification
if (punchOutOrderMessage.itemIn[i].itemDetail.classification &&
punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
// Find product to get UNSPSC code
var pimProduct = findProductByCode(
punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value
);
if (pimProduct && pimProduct.vendorProductGroupCode) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value =
pimProduct.vendorProductGroupCode;
}
}
}
}
}
When processing orders from punchout systems, you often need to map the address IDs from the procurement system to internal ERP address codes.
/*
Script designed to run during punchout OrderRequest to allow the system to map an addressID
coming from a punchout system to an ERP addressID stored in Kodaris.
Vars in scope:
- erpAddressCode - Should be set with the ERP address code to use
- punchoutAddressCode - Contains the punchout address ID from the procurement system
- isShipping - Boolean true if this is a shipping address
- isBilling - Boolean true if this is a billing address
- customer - The customer object for accessing customer data and settings
- companyCode - The company code of the current punchout customer
- punchoutSystem - The external punchout system making the call
*/
if (punchoutAddressCode && companyCode) {
// Look up address mapping using company address search
var searchParams = {
"filterFields": [
{
"name": "companyCode",
"operation": "IS",
"value": companyCode
},
{
"name": "user2", // Field where punchout address IDs are stored
"operation": "IS",
"value": punchoutAddressCode
}
]
};
var result = scriptServiceUtils.runAPIMethod(
'POST',
'/api/system/company/address/search',
null,
null,
searchParams
);
// If found, use the ERP address code from the extra2 field
if (result.totalElements >= 1 && result.content[0].extra2) {
erpAddressCode = result.content[0].extra2;
} else {
// Allow to continue, system will try to find it using the original code
erpAddressCode = null;
}
}
This script allows you to map address IDs from procurement systems to internal ERP address codes, ensuring that orders are processed with the correct shipping and billing information.
XSLT (Extensible Stylesheet Language Transformations) is a powerful technology for transforming XML documents into other formats or restructured XML. In the context of punchout, XSLT can be used to:
The Kodaris punchout implementation supports XSLT transformations specifically for order requests:
punchOutOrderRequestXSLT
company settingNote: XSLT transformations are particularly useful when interceptors alone are not sufficient for complex XML restructuring, or when the transformation logic needs to be externalized from code.
Order request transformations allow you to modify incoming PunchOutOrderRequest XML before it's processed by the system.
Order request transformations can be configured using the punchOutOrderRequestXSLT
company-specific setting.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<!-- Copy all -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<!-- End -->
<!-- Replace ShipTo addressID by Extrinsic/SpanNumber-->
<xsl:variable name="shipToAddressCode">
<xsl:for-each select="cXML/Request/OrderRequest/OrderRequestHeader/Extrinsic">
<xsl:if test="@name='SpanNumber'">
<xsl:value-of select="." />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:template match="ShipTo/Address/@addressID">
<xsl:attribute name="addressID">
<xsl:value-of select="$shipToAddressCode" />
</xsl:attribute>
</xsl:template>
<!-- End -->
</xsl:transform>
Explanation: This real-world example demonstrates a practical XSLT transformation that:
shipToAddressCode
that extracts the value from an Extrinsic element with name="SpanNumber"addressID
attribute in ShipTo/Address
with this extracted valueThis transformation is useful when the procurement system sends the actual shipping address code in an Extrinsic field rather than in the standard addressID attribute, allowing the system to map addresses correctly.
Ariba is one of the most common punchout providers and often requires specific customizations.
// punchOutSetupRequestSuccessResponse.js for Ariba
if (xmlRequest.match("ariba\\.com")) {
// Remove standalone attribute from xml header
xmlResponse = xmlResponse.replace(" standalone=\"yes\"", "");
// Change success text status code from OK to Accepted
xmlResponse = xmlResponse.replace("text=\"OK\"", "text=\"Accepted\"");
// Other Ariba-specific customizations...
}
// cartPunchOutOrderMessage.js for Ariba
if (punchOutSetupRequestCXML.payloadID.indexOf("ariba\\.com") !== -1) {
if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
// Customize UNSPSC classification
if (punchOutOrderMessage.itemIn[i].itemDetail.classification &&
punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
// Get product info to set classification
var pimProduct = findProductByCode(
punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value
);
if (pimProduct && pimProduct.vendorProductGroupCode) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value =
pimProduct.vendorProductGroupCode;
}
}
}
}
}
Coupa is another popular punchout provider with its own set of requirements.
// cartPunchOutOrderMessage.js for Coupa
if (punchOutSetupRequestCXML.payloadID.indexOf("coupa") !== -1) {
for (var i=0; i < punchOutOrderMessage.itemIn.length; i++) {
// Normalize unit of measure
if ("each" === punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure.toLowerCase()) {
punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure = "EA";
}
// Add UNSPSC classification
var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
try {
var result = findProductByCode(productCode);
if(result && result.unspscCode) {
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = "UNSPSC";
punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = result.unspscCode;
}
} catch(err) {
console.log("Exception processing product: " + err);
}
}
}
Best Practices for Customization