diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..523fc6fce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Exclude vendored third-party code from git whitespace / diff-check hygiene +# so that upstream formatting choices (trailing whitespace, tabs, mixed EOL) +# do not block `git diff --check` or review tooling. +vendor/** -whitespace diff --git a/classes/Components/WooCommerce/ClientWrapper.php b/classes/Components/WooCommerce/ClientWrapper.php new file mode 100644 index 000000000..3bec69a13 --- /dev/null +++ b/classes/Components/WooCommerce/ClientWrapper.php @@ -0,0 +1,256 @@ + lowercase-keyed header map */ + private $lowercaseHeaders; + + public function __construct(\Automattic\WooCommerce\HttpClient\Response $response) + { + $this->response = $response; + + // Build a normalised, lowercase header map once. + $this->lowercaseHeaders = []; + foreach ($response->getHeaders() as $key => $value) { + $this->lowercaseHeaders[strtolower($key)] = $value; + } + } + + /** + * Case-insensitive single-header lookup. + * + * @param string $name Header name (e.g. 'x-wp-total', 'X-WP-TotalPages') + * @return string|null Header value, or null when not present + */ + public function getHeader($name) + { + $key = strtolower($name); + return isset($this->lowercaseHeaders[$key]) ? $this->lowercaseHeaders[$key] : null; + } + + /** + * All response headers (lowercase keys). + * + * @return array + */ + public function getHeaders() + { + return $this->lowercaseHeaders; + } + + /** @return int HTTP status code */ + public function getCode() + { + return $this->response->getCode(); + } + + /** @return string Response body */ + public function getBody() + { + return $this->response->getBody(); + } +} + +/** + * Drop-in replacement for the old inline WCClient. + * + * Wraps the upstream Automattic\WooCommerce\Client and adds: + * - getLastResponse(): ResponseWrapper + * - ssl_ignore support via verify_ssl option + * - PSR-3-style logger integration: HTTP >= 400 is logged as warning, + * HttpClientException is logged as error (and rethrown) + */ +class ClientWrapper +{ + /** @var UpstreamClient */ + private $client; + + /** @var ResponseWrapper|null */ + private $lastResponse; + + /** @var \Psr\Log\LoggerInterface|null */ + private $logger; + + /** + * @param string $url WooCommerce store URL + * @param string $consumerKey API consumer key + * @param string $consumerSecret API consumer secret + * @param array $options Upstream SDK options (version, timeout, …) + * @param mixed $logger PSR-3-compatible logger; null disables logging + * @param bool|mixed $sslIgnore When truthy, disables SSL certificate verification + */ + public function __construct($url, $consumerKey, $consumerSecret, $options = [], $logger = null, $sslIgnore = false) + { + if ($sslIgnore) { + $options['verify_ssl'] = false; + } + + $this->client = new UpstreamClient($url, $consumerKey, $consumerSecret, $options); + $this->logger = $logger; + } + + /** + * GET request. + * + * @param string $endpoint API endpoint + * @param array $parameters Query parameters + * @return \stdClass|array + * @throws HttpClientException + */ + public function get($endpoint, $parameters = []) + { + return $this->dispatch('GET', $endpoint, function () use ($endpoint, $parameters) { + return $this->client->get($endpoint, $parameters); + }); + } + + /** + * POST request. + * + * @param string $endpoint API endpoint + * @param array $data Request body + * @return \stdClass + * @throws HttpClientException + */ + public function post($endpoint, $data) + { + return $this->dispatch('POST', $endpoint, function () use ($endpoint, $data) { + return $this->client->post($endpoint, $data); + }); + } + + /** + * PUT request. + * + * @param string $endpoint API endpoint + * @param array $data Request body + * @return \stdClass + * @throws HttpClientException + */ + public function put($endpoint, $data) + { + return $this->dispatch('PUT', $endpoint, function () use ($endpoint, $data) { + return $this->client->put($endpoint, $data); + }); + } + + /** + * DELETE request. + * + * @param string $endpoint API endpoint + * @param array $parameters Query parameters + * @return \stdClass + * @throws HttpClientException + */ + public function delete($endpoint, $parameters = []) + { + return $this->dispatch('DELETE', $endpoint, function () use ($endpoint, $parameters) { + return $this->client->delete($endpoint, $parameters); + }); + } + + /** + * OPTIONS request. + * + * @param string $endpoint API endpoint + * @return \stdClass + * @throws HttpClientException + */ + public function options($endpoint) + { + return $this->dispatch('OPTIONS', $endpoint, function () use ($endpoint) { + return $this->client->options($endpoint); + }); + } + + /** + * Returns the response from the most recent API call, or null when no + * request has been made yet. + * + * @return ResponseWrapper|null + */ + public function getLastResponse() + { + return $this->lastResponse; + } + + /** + * Run an upstream call with unified response capture and error logging. + * Exceptions are logged (when a logger is available) and rethrown so + * existing callsites keep their try/catch contract. + * + * @param string $method HTTP verb for log context + * @param string $endpoint API endpoint for log context + * @param callable $call Zero-argument closure performing the upstream call + * @return mixed Upstream result + * @throws HttpClientException + */ + private function dispatch($method, $endpoint, callable $call) + { + try { + $result = $call(); + } catch (HttpClientException $e) { + $this->captureResponse($method, $endpoint); + if ($this->logger !== null) { + $this->logger->error( + sprintf('WooCommerce %s %s failed: %s', $method, $endpoint, $e->getMessage()), + ['code' => $e->getCode()] + ); + } + throw $e; + } + $this->captureResponse($method, $endpoint); + return $result; + } + + /** + * Snapshot the upstream response after each request. HTTP >= 400 is + * logged as warning with a truncated body excerpt to keep logs small. + * + * @param string $method HTTP verb for log context + * @param string $endpoint API endpoint for log context + */ + private function captureResponse($method, $endpoint) + { + $upstreamResponse = $this->client->http->getResponse(); + if ($upstreamResponse === null) { + return; + } + $this->lastResponse = new ResponseWrapper($upstreamResponse); + + if ($this->logger !== null) { + $code = $upstreamResponse->getCode(); + if ($code >= 400) { + $this->logger->warning( + sprintf('WooCommerce %s %s returned HTTP %d', $method, $endpoint, $code), + ['body' => substr((string) $upstreamResponse->getBody(), 0, 500)] + ); + } + } + } +} diff --git a/composer.json b/composer.json index 06e2a500e..e4cb4f323 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "swiss-payment-slip/swiss-payment-slip": "0.13.0 as 0.11.1", "swiss-payment-slip/swiss-payment-slip-fpdf": "0.6.0", "tecnickcom/tcpdf": "6.3.5", - "y0lk/oauth1-etsy": "1.1.0" + "y0lk/oauth1-etsy": "1.1.0", + "automattic/woocommerce": "^3.1" }, "replace": { "itbz/fpdf": "*" @@ -41,4 +42,4 @@ }, "optimize-autoloader": true } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 85d34929c..e42add22d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "229fd9b44bc0380415380fe519e53dfb", + "content-hash": "0e587d2db823187eb218c8737c439bff", "packages": [ { "name": "aura/sqlquery", @@ -74,6 +74,113 @@ }, "time": "2016-10-03T20:34:56+00:00" }, + { + "name": "automattic/woocommerce", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/wc-api-php.git", + "reference": "e378120df655b7dacbeff4756e23b41b66fe9688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/wc-api-php/zipball/e378120df655b7dacbeff4756e23b41b66fe9688", + "reference": "e378120df655b7dacbeff4756e23b41b66fe9688", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">= 7.4.0" + }, + "require-dev": { + "doctrine/instantiator": "^1.5.0", + "phpunit/phpunit": "^9.5.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Automattic\\WooCommerce\\": [ + "src/WooCommerce" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Claudio Sanches", + "email": "claudio.sanches@automattic.com" + } + ], + "description": "A PHP wrapper for the WooCommerce REST API", + "keywords": [ + "api", + "woocommerce" + ], + "support": { + "issues": "https://github.com/woocommerce/wc-api-php/issues", + "source": "https://github.com/woocommerce/wc-api-php/tree/3.1.1" + }, + "time": "2026-01-30T16:26:03+00:00" + }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, { "name": "aws/aws-sdk-php", "version": "3.175.2", diff --git a/docs/plans/woocommerce-batch-stock-sync.md b/docs/plans/woocommerce-batch-stock-sync.md new file mode 100644 index 000000000..fc9d54e2a --- /dev/null +++ b/docs/plans/woocommerce-batch-stock-sync.md @@ -0,0 +1,126 @@ +# WooCommerce Batch Stock Sync — Plan + +Companion document to [GitHub Issue #263](https://github.com/OpenXE-org/OpenXE/issues/263) +and the `feature/woocommerce-batch-stock-sync` branch. + +--- + +## §1 Ziel + +`ImportSendListLager()` bisher: 2 HTTP-Requests pro Artikel (SKU-Lookup + PUT). +Bei 1.000 Artikeln = 2.000 Requests — langsam, rate-limit-anfällig, kein Partial-Error-Handling. + +Ziel: Nutzung der offiziellen WC REST v3 Batch-Endpoints, um den Request-Count auf +`ceil(n/100)` SKU-Lookups + `ceil(n/100)` Batch-Updates zu reduzieren (~20 statt 2.000). + +--- + +## §2 Scope + +### IN +- Refactor von `ImportSendListLager()` in `www/pages/shopimporter_woocommerce.php` +- Neue private Hilfsmethode `processBatchResponse()` in derselben Klasse +- Plan-Dokument `docs/plans/woocommerce-batch-stock-sync.md` + +### OUT +- Kein neuer `postBatch()`-Helper auf `WCClient` — `post()` reicht direkt +- Keine Änderungen an `ImportGetAuftrag*`, `getKonfig`, `parseOrder` +- Kein Retry-Mechanismus bei HTTP-Fehlern (separater Task) +- Kein DB-Schema-Change +- Kein PR-Open (nach Review durch Maintainer) + +--- + +## §3 Fix-Parameter + +| Parameter | Wert | Begründung | +|---|---|---| +| Batch-Size | 100 Items | WC REST v3 Maximum | +| SKU-Chunk-Size | 100 SKUs | Passend zu `per_page=100` | +| Retry | Keiner | Out-of-scope für diesen PR | +| SKU-CSV-Support | `?sku=a,b,c` | WC REST v3 akzeptiert kommaseparierte Liste | + +--- + +## §4 Funktions-Landkarte + +Nur `ImportSendListLager()` wird geändert. Neue private Methode `processBatchResponse()`. + +``` +ImportSendListLager() + ├── Schritt 1: Alle SKUs + Stock-Params sammeln (pendingUpdates[]) + ├── Schritt 2: Bulk-SKU-Auflösung + │ └── GET products?sku=&per_page=100 (je Chunk à 100 SKUs) + │ → skuMap[sku] = {id, parent, isvariant} + ├── Schritt 3: Gruppieren + │ ├── simpleItems[] (für products/batch) + │ └── variationItems[parent_id][] (für products/{id}/variations/batch) + └── Schritt 4: Batch-Updates senden (je Chunk à 100) + ├── POST products/batch {update: [...]} + ├── POST products/{parent}/variations/batch {update: [...]} + └── processBatchResponse() → Partial-Error-Logging, zählt Erfolge + +processBatchResponse($response, $endpoint) + ├── Iteriert response->update[] + │ ├── item->error vorhanden → logger->error + │ └── kein Fehler → successCount++ + └── Iteriert response->errors[] (WC-Fallback) +``` + +--- + +## §5 Implementierungsschritte + +1. **Vorverlagerte Datensammlung:** Statt der bisherigen per-Artikel-Schleife erst alle SKUs + und Stock-Params in `$pendingUpdates[]` sammeln. + +2. **Bulk-SKU-Auflösung:** `GET products?sku=&per_page=100` in Chunks à 100 SKUs. + Ergebnis in `$skuMap[sku]` cachen. + +3. **Gruppierung:** Simple products in `$simpleItems[]`, Variations pro Parent-ID in + `$variationItems[parent_id][]`. + +4. **Batch-POST:** `$this->client->post('products/batch', ['update' => $chunk])` für + simple products; analog für Variations. + +5. **Partial-Error-Handling:** `processBatchResponse()` liest `response->update[]` und + `response->errors[]`, loggt Fehler per Item ohne den restlichen Sync abzubrechen. + +--- + +## §6 Test-Matrix + +Testumgebung: Docker-Shop auf `192.168.0.143:8080`, WC 10.7, Consumer Keys aktiv. + +| # | Szenario | Erwartetes Ergebnis | +|---|---|---| +| T1 | 1 Artikel | 1 SKU-GET + 1 Batch-POST, Lagerbestand korrekt | +| T2 | 100 Artikel | 1 SKU-GET + 1 Batch-POST (1 Chunk) | +| T3 | 250 Artikel | 3 SKU-GETs + 3 Batch-POSTs (100+100+50) | +| T4 | 1 falsche SKU in 100er-Batch | 99 korrekt updated, 1 Error geloggt, sync läuft durch | +| T5 | Variation (parent != 0) | Variation-Batch-Endpoint genutzt, nicht products/batch | +| T6 | `ausverkauft=1` | stock_quantity=0 im Batch-Item | +| T7 | `inaktiv=1` | status='private' im Batch-Item | +| T8 | `pseudolager` gesetzt | lageranzahl aus pseudolager, nicht anzahl_lager | +| T9 | Leere Artikelliste | Rückgabe 0, keine HTTP-Requests | + +--- + +## §7 Rollout & Rückwärts-Kompatibilität + +- **Interface unverändert:** `ImportSendListLager()` behält Signatur `(): int`. +- **Keine DB-Migration** erforderlich. +- **WC-Mindestversion:** REST v3 Batch-Endpoints seit WC 3.0 (2017) — unkritisch. +- **Rollback:** Git-Revert des Feature-Branch reicht. + +--- + +## §8 Risiken + +| Risiko | Mitigation | +|---|---| +| WC akzeptiert SKU-CSV nicht (`?sku=a,b`) | Verifikation in T1–T3 gegen 192.168.0.143:8080 — falls nicht, Fallback: sequentielle Einzellookups (wie vorher) | +| Batch-Response enthält keine `update`-Keys | `processBatchResponse()` defensiv mit `?? []` abgesichert | +| Hoster-seitiger `per_page`-Cap unter 100 | Derzeit nicht abgefangen; separater Task | +| Partial-Error zählt als nicht-erfolgreich | Korrekt: `$anzahl` zählt nur Items ohne Fehler | +| Variations mit unbekanntem Parent | Variation-Batch schlägt fehl → WCHttpClientException propagiert nach oben (bestehende Semantik) | diff --git a/docs/plans/woocommerce-pagination-fix.md b/docs/plans/woocommerce-pagination-fix.md new file mode 100644 index 000000000..5d874b990 --- /dev/null +++ b/docs/plans/woocommerce-pagination-fix.md @@ -0,0 +1,340 @@ +# WooCommerce-Bestellimport: Pagination + `after`-Filter + +**Issue:** [openxe-org/openxe#262](https://github.com/OpenXE-org/OpenXE/issues/262) +**Branch:** `fix/woocommerce-order-import-pagination` +**Base:** `development` @ `8bb13973` +**Typ:** Bugfix (stiller Datenverlust) + +--- + +## 1. Ziel + +Der WooCommerce-Shopimporter verliert Bestellungen, wenn mehr als 20 neue Aufträge +zwischen zwei Cron-Läufen eingehen. Dieser Fix macht den Bestellabruf vollständig +und verlustfrei, ohne neue Plugin-Abhängigkeiten und ohne Wechsel der API-Version +(bleibt auf `wc/v3`). + +## 2. Scope + +### IN-Scope + +- Single-Order-Abruf mit `after`-Filter + `exclude`-Tupel-Cursor; Caller-Loop iteriert. +- `after`-Filter (ISO-8601) statt 800er-`include`-Hack +- Persistenz "letzter erfolgreicher Import-Timestamp" pro Shop +- Fallback-Logik für Erstlauf (kein Timestamp vorhanden) +- Transitions-Kompatibilität zum Legacy-Parameter `ab_nummer` + +### OUT-of-Scope (eigene Issues/PRs) + +- Batch-Endpoints fuer Stock-Sync (`products/batch`) +- Retry / Backoff fuer 429/5xx +- Composer-Migration des inline-WC-SDK +- Webhook-Support (#239) +- HPOS-Testmatrix +- UI-Reset-Button fuer den Import-Timestamp + +## 3. Fix-Parameter (final) + +| Parameter | Wert | Begruendung | +|---|---|---| +| Erstlauf-Fallback | **30 Tage** | Sinnvoller Mittelweg: holt historische Bestellungen, aber nicht unbegrenzt. UI-Override in Folge-PR moeglich. | +| Persistenz-Feld | `shopexport.einstellungen_json` → `felder.letzter_import_timestamp` | Bestehende Struktur nutzen, keine DB-Migration. | +| Timestamp-Format | ISO-8601 `Y-m-d\TH:i:s` (UTC) | Direkt an `after=` durchreichbar. | +| Caller-Cap (pre-existing) | `shopexport.maxmanuell`, default 100 | Der Batch-Cap liegt beim shopimport.php-Caller, nicht im Importer. | +| Cursor-Format | Tupel `(letzter_import_timestamp, letzter_import_order_ids: [int])` | IDs sammeln den aktuellen Sekunden-Bucket; -1s-Verschiebung greift nur mit mindestens einer ID. | + +## 4. Datei- und Funktions-Landkarte + +Zielmodul: `www/pages/shopimporter_woocommerce.php` + +| Funktion | Zeilen | Betroffen? | Aenderung | +|---|---|---|---| +| `ImportGetAuftraegeAnzahl` | ~80–135 | ja | `include`-Hack raus, `after`-Filter rein, nur noch Count via `X-WP-Total` | +| `ImportGetAuftrag` | ~121–185 | ja | Single-Order-Query mit after-Filter; Caller-Loop in shopimport.php iteriert | +| `parseOrder` | ~222–305 | nein | unveraendert (liefert weiterhin pro Order) | +| `CatchRemoteCommand('data')` | mehrfach | mittelbar | stellt `letzter_import_timestamp` aus `einstellungen_json` bereit | +| `getKonfig` | ~802–837 | nein | nicht anfassen (separates Issue #224) | +| Inline-Client (`WCClient`, `WCHttpClient`, `WCResponse`) | 1021–2370 | ggf. ja | Header-Accessor ergaenzen falls nicht vorhanden | + +## 5. Implementierungsschritte + +### Schritt 1 — Header-Durchreichung im inline-Client pruefen + +**Aufgabe:** Klaeren, ob `WCResponse` die HTTP-Antwort-Header (`X-WP-Total`, +`X-WP-TotalPages`, `Link`) bereits als Array liefert oder ob der inline-Client +angepasst werden muss. + +**Deep-Read-Ziele (nur Lesen, kein Edit):** +- `WCHttpClient::processResponse()` / aequivalent (vermutlich ~Zeile 2140–2200) +- `WCResponse`-Constructor: werden Headers gespeichert? +- `curl_setopt`-Setup: ist `CURLOPT_HEADERFUNCTION` oder `CURLOPT_HEADER` aktiv? + +**Entscheidungs-Gate:** +- Wenn Headers bereits im Response-Objekt: direkt weiter mit Schritt 3. +- Wenn nicht: Schritt 2 vor Schritt 3. + +**Akzeptanzkriterium:** Klarer Plan, welche Client-Aenderung noetig ist (oder dass keine noetig ist). + +### Schritt 2 — Headers exponieren (bedingt) + +**Nur falls Schritt 1 ergibt, dass Headers nicht zur Businesslogik durchdringen.** + +- `WCHttpClient::processResponse()`: Header-String in assoziatives Array parsen. +- `WCResponse` um `getHeaders()` / `getHeader(string $name)` erweitern. +- cURL-Setup: `CURLOPT_HEADERFUNCTION` registrieren, Headers in Sammelarray schreiben. +- Case-insensitive Lookup (`strtolower`-normalisiert speichern), da WP Headers teils + `x-wp-totalpages` vs. `X-WP-TotalPages` sendet. + +**Akzeptanzkriterium:** `$response->getHeader('x-wp-totalpages')` liefert eine Zahl als +String (z.B. `"3"`). + +**Risiko:** Client-Klassen werden auch an anderen Stellen instanziiert (theoretisch). +→ Mit `grep` verifizieren, dass `WCClient`/`WCResponse` nur in +`shopimporter_woocommerce.php` verwendet werden (keine Referenzen in +anderen `www/pages/*.php`-Dateien). + +### Schritt 3 — Timestamp-Persistenz + +**3a — Lesen:** +- In `getKonfig()` oder `CatchRemoteCommand('data')` den Wert + `$felder['letzter_import_timestamp']` auslesen. +- Fallback: `date('Y-m-d\TH:i:s', strtotime('-30 days'))` wenn leer/null. +- Auf Klassen-Property `$this->lastImportTimestamp` ablegen. + +**3b — Schreiben:** +- Nach erfolgreichem Lauf (am Ende von `ImportGetAuftrag`): den Timestamp der + **zuletzt verarbeiteten Bestellung** (nicht `now()`) in `einstellungen_json` zurueckschreiben. +- Grund: wenn der Lauf bei Order #n+3 abbricht, beim naechsten Lauf mit Order #n+3 + weiterarbeiten, nicht mit `now()` (→ Datenverlust). +- SQL: `UPDATE shopexport SET einstellungen_json = :json WHERE id = :id`. +- Muss ueber den in `DatabaseService` vorhandenen Mechanismus laufen (named params, + Prepared Statement — siehe `CLAUDE.md`-Projektregel). + +**3c — Atomic Update:** +- Nur bei erfolgreichem Import-Ende Timestamp persistieren. +- Bei Exception mitten im Lauf: Timestamp NICHT auf den Absturzpunkt schreiben + — besser: pro erfolgreich verarbeiteter Order einzeln fortschreiben (Progress), + sodass ein Absturz nur den aktuellen, nicht alle bisherigen, verliert. + +**Akzeptanzkriterium:** +``` +SELECT einstellungen_json FROM shopexport WHERE id = +→ enthaelt 'letzter_import_timestamp': '2026-04-20T12:34:56' +``` + +### Schritt 4 — Refactor `ImportGetAuftraegeAnzahl` (Count-Funktion) + +**Alt (Zeile 80–135):** +- Query: `orders?status=…&include=<800 IDs>&per_page=100`. +- Liest `count($response)` als Return. + +**Neu:** +- Query: `orders?status[]=&status[]=&after=&per_page=1`. +- Return: `(int) $response->getHeader('x-wp-total')`. +- `per_page=1` reicht — wir brauchen nur den Count-Header, nicht die Daten. + +**Akzeptanzkriterium:** +- Bei 0 neuen Orders: liefert 0. +- Bei 250 neuen Orders: liefert 250 (nicht 100). + +### Schritt 5 — Refactor `ImportGetAuftrag` (Import-Funktion) + +**Alt:** Query mit `include`-Liste, Iteration ueber bis zu 20 Orders, kein Cursor. + +**Neu — Single-Order-Pseudocode:** + +``` +# Fenster aufbauen +if last_order_ids is not empty: + after = last_ts - 1s + exclude = last_order_ids +else: + after = last_ts + exclude = (nicht gesetzt) + +# Fetch +order = client.get(orders, after=after, exclude=exclude, per_page=1, orderby=date, order=asc)[0] + +if response is empty: + return [] + +wcOrder = response[0] +order = parseOrder(wcOrder) + +# Persist +if order.date_created_gmt == last_ts: + last_order_ids = last_order_ids + [order.id] +else: + last_ts = order.date_created_gmt + last_order_ids = [order.id] + +persistLastImportCursor(order.date_created_gmt, order.id) + +return [{ id: order.auftrag, sessionid: '', logdatei: '', warenkorb: ... }] +``` + +**Wichtig:** +- `orderby=date` + `order=asc` garantiert, dass die **aelteste** neue Order zuerst kommt. + Dadurch kann der Progress-Timestamp monoton wachsen. +- `date_created_gmt` als Referenz (nicht `date_created` — Zeitzonen-Fallen vermeiden). +- Volume-Handling liegt beim Caller (shopimport.php), nicht im Importer. + +**Akzeptanzkriterium:** +- Pro Call: exakt 1 Order zurueck (oder leer). +- Bei >100 neuen Orders: Caller-Schleife (maxmanuell-gekappt) iteriert bis Ende. + +### Schritt 5a — Caller-Kontrakt + +Der Caller in shopimport.php:1304-1306 ruft pro Order-Import-Iteration: +- `ImportGetAuftraegeAnzahl()` einmal fuer Count (mit maxmanuell-Cap) +- `ImportGetAuftrag()` in for-Schleife, verarbeitet `$result[0]` + +Der Importer muss diesen Kontrakt einhalten: pro Call max. 1 Order. +Der after-Filter sorgt dafuer, dass jede Iteration die naechste Order +holt. Ein Crash zwischen `RemoteGetAuftrag()` und `shopimport_auftraege`- +Insert verliert max. diese eine Order. + +### Schritt 5b — Migration-Helper + +Um die ab_nummer → timestamp Migration auch im Count-Pfad auszufuehren +(shopimport.php ruft ImportGetAuftraegeAnzahl() BEFORE ImportGetAuftrag()): + +- Extraktion in eine private Methode `migrateAbNummerIfNeeded()`. +- Aufruf am Anfang von beiden ImportGetAuftraegeAnzahl() und ImportGetAuftrag(). +- Idempotent durch `$lastImportTimestampIsFallback`-Check — nach einmaliger Migration + keine weiteren Reads. + +### Schritt 6 — Transitions-Kompatibilitaet `ab_nummer` + +**Ist-Zustand:** `CatchRemoteCommand('data')` liefert u.a. `ab_nummer` — die naechste +Bestell-Nummer, ab der gelesen werden soll (Legacy-Cursor). + +**Uebergangsregel:** +- Wenn `letzter_import_timestamp` gesetzt → `after`-Filter nutzen, `ab_nummer` ignorieren. +- Wenn `letzter_import_timestamp` leer aber `ab_nummer` > 0 → einmalig `ab_nummer` in + einen Timestamp uebersetzen (Query `GET orders/{ab_nummer}` → `date_created_gmt` lesen), + als `letzter_import_timestamp` persistieren, ab dann `after`-Logik. +- Wenn beides leer → 30-Tage-Fallback (Schritt 3a). + +**Akzeptanzkriterium:** Shop, der bisher mit `ab_nummer` lief, importiert nach +Update **keine Duplikate** und **keine Luecken**. + +### Schritt 7 — `include`-Hack entfernen + +- Loeschen: + - Zeile ~99–113 (Count-Pfad `include`-Aufbau) + - Zeile ~153–167 (Import-Pfad `include`-Aufbau) + - Die beiden selbstkritischen Code-Kommentare *"fake"-Filter* +- Keine Ersatz-Struktur — `after` uebernimmt den Job komplett. + +### Schritt 8 — Cleanup & Commits + +**Pre-Commit-Checks:** +- `php -l www/pages/shopimporter_woocommerce.php` +- Trailing Whitespace raus (Subagent-Reste) +- CRLF-Warnungen ignorieren (autocrlf-Artefakte per Projekt-Regel) + +**Commit-Struktur (atomar, auf `fix/woocommerce-order-import-pagination`):** + +1. `fix(woocommerce): expose response headers from inline WC client` + → nur Client-Aenderung (bedingt; entfaellt wenn Schritt 1 Headers schon freigibt) + +2. `fix(woocommerce): persist last-import timestamp in shopexport config` + → Timestamp-Read/Write + 30-Tage-Fallback + Progress-Update pro Order + +3. `fix(woocommerce): use after-filter and pagination for order import` + → Kernaenderung an `ImportGetAuftrag` und `ImportGetAuftraegeAnzahl` + +4. `refactor(woocommerce): remove 800-id include hack` + → Aufraeumen toter Code + Kommentare + +5. `docs: add plan for woocommerce pagination fix` + → Diese Datei (`docs/plans/woocommerce-pagination-fix.md`) + +**PR-Ziel:** `openxe-org/openxe:master` (Upstream hat kein `development`). +**PR-Body:** Verweis auf Issue #262 + knappe Zusammenfassung der 4 Commits. + +## 6. Test-Plan (Integration gegen `192.168.0.143`) + +Keine Unit-Tests (OpenXE hat keine Test-Suite fuer Shopimporter). +Manuelle Integrationstests mit seeded Test-Orders. + +### Setup +- WP-Backend: `admin:password` +- WC-REST: `consumer_key`/`consumer_secret` generieren (Admin → WooCommerce → Einstellungen → Erweitert → REST-API) +- OpenXE-Shop-Konfiguration: bestehende Testinstanz wiederverwenden + +### Test-Matrix + +| # | Szenario | Start-State | Aktion | Erwartung | +|---|---|---|---|---| +| T1 | Frischinstall, keine Orders | `letzter_import_timestamp` leer | Lauf starten | 0 Orders, Timestamp bleibt leer | +| T2 | Frischinstall, 10 Orders <30 Tage alt | `letzter_import_timestamp` leer | Lauf starten | 10 Orders importiert, Timestamp = neueste Order | +| T3 | Frischinstall, 10 Orders >30 Tage alt | `letzter_import_timestamp` leer | Lauf starten | 0 Orders (Fallback-Schwelle), Timestamp leer | +| T4 | Standardlauf, 30 neue Orders | Timestamp gesetzt | Lauf starten | 30 Orders, Timestamp fortgeschrieben | +| T5 | Spike, 150 neue Orders | Timestamp gesetzt | Lauf starten | 150 Orders, Caller-Schleife durch maxmanuell auf 100 gekappt; Folgelauf holt Rest | +| T6 | Rueckstand, >100 neue Orders | Timestamp gesetzt | Lauf 1 starten | 100 Orders (maxmanuell-Cap), Cursor fortgeschrieben | +| T6b | Rueckstand Teil 2 | nach T6 | Lauf 2 starten | restliche Orders bis maxmanuell-Cap, Timestamp final | +| T7 | Transition, Shop mit `ab_nummer`=12345, Timestamp leer | `ab_nummer=12345` | Lauf starten | `ab_nummer` in Timestamp uebersetzt, keine Duplikate | +| T8 | Abbruch mitten im Lauf | Exception nach Fetch, vor INSERT | neuer Lauf | bei Abbruch zwischen Fetch und Insert max. 1 Order verloren; ab naechster Order fortgesetzt | +| T9 | Idempotenz | T4 erfolgreich gelaufen | T4 nochmal starten | 0 neue Orders, keine Duplikate | +| T10 | URL-Laenge (Regression) | 800 alte Orders existieren | Lauf starten | URL bleibt kurz (<2 KB), keine `include`-Liste mehr | + +### Mess-Artefakte (vor/nach) + +- Anzahl HTTP-Requests pro Lauf (via Apache-Log auf Testinstanz) +- Laenge der URL im `GET /orders`-Request +- Laufzeit pro Lauf +- Anzahl importierte Orders pro Lauf + +## 7. Rollout & Rueckwaerts-Kompatibilitaet + +### Migration beim Update +- Keine DB-Migration noetig. +- Beim ersten Lauf nach Update: + - Wenn `ab_nummer` gesetzt → einmalige Uebersetzung (Schritt 6). + - Wenn nichts gesetzt → 30-Tage-Fallback. +- Kein Breaking Change fuer bestehende Shops. + +### Rollback-Szenario +- Rueckkehr zum alten Verhalten: Revert der 4 Commits aus Schritt 8. +- `letzter_import_timestamp` in `einstellungen_json` stoert alte Version nicht + (unbekannte Keys werden ignoriert). + +### Kompatibilitaet mit anderen Modulen +- `class.remote.php` → nicht beruehrt. +- Andere `shopimporter_*`-Module → nicht beruehrt (nur WooCommerce-spezifisch). +- `class.erpapi.php` → nicht beruehrt. + +## 8. Risiken & Mitigations + +| Risiko | Wahrscheinlichkeit | Auswirkung | Mitigation | +|---|---|---|---| +| Inline-Client liefert Headers nicht durch | mittel | hoch (blockiert Schritt 4/5) | Schritt 1 als Entscheidungs-Gate, Schritt 2 bedingter Vorarbeits-Schritt | +| Shop-Timezone vs. `after`-Zeitzone | mittel | mittel (Off-by-one-Tag) | `date_created_gmt` und `after` beide in UTC, explizit testen (T4, T9) | +| Kunden ohne Timestamp + >30 Tage alte neue Orders | niedrig | mittel | Dokumentiert in PR-Body, UI-Override in Folge-PR | +| `orderby=date` Ordering nicht deterministisch bei gleichem Timestamp | niedrig | niedrig | Zusaetzlich `orderby=date&order=asc` und WC liefert stabile Sekundaersortierung nach ID | +| Andere Shopimporter erben Basisklasse-Struktur | niedrig | niedrig | Nur `shopimporter_woocommerce.php` anfassen, `ShopimporterBase` nicht anruehren | +| URL-Laengenlimits der WAF beim `status[]`-Array | sehr niedrig | niedrig | Typisch 2-3 Status-Werte, URL bleibt kurz | +| Caller-Kontrakt-Abweichung (1 vs. n Orders) | niedrig | hoch | Per Design Single-Order-Return; Caller-Loop fuer Volume-Handling | +| Timestamp-Kollision bei identischem date_created_gmt | niedrig | mittel | Adressiert durch Tupel-Cursor + Bucket-Akkumulation (letzter_import_order_ids sammelt alle IDs des Buckets) | +| Unbounded exclude-Liste bei grossem Bucket | niedrig | niedrig | In der Praxis kleine Listen; URL < 2 KB bei ~100 IDs | + +## 9. Definition of Done + +- [ ] Schritt 1 abgeschlossen, Entscheidung fuer/gegen Schritt 2 dokumentiert im PR-Body +- [ ] Alle geplanten Commits auf `fix/woocommerce-order-import-pagination` +- [ ] `php -l` ohne Fehler +- [ ] Testmatrix T1–T10 auf `192.168.0.143` durchgelaufen, Ergebnisse im PR-Body +- [ ] Issue #262 im PR referenziert (`Fixes #262`) +- [ ] PR gegen `openxe-org/openxe:master` eroeffnet +- [ ] Diese Plan-Datei mitversioniert im Fix-Branch + +## 10. Referenzen + +- Issue: https://github.com/OpenXE-org/OpenXE/issues/262 +- WC REST API Docs (Orders): https://woocommerce.github.io/woocommerce-rest-api-docs/#orders +- WP-REST-API Pagination: https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/ +- Parallele Issues: + - #239 Webhook Support (push statt poll) + - #224 JSON-Error in `getKonfig` diff --git a/vendor/autoload.php b/vendor/autoload.php index 76bb3835a..3d848fa8f 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -14,10 +14,7 @@ echo $err; } } - trigger_error( - $err, - E_USER_ERROR - ); + throw new RuntimeException($err); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/vendor/automattic/woocommerce/LICENSE b/vendor/automattic/woocommerce/LICENSE new file mode 100644 index 000000000..7e67c6ccb --- /dev/null +++ b/vendor/automattic/woocommerce/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016, Automattic (https://automattic.com/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/automattic/woocommerce/README.md b/vendor/automattic/woocommerce/README.md new file mode 100644 index 000000000..2ddc4cfa7 --- /dev/null +++ b/vendor/automattic/woocommerce/README.md @@ -0,0 +1,169 @@ +# WooCommerce API - PHP Client + +A PHP wrapper for the WooCommerce REST API. Easily interact with the WooCommerce REST API securely using this library. If using a HTTPS connection this library uses BasicAuth, else it uses Oauth to provide a secure connection to WooCommerce. + +[![CI status](https://github.com/woocommerce/wc-api-php/actions/workflows/ci.yml/badge.svg?branch=trunk)](https://github.com/woocommerce/wc-api-php/actions/workflows/ci.yml) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/woocommerce/wc-api-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/woocommerce/wc-api-php/?branch=master) +[![PHP version](https://badge.fury.io/ph/automattic%2Fwoocommerce.svg)](https://packagist.org/packages/automattic/woocommerce) + +## Installation + +``` +composer require automattic/woocommerce +``` + +## Getting started + +Generate API credentials (Consumer Key & Consumer Secret) following this instructions +. + +Check out the WooCommerce API endpoints and data that can be manipulated in . + +## Setup + +Setup for the new WP REST API integration (WooCommerce 2.6 or later): + +```php +require __DIR__ . '/vendor/autoload.php'; + +use Automattic\WooCommerce\Client; + +$woocommerce = new Client( + 'http://example.com', + 'ck_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + 'cs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + [ + 'version' => 'wc/v3', + ] +); +``` + +## Client class + +```php +$woocommerce = new Client($url, $consumer_key, $consumer_secret, $options); +``` + +### Options + +| Option | Type | Required | Description | +| ----------------- | -------- | -------- | ------------------------------------------ | +| `url` | `string` | yes | Your Store URL, example: http://woo.dev/ | +| `consumer_key` | `string` | yes | Your API consumer key | +| `consumer_secret` | `string` | yes | Your API consumer secret | +| `options` | `array` | no | Extra arguments (see client options table) | + +#### Client options + +| Option | Type | Required | Description | +| ------------------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `version` | `string` | no | API version, default is `wc/v3` | +| `timeout` | `int` | no | Request timeout, default is `15` | +| `verify_ssl` | `bool` | no | Verify SSL when connect, use this option as `false` when need to test with self-signed certificates, default is `true` | +| `follow_redirects` | `bool` | no | Allow the API call to follow redirects | +| `query_string_auth` | `bool` | no | Force Basic Authentication as query string when `true` and using under HTTPS, default is `false` | +| `oauth_timestamp` | `string` | no | Custom oAuth timestamp, default is `time()` | +| `oauth_only` | `bool` | no | Only use oauth for requests, it will disable Basic Auth, default is `false` | +| `user_agent` | `string` | no | Custom user-agent, default is `WooCommerce API Client-PHP` | +| `wp_api_prefix` | `string` | no | Custom WP REST API URL prefix, used to support custom prefixes created with the `rest_url_prefix` filter | +| `wp_api` | `bool` | no | Set to `false` in order to use the legacy WooCommerce REST API (deprecated and not recommended) | +| `method_override_query` | `bool` | no | If true will mask all non-GET/POST methods by using POST method with added query parameter `?_method=METHOD` into URL | +| `method_override_header` | `bool` | no | If true will mask all non-GET/POST methods (PUT/DELETE/etc.) by using POST method with added `X-HTTP-Method-Override: METHOD` HTTP header into request | + +## Client methods + +### GET + +```php +$woocommerce->get($endpoint, $parameters = []); +``` + +### POST + +```php +$woocommerce->post($endpoint, $data); +``` + +### PUT + +```php +$woocommerce->put($endpoint, $data); +``` + +### DELETE + +```php +$woocommerce->delete($endpoint, $parameters = []); +``` + +### OPTIONS + +```php +$woocommerce->options($endpoint); +``` + +#### Arguments + +| Params | Type | Description | +| ------------ | -------- | ------------------------------------------------------------ | +| `endpoint` | `string` | WooCommerce API endpoint, example: `customers` or `orders/12` | +| `data` | `array` | Only for POST and PUT, data that will be converted to JSON | +| `parameters` | `array` | Only for GET and DELETE, request query string | + +#### Response + +All methods will return arrays on success or throwing `HttpClientException` errors on failure. + +```php +use Automattic\WooCommerce\HttpClient\HttpClientException; + +try { + // Array of response results. + $results = $woocommerce->get('customers'); + // Example: ['customers' => [[ 'id' => 8, 'created_at' => '2015-05-06T17:43:51Z', 'email' => ... + echo '
' . print_r($results, true) . '
'; // JSON output.
+
+  // Last request data.
+  $lastRequest = $woocommerce->http->getRequest();
+  echo '
' . print_r($lastRequest->getUrl(), true) . '
'; // Requested URL (string).
+  echo '
' .
+    print_r($lastRequest->getMethod(), true) .
+    '
'; // Request method (string).
+  echo '
' .
+    print_r($lastRequest->getParameters(), true) .
+    '
'; // Request parameters (array).
+  echo '
' .
+    print_r($lastRequest->getHeaders(), true) .
+    '
'; // Request headers (array).
+  echo '
' . print_r($lastRequest->getBody(), true) . '
'; // Request body (JSON).
+
+  // Last response data.
+  $lastResponse = $woocommerce->http->getResponse();
+  echo '
' . print_r($lastResponse->getCode(), true) . '
'; // Response code (int).
+  echo '
' .
+    print_r($lastResponse->getHeaders(), true) .
+    '
'; // Response headers (array).
+  echo '
' . print_r($lastResponse->getBody(), true) . '
'; // Response body (JSON).
+} catch (HttpClientException $e) {
+  echo '
' . print_r($e->getMessage(), true) . '
'; // Error message.
+  echo '
' . print_r($e->getRequest(), true) . '
'; // Last request data.
+  echo '
' . print_r($e->getResponse(), true) . '
'; // Last response data.
+}
+```
+
+## Release History
+
+- 2022-03-18 - 3.1.0 - Added new options to support `_method` and `X-HTTP-Method-Override` from WP, supports 7+, dropped support to PHP 5.
+- 2019-01-16 - 3.0.0 - Legacy API turned off by default, and improved JSON error handler.
+- 2018-03-29 - 2.0.1 - Fixed fatal errors on `lookForErrors`.
+- 2018-01-12 - 2.0.0 - Responses changes from arrays to `stdClass` objects. Added `follow_redirects` option.
+- 2017-06-06 - 1.3.0 - Remove BOM before decoding and added support for multi-dimensional arrays for oAuth1.0a.
+- 2017-03-15 - 1.2.0 - Added `user_agent` option.
+- 2016-12-14 - 1.1.4 - Fixed WordPress 4.7 compatibility.
+- 2016-10-26 - 1.1.3 - Allow set `oauth_timestamp` and improved how is handled the response headers.
+- 2016-09-30 - 1.1.2 - Added `wp_api_prefix` option to allow custom WP REST API URL prefix.
+- 2016-05-10 - 1.1.1 - Fixed oAuth and error handler for WP REST API.
+- 2016-05-09 - 1.1.0 - Added support for WP REST API, added method `Automattic\WooCommerce\Client::options` and fixed multiple headers responses.
+- 2016-01-25 - 1.0.2 - Fixed an error when getting data containing non-latin characters.
+- 2016-01-21 - 1.0.1 - Sort all oAuth parameters before build request URLs.
+- 2016-01-11 - 1.0.0 - Stable release.
diff --git a/vendor/automattic/woocommerce/composer.json b/vendor/automattic/woocommerce/composer.json
new file mode 100644
index 000000000..5390aec54
--- /dev/null
+++ b/vendor/automattic/woocommerce/composer.json
@@ -0,0 +1,42 @@
+{
+  "name": "automattic/woocommerce",
+  "description": "A PHP wrapper for the WooCommerce REST API",
+  "type": "library",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "Claudio Sanches",
+      "email": "claudio.sanches@automattic.com"
+    }
+  ],
+  "minimum-stability": "dev",
+  "keywords": [
+    "API",
+    "WooCommerce"
+  ],
+  "require": {
+    "php": ">= 7.4.0",
+    "ext-curl": "*",
+    "ext-json": "*"
+  },
+  "require-dev": {
+    "phpunit/phpunit": "^9.5.0",
+    "squizlabs/php_codesniffer": "3.*",
+    "doctrine/instantiator": "^1.5.0"
+  },
+  "scripts": {
+    "phpcs": "vendor/bin/phpcs --colors --standard=phpcs.xml",
+    "phpcbf": "vendor/bin/phpcbf --colors --standard=phpcs.xml"
+  },
+  "autoload": {
+    "psr-4": {
+      "Automattic\\WooCommerce\\": ["src/WooCommerce"]
+    }
+  },
+  "autoload-dev": {
+    "psr-4": { 
+      "Automattic\\WooCommerce\\LegacyTests\\": "tests/legacy-php/WooCommerce/Tests",
+      "Automattic\\WooCommerce\\Tests\\": "tests/php/WooCommerce/Tests"
+    }
+  }
+}
\ No newline at end of file
diff --git a/vendor/automattic/woocommerce/composer.lock b/vendor/automattic/woocommerce/composer.lock
new file mode 100644
index 000000000..83633c08f
--- /dev/null
+++ b/vendor/automattic/woocommerce/composer.lock
@@ -0,0 +1,1897 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "8410ab16c98c12d7b5806814e8b21d24",
+    "packages": [],
+    "packages-dev": [
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.5.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "12be2483e1f0e850b353e26869e4e6c038459501"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/12be2483e1f0e850b353e26869e4e6c038459501",
+                "reference": "12be2483e1f0e850b353e26869e4e6c038459501",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^9 || ^12",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpbench/phpbench": "^0.16 || ^1",
+                "phpstan/phpstan": "^1.4",
+                "phpstan/phpstan-phpunit": "^1",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
+                "vimeo/psalm": "^4.30 || ^5.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com",
+                    "homepage": "https://ocramius.github.io/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "support": {
+                "issues": "https://github.com/doctrine/instantiator/issues",
+                "source": "https://github.com/doctrine/instantiator/tree/1.5.x"
+            },
+            "funding": [
+                {
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://www.patreon.com/phpdoctrine",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-12-09T14:16:53+00:00"
+        },
+        {
+            "name": "myclabs/deep-copy",
+            "version": "1.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/DeepCopy.git",
+                "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+                "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "conflict": {
+                "doctrine/collections": "<1.6.8",
+                "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+            },
+            "require-dev": {
+                "doctrine/collections": "^1.6.8",
+                "doctrine/common": "^2.13.3 || ^3.2.2",
+                "phpspec/prophecy": "^1.10",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+            },
+            "default-branch": true,
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ],
+                "psr-4": {
+                    "DeepCopy\\": "src/DeepCopy/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "support": {
+                "issues": "https://github.com/myclabs/DeepCopy/issues",
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+            },
+            "funding": [
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-08-01T08:46:24+00:00"
+        },
+        {
+            "name": "nikic/php-parser",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nikic/PHP-Parser.git",
+                "reference": "0da2d6679a3df45d6d720aa2e0d4568f82a32e46"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0da2d6679a3df45d6d720aa2e0d4568f82a32e46",
+                "reference": "0da2d6679a3df45d6d720aa2e0d4568f82a32e46",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-json": "*",
+                "ext-tokenizer": "*",
+                "php": ">=7.4"
+            },
+            "require-dev": {
+                "ircmaxell/php-yacc": "^0.0.7",
+                "phpunit/phpunit": "^9.0"
+            },
+            "default-branch": true,
+            "bin": [
+                "bin/php-parse"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpParser\\": "lib/PhpParser"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Nikita Popov"
+                }
+            ],
+            "description": "A PHP parser written in PHP",
+            "keywords": [
+                "parser",
+                "php"
+            ],
+            "support": {
+                "issues": "https://github.com/nikic/PHP-Parser/issues",
+                "source": "https://github.com/nikic/PHP-Parser/tree/master"
+            },
+            "time": "2025-09-08T07:54:25+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git",
+                "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/65f90285728eae4eae313b8b6ba11b2f5436038e",
+                "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-libxml": "*",
+                "ext-phar": "*",
+                "ext-xmlwriter": "*",
+                "phar-io/version": "^3.0.1",
+                "php": "^7.2 || ^8.0"
+            },
+            "default-branch": true,
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+            "support": {
+                "issues": "https://github.com/phar-io/manifest/issues",
+                "source": "https://github.com/phar-io/manifest/tree/master"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2025-07-05T08:48:25+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "3.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git",
+                "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+                "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and constraints",
+            "support": {
+                "issues": "https://github.com/phar-io/version/issues",
+                "source": "https://github.com/phar-io/version/tree/3.2.1"
+            },
+            "time": "2022-02-21T01:04:05+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "9.2.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+                "reference": "0448d60087a382392a1b2a1abe434466e03dcc87"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0448d60087a382392a1b2a1abe434466e03dcc87",
+                "reference": "0448d60087a382392a1b2a1abe434466e03dcc87",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-libxml": "*",
+                "ext-xmlwriter": "*",
+                "nikic/php-parser": "^4.19.1 || ^5.1.0",
+                "php": ">=7.3",
+                "phpunit/php-file-iterator": "^3.0.6",
+                "phpunit/php-text-template": "^2.0.4",
+                "sebastian/code-unit-reverse-lookup": "^2.0.3",
+                "sebastian/complexity": "^2.0.3",
+                "sebastian/environment": "^5.1.5",
+                "sebastian/lines-of-code": "^1.0.4",
+                "sebastian/version": "^3.0.2",
+                "theseer/tokenizer": "^1.2.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.6"
+            },
+            "suggest": {
+                "ext-pcov": "PHP extension that provides line coverage",
+                "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "9.2.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+                "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-10-31T05:58:25+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "3.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+                "reference": "38b24367e1b340aa78b96d7cab042942d917bb84"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84",
+                "reference": "38b24367e1b340aa78b96d7cab042942d917bb84",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2022-02-11T16:23:04+00:00"
+        },
+        {
+            "name": "phpunit/php-invoker",
+            "version": "3.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-invoker.git",
+                "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+                "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "ext-pcntl": "*",
+                "phpunit/phpunit": "^9.3"
+            },
+            "suggest": {
+                "ext-pcntl": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Invoke callables with a timeout",
+            "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+            "keywords": [
+                "process"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+                "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-09-28T05:58:55+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-text-template.git",
+                "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+                "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+            "keywords": [
+                "template"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+                "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T05:33:50+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "5.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git",
+                "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+                "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/",
+            "keywords": [
+                "timer"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+                "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T13:16:10+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "9.6.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git",
+                "reference": "996021ad5437394004bf3aeb6ae52a9620f47157"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/996021ad5437394004bf3aeb6ae52a9620f47157",
+                "reference": "996021ad5437394004bf3aeb6ae52a9620f47157",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.5.0 || ^2",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-xml": "*",
+                "ext-xmlwriter": "*",
+                "myclabs/deep-copy": "^1.13.4",
+                "phar-io/manifest": "^2.0.4",
+                "phar-io/version": "^3.2.1",
+                "php": ">=7.3",
+                "phpunit/php-code-coverage": "^9.2.32",
+                "phpunit/php-file-iterator": "^3.0.6",
+                "phpunit/php-invoker": "^3.1.1",
+                "phpunit/php-text-template": "^2.0.4",
+                "phpunit/php-timer": "^5.0.3",
+                "sebastian/cli-parser": "^1.0.2",
+                "sebastian/code-unit": "^1.0.8",
+                "sebastian/comparator": "^4.0.9",
+                "sebastian/diff": "^4.0.6",
+                "sebastian/environment": "^5.1.5",
+                "sebastian/exporter": "^4.0.6",
+                "sebastian/global-state": "^5.0.8",
+                "sebastian/object-enumerator": "^4.0.4",
+                "sebastian/resource-operations": "^3.0.4",
+                "sebastian/type": "^3.2.1",
+                "sebastian/version": "^3.0.2"
+            },
+            "suggest": {
+                "ext-soap": "To be able to generate mocks based on WSDL files",
+                "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "9.6-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/Framework/Assert/Functions.php"
+                ],
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/",
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+                "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6"
+            },
+            "funding": [
+                {
+                    "url": "https://phpunit.de/sponsors.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-09-12T05:34:54+00:00"
+        },
+        {
+            "name": "sebastian/cli-parser",
+            "version": "1.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/cli-parser.git",
+                "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+                "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for parsing CLI options",
+            "homepage": "https://github.com/sebastianbergmann/cli-parser",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+                "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-02T06:27:43+00:00"
+        },
+        {
+            "name": "sebastian/code-unit",
+            "version": "1.0.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/code-unit.git",
+                "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+                "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the PHP code units",
+            "homepage": "https://github.com/sebastianbergmann/code-unit",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+                "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T13:08:54+00:00"
+        },
+        {
+            "name": "sebastian/code-unit-reverse-lookup",
+            "version": "2.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+                "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+                "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Looks up which function or method a line of code belongs to",
+            "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-09-28T05:30:19+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "4.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git",
+                "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
+                "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3",
+                "sebastian/diff": "^4.0",
+                "sebastian/exporter": "^4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@2bepublished.at"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values for equality",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/comparator/issues",
+                "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-08-10T06:51:50+00:00"
+        },
+        {
+            "name": "sebastian/complexity",
+            "version": "2.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/complexity.git",
+                "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+                "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+                "shasum": ""
+            },
+            "require": {
+                "nikic/php-parser": "^4.18 || ^5.0",
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for calculating the complexity of PHP code units",
+            "homepage": "https://github.com/sebastianbergmann/complexity",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/complexity/issues",
+                "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-12-22T06:19:30+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "4.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+                "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3",
+                "symfony/process": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-02T06:30:58+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "5.1.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git",
+                "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+                "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "suggest": {
+                "ext-posix": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/environment/issues",
+                "source": "https://github.com/sebastianbergmann/environment/tree/5.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-02-03T06:03:51+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "4.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git",
+                "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+                "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3",
+                "sebastian/recursion-context": "^4.0"
+            },
+            "require-dev": {
+                "ext-mbstring": "*",
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables for visualization",
+            "homepage": "https://www.github.com/sebastianbergmann/exporter",
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/exporter/issues",
+                "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-02T06:33:00+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "5.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git",
+                "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+                "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3",
+                "sebastian/object-reflector": "^2.0",
+                "sebastian/recursion-context": "^4.0"
+            },
+            "require-dev": {
+                "ext-dom": "*",
+                "phpunit/phpunit": "^9.3"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "keywords": [
+                "global state"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/global-state/issues",
+                "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-08-10T07:10:35+00:00"
+        },
+        {
+            "name": "sebastian/lines-of-code",
+            "version": "1.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+                "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+                "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+                "shasum": ""
+            },
+            "require": {
+                "nikic/php-parser": "^4.18 || ^5.0",
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library for counting the lines of code in PHP source code",
+            "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+                "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-12-22T06:20:34+00:00"
+        },
+        {
+            "name": "sebastian/object-enumerator",
+            "version": "4.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+                "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+                "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3",
+                "sebastian/object-reflector": "^2.0",
+                "sebastian/recursion-context": "^4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+            "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T13:12:34+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-reflector.git",
+                "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+                "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including inherited and non-public ones",
+            "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+                "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T13:14:26+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "4.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/recursion-context.git",
+                "reference": "539c6691e0623af6dc6f9c20384c120f963465a0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0",
+                "reference": "539c6691e0623af6dc6f9c20384c120f963465a0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP variables",
+            "homepage": "https://github.com/sebastianbergmann/recursion-context",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                },
+                {
+                    "url": "https://liberapay.com/sebastianbergmann",
+                    "type": "liberapay"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/sebastianbergmann",
+                    "type": "thanks_dev"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-08-10T06:57:39+00:00"
+        },
+        {
+            "name": "sebastian/resource-operations",
+            "version": "dev-main",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/resource-operations.git",
+                "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25",
+                "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.0"
+            },
+            "default-branch": true,
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides a list of PHP built-in functions that operate on resources",
+            "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+            "support": {
+                "source": "https://github.com/sebastianbergmann/resource-operations/tree/main"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-14T18:47:08+00:00"
+        },
+        {
+            "name": "sebastian/type",
+            "version": "3.2.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/type.git",
+                "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+                "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Collection of value objects that represent the types of the PHP type system",
+            "homepage": "https://github.com/sebastianbergmann/type",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/type/issues",
+                "source": "https://github.com/sebastianbergmann/type/tree/3.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-02-03T06:13:03+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "3.0.x-dev",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git",
+                "reference": "c6c1022351a901512170118436c764e473f6de8c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+                "reference": "c6c1022351a901512170118436c764e473f6de8c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/version/issues",
+                "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-09-28T06:39:44+00:00"
+        },
+        {
+            "name": "squizlabs/php_codesniffer",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+                "reference": "fcfefc635209c324eb6ed47280d4cc5d245c7cd6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/fcfefc635209c324eb6ed47280d4cc5d245c7cd6",
+                "reference": "fcfefc635209c324eb6ed47280d4cc5d245c7cd6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-simplexml": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+            },
+            "default-branch": true,
+            "bin": [
+                "bin/phpcbf",
+                "bin/phpcs"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Greg Sherwood",
+                    "role": "Former lead"
+                },
+                {
+                    "name": "Juliette Reinders Folmer",
+                    "role": "Current lead"
+                },
+                {
+                    "name": "Contributors",
+                    "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+                }
+            ],
+            "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+            "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+            "keywords": [
+                "phpcs",
+                "standards",
+                "static analysis"
+            ],
+            "support": {
+                "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+                "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+                "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+                "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/PHPCSStandards",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/jrfnl",
+                    "type": "github"
+                },
+                {
+                    "url": "https://opencollective.com/php_codesniffer",
+                    "type": "open_collective"
+                },
+                {
+                    "url": "https://thanks.dev/u/gh/phpcsstandards",
+                    "type": "thanks_dev"
+                }
+            ],
+            "time": "2025-09-11T05:53:07+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git",
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+                "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues",
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2024-03-03T12:36:25+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "dev",
+    "stability-flags": {},
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">= 7.4.0",
+        "ext-curl": "*",
+        "ext-json": "*"
+    },
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/Client.php b/vendor/automattic/woocommerce/src/WooCommerce/Client.php
new file mode 100644
index 000000000..2bceda31d
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/Client.php
@@ -0,0 +1,109 @@
+http = new HttpClient($url, $consumerKey, $consumerSecret, $options);
+    }
+
+    /**
+     * POST method.
+     *
+     * @param string $endpoint API endpoint.
+     * @param array  $data     Request data.
+     *
+     * @return \stdClass
+     */
+    public function post($endpoint, $data)
+    {
+        return $this->http->request($endpoint, 'POST', $data);
+    }
+
+    /**
+     * PUT method.
+     *
+     * @param string $endpoint API endpoint.
+     * @param array  $data     Request data.
+     *
+     * @return \stdClass
+     */
+    public function put($endpoint, $data)
+    {
+        return $this->http->request($endpoint, 'PUT', $data);
+    }
+
+    /**
+     * GET method.
+     *
+     * @param string $endpoint   API endpoint.
+     * @param array  $parameters Request parameters.
+     *
+     * @return \stdClass|array
+     */
+    public function get($endpoint, $parameters = [])
+    {
+        return $this->http->request($endpoint, 'GET', [], $parameters);
+    }
+
+    /**
+     * DELETE method.
+     *
+     * @param string $endpoint   API endpoint.
+     * @param array  $parameters Request parameters.
+     *
+     * @return \stdClass
+     */
+    public function delete($endpoint, $parameters = [])
+    {
+        return $this->http->request($endpoint, 'DELETE', [], $parameters);
+    }
+
+    /**
+     * OPTIONS method.
+     *
+     * @param string $endpoint API endpoint.
+     *
+     * @return \stdClass
+     */
+    public function options($endpoint)
+    {
+        return $this->http->request($endpoint, 'OPTIONS', [], []);
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/BasicAuth.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/BasicAuth.php
new file mode 100644
index 000000000..b63916812
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/BasicAuth.php
@@ -0,0 +1,96 @@
+ch             = $ch;
+        $this->consumerKey    = $consumerKey;
+        $this->consumerSecret = $consumerSecret;
+        $this->doQueryString  = $doQueryString;
+        $this->parameters     = $parameters;
+
+        $this->processAuth();
+    }
+
+    /**
+     * Process auth.
+     */
+    protected function processAuth()
+    {
+        if ($this->doQueryString) {
+            $this->parameters['consumer_key']    = $this->consumerKey;
+            $this->parameters['consumer_secret'] = $this->consumerSecret;
+        } else {
+            \curl_setopt($this->ch, CURLOPT_USERPWD, $this->consumerKey . ':' . $this->consumerSecret);
+        }
+    }
+
+    /**
+     * Get parameters.
+     *
+     * @return array
+     */
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClient.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClient.php
new file mode 100644
index 000000000..2180262de
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClient.php
@@ -0,0 +1,490 @@
+options        = new Options($options);
+        $this->url            = $this->buildApiUrl($url);
+        $this->consumerKey    = $consumerKey;
+        $this->consumerSecret = $consumerSecret;
+    }
+
+    /**
+     * Check if is under SSL.
+     *
+     * @return bool
+     */
+    protected function isSsl()
+    {
+        return 'https://' === \substr($this->url, 0, 8);
+    }
+
+    /**
+     * Build API URL.
+     *
+     * @param string $url Store URL.
+     *
+     * @return string
+     */
+    protected function buildApiUrl($url)
+    {
+        $api = $this->options->isWPAPI() ? $this->options->apiPrefix() : '/wc-api/';
+
+        return \rtrim($url, '/') . $api . $this->options->getVersion() . '/';
+    }
+
+    /**
+     * Build URL.
+     *
+     * @param string $url        URL.
+     * @param array  $parameters Query string parameters.
+     *
+     * @return string
+     */
+    protected function buildUrlQuery($url, $parameters = [])
+    {
+        if (!empty($parameters)) {
+            if (false !== strpos($url, '?')) {
+                $url .= '&' . \http_build_query($parameters, '', '&');
+            } else {
+                $url .= '?' . \http_build_query($parameters, '', '&');
+            }
+        }
+
+        return $url;
+    }
+
+    /**
+     * Authenticate.
+     *
+     * @param string $url        Request URL.
+     * @param string $method     Request method.
+     * @param array  $parameters Request parameters.
+     *
+     * @return array
+     */
+    protected function authenticate($url, $method, $parameters = [])
+    {
+        // Setup authentication.
+        if (!$this->options->isOAuthOnly() && $this->isSsl()) {
+            $basicAuth = new BasicAuth(
+                $this->ch,
+                $this->consumerKey,
+                $this->consumerSecret,
+                $this->options->isQueryStringAuth(),
+                $parameters
+            );
+            $parameters = $basicAuth->getParameters();
+        } else {
+            $oAuth = new OAuth(
+                $url,
+                $this->consumerKey,
+                $this->consumerSecret,
+                $this->options->getVersion(),
+                $method,
+                $parameters,
+                $this->options->oauthTimestamp()
+            );
+            $parameters = $oAuth->getParameters();
+        }
+
+        return $parameters;
+    }
+
+    /**
+     * Setup method.
+     *
+     * @param string $method Request method.
+     */
+    protected function setupMethod($method)
+    {
+        if ('POST' == $method) {
+            \curl_setopt($this->ch, CURLOPT_POST, true);
+        } elseif (\in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) {
+            \curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method);
+        }
+    }
+
+    /**
+     * Get request headers.
+     *
+     * @param  bool $sendData If request send data or not.
+     *
+     * @return array
+     */
+    protected function getRequestHeaders($sendData = false)
+    {
+        $headers = [
+            'Accept'     => 'application/json',
+            'User-Agent' => $this->options->userAgent() . '/' . Client::VERSION,
+        ];
+
+        if ($sendData) {
+            $headers['Content-Type'] = 'application/json;charset=utf-8';
+        }
+
+        return $headers;
+    }
+
+    /**
+     * Create request.
+     *
+     * @param string $endpoint   Request endpoint.
+     * @param string $method     Request method.
+     * @param array  $data       Request data.
+     * @param array  $parameters Request parameters.
+     *
+     * @return Request
+     */
+    protected function createRequest($endpoint, $method, $data = [], $parameters = [])
+    {
+        $body    = '';
+        $url     = $this->url . $endpoint;
+        $hasData = !empty($data);
+        $headers = $this->getRequestHeaders($hasData);
+
+        // HTTP method override feature which masks PUT and DELETE HTTP methods as POST method with added
+        // ?_method=PUT query parameter and/or X-HTTP-Method-Override HTTP header.
+        if (!in_array($method, ['GET', 'POST'])) {
+            $usePostMethod = false;
+            if ($this->options->isMethodOverrideQuery()) {
+                $parameters = array_merge(['_method' => $method], $parameters);
+                $usePostMethod = true;
+            }
+            if ($this->options->isMethodOverrideHeader()) {
+                $headers['X-HTTP-Method-Override'] = $method;
+                $usePostMethod = true;
+            }
+            if ($usePostMethod) {
+                $method = 'POST';
+            }
+        }
+
+        // Setup authentication.
+        $parameters = $this->authenticate($url, $method, $parameters);
+
+        // Setup method.
+        $this->setupMethod($method);
+
+        // Include post fields.
+        if ($hasData) {
+            $body = \json_encode($data);
+            \curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body);
+        }
+
+        $this->request = new Request(
+            $this->buildUrlQuery($url, $parameters),
+            $method,
+            $parameters,
+            $headers,
+            $body
+        );
+
+        return $this->getRequest();
+    }
+
+    /**
+     * Get response headers.
+     *
+     * @return array
+     */
+    protected function getResponseHeaders()
+    {
+        $headers = [];
+        $lines   = \explode("\n", $this->responseHeaders);
+        $lines   = \array_filter($lines, 'trim');
+
+        foreach ($lines as $index => $line) {
+            // Remove HTTP/xxx params.
+            if (strpos($line, ': ') === false) {
+                continue;
+            }
+
+            list($key, $value) = \explode(': ', $line);
+
+            $headers[$key] = isset($headers[$key]) ? $headers[$key] . ', ' . trim($value) : trim($value);
+        }
+
+        return $headers;
+    }
+
+    /**
+     * Create response.
+     *
+     * @return Response
+     */
+    protected function createResponse()
+    {
+
+        // Set response headers.
+        $this->responseHeaders = '';
+        \curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) {
+            $this->responseHeaders .= $headers;
+            return \strlen($headers);
+        });
+
+        // Get response data.
+        $body    = \curl_exec($this->ch);
+        $code    = \curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
+        $headers = $this->getResponseHeaders();
+
+        // Register response.
+        $this->response = new Response($code, $headers, $body);
+
+        return $this->getResponse();
+    }
+
+    /**
+     * Set default cURL settings.
+     */
+    protected function setDefaultCurlSettings()
+    {
+        $verifySsl       = $this->options->verifySsl();
+        $timeout         = $this->options->getTimeout();
+        $followRedirects = $this->options->getFollowRedirects();
+
+        \curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl);
+        if (!$verifySsl) {
+            \curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl);
+        }
+        if ($followRedirects) {
+            \curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
+        }
+        \curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout);
+        \curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout);
+        \curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
+        \curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->request->getRawHeaders());
+        \curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl());
+
+        foreach ($this->customCurlOptions as $customCurlOptionKey => $customCurlOptionValue) {
+            \curl_setopt($this->ch, $customCurlOptionKey, $customCurlOptionValue);
+        }
+    }
+
+    /**
+     * Look for errors in the request.
+     *
+     * @param array $parsedResponse Parsed body response.
+     */
+    protected function lookForErrors($parsedResponse)
+    {
+        // Any non-200/201/202 response code indicates an error.
+        if (!\in_array($this->response->getCode(), ['200', '201', '202'])) {
+            $errors = isset($parsedResponse->errors) ? $parsedResponse->errors : $parsedResponse;
+            $errorMessage = '';
+            $errorCode = '';
+
+            if (is_array($errors) && $errors) {
+                $errorMessage = $errors[0]->message;
+                $errorCode    = $errors[0]->code;
+            } elseif (isset($errors->message, $errors->code)) {
+                $errorMessage = $errors->message;
+                $errorCode    = $errors->code;
+            }
+
+            throw new HttpClientException(
+                \sprintf('Error: %s [%s]', $errorMessage, $errorCode),
+                $this->response->getCode(),
+                $this->request,
+                $this->response
+            );
+        }
+    }
+
+    /**
+     * Process response.
+     *
+     * @return \stdClass|array
+     */
+    protected function processResponse()
+    {
+        $body = $this->response->getBody();
+
+        // Look for UTF-8 BOM and remove.
+        if (0 === strpos(bin2hex(substr($body, 0, 4)), 'efbbbf')) {
+            $body = substr($body, 3);
+        }
+
+        $parsedResponse = \json_decode($body);
+
+        // Test if return a valid JSON.
+        if (JSON_ERROR_NONE !== json_last_error()) {
+            $message = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Invalid JSON returned';
+            throw new HttpClientException(
+                sprintf('JSON ERROR: %s', $message),
+                $this->response->getCode(),
+                $this->request,
+                $this->response
+            );
+        }
+
+        $this->lookForErrors($parsedResponse);
+
+        return $parsedResponse;
+    }
+
+    /**
+     * Make requests.
+     *
+     * @param string $endpoint   Request endpoint.
+     * @param string $method     Request method.
+     * @param array  $data       Request data.
+     * @param array  $parameters Request parameters.
+     *
+     * @return \stdClass|array
+     */
+    public function request($endpoint, $method, $data = [], $parameters = [])
+    {
+        // Initialize cURL.
+        $this->ch = \curl_init();
+
+        // Set request args.
+        $request = $this->createRequest($endpoint, $method, $data, $parameters);
+
+        // Default cURL settings.
+        $this->setDefaultCurlSettings();
+
+        // Get response.
+        $response = $this->createResponse();
+
+        // Check for cURL errors.
+        if (\curl_errno($this->ch)) {
+            throw new HttpClientException('cURL Error: ' . \curl_error($this->ch), 0, $request, $response);
+        }
+
+        // we have to call curl_close only for PHP < 8
+        if (\is_resource($this->ch)) {
+            \curl_close($this->ch);
+        }
+
+        return $this->processResponse();
+    }
+
+    /**
+     * Get request data.
+     *
+     * @return Request
+     */
+    public function getRequest()
+    {
+        return $this->request;
+    }
+
+    /**
+     * Get response data.
+     *
+     * @return Response
+     */
+    public function getResponse()
+    {
+        return $this->response;
+    }
+
+    /**
+     * Set custom cURL options to use in requests.
+     *
+     * @param array $curlOptions
+     */
+    public function setCustomCurlOptions(array $curlOptions)
+    {
+        $this->customCurlOptions = $curlOptions;
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClientException.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClientException.php
new file mode 100644
index 000000000..bfd149fe6
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClientException.php
@@ -0,0 +1,71 @@
+request  = $request;
+        $this->response = $response;
+    }
+
+    /**
+     * Get request data.
+     *
+     * @return Request
+     */
+    public function getRequest()
+    {
+        return $this->request;
+    }
+
+    /**
+     * Get response data.
+     *
+     * @return Response
+     */
+    public function getResponse()
+    {
+        return $this->response;
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/OAuth.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/OAuth.php
new file mode 100644
index 000000000..5f3a4a13a
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/OAuth.php
@@ -0,0 +1,268 @@
+url            = $url;
+        $this->consumerKey    = $consumerKey;
+        $this->consumerSecret = $consumerSecret;
+        $this->apiVersion     = $apiVersion;
+        $this->method         = $method;
+        $this->parameters     = $parameters;
+        $this->timestamp      = $timestamp;
+    }
+
+    /**
+     * Encode according to RFC 3986.
+     *
+     * @param string|array $value Value to be normalized.
+     *
+     * @return string
+     */
+    protected function encode($value)
+    {
+        if (is_array($value)) {
+            return array_map([$this, 'encode'], $value);
+        } else {
+            return str_replace(['+', '%7E'], [' ', '~'], rawurlencode($value));
+        }
+    }
+
+    /**
+     * Normalize parameters.
+     *
+     * @param array $parameters Parameters to normalize.
+     *
+     * @return array
+     */
+    protected function normalizeParameters($parameters)
+    {
+        $normalized = [];
+
+        foreach ($parameters as $key => $value) {
+            // Percent symbols (%) must be double-encoded.
+            $key   = $this->encode($key);
+            $value = $this->encode($value);
+
+            $normalized[$key] = $value;
+        }
+
+        return $normalized;
+    }
+
+    /**
+     * Process filters.
+     *
+     * @param array $parameters Request parameters.
+     *
+     * @return array
+     */
+    protected function processFilters($parameters)
+    {
+        if (isset($parameters['filter'])) {
+            $filters = $parameters['filter'];
+            unset($parameters['filter']);
+            foreach ($filters as $filter => $value) {
+                $parameters['filter[' . $filter . ']'] = $value;
+            }
+        }
+
+        return $parameters;
+    }
+
+    /**
+     * Get secret.
+     *
+     * @return string
+     */
+    protected function getSecret()
+    {
+        $secret = $this->consumerSecret;
+
+        // Fix secret for v3 or later.
+        if (!\in_array($this->apiVersion, ['v1', 'v2'])) {
+            $secret .= '&';
+        }
+
+        return $secret;
+    }
+
+    /**
+     * Generate oAuth1.0 signature.
+     *
+     * @param array $parameters Request parameters including oauth.
+     *
+     * @return string
+     */
+    protected function generateOauthSignature($parameters)
+    {
+        $baseRequestUri = \rawurlencode($this->url);
+
+        // Extract filters.
+        $parameters = $this->processFilters($parameters);
+
+        // Normalize parameter key/values and sort them.
+        $parameters = $this->normalizeParameters($parameters);
+        $parameters = $this->getSortedParameters($parameters);
+
+        // Set query string.
+        $queryString  = \implode('%26', $this->joinWithEqualsSign($parameters)); // Join with ampersand.
+        $stringToSign = $this->method . '&' . $baseRequestUri . '&' . $queryString;
+        $secret       = $this->getSecret();
+
+        return \base64_encode(\hash_hmac(self::HASH_ALGORITHM, $stringToSign, $secret, true));
+    }
+
+    /**
+     * Creates an array of urlencoded strings out of each array key/value pairs.
+     *
+     * @param  array  $params      Array of parameters to convert.
+     * @param  array  $queryParams Array to extend.
+     * @param  string $key         Optional Array key to append
+     * @return array               Array of urlencoded strings
+     */
+    protected function joinWithEqualsSign($params, $queryParams = [], $key = '')
+    {
+        foreach ($params as $paramKey => $paramValue) {
+            if ($key) {
+                $paramKey = $key . '%5B' . $paramKey . '%5D'; // Handle multi-dimensional array.
+            }
+
+            if (is_array($paramValue)) {
+                $queryParams = $this->joinWithEqualsSign($paramValue, $queryParams, $paramKey);
+            } else {
+                $string = $paramKey . '=' . $paramValue; // Join with equals sign.
+                $queryParams[] = $this->encode($string);
+            }
+        }
+
+        return $queryParams;
+    }
+
+    /**
+     * Sort parameters.
+     *
+     * @param array $parameters Parameters to sort in byte-order.
+     *
+     * @return array
+     */
+    protected function getSortedParameters($parameters)
+    {
+        \uksort($parameters, 'strcmp');
+
+        foreach ($parameters as $key => $value) {
+            if (\is_array($value)) {
+                \uksort($parameters[$key], 'strcmp');
+            }
+        }
+
+        return $parameters;
+    }
+
+    /**
+     * Get oAuth1.0 parameters.
+     *
+     * @return string
+     */
+    public function getParameters()
+    {
+        $parameters = \array_merge($this->parameters, [
+            'oauth_consumer_key'     => $this->consumerKey,
+            'oauth_timestamp'        => $this->timestamp,
+            'oauth_nonce'            => \sha1(\microtime()),
+            'oauth_signature_method' => 'HMAC-' . self::HASH_ALGORITHM,
+        ]);
+
+        // The parameters above must be included in the signature generation.
+        $parameters['oauth_signature'] = $this->generateOauthSignature($parameters);
+
+        return $this->getSortedParameters($parameters);
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Options.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Options.php
new file mode 100644
index 000000000..3e2ce6a14
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Options.php
@@ -0,0 +1,182 @@
+options = $options;
+    }
+
+    /**
+     * Get API version.
+     *
+     * @return string
+     */
+    public function getVersion()
+    {
+        return isset($this->options['version']) ? $this->options['version'] : self::VERSION;
+    }
+
+    /**
+     * Check if need to verify SSL.
+     *
+     * @return bool
+     */
+    public function verifySsl()
+    {
+        return isset($this->options['verify_ssl']) ? (bool) $this->options['verify_ssl'] : true;
+    }
+
+    /**
+     * Only use OAuth.
+     *
+     * @return bool
+     */
+    public function isOAuthOnly()
+    {
+        return isset($this->options['oauth_only']) ? (bool) $this->options['oauth_only'] : false;
+    }
+
+    /**
+     * Get timeout.
+     *
+     * @return int
+     */
+    public function getTimeout()
+    {
+        return isset($this->options['timeout']) ? (int) $this->options['timeout'] : self::TIMEOUT;
+    }
+
+    /**
+     * Basic Authentication as query string.
+     * Some old servers are not able to use CURLOPT_USERPWD.
+     *
+     * @return bool
+     */
+    public function isQueryStringAuth()
+    {
+        return isset($this->options['query_string_auth']) ? (bool) $this->options['query_string_auth'] : false;
+    }
+
+    /**
+     * Check if is WP REST API.
+     *
+     * @return bool
+     */
+    public function isWPAPI()
+    {
+        return isset($this->options['wp_api']) ? (bool) $this->options['wp_api'] : true;
+    }
+
+    /**
+     * Custom API Prefix for WP API.
+     *
+     * @return string
+     */
+    public function apiPrefix()
+    {
+        return isset($this->options['wp_api_prefix']) ? $this->options['wp_api_prefix'] : self::WP_API_PREFIX;
+    }
+
+    /**
+     * oAuth timestamp.
+     *
+     * @return string
+     */
+    public function oauthTimestamp()
+    {
+        return isset($this->options['oauth_timestamp']) ? $this->options['oauth_timestamp'] : \time();
+    }
+
+    /**
+     * Custom user agent.
+     *
+     * @return string
+     */
+    public function userAgent()
+    {
+        return isset($this->options['user_agent']) ? $this->options['user_agent'] : self::USER_AGENT;
+    }
+
+    /**
+     * Get follow redirects.
+     *
+     * @return bool
+     */
+    public function getFollowRedirects()
+    {
+        return isset($this->options['follow_redirects']) ? (bool) $this->options['follow_redirects'] : false;
+    }
+
+    /**
+     * Check is it needed to mask all non-GET/POST methods (PUT/DELETE/etc.) by using POST method with added
+     * query parameter ?_method=METHOD into URL.
+     *
+     * @return bool
+     */
+    public function isMethodOverrideQuery()
+    {
+        return isset($this->options['method_override_query']) && $this->options['method_override_query'];
+    }
+
+    /**
+     * Check is it needed to mask all non-GET/POST methods (PUT/DELETE/etc.) by using POST method with added
+     * "X-HTTP-Method-Override: METHOD" HTTP header into request.
+     *
+     * @return bool
+     */
+    public function isMethodOverrideHeader()
+    {
+        return isset($this->options['method_override_header']) && $this->options['method_override_header'];
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Request.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Request.php
new file mode 100644
index 000000000..b9db6bd16
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Request.php
@@ -0,0 +1,187 @@
+url        = $url;
+        $this->method     = $method;
+        $this->parameters = $parameters;
+        $this->headers    = $headers;
+        $this->body       = $body;
+    }
+
+    /**
+     * Set url.
+     *
+     * @param string $url Request url.
+     */
+    public function setUrl($url)
+    {
+        $this->url = $url;
+    }
+
+    /**
+     * Set method.
+     *
+     * @param string $method Request method.
+     */
+    public function setMethod($method)
+    {
+        $this->method = $method;
+    }
+
+    /**
+     * Set parameters.
+     *
+     * @param array $parameters Request paramenters.
+     */
+    public function setParameters($parameters)
+    {
+        $this->parameters = $parameters;
+    }
+
+    /**
+     * Set headers.
+     *
+     * @param array $headers Request headers.
+     */
+    public function setHeaders($headers)
+    {
+        $this->headers = $headers;
+    }
+
+    /**
+     * Set body.
+     *
+     * @param string $body Request body.
+     */
+    public function setBody($body)
+    {
+        $this->body = $body;
+    }
+
+    /**
+     * Get url.
+     *
+     * @return string
+     */
+    public function getUrl()
+    {
+        return $this->url;
+    }
+
+    /**
+     * Get method.
+     *
+     * @return string
+     */
+    public function getMethod()
+    {
+        return $this->method;
+    }
+
+    /**
+     * Get parameters.
+     *
+     * @return array
+     */
+    public function getParameters()
+    {
+        return $this->parameters;
+    }
+
+    /**
+     * Get headers.
+     *
+     * @return array
+     */
+    public function getHeaders()
+    {
+        return $this->headers;
+    }
+
+    /**
+     * Get raw headers.
+     *
+     * @return array
+     */
+    public function getRawHeaders()
+    {
+        $headers = [];
+
+        foreach ($this->headers as $key => $value) {
+            $headers[] = $key . ': ' . $value;
+        }
+
+        return $headers;
+    }
+
+    /**
+     * Get body.
+     *
+     * @return string
+     */
+    public function getBody()
+    {
+        return $this->body;
+    }
+}
diff --git a/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Response.php b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Response.php
new file mode 100644
index 000000000..b5a5de19c
--- /dev/null
+++ b/vendor/automattic/woocommerce/src/WooCommerce/HttpClient/Response.php
@@ -0,0 +1,127 @@
+code    = $code;
+        $this->headers = $headers;
+        $this->body    = $body;
+    }
+
+    /**
+     * To string.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return \json_encode([
+            'code'    => $this->code,
+            'headers' => $this->headers,
+            'body'    => $this->body,
+        ]);
+    }
+
+    /**
+     * Set code.
+     *
+     * @param int $code Response code.
+     */
+    public function setCode($code)
+    {
+        $this->code = (int) $code;
+    }
+
+    /**
+     * Set headers.
+     *
+     * @param array $headers Response headers.
+     */
+    public function setHeaders($headers)
+    {
+        $this->headers = $headers;
+    }
+
+    /**
+     * Set body.
+     *
+     * @param string $body Response body.
+     */
+    public function setBody($body)
+    {
+        $this->body = $body;
+    }
+
+    /**
+     * Get code.
+     *
+     * @return int
+     */
+    public function getCode()
+    {
+        return $this->code;
+    }
+
+    /**
+     * Get headers.
+     *
+     * @return array $headers Response headers.
+     */
+    public function getHeaders()
+    {
+        return $this->headers;
+    }
+
+    /**
+     * Get body.
+     *
+     * @return string $body Response body.
+     */
+    public function getBody()
+    {
+        return $this->body;
+    }
+}
diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php
index 6d29bff66..2052022fd 100644
--- a/vendor/composer/InstalledVersions.php
+++ b/vendor/composer/InstalledVersions.php
@@ -26,6 +26,12 @@
  */
 class InstalledVersions
 {
+    /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
     /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
@@ -322,6 +328,18 @@ public static function reload($data)
         self::$installedIsLocalDir = false;
     }
 
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\\', '/');
+        }
+
+        return self::$selfDir;
+    }
+
     /**
      * @return array[]
      * @psalm-return list}>
@@ -336,7 +354,7 @@ private static function getInstalled()
         $copiedLocalDir = false;
 
         if (self::$canGetVendors) {
-            $selfDir = strtr(__DIR__, '\\', '/');
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
                 $vendorDir = strtr($vendorDir, '\\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php
index c0eb26162..fbde08bae 100644
--- a/vendor/composer/autoload_classmap.php
+++ b/vendor/composer/autoload_classmap.php
@@ -6,6 +6,29 @@
 $baseDir = dirname($vendorDir);
 
 return array(
+    'AWS\\CRT\\Auth\\AwsCredentials' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php',
+    'AWS\\CRT\\Auth\\CredentialsProvider' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php',
+    'AWS\\CRT\\Auth\\Signable' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php',
+    'AWS\\CRT\\Auth\\SignatureType' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php',
+    'AWS\\CRT\\Auth\\SignedBodyHeaderType' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php',
+    'AWS\\CRT\\Auth\\Signing' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php',
+    'AWS\\CRT\\Auth\\SigningAlgorithm' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php',
+    'AWS\\CRT\\Auth\\SigningConfigAWS' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php',
+    'AWS\\CRT\\Auth\\SigningResult' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php',
+    'AWS\\CRT\\Auth\\StaticCredentialsProvider' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php',
+    'AWS\\CRT\\CRT' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/CRT.php',
+    'AWS\\CRT\\HTTP\\Headers' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php',
+    'AWS\\CRT\\HTTP\\Message' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php',
+    'AWS\\CRT\\HTTP\\Request' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php',
+    'AWS\\CRT\\HTTP\\Response' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php',
+    'AWS\\CRT\\IO\\EventLoopGroup' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php',
+    'AWS\\CRT\\IO\\InputStream' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php',
+    'AWS\\CRT\\Internal\\Encoding' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php',
+    'AWS\\CRT\\Internal\\Extension' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php',
+    'AWS\\CRT\\Log' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Log.php',
+    'AWS\\CRT\\NativeResource' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/NativeResource.php',
+    'AWS\\CRT\\OptionValue' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Options.php',
+    'AWS\\CRT\\Options' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Options.php',
     'Aura\\SqlQuery\\AbstractDmlQuery' => $vendorDir . '/aura/sqlquery/src/AbstractDmlQuery.php',
     'Aura\\SqlQuery\\AbstractQuery' => $vendorDir . '/aura/sqlquery/src/AbstractQuery.php',
     'Aura\\SqlQuery\\Common\\Delete' => $vendorDir . '/aura/sqlquery/src/Common/Delete.php',
@@ -43,6 +66,14 @@
     'Aura\\SqlQuery\\Sqlsrv\\Insert' => $vendorDir . '/aura/sqlquery/src/Sqlsrv/Insert.php',
     'Aura\\SqlQuery\\Sqlsrv\\Select' => $vendorDir . '/aura/sqlquery/src/Sqlsrv/Select.php',
     'Aura\\SqlQuery\\Sqlsrv\\Update' => $vendorDir . '/aura/sqlquery/src/Sqlsrv/Update.php',
+    'Automattic\\WooCommerce\\Client' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/Client.php',
+    'Automattic\\WooCommerce\\HttpClient\\BasicAuth' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/BasicAuth.php',
+    'Automattic\\WooCommerce\\HttpClient\\HttpClient' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClient.php',
+    'Automattic\\WooCommerce\\HttpClient\\HttpClientException' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClientException.php',
+    'Automattic\\WooCommerce\\HttpClient\\OAuth' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/OAuth.php',
+    'Automattic\\WooCommerce\\HttpClient\\Options' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/Options.php',
+    'Automattic\\WooCommerce\\HttpClient\\Request' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/Request.php',
+    'Automattic\\WooCommerce\\HttpClient\\Response' => $vendorDir . '/automattic/woocommerce/src/WooCommerce/HttpClient/Response.php',
     'Aws\\ACMPCA\\ACMPCAClient' => $vendorDir . '/aws/aws-sdk-php/src/ACMPCA/ACMPCAClient.php',
     'Aws\\ACMPCA\\Exception\\ACMPCAException' => $vendorDir . '/aws/aws-sdk-php/src/ACMPCA/Exception/ACMPCAException.php',
     'Aws\\AbstractConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/AbstractConfigurationProvider.php',
@@ -2514,8 +2545,6 @@
     'Xentral\\Components\\Mailer\\Transport\\PhpMailerOAuth' => $baseDir . '/classes/Components/Mailer/Transport/PhpMailerOAuth.php',
     'Xentral\\Components\\Mailer\\Transport\\PhpMailerOAuthAuthentificationInterface' => $baseDir . '/classes/Components/Mailer/Transport/PhpMailerOAuthAuthentificationInterface.php',
     'Xentral\\Components\\Mailer\\Transport\\PhpMailerTransport' => $baseDir . '/classes/Components/Mailer/Transport/PhpMailerTransport.php',
-    'Xentral\\Components\\Mailer\\Wrapper\\LoggerWrapper' => $baseDir . '/classes/Components/Mailer/Wrapper/LoggerWrapper.php',
-    'Xentral\\Components\\Mailer\\Wrapper\\MemoryLogger' => $baseDir . '/classes/Components/Mailer/Wrapper/MemoryLogger.php',
     'Xentral\\Components\\Pdf\\Bootstrap' => $baseDir . '/classes/Components/Pdf/Bootstrap.php',
     'Xentral\\Components\\Pdf\\Exception\\FileExistsException' => $baseDir . '/classes/Components/Pdf/Exception/FileExistsException.php',
     'Xentral\\Components\\Pdf\\Exception\\FileNotFoundException' => $baseDir . '/classes/Components/Pdf/Exception/FileNotFoundException.php',
@@ -2634,6 +2663,7 @@
     'Xentral\\Components\\Util\\Exception\\StringUtilException' => $baseDir . '/classes/Components/Util/Exception/StringUtilException.php',
     'Xentral\\Components\\Util\\Exception\\UtilExceptionInterface' => $baseDir . '/classes/Components/Util/Exception/UtilExceptionInterface.php',
     'Xentral\\Components\\Util\\StringUtil' => $baseDir . '/classes/Components/Util/StringUtil.php',
+    'Xentral\\Components\\WooCommerce\\ClientWrapper' => $baseDir . '/classes/Components/WooCommerce/ClientWrapper.php',
     'Xentral\\Core\\DependencyInjection\\AbstractBaseContainer' => $baseDir . '/classes/Core/DependencyInjection/AbstractBaseContainer.php',
     'Xentral\\Core\\DependencyInjection\\ContainerInterface' => $baseDir . '/classes/Core/DependencyInjection/ContainerInterface.php',
     'Xentral\\Core\\DependencyInjection\\Definition\\FactoryMethodDefinition' => $baseDir . '/classes/Core/DependencyInjection/Definition/FactoryMethodDefinition.php',
@@ -2951,6 +2981,7 @@
     'Xentral\\Modules\\Datanorm\\Service\\DatanormReader' => $baseDir . '/classes/Modules/Datanorm/Service/DatanormReader.php',
     'Xentral\\Modules\\Datanorm\\Wrapper\\AddressWrapper' => $baseDir . '/classes/Modules/Datanorm/Wrapper/AddressWrapper.php',
     'Xentral\\Modules\\DatevApi\\DataTable\\DatevExportDataTable' => $baseDir . '/classes/Modules/DatevApi/DataTable/DatevExportDataTable.php',
+    'Xentral\\Modules\\DatevExport\\DatevExport' => $baseDir . '/classes/Modules/DatevExport/DatevExport.php',
     'Xentral\\Modules\\DemoExporter\\Bootstrap' => $baseDir . '/classes/Modules/DemoExporter/Bootstrap.php',
     'Xentral\\Modules\\DemoExporter\\DemoExporterCleanerService' => $baseDir . '/classes/Modules/DemoExporter/DemoExporterCleanerService.php',
     'Xentral\\Modules\\DemoExporter\\DemoExporterDateiService' => $baseDir . '/classes/Modules/DemoExporter/DemoExporterDateiService.php',
@@ -3291,7 +3322,6 @@
     'Xentral\\Modules\\Log\\Service\\DatabaseLogGateway' => $baseDir . '/classes/Modules/Log/Service/DatabaseLogGateway.php',
     'Xentral\\Modules\\Log\\Service\\DatabaseLogService' => $baseDir . '/classes/Modules/Log/Service/DatabaseLogService.php',
     'Xentral\\Modules\\Log\\Service\\LoggerConfigService' => $baseDir . '/classes/Modules/Log/Service/LoggerConfigService.php',
-    'Xentral\\Modules\\Log\\Wrapper\\CompanyConfigWrapper' => $baseDir . '/classes/Modules/Log/Wrapper/CompanyConfigWrapper.php',
     'Xentral\\Modules\\MandatoryFields\\Bootstrap' => $baseDir . '/classes/Modules/MandatoryFields/Bootstrap.php',
     'Xentral\\Modules\\MandatoryFields\\Data\\MandatoryFieldData' => $baseDir . '/classes/Modules/MandatoryFields/Data/MandatoryFieldData.php',
     'Xentral\\Modules\\MandatoryFields\\Data\\ValidatorResultData' => $baseDir . '/classes/Modules/MandatoryFields/Data/ValidatorResultData.php',
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
index aa47ba3c3..6ff597737 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -49,6 +49,7 @@
     'Datto\\JsonRpc\\Http\\' => array($vendorDir . '/datto/json-rpc-http/src'),
     'Datto\\JsonRpc\\' => array($vendorDir . '/datto/json-rpc/src'),
     'Aws\\' => array($vendorDir . '/aws/aws-sdk-php/src'),
+    'Automattic\\WooCommerce\\' => array($vendorDir . '/automattic/woocommerce/src/WooCommerce'),
     'Aura\\SqlQuery\\' => array($vendorDir . '/aura/sqlquery/src'),
     '' => array($vendorDir . '/league/color-extractor/src'),
 );
diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php
index adb873a96..ac10ca409 100644
--- a/vendor/composer/autoload_real.php
+++ b/vendor/composer/autoload_real.php
@@ -22,8 +22,6 @@ public static function getLoader()
             return self::$loader;
         }
 
-        require __DIR__ . '/platform_check.php';
-
         spl_autoload_register(array('ComposerAutoloaderInit0c49a81c1214ef2f7493c6ce921b17ee', 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
         spl_autoload_unregister(array('ComposerAutoloaderInit0c49a81c1214ef2f7493c6ce921b17ee', 'loadClassLoader'));
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index 449b91726..c6768b765 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -29,23 +29,23 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
     );
 
     public static $prefixLengthsPsr4 = array (
-        'l' => 
+        'l' =>
         array (
             'lfkeitel\\phptotp\\' => 17,
         ),
-        'Y' => 
+        'Y' =>
         array (
             'Y0lk\\OAuth1\\Client\\Server\\' => 26,
         ),
-        'X' => 
+        'X' =>
         array (
             'Xentral\\' => 8,
         ),
-        'W' => 
+        'W' =>
         array (
             'Webmozart\\Assert\\' => 17,
         ),
-        'S' => 
+        'S' =>
         array (
             'Symfony\\Polyfill\\Mbstring\\' => 26,
             'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
@@ -63,18 +63,18 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
             'Sabre\\CardDAV\\' => 14,
             'Sabre\\CalDAV\\' => 13,
         ),
-        'R' => 
+        'R' =>
         array (
             'Rakit\\Validation\\' => 17,
         ),
-        'P' => 
+        'P' =>
         array (
             'Psr\\Log\\' => 8,
             'Psr\\Http\\Message\\' => 17,
             'Psr\\Container\\' => 14,
             'PHPMailer\\PHPMailer\\' => 20,
         ),
-        'L' => 
+        'L' =>
         array (
             'League\\OAuth1\\Client\\' => 21,
             'League\\MimeTypeDetection\\' => 25,
@@ -86,208 +86,213 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
             'Laminas\\Mail\\' => 13,
             'Laminas\\Loader\\' => 15,
         ),
-        'J' => 
+        'J' =>
         array (
             'JmesPath\\' => 9,
         ),
-        'G' => 
+        'G' =>
         array (
             'GuzzleHttp\\Psr7\\' => 16,
             'GuzzleHttp\\Promise\\' => 19,
             'GuzzleHttp\\' => 11,
         ),
-        'F' => 
+        'F' =>
         array (
             'FiskalyClient\\' => 14,
             'FastRoute\\' => 10,
         ),
-        'D' => 
+        'D' =>
         array (
             'Datto\\JsonRpc\\Http\\Examples\\' => 28,
             'Datto\\JsonRpc\\Http\\' => 19,
             'Datto\\JsonRpc\\' => 14,
         ),
-        'A' => 
+        'A' =>
         array (
             'Aws\\' => 4,
+            'Automattic\\WooCommerce\\' => 23,
             'Aura\\SqlQuery\\' => 14,
         ),
     );
 
     public static $prefixDirsPsr4 = array (
-        'lfkeitel\\phptotp\\' => 
+        'lfkeitel\\phptotp\\' =>
         array (
             0 => __DIR__ . '/..' . '/lfkeitel/phptotp/src',
         ),
-        'Y0lk\\OAuth1\\Client\\Server\\' => 
+        'Y0lk\\OAuth1\\Client\\Server\\' =>
         array (
             0 => __DIR__ . '/..' . '/y0lk/oauth1-etsy/src',
         ),
-        'Xentral\\' => 
+        'Xentral\\' =>
         array (
             0 => __DIR__ . '/../..' . '/classes',
         ),
-        'Webmozart\\Assert\\' => 
+        'Webmozart\\Assert\\' =>
         array (
             0 => __DIR__ . '/..' . '/webmozart/assert/src',
         ),
-        'Symfony\\Polyfill\\Mbstring\\' => 
+        'Symfony\\Polyfill\\Mbstring\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
         ),
-        'Symfony\\Polyfill\\Intl\\Normalizer\\' => 
+        'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
         ),
-        'Symfony\\Polyfill\\Intl\\Idn\\' => 
+        'Symfony\\Polyfill\\Intl\\Idn\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
         ),
-        'SwissPaymentSlip\\SwissPaymentSlip\\' => 
+        'SwissPaymentSlip\\SwissPaymentSlip\\' =>
         array (
             0 => __DIR__ . '/..' . '/swiss-payment-slip/swiss-payment-slip/src',
         ),
-        'SwissPaymentSlip\\SwissPaymentSlipPdf\\' => 
+        'SwissPaymentSlip\\SwissPaymentSlipPdf\\' =>
         array (
             0 => __DIR__ . '/..' . '/swiss-payment-slip/swiss-payment-slip-pdf/src',
         ),
-        'SwissPaymentSlip\\SwissPaymentSlipFpdf\\' => 
+        'SwissPaymentSlip\\SwissPaymentSlipFpdf\\' =>
         array (
             0 => __DIR__ . '/..' . '/swiss-payment-slip/swiss-payment-slip-fpdf/src',
         ),
-        'Sabre\\Xml\\' => 
+        'Sabre\\Xml\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/xml/lib',
         ),
-        'Sabre\\VObject\\' => 
+        'Sabre\\VObject\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/vobject/lib',
         ),
-        'Sabre\\Uri\\' => 
+        'Sabre\\Uri\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/uri/lib',
         ),
-        'Sabre\\HTTP\\' => 
+        'Sabre\\HTTP\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/http/lib',
         ),
-        'Sabre\\Event\\' => 
+        'Sabre\\Event\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/event/lib',
         ),
-        'Sabre\\DAV\\' => 
+        'Sabre\\DAV\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/dav/lib/DAV',
         ),
-        'Sabre\\DAVACL\\' => 
+        'Sabre\\DAVACL\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL',
         ),
-        'Sabre\\CardDAV\\' => 
+        'Sabre\\CardDAV\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV',
         ),
-        'Sabre\\CalDAV\\' => 
+        'Sabre\\CalDAV\\' =>
         array (
             0 => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV',
         ),
-        'Rakit\\Validation\\' => 
+        'Rakit\\Validation\\' =>
         array (
             0 => __DIR__ . '/..' . '/rakit/validation/src',
         ),
-        'Psr\\Log\\' => 
+        'Psr\\Log\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
         ),
-        'Psr\\Http\\Message\\' => 
+        'Psr\\Http\\Message\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-message/src',
         ),
-        'Psr\\Container\\' => 
+        'Psr\\Container\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/container/src',
         ),
-        'PHPMailer\\PHPMailer\\' => 
+        'PHPMailer\\PHPMailer\\' =>
         array (
             0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
         ),
-        'League\\OAuth1\\Client\\' => 
+        'League\\OAuth1\\Client\\' =>
         array (
             0 => __DIR__ . '/..' . '/league/oauth1-client/src',
         ),
-        'League\\MimeTypeDetection\\' => 
+        'League\\MimeTypeDetection\\' =>
         array (
             0 => __DIR__ . '/..' . '/league/mime-type-detection/src',
         ),
-        'League\\Flysystem\\' => 
+        'League\\Flysystem\\' =>
         array (
             0 => __DIR__ . '/..' . '/league/flysystem/src',
         ),
-        'Laminas\\Validator\\' => 
+        'Laminas\\Validator\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-validator/src',
         ),
-        'Laminas\\Stdlib\\' => 
+        'Laminas\\Stdlib\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-stdlib/src',
         ),
-        'Laminas\\ServiceManager\\' => 
+        'Laminas\\ServiceManager\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-servicemanager/src',
         ),
-        'Laminas\\Mime\\' => 
+        'Laminas\\Mime\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-mime/src',
         ),
-        'Laminas\\Mail\\' => 
+        'Laminas\\Mail\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-mail/src',
         ),
-        'Laminas\\Loader\\' => 
+        'Laminas\\Loader\\' =>
         array (
             0 => __DIR__ . '/..' . '/laminas/laminas-loader/src',
         ),
-        'JmesPath\\' => 
+        'JmesPath\\' =>
         array (
             0 => __DIR__ . '/..' . '/mtdowling/jmespath.php/src',
         ),
-        'GuzzleHttp\\Psr7\\' => 
+        'GuzzleHttp\\Psr7\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
         ),
-        'GuzzleHttp\\Promise\\' => 
+        'GuzzleHttp\\Promise\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
         ),
-        'GuzzleHttp\\' => 
+        'GuzzleHttp\\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
         ),
-        'FiskalyClient\\' => 
+        'FiskalyClient\\' =>
         array (
             0 => __DIR__ . '/..' . '/fiskaly/fiskaly-sdk-php/src',
         ),
-        'FastRoute\\' => 
+        'FastRoute\\' =>
         array (
             0 => __DIR__ . '/..' . '/nikic/fast-route/src',
         ),
-        'Datto\\JsonRpc\\Http\\Examples\\' => 
+        'Datto\\JsonRpc\\Http\\Examples\\' =>
         array (
             0 => __DIR__ . '/..' . '/datto/json-rpc-http/examples/src',
         ),
-        'Datto\\JsonRpc\\Http\\' => 
+        'Datto\\JsonRpc\\Http\\' =>
         array (
             0 => __DIR__ . '/..' . '/datto/json-rpc-http/src',
         ),
-        'Datto\\JsonRpc\\' => 
+        'Datto\\JsonRpc\\' =>
         array (
             0 => __DIR__ . '/..' . '/datto/json-rpc/src',
         ),
-        'Aws\\' => 
+        'Aws\\' =>
         array (
             0 => __DIR__ . '/..' . '/aws/aws-sdk-php/src',
         ),
-        'Aura\\SqlQuery\\' => 
+        'Automattic\\WooCommerce\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce',
+        ),
+        'Aura\\SqlQuery\\' =>
         array (
             0 => __DIR__ . '/..' . '/aura/sqlquery/src',
         ),
@@ -298,9 +303,9 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
     );
 
     public static $prefixesPsr0 = array (
-        'H' => 
+        'H' =>
         array (
-            'HTMLPurifier' => 
+            'HTMLPurifier' =>
             array (
                 0 => __DIR__ . '/..' . '/ezyang/htmlpurifier/library',
             ),
@@ -308,6 +313,29 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
     );
 
     public static $classMap = array (
+        'AWS\\CRT\\Auth\\AwsCredentials' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php',
+        'AWS\\CRT\\Auth\\CredentialsProvider' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php',
+        'AWS\\CRT\\Auth\\Signable' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php',
+        'AWS\\CRT\\Auth\\SignatureType' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php',
+        'AWS\\CRT\\Auth\\SignedBodyHeaderType' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php',
+        'AWS\\CRT\\Auth\\Signing' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php',
+        'AWS\\CRT\\Auth\\SigningAlgorithm' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php',
+        'AWS\\CRT\\Auth\\SigningConfigAWS' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php',
+        'AWS\\CRT\\Auth\\SigningResult' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php',
+        'AWS\\CRT\\Auth\\StaticCredentialsProvider' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php',
+        'AWS\\CRT\\CRT' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/CRT.php',
+        'AWS\\CRT\\HTTP\\Headers' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php',
+        'AWS\\CRT\\HTTP\\Message' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php',
+        'AWS\\CRT\\HTTP\\Request' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php',
+        'AWS\\CRT\\HTTP\\Response' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php',
+        'AWS\\CRT\\IO\\EventLoopGroup' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php',
+        'AWS\\CRT\\IO\\InputStream' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php',
+        'AWS\\CRT\\Internal\\Encoding' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php',
+        'AWS\\CRT\\Internal\\Extension' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php',
+        'AWS\\CRT\\Log' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Log.php',
+        'AWS\\CRT\\NativeResource' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/NativeResource.php',
+        'AWS\\CRT\\OptionValue' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Options.php',
+        'AWS\\CRT\\Options' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Options.php',
         'Aura\\SqlQuery\\AbstractDmlQuery' => __DIR__ . '/..' . '/aura/sqlquery/src/AbstractDmlQuery.php',
         'Aura\\SqlQuery\\AbstractQuery' => __DIR__ . '/..' . '/aura/sqlquery/src/AbstractQuery.php',
         'Aura\\SqlQuery\\Common\\Delete' => __DIR__ . '/..' . '/aura/sqlquery/src/Common/Delete.php',
@@ -345,6 +373,14 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
         'Aura\\SqlQuery\\Sqlsrv\\Insert' => __DIR__ . '/..' . '/aura/sqlquery/src/Sqlsrv/Insert.php',
         'Aura\\SqlQuery\\Sqlsrv\\Select' => __DIR__ . '/..' . '/aura/sqlquery/src/Sqlsrv/Select.php',
         'Aura\\SqlQuery\\Sqlsrv\\Update' => __DIR__ . '/..' . '/aura/sqlquery/src/Sqlsrv/Update.php',
+        'Automattic\\WooCommerce\\Client' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/Client.php',
+        'Automattic\\WooCommerce\\HttpClient\\BasicAuth' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/BasicAuth.php',
+        'Automattic\\WooCommerce\\HttpClient\\HttpClient' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClient.php',
+        'Automattic\\WooCommerce\\HttpClient\\HttpClientException' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/HttpClientException.php',
+        'Automattic\\WooCommerce\\HttpClient\\OAuth' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/OAuth.php',
+        'Automattic\\WooCommerce\\HttpClient\\Options' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/Options.php',
+        'Automattic\\WooCommerce\\HttpClient\\Request' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/Request.php',
+        'Automattic\\WooCommerce\\HttpClient\\Response' => __DIR__ . '/..' . '/automattic/woocommerce/src/WooCommerce/HttpClient/Response.php',
         'Aws\\ACMPCA\\ACMPCAClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ACMPCA/ACMPCAClient.php',
         'Aws\\ACMPCA\\Exception\\ACMPCAException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ACMPCA/Exception/ACMPCAException.php',
         'Aws\\AbstractConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/AbstractConfigurationProvider.php',
@@ -2816,8 +2852,6 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
         'Xentral\\Components\\Mailer\\Transport\\PhpMailerOAuth' => __DIR__ . '/../..' . '/classes/Components/Mailer/Transport/PhpMailerOAuth.php',
         'Xentral\\Components\\Mailer\\Transport\\PhpMailerOAuthAuthentificationInterface' => __DIR__ . '/../..' . '/classes/Components/Mailer/Transport/PhpMailerOAuthAuthentificationInterface.php',
         'Xentral\\Components\\Mailer\\Transport\\PhpMailerTransport' => __DIR__ . '/../..' . '/classes/Components/Mailer/Transport/PhpMailerTransport.php',
-        'Xentral\\Components\\Mailer\\Wrapper\\LoggerWrapper' => __DIR__ . '/../..' . '/classes/Components/Mailer/Wrapper/LoggerWrapper.php',
-        'Xentral\\Components\\Mailer\\Wrapper\\MemoryLogger' => __DIR__ . '/../..' . '/classes/Components/Mailer/Wrapper/MemoryLogger.php',
         'Xentral\\Components\\Pdf\\Bootstrap' => __DIR__ . '/../..' . '/classes/Components/Pdf/Bootstrap.php',
         'Xentral\\Components\\Pdf\\Exception\\FileExistsException' => __DIR__ . '/../..' . '/classes/Components/Pdf/Exception/FileExistsException.php',
         'Xentral\\Components\\Pdf\\Exception\\FileNotFoundException' => __DIR__ . '/../..' . '/classes/Components/Pdf/Exception/FileNotFoundException.php',
@@ -2936,6 +2970,7 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
         'Xentral\\Components\\Util\\Exception\\StringUtilException' => __DIR__ . '/../..' . '/classes/Components/Util/Exception/StringUtilException.php',
         'Xentral\\Components\\Util\\Exception\\UtilExceptionInterface' => __DIR__ . '/../..' . '/classes/Components/Util/Exception/UtilExceptionInterface.php',
         'Xentral\\Components\\Util\\StringUtil' => __DIR__ . '/../..' . '/classes/Components/Util/StringUtil.php',
+        'Xentral\\Components\\WooCommerce\\ClientWrapper' => __DIR__ . '/../..' . '/classes/Components/WooCommerce/ClientWrapper.php',
         'Xentral\\Core\\DependencyInjection\\AbstractBaseContainer' => __DIR__ . '/../..' . '/classes/Core/DependencyInjection/AbstractBaseContainer.php',
         'Xentral\\Core\\DependencyInjection\\ContainerInterface' => __DIR__ . '/../..' . '/classes/Core/DependencyInjection/ContainerInterface.php',
         'Xentral\\Core\\DependencyInjection\\Definition\\FactoryMethodDefinition' => __DIR__ . '/../..' . '/classes/Core/DependencyInjection/Definition/FactoryMethodDefinition.php',
@@ -3253,6 +3288,7 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
         'Xentral\\Modules\\Datanorm\\Service\\DatanormReader' => __DIR__ . '/../..' . '/classes/Modules/Datanorm/Service/DatanormReader.php',
         'Xentral\\Modules\\Datanorm\\Wrapper\\AddressWrapper' => __DIR__ . '/../..' . '/classes/Modules/Datanorm/Wrapper/AddressWrapper.php',
         'Xentral\\Modules\\DatevApi\\DataTable\\DatevExportDataTable' => __DIR__ . '/../..' . '/classes/Modules/DatevApi/DataTable/DatevExportDataTable.php',
+        'Xentral\\Modules\\DatevExport\\DatevExport' => __DIR__ . '/../..' . '/classes/Modules/DatevExport/DatevExport.php',
         'Xentral\\Modules\\DemoExporter\\Bootstrap' => __DIR__ . '/../..' . '/classes/Modules/DemoExporter/Bootstrap.php',
         'Xentral\\Modules\\DemoExporter\\DemoExporterCleanerService' => __DIR__ . '/../..' . '/classes/Modules/DemoExporter/DemoExporterCleanerService.php',
         'Xentral\\Modules\\DemoExporter\\DemoExporterDateiService' => __DIR__ . '/../..' . '/classes/Modules/DemoExporter/DemoExporterDateiService.php',
@@ -3593,7 +3629,6 @@ class ComposerStaticInit0c49a81c1214ef2f7493c6ce921b17ee
         'Xentral\\Modules\\Log\\Service\\DatabaseLogGateway' => __DIR__ . '/../..' . '/classes/Modules/Log/Service/DatabaseLogGateway.php',
         'Xentral\\Modules\\Log\\Service\\DatabaseLogService' => __DIR__ . '/../..' . '/classes/Modules/Log/Service/DatabaseLogService.php',
         'Xentral\\Modules\\Log\\Service\\LoggerConfigService' => __DIR__ . '/../..' . '/classes/Modules/Log/Service/LoggerConfigService.php',
-        'Xentral\\Modules\\Log\\Wrapper\\CompanyConfigWrapper' => __DIR__ . '/../..' . '/classes/Modules/Log/Wrapper/CompanyConfigWrapper.php',
         'Xentral\\Modules\\MandatoryFields\\Bootstrap' => __DIR__ . '/../..' . '/classes/Modules/MandatoryFields/Bootstrap.php',
         'Xentral\\Modules\\MandatoryFields\\Data\\MandatoryFieldData' => __DIR__ . '/../..' . '/classes/Modules/MandatoryFields/Data/MandatoryFieldData.php',
         'Xentral\\Modules\\MandatoryFields\\Data\\ValidatorResultData' => __DIR__ . '/../..' . '/classes/Modules/MandatoryFields/Data/ValidatorResultData.php',
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 8f974ba08..c0f1f1847 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -71,6 +71,119 @@
                         },
                         "install-path": "../aura/sqlquery"
                 },
+                {
+                        "name": "automattic/woocommerce",
+                        "version": "3.1.1",
+                        "version_normalized": "3.1.1.0",
+                        "source": {
+                                "type": "git",
+                                "url": "https://github.com/woocommerce/wc-api-php.git",
+                                "reference": "e378120df655b7dacbeff4756e23b41b66fe9688"
+                        },
+                        "dist": {
+                                "type": "zip",
+                                "url": "https://api.github.com/repos/woocommerce/wc-api-php/zipball/e378120df655b7dacbeff4756e23b41b66fe9688",
+                                "reference": "e378120df655b7dacbeff4756e23b41b66fe9688",
+                                "shasum": ""
+                        },
+                        "require": {
+                                "ext-curl": "*",
+                                "ext-json": "*",
+                                "php": ">= 7.4.0"
+                        },
+                        "require-dev": {
+                                "doctrine/instantiator": "^1.5.0",
+                                "phpunit/phpunit": "^9.5.0",
+                                "squizlabs/php_codesniffer": "3.*"
+                        },
+                        "time": "2026-01-30T16:26:03+00:00",
+                        "type": "library",
+                        "installation-source": "dist",
+                        "autoload": {
+                                "psr-4": {
+                                        "Automattic\\WooCommerce\\": [
+                                                "src/WooCommerce"
+                                        ]
+                                }
+                        },
+                        "notification-url": "https://packagist.org/downloads/",
+                        "license": [
+                                "MIT"
+                        ],
+                        "authors": [
+                                {
+                                        "name": "Claudio Sanches",
+                                        "email": "claudio.sanches@automattic.com"
+                                }
+                        ],
+                        "description": "A PHP wrapper for the WooCommerce REST API",
+                        "keywords": [
+                                "api",
+                                "woocommerce"
+                        ],
+                        "support": {
+                                "issues": "https://github.com/woocommerce/wc-api-php/issues",
+                                "source": "https://github.com/woocommerce/wc-api-php/tree/3.1.1"
+                        },
+                        "install-path": "../automattic/woocommerce"
+                },
+                {
+                        "name": "aws/aws-crt-php",
+                        "version": "v1.2.7",
+                        "version_normalized": "1.2.7.0",
+                        "source": {
+                                "type": "git",
+                                "url": "https://github.com/awslabs/aws-crt-php.git",
+                                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e"
+                        },
+                        "dist": {
+                                "type": "zip",
+                                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e",
+                                "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e",
+                                "shasum": ""
+                        },
+                        "require": {
+                                "php": ">=5.5"
+                        },
+                        "require-dev": {
+                                "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+                                "yoast/phpunit-polyfills": "^1.0"
+                        },
+                        "suggest": {
+                                "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
+                        },
+                        "time": "2024-10-18T22:15:13+00:00",
+                        "type": "library",
+                        "installation-source": "dist",
+                        "autoload": {
+                                "classmap": [
+                                        "src/"
+                                ]
+                        },
+                        "notification-url": "https://packagist.org/downloads/",
+                        "license": [
+                                "Apache-2.0"
+                        ],
+                        "authors": [
+                                {
+                                        "name": "AWS SDK Common Runtime Team",
+                                        "email": "aws-sdk-common-runtime@amazon.com"
+                                }
+                        ],
+                        "description": "AWS Common Runtime for PHP",
+                        "homepage": "https://github.com/awslabs/aws-crt-php",
+                        "keywords": [
+                                "amazon",
+                                "aws",
+                                "crt",
+                                "sdk"
+                        ],
+                        "support": {
+                                "issues": "https://github.com/awslabs/aws-crt-php/issues",
+                                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
+                        },
+                        "install-path": "../aws/aws-crt-php"
+                },
                 {
                         "name": "aws/aws-sdk-php",
                         "version": "3.175.2",
@@ -382,8 +495,9 @@
                         ],
                         "support": {
                                 "issues": "https://github.com/fiskaly/fiskaly-sdk-php/issues",
-                                "source": "https://github.com/fiskaly/fiskaly-sdk-php/tree/v1.2.100"
+                                "source": "https://github.com/fiskaly/fiskaly-sdk-php/tree/master"
                         },
+                        "abandoned": true,
                         "install-path": "../fiskaly/fiskaly-sdk-php"
                 },
                 {
diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php
index 1292bc1b3..6144f471c 100644
--- a/vendor/composer/installed.php
+++ b/vendor/composer/installed.php
@@ -1,9 +1,9 @@
  array(
         'name' => '__root__',
-        'pretty_version' => '1.0.0+no-version-set',
-        'version' => '1.0.0.0',
-        'reference' => null,
+        'pretty_version' => 'dev-master',
+        'version' => 'dev-master',
+        'reference' => 'fa5dd52f24c581680e2c3dc549c544db5d9c0c81',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         '__root__' => array(
-            'pretty_version' => '1.0.0+no-version-set',
-            'version' => '1.0.0.0',
-            'reference' => null,
+            'pretty_version' => 'dev-master',
+            'version' => 'dev-master',
+            'reference' => 'fa5dd52f24c581680e2c3dc549c544db5d9c0c81',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
@@ -28,6 +28,24 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'automattic/woocommerce' => array(
+            'pretty_version' => '3.1.1',
+            'version' => '3.1.1.0',
+            'reference' => 'e378120df655b7dacbeff4756e23b41b66fe9688',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../automattic/woocommerce',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'aws/aws-crt-php' => array(
+            'pretty_version' => 'v1.2.7',
+            'version' => '1.2.7.0',
+            'reference' => 'd71d9906c7bb63a28295447ba12e74723bd3730e',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../aws/aws-crt-php',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'aws/aws-sdk-php' => array(
             'pretty_version' => '3.175.2',
             'version' => '3.175.2.0',
diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php
deleted file mode 100644
index 4c3a5d68f..000000000
--- a/vendor/composer/platform_check.php
+++ /dev/null
@@ -1,26 +0,0 @@
-= 80100)) {
-    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
-}
-
-if ($issues) {
-    if (!headers_sent()) {
-        header('HTTP/1.1 500 Internal Server Error');
-    }
-    if (!ini_get('display_errors')) {
-        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
-            fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
-        } elseif (!headers_sent()) {
-            echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
-        }
-    }
-    trigger_error(
-        'Composer detected issues in your platform: ' . implode(' ', $issues),
-        E_USER_ERROR
-    );
-}
diff --git a/www/pages/shopimporter_woocommerce.php b/www/pages/shopimporter_woocommerce.php
index 4b02f3861..bf33fcb96 100644
--- a/www/pages/shopimporter_woocommerce.php
+++ b/www/pages/shopimporter_woocommerce.php
@@ -18,6 +18,8 @@
 use Xentral\Modules\Onlineshop\Data\OrderStatus;
 use Xentral\Modules\Onlineshop\Data\OrderStatusUpdateRequest;
 use Xentral\Components\Logger\Logger;
+use Xentral\Components\WooCommerce\ClientWrapper;
+use Automattic\WooCommerce\HttpClient\HttpClientException;
 
 class Shopimporter_Woocommerce extends ShopimporterBase
 {
@@ -27,12 +29,10 @@ class Shopimporter_Woocommerce extends ShopimporterBase
   public $shopid;
   public $data;
 
-  const TIMEOUT = 30;
-
   /**
-   * @var $client WCClient $client
+   * @var ClientWrapper $client
    */
-  public WCClient $client;
+  public $client;
   public $url;
 
   /** These variables hold the status strings WooCommerce is using to represent
@@ -59,17 +59,21 @@ class Shopimporter_Woocommerce extends ShopimporterBase
   protected $app;
   protected $dump;
 
-  /*
-   * Variables for wordpress API
-   */
-  private $ImportWordpressUserName;
-  private $ImportWordpressApplicationPassword;
-  private $wordpress_api_path = "wp-json/wp/v2/";
-  private $dateienuebertragen;
-
-/** @var Logger $logger */
+  /** @var Logger $logger */
   public $logger;
 
+  /** @var bool $ssl_ignore Whether to ignore SSL certificate validation */
+  public $ssl_ignore;
+
+  /** @var string $lastImportTimestamp ISO-8601 UTC timestamp of the last successful import */
+  public $lastImportTimestamp;
+
+  /** @var bool $lastImportTimestampIsFallback True when lastImportTimestamp was computed as 30-day fallback */
+  public $lastImportTimestampIsFallback = false;
+
+  /** @var int[] $lastImportOrderIds WooCommerce order IDs within the current timestamp bucket */
+  public $lastImportOrderIds = [];
+
   public function __construct($app, $intern = false)
   {
     $this->app = $app;
@@ -85,135 +89,190 @@ public function ImportList()
   }
 
   /**
-   * This function returns the number of orders which have not yet been imported
+   * Returns the total number of orders pending import since the last import
+   * timestamp. Uses the WC v3 after= parameter and reads the count from
+   * the X-WP-Total response header (per_page=1 to minimise payload).
+   *
+   * @return int
    */
   public function ImportGetAuftraegeAnzahl()
   {
-    // Query the API to get new orders, filtered by the order status as specifed by the user.
-    // We set per_page to 100 - this could lead to a situation where there are more than
-    // 100 new Orders, but we still only return 100.
-
-    // Array containing additional settings, namely 'ab_nummer' (containting the next order number to get)
-    // and 'holeallestati' (an integer)
-    $tmp = $this->CatchRemoteCommand('data');
-
-    // Only orders having an order number greater or equal than this should be fetched. null otherwise
-    $number_from = empty($tmp['ab_nummer']) ? null : (int) $tmp['ab_nummer'];
-
-    // pending orders will be fetched into this array. it's length is returned at the end of the funciton
-    $pendingOrders = array();
-
-    if ($number_from) {
-      // Number-based import is selected
-
-      // The WooCommerce API doenst allow for a proper "greater than id n" request.
-      // we fake this behavior by creating an array that contains 'many' (~ 1000) consecutive
-      // ids that are greater than $from_number and use this array with the 'include' property
-      // of the WooCommerce API
+    $this->migrateAbNummerIfNeeded();
 
-      $number_to = $number_from + 800;
-      if (!empty($tmp['bis_nummer'])) {
-        $number_to = $tmp['bis_nummer'];
-      }
-
-      $fakeGreaterThanIds = range($number_from, $number_to);
-
-      $pendingOrders = $this->client->get('orders', [
-        'per_page' => 100,
-        'include' => implode(",", $fakeGreaterThanIds),
-      ]);
+    $configuredStatuses = array_map('trim', explode(';', (string) $this->statusPending));
 
+    if (!empty($this->lastImportOrderIds)) {
+      $afterTs = gmdate('Y-m-d\TH:i:s', max(0, strtotime($this->lastImportTimestamp) - 1));
+      $queryArgs = [
+        'status'   => $configuredStatuses,
+        'after'    => $afterTs,
+        'per_page' => 1,
+        'exclude'  => array_values($this->lastImportOrderIds),
+      ];
     } else {
-      // fetch posts by status
+      $queryArgs = [
+        'status'   => $configuredStatuses,
+        'after'    => $this->lastImportTimestamp,
+        'per_page' => 1,
+      ];
+    }
 
-      $pendingOrders = $this->client->get('orders', [
-        'status' => array_map('trim', explode(';', $this->statusPending)),
-        'per_page' => 100
-      ]);
+    try {
+      $this->client->get('orders', $queryArgs);
+    } catch (Exception $e) {
+      $this->logger->warning('WooCommerce ImportGetAuftraegeAnzahl: API request failed: ' . $e->getMessage());
+      return 0;
+    }
+
+    $wcResponse = $this->client->getLastResponse();
+    if ($wcResponse === null) {
+      $this->logger->warning('WooCommerce ImportGetAuftraegeAnzahl: getLastResponse() returned null');
+      return 0;
+    }
 
+    $total = $wcResponse->getHeader('x-wp-total');
+    if ($total === null) {
+      $this->logger->warning('WooCommerce ImportGetAuftraegeAnzahl: X-WP-Total header missing');
+      return 0;
     }
 
-    return (!empty($pendingOrders) ? count($pendingOrders) : 0);
+    return (int) $total;
   }
 
   /**
-   * Calling this function queries the api for pending orders and returns them
-   * as an array.
+   * Queries the WooCommerce API for the oldest pending order since the last
+   * import timestamp and returns it as a Xentral-formatted array with at most
+   * one element. The caller (shopimport.php::RemoteGetAuftrag loop) expects
+   * $result[0] per iteration; this contract must be maintained.
+   *
+   * The after-filter advances per order so each caller-iteration fetches the
+   * next order. A crash between RemoteGetAuftrag() and the shopimport_auftraege
+   * INSERT loses at most this one order (consistent with pre-#262 behaviour).
    *
-   * TODO: Only one single order is returned per invocation of this function.
-   * Given that we have to perform an exteremly expensive external HTTP call
-   * every time we call this function and could easily process more than one
-   * order this seems very bad performance-wise.
+   * @return array Array with at most one order entry, or empty array if none.
    */
   public function ImportGetAuftrag()
   {
-    // Array containing additional settings, namely 'ab_nummer' (containting the next order number to get)
-    // and 'holeallestati' (an integer)
-    $tmp = $this->CatchRemoteCommand('data');
+    $data = $this->CatchRemoteCommand('data');
 
-    // Only orders having an order number greater or equal than this should be fetched. null otherwise
-    $number_from = empty($tmp['ab_nummer']) ? null : (int) $tmp['ab_nummer'];
+    $this->migrateAbNummerIfNeeded();
 
-    // pending orders will be fetched into this array. it's length is returned at the end of the funciton
-    $pendingOrders = array();
+    $configuredStatuses = array_map('trim', explode(';', (string) $this->statusPending));
 
-    if ($number_from) {
-      // Number-based import is selected
+    if (!empty($this->lastImportOrderIds)) {
+      $afterTs = gmdate('Y-m-d\TH:i:s', max(0, strtotime($this->lastImportTimestamp) - 1));
+      $queryArgs = [
+        'status'   => $configuredStatuses,
+        'after'    => $afterTs,
+        'per_page' => 1,
+        'page'     => 1,
+        'orderby'  => 'date',
+        'order'    => 'asc',
+        'exclude'  => array_values($this->lastImportOrderIds),
+      ];
+    } else {
+      $queryArgs = [
+        'status'   => $configuredStatuses,
+        'after'    => $this->lastImportTimestamp,
+        'per_page' => 1,
+        'page'     => 1,
+        'orderby'  => 'date',
+        'order'    => 'asc',
+      ];
+    }
 
-      // The WooCommerce API doenst allow for a proper "greater than id n" request.
-      // we fake this behavior by creating an array that contains 'many' (~ 1000) consecutive
-      // ids that are greater than $from_number and use this array with the 'include' property
-      // of the WooCommerce API
+    try {
+      $pageOrders = $this->client->get('orders', $queryArgs);
+    } catch (Exception $e) {
+      $this->logger->warning('WooCommerce ImportGetAuftrag: ' . $e->getMessage());
+      return null;
+    }
 
-      $number_to = $number_from + 800;
-      if (!empty($tmp['bis_nummer'])) {
-        $number_to = $tmp['bis_nummer'];
-      }
+    if (empty($pageOrders)) {
+      return null;
+    }
 
-      $fakeGreaterThanIds = range($number_from, $number_to);
+    $wcOrder = $pageOrders[0] ?? null;
+    if ($wcOrder === null) {
+      return null;
+    }
 
-      $pendingOrders = $this->client->get('orders', [
-        'per_page' => 20,
-        'include' => implode(',', $fakeGreaterThanIds),
-        'order' => 'asc',
-        'orderby' => 'id'
-      ]);
+    $order = $this->parseOrder($wcOrder);
 
-    } else {
-      // fetch posts by status
+    // Persist tuple cursor so the next Caller-iteration advances past this order.
+    // Using ts-1s in the query means same-second peers are still fetched, while
+    // the exclude=[id] parameter prevents re-delivering this exact order.
+    if (!empty($wcOrder->date_created_gmt) && !empty($wcOrder->id)) {
+      $this->persistLastImportCursor((string) $wcOrder->date_created_gmt, (int) $wcOrder->id);
+    }
 
-      $pendingOrders = $this->client->get('orders', [
-        'status' => array_map('trim', explode(';', $this->statusPending)),
-        'per_page' => 20,
-        'order' => 'asc',
-        'orderby' => 'id'
-      ]);
+    return [[
+      'id'        => $order['auftrag'],
+      'sessionid' => '',
+      'logdatei'  => '',
+      'warenkorb' => base64_encode(serialize($order)),
+    ]];
+  }
 
+  /**
+   * Resolves a legacy WooCommerce order ID (ab_nummer) to the order's
+   * date_created_gmt timestamp for the one-shot transition from cursor-
+   * based to timestamp-based import.
+   *
+   * @param int $abNummer WooCommerce order ID
+   * @return string|null ISO-8601 UTC timestamp or null on failure
+   */
+  private function resolveAbNummerToTimestamp($abNummer)
+  {
+    try {
+      $order = $this->client->get('orders/' . $abNummer);
+    } catch (Exception $e) {
+      $this->logger->warning('WooCommerce resolveAbNummerToTimestamp(' . $abNummer . '): ' . $e->getMessage());
+      return null;
     }
 
-    // Return an empty array in case there are no orders to import
-    if ((!empty($pendingOrders) ? count($pendingOrders) : 0) === 0) {
+    if (empty($order->date_created_gmt)) {
+      $this->logger->warning('WooCommerce resolveAbNummerToTimestamp(' . $abNummer . '): date_created_gmt missing');
       return null;
     }
 
-    $tmp = [];
-
-    foreach ($pendingOrders as $pendingOrder) {
-      $wcOrder = $pendingOrder;
-      $order = $this->parseOrder($wcOrder);
-
-      if (is_null($wcOrder)) {
-        continue;
-      }
+    $ts = strtotime((string) $order->date_created_gmt);
+    if ($ts === false) {
+      return null;
+    }
+    return gmdate('Y-m-d\TH:i:s', $ts - 1);
+  }
 
-      $tmp[] = [
-        'id' => $order['auftrag'],
-        'sessionid' => '',
-        'logdatei' => '',
-        'warenkorb' => base64_encode(serialize($order)),
-      ];
+  /**
+   * Runs the one-shot legacy ab_nummer -> timestamp migration when the stored
+   * cursor is still the 30-day fallback and the caller passes an ab_nummer.
+   * Idempotent: once migrated, lastImportTimestampIsFallback flips to false
+   * and subsequent calls become no-ops.
+   */
+  private function migrateAbNummerIfNeeded()
+  {
+    if (!$this->lastImportTimestampIsFallback) {
+      return;
+    }
+    $data = $this->CatchRemoteCommand('data');
+    if (empty($data['ab_nummer'])) {
+      return;
     }
-    return $tmp;
+    $resolved = $this->resolveAbNummerToTimestamp((int) $data['ab_nummer']);
+    if ($resolved !== null) {
+      $this->persistLastImportTimestamp($resolved);
+      return;
+    }
+    // Resolution failed (order deleted, 404, missing date_created_gmt). Persist the
+    // current 30-day fallback so subsequent runs use a stable lower bound
+    // instead of sliding the window on every cron cycle.
+    $this->logger->warning(
+      sprintf(
+        'WooCommerce ab_nummer=%d konnte nicht aufgeloest werden; persistiere 30-Tage-Fallback als Cursor',
+        (int) $data['ab_nummer']
+      )
+    );
+    $this->persistLastImportTimestamp($this->lastImportTimestamp);
   }
 
   // This function searches the wcOrder for the specified WC Meta key
@@ -448,7 +507,7 @@ public function ImportDeleteAuftrag()
    * Updates the order status once payment and shipping are set to ok.
    * Also updates the order with the shipping tracking code
    * @return string
-   * @throws WCHttpClientException
+   * @throws HttpClientException
    */
   public function ImportUpdateAuftrag()
   {
@@ -502,18 +561,25 @@ public function ImportUpdateAuftrag()
   }
 
   /**
-   * This function syncs the current stock to the remote WooCommerce shop
-   * @return int
-   * @throws WCHttpClientException
+   * This function syncs the current stock to the remote WooCommerce shop.
+   * Uses WC REST v3 batch endpoints to reduce HTTP round-trips from 2n to
+   * roughly ceil(n/100) + ceil(n/100) requests.
+   *
+   * @return int Number of articles successfully synced
+   * @throws HttpClientException
    */
   public function ImportSendListLager()
   {
     $tmp = $this->CatchRemoteCommand('data');
     $anzahl = 0;
     $ctmp = (!empty($tmp) ? count($tmp) : 0);
-    $this->logger->debug("WooCommerce ImportSendListLager ".$ctmp." entries");
+
+    // --- Step 1: Collect all SKUs and compute desired stock params ---
+
+    // $pendingUpdates: sku => ['lageranzahl' => int, 'status' => string]
+    $pendingUpdates = [];
+
     for ($i = 0; $i < $ctmp; $i++) {
-      // Get important values from input data
       $artikel = $tmp[$i]['artikel'];
       if ($artikel === 'ignore') {
         continue;
@@ -527,53 +593,170 @@ public function ImportSendListLager()
       $inaktiv = $tmp[$i]['inaktiv'];
       $status = 'publish';
 
-      // Do some computations, sanitize input
-
       if ($pseudolager !== '') {
         $lageranzahl = $pseudolager;
       }
-
       if ($tmp[$i]['ausverkauft']) {
         $lageranzahl = 0;
       }
-
       if ($inaktiv) {
         $status = 'private';
       }
 
-      // get the product id that WooCommerce uses to represent the current article
-      $remoteIdInformation = $this->getShopIdBySKU($nummer);
+      $pendingUpdates[$nummer] = [
+        'lageranzahl' => $lageranzahl,
+        'status' => $status,
+      ];
+    }
+
+    if (empty($pendingUpdates)) {
+      return 0;
+    }
+
+    // --- Step 2: Bulk-resolve SKUs to WC product IDs ---
+    // WC REST v3 accepts a comma-separated list in the ?sku= parameter.
+    // We fetch in chunks of 100 to stay within per_page limits.
+
+    // $skuMap: sku => ['id' => int, 'parent' => int, 'isvariant' => bool]
+    $skuMap = [];
+    $skuChunks = array_chunk(array_keys($pendingUpdates), 100);
+
+    foreach ($skuChunks as $skuChunk) {
+      $skuCsv = implode(',', $skuChunk);
+      try {
+        $products = $this->client->get('products', [
+          'sku' => $skuCsv,
+          'per_page' => 100,
+        ]);
+      } catch (Exception $e) {
+        $this->logger->error(
+          'WooCommerce SKU-Lookup-Chunk fehlgeschlagen: ' . $e->getMessage(),
+          ['chunk_size' => count($skuChunk)]
+        );
+        continue;
+      }
+      if (!is_array($products)) {
+        continue;
+      }
+      foreach ($products as $product) {
+        if (!isset($product->sku)) {
+          continue;
+        }
+        $skuMap[$product->sku] = [
+          'id' => $product->id,
+          'parent' => $product->parent_id,
+          'isvariant' => !empty($product->parent_id),
+        ];
+      }
+    }
+
+    // --- Step 3: Split into simple products and variations ---
+    // simpleItems: list of batch-update items for POST products/batch
+    // variationItems: parent_id => list of batch-update items for POST products/{parent}/variations/batch
 
-      if (empty($remoteIdInformation['id'])) {
-        // The online shop doesnt know this article, write to log and continue with next product
-        $this->logger->error("WooCommerce Lagerzahlen Artikel $nummer wurde im Online-Shop nicht gefunden");
+    $simpleItems = [];
+    $variationItems = [];
+
+    foreach ($pendingUpdates as $sku => $params) {
+      if (!isset($skuMap[$sku])) {
+        $this->logger->error(
+          "WooCommerce Artikel $sku wurde im Online-Shop nicht gefunden! Falsche Artikelnummer im Shop hinterlegt?"
+        );
         continue;
       }
 
-      // Sync settings to online store
-      $updateProductParams = [
+      $info = $skuMap[$sku];
+      $item = [
+        'id' => $info['id'],
         'manage_stock' => true,
-        'status' => $status,
-        'stock_quantity' => $lageranzahl
-        // WooCommerce doesnt have a standard property for the other values, we're ignoring them
+        'stock_quantity' => $params['lageranzahl'],
+        'status' => $params['status'],
       ];
-      if ($remoteIdInformation['isvariant']) {
-        $result = $this->client->put('products/' . $remoteIdInformation['parent'] . '/variations/' . $remoteIdInformation['id'], $updateProductParams);
+
+      if ($info['isvariant']) {
+        $variationItems[$info['parent']][] = $item;
       } else {
-        $result = $this->client->put('products/' . $remoteIdInformation['id'], $updateProductParams);
+        $simpleItems[] = $item;
       }
+    }
 
-      $this->logger->info(
-        "WooCommerce Lagerzahlenübertragung für Artikel: $nummer / $remoteIdInformation[id] - Anzahl: $lageranzahl",
-        [
-          'result' => $result
-        ]
-      );
-      $anzahl++;
+    // --- Step 4: Send batch updates in chunks of 100, handle partial errors ---
+
+    // Simple products
+    foreach (array_chunk($simpleItems, 100) as $chunk) {
+      try {
+        $response = $this->client->post('products/batch', ['update' => $chunk]);
+        $anzahl += $this->processBatchResponse($response, 'products/batch');
+      } catch (Exception $e) {
+        $this->logger->error('WooCommerce Batch-Request fehlgeschlagen fuer products/batch: ' . $e->getMessage());
+      }
+    }
+
+    // Variations (one batch endpoint per parent product)
+    foreach ($variationItems as $parentId => $items) {
+      foreach (array_chunk($items, 100) as $chunk) {
+        $endpoint = 'products/' . $parentId . '/variations/batch';
+        try {
+          $response = $this->client->post($endpoint, ['update' => $chunk]);
+          $anzahl += $this->processBatchResponse($response, $endpoint);
+        } catch (Exception $e) {
+          $this->logger->error('WooCommerce Batch-Request fehlgeschlagen fuer ' . $endpoint . ': ' . $e->getMessage());
+        }
+      }
     }
+
     return $anzahl;
   }
 
+  /**
+   * Evaluates a WC batch response object, logs per-item errors, and returns
+   * the count of successfully updated items.
+   *
+   * @param object $response Decoded JSON response from the batch endpoint.
+   * @param string $endpoint Endpoint label used in log messages.
+   * @return int Number of items reported as updated without error.
+   */
+  private function processBatchResponse($response, $endpoint)
+  {
+    $successCount = 0;
+
+    if (!is_object($response) && !is_array($response)) {
+      $this->logger->error("WooCommerce Batch-Response ungueltig fuer $endpoint");
+      return 0;
+    }
+
+    // Successful updates are in response->update
+    $updated = is_object($response) ? ($response->update ?? []) : [];
+    foreach ($updated as $item) {
+      // WC embeds per-item errors inside the update array when an item fails
+      if (isset($item->error)) {
+        $code = $item->error->code ?? '';
+        $message = $item->error->message ?? '';
+        $this->logger->error(
+          "WooCommerce Batch-Fehler ($endpoint) fuer ID {$item->id}: [$code] $message"
+        );
+      } else {
+        $this->logger->info(
+          "WooCommerce Lagerzahlenübertragung (Batch) fuer Artikel-ID {$item->id} erfolgreich",
+          ['endpoint' => $endpoint]
+        );
+        $successCount++;
+      }
+    }
+
+    // Top-level errors array (some WC versions use this)
+    $errors = is_object($response) ? ($response->errors ?? []) : [];
+    foreach ($errors as $err) {
+      $code = $err->code ?? '';
+      $message = $err->message ?? '';
+      $this->logger->error(
+        "WooCommerce Batch-Fehler ($endpoint): [$code] $message"
+      );
+    }
+
+    return $successCount;
+  }
+
   public function ImportStorniereAuftrag()
   {
     $orderId = $this->CatchRemoteCommand('data')['auftrag'];
@@ -592,84 +775,9 @@ public function ImportSendList()
     $tmp = $this->catchRemoteCommand('data');
     $anzahl = 0;
     $return = [];
-    $ctmp = (!empty($tmp) ? count($tmp) : 0);
-    $this->logger->debug("WooCommerce ImportSendList ".$ctmp." entries");
-
-    // Prepare files
-    $alle_dateien = array(); // pfad, dateiname
-    if ($this->dateienuebertragen) {
-        for ($i = 0; $i < $ctmp; $i++) {
-            $dateien_artikel = array();
-            if (isset($tmp[$i]['Dateien'])) {
-                // apply naming
-                foreach ($tmp[$i]['Dateien'] as $key => $datei) {
-                    if (!empty($this->filenamesmarty)) {
-                        $smarty = new Smarty;
-                        $directory = $this->app->erp->GetTMP().'/smarty/templates';
-                        $smarty->setCompileDir($directory);
-                        $dateidata = $datei;
-                        $dateidata['dateiname'] = pathinfo($dateidata['filename'] , PATHINFO_FILENAME);
-                        $dateidata['endung'] = pathinfo($dateidata['filename'], PATHINFO_EXTENSION);
-                        $dateidata['artikel'] = (object) $datei['artikeldata'];
-                        $smarty->assign('artikel', (object) $tmp[$i]);
-                        $smarty->assign('datei', (object) $dateidata);
-                        $transformed = $smarty->fetch('string:'.$this->filenamesmarty);
-                        if (!empty($transformed)) {
-                            $basename = pathinfo($transformed, PATHINFO_FILENAME);
-                            $ext = pathinfo($transformed, PATHINFO_EXTENSION);
-                            if (empty($ext)) {
-                                $ext = $datei['dateiendung'];
-                            }
-                            $datei['filename'] = $basename.".".$ext;
-                        }
-                    }
-                    // Ensure unique names
-                    if (in_array($datei['filename'],array_column($dateien_artikel,'filename'))) {
-                        $filecount = 0;
-                        $base = pathinfo($datei['filename'], PATHINFO_FILENAME);
-                        $extension = pathinfo($datei['filename'], PATHINFO_EXTENSION);
-                        do {
-                            $filecount++;
-                            $filename = $base.$this->fileunique.$filecount.'.'.$extension;
-                        } while (in_array($filename,array_column($dateien_artikel,'filename')));
-                        $datei['filename'] = $filename;
-                    }
-                    unset($datei['artikeldata']);
-                    $dateien_artikel[] = $datei;
-                }
-                $alle_dateien = array_merge($alle_dateien,$dateien_artikel);
-            }
-        } // Artikel loop files
-
-        // Each filename only once
-        $unique_filename = array_unique(array_column($alle_dateien, 'filename'));
-        $alle_dateien = array_intersect_key($alle_dateien, $unique_filename);
-        $this->logger->debug("WooCommerce ImportSendList ".count($alle_dateien)." files", $alle_dateien);
-
-        // Upload all the files and save wordpress id
-        $uploaded_files = $this->uploadFiles($alle_dateien);
-        $this->logger->debug("WooCommerce ImportSendList uploaded " .count($uploaded_files)." files", $uploaded_files);
-    }
-    // Files
-
-    for ($i = 0; $i < $ctmp; $i++) {
+    for ($i = 0; $i < (!empty($tmp) ? count($tmp) : 0); $i++) {
       $return[$i] = new ArticleExportResult();
-      $return[$i]->success = true;
       $artikel = $tmp[$i]['artikel'];
-
-        if (!empty($this->productsmarty)) {
-            $smarty = new Smarty;
-            $directory = $this->app->erp->GetTMP().'/smarty/templates';
-            $smarty->setCompileDir($directory);
-            $smarty->assign('artikel', (object) $tmp[$i]);
-            $smarty_array = json_decode($this->productsmarty, true);
-            if (empty($smarty_array)) {
-                throw new Exception("Smarty template JSON decode error! ".print_r(json_last_error_msg(),true));
-            }
-            $transformed = json_decode($smarty->fetch('string:'.$this->productsmarty),true);
-            $tmp[$i] = array_merge((array) $tmp[$i], $transformed);
-        }
-
       $return[$i]->articleId = intval($artikel);
       $nummer = $tmp[$i]['nummer'];
       if (!empty($tmp[$i]['artikelnummer_fremdnummern'][0]['nummer'])) {
@@ -682,11 +790,8 @@ public function ImportSendList()
       $hersteller = $tmp[$i]['hersteller'];
       $herstellerlink = $tmp[$i]['herstellerlink'];
 
-      $name_de = html_entity_decode($tmp[$i]['name_de']);
-      $name_en = html_entity_decode($tmp[$i]['name_en']);
-
-//        print_r($tmp[$i]); exit();
-
+      $name_de = $tmp[$i]['name_de'];
+      $name_en = $tmp[$i]['name_en'];
       $description = html_entity_decode($tmp[$i]['uebersicht_de']);
       $description_en = html_entity_decode($tmp[$i]['uebersicht_en']);
       $preis = $tmp[$i]['preis'];
@@ -715,7 +820,7 @@ public function ImportSendList()
       $meta_desc = $tmp[$i]['metadescription_de'];
       $meta_title = $tmp[$i]['metatitle_de'];
 
-      $pseudopreis = $tmp[$i]['pseudopreis'];// *1.19;
+      $pseudopreis = $tmp[$i]['pseudopreis'];//*1.19;
       if ($pseudopreis <= $preis)
         $pseudopreis = $preis;
       $steuersatz = $tmp[$i]['steuersatz'];
@@ -754,9 +859,6 @@ public function ImportSendList()
         ['key' => '_yoast_wpseo_title', 'value' => $meta_title],
       ];
 
-        foreach ($tmp[$i]['meta_data'] as $key => $value) {
-            $commonMetaData[] = ['key' => $this->metadataprefix.$key, 'value' => $value];
-        }
 
       // Attributes that are used for both updating an existing product as well as creating a new one
       $commonProductAtts = [
@@ -772,7 +874,6 @@ public function ImportSendList()
           'width' => $dim_width,
           'height' => $dim_height
         ],
-        'global_unique_id' => $tmp[$i]['ean'],
         'meta_data' => $commonMetaData,
       ];
 
@@ -791,44 +892,8 @@ public function ImportSendList()
         $commonProductAtts['stock_quantity'] = (int) $lageranzahl;
       }
 
-      if (isset($tmp[$i]['Dateien'])) {
-        if (!empty($uploaded_files)) {
-            $attachments = array();
-
-            foreach ($tmp[$i]['Dateien'] as $datei) {
-                // find file in uploaded_files
-                $key = array_search($datei['dateipfad'], array_column($uploaded_files, 'dateipfad'));
-                if ($key !== false) {
-                    $uploaded_file = $uploaded_files[$key];
-                    if ($uploaded_file['type'] == 'shopbild') {
-                        $commonProductAtts['images'][] = [
-                            'id' => $uploaded_file['wordpressid']
-                        ];
-                    }
-                    else if (in_array(strtolower($uploaded_file['type']),$this->dateienuebertragen)) {
-                        $attachments[] = array('name' => $uploaded_file['name'], 'wp_media_id' => $uploaded_file['wordpressid'], 'typ' => $uploaded_file['type']);
-                    }
-                } else {
-                    $return[$i]->message .= "Datei wurde nicht exportiert: ".$datei['filename'];
-                }
-            }
-
-            array_multisort(
-                array_column($attachments, 'name'),
-                SORT_ASC,
-                SORT_NATURAL,
-                $attachments
-            );
-
-            if (!empty($attachments)) {
-                $commonProductAtts['meta_data'][] = array('key' => 'openxe_product_attachments', 'value' => $attachments);
-            }
-        }
-      }
-
       if (!is_null($product_id)) {
         // Product exists - check if it's a variation or regular product
-        $this->logger->debug("WooCommerce ImportSendList update product");
         if ($isVariant && !empty($parent_id)) {
           // This is a VARIATION - use the variations endpoint
           // Variations don't support certain attributes (they inherit from parent)
@@ -850,105 +915,110 @@ public function ImportSendList()
 
           $this->client->put('products/' . $parent_id . '/variations/' . $product_id, $variationAtts);
 
-          $this->logger->info("WooCommerce Variante geändert für Artikel: $nummer / Variation: $product_id (Parent: $parent_id), noch ".($ctmp - $i -1 )." Artikel");
+          $this->logger->info("WooCommerce Variante geändert für Artikel: $nummer / Variation: $product_id (Parent: $parent_id)");
         } else {
           // This is a regular product
           $this->client->put('products/' . $product_id, array_merge([
 
           ], $commonProductAtts));
 
-          $this->logger->info("WooCommerce Artikel geändert für Artikel: $nummer / $product_id, noch ".($ctmp - $i -1 )." Artikel");
+          $this->logger->info("WooCommerce Artikel geändert für Artikel: $nummer / $product_id");
         }
       } else {
         // create a new product
-        $this->logger->debug("WooCommerce ImportSendList create product");
         $product_id = $this->client->post('products/', array_merge([
           'sku' => $nummer,
         ], $commonProductAtts))->id;
-        $this->logger->info("WooCommerce neuer Artikel angelegt: $nummer, noch ".($ctmp - $i - 1)." Artikel");
+        $this->logger->info("WooCommerce neuer Artikel angelegt: $nummer");
       }
 
-      // TODO: Kategoriebaum wird noch nicht uebertragen
+      // TODO: Kategoriebaum und Bilder werden noch nicht uebertragen
 
       // if(isset($tmp[$i]['kompletter_kategorienbaum'])){
       //   $baum = $tmp[$i]['kompletter_kategorienbaum'];
       //   $this->updateKategorieBaum($baum);
       // }
 
+      // if(isset($tmp[$i]['Dateien'])){
+      //   $dateien = $tmp[$i]['Dateien'];
+      //   $this->save_images($dateien, $product_id);
+      // }
+
       // Update the associated product categories
 
-      if ($this->kategorienuebertragen) {
-          $chosenCats = array();
-          if (isset($tmp[$i]['kategorien']) || isset($tmp[$i]['kategoriename'])) {
-            $kategorien = $tmp[$i]['kategorien'];
-            if (!($kategorien) && !self::emptyString($tmp[$i]['kategoriename'])) {
-              $kategorien = array(
-                array(
-                  'name' => $tmp[$i]['kategoriename'],
-                )
-              );
+      $chosenCats = array();
+      if (isset($tmp[$i]['kategorien']) || isset($tmp[$i]['kategoriename'])) {
+        $kategorien = $tmp[$i]['kategorien'];
+        if (!($kategorien) && !self::emptyString($tmp[$i]['kategoriename'])) {
+          $kategorien = array(
+            array(
+              'name' => $tmp[$i]['kategoriename'],
+            )
+          );
+        }
+        if ((!empty($kategorien) ? count($kategorien) : 0) > 0) {
+          // Retrive all WC categories via API
+          $allWooCommerceCategories = $this->client->get('products/categories', ['per_page' => '100']);
+
+          $searchWpCategories = [];
+          foreach ($allWooCommerceCategories as $a) {
+            $searchWpCategories[$a->id] = $a->name;
+          }
+          // searchWPCategories is an assoc array of type WCCatId(Int) -> WCCatName(string)
+
+          // Iterate over the categories that are choosen in xentral
+          foreach ($kategorien as $k => $v) {
+            $wawi_cat_name = $v['name'];
+
+            $wcCatId = null;
+
+            // If WC has a matching category. We match based on name!
+            if (array_search($wawi_cat_name, array_values($searchWpCategories)) !== false) {
+              // get  id of that WC Category
+              $wcCatId = array_search($wawi_cat_name, $searchWpCategories);
+
+            } else {
+              // No matching category exists
+              $wcCatId = $this->client->post('products/categories', [
+                'name' => $wawi_cat_name,
+              ])->id;
+
             }
-            if ((!empty($kategorien) ? count($kategorien) : 0) > 0) {
-              // Retrive all WC categories via API
-              $allWooCommerceCategories = $this->client->get('products/categories', ['per_page' => '100']);
 
-              $searchWpCategories = [];
-              foreach ($allWooCommerceCategories as $a) {
-                $searchWpCategories[$a->id] = $a->name;
+            if ($wcCatId) {
+              // update category. We first retrieve the product and append the new product category, not replace the entire category array.
+              $alreadyAssignedWCCats = $this->client->get('products/' . $product_id, [
+                'per_page' => 1,
+              ])->categories;
+
+              // Get ids of existing categories
+              $existingCategoryIds = [];
+              foreach ($alreadyAssignedWCCats as $cat) {
+                $existingCategoryIds[] = $cat->id;
               }
-              // searchWPCategories is an assoc array of type WCCatId(Int) -> WCCatName(string)
-
-              // Iterate over the categories that are choosen in xentral
-              foreach ($kategorien as $k => $v) {
-                $wawi_cat_name = $v['name'];
-
-                $wcCatId = null;
-
-                // If WC has a matching category. We match based on name!
-                if (array_search($wawi_cat_name, array_values($searchWpCategories)) !== false) {
-                  // get  id of that WC Category
-                  $wcCatId = array_search($wawi_cat_name, $searchWpCategories);
-
-                } else {
-                  // No matching category exists
-                  $wcCatId = $this->client->post('products/categories', [
-                    'name' => $wawi_cat_name,
-                  ])->id;
-
-                }
-
-                if ($wcCatId) {
-                  // update category. We first retrieve the product and append the new product category, not replace the entire category array.
-                  $alreadyAssignedWCCats = $this->client->get('products/' . $product_id, [
-                    'per_page' => 1,
-                  ])->categories;
-
-                  // Get ids of existing categories
-                  $existingCategoryIds = [];
-                  foreach ($alreadyAssignedWCCats as $cat) {
-                    $existingCategoryIds[] = $cat->id;
-                  }
-
-                  $allCatIds = array_merge($existingCategoryIds, array($wcCatId));
-
-                  // prepare data to be in correct format for WC api. should be individual items with key 'id' and id as value
-                  $allCatIdsWCAPIRep = array();
-                  foreach ($allCatIds as $id) {
-                    $allCatIdsWCAPIRep[] = ['id' => $id];
-                  }
-
-                  // Update category assignment
-                  $this->client->put('products/' . $product_id, [
-                    'categories' => $allCatIdsWCAPIRep,
-                  ]);
-
-                  $chosenCats[] = $wcCatId;
-                }
+
+              $allCatIds = array_merge($existingCategoryIds, array($wcCatId));
+
+              // prepare data to be in correct format for WC api. should be individual items with key 'id' and id as value
+              $allCatIdsWCAPIRep = array();
+              foreach ($allCatIds as $id) {
+                $allCatIdsWCAPIRep[] = ['id' => $id];
               }
+
+              // Update category assignment
+              $this->client->put('products/' . $product_id, [
+                'categories' => $allCatIdsWCAPIRep,
+              ]);
+
+              $chosenCats[] = $wcCatId;
             }
-        } // kategorienuebertragen
+          }
+        }
       }
+
+      $return[$i]->success = true;
     }
+
     return $return;
     // return array($product_id,$anzahl,$nummer,$steuersatz, $preis);
   }
@@ -962,17 +1032,10 @@ public function ImportAuth()
   {
     try {
       $orders = $this->client->get('orders', ['per_page' => '1']);
+      return 'success';
     } catch (Exception $e) {
       return 'failed: Keine Verbindung zur API - ' . $e->getMessage();
     }
-
-    if (!empty($this->ImportWordpressUserName)) {
-        $result = $this->wordpress_request('users/me', debug: true, ssl_ignore: true);
-        if ($result['status'] != 1) {
-            return 'failed: Verbindung zur Wordpress API '.$result['message'];
-        }
-    }
-    return 'success';
   }
 
   /**
@@ -980,7 +1043,7 @@ public function ImportAuth()
    * @param  [type] $shopid [description]
    * @param  [type] $data   [description]
    * @return [type]         [description]
-   * @throws WCHttpClientException
+   * @throws HttpClientException
    */
   public function getKonfig($shopid, $data)
   {
@@ -1001,41 +1064,127 @@ public function getKonfig($shopid, $data)
     $ImportWooCommerceApiKey = $felder['ImportWoocommerceApiKey'] ?? '';
     $ImportWooCommerceApiUrl = $felder['ImportWoocommerceApiUrl'] ?? '';
 
-    $this->ImportWordpressUserName = $felder['ImportWordpressUserName'];
-    $this->ImportWordpressApplicationPassword = $felder['ImportWordpressApplicationPassword'];
-
-    $this->filenamesmarty = $felder['filenamesmarty'];
-    $this->productsmarty = $felder['productsmarty'];
-    $this->fileunique = $felder['fileunique'];
-    $this->metadataprefix = $felder['metadataprefix'];
-
-    $shopexportArr =  $this->app->DB->SelectRow("SELECT * FROM shopexport WHERE id = '$this->shopid' LIMIT 1");
-
-    $dateienuebertragen = $shopexportArr['dateienuebertragen'];
-    $this->kategorienuebertragen = $shopexportArr['kategorienuebertragen'];
-
-    $this->dateienuebertragen = explode(',',str_replace(' ','',strtolower($dateienuebertragen)));
-
     $this->statusPending = $felder['statusPending'] ?? 'pending';
     $this->statusProcessing = $felder['statusProcessing'] ?? 'processing';
     $this->statusCompleted = $felder['statusCompleted'] ?? 'completed';
-    $this->timeout = (int) $felder['timeout'] ?? SELF::TIMEOUT;
 
     $this->priceType = $felder['priceType'] ?? null;
 
     $this->url = $ImportWooCommerceApiUrl;
-    $this->client = new WCClient(
+    $this->client = new ClientWrapper(
       //URL des WooCommerce Rest Servers
       $ImportWooCommerceApiUrl,
       //WooCommerce API Key
       $ImportWooCommerceApiKey,
       //WooCommerce API Secret
       $ImportWooCommerceApiSecret,
-      ["query_string_auth" => true, 'timeout' => $this->timeout],
+      ["query_string_auth" => true],
       $this->logger,
       $this->ssl_ignore
     );
 
+    $storedTimestamp = $preferences['felder']['letzter_import_timestamp'] ?? null;
+    if (!empty($storedTimestamp)) {
+      $this->lastImportTimestamp = $storedTimestamp;
+      $this->lastImportTimestampIsFallback = false;
+    } else {
+      $this->lastImportTimestamp = gmdate('Y-m-d\TH:i:s', strtotime('-30 days'));
+      $this->lastImportTimestampIsFallback = true;
+    }
+
+    $storedIds = $preferences['felder']['letzter_import_order_ids'] ?? null;
+    $this->lastImportOrderIds = is_array($storedIds)
+      ? array_values(array_filter(array_map('intval', $storedIds)))
+      : [];
+
+  }
+
+  /**
+   * Backwards-compatible wrapper: persists timestamp only (order id cleared).
+   * Use persistLastImportCursor() when both timestamp and order id are available.
+   *
+   * @param string $isoUtcDate ISO-8601 UTC timestamp, e.g. '2026-04-20T12:34:56'
+   * @return void
+   */
+  public function persistLastImportTimestamp($isoUtcDate)
+  {
+    $this->persistLastImportCursor($isoUtcDate, null);
+  }
+
+  /**
+   * Persists the tuple cursor (timestamp + accumulated order-id bucket) to
+   * shopexport.einstellungen_json. Does a read-modify-write to preserve all
+   * other fields.
+   *
+   * Bucket logic:
+   *  - $orderId === null  → migration path; ids list is cleared.
+   *  - same timestamp as stored → append $orderId to the ids list.
+   *  - new timestamp → reset ids list to [$orderId].
+   *
+   * @param string   $isoUtcDate ISO-8601 UTC timestamp, e.g. '2026-04-20T12:34:56'
+   * @param int|null $orderId    WooCommerce order ID, or null (migration path)
+   * @return void
+   */
+  public function persistLastImportCursor($isoUtcDate, $orderId = null)
+  {
+    $shopid = (int)$this->shopid;
+    // Prefer DatabaseService when available (web context), fall back to DB
+    // so this method also works in the CLI/cron context.
+    if (!empty($this->app->DatabaseService)) {
+      $einstellungen_json = $this->app->DatabaseService->selectValue(
+        "SELECT einstellungen_json FROM shopexport WHERE id = :id LIMIT 1",
+        ['id' => $shopid]
+      );
+    } else {
+      $einstellungen_json = $this->app->DB->Select(
+        "SELECT einstellungen_json FROM shopexport WHERE id = '$shopid' LIMIT 1"
+      );
+    }
+    $current = [];
+    if (!empty($einstellungen_json)) {
+      $current = json_decode($einstellungen_json, true) ?: [];
+    }
+    if (!isset($current['felder']) || !is_array($current['felder'])) {
+      $current['felder'] = [];
+    }
+
+    $previousTs  = $current['felder']['letzter_import_timestamp'] ?? null;
+    $previousIds = $current['felder']['letzter_import_order_ids'] ?? [];
+    if (!is_array($previousIds)) {
+      $previousIds = [];
+    }
+
+    // Determine ids list for the new state.
+    if ($orderId === null) {
+      // Migration path — timestamp without a concrete order-id anchor.
+      $newIds = [];
+    } elseif ($previousTs !== null && $isoUtcDate === $previousTs) {
+      // Same timestamp bucket — append id if not already present.
+      $newIds = $previousIds;
+      if (!in_array((int) $orderId, $newIds, true)) {
+        $newIds[] = (int) $orderId;
+      }
+    } else {
+      // New timestamp bucket — reset list to only this id.
+      $newIds = [(int) $orderId];
+    }
+
+    $current['felder']['letzter_import_timestamp']  = $isoUtcDate;
+    $current['felder']['letzter_import_order_ids']  = $newIds;
+
+    $jsonEncoded = $this->app->DB->real_escape_string(json_encode($current));
+    if (!empty($this->app->DatabaseService)) {
+      $this->app->DatabaseService->execute(
+        "UPDATE shopexport SET einstellungen_json = :json WHERE id = :id",
+        ['json' => json_encode($current), 'id' => $shopid]
+      );
+    } else {
+      $this->app->DB->Update(
+        "UPDATE shopexport SET einstellungen_json = '$jsonEncoded' WHERE id = '$shopid'"
+      );
+    }
+    $this->lastImportTimestamp = $isoUtcDate;
+    $this->lastImportOrderIds  = $newIds;
   }
 
   /**
@@ -1071,7 +1220,7 @@ public function AuthByAssistent()
     if (empty($ImportWooCommerceApiSecret)) {
       return new JsonResponse(['error' => 'Bitte das API-Secret angeben'], JsonResponse::HTTP_BAD_REQUEST);
     }
-    $this->client = new WCClient(
+    $this->client = new ClientWrapper(
       $ImportWooCommerceApiUrl,
       $ImportWooCommerceApiKey,
       $ImportWooCommerceApiSecret,
@@ -1140,7 +1289,7 @@ public function getCreateForm()
    * @param string $sku Artikelnummer
    *
    * @return array|null The WooCommerce product id of the given product, null if such a product does not exist
-   * @throws WCHttpClientException
+   * @throws HttpClientException
    */
   private function getShopIdBySKU($sku)
   {
@@ -1180,37 +1329,15 @@ public function EinstellungenStruktur()
         'ausblenden' => array('abholmodus' => array('zeitbereich')),
         'archiv' => array('ab_nummer'),
         'felder' => array(
-        //          'protokoll'=>array('typ'=>'checkbox','bezeichnung'=>'Protokollierung im Logfile:'),
-        'ssl_ignore' => array('typ' => 'checkbox', 'bezeichnung' => 'SSL-Prüfung abschalten:', 'info' => 'Nur für Testzwecke!'),
-        'ImportWoocommerceApiKey' => array('typ' => 'text', 'bezeichnung' => '{|API Key:', 'size' => 60),
-        'ImportWoocommerceApiSecret' => array('typ' => 'text', 'bezeichnung' => '{|API Secret|}:', 'size' => 60),
-        'ImportWoocommerceApiUrl' => array('typ' => 'text', 'bezeichnung' => '{|API Url|}:', 'size' => 40),
-        'ImportWordpressUserName' => array('typ' => 'text', 'bezeichnung' => '{|Wordpress Benutzername:', 'size' => 60, 'info' => 'Für Dateiuploads'),
-        'ImportWordpressApplicationPassword' => array('typ' => 'text', 'bezeichnung' => '{|Wordpress Anwendungspasswort:', 'size' => 60, 'info' => 'Für Dateiuploads'),
-        'statusPending' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung offen|}:', 'size' => 40, 'default' => 'pending', 'info' => '({|ggfs. getrennt durch ";": pending;on-hold|})'),
-        'statusProcessing' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung in Bearbeitung|}:', 'size' => 10, 'default' => 'processing'),
-        'statusCompleted' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung fertig|}:', 'size' => 10, 'default' => 'completed'),
-        'priceType' => array('typ' => 'select', 'bezeichnung' => '{|Preisberechnungsgrundlage bei Auftragsimport|}', 'optionen' => array('netcalculated' => '{|Nettopreis zurückrechnen (Standard)|}', 'grosscalculated' => '{|Bruttopreis zurückrechnen|}')),
-        'timeout' => array('typ' => 'text', 'bezeichnung' => '{|Timeout in Sekunden|}:', 'size' => 40, 'default' => '30', 'info' => ''),
-        'metadataprefix' => array('typ' => 'text', 'bezeichnung' => '{|Präfix für Metadaten|}:', 'size' => 40, 'default' => 'openxe_meta_', 'info' => ''),
-        'productsmarty' => [
-            'typ' => 'textarea',
-            'cols' => 80,
-            'rows' => 5,
-            'bezeichnung' => '{|Smarty-Template JSON|}:',
-            'info' => 'Beispiel:
{
    "name_de": "{$artikel->name_en}",
    "meta_data": {
        "mpn": "{$artikel->herstellernummer}",
        "manufacturer": "{$artikel->hersteller}"
    }
}', - 'size' => 120, - ], - 'filenamesmarty' => [ - 'typ' => 'textarea', - 'cols' => 80, - 'rows' => 5, - 'bezeichnung' => '{|Smarty-Template für Benennung der Dateien|}:', - 'info' => 'Beispiel:{$artikel->hersteller}-{$artikel->herstellernummer}-{$artikel->stichwort}-{$datei->titel}.{$datei->endung}
Dateifelder: dateiname, endung, mimetype, titel, beschreibung, id, version, stichwort, extid', - 'size' => 120, - ], - 'fileunique' => array('typ' => 'text', 'bezeichnung' => '{|Trennzeichen für Mehrfachdateinamen|}:', 'size' => 11, 'default' => '-'), - 'dateienuebertragen' => array('typ' => 'info', 'bezeichnung' => '{|Dateiübertragung|}', 'info' => 'Dateien werden über Wordpress API hochgeladen und im Produkt in den Metadaten als Array "product_attachments" mit Präfix (s.o.) hinterlegt. Diese können dann z.B. mit einem Snippet angezeigt werden. Felder: "typ", "wp_media_id", "name". Der URL kann mit wp_get_attachment_url($wp_media_id) ermittelt werden.'), + // 'protokoll'=>array('typ'=>'checkbox','bezeichnung'=>'Protokollierung im Logfile:'), + 'ssl_ignore' => array('typ' => 'checkbox', 'bezeichnung' => 'SSL-Prüfung abschalten:', 'info' => 'Nur für Testzwecke!'), + 'ImportWoocommerceApiKey' => array('typ' => 'text', 'bezeichnung' => '{|API Key:', 'size' => 60), + 'ImportWoocommerceApiSecret' => array('typ' => 'text', 'bezeichnung' => '{|API Secret|}:', 'size' => 60), + 'ImportWoocommerceApiUrl' => array('typ' => 'text', 'bezeichnung' => '{|API Url|}:', 'size' => 40), + 'statusPending' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung offen|}:', 'size' => 40, 'default' => 'pending', 'info' => '({|ggfs. getrennt durch ";": pending;on-hold|})'), + 'statusProcessing' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung in Bearbeitung|}:', 'size' => 10, 'default' => 'processing'), + 'statusCompleted' => array('typ' => 'text', 'bezeichnung' => '{|Statusname Bestellung fertig|}:', 'size' => 10, 'default' => 'completed'), + 'priceType' => array('typ' => 'select', 'bezeichnung' => '{|Preisberechnungsgrundlage bei Auftragsimport|}', 'optionen' => array('netcalculated' => '{|Nettopreis zurückrechnen (Standard)|}', 'grosscalculated' => '{|Bruttopreis zurückrechnen|}')), ) ); } @@ -1246,1553 +1373,4 @@ protected static function emptyString($string) return (strlen(trim($string)) == 0); } - function wordpress_request(string $endpoint, $postdata = null, $method = null, $getdata = null, string $content_disposition = null, string $content_type = null, $debug = false, $debugurl = null, bool $ssl_ignore = false, int $timeout_seconds = 30) { - $ch = curl_init(); - $url_addition = ""; - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - curl_setopt($ch, CURLOPT_USERPWD, $this->ImportWordpressUserName . ":" . $this->ImportWordpressApplicationPassword); - - if (!empty($getdata)) { - $url_addition = "?"; - $ampersand = ""; - foreach ($getdata as $key => $value) { - $url_addition .= $ampersand . $key . "=" . $value; - $ampersand = "&"; - } - } - if (!empty($postdata)) { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata); - $headers[] = 'Content-Type: ' . $content_type; - if (!empty($content_disposition)) { - $headers[] = 'Content-Disposition: '.$content_disposition; - } - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } - - if ($debugurl) { - $url = $debugurl; - } else { - $url = $this->shopUrl; - } - - curl_setopt($ch, CURLOPT_URL, $url . $this->url. $this->wordpress_api_path . $endpoint . $url_addition); - - curl_setopt($ch, CURLINFO_HEADER_OUT, true); - curl_setopt($ch, CURLOPT_VERBOSE, true); - - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, !$ssl_ignore); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, !$ssl_ignore); - - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout_seconds); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout_seconds); - - $response = curl_exec($ch); - if (curl_error($ch)) { - $this->error[] = curl_error($ch); - } else { - $response = json_decode($response, associative: true); - } - curl_close($ch); - - $information = curl_getinfo($ch); - - $result = [ - 'status' => ($information['http_code'] >= 200 && $information['http_code'] <= 299), - 'response' => $response, - 'message' => $response['message'] - ]; - - if ($debug) { - $result['curl_getinfo'] = $information; - $result['curl_postdata_500_bytes'] = mb_substr($postdata,0,500); - $this->logger->debug( - 'Woocommerce Wordpress debug', - $result - ); - } - - return $result; - } - - /* - * Upload files to wordpress API - * Returns list of files with url - */ - - function uploadFiles($dateien) { - - $uploadFilesResult = array(); - - foreach ($dateien as $datei) { - - $fileResult = [ - 'success' => true, - 'name'=> $datei['filename'], - 'type' => strtolower($datei['stichwort']) - ]; - - $postdata = file_get_contents($datei['dateipfad']); - - $result = $this->wordpress_request( - endpoint: 'media', - getdata: [ - 'search' => urlencode($datei['filename']) - ], - debug: true, - ssl_ignore: true - ); - - if ($result['status'] == 1) { - $response = $result['response']; - - $file_wordpress_id = $response[0]['id']; - - if (is_numeric($file_wordpress_id)) { // File exists -> update - $result = $this->wordpress_request( - endpoint: 'media/' . $file_wordpress_id, - postdata: $postdata, - method: 'POST', - content_disposition: 'attachment; filename="' . $datei['filename'] . '"', - content_type: $datei['mimetype'] ?: 'application:octet-stream', - debug: true, - ssl_ignore: true - ); - - $response = $result['response']; - - if ($result['status'] != 1 || $response['id'] != $file_wordpress_id) { - $fileResult['success'] = false; - $fileResult['status'] = 'file update failed'; - } else { - $fileResult['status'] = 'updated'; - $fileResult['wordpressid'] = $response['id']; - $fileResult['url'] = $response['guid']['rendered']; - } - } else { // New file - $result = $this->wordpress_request( - endpoint: 'media', - postdata: $postdata, - method: 'POST', - content_disposition: 'attachment; filename="' . $datei['filename'] . '"', - content_type: $datei['mimetype'] ?: 'application:octet-stream', - debug: true, - ssl_ignore: true - ); - if ($result['http_code'] >= 200 && $result['http_code'] <= 299) { - $fileResult['status'] = 'created'; - $fileResult['wordpressid'] = $response['id']; - $fileResult['url'] = $response['guid']['rendered']; - } else { - $fileResult['success'] = false; - $fileResult['status'] = 'file creation failed'.$result['http_code']?(" http code".$result['http_code']):''; - } - } - } else { - $fileResult['success'] = false; - $fileResult['status'] = 'media search failed'; - } - $fileResult['dateipfad'] = $datei['dateipfad']; - $uploadFilesResult[] = $fileResult; - } - $this->logger->debug( - 'WooCommerce upload files', - $uploadFilesResult - ); - - return($uploadFilesResult); - } -} - - -class WCClient -{ - /** - * WooCommerce REST API WCClient version. - */ - const VERSION = '3.0.0'; - - /** - * HttpClient instance. - * - * @var WCHttpClient - */ - public $http; - - /** @var Logger $logger */ - public $logger; - - public $ssl_ignore = false; - - /** - * Initialize client. - * - * @param string $url Store URL. - * @param string $consumerKey Consumer key. - * @param string $consumerSecret Consumer secret. - * @param array $options WCOptions (version, timeout, verify_ssl). - * - * @throws WCHttpClientException - */ - public function __construct($url, $consumerKey, $consumerSecret, $options = [], $logger, $ssl_ignore) - { - $this->http = new WCHttpClient($url, $consumerKey, $consumerSecret, $options, $logger, $ssl_ignore); - $this->logger = $logger; - } - - /** - * POST method. - * - * @param string $endpoint API endpoint. - * @param array $data WCRequest data. - * - * @throws WCHttpClientException - * - * @return array - */ - public function post($endpoint, $data) - { - return $this->http->request($endpoint, 'POST', $data); - } - - /** - * PUT method. - * - * @param string $endpoint API endpoint. - * @param array $data WCRequest data. - * - * @throws WCHttpClientException - * - * @return array - */ - public function put($endpoint, $data) - { - return $this->http->request($endpoint, 'PUT', $data); - } - - /** - * GET method. - * - * @param string $endpoint API endpoint. - * @param array $parameters WCRequest parameters. - * - * @throws WCHttpClientException - * - * @return array - */ - public function get($endpoint, $parameters = []) - { - return $this->http->request($endpoint, 'GET', [], $parameters); - } - - /** - * DELETE method. - * - * @param string $endpoint API endpoint. - * @param array $parameters WCRequest parameters. - * - * @throws WCHttpClientException - * - * @return array - */ - public function delete($endpoint, $parameters = []) - { - return $this->http->request($endpoint, 'DELETE', [], $parameters); - } - - /** - * OPTIONS method. - * - * @param string $endpoint API endpoint. - * - * @throws WCHttpClientException - * - * @return array - */ - public function options($endpoint) - { - return $this->http->request($endpoint, 'OPTIONS'); - } -} - -class WCResponse -{ - /** - * WCResponse code. - * - * @var int - */ - private $code; - - /** - * WCResponse headers. - * - * @var array - */ - private $headers; - - /** - * WCResponse body. - * - * @var string - */ - private $body; - - /** - * Initialize response. - * - * @param int $code WCResponse code. - * @param array $headers WCResponse headers. - * @param string $body WCResponse body. - */ - public function __construct($code = 0, $headers = [], $body = '') - { - $this->code = $code; - $this->headers = $headers; - $this->body = $body; - } - - /** - * Set code. - * - * @param int $code WCResponse code. - */ - public function setCode($code) - { - $this->code = (int) $code; - } - - /** - * Set headers. - * - * @param array $headers WCResponse headers. - */ - public function setHeaders($headers) - { - $this->headers = $headers; - } - - /** - * Set body. - * - * @param string $body WCResponse body. - */ - public function setBody($body) - { - $this->body = $body; - } - - /** - * Get code. - * - * @return int - */ - public function getCode() - { - return $this->code; - } - - /** - * Get headers. - * - * @return array $headers WCResponse headers. - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * Get body. - * - * @return string $body WCResponse body. - */ - public function getBody() - { - return $this->body; - } -} - -class WCOptions -{ - /** - * Default WooCommerce REST API version. - */ - const VERSION = 'wc/v3'; - - /** - * Default request timeout. - */ - const TIMEOUT = 30; - - /** - * Default WP API prefix. - * Including leading and trailing slashes. - */ - const WP_API_PREFIX = '/wp-json/'; - - /** - * Default User Agent. - * No version number. - */ - const USER_AGENT = 'WooCommerce API Client-PHP'; - - /** - * WCOptions. - * - * @var array - */ - private $options; - - /** - * Initialize HTTP client options. - * - * @param array $options Client options. - */ - public function __construct($options) - { - $this->options = $options; - } - - /** - * Get API version. - * - * @return string - */ - public function getVersion() - { - return isset($this->options['version']) ? $this->options['version'] : self::VERSION; - } - - /** - * Check if need to verify SSL. - * - * @return bool - */ - public function verifySsl() - { - return isset($this->options['verify_ssl']) ? (bool) $this->options['verify_ssl'] : true; - } - - /** - * Get timeout. - * - * @return int - */ - public function getTimeout() - { - return isset($this->options['timeout']) ? (int) $this->options['timeout'] : self::TIMEOUT; - } - - /** - * Basic Authentication as query string. - * Some old servers are not able to use CURLOPT_USERPWD. - * - * @return bool - */ - public function isQueryStringAuth() - { - return isset($this->options['query_string_auth']) ? (bool) $this->options['query_string_auth'] : false; - } - - /** - * Check if is WP REST API. - * - * @return bool - */ - public function isWPAPI() - { - return isset($this->options['wp_api']) ? (bool) $this->options['wp_api'] : true; - } - - /** - * Custom API Prefix for WP API. - * - * @return string - */ - public function apiPrefix() - { - return isset($this->options['wp_api_prefix']) ? $this->options['wp_api_prefix'] : self::WP_API_PREFIX; - } - - /** - * oAuth timestamp. - * - * @return string - */ - public function oauthTimestamp() - { - return isset($this->options['oauth_timestamp']) ? $this->options['oauth_timestamp'] : \time(); - } - - /** - * Custom user agent. - * - * @return string - */ - public function userAgent() - { - return isset($this->options['user_agent']) ? $this->options['user_agent'] : self::USER_AGENT; - } - - /** - * Get follow redirects - * - * @return bool - */ - public function getFollowRedirects() - { - return isset($this->options['follow_redirects']) ? (bool) $this->options['follow_redirects'] : false; - } -} - -class WCRequest -{ - /** - * WCRequest url. - * - * @var string - */ - private $url; - - /** - * WCRequest method. - * - * @var string - */ - private $method; - - /** - * WCRequest paramenters. - * - * @var array - */ - private $parameters; - - /** - * WCRequest headers. - * - * @var array - */ - private $headers; - - /** - * WCRequest body. - * - * @var string - */ - private $body; - - /** - * Initialize request. - * - * @param string $url WCRequest url. - * @param string $method WCRequest method. - * @param array $parameters WCRequest paramenters. - * @param array $headers WCRequest headers. - * @param string $body WCRequest body. - */ - public function __construct($url = '', $method = 'POST', $parameters = [], $headers = [], $body = '') - { - $this->url = $url; - $this->method = $method; - $this->parameters = $parameters; - $this->headers = $headers; - $this->body = $body; - } - - /** - * Set url. - * - * @param string $url WCRequest url. - */ - public function setUrl($url) - { - $this->url = $url; - } - - /** - * Set method. - * - * @param string $method WCRequest method. - */ - public function setMethod($method) - { - $this->method = $method; - } - - /** - * Set parameters. - * - * @param array $parameters WCRequest paramenters. - */ - public function setParameters($parameters) - { - $this->parameters = $parameters; - } - - /** - * Set headers. - * - * @param array $headers WCRequest headers. - */ - public function setHeaders($headers) - { - $this->headers = $headers; - } - - /** - * Set body. - * - * @param string $body WCRequest body. - */ - public function setBody($body) - { - $this->body = $body; - } - - /** - * Get url. - * - * @return string - */ - public function getUrl() - { - return $this->url; - } - - /** - * Get method. - * - * @return string - */ - public function getMethod() - { - return $this->method; - } - - /** - * Get parameters. - * - * @return array - */ - public function getParameters() - { - return $this->parameters; - } - - /** - * Get headers. - * - * @return array - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * Get raw headers. - * - * @return array - */ - public function getRawHeaders() - { - $headers = []; - - foreach ($this->headers as $key => $value) { - $headers[] = $key . ': ' . $value; - } - - return $headers; - } - - /** - * Get body. - * - * @return string - */ - public function getBody() - { - return $this->body; - } -} - -class WCOAuth -{ - /** - * OAuth signature method algorithm. - */ - const HASH_ALGORITHM = 'SHA256'; - - /** - * API endpoint URL. - * - * @var string - */ - protected $url; - - /** - * Consumer key. - * - * @var string - */ - protected $consumerKey; - - /** - * Consumer secret. - * - * @var string - */ - protected $consumerSecret; - - /** - * API version. - * - * @var string - */ - protected $apiVersion; - - /** - * WCRequest method. - * - * @var string - */ - protected $method; - - /** - * WCRequest parameters. - * - * @var array - */ - protected $parameters; - - /** - * Timestamp. - * - * @var string - */ - protected $timestamp; - - /** - * Initialize oAuth class. - * - * @param string $url Store URL. - * @param string $consumerKey Consumer key. - * @param string $consumerSecret Consumer Secret. - * @param string $method WCRequest method. - * @param string $apiVersion API version. - * @param array $parameters WCRequest parameters. - * @param string $timestamp Timestamp. - */ - public function __construct( - $url, - $consumerKey, - $consumerSecret, - $apiVersion, - $method, - $parameters = [], - $timestamp = '' - ) { - $this->url = $url; - $this->consumerKey = $consumerKey; - $this->consumerSecret = $consumerSecret; - $this->apiVersion = $apiVersion; - $this->method = $method; - $this->parameters = $parameters; - $this->timestamp = $timestamp; - } - - /** - * Encode according to RFC 3986. - * - * @param string|array $value Value to be normalized. - * - * @return string - */ - //TODO Rückgbabetyp prüfen - protected function encode($value) - { - if (is_array($value)) { - return array_map([$this, 'encode'], $value); - } else { - return str_replace(['+', '%7E'], [' ', '~'], rawurlencode($value)); - } - } - - /** - * Normalize parameters. - * - * @param array $parameters Parameters to normalize. - * - * @return array - */ - protected function normalizeParameters($parameters) - { - $normalized = []; - - foreach ($parameters as $key => $value) { - // Percent symbols (%) must be double-encoded. - $key = $this->encode($key); - $value = $this->encode($value); - - $normalized[$key] = $value; - } - - return $normalized; - } - - /** - * Process filters. - * - * @param array $parameters WCRequest parameters. - * - * @return array - */ - protected function processFilters($parameters) - { - if (isset($parameters['filter'])) { - $filters = $parameters['filter']; - unset($parameters['filter']); - foreach ($filters as $filter => $value) { - $parameters['filter[' . $filter . ']'] = $value; - } - } - - return $parameters; - } - - /** - * Get secret. - * - * @return string - */ - protected function getSecret() - { - $secret = $this->consumerSecret; - - // Fix secret for v3 or later. - if (!\in_array($this->apiVersion, ['v1', 'v2'])) { - $secret .= '&'; - } - - return $secret; - } - - /** - * Generate oAuth1.0 signature. - * - * @param array $parameters WCRequest parameters including oauth. - * - * @return string - */ - protected function generateOauthSignature($parameters) - { - $baseRequestUri = rawurlencode($this->url); - - // Extract filters. - $parameters = $this->processFilters($parameters); - - // Normalize parameter key/values and sort them. - $parameters = $this->normalizeParameters($parameters); - uksort($parameters, 'strcmp'); - - // Set query string. - $queryString = implode('%26', $this->joinWithEqualsSign($parameters)); // Join with ampersand. - $stringToSign = $this->method . '&' . $baseRequestUri . '&' . $queryString; - $secret = $this->getSecret(); - - return base64_encode(hash_hmac(self::HASH_ALGORITHM, $stringToSign, $secret, true)); - } - - /** - * Creates an array of urlencoded strings out of each array key/value pairs. - * - * @param array $params Array of parameters to convert. - * @param array $queryParams Array to extend. - * @param string $key Optional Array key to append - * @return string Array of urlencoded strings - */ - protected function joinWithEqualsSign($params, $queryParams = [], $key = '') - { - foreach ($params as $paramKey => $paramValue) { - if ($key) { - $paramKey = $key . '%5B' . $paramKey . '%5D'; // Handle multi-dimensional array. - } - - if (is_array($paramValue)) { - //TODO Typ prüfen - $queryParams = $this->joinWithEqualsSign($paramValue, $queryParams, $paramKey); - } else { - $string = $paramKey . '=' . $paramValue; // Join with equals sign. - $queryParams[] = $this->encode($string); - } - } - - return $queryParams; - } - - /** - * Sort parameters. - * - * @param array $parameters Parameters to sort in byte-order. - * - * @return array - */ - protected function getSortedParameters($parameters) - { - uksort($parameters, 'strcmp'); - - foreach ($parameters as $key => $value) { - if (is_array($value)) { - uksort($parameters[$key], 'strcmp'); - } - } - - return $parameters; - } - - /** - * Get oAuth1.0 parameters. - * - * @return string - */ - public function getParameters() - { - $parameters = \array_merge($this->parameters, [ - 'oauth_consumer_key' => $this->consumerKey, - 'oauth_timestamp' => $this->timestamp, - 'oauth_nonce' => \sha1(\microtime()), - 'oauth_signature_method' => 'HMAC-' . self::HASH_ALGORITHM, - ]); - - // The parameters above must be included in the signature generation. - $parameters['oauth_signature'] = $this->generateOauthSignature($parameters); - - //TODO Typ prüfen - return $this->getSortedParameters($parameters); - } -} - -class WCHttpClientException extends \Exception -{ - /** - * WCRequest. - * - * @var WCRequest - */ - private $request; - - /** - * WCResponse. - * - * @var WCResponse - */ - private $response; - - /** - * Initialize exception. - * - * @param string $message Error message. - * @param int $code Error code. - * @param WCRequest $request Request data. - * @param WCResponse $response Response data. - */ - public function __construct($message, $code, WCRequest $request, WCResponse $response) - { - parent::__construct($message, $code); - - $this->request = $request; - $this->response = $response; - } - - /** - * Get request data. - * - * @return WCRequest - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response data. - * - * @return WCResponse - */ - public function getResponse() - { - return $this->response; - } -} - -class WCHttpClient -{ - /** - * cURL handle. - * - * @var resource - */ - protected $ch; - - /** - * Store API URL. - * - * @var string - */ - protected $url; - - /** - * Consumer key. - * - * @var string - */ - protected $consumerKey; - - /** - * Consumer secret. - * - * @var string - */ - protected $consumerSecret; - - /** - * WCClient options. - * - * @var WCOptions - */ - protected $options; - - /** - * WCRequest. - * - * @var WCRequest - */ - private $request; - - /** - * WCResponse. - * - * @var WCResponse - */ - private $response; - - /** - * WCResponse headers. - * - * @var string - */ - private $responseHeaders; - - /** @var Logger $logger */ - public $logger; - - public $ssl_ignore = false; - - /** - * Initialize HTTP client. - * - * @param string $url Store URL. - * @param string $consumerKey Consumer key. - * @param string $consumerSecret Consumer Secret. - * @param array $options WCClient options. - * - * @throws WCHttpClientException - */ - public function __construct($url, $consumerKey, $consumerSecret, $options, $logger, $ssl_ignore) - { - if (!function_exists('curl_version')) { - throw new WCHttpClientException('cURL is NOT installed on this server', -1, new WCRequest(), new WCResponse()); - } - - $this->options = new WCOptions($options); - $this->url = $this->buildApiUrl($url); - $this->consumerKey = $consumerKey; - $this->consumerSecret = $consumerSecret; - $this->logger = $logger; - $this->ssl_ignore = $ssl_ignore; - } - - /** - * Check if is under SSL. - * - * @return bool - */ - protected function isSsl() - { - return strpos($this->url, 'https://') === 0; - - } - - /** - * Build API URL. - * - * @param string $url Store URL. - * - * @return string - */ - protected function buildApiUrl($url) - { - $api = $this->options->isWPAPI() ? $this->options->apiPrefix() : '/wc-api/'; - - return rtrim($url, '/') . $api . $this->options->getVersion() . '/'; - } - - /** - * Build URL. - * - * @param string $url URL. - * @param array $parameters Query string parameters. - * - * @return string - */ - protected function buildUrlQuery($url, $parameters = []) - { - if (!empty($parameters)) { - $url .= '?' . http_build_query($parameters); - } - - return $url; - } - - /** - * Authenticate. - * - * @param string $url WCRequest URL. - * @param string $method WCRequest method. - * @param array $parameters WCRequest parameters. - * - * @return array - */ - protected function authenticate($url, $method, $parameters = []) - { - // Setup authentication. - if ($this->isSsl()) { - $basicAuth = new WCBasicAuth( - $this->ch, - $this->consumerKey, - $this->consumerSecret, - $this->options->isQueryStringAuth(), - $parameters - ); - $parameters = $basicAuth->getParameters(); - } else { - $oAuth = new WCOAuth( - $url, - $this->consumerKey, - $this->consumerSecret, - $this->options->getVersion(), - $method, - $parameters, - $this->options->oauthTimestamp() - ); - //TODO Typ prüfen - $parameters = $oAuth->getParameters(); - } - - return $parameters; - } - - /** - * Setup method. - * - * @param string $method WCRequest method. - */ - protected function setupMethod($method) - { - if ('POST' === $method) { - curl_setopt($this->ch, CURLOPT_POST, true); - } elseif (in_array($method, ['PUT', 'DELETE', 'OPTIONS'])) { - curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, $method); - } - } - - /** - * Get request headers. - * - * @param bool $sendData If request send data or not. - * - * @return array - */ - protected function getRequestHeaders($sendData = false) - { - $headers = [ - 'Accept' => 'application/json', - 'User-Agent' => $this->options->userAgent() . '/' . WCClient::VERSION, - ]; - - if ($sendData) { - $headers['Content-Type'] = 'application/json;charset=utf-8'; - } - - return $headers; - } - - /** - * Create request. - * - * @param string $endpoint WCRequest endpoint. - * @param string $method WCRequest method. - * @param array $data WCRequest data. - * @param array $parameters WCRequest parameters. - * - * @return WCRequest - */ - protected function createRequest($endpoint, $method, $data = [], $parameters = []) - { - $body = ''; - $url = $this->url . $endpoint; - $hasData = !empty($data); - - // Setup authentication. - $parameters = $this->authenticate($url, $method, $parameters); - - // Setup method. - $this->setupMethod($method); - - // Include post fields. - if ($hasData) { - $body = json_encode($data); - curl_setopt($this->ch, CURLOPT_POSTFIELDS, $body); - } - - $this->request = new WCRequest( - $this->buildUrlQuery($url, $parameters), - $method, - $parameters, - $this->getRequestHeaders($hasData), - $body - ); - - return $this->getRequest(); - } - - /** - * Get response headers. - * - * @return array - */ - protected function getResponseHeaders() - { - $headers = []; - $lines = explode("\n", $this->responseHeaders); - $lines = array_filter($lines, 'trim'); - - foreach ($lines as $index => $line) { - // Remove HTTP/xxx params. - if (strpos($line, ': ') === false) { - continue; - } - - list($key, $value) = explode(': ', $line); - - $headers[$key] = isset($headers[$key]) ? $headers[$key] . ', ' . trim($value) : trim($value); - } - - return $headers; - } - - /** - * Create response. - * - * @return WCResponse - */ - protected function createResponse() - { - // Set response headers. - $this->responseHeaders = ''; - curl_setopt($this->ch, CURLOPT_HEADERFUNCTION, function ($_, $headers) { - $this->responseHeaders .= $headers; - return strlen($headers); - }); - - $this->curl_debug = true; - if ($this->curl_debug) { - // Verbose debugging - ob_start(); - $out = fopen('php://output', 'w'); - curl_setopt($this->ch, CURLOPT_VERBOSE, true); - curl_setopt($this->ch, CURLOPT_STDERR, $out); - // Verbose debugging - } - - // Get response data. - $body = curl_exec($this->ch); - $code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE); - $headers = $this->getResponseHeaders(); - - if ($this->curl_debug) { - // Verbose debugging - fclose($out); - $debug = ob_get_clean(); - $result['postdata_500_bytes'] = mb_substr($postdata,0,500); - $result['debug'] = $debug; - $curlinfo = curl_getinfo($this->ch); - // Verbose debugging - - $this->logger->debug( - 'WooCommerce debug', - [ - 'request' => $this->request, - 'response' => $this->response, - 'curlinfo' => $curlinfo, - 'result' => $result - ] - ); - } - - // Register response. - $this->response = new WCResponse($code, $headers, $body); - - return $this->getResponse(); - } - - /** - * Set default cURL settings. - */ - protected function setDefaultCurlSettings() - { - if (!$this->ssl_ignore) { - $verifySsl = $this->options->verifySsl(); - } - - $timeout = $this->options->getTimeout(); - $followRedirects = $this->options->getFollowRedirects(); - - curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, $verifySsl); - if (!$verifySsl) { - curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, $verifySsl); - } - if ($followRedirects) { - curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true); - } - curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $timeout); - curl_setopt($this->ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->request->getRawHeaders()); - curl_setopt($this->ch, CURLOPT_URL, $this->request->getUrl()); - } - - /** - * Look for errors in the request. - * - * @throws WCHttpClientException - * - * @param array $parsedResponse Parsed body response. - */ - protected function lookForErrors($parsedResponse) - { - // Any non-200/201/202 response code indicates an error. - if (!in_array($this->response->getCode(), ['200', '201', '202'])) { - $errors = isset($parsedResponse->errors) ? $parsedResponse->errors : $parsedResponse; - $errorMessage = ''; - $errorCode = ''; - - if (is_array($errors)) { - $errorMessage = $errors[0]->message; - $errorCode = $errors[0]->code; - } elseif (isset($errors->message, $errors->code)) { - $errorMessage = $errors->message; - $errorCode = $errors->code; - } - - $this->logger->error( - 'WooCommerce Error', - [ - 'request' => $this->request, - 'response' => $this->response - ] - ); - - throw new WCHttpClientException( - sprintf('Error: %s [%s]', $errorMessage, $errorCode), - $this->response->getCode(), - $this->request, - $this->response - ); - } - } - - /** - * Process response. - * - * @throws WCHttpClientException - * @return array - */ - - protected function processResponse() - { - $body = $this->response->getBody(); - - // Look for UTF-8 BOM and remove. - if (0 === strpos(bin2hex(substr($body, 0, 4)), 'efbbbf')) { - $body = substr($body, 3); - } - - $parsedResponse = json_decode($body); - - // Test if return a valid JSON. - if (JSON_ERROR_NONE !== json_last_error()) { - $message = function_exists('json_last_error_msg') ? json_last_error_msg() : 'Invalid JSON returned'; - // Log the problematic body for debugging - if (isset($this->logger)) { - $this->logger->error('JSON Parse Error - Raw Body', [ - 'body_preview' => substr($body, 0, 1000), - 'body_length' => strlen($body), - 'json_error' => $message - ]); - } - throw new WCHttpClientException( - sprintf('JSON ERROR: %s', $message), - $this->response->getCode(), - $this->request, - $this->response - ); - } - - $this->lookForErrors($parsedResponse); - - return $parsedResponse; - } - - /** - * Make requests. - * - * @param string $endpoint WCRequest endpoint. - * @param string $method WCRequest method. - * @param array $data WCRequest data. - * @param array $parameters WCRequest parameters. - * - * @throws WCHttpClientException - * - * @return array - */ - public function request($endpoint, $method, $data = [], $parameters = []) - { - - // Initialize cURL. - $this->ch = curl_init(); - - // Set request args. - $request = $this->createRequest($endpoint, $method, $data, $parameters); - - // Default cURL settings. - $this->setDefaultCurlSettings(); - - // Get response. - $response = $this->createResponse(); - - // Check for cURL errors. - if (curl_errno($this->ch)) { - throw new WCHttpClientException('cURL Error: ' . \curl_error($this->ch), 0, $request, $response); - } - - curl_close($this->ch); - - return $this->processResponse(); - } - - /** - * Get request data. - * - * @return WCRequest - */ - public function getRequest() - { - return $this->request; - } - - /** - * Get response data. - * - * @return WCResponse - */ - public function getResponse() - { - return $this->response; - } -} - -class WCBasicAuth -{ - /** - * cURL handle. - * - * @var resource - */ - protected $ch; - - /** - * Consumer key. - * - * @var string - */ - protected $consumerKey; - - /** - * Consumer secret. - * - * @var string - */ - protected $consumerSecret; - - /** - * Do query string auth. - * - * @var bool - */ - protected $doQueryString; - - /** - * WCRequest parameters. - * - * @var array - */ - protected $parameters; - - /** - * Initialize Basic Authentication class. - * - * @param resource $ch cURL handle. - * @param string $consumerKey Consumer key. - * @param string $consumerSecret Consumer Secret. - * @param bool $doQueryString Do or not query string auth. - * @param array $parameters WCRequest parameters. - */ - public function __construct($ch, $consumerKey, $consumerSecret, $doQueryString, $parameters = []) - { - $this->ch = $ch; - $this->consumerKey = $consumerKey; - $this->consumerSecret = $consumerSecret; - $this->doQueryString = $doQueryString; - $this->parameters = $parameters; - - $this->processAuth(); - } - - /** - * Process auth. - */ - protected function processAuth() - { - if ($this->doQueryString) { - $this->parameters['consumer_key'] = $this->consumerKey; - $this->parameters['consumer_secret'] = $this->consumerSecret; - } else { - \curl_setopt($this->ch, CURLOPT_USERPWD, $this->consumerKey . ':' . $this->consumerSecret); - } - } - - /** - * Get parameters. - * - * @return array - */ - public function getParameters() - { - return $this->parameters; - } }