betrieb: Logs je Thema in Tagesdateien (billing, ki, monitoring, market); Doku: Duplicate-Content-Decision durch Anhang 2 abgelöst

- config/logging.php: Themen-Channels billing, ki, monitoring, market als
  daily-Dateien mit gemeinsamer Aufbewahrung (LOG_DAILY_DAYS), Stack-Default
  auf daily; Channel `testing` für die Suite.
- Fachliche Log-Aufrufe in Billing, KI-Jobs, OperationalAlert und
  Kursdaten auf Log::channel() umgestellt.
- phpunit.xml + TestCase: Suite schreibt ausschließlich nach
  testing-YYYY-MM-DD.log; LogChannelsTest sichert die Konfiguration.
- docs/weiteres: Decision-Update Duplicate-Content entfernt (überholt durch
  Roadmap-Anhang 2), Decision-Update Verlinkung & Backlinks als Referenz
  wieder abgelegt, README-Index nachgezogen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 13:35:42 +00:00
parent 5122624687
commit 85d7c2b0d5
21 changed files with 338 additions and 107 deletions

View file

@ -114,7 +114,7 @@ class CheckoutController extends Controller
'redirect' => route('me.bookings.index', ['checkout' => 'wechsel']),
]);
} catch (\Throwable $exception) {
Log::error('Tarifwechsel fehlgeschlagen.', [
Log::channel('billing')->error('Tarifwechsel fehlgeschlagen.', [
'user_id' => $user->id,
'plan' => $plan->slug,
'interval' => $interval,

View file

@ -24,7 +24,7 @@ class StripeInvoiceRedirectController extends Controller
$stripeInvoice = Cashier::stripe()->invoices->retrieve($invoice->stripe_invoice_id);
$hostedUrl = $stripeInvoice->hosted_invoice_url;
} catch (\Throwable $exception) {
Log::warning('Stripe-Rechnungsansicht nicht erreichbar.', [
Log::channel('billing')->warning('Stripe-Rechnungsansicht nicht erreichbar.', [
'invoice_id' => $invoice->id,
'stripe_invoice_id' => $invoice->stripe_invoice_id,
'error' => $exception->getMessage(),

View file

@ -165,14 +165,14 @@ class ClassifyPressRelease implements ShouldQueue
} catch (StaleClassificationException $exception) {
// Inhalt zwischen Audit und Publish-Sperre geändert: publish() hat
// bereits eine Prüfung für den aktuellen Stand eingeplant (U02).
Log::info('Auto-Veröffentlichung übersprungen: Klassifikation passt nicht mehr zum Inhalt, Prüfung neu eingeplant.', [
Log::channel('ki')->info('Auto-Veröffentlichung übersprungen: Klassifikation passt nicht mehr zum Inhalt, Prüfung neu eingeplant.', [
'press_release_id' => $pressRelease->id,
]);
} catch (QuotaExceededException $exception) {
// Kontingent zwischen Einreichung und KI-Freigabe aufgebraucht: PM
// bleibt in Review, damit Autor/Admin bewusst entscheiden — kein
// Job-Retry, der die Klassifikation sinnlos wiederholt.
Log::info('Auto-Veröffentlichung übersprungen: PM-Kontingent aufgebraucht, PM bleibt in Review.', [
Log::channel('ki')->info('Auto-Veröffentlichung übersprungen: PM-Kontingent aufgebraucht, PM bleibt in Review.', [
'press_release_id' => $pressRelease->id,
]);
}
@ -208,7 +208,7 @@ class ClassifyPressRelease implements ShouldQueue
try {
return $manager->driver($provider)->classify($pressRelease);
} catch (\Throwable $exception) {
Log::warning('KI-Klassifikation fiel auf den deterministischen Treiber zurück.', [
Log::channel('ki')->warning('KI-Klassifikation fiel auf den deterministischen Treiber zurück.', [
'press_release_id' => $pressRelease->id,
'provider' => $provider,
'error' => $exception->getMessage(),

View file

@ -124,7 +124,7 @@ class ScorePressRelease implements ShouldQueue
try {
return $manager->driver($provider)->score($pressRelease);
} catch (\Throwable $exception) {
Log::warning('Content-Score fiel auf den deterministischen Treiber zurück.', [
Log::channel('ki')->warning('Content-Score fiel auf den deterministischen Treiber zurück.', [
'press_release_id' => $pressRelease->id,
'provider' => $provider,
'error' => $exception->getMessage(),

View file

@ -192,7 +192,7 @@ class ProcessStripeWebhook
]);
if ($cleared > 0) {
Log::info('Vorgemerkter Tarifwechsel wurde von Stripe vollzogen.', [
Log::channel('billing')->info('Vorgemerkter Tarifwechsel wurde von Stripe vollzogen.', [
'stripe_subscription_id' => $stripeId,
'stripe_price' => $price,
]);
@ -217,7 +217,7 @@ class ProcessStripeWebhook
$user = Cashier::findBillable($stripeInvoice['customer'] ?? null);
if (! $user instanceof User) {
Log::warning('STR-Spiegelung übersprungen: kein Billable zum Stripe-Customer.', [
Log::channel('billing')->warning('STR-Spiegelung übersprungen: kein Billable zum Stripe-Customer.', [
'stripe_invoice_id' => $stripeInvoiceId,
'stripe_customer' => $stripeInvoice['customer'] ?? null,
]);
@ -239,7 +239,7 @@ class ProcessStripeWebhook
// (QA-Befund 07.07.: unsigned-Spalten → Webhook-500) — der Beleg
// selbst bleibt bei Stripe einsehbar.
if ($total <= 0) {
Log::info('STR-Spiegelung übersprungen: Stripe-Beleg ohne positiven Rechnungsbetrag (Gutschrift/Null-Rechnung).', [
Log::channel('billing')->info('STR-Spiegelung übersprungen: Stripe-Beleg ohne positiven Rechnungsbetrag (Gutschrift/Null-Rechnung).', [
'stripe_invoice_id' => $stripeInvoiceId,
'total_cents' => $total,
]);
@ -286,7 +286,7 @@ class ProcessStripeWebhook
return;
}
Log::info('Stripe-Rechnung in den STR-Kreis gespiegelt.', [
Log::channel('billing')->info('Stripe-Rechnung in den STR-Kreis gespiegelt.', [
'number' => $invoice->number,
'stripe_invoice_id' => $stripeInvoiceId,
]);
@ -351,7 +351,7 @@ class ProcessStripeWebhook
// _failed`. Erfüllt wird deshalb nur bei bestätigter Zahlung; beide
// Erfüllungspfade sind idempotent, doppelte Events bleiben folgenlos.
if (! in_array($session['payment_status'] ?? null, ['paid', 'no_payment_required'], true)) {
Log::info('Checkout-Session noch unbezahlt — Erfüllung wartet auf async_payment_succeeded.', [
Log::channel('billing')->info('Checkout-Session noch unbezahlt — Erfüllung wartet auf async_payment_succeeded.', [
'session_id' => $session['id'] ?? null,
'payment_status' => $session['payment_status'] ?? null,
]);
@ -372,7 +372,7 @@ class ProcessStripeWebhook
*/
private function logFailedAsyncPayment(array $session): void
{
Log::warning('Asynchrone Stripe-Zahlung fehlgeschlagen — keine Erfüllung.', [
Log::channel('billing')->warning('Asynchrone Stripe-Zahlung fehlgeschlagen — keine Erfüllung.', [
'session_id' => $session['id'] ?? null,
'single_purchase_id' => $session['metadata']['single_purchase_id'] ?? null,
'credit_topup_id' => $session['metadata']['credit_topup_id'] ?? null,
@ -402,7 +402,7 @@ class ProcessStripeWebhook
$session['payment_intent'] ?? null,
);
Log::info('Wallet-Aufladung gutgeschrieben.', ['credit_topup_id' => $topup->id]);
Log::channel('billing')->info('Wallet-Aufladung gutgeschrieben.', ['credit_topup_id' => $topup->id]);
}
/**
@ -459,7 +459,7 @@ class ProcessStripeWebhook
$purchase = $result['purchase'];
if ($result['marked_paid']) {
Log::info('Einmalkauf als bezahlt markiert.', ['single_purchase_id' => $purchase->id]);
Log::channel('billing')->info('Einmalkauf als bezahlt markiert.', ['single_purchase_id' => $purchase->id]);
// Stufe 5 der Konversionsleiter. recordFirstBooking() zählt je
// Firma nur den ersten Treffer, deshalb dürfen beide Zahlungspfade
@ -479,7 +479,7 @@ class ProcessStripeWebhook
Mail::to($buyer->email)->queue(new PressReleaseChangeApplied($buyer, $changedRelease, $changeRequest));
}
Log::info('Pressemitteilung nach bezahlter Änderung aktualisiert.', [
Log::channel('billing')->info('Pressemitteilung nach bezahlter Änderung aktualisiert.', [
'single_purchase_id' => $purchase->id,
'press_release_id' => $changedRelease->id,
]);
@ -497,7 +497,7 @@ class ProcessStripeWebhook
Mail::to($buyer->email)->queue(new PressReleaseDepublished($buyer, $pressRelease));
}
Log::info('Pressemitteilung depubliziert.', [
Log::channel('billing')->info('Pressemitteilung depubliziert.', [
'single_purchase_id' => $purchase->id,
'press_release_id' => $pressRelease->id,
]);
@ -522,7 +522,7 @@ class ProcessStripeWebhook
->first();
if (! $pressRelease || $pressRelease->status !== PressReleaseStatus::Published) {
Log::warning('Depublikation bezahlt, aber PM nicht (mehr) veröffentlicht.', [
Log::channel('billing')->warning('Depublikation bezahlt, aber PM nicht (mehr) veröffentlicht.', [
'single_purchase_id' => $purchase->id,
'press_release_id' => $purchase->press_release_id,
]);
@ -568,7 +568,7 @@ class ProcessStripeWebhook
->first();
if (! $pressRelease || $pressRelease->status !== PressReleaseStatus::Published) {
Log::warning('Änderung bezahlt, aber PM nicht (mehr) veröffentlicht.', [
Log::channel('billing')->warning('Änderung bezahlt, aber PM nicht (mehr) veröffentlicht.', [
'single_purchase_id' => $purchase->id,
'press_release_id' => $purchase->press_release_id,
]);
@ -581,7 +581,7 @@ class ProcessStripeWebhook
} catch (CorrectionTooLargeException) {
// Kauf bleibt bezahlt/unkonsumiert — Klärung durch den Admin,
// statt eine Obergrenzen-Verletzung stillschweigend zu übernehmen.
Log::error('Bezahlte Änderung überschreitet die Obergrenze — nicht ausgeführt.', [
Log::channel('billing')->error('Bezahlte Änderung überschreitet die Obergrenze — nicht ausgeführt.', [
'single_purchase_id' => $purchase->id,
'legal_request_id' => $legalRequest->id,
]);

View file

@ -186,7 +186,7 @@ class ManualInvoiceService
*/
private function hold(UserPaymentOption $option, string $reason, array $context): null
{
Log::warning('MAN-Rechnung übersprungen: '.$reason.'.', $context);
Log::channel('billing')->warning('MAN-Rechnung übersprungen: '.$reason.'.', $context);
$option->forceFill([
'billing_hold_reason' => $reason,

View file

@ -212,7 +212,7 @@ class StripeCheckoutService
try {
Cashier::stripe()->subscriptionSchedules->release($subscription->stripe_schedule_id);
} catch (InvalidRequestException $exception) {
Log::warning('Downgrade-Vormerkung war bei Stripe nicht (mehr) auflösbar.', [
Log::channel('billing')->warning('Downgrade-Vormerkung war bei Stripe nicht (mehr) auflösbar.', [
'subscription_id' => $subscription->id,
'stripe_schedule_id' => $subscription->stripe_schedule_id,
'error' => $exception->getMessage(),
@ -241,7 +241,7 @@ class StripeCheckoutService
$user->createOrGetStripeCustomer();
$user->syncStripeCustomerDetails();
} catch (\Throwable $exception) {
Log::warning('Stripe-Customer konnte nicht mit der Rechnungsadresse synchronisiert werden.', [
Log::channel('billing')->warning('Stripe-Customer konnte nicht mit der Rechnungsadresse synchronisiert werden.', [
'user_id' => $user->id,
'error' => $exception->getMessage(),
]);
@ -299,7 +299,7 @@ class StripeCheckoutService
$user->createTaxId('eu_vat', $vatId);
}
} catch (\Throwable $exception) {
Log::warning('USt-ID konnte nicht mit Stripe abgeglichen werden.', [
Log::channel('billing')->warning('USt-ID konnte nicht mit Stripe abgeglichen werden.', [
'user_id' => $user->id,
'error' => $exception->getMessage(),
]);
@ -332,7 +332,7 @@ class StripeCheckoutService
$check = $this->vatIdValidation->check($vatId, (string) $user->billingAddress?->country_code);
if (! $check['status']->isUsable()) {
Log::warning('USt-ID vor der Buchung nicht bestätigt — sie wird nicht an Stripe übergeben.', [
Log::channel('billing')->warning('USt-ID vor der Buchung nicht bestätigt — sie wird nicht an Stripe übergeben.', [
'user_id' => $user->id,
'vat_id' => $vatId,
'status' => $check['status']->value,

View file

@ -117,7 +117,7 @@ class VatIdValidationService
'angefragteUstid' => $vatId,
]);
} catch (Throwable $exception) {
Log::warning('eVatR-Abfrage fehlgeschlagen.', ['vat_id' => $vatId, 'error' => $exception->getMessage()]);
Log::channel('billing')->warning('eVatR-Abfrage fehlgeschlagen.', ['vat_id' => $vatId, 'error' => $exception->getMessage()]);
return $this->result(
VatIdCheckStatus::Unverified,

View file

@ -42,7 +42,7 @@ class CompanyProfileCheck
try {
return $this->classification->driver()->classify($proposed);
} catch (\Throwable $e) {
Log::warning('Profilprüfung (Rot-Ebene) fehlgeschlagen — deterministischer Fallback.', [
Log::channel('ki')->warning('Profilprüfung (Rot-Ebene) fehlgeschlagen — deterministischer Fallback.', [
'company_id' => $company->getKey(),
'error' => $e->getMessage(),
]);

View file

@ -59,7 +59,7 @@ class CorrectionService
try {
return $this->classification->driver()->classify($proposed);
} catch (\Throwable $e) {
Log::warning('KI-Vorprüfung (Spur D) fehlgeschlagen — deterministischer Fallback.', [
Log::channel('ki')->warning('KI-Vorprüfung (Spur D) fehlgeschlagen — deterministischer Fallback.', [
'press_release_id' => $pressRelease->id,
'error' => $e->getMessage(),
]);
@ -223,7 +223,7 @@ class CorrectionService
}
if (! $classification['has_changes']) {
Log::warning('Bezahlte Änderung ohne wirksame Feld-Änderung — nichts zu tun.', [
Log::channel('billing')->warning('Bezahlte Änderung ohne wirksame Feld-Änderung — nichts zu tun.', [
'legal_request_id' => $legalRequest->id,
'press_release_id' => $pressRelease->id,
]);

View file

@ -64,7 +64,7 @@ class MarketDataService
}
if ($slides === []) {
Log::warning('market:refresh lieferte keine Kursdaten — bestehender Cache-Stand bleibt erhalten.');
Log::channel('market')->warning('market:refresh lieferte keine Kursdaten — bestehender Cache-Stand bleibt erhalten.');
return $this->slides();
}
@ -94,13 +94,13 @@ class MarketDataService
'interval' => '1d',
]);
} catch (Throwable $exception) {
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'error' => $exception->getMessage()]);
Log::channel('market')->warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'error' => $exception->getMessage()]);
return null;
}
if (! $response->successful()) {
Log::warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'status' => $response->status()]);
Log::channel('market')->warning('Kursabruf fehlgeschlagen.', ['symbol' => $symbol, 'status' => $response->status()]);
return null;
}

View file

@ -20,7 +20,7 @@ class OperationalAlert
*/
public static function send(string $summary, array $context = []): void
{
Log::critical('[monitoring] '.$summary, $context);
Log::channel('monitoring')->critical('[monitoring] '.$summary, $context);
if (! config('monitoring.alerts.enabled')) {
return;
@ -35,7 +35,7 @@ class OperationalAlert
try {
Mail::to($recipient)->send(new OperationalAlertMail($summary, $context));
} catch (Throwable $e) {
Log::error('[monitoring] Alert-Mail konnte nicht versendet werden.', [
Log::channel('monitoring')->error('[monitoring] Alert-Mail konnte nicht versendet werden.', [
'summary' => $summary,
'error' => $e->getMessage(),
]);