# Data Model — Managed Tenant Onboarding Wizard v1 This design is aligned to current repo reality where the “managed tenant” is the existing `Tenant` model. ## Entity: Tenant (`App\\Models\\Tenant`) ### Relevant existing fields - `id` (PK) - `name` (display name) - `tenant_id` (Entra tenant GUID; used as canonical external id) - `external_id` (route key; kept in sync with `tenant_id` when present) - `domain` (optional) - `environment` (`prod|dev|staging|other`) - `app_client_id` (optional) - `app_client_secret` (encrypted cast; must never be displayed back to the user) - RBAC health / verification storage: - `rbac_last_checked_at` (datetime) - `rbac_last_setup_at` (datetime) - `rbac_canary_results` (array) - `rbac_last_warnings` (array) ### New fields (proposed) If onboarding needs to be explicitly tracked on the tenant record: - `onboarding_status` enum-like string: `not_started|in_progress|completed` (default: `not_started`) - `onboarding_completed_at` nullable datetime Rationale: makes it cheap to render “Resume wizard” / completion status without loading session records. ## Entity: TenantOnboardingSession (new) ### Table name (proposed) - `tenant_onboarding_sessions` ### Columns - `id` (PK) - `tenant_id` nullable FK → `tenants.id` - nullable at the very beginning if the user hasn’t provided a valid tenant GUID yet - `created_by_user_id` FK → `users.id` - `status` string: `active|completed|abandoned` - `current_step` string: `welcome|tenant_details|credentials|permissions|verification` - `payload` jsonb - contains non-secret form state only (e.g., name, tenant_id, domain, environment) - MUST NOT contain secrets - `last_error_code` nullable string - `last_error_message` nullable string (sanitized; no tokens/secrets) - `completed_at` nullable datetime - `abandoned_at` nullable datetime - `created_at`, `updated_at` ### Indexes and constraints - Ensure at most one active session per tenant: - PostgreSQL partial unique index: `(tenant_id)` where `status = 'active'` - Dedupe/resume lookup: - index `(created_by_user_id, status)` - index `(tenant_id, status)` ### State transitions - `active` → `completed` when: - tenant record exists - credentials requirement (if enabled) is satisfied - last verification run indicates success - `active` → `abandoned` when user explicitly cancels ## Entity: OperationRun (`App\\Models\\OperationRun`) Wizard-triggered checks must be observable via `OperationRun`. ### Relevant fields - `tenant_id` FK - `type` string (examples already in repo: `provider.connection.check`, `inventory.sync`, `compliance.snapshot`) - `status` / `outcome` - `run_identity_hash` (dedupe identity) - `context` (json) ### Idempotency Use `OperationRunService::ensureRun()` / `ensureRunWithIdentity()` to get DB-level active-run dedupe. ## Capability / Authorization model - Capabilities are strings from the canonical registry `App\\Support\\Auth\\Capabilities`. - Capability checks: - Membership: `CapabilityResolver::isMember()` - Capability: `CapabilityResolver::can()` - Tenant-scoped non-member access is denied-as-not-found (404) by `DenyNonMemberTenantAccess` middleware. - Filament actions use `App\\Support\\Rbac\\UiEnforcement` to apply: - hidden UI for non-members - disabled UI + tooltip for members lacking the capability - server-side 404/403 guardrails