Skip to main content

Shopware 6 Gateway Plugin (Model B)

Überblick

Das PunchFlow Gateway Plugin für Shopware 6 ermöglicht den Gateway/Proxy-Modus (Model B) für PunchOut-Integration. Im Gegensatz zum API-basierten Ansatz (Model A) läuft hier der gesamte PunchOut-Flow direkt im Shop.

Model A vs Model B
  • Model A (Hosted Catalog): Produkte werden zu PunchFlow synchronisiert, Einkauf auf PunchFlow
  • Model B (Gateway/Proxy): Benutzer kaufen direkt im Shop, Plugin handhabt Session und cXML

Voraussetzungen

  • Shopware 6.5.0 oder höher (6.7+ empfohlen)
  • PHP 8.1 oder höher
  • Composer
  • PunchFlow Account mit Gateway-Lizenz

Installation

Via Composer (empfohlen)

# Im Shopware-Root-Verzeichnis
composer require punchflow/shopware6-punchout

# Plugin aktivieren
bin/console plugin:refresh
bin/console plugin:install --activate PunchFlowPunchOut

# Cache leeren
bin/console cache:clear

Manuelle Installation

# Plugin herunterladen
cd custom/plugins
git clone https://github.com/punchflow/shopware6-punchout.git PunchFlowPunchOut

# Composer Autoload aktualisieren
cd ../..
composer dump-autoload

# Plugin installieren
bin/console plugin:refresh
bin/console plugin:install --activate PunchFlowPunchOut
bin/console cache:clear

Konfiguration

Admin-Oberfläche

  1. Administration → Einstellungen → Plugins → PunchFlow PunchOut
  2. Konfigurieren Sie:
EinstellungBeschreibungStandard
API SecretShared Secret für HMAC-ValidierungErforderlich
Session TimeoutSitzungsdauer in Minuten60
Debug ModeErweiterte ProtokollierungAus
Auto-LoginAutomatische KundenerstellungEin
Product ID FieldFeld für SupplierPartIDproductNumber

Umgebungsvariablen

# .env.local
PUNCHFLOW_API_SECRET=your-secure-secret-key
PUNCHFLOW_DEBUG=false
PUNCHFLOW_SESSION_TIMEOUT=3600

config/packages/punchflow.yaml

punchflow_punchout:
api_secret: '%env(PUNCHFLOW_API_SECRET)%'
session_timeout: '%env(int:PUNCHFLOW_SESSION_TIMEOUT)%'
debug: '%env(bool:PUNCHFLOW_DEBUG)%'

# Produkt-Mapping
mapping:
supplier_part_id: 'productNumber' # oder 'ean', 'manufacturerNumber'
unspsc_attribute: 'customFields.punchflow_unspsc'
manufacturer_part_id: 'manufacturerNumber'

# Kunden-Konfiguration
customer:
auto_create: true
default_group: 'B2B'
default_payment_method: 'invoice'
default_salutation: 'mr'

# cXML-Konfiguration
cxml:
version: '1.2.050'
language: 'de-DE'
currency_from_context: true

Architektur

Plugin-Struktur

PunchFlowPunchOut/
├── src/
│ ├── Controller/
│ │ └── PunchOutController.php # Haupt-Endpoints
│ ├── Entity/
│ │ ├── PunchOutSession/ # Session-Entity
│ │ └── PunchOutOrderMessage/ # cXML-Nachrichten
│ ├── Service/
│ │ ├── SessionService.php # Session-Management
│ │ ├── TokenValidationService.php # HMAC-Validierung
│ │ ├── CxmlOrderMessageBuilder.php # cXML-Generierung
│ │ ├── ProductMappingService.php # Produkt-Mapping
│ │ ├── CookieService.php # Cookie-Handling
│ │ └── PunchOutLogger.php # Logging
│ ├── Subscriber/
│ │ └── CheckoutSubscriber.php # Checkout-Events
│ ├── Migration/
│ │ └── Migration1701234567CreatePunchOutTables.php
│ └── Resources/
│ ├── config/
│ │ ├── services.xml
│ │ └── routes.xml
│ └── app/
│ └── administration/ # Admin UI
├── tests/
│ ├── Unit/
│ └── Integration/
└── composer.json

Datenbank-Schema

