npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vendure-community/punchout-gateway-plugin

v0.1.0

Published

A Vendure plugin for integrating with [PunchCommerce](https://www.punchcommerce.de), a PunchOut gateway that connects your Vendure store with enterprise procurement systems (SAP Ariba, Coupa, etc.) via OCI/cXML protocols.

Readme

PunchOut Gateway Plugin

A Vendure plugin for integrating with PunchCommerce, a PunchOut gateway that connects your Vendure store with enterprise procurement systems (SAP Ariba, Coupa, etc.) via OCI/cXML protocols.

PunchCommerce handles all protocol translation — this plugin only speaks JSON over HTTPS.

How It Works

  1. Buyer clicks PunchOut link in their ERP → PunchCommerce redirects to your storefront with sID and uID query params
  2. Storefront authenticates the buyer by calling Vendure's authenticate mutation with the punchout strategy
  3. Buyer shops normally — all order mutations use activeOrderInput to scope the cart to the PunchOut session
  4. On checkout, storefront calls transferPunchOutCart(sID) to send the cart back to PunchCommerce

Installation

npm install @vendure-community/punchout-gateway-plugin

Configuration

import { PunchOutGatewayPlugin } from '@vendure-community/punchout-gateway-plugin';

export const config: VendureConfig = {
    plugins: [
        PunchOutGatewayPlugin.init({
            // All options are optional — defaults work out of the box
        }),
    ],
};

Options

| Option | Required | Default | Description | | --- | --- | --- | --- | | apiUrl | No | https://www.punchcommerce.de | Base URL of the PunchCommerce gateway. Override for staging or self-hosted instances. | | shippingCostMode | No | 'nonZero' | Controls shipping line item in the basket: 'all' = always include, 'nonZero' = only when > 0, 'none' = never include. | | productFieldMapping | No | — | Maps PunchCommerce product fields to static values or ProductVariant custom field names. See below. |

Product Field Mapping

By default, all products are sent as pieces (unit: 'PCE'). If your catalog includes products with different units (weight, volume, etc.), you can map PunchCommerce fields to ProductVariant custom fields or static values.

Each field accepts either a static value or a { customField, default } object that reads from the variant at transfer time:

PunchOutGatewayPlugin.init({
    productFieldMapping: {
        // Static value for all products
        packaging_unit: 'Bag',

        // Read from ProductVariant custom field, fall back to 'PCE' if empty
        unit: { customField: 'punchOutUnit', default: 'PCE' },
        unit_name: { customField: 'punchOutUnitName', default: 'Piece' },
        weight: { customField: 'productWeight', default: 0 },
    },
})

Available fields:

| Field | Default | Description | | --- | --- | --- | | unit | 'PCE' | OCI unit code (e.g. 'PCE', 'KG', 'LTR') | | unit_name | 'Piece' | Human-readable unit name | | packaging_unit | 'Piece' | Packaging unit description | | purchase_unit | 1 | Purchase unit quantity | | reference_unit | 1 | Reference unit quantity | | weight | 0 | Product weight |

Customer Setup

Customers are linked to PunchCommerce via a custom field on the Customer entity.

  1. In PunchCommerce: create a customer and set the "Customer identification" (this becomes the uID)
  2. In Vendure admin: open the customer, set the "PunchOut Customer ID (uID)" custom field to the same value

PunchCommerce Configuration

In the PunchCommerce dashboard, configure your customer:

  • Entry address: your storefront's PunchOut landing page URL (e.g. https://my-store.com/punchout)
  • Customer identification: a unique identifier matching the Vendure customer's custom field

PunchCommerce will redirect buyers to your Entry address with ?sID={UUID}&uID={identifier} appended.

Storefront Requirements

Since Vendure is headless, your storefront must handle the PunchOut flow. A full working example is available at vendurehq/punchcommerce-storefront-demo.

Here's what needs to be implemented:

1. PunchOut Landing Page

Create a route (e.g. /punchout) that PunchCommerce redirects to. This page must:

  1. Extract sID and uID from the query params
  2. Store the sID for the duration of the session (e.g. in sessionStorage)
  3. Call the authenticate mutation
  4. Redirect to the shop homepage on success
// e.g. https://my-store.com/punchout?sID=abc-123&uID=test-customer
const params = new URLSearchParams(window.location.search);
const sID = params.get('sID');
const uID = params.get('uID');

// Store sID for the session — needed for all order operations
sessionStorage.setItem('punchoutSID', sID);

const { authenticate } = await graphqlClient.mutate({
    mutation: gql`
        mutation PunchOutLogin($sID: String!, $uID: String!) {
            authenticate(input: { punchout: { sID: $sID, uID: $uID } }) {
                ... on CurrentUser { id }
                ... on InvalidCredentialsError { message }
            }
        }
    `,
    variables: { sID, uID },
});

2. Session-Scoped Cart (activeOrderInput)

All order operations (queries and mutations) must include activeOrderInput: { punchout: { sID } } to scope the cart to the PunchOut session. This enables parallel sessions for the same customer.

const sID = sessionStorage.getItem('punchoutSID');

await graphqlClient.mutate({
    mutation: gql`
        mutation AddItem($variantId: ID!, $qty: Int!, $activeOrderInput: ActiveOrderInput) {
            addItemToOrder(
                productVariantId: $variantId
                quantity: $qty
                activeOrderInput: $activeOrderInput
            ) {
                ... on Order { id totalWithTax }
                ... on ErrorResult { message }
            }
        }
    `,
    variables: {
        variantId: '42',
        qty: 1,
        activeOrderInput: { punchout: { sID } },
    },
});

Pass activeOrderInput on all order operations: activeOrder, addItemToOrder, adjustOrderLine, removeOrderLine, setOrderShippingAddress, setOrderShippingMethod, eligibleShippingMethods, etc.

To display the cart, query activeOrder with the same input:

const sID = sessionStorage.getItem('punchoutSID');

const { activeOrder } = await graphqlClient.query({
    query: gql`
        query PunchOutCart($activeOrderInput: ActiveOrderInput) {
            activeOrder(activeOrderInput: $activeOrderInput) {
                id
                totalWithTax
                totalQuantity
                lines {
                    id
                    quantity
                    unitPriceWithTax
                    linePriceWithTax
                    productVariant { name sku }
                }
            }
        }
    `,
    variables: {
        activeOrderInput: { punchout: { sID } },
    },
});

3. Transfer Cart (replaces Checkout)

Replace the normal checkout flow with a "Transfer Cart" / "Back to Procurement" button that sends the cart to PunchCommerce:

const { transferPunchOutCart } = await graphqlClient.mutate({
    mutation: gql`
        mutation TransferCart($sID: String!) {
            transferPunchOutCart(sID: $sID) { success message }
        }
    `,
    variables: { sID: sessionStorage.getItem('punchoutSID') },
});

if (transferPunchOutCart.success) {
    // Cart transferred — show confirmation to the buyer
}

4. iFrame Support (if applicable)

If PunchCommerce is configured for iFrame PunchOut (embedding the shop inside the ERP), your storefront must:

  • Set SameSite=None; Secure on all session cookies
  • Remove the X-Frame-Options header during PunchOut sessions
  • These are typically configured in your web server or storefront framework

GraphQL API Reference

Authentication (built-in mutation)

mutation {
    authenticate(input: { punchout: { sID: "...", uID: "..." } }) {
        ... on CurrentUser { id }
        ... on InvalidCredentialsError { message }
    }
}

Transfer Cart

mutation {
    transferPunchOutCart(sID: "...") {
        success
        message
    }
}

Requires an authenticated PunchOut session.

Cart Mapping

The plugin maps Vendure order lines to PunchCommerce basket positions:

  • Prices use gross/net pattern: price = gross (with tax), price_net = net (without tax)
  • All monetary values are converted from Vendure's integer cents to decimal (÷ 100)
  • Shipping is included as a separate position with type: 'shipping-costs' (controlled by shippingCostMode)
  • Product descriptions: description is plain text (HTML stripped), description_long preserves HTML
  • Basket is sent as multipart/form-data to PunchCommerce's /gateway/v3/return endpoint

Order Lifecycle

After a successful cart transfer, the order transitions to a custom Transferred state:

AddingItems → Transferred
  • The order becomes inactive (active = false), so a new PunchOut session creates a fresh cart
  • The order and all its line items are preserved in Vendure for record-keeping
  • The order is visible in the Vendure admin under Orders with state Transferred
  • Re-transferring the same session returns an error since no active order exists

The actual purchase order (PO) comes later through a separate channel — either manually or via cXML order transmission (future scope). The Transferred state represents "cart handed off to procurement system, awaiting PO."

Parallel Sessions

The plugin uses a custom ActiveOrderStrategy to scope orders by PunchOut session ID (sID). At the API level:

  • Each PunchOut session gets its own empty cart
  • The same customer can have multiple concurrent PunchOut sessions
  • Carts are isolated — items added in one session don't appear in another

Storefront considerations

Browser cookies are scoped per-domain, not per-tab. If your storefront stores the sID in a cookie, only one PunchOut session can be active at a time — starting a new session overwrites the cookie and the previous session's cart becomes inaccessible from the UI.

To support truly parallel sessions, store the sID in sessionStorage (which is tab-scoped) and pass it explicitly to server actions. This way each browser tab/iframe maintains its own independent PunchOut session.

When a new PunchOut session starts and replaces the previous sID, make sure to revalidate any cached cart data so the UI reflects the new (empty) cart instead of showing stale items from the previous session.