From df18ff9077cc54e875ce0e71555e21b5aae1f3b7 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 15 Sep 2026 19:13:49 +0200 Subject: [PATCH] =?UTF-8?q?L15=20Testphase=20&=20Onboarding:=20Lane-Berich?= =?UTF-8?q?t,=20Smoke=20pr=C3=BCft=20mobilen=20Scope?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- docs/craftvia/lanes/testphase.md | 108 +++++++++++++++++++++++++++++++ scripts/smoke-testphase.ts | 2 +- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/craftvia/lanes/testphase.md diff --git a/docs/craftvia/lanes/testphase.md b/docs/craftvia/lanes/testphase.md new file mode 100644 index 0000000..f684f57 --- /dev/null +++ b/docs/craftvia/lanes/testphase.md @@ -0,0 +1,108 @@ +# Lane L15 – Testphase & Onboarding (`lane/testphase`) + +Stand: 2026-09-16 · Basis `6b8cdf5` (`feature/craftvia-mvp`, L1–L14 integriert) · Betriebsdoku: [../TESTPHASE.md](../TESTPHASE.md) + +## 1. Umfang / erfüllte Punkte + +| Punkt | Umsetzung | +|---|---| +| **1 Datenmodell** | Migration `20260916100000_testphase`: `Tenant.plan` (`FULL`/`TRIAL`), `trialSource`, `trialStartedAt`, `trialEndsAt` (exklusives Ende = Beginn des Folgetags in Europe/Berlin), `convertedAt`, `readOnlySince`, `deletionDueAt`, `trialDeletedAt`, Versandmarker `trialReminder7/3/1At`, `trialExpiredNoticeAt`, `trialDeletionNoticeAt` (Plattform-Daten, keine RLS). `TrialSignup` (Plattform-Tabelle ohne `tenant_id`): Firmendaten, E-Mail, Argon2id-Hash (+ Pepper, wird nach Bestätigung/Ablauf geleert), Enddatum, Beispieldaten/Module, Token-**SHA-256**, Ablauf 24 h, IP-**HMAC**, Status. `TenantExport` (Mandanten-Tabelle, `enable_tenant_rls`, beide `TENANT_MODELS`, `requestedById` in `pii-fields.ts`). `TenantSettings.onboarding` (Checkliste). | +| **2 Öffentlicher Wizard** `/testen` | 5 Schritte (Betrieb · Admin-Konto · Testzeitraum · Einrichtung · Zusammenfassung), Fortschrittsanzeige, alle Werte in einem State (Zurück ohne Datenverlust), Validierung je Schritt im Browser **und** per Server-Action (`checkTrialStepAction`, gleiche Regeln aus `lib/trial/signup.ts`), finale Prüfung serverseitig. Branche: Auswahlliste + Freitext, Betriebsgröße optional, Passwort-Policy wie Identity, Enddatum per Datumsfeld (Vorbelegung heute + 14, erlaubt morgen … heute + `TRIAL_MAX_DAYS`, Anzeige „Testphase bis TT.MM.JJJJ (X Tage)“), Beispieldaten ja/nein, Module vorausgewählt (abwählbar), Pflicht-Checkboxen mit Platzhalter-Seiten `/testen/nutzungsbedingungen`, `/testen/datenschutz`. Double-Opt-in-Mail → `/testen/bestaetigen` zeigt die Anmeldung, **Einrichten erst per POST** (Mail-Scanner verbrauchen nichts) → `provisionTrialTenant` (tenant-admin, TRIAL, Ende des Tages Berlin, Löschung +30 Tage) → **direkt angemeldet** über den bestehenden `login-ticket`-Provider → `/dashboard?welcome=1` mit **„Erste Schritte“** (Firmendaten, Team, Monteur einladen, erster Auftrag, mobile App; automatisch erkannt oder abhakbar, ausblendbar). Missbrauchsschutz: Rate-Limit je IP und je E-Mail (neue Scopes `trialSignup`, `trialConfirm`, `trialStepCheck`), Honeypot, identische Antwort bei vorhandener E-Mail (+ Hinweis-Mail, gleicher Hash-Aufwand gegen Timing), Slug-Kollisionen (Reservierung per `create`, `-2 … -20`, Zufallssuffix), ältere Links derselben Adresse entwertet. | +| **3 Plattform-Admin** | `/admin/trial` „Testmandant anlegen“ (Firma, Branche, Admin, Enddatum bis 1 Jahr, Beispieldaten) → Einladung über `issueToken("invitation")` + `sendUserInvitationMail` (bestehende Person: nur verknüpft). Mandantenliste: Spalte „Version“ mit Badges „Test bis …“ / „abgelaufen – nur lesen“ / „Löschung am …“ / „gelöscht“ / „Vollversion“, Filter Alle/Test/Voll. Mandantendetail: Karte „Testphase“ mit Enddatum ändern/verlängern (auch nach Ablauf), umwandeln, sofort beenden, Löschung vormerken/abbrechen – jeweils Popup mit Bestätigungs-Checkbox, nur Voll-Admins, Audit (scope platform, am Mandanten) mit before/after. | +| **4 Nur-Lesen nach Ablauf** | `services/trial/state.ts#assertTenantWritable` → `ServiceError("blocked", "trial_expired", { readOnly, message, deletionDueAt })`, zeitgenau über `trialEndsAt`. Zentral in `moduleGuard` (alle Modul-Actions inkl. Lotse), `requireApiContext` für jede nicht lesende `/api/v1`-Anfrage (Methode aus `withApi`, AsyncLocalStorage → 422; Sync, Uploads eingeschlossen), explizit in `/documents/upload`, `POST /api/v1/work-orders/[id]/documents` (ohne `withApi`), Einstellungen, Nutzer-/Rollenverwaltung, Lotse-Einstellungen, Onboarding. Worker überspringt `import-extraction`/`transcription` abgelaufener Mandanten. Offen bleiben Login, Konto, Lesen, `/files`, PDFs, Export. Lesepfade, die `moduleGuard` nutzen (mobile Seitenkontexte, Import-Datei-Download), verwenden `moduleGuard(key, { read: true })`; der Guard-Check verbietet diesen Modus in Actions. Banner in Backoffice und mobiler App: „Testphase endet in X Tagen“ (ab 7 Tagen) bzw. „Testphase abgelaufen – nur Lesezugriff. Daten werden am … gelöscht.“ + Kontakt (`TRIAL_CONTACT_EMAIL`) + Export-Link (Admins). | +| **5 Export** | `/settings/export` (tenant:manage, auch im Nur-Lesen-Zustand): Worker-Job `tenant-export` baut ZIP mit `csv/` (Semikolon, BOM, Formel-Injektion entschärft) + `json/` für Kunden, Ansprechpartner, Objekte, Teams, Nutzer (ohne Secrets), Aufträge, Checklisten, Material (Vorgabe/Verbrauch), Einsätze, Zeiten, Notizen, Berichte, Unterschriften, Fotos, Dokumente, Meilensteine, Abrechnung + `dateien/` (alle gespeicherten Dateien inkl. Berichts-PDFs, Limit 500 MB) + `LIESMICH.txt`; Ablage über `storage.put` unter `/uploads/`, Download `/settings/export/` (Session + DB-autoritatives `tenant:manage`, 7 Tage, Audit). Der DSGVO-/Backup-Export ist ein Betreiber-Werkzeug (alle Tabellen, JSON, Plattform-Portal) – wiederverwendet wurde sein ZIP-Writer (`backup/zip.ts`). | +| **6 Lebenszyklus-Job** | Queue `trial-lifecycle`, Job-Scheduler `trial-lifecycle-daily` (24 h) in `scheduleRecurringJobs`. Erinnerungen 7/3/1 Tage (verpasster Lauf → nur die nächstliegende, ältere als erledigt markiert), Ablaufmail, Löschhinweis 7 Tage vorher, Löschung an `deletionDueAt` über `dsgvo/deletion.ts#offboardTenant` (Topologie aller `TENANT_MODELS`, Identities ohne Rest-Mitgliedschaft, Löschnachweis) + Objektspeicher-Präfix `/` + Plattform-Audit. Jede Mail wird per bedingtem Update beansprucht (genau einmal, auch parallel). Doppelprüfung + Claim unmittelbar vor dem Löschen (TRIAL, nicht umgewandelt, abgelaufen, fällig → `SUSPENDED`, danach `ARCHIVED`). Anmeldungen werden 7 Tage nach Link-Ablauf gelöscht. | +| **7 i18n/Doku** | Namespace `trial` (de/en), `nav.dataExport`; Mail-Vorlagen `trial_confirm`, `trial_existing_account`, `trial_reminder`, `trial_expired`, `trial_deletion_notice` (de/en, `TRIAL_TEMPLATE_KEYS`). `docs/craftvia/TESTPHASE.md` (Lebenszyklus, env, Jobs, Sperre, Datenschutz), Abschnitte in `DEPLOY.md` (7.4) und `API.md`, env-Beispiele. | + +### Entscheidungen + +- **Beispieldaten:** `scripts/lib/demo-seed.ts` setzt sieben Nutzer mit festen Rollen sowie Foto-/PDF-/Import-Pipeline voraus. Ein Testmandant startet mit genau einem Admin → `services/trial/sample-data.ts` ist eine gekürzte Variante nach demselben Prinzip (ausschließlich echte Fachservices: 3 Kunden, 3 Objekte, „Beispielteam“, 4 Aufträge in Entwurf/Geplant/Zugewiesen/Prüfung; Kennung `BEISPIEL-`, zählt nicht für die Checkliste). +- **Provisionierung:** `provisionTenant` upsertet per Slug. Damit zwei gleichnamige Anmeldungen nie im selben Mandanten landen, reserviert `provisionTrialTenant` den Slug per `create` (inkl. Testphasen-Feldern) und ruft erst dann `provisionTenant` auf. +- **Sync im Nur-Lesen-Zustand:** Der Batch wird mit 422 abgewiesen, bevor eine Operation angewendet wird; die Outbox behandelt Nicht-OK beim Batch als vorübergehend und behält die Operationen (kein Datenverlust nach Verlängerung/Umwandlung). `services/sync/apply.ts` blieb unverändert. +- **Plattform-Wizard** als eine Seite mit vier nummerierten Abschnitten (Betreiber-Werkzeug, Einladung statt Passwort). + +## 2. Dateien + +**Neu** +- Migration `prisma/migrations/20260916100000_testphase/` +- `src/lib/trial/{dates,signup}.ts` +- `src/server/services/trial/{config,state,signup,abuse,provision,sample-data,admin,lifecycle,export,onboarding,mail,jobs,storage-purge}.ts` +- `src/server/actions/trial-{signup,platform,tenant}.ts` +- `src/server/jobs/processors/{trial-lifecycle,tenant-export}.ts` +- `src/app/testen/{layout,page}.tsx`, `src/app/testen/{bestaetigen,nutzungsbedingungen,datenschutz}/page.tsx` +- `src/app/(app)/settings/export/page.tsx`, `src/app/(app)/settings/export/[id]/route.ts`, `src/app/(platform)/admin/trial/page.tsx` +- `src/components/trial/{signup-wizard,confirm-form,legal-placeholder,trial-banner,getting-started,trial-badge,trial-admin-card,platform-forms}.tsx` +- `messages/{de,en}/trial.json`, `docs/craftvia/TESTPHASE.md`, dieser Bericht +- Tests/Smoke: `scripts/test-testphase-{signup,readonly,lifecycle}.ts`, `scripts/lib/testphase-fixture.ts`, `scripts/smoke-testphase.ts` + +**Fundament-/Fremdeingriffe (laut Auftrag erlaubt, jeweils minimal)** +| Datei | Eingriff | Grund | +|---|---|---| +| `prisma/schema.prisma` | Tenant-Felder, Enum `TenantPlan`, `TenantSettings.onboarding`, Modelle `TrialSignup`, `TenantExport` | Punkt 1 | +| `src/server/provision.ts` | optional `admin.passwordHash`, `admin.mustChangePassword`, `modules`; Rückgabe um `adminUserId`, `identityId`, `identityCreated` ergänzt; Identity per `findUnique`+`create` statt `upsert` (Verhalten für Bestandsaufrufer gleich) | Hash aus der Anmeldung übernehmen, Modulauswahl, Einladung nur für neue Identity | +| `src/server/action-guard.ts` | `assertTenantWritable` nach Modul-Check; Option `{ read: true }` | zentrale Sperre; Lesepfade | +| `src/server/api/respond.ts` | `withApi` legt die Methode in AsyncLocalStorage ab (`currentApiMethod`, `isMutatingApiRequest`) | Sperre nur für nicht lesende Anfragen | +| `src/server/api/context.ts` | `assertApiWriteAllowed(tenantId)` in `requireApiContext` | zentrale API-Sperre | +| `src/app/(app)/documents/upload/route.ts`, `src/app/api/v1/work-orders/[id]/documents/route.ts` | je 1 Zeile `assertTenantWritable` | Upload-Routen ohne `withApi` | +| `src/server/actions/{tenant-settings,tenant-users,lotse-settings}.ts` | je 1 Zeile Sperre (+ Klartext in `tenant-users#actionError`) | Einstellungen/Nutzerverwaltung gesperrt | +| `src/server/services/field/page-context.ts`, `src/app/(field)/m/emergency/page.tsx`, `src/app/(app)/imports/[id]/file/route.ts` | `moduleGuard(key, { read: true })` | Lesepfade ohne Sperre (Smoke-Befund: sonst 500 auf `/m`) | +| `src/proxy.ts` | `/testen` in `PUBLIC_PATHS` | öffentliche Anmeldung | +| `src/server/rate-limit.ts` | Scopes `trialSignup`, `trialConfirm`, `trialStepCheck` | Missbrauchsschutz | +| `src/server/mail/templates.ts` | 5 Vorlagen de/en, `TRIAL_TEMPLATE_KEYS` | Mails | +| `src/server/jobs/{queues,processors/index}.ts`, `scripts/craftvia-worker.ts` | 2 Queues + Scheduler, 2 Registry-Zeilen, Sperrprüfung vor jedem Job | Punkt 5/6 | +| `src/app/(app)/layout.tsx`, `src/app/(field)/m/layout.tsx` | `` | Banner | +| `src/app/(app)/dashboard/page.tsx` (L2) | 1 Zeile `` | Checkliste im Dashboard | +| `src/app/(platform)/admin/page.tsx`, `admin/[id]/page.tsx` | Badge-Spalte, Filter, Button; Karte `TrialAdminCard` | Punkt 3 | +| `src/lib/nav.ts`, `messages/{de,en}/nav.json` | Eintrag „Datenexport“ | Einzeiler | +| `src/server/db.ts`, `src/server/backup/topology.ts`, `src/server/dsgvo/pii-fields.ts` | `TenantExport` | Pflicht bei neuer Tenant-Tabelle | +| `scripts/check-module-guards.ts` | 3 Einträge, Kategorie `PUBLIC` (jede Action braucht Rate-Limit), Verbot des Lese-Modus in Actions | Top-Level-Actions | +| `scripts/test-e2e-tenant-isolation.ts` (L10) | 1 Fixture-Zeile `TenantExport` | Test verlangt Abdeckung aller Tenant-Modelle | +| `.env.example`, `.env.prod.example`, `.env.coolify.example`, `docs/craftvia/{DEPLOY,API}.md` | Testphase-Abschnitte | Betrieb | + +**Neue env-Variablen:** `TRIAL_MAX_DAYS` (Default 30), `TRIAL_CONTACT_EMAIL` (optional). Keine neuen npm-Abhängigkeiten. + +## 3. Tests + +| Skript | Prüfungen | Inhalt | +|---|---|---| +| `test-testphase-signup.ts` | 76 | Grenzen (morgen … heute + 30, Vorbelegung), Pflichtfelder, Passwort-Policy, Modul-/Checkbox-Pflicht, Normalisierung, `TRIAL_MAX_DAYS`, Ende des Tages Berlin (Winter/Sommer/Umstellung); ungültig/Honeypot ohne Seiteneffekt; Double-Opt-in (nur Token-Hash, Argon2id, IP-HMAC, 24 h, kein Mandant vor Bestätigung, ältere Links entwertet, Ablauf → expired + Hash geleert, Einmalverwendung); Provisionierung (TRIAL, Enddatum, Löschtermin, Slug, tenant-admin, Login-Passwort = Wizard-Passwort, Modulauswahl, Beispieldaten über Fachservices, Audit); Enumeration (identische Antwort, Hinweis-Mail ohne Link, keine offene Anmeldung); Rate-Limit je IP/E-Mail inkl. Retry-After, jede öffentliche Action limitiert, Proxy; ohne Beispieldaten; Slug-Kollision `-2`; Mandant B liest/ändert keine Kunden von A | +| `test-testphase-readonly.ts` | 59 | Checkliste (automatisch erkannt, Beispieldaten zählen nicht, manuell, Monteur forbidden/unsichtbar, Vollversion ohne Checkliste); Ablauf → `blocked trial_expired` mit Klartext; `withApi`+Sperre: POST/DELETE 422, GET 200, Mandant B 200; Sync-Batch 422 ohne angewendete Operation; statisch: moduleGuard, requireApiContext, **alle 24 mutierenden /api/v1-Routen**, Upload-Routen, Einstellungen, Nutzerverwaltung, Lotse, Worker, Lese-Modus nur in 3 Lesepfaden und nie in Actions; Jobs (Import/Transkription übersprungen, PDF/Export/Mandant B nicht); Lesen erlaubt; Export im Nur-Lesen-Zustand (ZIP, CSV mit BOM, JSON, Datei byte-identisch, keine Daten von B, keine Hashes, Formel-Injektion, paralleler Export conflict, Mandant B not_found, Monteur forbidden, Download-Ablauf, Audit); Verlängern hebt die Sperre auf; Mandant B unverändert | +| `test-testphase-lifecycle.ts` | 67 | Erinnerungen 7/3/1 genau einmal, verpasster Lauf; Ablaufmail einmal + readOnlySince; Löschhinweis einmal; Löschung nur fällig (Tabellen leer, Identity, Speicherobjekt, Nachweis, Plattform-Audit, keine Wiederholung); Vollversion B und nicht fälliger Test B2 unberührt; Umwandeln/Verlängern/Abbrechen verhindern Löschung; Vormerken ≥ 7 Tage, folgt neuem Ende; Audit before/after je Aktion; Plattform-Rechte (Mandanten-Admin, Read-only-Admin → forbidden, Action ohne Plattform-Session abgewiesen, Datumsgrenzen, Vollversion invalid, Mandanten-Actions ohne Testphasen-Änderung); Wizard (freies Enddatum, Einladung, Passwortzwang, Token, Beispieldaten, bestehende Person ohne neue Einladung, Audit); Vorlagen de/en; Processor + Scheduler | + +**Gate (`npm run gate`) grün:** prisma generate, tsc, lint (0 Fehler; 3 vorbestehende Warnungen in fremden Dateien), build inkl. Guard-Check (39 Action-Dateien), **79/79 Testskripte**. Lane-DB `craftvia_testphase`, `RLS_DATABASE_URL` auf dieselbe DB. Ein Lauf mit `RLS_ENFORCED=true` wurde nicht durchgeführt. + +**HTTP-Smoke** (`next build && next start -p 3115`, Session-Cookies ohne Passworteingabe): `scripts/smoke-testphase.ts` **32/32** – anonym `/testen` (Schritt 1 von 5), Bestätigung mit ungültigem Token, Rechtstexte, Redirects; abgelaufener Admin: Banner + Löschdatum + Export-Link, Lesen, Exportseite, `GET /api/v1/customers` 200, **422 `trial_expired`** für `POST /api/v1/customers`, `POST /api/v1/work-orders/[id]/documents`, `POST /documents/upload`; abgelaufener Monteur: `/m`, `/m/orders`, `/m/emergency` mit Banner, Auftrag ohne Zuweisung 404, Bundle 200, **422** für `/api/v1/sync` und `/api/v1/uploads`, keine Exportseite; laufender Test: „Testphase endet in 3 Tagen.“, „Erste Schritte“, Willkommen; Plattform: Liste mit Badges/Filtern, Wizard, Detail mit Karte und Popups. Zusätzlich `scripts/smoke-auth.ts` **137/137** (keine Regression). Der erste Smoke-Lauf fand den 500 auf `/m` (Lese-Kontext über `moduleGuard`) → behoben in `273a453`. + +**Visuell:** nicht geprüft – die Navigation des Browser-Panels auf localhost wurde abgelehnt. Bitte `/testen` (Wizard, 375/768/1024 px), Banner und Plattform-Karte manuell ansehen. + +## 4. Stubs / Abhängigkeiten + +Keine Stubs. Genutzt: `provisionTenant`, `issueToken`/`sendUserInvitationMail`, `login-ticket`/`signIn`, `offboardTenant` + Topologie, `storage`/`readStoredBytes`, `buildZip`, `enqueueMail`, `enqueueJob`/Scheduler, Fachservices L1/L2 (Kunden, Objekte, Teams, Aufträge, Zuweisung). + +## 5. Bekannte Lücken / offene Punkte + +1. **Offline-Fotos nach Ablauf:** `/api/v1/uploads` antwortet 422; die Outbox (L7) wertet 422 beim Upload als endgültig ungültig und verwirft das Blob auf dem Gerät. Sync-Operationen bleiben erhalten. Vorschlag L7: `blocked`/`trial_expired` als vorübergehend behandeln. +2. **Fehlertexte in fremden Formularen:** Actions anderer Lanes zeigen bei der Sperre ihre eigene Fehlerdarstellung (Code `blocked`/`trial_expired`, teils generisch); der Banner erklärt den Zustand. Eine einheitliche Klartext-Zuordnung je Lane steht aus. +3. **Rate-Limits je App-Instanz** (In-Memory wie SEC2/L10b). +4. **Rechtstexte** sind Platzhalter (`messages/*/trial.json` → `legal.*`). +5. **Fallback nach Bestätigung:** Scheitert die direkte Anmeldung (z. B. künftige MFA-Pflicht), leitet die Action auf `/login?trial=ready`; die Login-Seite (Fundament) zeigt dazu keinen eigenen Hinweis. +6. **Plattform-Einladung an bestehende Personen:** wird nur verknüpft (wie `createTenantUser`), ohne Hinweis-Mail. +7. **Export** im Speicher gebaut (Dateien bis 500 MB, kein ZIP64/Streaming); alte Export-Dateien werden nicht automatisch aus dem Speicher entfernt (nur mit dem Mandanten). +8. **Worker-Sperre** nur für `import-extraction` und `transcription`; Benachrichtigungs-Mails aus Ereignissen vor dem Ablauf laufen weiter. +9. **Slug** eines gelöschten Testmandanten bleibt belegt (Zeile `ARCHIVED` mit Löschnachweis); neue Anmeldungen erhalten ein Suffix. +10. **Checkliste:** „Mobile App öffnen“ nur manuell abhakbar; Checkliste nur für Mandanten, die als Testphase gestartet sind. +11. **RLS-Modus** (`RLS_ENFORCED=true`) für die neuen Tests nicht gelaufen. + +## 6. Screens / Routen + +| Route | Zugriff | Inhalt | +|---|---|---| +| `/testen` | öffentlich | Wizard „Kostenlos testen“ | +| `/testen/bestaetigen?token=` | öffentlich | Anmeldung prüfen, „Jetzt einrichten“ (POST) → Dashboard | +| `/testen/nutzungsbedingungen`, `/testen/datenschutz` | öffentlich | Platzhalter-Rechtstexte | +| `/dashboard` | Backoffice | Karte „Erste Schritte“ (Admins, Testphasen-Mandanten) | +| alle `(app)`- und `/m`-Seiten | angemeldet | Testphasen-Banner | +| `/settings/export`, `/settings/export/` | `tenant:manage` | Datenexport anfordern/herunterladen | +| `/admin` | Plattform | Spalte „Version“, Filter Alle/Test/Voll, Button „Testmandant anlegen“ | +| `/admin/trial` | Plattform (Voll-Admin für Aktion) | Wizard „Testmandant anlegen“ | +| `/admin/?trial=extend|convert|end|schedule|cancel` | Plattform-Voll-Admin | Karte „Testphase“ + Bestätigungs-Popups | diff --git a/scripts/smoke-testphase.ts b/scripts/smoke-testphase.ts index 47950e3..3e41e04 100644 --- a/scripts/smoke-testphase.ts +++ b/scripts/smoke-testphase.ts @@ -91,7 +91,7 @@ async function main() { checks: [ { path: "/m", mustContain: ["Testphase abgelaufen – nur Lesezugriff.", 'data-trial-banner="expired"'] }, { path: "/m/orders", mustContain: ["Testphase abgelaufen – nur Lesezugriff."] }, - { path: `/m/orders/${order.id}`, mustContain: [order.number] }, + { label: "GET /m/orders/[id] ohne Zuweisung → 404 (Scope)", path: `/m/orders/${order.id}`, expect: [404] }, { path: "/m/emergency", mustContain: ["Testphase abgelaufen – nur Lesezugriff."] }, { path: "/api/v1/field/bundle", mustContain: ['"orders"'] }, { label: "POST /api/v1/sync → gesperrt", path: "/api/v1/sync", method: "POST", body: JSON.stringify({ deviceId: "zz-smoke", operations: [] }), headers: { "content-type": "application/json" }, expect: [422], mustContain: ["trial_expired"] },