'array', 'has_errors' => 'bool', ]; public function shoppingOrder(): BelongsTo { return $this->belongsTo(ShoppingOrder::class, 'shopping_order_id'); } public function shoppingUser(): BelongsTo { return $this->belongsTo(ShoppingUser::class, 'shopping_user_id'); } /** * Fehlgeschlagene Übertragung ohne verwertbare Positionen (Kunde unbekannt, * Bestellung existiert bereits, Speicherfehler). * * @param mixed $rawOrder eingehender wp_order-Payload (String oder Array) */ public static function recordFailure(mixed $wpOrderNumber, ?ShoppingUser $shoppingUser, mixed $rawOrder, string $summary): self { return self::query()->create([ 'shopping_user_id' => $shoppingUser?->id, 'wp_order_number' => is_numeric($wpOrderNumber) ? (int) $wpOrderNumber : null, 'endpoint' => 'order', 'positions_count' => 0, 'payload' => ['raw' => is_string($rawOrder) ? $rawOrder : json_decode(json_encode($rawOrder), true)], 'has_errors' => true, 'error_summary' => $summary, ]); } /** * Erfolgreich verarbeitete Übertragung: Positionen inkl. Zuordnungsergebnis protokollieren. * `failed` = Position wurde NICHT in die Bestellung übernommen (SKU unbekannt ODER * Menge/Preis fehlt) — die Wahrheit kommt direkt aus prepareOrder() (`included`, * `product_id`, `product_name`), damit Protokoll und Bestellung nie auseinanderlaufen * (Review 20.07.2026: vorher wurde die SKU hier erneut aufgelöst, wodurch Zeilen mit * fehlender Menge/Preis fälschlich als „OK" geloggt wurden — plus N+1-Queries). * * @param array $lines Zeilen aus prepareOrder() (article/qty/price/message/included/product_id/product_name) */ public static function recordTransfer(mixed $wpOrderNumber, ShoppingUser $shoppingUser, ?ShoppingOrder $shoppingOrder, array $lines): self { $payloadLines = []; $failedCount = 0; foreach ($lines as $line) { $line = (object) $line; $failed = ! (bool) ($line->included ?? false); if ($failed) { $failedCount++; } $payloadLines[] = [ 'article' => $line->article ?? null, 'qty' => $line->qty ?? null, 'price' => $line->price ?? null, 'product_id' => $line->product_id ?? null, 'product_name' => $line->product_name ?? null, 'messages' => array_values((array) ($line->message ?? [])), 'failed' => $failed, ]; } return self::query()->create([ 'shopping_order_id' => $shoppingOrder?->id, 'shopping_user_id' => $shoppingUser->id, 'wp_order_number' => is_numeric($wpOrderNumber) ? (int) $wpOrderNumber : null, 'endpoint' => 'order', 'positions_count' => count($payloadLines), 'payload' => ['lines' => $payloadLines], 'has_errors' => $failedCount > 0, 'error_summary' => $failedCount > 0 ? __(':failed von :total Positionen ohne Produktzuordnung', ['failed' => $failedCount, 'total' => count($payloadLines)]) : null, ]); } }