-- punchflow_session
CREATE TABLE punchflow_session (
id BINARY(16) NOT NULL,
session_token VARCHAR(255) NOT NULL,
buyer_cookie VARCHAR(255) NOT NULL,
buyer_id VARCHAR(255),
buyer_name VARCHAR(255),
network_id VARCHAR(100),
return_url LONGTEXT NOT NULL,
operation VARCHAR(50),
cart_token VARCHAR(255),
customer_id BINARY(16),
status VARCHAR(50) NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
punchflow_session_id VARCHAR(255),
buyer_metadata JSON,
cart_snapshot JSON,
incoming_cxml LONGTEXT,
outgoing_cxml LONGTEXT,
created_at DATETIME(3) NOT NULL,
expires_at DATETIME(3) NOT NULL,
completed_at DATETIME(3),
updated_at DATETIME(3),
PRIMARY KEY (id),
INDEX idx_session_token (session_token),
INDEX idx_buyer_cookie (buyer_cookie),
INDEX idx_status (status)
);

-- punchflow_order_message
CREATE TABLE punchflow_order_message (
id BINARY(16) NOT NULL,
session_id BINARY(16) NOT NULL,
cxml_content LONGTEXT NOT NULL,
payload_id VARCHAR(255) NOT NULL,
direction VARCHAR(20),
message_type VARCHAR(50),
sent_at DATETIME(3),
response_code INT,
error_message VARCHAR(1000),
cart_data JSON,
item_count INT,
total_amount VARCHAR(50),
currency VARCHAR(3),
created_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (session_id) REFERENCES punchflow_session(id) ON DELETE CASCADE
);

API-Endpunkte

Session starten

POST /punchout/start
Content-Type: application/json
X-PunchFlow-Signature: hmac-sha256-signature
X-PunchFlow-Timestamp: 1701234567
X-PunchFlow-Nonce: unique-request-id

{
"buyer_cookie": "abc123",
"return_url": "https://erp.company.com/punchout/return",
"buyer_id": "DUNS:123456789",
"buyer_name": "Acme Corp",
"operation": "create",
"extrinsics": {
"UserEmail": "buyer@company.com",
"UserName": "Max Mustermann"
}
}

Response:

{
"success": true,
"session_id": "abc123def456",
"redirect_url": "https://shop.de/punchout/session/abc123def456",
"expires_at": "2024-12-01T12:00:00Z"
}

Warenkorb übertragen

POST /punchout/transfer
Cookie: punchout_session=abc123def456

# Response: Auto-Submit HTML Form mit cXML

Session-Status abfragen

GET /punchout/session/{sessionId}
X-PunchFlow-Signature: hmac-sha256-signature

# Response
{
"session_id": "abc123def456",
"status": "active",
"cart_items": 3,
"cart_total": "299.99",
"currency": "EUR",
"expires_at": "2024-12-01T12:00:00Z"
}

Sicherheit

HMAC-Validierung

Alle API-Requests werden mit HMAC-SHA256 signiert:

// Signature berechnen
$payload = $timestamp . $nonce . $requestBody;
$signature = hash_hmac('sha256', $payload, $apiSecret);

// Header setzen
X-PunchFlow-Signature: sha256=$signature
X-PunchFlow-Timestamp: $timestamp
X-PunchFlow-Nonce: $nonce

Nonce-Tracking

// Nonces werden in PSR-6 Cache gespeichert
// Verhindert Replay-Attacks
$cacheKey = "punchout_nonce_{$nonce}";
if ($this->cache->hasItem($cacheKey)) {
throw new NonceReusedException();
}
$this->cache->save(
$this->cache->getItem($cacheKey)->set(true)->expiresAfter(3600)
);

Session-Cookies

// Sichere Cookie-Konfiguration
$cookie = Cookie::create('punchout_session')
->withValue($sessionToken)
->withExpires($expiresAt)
->withSecure(true)
->withHttpOnly(true)
->withSameSite(Cookie::SAMESITE_STRICT);

cXML PunchOutOrderMessage

