Commit graph

83 commits

Author SHA1 Message Date
38884a53d6 Perf-Fix: User::accessibleCompanyIds() hydratisiert keine Company-Models mehr
Auf /admin/me/press-releases wurden bei einem User mit sehr vielen Firmen
~107.000 Company-Models geladen. Ursache: accessibleCompanyIds() nutzte
das Eloquent-Relation-pluck() (ownedCompanies()->pluck() / companies()->
pluck()), das pro ID ein vollstaendiges Model hydratisiert. Die Methode
wird ueber PressRelease::scopeAccessibleBy() pro Seitenaufruf ~2x
ausgewertet (Status-Count- + Listen-Query) -> ~2x 53k Models.

Fix:
- toBase()->pluck() statt Relation-pluck(): liefert nur die ID-Werte ohne
  Hydration, wendet die Global Scopes (PortalScope) weiterhin an.
- Ergebnis pro Request memoisiert (cachedAccessibleCompanyIds), da der Scope
  die Methode mehrfach aufruft.

Messung (User 14016, 26.772 Firmen): 107.238 -> 24 hydratisierte
Company-Models auf der PM-Liste. Ergebnis-IDs unveraendert.

Regressionstests: accessibleCompanyIds liefert owned+member ohne Dubletten
und hydratisiert 0 Company-Models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:18:51 +00:00
abe9a0aa69 Optimierung 5.3 (Teil 2): Category::localizedName()
Name aus der locale-gefiltert geladenen Uebersetzung mit #id-Fallback als
Model-Methode. Ersetzt die identische Inline-Logik an 4 Stellen (Footer-Codes
und Kategorien jeweils create/edit).

Test: CategoryLocalizedNameTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:19:06 +00:00
178adb00e2 Optimierung 5.3 (Teil 1): Contact::displayName()
Anzeigename eines Kontakts (Vor-/Nachname mit Fallback) als Model-Methode.
Ersetzt die identische Inline-Logik an 6 eindeutigen Relation-$contact-Stellen
(Admin: Benutzer-/PM-/Firmen-Detail; Kunde: Press-Kit-/PM-Detail).

Bewusst noch offen: Select-/toBase-Varianten ($contactOption/$contactResult/
$lookupContact, stdClass-Risiko) sowie web/release-detail (User-Frontend).

Test: ContactDisplayNameTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:15:38 +00:00
f20a95f9a4 Optimierung 4.2: Published-Release-Query ueber PressRelease-Scopes zentralisiert
Vier komponierbare Scopes auf PressRelease:
- published()         Status freigegeben + published_at gesetzt (ohne Zeitschranke,
                      damit Statistik-Fenster frei einschraenken koennen)
- visibleInArchive()  published() + published_at <= jetzt (oeffentlich sichtbar)
- forPortalValues()   whereIn portal
- forLanguage()       where language

~19 duplizierte Query-Bloecke in routes/web.php (Home, Kategorie, Detail,
Veroeffentlichen, Suche, Newsrooms, Newsroom-Detail, Legacy-Redirect) nutzen
jetzt diese Scopes. Jedes withoutGlobalScope(PortalScope) bleibt an seiner
Stelle erhalten; die Klausel-Reihenfolge ist fuers Ergebnis irrelevant (reine
AND-Verknuepfung), daher verhaltensgleich.

Test: Web/PublishedReleaseScopeTest; die 89 bestehenden Web/-Tests bleiben gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:35:55 +00:00
4466858aac Optimierung 3.5: Kleinere Zentralisierungen (VAT, findByEmail, Waehrung, Provisioning)
- VatResolver::normalizeCountryCode()/isEuCountry() als Single Source of Truth;
  resolve(), VatIdValidationService und StripeCheckoutService nutzen jetzt die
  zentrale EU-Pruefung statt eigener config-Lookups.
- User::findByEmail() buendelt die LOWER(email)-Lookups (ContactAccessService,
  SocialAuthService). Bleibt bewusst case-insensitiv, da E-Mails nicht
  normalisiert gespeichert werden; der indexfreundliche Direktvergleich
  braucht erst eine Schreib-Normalisierung (offen, dokumentiert).
- config('billing.default_currency') ersetzt 8 verstreute 'EUR'/'eur'-Literale
  (Stripe-Calls via strtolower). Vereinheitlicht nebenbei die zuvor uneinheitlich
  gespeicherte SinglePurchase.currency.
- Neuer CustomerAccountProvisioner::provisionVerifiedCustomer() ist die
  gemeinsame Provisioning-Methode (verifiziert + aktiv + customer-Rolle) fuer
  ContactAccessService und SocialAuthService.
- Tests: UserFindByEmailTest, erweiterte VatResolverTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:14:05 +00:00
9ed429fe74 Optimierung 3.4: Performance (Logging terminable, Single-Purchase memoisiert)
- LogApiUsage als terminable Middleware: der ApiUsageLog-INSERT laeuft nach
  gesendeter Antwort und verlaengert nicht mehr die Request-Dauer. Startzeit
  wird ueber ein Request-Attribut uebergeben (terminate() laeuft auf frischer
  Instanz).
- User::openSinglePurchaseCount() memoisiert die offenen Einmalkaeufe pro
  Request. Vorher wurde derselbe grantingSubmission-Count im Status-Widget
  mehrfach (Quota-Total, Quota-Remaining, openSinglePurchases) plus ein
  exists() in hasActiveBooking abgesetzt -> jetzt eine Query.
- CustomerCompanyContext::selectedCompany() nutzt eine einzige first()-Query
  statt exists()+first(); das Verwerfen nicht (mehr) erreichbarer Auswahlen
  bleibt erhalten.
- LegacySubscriptionService::cancelBundleFor() ist ein einziges Bulk-Update
  statt Update-pro-Iteration (UserPaymentOption hat keine Observer/Events).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:05:07 +00:00
2bf431c8ba Optimierung 3.2: API-PR-Ownership-Scope + zentrale Eager-Loads
- PressRelease::scopeOwnedByApiUser() (bewusst eng: nur user_id-Autorschaft,
  anders als das breitere scopeAccessibleBy) ersetzt die byte-gleiche
  findOwnedPressRelease-Query in PressReleaseController + ImageController.
- apiResourceRelations()-Helfer ersetzt den 5x identischen Eager-Load-Set
  (company/category.translations/images) in Index/Show/Create/Update/Reclassify.

Bewusst NICHT umgesetzt: Slug-Reimplementierung (uniqueSlug) durch
HasUniqueSlug ersetzen - der API-Fallback Str::random(8) weicht vom
Trait-slugFallback() ab (Verhaltensaenderung bei leer-slugifizierten Titeln).

Tests: neuer Scope-Test; volle Suite gruen (802 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:27:59 +00:00
9ddfab62c2 Optimierung 3.1: Company::scopeAccessibleBy() zentralisiert Firmenzugriff
Die 4x identische Zugriffs-Closure (owner_user_id == me OR whereHas('users',
me)) in CompanyController (index/show), PressReleaseController::
findOwnedCompany und CustomerCompanyContext::accessibleCompanyQuery durch
einen Company::scopeAccessibleBy() ersetzt; das Scope-Verhalten
(withoutGlobalScopes) bleibt beim Aufrufer.

Bewusst unangetastet: User::accessibleCompanyIds() (relationenbasiert MIT
globalen Scopes - anderes Scope-Verhalten) und PressReleaseImageController
(prueft PR-Autorschaft user_id, nicht Firmenzugriff).

Tests: neuer Scope-Test; volle Suite gruen (801 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:23:16 +00:00
28e9064410 Optimierung 2.7: Company::scopeSearchByTerm fuer die Admin-Combobox
Die in admin/create und admin/edit identische Inline-Firmensuche
(Volltext-oder-LIKE) inkl. des 2x duplizierten supportsFullTextSearch()-
Treiберchecks in einen Company::scopeSearchByTerm() zentralisiert; beide
Editoren nutzen ihn jetzt (Verhalten unveraendert). Ungenutzte DB-Importe
entfernt. Customer nutzt bereits CustomerCompanyContext (eigene
join-/user-gescopte Query, daher nicht zusammengelegt).

Tests: neuer Scope-Test; volle Suite gruen (799 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:06:54 +00:00
a82964df2b Optimierung Phase 0: Korrektheit & Security
Konsolidierter Entwicklungsplan aus den zwei Analyse-Dokumenten plus
Umsetzung der Phase-0-Massnahmen (Korrektheit/Security, je mit Tests):

0.1 BasicAuth: timing-sicherer Vergleich (hash_equals, beide Felder ohne
    Short-Circuit); Enabled-Check zur Laufzeit via config('auth.basic.*')
    statt env() im Bootstrap-Closure (greift jetzt unter config:cache);
    Config-Block + .env.example. Fund: auth.basic.* war nie als Config
    definiert, Gate war faktisch unkonfigurierbar.
0.2 Legacy-Invoice-"latest"-Abfrage zentralisiert (LegacyInvoice::
    scopeLatestFirst + User::latestLegacyInvoice); beide Consumer N+1-frei
    umgestellt; unterschiedliches null-Verhalten bewusst erhalten.
0.3 Payment-Option-Eligibility vereinheitlicht (period-aware, strikt):
    UserPaymentOption::scopeActiveNow/grandfatheredNow + User::
    hasActivePaymentOption; hasActiveBooking/hasUnlimitedPressReleaseQuota
    und ApiAccessEligibilityService darauf umgestellt; $hasGrandfathered-
    Mislabel behoben.
0.4 Theme->Portal-Vokabular: totes, projektfremdes ThemeMiddleware
    geloescht; Mapping in Portal::fromTheme() zentralisiert; fremden
    b2in-Default im ThemeServiceProvider auf Portal-Default korrigiert.
0.5 InvoiceNumberGenerator: Erst-Insert race-sicher via insertOrIgnore
    (Unique-Index auf circle existiert bereits). CreditWalletService war
    bereits abgesichert (Unique-Index + createOrFirst).

Tests: 4 neue Dateien, 5 bestehende auf gueltiges grandfathered_until
angepasst. Volle Suite gruen (793 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:09:02 +00:00
446aada9c8 Paket-Vorteile: dynamische, im Admin pflegbare Vorteilsliste je Tarif
Neue JSON-Spalte plans.benefits (Migration + Cast + Plan::benefitsList()).
Im Admin (Tarife & Pakete) pro Tarif zeilenweise pflegbar; die In-App-
Buchungsseite rendert die Liste in den Tarif-Karten statt einer festen
Zeile. Seeder/Factory mit Beispiel-Vorteilen. Single Source of Truth fuer
Buchungsseite und (folgt separat) die oeffentliche Preise-Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:36:09 +00:00
1c5eedec7b WS-8: doppelte Logik konsolidieren (Tags, Kontakt-Query, Billing)
- Themen-Tag-Verwaltung (addTag/removeTag/tagsArray) war in create- und
  edit-Editor byte-identisch dupliziert -> Trait
  App\Livewire\Concerns\InteractsWithPressReleaseTags.
- Kontaktauswahl je Firma (withoutGlobalScopes + where + Sortierung) aus
  create/edit/dashboard in Contact::scopeForCompany() zentralisiert.
- Rechnungsadress-Pflichtfelder als BillingAddress::REQUIRED_FIELDS; isComplete()
  und neues completenessPercent() teilen die Liste, Dashboard nutzt es statt
  eigener Feldliste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:33:59 +00:00
3da335388e WS-8: N+1 entschaerfen und PM-Zugriffsmuster konsolidieren
- User::currentPlan() und hasUnlimitedPressReleaseQuota() pro Request
  memoisiert. Beide werden je Render mehrfach abgefragt (Status-Widget auf
  jeder Seite, Buchungsseite, Quota-Methoden) und loesten bisher jeweils
  eigene subscription-/Plan-/payment-option-Queries aus. fresh() setzt den
  Cache zurueck.
- show.blade.php: getMyPR() war NICHT memoisiert und wurde pro Request bis
  zu 7x mit voller Eager-Load-Query aufgerufen -> jetzt memoisiert. Firmen-
  Kennzahlen direkt per withCount() eager geladen statt nachtraeglichem
  loadCount().
- Dupliziertes Zugriffsmuster (withoutGlobalScopes + user_id/company_id) aus
  edit/show/index in PressRelease::scopeAccessibleBy(User) zentralisiert
  (Single Source of Truth fuer Kunden-PM-Zugriff), mit Tests abgesichert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:27:39 +00:00
5feaa8a36a Zeitzone Europe/Berlin: unnoetige UTC-Konvertierung entfernt
Beim Speichern/Veroeffentlichen waren Termine 2 Stunden versetzt: die
App-Zeitzone stand auf UTC, Termine wurden nach UTC umgerechnet, aber
published_at roh angezeigt.

- config/app.php: timezone UTC -> Europe/Berlin
- PressRelease: DISPLAY_TIMEZONE + setTimezone-Umrechnung entfernt
- Admin-/Customer-Formulare: scheduledAtUtc() -> resolveScheduledAt(),
  parst direkt in App-Zeitzone; Edit-Hydration ohne setTimezone
- ProofPdfRenderer + Boost-Anzeige: setTimezone entfernt
- Tests an verschiebungsfreies Verhalten angepasst

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 12:40:19 +00:00
344aac0740 Wallet-Topup ueber Stripe (Credit-Pakete mit Bonus-Staffel)
End-to-end-Aufladung der Credit-Wallet, spiegelt den Einzel-PM-Fluss:

- Paket-Staffel mit Bonus in config/credits.php (10->10, 25->27, 50->55,
  100->115 Credits; price_cents netto, Stripe Tax ergaenzt USt.)
- credit_topups (Pending->Paid) + CreditTopupService: startTopup legt den
  Pending-Kauf an, fulfill() schreibt die Wallet idempotent gut (credited_at)
- StripeCheckoutService::forCreditTopup via checkoutCharge + credit_topup_id
  Metadata; ProcessStripeWebhook schreibt bei checkout.session.completed gut
- Checkout-Route /admin/me/checkout/credits/{pack} mit Rechnungsadress-Gate

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:56:23 +00:00
c7a4c8bfd4 Boost-Geschaeftslogik (Decision-Update 2.2)
Bezahlte Platzierung gruener, veroeffentlichter PMs ueber die Credit-Wallet:

- boosts Tabelle (Zeitraum starts_at/ends_at, days, credits_charged)
- BoostService: Gate (nur Published + Green), Preis nach Laufzeit
  (7/14/30 -> 12/20/35 Credits), Mehrfachkauf verlaengert vom laufenden
  Ende, Wallet-Belastung referenziert den Boost im Ledger
- PressRelease::boosts()/isBoosted()/scopeBoosted() als Basis fuer die
  Featured-Platzierung (Frontend-Anbindung bleibt der Web-Strecke ueberlassen)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:22:12 +00:00
3e8844245d Pruefzaehler + Pruefkontingent (Decision-Update Phase-2 vorgezogen)
Eigener Pruef-Zaehler, getrennt von der Credit-Wallet (Paragraph 4.2/4.3):

- review_checks Ledger (eine Zeile je Pruefung, source free|credit,
  charged_credits), aggregiert pro Account/Monat statt pro PM
- ReviewCheckService: Tageslimit (harte Bremse, nicht freikaufbar) ->
  Monats-Freikontingent (tier-gestaffelt 4/12/30/60/120) -> Overflow
  zieht 1 Credit/Pruefung aus der Wallet
- ReviewLimitException fuer das erreichte Tageslimit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:19:37 +00:00
b63cd26326 Credit-Wallet + Ledger + Tier-Preisableitung (Fundament)
Echte Credit-Wallet (1 Credit = 1 EUR) mit append-only Ledger als Basis fuer
die Credit-Oekonomie aus dem Decision-Update (Rev. 4):

- credit_wallets (denormalisierter Saldo) + credit_transactions (Ledger,
  vorzeichenbehaftet, balance_after, polymorphe reference)
- CreditWalletService: einziger Schreibpfad, atomar mit Row-Lock,
  InsufficientCreditsException mit shortfall fuer den Mini-Checkout
- Tier-Enum (Einzel/Starter/Business/Pro/Agency) + User::currentTier()
- CreditPricingService: tier-gestaffelte Ableitung aus config/credits.php
  (Extra-PM 19/15/12/10/8, Boost 12/20/35, PDF 3, Depublish 25, Pruef-Quota)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:16:43 +00:00
95007da826 WS-3: Recht & Compliance – Rechts-Kern (DSGVO/Persönlichkeitsrecht/Melden + Queue)
Launch-pflichtiger Compliance-Slice: öffentliche Anfrage zu einer PM speist eine
manuelle Admin-Queue (keine KI).

- Migration legal_requests + Model + Enums (Type: dsgvo/personal_rights/report,
  Status: open/in_progress/resolved/rejected) + Factory.
- Öffentliches Formular /release/{slug}/rechtliches (LegalRequestController +
  web/legal-request.blade.php): typ-abhängiger Hinweistext (Alpine), E-Mail bei
  DSGVO/Persönlichkeitsrecht erforderlich, zwei versteckte Honeypot-Felder,
  Rate-Limit + Bremse "1 offene Anfrage pro PM/Typ". Regeltexte als Entwurf mit
  TODO für rechtliche Finalisierung markiert.
- Routen bewusst in eigener routes/legal.php (entkoppelt vom laufenden Web-Umbau),
  host-agnostisch via domains.php eingebunden.
- Admin-Bereich "Recht & Compliance": Sidebar-Nav mit Offen-Zähler, Volt-Queue
  index/show (in Bearbeitung/erledigt/abgelehnt/wieder öffnen + interne Notiz).
- Tests: je Typ, Honeypots (Dataset), Bremse, Admin-Queue + Status-Übergänge.
- Doku: Detailplan WS-3-Status + Deployment-Migrationsreihenfolge ergänzt.

Hinweis: Der "Melden"-/E&F-Button auf der PM-Detailseite (release-detail.blade.php)
wird mit dem separaten Web-Frontend-Commit verdrahtet; Ziel ist legal-request.create.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:20:05 +00:00
068a5a4b49 WS-6: Google-Login via Laravel Socialite
- Socialite installiert; oauth_provider/oauth_provider_id an users (Migration).
- GoogleController (redirect/callback) + SocialAuthService: De-Dup über E-Mail,
  neuer User aktiv + verifiziert + customer (Verifizierung über den Google-
  Kanal), offener Selbst-Registrierer wird onboardet, deaktivierter Account wird
  NICHT reaktiviert. Abschluss über die gemeinsame LoginRedirect-Logik
  (rollengerecht, 403-sicher).
- Routen /auth/google/redirect + /auth/google/callback (guest), "Mit Google
  anmelden/registrieren"-Buttons auf Login und Register.
- config/services.php google + .env.example-Keys; Sicherheits-/Deployment-Doku
  ergänzt (Keys, Redirect-URI, Migration).

Tests: neuer User, De-Dup bestehender User, deaktivierter Account blockiert,
unverifizierter Registrierer onboardet, fehlgeschlagener Callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 10:39:19 +00:00
980763c362 WS-2: Firmen-Scope für PMs & Magic-Link-Zugang für Pressekontakte
Firmen-Scope (Fundament):
- PM-Zugriff war hart an user_id (Autor) gebunden. Jetzt additiv: Autor ODER
  Mitglied der zugeordneten Firma (Owner via owner_user_id oder company_user-
  Pivot). Geändert in PressReleasePolicy (canManage) sowie den Queries der
  Listen-, Show- und Edit-Komponenten. Helfer User::accessibleCompanyIds()/
  canAccessCompany(). Solo-Owner unverändert; Firmenmitglieder sehen/bearbeiten
  alle PMs ihrer Firma.

Magic-Link-Zugang für Pressekontakte (ContactAccessService):
- Öffentliches, enumeration-sicheres Formular (/pressekontakt-zugang) mit
  Honeypot + Rate-Limit. Eine hinterlegte Kontakt-E-Mail führt zu einem lazy
  angelegten, de-duplizierten customer-Account (aktiv, verifiziert über den
  Magic-Link-Kanal), der den Firmen seiner Kontakte als Mitglied zugeordnet
  wird. Versand über den bestehenden Login-Magic-Link (Generator + Consume
  wiederverwendet) – keine Schema-Änderung, kein paralleles System.
- Dezenter Einstiegslink von der Login-Seite (PM-Frontend-Wiring später).

Tests: PressReleaseCompanyScopeTest (3), ContactAccessTest (6, inkl. De-Dup,
Enumeration-Sicherheit, Honeypot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 08:33:12 +00:00
94cb209a9f WS-6: E-Mail-Verifizierung, Auth-Flow-Fixes & Legacy-Rollen-Sicherheitsfix
E-Mail-Verifizierung (Entscheidung 15.06.):
- User implementiert MustVerifyEmail; Registrierung legt inaktives, rollenloses
  Konto an und leitet auf die Danke-/Notice-Seite; Registered-Event versendet die
  Verifizierungsmail. Bestätigter Link aktiviert das Konto + vergibt customer-Rolle
  (ActivateUserAfterVerification). Backfill-Migration setzt email_verified_at für
  alle Bestands-User (sonst würde die verified-Middleware ~59k aktive Legacy-User
  aussperren). Seeder-User verifiziert.

Auth-Flow-Korrekturen:
- Magic-Link-Consume: rollensicherer Redirect ohne intended() (Customer landete
  sonst per stale intended=/dashboard im 403-Admin-Bereich).
- Guest-Redirect (bootstrap/app.php) rollen-/verifizierungsbewusst statt fix
  /dashboard – schließt die 403-Sackgasse auf /login und /register.
- Logout auf der Notice-Seite via echtes POST-Formular statt Livewire-Action
  (behebt 419 beim Session-Invalidate).
- Magic-Link-Anforderung über eigenes Modal mit separater E-Mail-Eingabe.
- Unverifizierte Login-Versuche landen auf der Notice-Seite.

Sicherheitsfix Legacy-Rollen:
- UserImporter mappte Alt-Gruppe 2 (Self-Publisher) auf editor (= Admin-Zugriff).
  Mapping auf customer korrigiert; Daten-Migration stuft die 65.950 fälschlichen
  Legacy-Editoren auf customer herab. Echte admin/api-only bleiben unberührt.

Tests: Registration, EmailVerification, Authentication (Guest-Redirect),
MagicLinkLogin (Modal/Redirect/Regression), Legacy-Import (Gruppen-Mapping).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 08:16:41 +00:00
cc7b3c3379 KI-generierte Bilder: eigener Lizenztyp, Anbieter-Bestätigung, Kennzeichnung
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:04:12 +00:00
afcca34f91 User-Panel-Restarbeiten: PM-Guard, Profil-Rework, USt-ID-Prüfung, Buchungspflicht-Adresse
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 14:36:18 +00:00
8f3261d0b4 Checkout: Stripe-Tax-Adressanforderung erfüllen
Stripe Tax verlangt eine gültige Kundenadresse. Beide Checkout-Sessions
erfassen jetzt die Rechnungsadresse verpflichtend und speichern sie am
Stripe-Customer (customer_update address/name = auto; Name ist Pflicht
bei aktivierter USt-ID-Abfrage). Zusätzlich liefert User::stripeAddress()
die lokale Rechnungsadresse bei der Customer-Anlage mit (Cashier-Hook).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:33:58 +00:00
c8dc99c3c8 Phase 9E (Abschluss): Checkout-Flows und Plan-Kontingent statt Quota-Stub
- Checkout-Backend: me.checkout.subscription (Tarif-Abo monatlich/jährlich)
  und me.checkout.single-pm (Einzel-PM 19 € netto, pending-Kauf mit
  Webhook-Erfüllung); StripeCheckoutService als mockbarer Stripe-Wrapper;
  Stripe Tax via Cashier::calculateTaxes() (Netto-Preise, USt-ID-Abfrage)
- Slot-Logik: Kontingent aus dem Tarif (plans.press_release_quota) plus
  bezahlte Einmalkäufe; Verbrauch bei Veröffentlichung zuerst aus dem
  Plan-Zähler, danach Einlösung des ältesten Einmalkaufs (consumed +
  PM-Verknüpfung); Grandfathered = unbegrenzt (Entscheidung 12.06.2026,
  Bestandsschutz); Stub-Spalte users.press_release_quota entfernt
- billing:sync-stripe-plans legt zusätzlich das Einzel-PM-Produkt an
  (STRIPE_PRICE_SINGLE_PM); Test-Mode-Sync gelaufen
- Buchungs-Seite: Rückmeldung nach Checkout (erfolg/abbruch/Guard-Hinweis)
- Tests: PressReleaseQuotaTest auf Plan-Semantik neu geschrieben,
  CheckoutFlowTest (8 Tests), Modal-/API-Tests angepasst; Suite 510 passed
- Doku: Billing-und-Rechnungskreise (Kontingent-Tabelle, Checkout-Routen,
  Webhook-Events, Stripe-CLI-Hinweis), PHASE-9-Plan 9E ✅, Checkliste,
  STATUS-ABGLEICH, PROGRESS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:10:32 +00:00
894a9436b0 USt-Behandlung: Netto-Preise, VatResolver und Steuer-Ausweis im MAN-Kreis
Einwand/Entscheidung 12.06.2026: Legacy fakturierte brutto (Steuer
inkludiert, z. B. 199 Euro; steuerbefreite Kunden mit Netto-Ausweis
167,23). Alle neuen Preise sind netto; die Steuer wird zur
Rechnungsstellung sauber validiert und ausgewiesen.

- VatResolver + VatTreatment: DE grundsaetzlich immer mit Steuer, EU nur
  mit (formal plausibler) USt-ID befreit (Reverse Charge inkl.
  Pflichthinweis), Drittlaender grundsaetzlich befreit;
  EU-Laenderliste + vat_rate in config/billing.php
- Schema: billing_addresses.vat_id + invoice_billing_addresses.vat_id
  (Snapshot pro Rechnung), invoices.tax_note; Profil-Formular schreibt
  die vorhandene USt-ID jetzt auch an die Rechnungsadresse
- ManualInvoiceService: rechnet auf Netto-Vertragsbasis
  (legacy_conditions.net_cents bzw. Netto-Katalogpreis) und bestimmt
  Steuer/is_netto/tax_note pro Rechnung ueber den VatResolver
- legacy:grandfather-subscriptions: leitet net_cents aus der letzten
  Legacy-Rechnung ab (brutto / 1,19 bzw. is_netto-Betrag direkt);
  fuer DE-Bestandskunden bleibt der Bruttobetrag unveraendert
  (199 brutto -> 167,23 netto + 31,77 USt = 199,00)
- Doku: Decision-Update 2.1 (Netto-Klarstellung), Phase-9-Plan,
  Checkliste, 05-DATABASE-MERGE 5.6; offen: VIES-Validierung der USt-ID

Tests: VatResolverTest (Datasets fuer alle Faelle), Reverse-Charge/
EU-/Drittland-Rechnungen, Netto-Ableitung; Suite 490 passed, 4 skipped.
Pint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:58:43 +00:00
d548f4b235 Phase 9D: Tarif-Datenmodell, Cashier und hybride Rechnungskreise STR-/MAN-
Tarif-Datenmodell (Decision-Update):
- plans: Starter/Business/Pro/Agency mit Monats-/Jahrespreis (Jahres =
  10 x Monat), PM-Kontingent, Tageslimit, Stripe-IDs; idempotenter Seeder
- single_purchases: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit
  Status-Lifecycle und Stripe-Checkout-Referenzen
- laravel/cashier ^16.5 installiert (freigegeben); User ist Billable,
  Cashier-Migrationen published + ausgefuehrt; lokale invoices()-Relation
  ueberschreibt bewusst die Cashier-Methode

Hybride Rechnungskreise (Entscheidung 12.06.2026):
- invoice_number_sequences + InvoiceNumberGenerator: atomare fortlaufende
  Nummern pro Kreis (STR- fuer den neuen Stripe-Shop, MAN- fuer den
  manuellen Legacy-Kreis); Alt-Archiv legacy_invoices bleibt unveraendert
- ManualInvoiceService + billing:generate-manual-invoices (Scheduler
  taeglich 04:30): prueft aktive/grandfathered user_payment_options ohne
  Stripe-Subscription auf erreichtes Periodenende, friert die
  Rechnungsadresse als Snapshot ein, stellt die MAN-Rechnung aus
  (Zahlungsziel billing.manual_due_days) und schaltet die Periode weiter;
  Konditions-Overrides via legacy_conditions, sonst Netto-Preis +
  billing.vat_rate; nicht abrechenbare Faelle werden geloggt und
  beim naechsten Lauf erneut geprueft

Submit-Gate:
- User::hasActiveBooking() prueft jetzt echt (hinter
  billing.enforce_booking): Cashier-Abo, bezahlter Einzel-/Extra-PM-Kauf
  oder laufende Legacy-Vereinbarung (MAN-Kreis)

Suite: 468 passed, 4 skipped (17 neue Billing-Tests). Pint clean.
Offen fuer 9E: Stripe-Checkout/Webhooks, STR-Spiegelung, Slot-Logik auf
Plan-Kontingent, Migration der aktiven Legacy-Zahlungen in
user_payment_options.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:15:46 +00:00
4419d9ff43 Phase 9 Block 1: Gelb-Routing Direkt-Live, Slot-Verbrauch bei Veroeffentlichung, Submit-Gate
9A — Gelb geht direkt live (Entscheidung 12.06.2026):
- routeByClassification(): Gelb durchlaeuft denselben Auto-Publish-Pfad
  wie Gruen (autoPublishApproved); nur Rot wird abgelehnt
- Scheduler publiziert faellige gelbe + gruene PMs; unklassifizierte
  bleiben als Fallback in der manuellen Queue

9B — Slot-Verbrauch bei Veroeffentlichung (Decision-Update 3.2):
- Increment aus submitForReview() entfernt; publish() und
  changeStatusFromAdmin() zaehlen idempotent beim ersten
  published-Uebergang (Pruefung ueber Status-Logs); Rot kostet nichts
- Submit-Guard: Einreichen erfordert freien Slot
  (QuotaExceededException, API 422)

9C — Submit-Gate vorbereitet (Decision-Update 5.1):
- User::hasActiveBooking()-Stub hinter config/billing.php
  (enforce_booking, Default aus); Tarif-Modul ersetzt nur den Rumpf
- Einreichungs-Modal zeigt ohne Buchung einen Buchungs-Hinweis;
  Server-Guard (BookingRequiredException), API antwortet 402
- Fix: Customer-Create legte PMs bei "Zur Pruefung senden" direkt mit
  Status review an (vorbei an Blacklist/Quota/KI/Status-Log) — laeuft
  jetzt immer ueber submitForReview()

Suite: 451 passed, 4 skipped (9 neue Tests). Pint clean.
Plan: docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md (Block 2 nach Review-Stopp).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:47:06 +00:00
a000238ca8 User Panel: Phase-8-Abschluss, Titelbild/Lizenzen/Zeitzonen und KI-Pruef-Pipeline
Phase 8 (Rest) + Umbauten vom 10./11.06.:
- Ein Titelbild pro PM (Cover 1280x580), SVG-Platzhalter-Set + Picker,
  PressReleaseCoverImage-Resolver
- Lizenz-/Rechteformular nach "Lizenztyp Bildupload" (7 Lizenztypen,
  Personen-/Sachrechte-Status, bedingte Pflichtfelder, Risikohinweise)
- Veroeffentlichungs-Box vereinfacht (Embargo aus der Form-UI entfernt),
  geplante Termine in Europe/Berlin (Speicherung UTC, DISPLAY_TIMEZONE)
- Quota-Stub (users.press_release_quota) + monatlicher Reset-Command
- Einreichungs-Modal einheitlich in Show/Create/Edit; Ghost-Buttons auf
  filled; PM-Editor-Layout responsive entkoppelt (.pr-editor-layout)

KI-Pruef-Pipeline (Phasen 1-5 des Entwicklungsplans):
- API-Haertung: status nicht mehr per API setzbar, eigene Submit-Route
  durch denselben Funnel (Blacklist, Quota, Status-Log)
- Klassifikation Rot/Gelb/Gruen asynchron (Queue classification,
  OpenAI-Treiber + deterministischer Fallback), ki_audits-Audit-Log
- Routing: Rot -> rejected + Mail, Gelb -> Review-Queue, Gruen ->
  Auto-Publish; Scheduler publiziert nur gruene faellige PMs
- Content-Score 0-100 -> Stufe (Standard/Geprueft/Hochwertig) inkl.
  Editor-Panel und Badges; Re-Klassifikation/-Score bei Aenderung
- Admin: KI-Badge + Filter, On-Demand-Pruefung mit Anbieter-Override

Suite: 442 passed, 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:30:13 +00:00
d2ba22c0cf create PM v0.5
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2026-05-20 19:14:39 +02:00
5b8bdf4182 12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
2026-05-12 18:32:33 +02:00
405df0a122 first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-10-20 17:53:02 +02:00