L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job
- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt, Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status - /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung), Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot, Enumeration-Schutz, Slug-Kollisionen - Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern, umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit) - Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi), Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil - Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand - Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding - Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,23 @@ model Tenant {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
// L15 Testphase: Lebenszyklus (Plattform-Daten, keine Mandanten-RLS)
|
||||
plan TenantPlan @default(FULL)
|
||||
trialSource String? @map("trial_source") // self_signup | platform
|
||||
trialStartedAt DateTime? @map("trial_started_at")
|
||||
/// Exklusives Ende: Beginn des Folgetags des gewählten Enddatums (Europe/Berlin)
|
||||
trialEndsAt DateTime? @map("trial_ends_at")
|
||||
convertedAt DateTime? @map("converted_at")
|
||||
readOnlySince DateTime? @map("read_only_since")
|
||||
/// null = keine automatische Löschung vorgemerkt
|
||||
deletionDueAt DateTime? @map("deletion_due_at")
|
||||
trialDeletedAt DateTime? @map("trial_deleted_at")
|
||||
trialReminder7At DateTime? @map("trial_reminder_7_at")
|
||||
trialReminder3At DateTime? @map("trial_reminder_3_at")
|
||||
trialReminder1At DateTime? @map("trial_reminder_1_at")
|
||||
trialExpiredNoticeAt DateTime? @map("trial_expired_notice_at")
|
||||
trialDeletionNoticeAt DateTime? @map("trial_deletion_notice_at")
|
||||
|
||||
users User[]
|
||||
roles Role[]
|
||||
auditLogs AuditLog[]
|
||||
@@ -79,6 +96,8 @@ model TenantSettings {
|
||||
lotseAddressForm String? @map("lotse_address_form")
|
||||
// L10b (Spec §31): monthly AI token budget (input + output) per tenant; null = env AI_MONTHLY_TOKEN_LIMIT, 0 = unlimited
|
||||
aiMonthlyTokenLimit Int? @map("ai_monthly_token_limit")
|
||||
// L15: „Erste Schritte"-Checkliste ({ done: string[], hidden: boolean })
|
||||
onboarding Json @default("{}")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@ -1433,3 +1452,62 @@ model BillingRecord {
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("billing_records")
|
||||
}
|
||||
|
||||
// ── L15 Testphase & Onboarding ─────────────────────────────────────────────────
|
||||
|
||||
enum TenantPlan {
|
||||
FULL
|
||||
TRIAL
|
||||
}
|
||||
|
||||
/// Selbstanmeldung „Kostenlos testen" vor der E-Mail-Bestätigung (Plattform-Tabelle, kein tenant_id).
|
||||
/// Keine Klartext-Passwörter/Token: Argon2id-Hash (+ Pepper) bzw. SHA-256 des Tokens; IP nur als HMAC.
|
||||
model TrialSignup {
|
||||
id String @id @default(cuid())
|
||||
/// pending | confirmed | superseded | expired | existing_account | failed
|
||||
status String @default("pending")
|
||||
companyName String @map("company_name")
|
||||
sector String?
|
||||
companySize String? @map("company_size")
|
||||
adminName String @map("admin_name")
|
||||
email String
|
||||
/// Wird nach der Bestätigung geleert.
|
||||
passwordHash String @map("password_hash")
|
||||
/// Gewähltes Enddatum YYYY-MM-DD (Europe/Berlin)
|
||||
trialEndDate String @map("trial_end_date")
|
||||
sampleData Boolean @default(true) @map("sample_data")
|
||||
modules String[] @default([])
|
||||
locale String @default("de")
|
||||
tokenHash String @unique @map("token_hash")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
ipHash String? @map("ip_hash")
|
||||
acceptedTermsAt DateTime @map("accepted_terms_at")
|
||||
confirmedAt DateTime? @map("confirmed_at")
|
||||
provisionedTenantId String? @map("provisioned_tenant_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([email])
|
||||
@@index([status, expiresAt])
|
||||
@@map("trial_signups")
|
||||
}
|
||||
|
||||
/// Daten-Export eines Mandanten (ZIP mit CSV/JSON + Dateien), auch im Nur-Lesen-Zustand.
|
||||
model TenantExport {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
requestedById String? @map("requested_by_id")
|
||||
/// queued | running | done | failed
|
||||
status String @default("queued")
|
||||
storageKey String? @map("storage_key")
|
||||
fileName String? @map("file_name")
|
||||
bytes Int?
|
||||
summary Json?
|
||||
error String?
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([tenantId, createdAt])
|
||||
@@map("tenant_exports")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user