Generierte Struktur

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE cXML SYSTEM "http://xml.cxml.org/schemas/cXML/1.2.050/cXML.dtd">
<cXML version="1.2.050" payloadID="20241201120000.abc123@punchflow.de"
timestamp="2024-12-01T12:00:00+01:00" xml:lang="de-DE">
<Header>
<From>
<Credential domain="DUNS">
<Identity>PunchFlowShop</Identity>
</Credential>
</From>
<To>
<Credential domain="NetworkID">
<Identity>AN123456789</Identity>
</Credential>
</To>
<Sender>
<Credential domain="PunchFlow">
<Identity>PunchFlow</Identity>
<SharedSecret>HIDDEN</SharedSecret>
</Credential>
<UserAgent>PunchFlow Shopware 6 Plugin</UserAgent>
</Sender>
</Header>
<Message>
<PunchOutOrderMessage>
<BuyerCookie>abc123</BuyerCookie>
<PunchOutOrderMessageHeader operationAllowed="edit">
<Total>
<Money currency="EUR">299.99</Money>
</Total>
</PunchOutOrderMessageHeader>
<ItemIn quantity="2">
<ItemID>
<SupplierPartID>SW-PROD-001</SupplierPartID>
</ItemID>
<ItemDetail>
<UnitPrice>
<Money currency="EUR">99.99</Money>
</UnitPrice>
<Description xml:lang="de-DE">Produktname</Description>
<UnitOfMeasure>EA</UnitOfMeasure>
<Classification domain="UNSPSC">43211500</Classification>
<ManufacturerPartID>MPN-123</ManufacturerPartID>
<ManufacturerName>Hersteller GmbH</ManufacturerName>
</ItemDetail>
</ItemIn>
</PunchOutOrderMessage>
</Message>
</cXML>

Admin-Oberfläche

Session-Übersicht

Das Plugin fügt einen neuen Menüpunkt "PunchOut Sessions" hinzu:

  • Aktive Sessions anzeigen
  • Session-Details mit cXML-Vorschau
  • Session manuell beenden
  • Statistiken und Charts

Konfiguration im Admin

// Administration → Einstellungen → Plugins → PunchFlow PunchOut
// Konfigurationsfelder:
- API Secret (verschlüsselt)
- Session Timeout
- Debug Mode Toggle
- Auto-Login aktivieren
- Standard-Kundengruppe
- Produkt-ID Feld auswählen

Testing

Unit Tests ausführen

# Im Plugin-Verzeichnis
./vendor/bin/phpunit --configuration phpunit.xml.dist

# Mit Coverage
./vendor/bin/phpunit --coverage-html coverage/

Test-Session erstellen

# Via CLI
bin/console punchflow:session:create \
--buyer-cookie="test123" \
--return-url="https://test.erp.com/return" \
--buyer-email="test@example.com"

# Via API (mit gültiger Signatur)
curl -X POST "https://shop.de/punchout/start" \
-H "Content-Type: application/json" \
-H "X-PunchFlow-Signature: sha256=..." \
-H "X-PunchFlow-Timestamp: $(date +%s)" \
-H "X-PunchFlow-Nonce: $(uuidgen)" \
-d '{"buyer_cookie": "test123", "return_url": "https://test.erp.com/return"}'

Troubleshooting

Logs prüfen

# Plugin-spezifische Logs
tail -f var/log/punchflow_punchout.log

# Shopware Logs
tail -f var/log/prod.log | grep -i punchout

Debug-Modus aktivieren

# config/packages/punchflow.yaml
punchflow_punchout:
debug: true

Häufige Probleme

"Invalid Signature" Fehler

# Secret prüfen
bin/console debug:config punchflow_punchout api_secret

# Timestamp-Drift prüfen (max 5 Minuten)
date +%s # Lokale Zeit

Session wird nicht gefunden

# Session in Datenbank prüfen
bin/console dbal:run-sql "SELECT * FROM punchflow_session WHERE session_token = 'xxx'"

# Cookie prüfen (Browser DevTools)

cXML-Validierungsfehler

# cXML gegen DTD validieren
xmllint --dtdvalid http://xml.cxml.org/schemas/cXML/1.2.050/cXML.dtd output.xml

Performance

Caching-Empfehlungen

# config/packages/cache.yaml
framework:
cache:
pools:
punchflow.session_cache:
adapter: cache.adapter.redis
default_lifetime: 3600

Session-Cleanup

# Abgelaufene Sessions bereinigen (Cronjob)
bin/console punchflow:session:cleanup --older-than="7 days"

# Oder via Scheduled Task
# Das Plugin registriert automatisch einen Task

Updates

Plugin aktualisieren

composer update punchflow/shopware6-punchout

bin/console plugin:update PunchFlowPunchOut
bin/console cache:clear

Migrationen ausführen

bin/console database:migrate --all PunchFlowPunchOut

Support

Bei Fragen oder Problemen: