punchout-catalogs / punchout-catalog-spryker
Punchout Catalog Module for Spryker eCommerce Platform
Installs: 29 651
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 4
Forks: 1
Open Issues: 0
pkg:composer/punchout-catalogs/punchout-catalog-spryker
Requires
- php: >=7.1
- ext-json: *
- ext-mbstring: *
- ext-simplexml: *
- spryker/cart: ^5.0.0 || ^7.0.0
- spryker/company-business-unit: ^2.9.0
- spryker/company-user: ^2.7.0
- spryker/customer: ^7.0.0
- spryker/data-import: ^1.6.0
- spryker/glossary: ^3.0.0
- spryker/glossary-storage: ^1.0.0
- spryker/kernel: ^3.33.0
- spryker/money: ^2.6.0
- spryker/oauth-company-user: ^2.0.0
- spryker/product-storage: ^1.11.0
- spryker/quote: ^2.0.0
- spryker/store: ^1.1.0
- spryker/util-uuid-generator: ^1.0.0
- spryker/vault: ^1.1.0
- spryker/zed-request: ^3.6.0
Requires (Dev)
Suggests
- spryker/product-bundle: Use at least ^6.0.0 to have ProductBundleCartItemTransformerPlugin
- 2.4.2
- 2.4.1
- 2.3.0
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.1
- 2.0.0
- 1.1.2
- 1.1.1
- 1.1.0
- dev-master / 1.0.x-dev
- 1.0.1
- 1.0.0
- dev-master-2021-dec-07
- dev-bugfix/support-release-202001
- dev-master-2021-nov-25
- dev-feature/POC-2628-price-per-quantity
- dev-feature/ps-8578/dev-punchout-catalog-cart-bc
- dev-fixes/db-structure-fix
- dev-feature/ps-8578/dev-punchout-catalog-architecture-overview
- dev-feature/ps-8578/dev-punchout-catalog
This package is auto-updated.
Last update: 2025-10-11 03:32:54 UTC
README
Punchout Catalog Module for Spryker eCommerce Platform
Important Changes
Since the 2.4.0 version it has been changed the way of punchout connection load.
#1. Business Unit become optional in Gateway URL (Entry Point)
#2. It is required to enter Buyer ID for cXML Setup Request in the following format:
Credential_Domain_Value/Credential_Identity_Value.
Example #1:
AribaNetworkId/AN119990XX
Example #2:
NetworkId/NID119990XX
Installation
composer require punchout-catalogs/punchout-catalog-spryker
B2C store additionally requires the spryker-feature/company-account feature.
Look at etc/integration-sample/*.patch patches as example of integration with Spryker B2B  and B2C demo stores.
Documentation
Testing
Running:
./vendor/bin/codecept run
Custom cart mapping
Extending cart mapping behavior could be implemented by overriding PunchoutCatalog\Yves\PunchoutCatalog\PunchoutCatalogConfig class method:
<?php namespace Pyz\Yves\PunchoutCatalog; use PunchoutCatalog\Yves\PunchoutCatalog\PunchoutCatalogConfig as BasePunchoutCatalogConfig; class PunchoutCatalogConfig extends BasePunchoutCatalogConfig { /** * @return array */ public function getCustomCartMapping(): array { return [ // QuoteTransfer => PunchoutCatalogDocumentCartTransfer // without key, should return transfer object function ($quoteTransfer, $cartRequestTransfer, $plugin) { $cartRequestTransfer->setCoupon('Coupon for ' . $quoteTransfer->getName()); return $cartRequestTransfer; }, 'cart_note' => 'name', ]; } /** * @return array */ public function getCustomCartItemMapping(): array { return [ //ItemTransfer => PunchoutCatalogDocumentCartItemTransfer 'custom_sku' => function($quoteItemTransfer, $documentCartItemTransfer, $quoteTransfer, $plugin) { return 'here-is-custom-sku-' . $quoteItemTransfer->getAbstractSku(); }, 'sale_bunch_quantity' => function($quoteItemTransfer, $documentCartItemTransfer, $quoteTransfer, $plugin) { //Product #1 if ($quoteItemTransfer->getAbstractSku() === 'any_condition_1') { return 100; } //Product #2 if ($quoteItemTransfer->getAbstractSku() === 'any_condition_2') { return 50; } return 1; }, 'custom_fields' => function($quoteItemTransfer, $documentCartItemTransfer, $quoteTransfer, $plugin) { return array( 'custom_field_1' => 'quote-item-id=' . $quoteItemTransfer->getId(), 'custom_field_2' => 'custom-abstract-sku-' . $quoteItemTransfer->getAbstractSku(), 'custom_field_3' => 'custom_field_value_3', 'custom_field_4' => 'custom_field_value_4_' . uniqid(), 'custom_field_5' => 'custom_field_value_5_' . uniqid(), 'custom_field_contract' => 'ContractID-'. uniqid(), 'custom_field_org' => 'TestPurchOrg', 'custom_field_ref' => 'some-ref', //...add as many custom fields as you need and can use in mapping ); }, /** * @param \Generated\Shared\Transfer\ItemTransfer * @param \Generated\Shared\Transfer\PunchoutCatalogDocumentCartItemTransfer * @param \Generated\Shared\Transfer\QuoteTransfer * @param \PunchoutCatalog\Yves\PunchoutCatalog\Mapper\CartTransferMapperDefaultPlugin */ function ($quoteItemTransfer, $documentCartItemTransfer, $quoteTransfer, $plugin) { $name = trim($quoteItemTransfer->getName()); $documentCartItemTransfer->setDiscountDescription('Custom discount description for ' . $name); return $documentCartItemTransfer; }, 'discount_description' => 'name', 'cart_note' => 'group_key', ]; } /** * @return array */ public function getCustomCartCustomerMapping(): array { return [ //CustomerTransfer => PunchoutCatalogDocumentCartCustomerTransfer 'first_name' => 'customer_reference', /** * @param \Generated\Shared\Transfer\CustomerTransfer * @param \Generated\Shared\Transfer\PunchoutCatalogDocumentCustomerTransfer * @param \Generated\Shared\Transfer\QuoteTransfer * @param \PunchoutCatalog\Yves\PunchoutCatalog\Mapper\CartTransferMapperDefaultPlugin */ function ($quoteCustomerTransfer, $documentCartCustomerTransfer, $quoteTransfer, $plugin) { return $documentCartCustomerTransfer; }, ]; } }
If this opportunity is not enough, you could define your own plugin that should implement PunchoutCatalog\Yves\PunchoutCatalog\Mapper\CartTransferMapperPluginInterface
and add it by overriding PunchoutCatalog\Yves\PunchoutCatalog\PunchoutCatalogDependencyProvider::getCartTransferMapperPlugins method.
Enable Yves Controllers in a new way (since Spryker version 202001)
Enable Yves Punchout routes in src/Pyz/Yves/Router/RouterDependencyProvider.php:
<?php namespace Pyz\Yves\Router; use PunchoutCatalog\Yves\PunchoutCatalog\Plugin\Router\PunchoutCatalogRouteProviderPlugin; use Spryker\Yves\Router\RouterDependencyProvider as SprykerRouterDependencyProvider; class RouterDependencyProvider extends SprykerRouterDependencyProvider { /** * @return \Spryker\Yves\RouterExtension\Dependency\Plugin\RouteProviderPluginInterface[] */ protected function getRouteProvider(): array { return [ new PunchoutCatalogRouteProviderPlugin() ]; } }
Enable Yves Controllers in a legacy way (before Spryker version 202001)
Register Punchout routes in src/Pyz/Yves/ShopApplication/YvesBootstrap.php:
<?php namespace Pyz\Yves\ShopApplication; use PunchoutCatalog\Yves\PunchoutCatalog\Plugin\Provider\PunchoutCatalogControllerProvider; use SprykerShop\Yves\ShopApplication\YvesBootstrap as SprykerYvesBootstrap; class YvesBootstrap extends SprykerYvesBootstrap { /** * @param bool|null $isSsl * * @return \SprykerShop\Yves\ShopApplication\Plugin\Provider\AbstractYvesControllerProvider[] */ protected function getControllerProviderStack($isSsl) { return [ new PunchoutCatalogControllerProvider($isSsl), ]; } }
Enable Zed Controllers in a new way (since Spryker version 202001)
Enable Zed Punchout routes in src/Pyz/Zed/Router/RouterConfig.php:
<?php namespace Pyz\Zed\Router; use Spryker\Zed\Router\RouterConfig as SprykerRouterConfig; class RouterConfig extends SprykerRouterConfig { /** * @return string[] */ public function getControllerDirectories(): array { $controllerDirectories = parent::getControllerDirectories(); //... $controllerDirectories[] = sprintf('%s/punchout-catalogs/*/src/*/Zed/*/Communication/Controller/', APPLICATION_VENDOR_DIR); return array_filter($controllerDirectories, 'glob'); } }
Troubleshooting
Issue with Auth Token Create Error
The punchout-catalog.error.auth.token.create error may happen if the spy_oauth_access_token.user_identifier field is too small for data which is storing in the field. By default it is varchar(1024).
Solution:
The easiest way to improve it is upgrading the field from varchar(1024) to LONGVARCHAR.
Create a scheme file #1: src/Pyz/Zed/PunchoutCatalog/Persistence/Propel/Schema/spy_oauth.schema.xml:
<?xml version="1.0"?> <database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed" xsi:noNamespaceSchemaLocation="http://static.spryker.com/schema-01.xsd" namespace="Orm\Zed\Oauth\Persistence" package="src.Orm.Zed.Oauth.Persistence"> <table name="spy_oauth_access_token"> <column name="user_identifier" type="LONGVARCHAR"/> </table> </database>
Create a scheme file #2: src/Pyz/Zed/PunchoutCatalog/Persistence/Propel/Schema/spy_oauth_revoke.schema.xml:
<?xml version="1.0"?> <database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed" xsi:noNamespaceSchemaLocation="http://static.spryker.com/schema-01.xsd" namespace="Orm\Zed\OauthRevoke\Persistence" package="src.Orm.Zed.OauthRevoke.Persistence"> <table name="spy_oauth_refresh_token"> <column name="user_identifier" type="LONGVARCHAR"/> </table> </database>
DB upgrade:
vendor/bin/console propel:install
Issue with disappeared PunchOut menu item in admin panel ( related to spryker-eco/punchout-catalogs )
Possible Reason:
Using BREADCRUMB_MERGE_STRATEGY hides all custom menu items which are not defined in the config/Zed/navigation.xml file.
See: https://docs.spryker.com/docs/scos/dev/back-end-development/extending-spryker/adding-navigation-in-the-back-office.html#defining-a-navigation-merge-strategy
Strategy defined in the src/Pyz/Zed/ZedNavigation/ZedNavigationConfig.php file.
Solution:
Restore menu items for BREADCRUMB_MERGE_STRATEGY easily by adding the following code to the config/Zed/navigation.xml file:
    <punchout-catalogs>
        <label>PunchOut</label>
        <title>PunchOut</title>
        <pages>
            <connection>
                <label>Connections</label>
                <title>Connections</title>
                <bundle>punchout-catalogs</bundle>
                <controller>index</controller>
                <action>index</action>
                <visible>1</visible>
            </connection>
            <transaction-log>
                <label>Transactions Log</label>
                <title>Transactions Log</title>
                <bundle>punchout-catalogs</bundle>
                <controller>transaction</controller>
                <action>index</action>
                <visible>1</visible>
            </transaction-log>
        </pages>
    </punchout-catalogs>
And run the application:build-navigation-cache command if navigation menu is cached (store runs in production mode).
Example of OCI Cart mapping with many custom fields:
{
    "cart_item": {
        "fields": {
            "quantity": {
                "path": "NEW_ITEM-QUANTITY[%line_number%]"
            },
            "internal_id": {
                "path": "NEW_ITEM-EXT_PRODUCT_ID[%line_number%]"
            },
            "parent_line_number": {
                "path": "NEW_ITEM-PARENT_ID[%line_number%]"
            },
            "item_type": {
                "path": "NEW_ITEM-ITEM_TYPE[%line_number%]",
                "transform":
                [
                    {
                        "map": {
                            "value": "composite",
                            "result": "R"
                        }
                    },
                    {
                        "map": {
                            "value": "item",
                            "result": "O"
                        }
                    }
                ]
            },
            "sku": {
                "path": "NEW_ITEM-VENDORMAT[%line_number%],NEW_ITEM-MANUFACTMAT[%line_number%]"
            },
            "currency": {
                "path": "NEW_ITEM-CURRENCY[%line_number%]"
            },
            "unit_total": {
                "path": "NEW_ITEM-PRICE[%line_number%]"
            },
            "name": {
                "path": "NEW_ITEM-DESCRIPTION[%line_number%]",
                "transform": [{
                    "cut": {
                        "len": "40"
                    }
                }]
            },
            "long_description": {
                "path": "NEW_ITEM-LONGTEXT_%line_number%:132[]"
            },
            "uom": {
                "path": "NEW_ITEM-UNIT[%line_number%]",
                "transform": [{
                    "default": {
                        "value": "EA"
                    }
                }]
            },
            "unspsc": {
                "path": "NEW_ITEM-MATGROUP[%line_number%]"
            },
            "supplier_id": {
                "path": "NEW_ITEM-VENDOR[%line_number%]"
            },
            "sale_bunch_quantity": {
                "path": "NEW_ITEM-PRICEUNIT[%line_number%]"
            },
            "custom_fields/custom_field_org": {
                "path": "NEW_ITEM-PURCHORG[%line_number%]"
            },
            "custom_fields/custom_field_ref": {
                "path": "NEW_ITEM-PURCHINFREC[%line_number%]",
                "transform": [
                    "uppercase"
                ]
            },
            "custom_fields/custom_field_contract": {
                "path": "NEW_ITEM-CONTRACT[%line_number%]",
                "transform": [
                    "lowercase"
                ]
            },
            "custom_fields/custom_field_1": {
                "path": "NEW_ITEM-CUSTFIELD1[%line_number%]"
            },
            "custom_fields/custom_field_2": {
                "path": "NEW_ITEM-CUSTFIELD2[%line_number%]"
            },
            "custom_fields/custom_field_3": {
                "path": "NEW_ITEM-CUSTFIELD3[%line_number%]"
            },
            "custom_fields/custom_field_4": {
                "path": "NEW_ITEM-CUSTFIELD4[%line_number%]"
            },
            "custom_fields/custom_field_5": {
                "path": "NEW_ITEM-CUSTFIELD5[%line_number%]"
            }
        }
    }
}
Example of OCI Cart mapping with integrated sale_bunch_quantity (and with NEW_ITEM-PRICEUNIT):
{
    "cart_item": {
        "fields": {
            ...
            "quantity": {
                "path": "NEW_ITEM-QUANTITY[%line_number%]"
            },
            "sale_bunch_quantity": {
                "path": "NEW_ITEM-PRICEUNIT[%line_number%]",
                "transform": [
                    "round"
                ]
            },
            "unit_total": {
                "path": "NEW_ITEM-PRICE[%line_number%]"
            }
        }
    }
}
Example of OCI Cart mapping with sale_quantity (without NEW_ITEM-PRICEUNIT and divided prices):
sale_quantity = quantity / sales_bunch_quantity
{
    "cart_item": {
        "fields": {
            ...
            "sale_quantity": {
                "path": "NEW_ITEM-QUANTITY[%line_number%]"
            },
            "sale_bunch_quantity_unit_total": {
                "path": "NEW_ITEM-PRICE[%line_number%]",
                "transform": [
                    {"round":  {"precision":  "3"}}
                ]
            }
        }
    }
}
Example of cXML Cart mapping:
{
  "cart": {
    "fields": {
      "grand_total": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Total[1]/Money[1]"
      },
      "tax_total": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Tax[1]/Money[1]"
      },
      "tax_description": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Tax[1]/Description[1]"
      },
      "discount_total": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Discount[1]/Money[1]"
      },
      "discount_description": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Discount[1]/Description[1]"
      },
      "currency": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Total[1]/Money[1]/@currency,/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Tax[1]/Money[1]/@currency,/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Discount[1]/Money[1]/@currency",
        "append": true
      },
                  "cart_note": {
        "path": "/cXML/Message[1]/PunchOutOrderMessage[1]/PunchOutOrderMessageHeader[1]/Comments[1]"
      }
    }
  },
  "cart_item": {
    "fields": {
      "line_number": {
        "path": "@lineNumber"
      },
                  "parent_line_number": {
        "path": "@parentLineNumber"
      },
                  "item_type": {
        "path": "@itemType"
      },
                  "composite_item_type": {
        "path": "@compositeItemType"
      },
      "quantity": {
        "path": "@quantity"
      },
      "internal_id": {
        "path": "ItemID[1]/SupplierPartAuxiliaryID[1]"
      },
      "sku": {
        "path": "ItemID[1]/SupplierPartID[1],ItemDetail[1]/BuyerPartID[1],ItemDetail[1]/ManufacturerPartID[1]"
      },
      "unit_total": {
        "path": "ItemDetail[1]/UnitPrice[1]/Money[1]"
      },
      "currency": {
        "path": "ItemDetail[1]/UnitPrice[1]/Money[1]/@currency"
      },
      "name": {
        "path": "ItemDetail[1]/Description[1]/ShortName"
      },
      "long_description": {
        "path": "ItemDetail[1]/Description[1]"
      },
      "uom": {
        "path": "ItemDetail[1]/UnitOfMeasure[1]",
        "transform": [{
          "default": {
            "value": "EA"
          }
        }]
      },
      "brand": {
        "path": "ItemDetail[1]/ManufacturerName[1]"
      },
      "supplier_id": {
        "path": "ItemDetail[1]/SupplierID[1]"
      },
      "cart_note": {
        "path": "ItemDetail[1]/Comments[1]"
      },
      "image_url": {
        "path": "ItemDetail[1]/Extrinsic[@name='ImageURL']"
      },
      "locale": {
        "path": "ItemDetail[1]/Description[1]/@xml:lang"
      },
      "options": {
        "path": "ItemDetail[1]/Extrinsic/customOption()",
        "multiple": true
      }
    }
  },
  "customOption": {
    "fields": {
      "code": {
        "path": "@name"
      },
      "value": {
        "path": "./"
      }
    }
  }
}
Example of cXML Request mapping:
{
  "customer": {
    "fields": {
      "first_name": {
        "path": "/cXML/Request[1]/PunchOutSetupRequest[1]/Extrinsic[@name='FirstName']"
      },
      "last_name": {
        "path": "/cXML/Request[1]/PunchOutSetupRequest[1]/Extrinsic[@name='LastName']"
      },
      "email": {
        "path": "/cXML/Request[1]/PunchOutSetupRequest[1]/Extrinsic[@name='UserEmail']"
      }
    }
  },
  "cart_item": {
    "fields": {
      "internal_id":{
        "path": "/cXML/Request[1]/PunchOutSetupRequest[1]/ItemOut/ItemID[1]/SupplierPartAuxiliaryID"
      }
    }
  }
}