Das Problem
Nach dem Update eines Shopware-6-Shops auf Version 6.7.9.0 schlug jeder Produkt-Push aus der JTL-Wawi fehl. Im Abgleich-Log der Wawi tauchten pro Artikel 25 Validierungsfehler auf:
Code = 76454e69-502c-46c5-9643-f447d837c4d5
Detail = This value should be 1 or more.
Pointer = /0/0/prices/0/quantityStart
Code = 76454e69-502c-46c5-9643-f447d837c4d5
Detail = This value should be 1 or more.
Pointer = /0/0/prices/1/quantityStart
...
Der Fehler-Code 76454e69-502c-46c5-9643-f447d837c4d5 ist die Kennung der Symfony-Validator-Constraint GreaterThanOrEqual(1). Shopware 6.7 verlangt für das Feld quantityStart bei Produktpreisen einen Wert von mindestens 1 - der Connector sendet aber irgendwo 0.
Der Verdacht fiel zunächst auf fehlerhafte Staffelpreise in der JTL-Wawi. Die sind in der WaWi aber korrekt gepflegt oder gar nicht vorhanden - und trotzdem tritt der Fehler auf. Auch nach dem Löschen der betroffenen Artikel aus Shopware und komplettem Neu-Push kommt exakt dieselbe Meldung.
Die Ursachenforschung
Die JTL-Wawi-Datenbank gibt auf den ersten Blick keinen Aufschluss. Eine Prüfung der Staffelpreis-Tabelle tPreisDetail auf ungültige Mengen liefert zwar Treffer, aber die betroffenen Artikel sind darunter nicht dabei:
SELECT a.cArtNr, pd.nAnzahlAb, pd.fNettoPreis
FROM tArtikel a
JOIN tPreis p ON p.kArtikel = a.kArtikel
JOIN tPreisDetail pd ON pd.kPreis = p.kPreis
WHERE pd.nAnzahlAb < 1;
Für die tatsächlich betroffenen Artikel liefert die Abfrage - sowohl über tPreis als auch über andere preisrelevante Tabellen - leere Ergebnisse. Es gibt also in der WaWi gar keine Preisdaten mit ungültigen Mengen. Trotzdem baut der Connector einen Payload, den Shopware als ungültig ablehnt.
Damit war klar: Der Fehler entsteht im Connector selbst, beim Aufbau des API-Payloads - nicht durch fehlerhafte Stammdaten.
Der eigentliche Bug im Connector-Payload
Um den Fehler endgültig zu verifizieren, haben wir den ausgehenden API-Request auf Shopware-Seite mitgeschnitten. In der public/index.php landete temporär ein kleiner Debug-Logger, der den Raw-Body aller POST-Requests an /api/_action/sync in eine Datei schreibt.
Das Ergebnis für einen einzigen Artikel enthielt pro Artikel zehn Preis-Records - fünf Kundengruppen mal zwei Varianten:
// Set 1 (5 Einträge, alle mit quantityStart: 0)
{
"versionId": null,
"productId": null,
"ruleId": "9a705dc5c5854f0b8fb238a812ba2474",
"price": [{ "net": 108.4034, "gross": 129, ... }],
"quantityStart": 0,
"quantityEnd": null,
"id": "e1e2acec615d4d699c5a5618a44a94c8"
},
// Set 2 (5 Einträge, alle mit quantityStart: 1)
{
"versionId": "0fa91ce3e96a4bc2be4bd9ce752c3425",
"productId": "239432fbb97f4aa6b18589c5c80c733b",
"ruleId": "9a705dc5c5854f0b8fb238a812ba2474",
"price": [{ "net": 108.4034, "gross": 129, ... }],
"quantityStart": 1,
"quantityEnd": null,
"id": "46ddaa9956ff4ed4b5b048c3dc23f31c"
}
Der Connector sendet pro Artikel also doppelte Preis-Records: einmal als Create-Payload (ohne productId, mit quantityStart: 0) und einmal als Update-Payload (mit gültiger productId und quantityStart: 1). Shopware 6.7 lehnt den Create-Payload wegen der strengen Validierung ab - und damit die gesamte Anfrage.
Die Kompatibilitätslage bei JTL
Ein Blick ins offizielle JTL-Forum bestätigt das Bild: Der JTL-Connector ist per Stand April 2026 offiziell nur bis Shopware 6.7.6.0 freigegeben. Die letzte Connector-Version 2.1.3 stammt vom 19. Februar 2026. Seit dem Sprung auf Shopware 6.7.7 und höher häufen sich im Forum Fehlerberichte - der quantityStart-Fehler ist einer davon, aber nicht der einzige.
JTL hat angekündigt, den Connector für neuere Shopware-Versionen nachzuziehen - der Takt der Updates ist allerdings langsam. Bis ein offizieller Fix verfügbar ist, stehen Shop-Betreiber vor der Wahl: Shopware-Downgrade, auf das JTL-Update warten oder einen eigenen Workaround bauen.
Die Lösung: Ein Request-Level-Decorator
Die naheliegende Stelle für einen Fix wäre der PreWriteValidationEvent der Shopware Data Abstraction Layer - der Event feuert, bevor die Validierung auf Write-Command-Ebene läuft. In Shopware 6.7 reicht das aber nicht: Die quantityStart-Validierung erfolgt bereits vor der DAL, direkt auf dem eingehenden API-DTO. Der Event wird nie erreicht.
Die Lösung liegt eine Ebene höher: Der HTTP-Request wird über einen kernel.request-Listener mit hoher Priority abgefangen - noch bevor Shopwares API-Controller den Body überhaupt auspackt. Der Listener parsed den JSON-Body, normalisiert alle quantityStart-Werte unter 1 auf genau 1 und schreibt den bereinigten Body zurück.
Der Subscriber im Detail
class ProductPriceQuantityStartFixSubscriber implements EventSubscriberInterface
{
private const MIN_VALUE = 1;
private const TARGET_ROUTES =
[
'/api/_action/sync',
'/api/product',
];
public static function getSubscribedEvents() : array
{
return
[
KernelEvents::REQUEST => ['onKernelRequest', 10000],
];
}
public function onKernelRequest(RequestEvent $event) : void
{
if (!$event->isMainRequest())
{
return;
}
$request = $event->getRequest();
if (!in_array($request->getMethod(), ['POST', 'PATCH', 'PUT'], true))
{
return;
}
$uri = $request->getPathInfo();
$matches = false;
foreach (self::TARGET_ROUTES as $needle)
{
if (str_contains($uri, $needle))
{
$matches = true;
break;
}
}
if (!$matches)
{
return;
}
$content = $request->getContent();
$data = json_decode($content, true);
if (!is_array($data))
{
return;
}
$fixCount = 0;
$this->walk($data, $fixCount);
if ($fixCount === 0)
{
return;
}
$newContent = json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
$this->replaceRequestContent($request, $newContent);
}
private function walk(array &$data, int &$fixCount) : void
{
foreach ($data as $key => &$value)
{
if (($key === 'quantityStart' || $key === 'quantity_start')
&& ($value === null || (is_numeric($value) && (int) $value < self::MIN_VALUE))
)
{
$value = self::MIN_VALUE;
$fixCount++;
continue;
}
if (is_array($value))
{
$this->walk($value, $fixCount);
}
}
}
}
Warum dieser Ansatz
Keine Abhängigkeit von Shopware-internen Events - der kernel.request-Event ist Symfony-Standard und verhält sich versionsübergreifend stabil. Bei einem Shopware-Update in der Zukunft muss nichts nachgezogen werden.
Minimaler Footprint ein Subscriber, eine Methode, ein Feld. Kein Eingriff in Steuerlogik, Kundengruppen, Preisregeln oder sonstige Business-Logik.
Rückstandsfrei entfernbar das Plugin hat keine Datenbank-Migrationen, keine geänderten Entitäten, keine Template-Overrides. Nach Deinstallation ist der Shop im Ausgangszustand.
Sichtbares Logging jeder Normalisierungsvorgang wird ins prod.log geschrieben. So lässt sich jederzeit nachvollziehen, ob und wann der Fix greift und wann er dank eines offiziellen JTL-Updates endlich überflüssig wird.
Installation und Download
Das fertige Plugin steht als ZIP zum Download bereit:
Manuelle Installation
# ZIP in das Plugin-Verzeichnis entpacken
cd /var/www/html/custom/plugins
unzip /pfad/zu/FeinwerkJtlQuantityStartFix.zip
# Plugin registrieren und aktivieren
cd /var/www/html
bin/console plugin:refresh
bin/console plugin:install --activate FeinwerkJtlQuantityStartFix
bin/console cache:clear
Bei Containerbetrieb anschließend den Shop-Container einmal neustarten. OPcache ist in Produktion meist so konfiguriert, dass opcache_reset() allein nicht ausreicht neue PHP-Klassen werden erst nach einem Neustart des FPM-Pools geladen.
Verifikation
Nach Aktivierung einen Produkt-Push aus der JTL-Wawi anstoßen. Im var/log/prod.log erscheinen Einträge der Form:
FeinwerkJtlQuantityStartFix: normalized quantityStart values in request payload
uri: /api/_action/sync
method: POST
fix_count: 25
Der API-Fehler bleibt aus, Produkte erscheinen korrekt im Shop inklusive aller Preise pro Kundengruppe.
Fallstricke aus der Praxis
OPcache ist hartnäckiger als erwartet. In Produktions-Containern reicht ein bin/console cache:clear nicht, um neue PHP-Klassen zu laden. Symfonys Cache betrifft nur den Container-Cache — der OPcache von PHP-FPM hält kompilierte Bytecode-Versionen und ignoriert Dateiänderungen, wenn opcache.validate_timestamps=0 gesetzt ist. Die zuverlässige Lösung ist ein Container-Restart.
Der PreWriteValidationEvent ist in Shopware 6.7 zu spät. Die naheliegende DAL-Event-Lösung funktioniert nicht, weil die Validierung schon auf API-DTO-Ebene läuft bevor überhaupt Write-Commands gebaut werden. Wer diesen Ansatz probiert, sieht keine Log-Einträge und keinen Effekt. Der kernel.request-Listener ist die richtige Schicht.
Content-Length muss aktualisiert werden. Nach dem Ersetzen des Request-Bodies via Reflection muss auch der Content-Length-Header nachgezogen werden sonst trunkieren nachgelagerte Parser den modifizierten Body. Das Plugin erledigt das automatisch.
Der Fix ist ein Workaround, kein Ersatz für ein JTL-Update. Das Plugin korrigiert ein Symptom die eigentliche Ursache liegt im Connector-Code. Sobald JTL einen offiziellen Fix ausliefert, sollte das Plugin deinstalliert werden, um die Verantwortlichkeiten sauber zu halten.
Vorgehen bei der Installation
- Shopware-Version prüfen das Plugin ist für Shopware 6.7+ gebaut. Bei älteren Versionen besteht das Problem ohnehin nicht.
- Backup erstellen zumindest Datenbank und
custom/plugins/-Verzeichnis - Plugin-ZIP in
custom/plugins/entpacken - Plugin registrieren und aktivieren
- Container neustarten (bei Podman/Docker-Betrieb)
- Produkt-Push aus der WaWi testen mit einem einzelnen Artikel, nicht dem kompletten Bestand
- Log prüfen
prod.logaufFeinwerkJtlQuantityStartFix-Einträge durchsuchen - Bei Erfolg: Vollabgleich starten
Ergebnis
Nach Installation des Plugins läuft die JTL-Wawi-Synchronisation mit Shopware 6.7.9.0 wieder fehlerfrei. Sowohl Neuanlagen als auch Updates bestehender Artikel werden korrekt übertragen inklusive aller Preise pro Kundengruppe. Der Fix greift vollautomatisch bei jedem Request und protokolliert jeden Eingriff transparent im Log.
Für den Zeitraum bis zum offiziellen JTL-Connector-Update ist damit ein akutes Geschäftsrisiko beseitigt: Produkte landen im Shop, Bestellungen können fließen, und das Tagesgeschäft ist wieder handlungsfähig.
Fazit: Nicht jeder Bug in Drittanbieter-Software muss per Support-Ticket ausgesessen werden. Wer die Symfony-Request-Pipeline versteht, kann mit wenigen Zeilen Code gezielt eingreifen minimal-invasiv, auditierbar und rückstandsfrei. Das Plugin schließt die Lücke zwischen Shopware 6.7.9 und dem aktuellen JTL-Connector genau da, wo der Payload kaputt ist: im eingehenden API-Request, bevor der Validator zuschlägt.
Nachtrag April 2026: Update auf v1.2.1 | doppelte Preise sauber bereinigt
Nach der ersten Auslieferung des Plugins zeigte sich im Live-Betrieb ein zweites Problem: Die Version 1.0 normalisierte zwar quantityStart: 0 korrekt auf 1, schickte aber damit pro Kundengruppen-Regel weiterhin zwei identische Preis-Records zu Shopware. Im Storefront erschien das als irreführender Mengenrahmen (“ab 1 Stück: 12,99 EUR / ab 1 Stück: 12,99 EUR”), den ein Kunde berechtigterweise nicht im Shop haben wollte.
Eine genauere Analyse des Connector-Payloads zeigte das eigentliche Muster: Pro Artikel und Kundengruppen-Regel sendet der JTL-Connector bei bestehenden Artikeln zwei Einträge einen “Create”-Record mit productId: null und quantityStart: 0 (Set A) sowie einen “Update”-Record mit gültiger productId und quantityStart: 1 (Set B). Set B enthält den korrekten Preis, Set A ist redundant. Bei einem Erst-Create (Artikel noch nicht in Shopware) wird dagegen nur Set A gesendet, was hier legitim ist denn es gibt noch keine productId zum Referenzieren.
Daraus ergab sich die finale Strategie in Plugin-Version 1.2.1:
Pro prices[]-Array werden die Einträge nach ruleId gruppiert. Pro Regel wird entschieden:
- Existieren sowohl Einträge mit gesetzter
productIdals auch Orphans -> die Orphans werden verworfen (Update-Fall, dedupliziert) - Existieren nur Orphan-Einträge -> sie werden behalten und
quantityStart < 1auf1normalisiert (Erst-Create-Fall, gerettet)
Damit funktionieren alle drei kritischen Fälle sauber: Update bestehender Artikel, Erst-Anlage einfacher Artikel und Variantenartikel mit Vater plus Kindern.
Bereinigung bestehender Doppel-Records
Bei Shops, in denen bereits Plugin-Version 1.0 lief oder die schon vor Installation des Plugins regelmäßig vom JTL-Connector synchronisiert wurden liegen in der Shopware-Datenbank möglicherweise doppelte product_price-Records. Diese verursachen die Mengenrahmen-Anzeige im Storefront und sollten einmalig bereinigt werden.
Schritt 1 - Inspektion: Wieviele Records sind betroffen?
SELECT
COUNT(*) AS doppelte_records_gesamt,
COUNT(DISTINCT pp.product_id) AS betroffene_produkte
FROM product_price pp
WHERE EXISTS (
SELECT 1 FROM product_price pp2
WHERE pp2.product_id = pp.product_id
AND pp2.rule_id = pp.rule_id
AND pp2.quantity_start = pp.quantity_start
AND pp2.id > pp.id
);
Die Ausgabe gibt die genaue Zahl der zu löschenden Records und der betroffenen Produkte. Diese Zahl wird gleich zur Verifikation gebraucht.
Schritt 2 - Stichprobe: Sind die Preise in den Doppel-Gruppen wirklich identisch?
SELECT
p.product_number,
pp.quantity_start,
pp.price,
pp.created_at
FROM product_price pp
INNER JOIN product p ON p.id = pp.product_id
WHERE pp.product_id IN (
SELECT product_id FROM product_price
GROUP BY product_id, rule_id, quantity_start
HAVING COUNT(*) > 1
)
ORDER BY p.product_number, pp.rule_id, pp.id
LIMIT 30;
Wenn alle Doppel-Records innerhalb einer Gruppe identische Preisbeträge zeigen (was beim hier beschriebenen Bug der Regelfall ist), ist die Bereinigung inhaltlich risikofrei. Falls Beträge abweichen z. B. weil zwischenzeitlich Preise aktualisiert wurden — sollte vor dem DELETE Rücksprache mit dem Shop-Inhaber gehalten werden.
Schritt 3 - Bereinigung in einer Transaktion:
START TRANSACTION;
-- Behält pro Gruppe den jüngsten Record (höchstes created_at).
-- Bei Gleichstand entscheidet die höhere id.
DELETE pp1
FROM product_price pp1
INNER JOIN product_price pp2
ON pp1.product_id = pp2.product_id
AND pp1.rule_id = pp2.rule_id
AND pp1.quantity_start = pp2.quantity_start
AND (
pp1.created_at < pp2.created_at
OR (pp1.created_at = pp2.created_at AND pp1.id < pp2.id)
);
SELECT ROW_COUNT() AS geloescht;
-- Wenn die Zahl mit Schritt 1 übereinstimmt:
-- COMMIT;
-- Bei Zweifel:
-- ROLLBACK;
Schritt 4 - Verifikation: Sind wirklich keine Doppelungen mehr vorhanden?
SELECT COUNT(*) AS verbliebene_doppelungen
FROM product_price pp
WHERE EXISTS (
SELECT 1 FROM product_price pp2
WHERE pp2.product_id = pp.product_id
AND pp2.rule_id = pp.rule_id
AND pp2.quantity_start = pp.quantity_start
AND pp2.id != pp.id
);
Erwartete Ausgabe: 0. Anschliessend Shopware-Cache leeren, damit das Storefront keine veralteten Preise mehr ausliefert:
bin/console cache:clear
Stichprobenartig 2-3 zuvor betroffene Produkte im Storefront aufrufen der Mengenrahmen sollte verschwunden sein, der Preis korrekt und ein einzelner Eintrag.
Beobachtungen aus der Praxis
In einem Live-System mit rund 700 Artikeln waren nach der Bereinigung 95 Doppel-Records über 13 Produkte identifiziert und sauber entfernt worden.
Die aktualisierte Plugin-Version 1.2.1 enthält zusätzlich ein Plugin-eigenes Logfile unter var/log/feinwerk-jtl-fix.log. Das ist unabhängig von der Shopware-Monolog-Konfiguration und protokolliert jeden Bereinigungsvorgang in der Form:
[2026-04-22 10:16:39] cleaned product_price entries: removed_orphans=5 normalized_qstart=0 uri=/api/_action/sync method=POST
Damit ist im Live-Betrieb jederzeit nachvollziehbar, wie viele Einträge das Plugin pro Sync bereinigt und wann der Fix dank eines offiziellen JTL-Updates endlich nicht mehr greifen muss.
Fazit zum Nachtrag: Was wie ein einfacher Validierungsfehler aussah, entpuppte sich als zweistufiges Problem: erst die abgewiesene Validierung, dann die Doppel-Records im Datenbestand. Die finale Plugin-Version 1.2.1 deckt beide Fälle ab und behandelt zusätzlich die Erst-Anlage von Artikeln korrekt ein Edge-Case, den die Zwischenversion 1.1 noch broken hatte. Der bereitgestellte SQL-Bereinigungsblock räumt verbleibende Datenartefakte sauber auf.