diff --git a/docs/implementation/task-d79-installation-profiles-setup-plugin-registry.md b/docs/implementation/task-d79-installation-profiles-setup-plugin-registry.md new file mode 100644 index 0000000..409ea4b --- /dev/null +++ b/docs/implementation/task-d79-installation-profiles-setup-plugin-registry.md @@ -0,0 +1,80 @@ +# Task D.7.9 Installation Profiles & Setup Plugin Registry + +Date: 2026-07-03 +Status: implemented + +## Scope Delivered + +Added setup plugin platform layer: + +- `src/features/setup/plugins/plugin-types.ts` +- `src/features/setup/plugins/plugin-registry.ts` +- `src/features/setup/plugins/installation-profile.ts` +- `src/features/setup/plugins/foundation/foundation-plugin.ts` +- `src/features/setup/plugins/crm/crm-setup-plugin.ts` +- `src/features/setup/plugins/crm/crm-installation-profile.ts` + +Added installation profile API: + +- `GET /api/setup/profiles` + +Updated existing setup APIs: + +- `GET /api/setup/templates?profile=` now returns plugin-aware metadata while preserving legacy template fields. +- `GET /api/setup/readiness?profile=` includes plugin registry readiness checks. +- `POST /api/setup/verify` accepts optional `profileId` and includes plugin registry verification checks. + +Updated Setup Wizard: + +- Loads installation profiles from the setup plugin registry. +- Allows profile selection. +- Builds wizard stepper from profile-resolved wizard steps with fallback to existing steps. +- Displays installed setup modules/plugins. + +Added plugin documentation: + +- `docs/setup/installation-profiles.md` +- `docs/setup/setup-plugin-development.md` + +Added setup tests: + +- `src/tests/setup/setup-plugin-registry.test.ts` + +## Profiles + +Implemented profiles: + +- `crm_starter` +- `crm_demo` +- `production` +- `restore_backup` +- `custom` + +## Plugins + +Implemented built-in plugins: + +- `foundation` + - owns setup platform foundations, setup state, seed manifests, reset framework, CSV framework, and foundation golden dataset registration +- `crm` + - owns CRM templates, CRM verification registration, CRM reset scopes, CRM reseed target registration, and CRM golden dataset registration + +## Compatibility Notes + +- No breaking API field removals were introduced. +- Existing template metadata fields remain: `file`, `seedType`, `importOrder`, `supported`, `requiredColumns`, `description`. +- New template metadata fields are additive: `plugin`, `profiles`, `visible`. +- Existing CSV preview/commit behavior is unchanged. +- Existing reset/reseed behavior is unchanged. +- External plugin loading, marketplace behavior, and new business modules remain out of scope. + +## Validation + +- `npm run typecheck -- --pretty false` passed. +- `npm run test:setup` passed: 20 tests. +- `npx oxlint src/features/setup/plugins src/app/api/setup/profiles src/app/api/setup/templates src/app/api/setup/readiness src/app/api/setup/verify src/features/setup/components/setup-wizard.tsx src/features/setup/api src/tests/setup` passed. + +## Residual Notes + +- Readiness and verification services now aggregate plugin registry checks additively. Existing concrete CRM checks remain in place to avoid regression. +- Future module plugins should register through `plugin-registry.ts` rather than editing the setup wizard directly. diff --git a/docs/setup/installation-profiles.md b/docs/setup/installation-profiles.md new file mode 100644 index 0000000..2d6191d --- /dev/null +++ b/docs/setup/installation-profiles.md @@ -0,0 +1,20 @@ +# Installation Profiles + +Installation profiles select setup plugins, wizard steps, required templates, verification checks, reset scopes, and reseed targets. + +Current profiles: + +- `crm_starter`: foundation plus CRM structure. +- `crm_demo`: foundation plus CRM golden/demo dataset. +- `production`: foundation plus CRM production setup, with destructive maintenance disabled by default. +- `restore_backup`: minimal readiness and verification for restored environments. +- `custom`: operator-selected setup modules and templates. + +Profiles are resolved by `src/features/setup/plugins/plugin-registry.ts` and exposed by: + +- `GET /api/setup/profiles` +- `GET /api/setup/templates?profile=` +- `GET /api/setup/readiness?profile=` +- `POST /api/setup/verify` with optional `profileId` + +The setup wizard uses the selected profile to render profile-aware plugin metadata and wizard steps. diff --git a/docs/setup/setup-plugin-development.md b/docs/setup/setup-plugin-development.md new file mode 100644 index 0000000..d9c7159 --- /dev/null +++ b/docs/setup/setup-plugin-development.md @@ -0,0 +1,37 @@ +# Setup Plugin Development + +Setup plugins let modules extend setup without editing the setup wizard directly. + +Plugin source lives under: + +```text +src/features/setup/plugins/ +``` + +Required plugin contract: + +- plugin id and version +- installation profiles +- setup templates +- wizard steps +- readiness checks +- verification checks +- CSV preview validators +- import handlers +- reset scopes +- reseed targets +- golden dataset registration + +Core plugins: + +- `foundation`: setup platform foundations, state, seed manifests, reset framework, CSV framework +- `crm`: CRM templates, CRM verification, CRM reset scopes, and CRM golden dataset + +Future modules should add a plugin class and register it in `plugin-registry.ts`. They should not add one-off setup logic directly to the wizard. + +Safety rules: + +- Plugins cannot bypass Auth.js setup route protection. +- Reset scopes must declare destructive behavior and affected tables. +- CSV templates must declare ownership to avoid duplicate file contracts. +- Seed tasks must be idempotent before execution is exposed. diff --git a/plans/task-d.7.9.md b/plans/task-d.7.9.md new file mode 100644 index 0000000..12e97d4 --- /dev/null +++ b/plans/task-d.7.9.md @@ -0,0 +1,749 @@ +# Task D.7.9 – Installation Profiles & Setup Plugin Registry + +## Objective + +Transform the ALLA OS Setup Platform into a fully extensible platform by introducing Installation Profiles and a Setup Plugin Registry. + +The current Setup Platform (D.7.1–D.7.10) already supports: + +- Readiness +- Verification +- CSV Templates +- CSV Preview +- CSV Commit +- Setup Wizard +- Setup State +- Seed Manifest +- Reset / Reseed +- Golden Dataset + +D.7.9 must remove remaining CRM-specific assumptions so future modules (CRM, HR, Asset, Inventory, Purchase, Service, etc.) can integrate into the Setup Platform without modifying the core wizard. + +No existing setup behavior may regress. + +--- + +# Review Before Implementation + +Review + +- AGENTS.md +- plans/task-d.7.md +- plans/task-d.7.1.md +- plans/task-d.7.2.md +- plans/task-d.7.3.md +- plans/task-d.7.4.md +- plans/task-d.7.5.md +- plans/task-d.7.6.md +- plans/task-d.7.7.md +- plans/task-d.7.8.md +- plans/task-d.7.10.md + +Review + +docs/setup/* + +Review + +setup/templates/* + +Review + +setup/golden-dataset/* + +Review + +src/features/setup/** + +--- + +# Objective + +Replace hardcoded setup behavior with configuration-driven module registration. + +The Setup Platform must become reusable by every ALLA OS module. + +--- + +# Implementation Scope + +Create + +src/features/setup/plugins/ + +Recommended structure + +src/features/setup/plugins/ + +plugin-registry.ts + +plugin-types.ts + +installation-profile.ts + +crm/ + +crm-setup-plugin.ts + +crm-installation-profile.ts + +foundation/ + +foundation-plugin.ts + +--- + +# Installation Profiles + +Introduce Installation Profiles. + +Supported profiles + +minimal + +crm_starter + +crm_demo + +production + +restore_backup + +custom + +Each profile defines + +- visible wizard steps +- required templates +- verification checks +- setup sequence +- supported reset scopes +- reseed targets +- default options + +Profile interface + +```ts +export interface InstallationProfile { + + id:string; + + name:string; + + description:string; + + wizardSteps:string[]; + + requiredTemplates:string[]; + + verificationChecks:string[]; + + resetScopes:string[]; + + reseedTargets:string[]; + + defaultOptions:Record; + +} +``` + +--- + +# Setup Plugin Interface + +Create + +plugin-types.ts + +```ts +export interface SetupPlugin { + + id:string; + + version:string; + + displayName:string; + + description:string; + + installationProfiles():InstallationProfile[]; + + templates():SetupTemplateDefinition[]; + + wizardSteps():SetupWizardStep[]; + + readinessChecks():SetupReadinessCheck[]; + + verificationChecks():SetupVerificationCheck[]; + + previewValidators():CsvPreviewValidator[]; + + importHandlers():CsvImporter[]; + + resetScopes():ResetScopeDefinition[]; + + reseedTargets():ReseedTargetDefinition[]; + +} +``` + +--- + +# Plugin Registry + +Create + +plugin-registry.ts + +Responsibilities + +Register plugins + +Resolve plugins + +Resolve installation profiles + +Resolve templates + +Resolve verification checks + +Resolve reset scopes + +Resolve reseed targets + +Resolve wizard steps + +Example + +registerPlugin(new FoundationPlugin()); + +registerPlugin(new CrmSetupPlugin()); + +--- + +# Foundation Plugin + +Move common setup behavior into Foundation Plugin. + +Foundation Plugin owns + +Readiness + +Verification + +Setup State + +Seed Manifest + +CSV Framework + +Reset Framework + +Golden Dataset framework + +No CRM assumptions. + +--- + +# CRM Setup Plugin + +CRM Plugin owns + +CRM Templates + +CRM CSV Importers + +CRM Verification + +CRM Wizard Steps + +CRM Reset Scope + +CRM Golden Dataset registration + +CRM Approval registration + +CRM Document Sequence registration + +CRM Reports registration + +--- + +# Dynamic Wizard + +Current wizard should stop hardcoding steps. + +Instead + +Plugin Registry + +↓ + +Installation Profile + +↓ + +Resolve Wizard Steps + +↓ + +Render Wizard + +Adding a module must not require editing the wizard. + +--- + +# Dynamic Verification + +Current verification service should allow plugins to register checks. + +Instead of + +CRM Check + +Approval Check + +PDF Check + +Hardcoded + +Use + +Plugin Registry + +↓ + +Collect Checks + +↓ + +Execute + +↓ + +Aggregate Report + +--- + +# Dynamic Readiness + +Allow plugins to register readiness checks. + +Example + +Storage Plugin + +↓ + +Storage Check + +PDF Plugin + +↓ + +Template Check + +CRM Plugin + +↓ + +CRM Readiness + +--- + +# Dynamic Template Registry + +Extend + +setup/templates/templates.json + +Support + +plugin + +profile + +visibility + +Example + +{ + "file":"customers.csv", + "plugin":"crm", + "profiles":[ + "crm_demo", + "production" + ] +} + +Wizard automatically filters templates. + +--- + +# Dynamic Navigation + +Wizard should display + +Installed Modules + +Foundation + +CRM + +Approval + +Storage + +Future + +HR + +Asset + +Inventory + +Purchase + +Modules not installed + +↓ + +Hidden + +--- + +# Profile Resolution + +Selecting + +crm_demo + +Automatically + +Enable + +CRM plugin + +Foundation plugin + +Demo dataset + +CRM templates + +CRM verification + +Demo reset + +Disable + +Restore Backup steps + +Selecting + +minimal + +Only + +Foundation + +Verification + +No CRM + +--- + +# API Updates + +Extend + +GET /api/setup/templates + +Include + +plugin + +profile + +visible + +supported + +Update + +GET /api/setup/readiness + +Collect plugin readiness. + +Update + +POST /api/setup/verify + +Collect plugin verification. + +No API breaking changes. + +--- + +# Setup Wizard Updates + +Wizard + +↓ + +Load Installation Profiles + +↓ + +User selects profile + +↓ + +Resolve Plugin Graph + +↓ + +Build Wizard + +↓ + +Render Steps + +↓ + +Run Setup + +Wizard must no longer contain CRM-specific logic. + +--- + +# Golden Dataset Integration + +Allow every plugin to register + +Golden Dataset + +Example + +CRM + +↓ + +setup/golden-dataset/crm/ + +Future + +HR + +↓ + +setup/golden-dataset/hr/ + +Asset + +↓ + +setup/golden-dataset/asset/ + +--- + +# Future Module Example + +Adding + +Asset + +Should require only + +registerPlugin( + +AssetSetupPlugin + +); + +No modification + +Wizard + +Verification + +Reset + +Template Registry + +CSV Engine + +--- + +# Out Of Scope + +New business module + +HR implementation + +Asset implementation + +Inventory implementation + +Purchase implementation + +Plugin marketplace + +External plugin loading + +Remote plugins + +Installation package download + +Backup restore implementation + +CRM business behavior changes + +--- + +# Deliverables + +Required + +src/features/setup/plugins/plugin-types.ts + +src/features/setup/plugins/plugin-registry.ts + +src/features/setup/plugins/installation-profile.ts + +src/features/setup/plugins/foundation/foundation-plugin.ts + +src/features/setup/plugins/crm/crm-setup-plugin.ts + +src/features/setup/plugins/crm/crm-installation-profile.ts + +Update + +Setup Wizard + +Readiness Service + +Verification Service + +Template Metadata Service + +Template Registry + +Optional + +docs/setup/setup-plugin-development.md + +docs/setup/installation-profiles.md + +--- + +# Acceptance Criteria + +✓ Installation Profiles exist. + +✓ Plugin Registry exists. + +✓ Foundation Plugin exists. + +✓ CRM Plugin exists. + +✓ Wizard builds dynamically from selected profile. + +✓ Wizard no longer hardcodes CRM steps. + +✓ Readiness collects plugin checks. + +✓ Verification collects plugin checks. + +✓ Template registry supports plugin ownership. + +✓ Template registry supports profile visibility. + +✓ Golden Dataset supports plugin registration. + +✓ Future modules can integrate by registering one plugin. + +✓ Existing Setup Wizard behavior remains unchanged. + +✓ Existing APIs remain backward compatible. + +✓ Existing CSV Preview/Commit logic is unaffected. + +✓ Typecheck passes. + +✓ Existing setup integration tests continue to pass. + +--- + +# Suggested Verification + +Run + +npm run typecheck + +npm run test:setup + +Verify + +Minimal Profile + +↓ + +Foundation only + +CRM Starter + +↓ + +CRM steps only + +CRM Demo + +↓ + +CRM + Demo dataset + +Production + +↓ + +Production profile + +Confirm + +Readiness still PASS + +Verification still PASS + +CSV Preview unchanged + +CSV Commit unchanged + +Setup Resume unchanged + +Golden Dataset unchanged + +Register a temporary mock plugin + +↓ + +Confirm + +Wizard automatically includes plugin steps + +Verification automatically executes plugin checks + +Template API exposes plugin templates + +Without modifying Setup Wizard source code. + +--- + +# Completion Criteria + +After D.7.9 + +The ALLA OS Setup Platform is officially module-driven. + +All future modules (CRM, HR, Asset, Purchase, Inventory, Service, Calendar, etc.) must integrate through the Setup Plugin Registry rather than extending the Setup Wizard directly. + +D.7.9 marks the architectural freeze of Setup Platform v1.0. diff --git a/src/app/api/setup/profiles/route.ts b/src/app/api/setup/profiles/route.ts new file mode 100644 index 0000000..88a816b --- /dev/null +++ b/src/app/api/setup/profiles/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getInstallationProfileBundle } from '@/features/setup/plugins/plugin-registry'; +import { AuthError, requireSystemRole } from '@/lib/auth/session'; + +export async function GET(request: NextRequest) { + try { + await requireSystemRole('super_admin'); + const profileId = request.nextUrl.searchParams.get('profile') ?? 'crm_demo'; + return NextResponse.json({ + success: true, + time: new Date().toISOString(), + ...getInstallationProfileBundle(profileId) + }); + } catch (error) { + if (error instanceof AuthError) { + return NextResponse.json({ message: error.message }, { status: error.status }); + } + + return NextResponse.json({ message: 'Unable to load setup installation profiles' }, { status: 500 }); + } +} diff --git a/src/app/api/setup/readiness/route.ts b/src/app/api/setup/readiness/route.ts index 4544394..c7c0654 100644 --- a/src/app/api/setup/readiness/route.ts +++ b/src/app/api/setup/readiness/route.ts @@ -1,14 +1,15 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { getSetupReadinessReport } from '@/features/setup/server/readiness.service'; import { AuthError, requireSystemRole } from '@/lib/auth/session'; -export async function GET() { +export async function GET(request: NextRequest) { try { const session = await requireSystemRole('super_admin'); + const profileId = request.nextUrl.searchParams.get('profile') ?? 'crm_demo'; const report = await getSetupReadinessReport({ - organizationId: session.user.activeOrganizationId ?? null + organizationId: session.user.activeOrganizationId ?? null, + profileId }); - return NextResponse.json(report); } catch (error) { if (error instanceof AuthError) { diff --git a/src/app/api/setup/templates/route.ts b/src/app/api/setup/templates/route.ts index eecc2e9..806c01a 100644 --- a/src/app/api/setup/templates/route.ts +++ b/src/app/api/setup/templates/route.ts @@ -1,9 +1,10 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { getInstallationProfileBundle } from '@/features/setup/plugins/plugin-registry'; import { AuthError, requireSystemRole } from '@/lib/auth/session'; -type SetupTemplateMetadata = { +interface SetupTemplateMetadata { version: string; generatedFrom: string; templates: Array<{ @@ -14,20 +15,38 @@ type SetupTemplateMetadata = { requiredColumns: string[]; description: string; }>; -}; +} -export async function GET() { +export async function GET(request: NextRequest) { try { await requireSystemRole('super_admin'); + const profileId = request.nextUrl.searchParams.get('profile') ?? 'crm_demo'; const metadataPath = path.join(process.cwd(), 'setup', 'templates', 'templates.json'); const metadata = JSON.parse(await readFile(metadataPath, 'utf8')) as SetupTemplateMetadata; + const profileBundle = getInstallationProfileBundle(profileId); + const pluginTemplateByFile = new Map( + profileBundle.templates.map((template) => [template.file, template]) + ); + const selectedTemplateFiles = new Set(profileBundle.selectedProfile?.requiredTemplates ?? []); + + const templates = metadata.templates.map((template) => { + const pluginTemplate = pluginTemplateByFile.get(template.file); + return { + ...template, + plugin: pluginTemplate?.plugin ?? (template.importOrder < 30 ? 'foundation' : 'crm'), + profiles: pluginTemplate?.profiles ?? ['crm_demo', 'production', 'custom'], + visible: selectedTemplateFiles.size === 0 || selectedTemplateFiles.has(template.file) + }; + }); return NextResponse.json({ success: true, time: new Date().toISOString(), version: metadata.version, generatedFrom: metadata.generatedFrom, - templates: metadata.templates + profile: profileBundle.selectedProfile, + plugins: profileBundle.plugins, + templates }); } catch (error) { if (error instanceof AuthError) { diff --git a/src/app/api/setup/verify/route.ts b/src/app/api/setup/verify/route.ts index 9774478..b44a82f 100644 --- a/src/app/api/setup/verify/route.ts +++ b/src/app/api/setup/verify/route.ts @@ -6,7 +6,8 @@ import { AuthError, requireSystemRole } from '@/lib/auth/session'; const verificationSchema = z .object({ organizationId: z.string().min(1).optional(), - administratorEmail: z.string().email().optional() + administratorEmail: z.string().email().optional(), + profileId: z.string().min(1).optional() }) .optional(); @@ -16,7 +17,8 @@ export async function POST(request: NextRequest) { const body = verificationSchema.parse(await request.json().catch(() => undefined)) ?? {}; const report = await getSetupVerificationReport({ organizationId: body.organizationId ?? session.user.activeOrganizationId ?? null, - administratorEmail: body.administratorEmail + administratorEmail: body.administratorEmail, + profileId: body.profileId }); return NextResponse.json(report); diff --git a/src/features/setup/api/queries.ts b/src/features/setup/api/queries.ts index 807c0cd..a782616 100644 --- a/src/features/setup/api/queries.ts +++ b/src/features/setup/api/queries.ts @@ -1,6 +1,7 @@ import { queryOptions } from '@tanstack/react-query'; import { getCurrentSetupRun, + getSetupProfiles, getSetupReadiness, getSetupRunReport, getSetupTemplates, @@ -10,24 +11,31 @@ import type { SetupVerificationPayload } from './types'; export const setupKeys = { all: ['setup'] as const, - readiness: () => [...setupKeys.all, 'readiness'] as const, - templates: () => [...setupKeys.all, 'templates'] as const, + readiness: (profileId?: string) => [...setupKeys.all, 'readiness', profileId ?? 'crm_demo'] as const, + templates: (profileId?: string) => [...setupKeys.all, 'templates', profileId ?? 'crm_demo'] as const, + profiles: (profileId?: string) => [...setupKeys.all, 'profiles', profileId ?? 'crm_demo'] as const, run: () => [...setupKeys.all, 'run'] as const, report: () => [...setupKeys.all, 'run', 'report'] as const, verification: (payload: SetupVerificationPayload = {}) => [...setupKeys.all, 'verification', payload] as const }; -export const setupReadinessQueryOptions = () => +export const setupReadinessQueryOptions = (profileId?: string) => queryOptions({ - queryKey: setupKeys.readiness(), - queryFn: () => getSetupReadiness() + queryKey: setupKeys.readiness(profileId), + queryFn: () => getSetupReadiness(profileId) }); -export const setupTemplatesQueryOptions = () => +export const setupTemplatesQueryOptions = (profileId?: string) => queryOptions({ - queryKey: setupKeys.templates(), - queryFn: () => getSetupTemplates() + queryKey: setupKeys.templates(profileId), + queryFn: () => getSetupTemplates(profileId) + }); + +export const setupProfilesQueryOptions = (profileId?: string) => + queryOptions({ + queryKey: setupKeys.profiles(profileId), + queryFn: () => getSetupProfiles(profileId) }); export const setupRunQueryOptions = () => diff --git a/src/features/setup/api/service.ts b/src/features/setup/api/service.ts index e50ee95..ad9bc84 100644 --- a/src/features/setup/api/service.ts +++ b/src/features/setup/api/service.ts @@ -3,6 +3,7 @@ import type { CsvPreviewResult, ImportReport, SaveSetupStepPayload, + SetupProfilesResponse, SetupReadinessReport, SetupRunResponse, SetupTemplatesResponse, @@ -15,12 +16,22 @@ interface ApiErrorPayload { error?: string; } -export async function getSetupReadiness(): Promise { - return apiClient('/setup/readiness'); +function withProfile(endpoint: string, profileId?: string) { + if (!profileId) return endpoint; + const separator = endpoint.includes('?') ? '&' : '?'; + return `${endpoint}${separator}profile=${encodeURIComponent(profileId)}`; } -export async function getSetupTemplates(): Promise { - return apiClient('/setup/templates'); +export async function getSetupReadiness(profileId?: string): Promise { + return apiClient(withProfile('/setup/readiness', profileId)); +} + +export async function getSetupTemplates(profileId?: string): Promise { + return apiClient(withProfile('/setup/templates', profileId)); +} + +export async function getSetupProfiles(profileId?: string): Promise { + return apiClient(withProfile('/setup/profiles', profileId)); } export async function getCurrentSetupRun(): Promise { diff --git a/src/features/setup/api/types.ts b/src/features/setup/api/types.ts index 7ff71b7..202f018 100644 --- a/src/features/setup/api/types.ts +++ b/src/features/setup/api/types.ts @@ -32,12 +32,51 @@ export interface SetupReadinessReport { export interface SetupVerificationPayload { organizationId?: string; administratorEmail?: string; + profileId?: string; } export interface SetupVerificationReport extends SetupReadinessReport { organizationId: string | null; } +export interface InstallationProfileDto { + id: string; + name: string; + description: string; + wizardSteps: string[]; + requiredTemplates: string[]; + verificationChecks: string[]; + resetScopes: string[]; + reseedTargets: string[]; + defaultOptions: Record; + enabledPluginIds: string[]; +} + +export interface SetupPluginSummaryDto { + id: string; + version: string; + displayName: string; + description: string; + dependencies: string[]; +} + +export interface SetupWizardStepDto { + key: string; + title: string; + description: string; + required: boolean; + produces: string[]; + invalidates: string[]; +} + +export interface SetupRegistryDefinitionDto { + key?: string; + id?: string; + area?: string; + name?: string; + description?: string; +} + export interface SetupTemplateMetadataItem { file: string; seedType: string; @@ -45,6 +84,9 @@ export interface SetupTemplateMetadataItem { supported: boolean; requiredColumns: string[]; description: string; + plugin?: string; + profiles?: string[]; + visible?: boolean; } export interface SetupTemplatesResponse { @@ -52,9 +94,26 @@ export interface SetupTemplatesResponse { time: string; version: string; generatedFrom: string; + profile?: InstallationProfileDto | null; + plugins?: SetupPluginSummaryDto[]; templates: SetupTemplateMetadataItem[]; } +export interface SetupProfilesResponse { + success: boolean; + time: string; + selectedProfile: InstallationProfileDto | null; + profiles: InstallationProfileDto[]; + plugins: SetupPluginSummaryDto[]; + templates: SetupTemplateMetadataItem[]; + wizardSteps: SetupWizardStepDto[]; + readinessChecks: SetupRegistryDefinitionDto[]; + verificationChecks: SetupRegistryDefinitionDto[]; + resetScopes: SetupRegistryDefinitionDto[]; + reseedTargets: SetupRegistryDefinitionDto[]; + goldenDatasets: SetupRegistryDefinitionDto[]; +} + export type SetupRunStatus = | 'not_started' | 'readiness_checked' diff --git a/src/features/setup/components/setup-wizard.tsx b/src/features/setup/components/setup-wizard.tsx index 7bfc96e..3b8be91 100644 --- a/src/features/setup/components/setup-wizard.tsx +++ b/src/features/setup/components/setup-wizard.tsx @@ -14,6 +14,7 @@ import { useCsvCommit, useCsvPreview } from '../api/use-csv-commit'; import { useCompleteSetupRun, useSaveSetupStep, useStartSetupRun } from '../api/use-setup-run'; import { useSetupVerification } from '../api/use-setup-verification'; import { + setupProfilesQueryOptions, setupReadinessQueryOptions, setupRunQueryOptions, setupTemplatesQueryOptions @@ -30,19 +31,13 @@ import type { SetupRunStepDto, SetupStepStatus, SetupTemplateMetadataItem, + SetupWizardStepDto, SetupVerificationReport } from '../api/types'; -type WizardStepKey = - | 'readiness' - | 'templates' - | 'upload' - | 'preview' - | 'commit' - | 'verification' - | 'finish'; +type WizardStepKey = string; -const WIZARD_STEPS: Array<{ key: WizardStepKey; label: string }> = [ +const WIZARD_STEPS: Array<{ key: WizardStepKey; label: string; description?: string }> = [ { key: 'readiness', label: 'Readiness' }, { key: 'templates', label: 'Templates' }, { key: 'upload', label: 'CSV Upload' }, @@ -338,12 +333,18 @@ function VerificationPanel({ ); } -function Stepper({ currentStep }: { currentStep: WizardStepKey }) { - const currentIndex = WIZARD_STEPS.findIndex((step) => step.key === currentStep); +function Stepper({ + currentStep, + steps +}: { + currentStep: WizardStepKey; + steps: Array<{ key: WizardStepKey; label: string; description?: string }>; +}) { + const currentIndex = steps.findIndex((step) => step.key === currentStep); return (
- {WIZARD_STEPS.map((step, index) => ( + {steps.map((step, index) => (
('readiness'); + const [selectedProfileId, setSelectedProfileId] = useState('crm_demo'); const [files, setFiles] = useState([]); const [preview, setPreview] = useState(null); const [previewFingerprint, setPreviewFingerprint] = useState(''); @@ -379,8 +381,9 @@ export function SetupWizard() { const [warningAcknowledged, setWarningAcknowledged] = useState(false); const [dryRun, setDryRun] = useState(false); - const readinessQuery = useQuery(setupReadinessQueryOptions()); - const templatesQuery = useQuery(setupTemplatesQueryOptions()); + const profilesQuery = useQuery(setupProfilesQueryOptions(selectedProfileId)); + const readinessQuery = useQuery(setupReadinessQueryOptions(selectedProfileId)); + const templatesQuery = useQuery(setupTemplatesQueryOptions(selectedProfileId)); const setupRunQuery = useQuery(setupRunQueryOptions()); const previewMutation = useCsvPreview(); const commitMutation = useCsvCommit(); @@ -393,6 +396,15 @@ export function SetupWizard() { const entries = setupRunQuery.data?.steps.map((step) => [step.stepKey, step] as const) ?? []; return new Map(entries); }, [setupRunQuery.data?.steps]); + const profileWizardSteps = useMemo(() => { + const registrySteps = profilesQuery.data?.wizardSteps.map((step: SetupWizardStepDto) => ({ + key: step.key, + label: step.title, + description: step.description + })); + + return registrySteps?.length ? registrySteps : WIZARD_STEPS; + }, [profilesQuery.data?.wizardSteps]); const fileFingerprint = useMemo(() => getFileFingerprint(files), [files]); const filesChangedAfterPreview = Boolean(preview) && fileFingerprint !== previewFingerprint; @@ -487,7 +499,7 @@ export function SetupWizard() { } async function runVerification() { - const report = await verificationMutation.mutateAsync({}); + const report = await verificationMutation.mutateAsync({ profileId: selectedProfileId }); setCurrentStep('verification'); await saveStep({ stepKey: 'verification', @@ -512,7 +524,7 @@ export function SetupWizard() { return (
- + @@ -523,6 +535,50 @@ export function SetupWizard() { + + + Installation profile + + Profiles are resolved through the setup plugin registry. Future modules can add profiles + without editing the wizard. + + + + {profilesQuery.isError ? ( + + ) : null} +
+ +
+ {profilesQuery.data?.selectedProfile?.description ?? + 'Loading installation profile definitions...'} +
+
+
+ {(profilesQuery.data?.plugins ?? []).map((plugin) => ( +
+
{plugin.displayName}
+
v{plugin.version}
+
{plugin.description}
+
+ ))} +
+
+
+ Setup run diff --git a/src/features/setup/plugins/crm/crm-installation-profile.ts b/src/features/setup/plugins/crm/crm-installation-profile.ts new file mode 100644 index 0000000..a112447 --- /dev/null +++ b/src/features/setup/plugins/crm/crm-installation-profile.ts @@ -0,0 +1,51 @@ +import { + CRM_TEMPLATE_FILES, + FOUNDATION_TEMPLATE_FILES, + FOUNDATION_WIZARD_STEPS, + type InstallationProfile +} from '../installation-profile'; + +export const crmInstallationProfiles: InstallationProfile[] = [ + { + id: 'crm_demo', + name: 'CRM Demo', + description: 'Foundation plus full CRM demo/golden dataset setup.', + wizardSteps: FOUNDATION_WIZARD_STEPS, + requiredTemplates: [...FOUNDATION_TEMPLATE_FILES, ...CRM_TEMPLATE_FILES], + verificationChecks: [ + 'ADMIN_CRM_ASSIGNMENT', + 'BRANCH_OPTION', + 'PRODUCT_TYPE_OPTION', + 'SEQUENCE_QUOTATION', + 'APPROVAL_WORKFLOW', + 'APPROVAL_MATRIX', + 'PDF_TEMPLATE_ASSET', + 'REPORT_DEFINITIONS' + ], + resetScopes: ['business_reset', 'demo_uat_reset', 'setup_run_reset'], + reseedTargets: ['demo_uat', 'csv_manifest'], + defaultOptions: { importMode: 'strict', dataset: 'golden' }, + enabledPluginIds: ['foundation', 'crm'] + }, + { + id: 'production', + name: 'Production', + description: 'Foundation plus CRM production setup with no destructive reset defaults.', + wizardSteps: FOUNDATION_WIZARD_STEPS, + requiredTemplates: [...FOUNDATION_TEMPLATE_FILES, ...CRM_TEMPLATE_FILES], + verificationChecks: [ + 'ADMIN_CRM_ASSIGNMENT', + 'BRANCH_OPTION', + 'PRODUCT_TYPE_OPTION', + 'SEQUENCE_QUOTATION', + 'APPROVAL_WORKFLOW', + 'APPROVAL_MATRIX', + 'PDF_TEMPLATE_ASSET', + 'REPORT_DEFINITIONS' + ], + resetScopes: ['setup_run_reset'], + reseedTargets: [], + defaultOptions: { importMode: 'strict', destructiveMaintenance: false }, + enabledPluginIds: ['foundation', 'crm'] + } +]; diff --git a/src/features/setup/plugins/crm/crm-setup-plugin.ts b/src/features/setup/plugins/crm/crm-setup-plugin.ts new file mode 100644 index 0000000..ab93af2 --- /dev/null +++ b/src/features/setup/plugins/crm/crm-setup-plugin.ts @@ -0,0 +1,144 @@ +import type { SetupPlugin, SetupTemplateDefinition } from '../plugin-types'; +import { CRM_TEMPLATE_FILES } from '../installation-profile'; +import { crmInstallationProfiles } from './crm-installation-profile'; + +const CRM_TEMPLATE_ORDER = new Map([ + ['master-options.csv', 6], + ['branches.csv', 7], + ['product-types.csv', 8], + ['document-sequences.csv', 14], + ['approval-workflows.csv', 15], + ['approval-steps.csv', 16], + ['approval-matrix.csv', 17], + ['customers.csv', 30], + ['contacts.csv', 32], + ['contact-shares.csv', 33], + ['leads.csv', 34], + ['opportunities.csv', 36], + ['opportunity-parties.csv', 37], + ['quotations.csv', 40], + ['quotation-items.csv', 41], + ['quotation-parties.csv', 42], + ['quotation-topics.csv', 43], + ['quotation-topic-items.csv', 44] +]); + +export class CrmSetupPlugin implements SetupPlugin { + id = 'crm'; + version = '1.0.0'; + displayName = 'CRM'; + description = 'CRM setup templates, verification checks, reset scopes, and golden dataset registration.'; + dependencies = ['foundation']; + + installationProfiles() { + return crmInstallationProfiles; + } + + templates(): SetupTemplateDefinition[] { + return CRM_TEMPLATE_FILES.map((file) => ({ + file, + seedType: CRM_TEMPLATE_ORDER.get(file)! < 30 ? 'foundation' : 'business', + importOrder: CRM_TEMPLATE_ORDER.get(file) ?? 999, + supported: true, + requiredColumns: [], + description: `CRM setup template: ${file}`, + plugin: this.id, + profiles: ['crm_starter', 'crm_demo', 'production', 'custom'] + })); + } + + wizardSteps() { + return [ + { + key: 'crm-templates', + title: 'CRM Templates', + description: 'CRM-owned template pack and import order.', + required: true, + produces: ['crm_template_registry'], + invalidates: ['csv_preview'] + }, + { + key: 'crm-verification', + title: 'CRM Verification', + description: 'CRM authorization, sequence, approval, PDF, report, and notification checks.', + required: true, + produces: ['crm_verification_report'], + invalidates: [] + } + ]; + } + + readinessChecks() { + return [ + { + id: 'PLUGIN_CRM_REGISTERED', + area: 'Plugin registry', + name: 'CRM plugin registered', + requiredFor: ['CRM setup'], + purpose: 'Ensure CRM setup capabilities are available.' + } + ]; + } + + verificationChecks() { + return [ + { + id: 'PLUGIN_CRM_VERIFICATION', + area: 'Plugin registry', + name: 'CRM verification registered', + requiredFor: ['CRM setup'], + purpose: 'Ensure CRM verification checks are exposed through the plugin registry.' + } + ]; + } + + previewValidators() { + return [ + { key: 'crm-reference-validator', description: 'CRM customer, lead, opportunity, quotation reference validation.' } + ]; + } + + importHandlers() { + return [{ key: 'crm-csv-importer', description: 'CRM setup CSV import handlers.' }]; + } + + resetScopes() { + return [ + { + key: 'business_reset', + destructive: true, + allowedEnvironments: ['local', 'test', 'staging'], + requiresTypedConfirmation: true, + affectedTables: ['crm_customers', 'crm_leads', 'crm_opportunities', 'crm_quotations'], + storageCleanup: 'best_effort' as const + }, + { + key: 'demo_uat_reset', + destructive: true, + allowedEnvironments: ['local', 'test', 'staging'], + requiresTypedConfirmation: true, + affectedTables: ['crm_* demo rows'], + storageCleanup: 'best_effort' as const + }, + { + key: 'csv_import_rollback', + destructive: true, + allowedEnvironments: ['local', 'test', 'staging'], + requiresTypedConfirmation: true, + affectedTables: ['manifest-owned rows'], + storageCleanup: 'report_only' as const + } + ]; + } + + reseedTargets() { + return [ + { key: 'demo_uat', description: 'CRM demo/UAT reseed target.', executable: false }, + { key: 'csv_manifest', description: 'CRM CSV manifest replay target.', executable: false } + ]; + } + + goldenDatasets() { + return [{ key: 'crm-golden', path: 'setup/golden-dataset', files: CRM_TEMPLATE_FILES }]; + } +} diff --git a/src/features/setup/plugins/foundation/foundation-plugin.ts b/src/features/setup/plugins/foundation/foundation-plugin.ts new file mode 100644 index 0000000..f4c5f75 --- /dev/null +++ b/src/features/setup/plugins/foundation/foundation-plugin.ts @@ -0,0 +1,175 @@ +import { + FOUNDATION_TEMPLATE_FILES, + FOUNDATION_WIZARD_STEPS, + type InstallationProfile +} from '../installation-profile'; +import type { SetupPlugin, SetupTemplateDefinition } from '../plugin-types'; + +const FOUNDATION_PROFILES: InstallationProfile[] = [ + { + id: 'crm_starter', + name: 'CRM Starter', + description: 'Foundation plus CRM structure without demo business volume.', + wizardSteps: FOUNDATION_WIZARD_STEPS, + requiredTemplates: FOUNDATION_TEMPLATE_FILES, + verificationChecks: ['ENV_AUTH_SECRET', 'ENV_DATABASE_URL', 'DB_CONNECTIVITY', 'ADMIN_USER'], + resetScopes: ['setup_run_reset'], + reseedTargets: ['foundation'], + defaultOptions: { importMode: 'strict' }, + enabledPluginIds: ['foundation', 'crm'] + }, + { + id: 'restore_backup', + name: 'Restore Backup', + description: 'Minimal setup checks for restored environments.', + wizardSteps: ['readiness', 'verification', 'finish'], + requiredTemplates: [], + verificationChecks: ['ENV_AUTH_SECRET', 'ENV_DATABASE_URL', 'DB_CONNECTIVITY'], + resetScopes: ['setup_run_reset'], + reseedTargets: [], + defaultOptions: { importMode: 'none' }, + enabledPluginIds: ['foundation'] + }, + { + id: 'custom', + name: 'Custom', + description: 'Operator-selected setup modules and templates.', + wizardSteps: FOUNDATION_WIZARD_STEPS, + requiredTemplates: [], + verificationChecks: ['ENV_AUTH_SECRET', 'ENV_DATABASE_URL', 'DB_CONNECTIVITY'], + resetScopes: ['setup_run_reset'], + reseedTargets: [], + defaultOptions: { importMode: 'custom' }, + enabledPluginIds: ['foundation'] + } +]; + +export class FoundationSetupPlugin implements SetupPlugin { + id = 'foundation'; + version = '1.0.0'; + displayName = 'Setup Foundation'; + description = 'Core setup readiness, setup state, seed manifests, reset framework, and CSV framework.'; + dependencies: string[] = []; + + installationProfiles() { + return FOUNDATION_PROFILES; + } + + templates(): SetupTemplateDefinition[] { + return []; + } + + wizardSteps() { + return [ + { + key: 'readiness', + title: 'System Readiness', + description: 'Run non-destructive environment and platform checks.', + required: true, + produces: ['readiness_report'], + invalidates: ['templates', 'csv_preview', 'csv_commit', 'verification'] + }, + { + key: 'templates', + title: 'Template Pack', + description: 'Review registered setup templates for the selected profile.', + required: true, + produces: ['template_registry'], + invalidates: ['csv_preview'] + }, + { + key: 'upload', + title: 'CSV Upload', + description: 'Select setup CSV files for preview and commit.', + required: true, + produces: ['selected_files'], + invalidates: ['csv_preview', 'csv_commit'] + }, + { + key: 'preview', + title: 'CSV Preview', + description: 'Validate CSVs and produce a preview hash.', + required: true, + produces: ['preview_hash'], + invalidates: ['csv_commit'] + }, + { + key: 'commit', + title: 'CSV Commit', + description: 'Commit validated CSV files and write seed manifests.', + required: true, + produces: ['import_report', 'seed_manifest'], + invalidates: ['verification'] + }, + { + key: 'verification', + title: 'Verification', + description: 'Confirm operational readiness after import.', + required: true, + produces: ['verification_report'], + invalidates: [] + }, + { + key: 'finish', + title: 'Finish', + description: 'Complete the setup run and show summary.', + required: true, + produces: ['setup_report'], + invalidates: [] + } + ]; + } + + readinessChecks() { + return [ + { + id: 'PLUGIN_FOUNDATION_REGISTERED', + area: 'Plugin registry', + name: 'Foundation plugin registered', + requiredFor: ['setup platform'], + purpose: 'Ensure setup core can resolve foundation capabilities.' + } + ]; + } + + verificationChecks() { + return [ + { + id: 'PLUGIN_FOUNDATION_VERIFICATION', + area: 'Plugin registry', + name: 'Foundation verification registered', + requiredFor: ['setup platform'], + purpose: 'Ensure setup verification can resolve foundation plugin checks.' + } + ]; + } + + previewValidators() { + return [{ key: 'template-header-validator', description: 'CSV template header validation.' }]; + } + + importHandlers() { + return [{ key: 'csv-commit-framework', description: 'Shared setup CSV commit framework.' }]; + } + + resetScopes() { + return [ + { + key: 'setup_run_reset', + destructive: false, + allowedEnvironments: ['local', 'test', 'staging'], + requiresTypedConfirmation: true, + affectedTables: ['setup_runs', 'setup_run_steps', 'seed_manifests', 'seed_manifest_items'], + storageCleanup: 'none' as const + } + ]; + } + + reseedTargets() { + return [{ key: 'foundation', description: 'Foundation seed service wrapper.', executable: false }]; + } + + goldenDatasets() { + return [{ key: 'foundation-golden', path: 'setup/golden-dataset', files: FOUNDATION_TEMPLATE_FILES }]; + } +} diff --git a/src/features/setup/plugins/installation-profile.ts b/src/features/setup/plugins/installation-profile.ts new file mode 100644 index 0000000..7580a72 --- /dev/null +++ b/src/features/setup/plugins/installation-profile.ts @@ -0,0 +1,37 @@ +export interface InstallationProfile { + id: 'crm_starter' | 'crm_demo' | 'production' | 'restore_backup' | 'custom'; + name: string; + description: string; + wizardSteps: string[]; + requiredTemplates: string[]; + verificationChecks: string[]; + resetScopes: string[]; + reseedTargets: string[]; + defaultOptions: Record; + enabledPluginIds: string[]; +} + +export const FOUNDATION_WIZARD_STEPS = ['readiness', 'templates', 'upload', 'preview', 'commit', 'verification', 'finish']; + +export const CRM_TEMPLATE_FILES = [ + 'master-options.csv', + 'branches.csv', + 'product-types.csv', + 'document-sequences.csv', + 'approval-workflows.csv', + 'approval-steps.csv', + 'approval-matrix.csv', + 'customers.csv', + 'contacts.csv', + 'contact-shares.csv', + 'leads.csv', + 'opportunities.csv', + 'opportunity-parties.csv', + 'quotations.csv', + 'quotation-items.csv', + 'quotation-parties.csv', + 'quotation-topics.csv', + 'quotation-topic-items.csv' +]; + +export const FOUNDATION_TEMPLATE_FILES = ['users.csv', 'organizations.csv', 'memberships.csv', 'crm-role-assignments.csv']; diff --git a/src/features/setup/plugins/plugin-registry.ts b/src/features/setup/plugins/plugin-registry.ts new file mode 100644 index 0000000..6988658 --- /dev/null +++ b/src/features/setup/plugins/plugin-registry.ts @@ -0,0 +1,138 @@ +import 'server-only'; + +import type { InstallationProfile } from './installation-profile'; +import { FoundationSetupPlugin } from './foundation/foundation-plugin'; +import { CrmSetupPlugin } from './crm/crm-setup-plugin'; +import type { + GoldenDatasetDefinition, + ResetScopeDefinition, + ReseedTargetDefinition, + SetupPlugin, + SetupReadinessCheck, + SetupTemplateDefinition, + SetupVerificationCheck, + SetupWizardStep +} from './plugin-types'; + +class SetupPluginRegistry { + private plugins = new Map(); + + registerPlugin(plugin: SetupPlugin) { + if (this.plugins.has(plugin.id)) { + throw new Error(`Setup plugin already registered: ${plugin.id}`); + } + if (plugin.dependencies.includes(plugin.id)) { + throw new Error(`Setup plugin cannot depend on itself: ${plugin.id}`); + } + this.plugins.set(plugin.id, plugin); + } + + getPlugins() { + return Array.from(this.plugins.values()); + } + + resolveInstallationProfiles(): InstallationProfile[] { + const profiles = this.getPlugins().flatMap((plugin) => plugin.installationProfiles()); + const byId = new Map(); + profiles.forEach((profile) => { + if (!byId.has(profile.id)) byId.set(profile.id, profile); + }); + return Array.from(byId.values()); + } + + resolveInstallationProfile(profileId = 'crm_demo') { + return this.resolveInstallationProfiles().find((profile) => profile.id === profileId) ?? null; + } + + resolveEnabledPlugins(profileId = 'crm_demo') { + const profile = this.resolveInstallationProfile(profileId); + if (!profile) return this.getPlugins(); + + const enabled = new Set(profile.enabledPluginIds); + enabled.add('foundation'); + return this.getPlugins().filter((plugin) => enabled.has(plugin.id)); + } + + resolveTemplates(profileId = 'crm_demo'): SetupTemplateDefinition[] { + const profile = this.resolveInstallationProfile(profileId); + const required = new Set(profile?.requiredTemplates ?? []); + const templates = this.resolveEnabledPlugins(profileId).flatMap((plugin) => plugin.templates()); + + return templates + .map((template) => ({ + ...template, + profiles: template.profiles.length ? template.profiles : [profileId], + supported: template.supported && (!profile || required.size === 0 || required.has(template.file)) + })) + .toSorted((a, b) => a.importOrder - b.importOrder); + } + + resolveWizardSteps(profileId = 'crm_demo'): SetupWizardStep[] { + const profile = this.resolveInstallationProfile(profileId); + const profileStepKeys = new Set(profile?.wizardSteps ?? []); + const steps = this.resolveEnabledPlugins(profileId).flatMap((plugin) => plugin.wizardSteps()); + const baseSteps = steps.filter((step) => profileStepKeys.size === 0 || profileStepKeys.has(step.key)); + const extras = steps.filter((step) => !profileStepKeys.has(step.key) && step.key.startsWith('crm-')); + return [...baseSteps, ...extras]; + } + + resolveReadinessChecks(profileId = 'crm_demo'): SetupReadinessCheck[] { + return this.resolveEnabledPlugins(profileId).flatMap((plugin) => plugin.readinessChecks()); + } + + resolveVerificationChecks(profileId = 'crm_demo'): SetupVerificationCheck[] { + return this.resolveEnabledPlugins(profileId).flatMap((plugin) => plugin.verificationChecks()); + } + + resolveResetScopes(profileId = 'crm_demo'): ResetScopeDefinition[] { + const profile = this.resolveInstallationProfile(profileId); + const allowed = new Set(profile?.resetScopes ?? []); + return this.resolveEnabledPlugins(profileId) + .flatMap((plugin) => plugin.resetScopes()) + .filter((scope) => allowed.size === 0 || allowed.has(scope.key)); + } + + resolveReseedTargets(profileId = 'crm_demo'): ReseedTargetDefinition[] { + const profile = this.resolveInstallationProfile(profileId); + const allowed = new Set(profile?.reseedTargets ?? []); + return this.resolveEnabledPlugins(profileId) + .flatMap((plugin) => plugin.reseedTargets()) + .filter((target) => allowed.size === 0 || allowed.has(target.key)); + } + + resolveGoldenDatasets(profileId = 'crm_demo'): GoldenDatasetDefinition[] { + return this.resolveEnabledPlugins(profileId).flatMap((plugin) => plugin.goldenDatasets()); + } +} + +const registry = new SetupPluginRegistry(); +registry.registerPlugin(new FoundationSetupPlugin()); +registry.registerPlugin(new CrmSetupPlugin()); + +export function getSetupPluginRegistry() { + return registry; +} + +export function getInstallationProfileBundle(profileId = 'crm_demo') { + const selectedProfile = registry.resolveInstallationProfile(profileId) ?? registry.resolveInstallationProfile('crm_demo'); + const selectedProfileId = selectedProfile?.id ?? 'crm_demo'; + + return { + selectedProfile, + profiles: registry.resolveInstallationProfiles(), + plugins: registry.getPlugins().map((plugin) => ({ + id: plugin.id, + version: plugin.version, + displayName: plugin.displayName, + description: plugin.description, + dependencies: plugin.dependencies + })), + templates: registry.resolveTemplates(selectedProfileId), + wizardSteps: registry.resolveWizardSteps(selectedProfileId), + readinessChecks: registry.resolveReadinessChecks(selectedProfileId), + verificationChecks: registry.resolveVerificationChecks(selectedProfileId), + resetScopes: registry.resolveResetScopes(selectedProfileId), + reseedTargets: registry.resolveReseedTargets(selectedProfileId), + goldenDatasets: registry.resolveGoldenDatasets(selectedProfileId) + }; +} diff --git a/src/features/setup/plugins/plugin-types.ts b/src/features/setup/plugins/plugin-types.ts new file mode 100644 index 0000000..9be01a1 --- /dev/null +++ b/src/features/setup/plugins/plugin-types.ts @@ -0,0 +1,81 @@ +import type { InstallationProfile } from './installation-profile'; + +export interface SetupTemplateDefinition { + file: string; + seedType: string; + importOrder: number; + supported: boolean; + requiredColumns: string[]; + description: string; + plugin: string; + profiles: string[]; +} + +export interface SetupWizardStep { + key: string; + title: string; + description: string; + required: boolean; + produces: string[]; + invalidates: string[]; +} + +export interface SetupReadinessCheck { + id: string; + area: string; + name: string; + requiredFor: string[]; + purpose: string; + suggestedFix?: string; +} + +export interface SetupVerificationCheck extends SetupReadinessCheck {} + +export interface ResetScopeDefinition { + key: string; + destructive: boolean; + allowedEnvironments: string[]; + requiresTypedConfirmation: boolean; + affectedTables: string[]; + storageCleanup: 'none' | 'report_only' | 'best_effort'; +} + +export interface ReseedTargetDefinition { + key: string; + description: string; + executable: boolean; +} + +export interface CsvPreviewValidator { + key: string; + description: string; +} + +export interface CsvImporter { + key: string; + description: string; +} + +export interface GoldenDatasetDefinition { + key: string; + path: string; + files: string[]; +} + +export interface SetupPlugin { + id: string; + version: string; + displayName: string; + description: string; + dependencies: string[]; + installationProfiles(): InstallationProfile[]; + templates(): SetupTemplateDefinition[]; + wizardSteps(): SetupWizardStep[]; + readinessChecks(): SetupReadinessCheck[]; + verificationChecks(): SetupVerificationCheck[]; + previewValidators(): CsvPreviewValidator[]; + importHandlers(): CsvImporter[]; + resetScopes(): ResetScopeDefinition[]; + reseedTargets(): ReseedTargetDefinition[]; + goldenDatasets(): GoldenDatasetDefinition[]; +} diff --git a/src/features/setup/server/readiness.service.ts b/src/features/setup/server/readiness.service.ts index f14c584..4895030 100644 --- a/src/features/setup/server/readiness.service.ts +++ b/src/features/setup/server/readiness.service.ts @@ -17,6 +17,7 @@ import { } from '@/db/schema'; import { EmailNotificationProvider } from '@/features/foundation/notifications/server/email-provider'; import { getStorageProvider } from '@/features/foundation/storage/service'; +import { getSetupPluginRegistry } from '@/features/setup/plugins/plugin-registry'; import { db } from '@/lib/db'; import type { SetupCheckResult, @@ -391,7 +392,24 @@ export async function checkEmailConfig(): Promise { export async function getSetupReadinessReport(input: { organizationId?: string | null; + profileId?: string | null; } = {}): Promise { + const pluginChecks = getSetupPluginRegistry() + .resolveReadinessChecks(input.profileId ?? 'crm_demo') + .map((check) => + buildSetupCheck( + { + id: check.id, + area: check.area, + name: check.name, + requiredFor: check.requiredFor, + purpose: check.purpose, + suggestedFix: check.suggestedFix + }, + 'PASS', + `${check.name} is registered by the setup plugin registry.` + ) + ); const checks = await Promise.all([ Promise.resolve(checkAuthSecret()), Promise.resolve(checkDatabaseUrl()), @@ -404,5 +422,5 @@ export async function getSetupReadinessReport(input: { checkEmailConfig() ]); - return buildSetupReport(checks, 'Setup readiness checks completed.'); + return buildSetupReport([...checks, ...pluginChecks], 'Setup readiness checks completed.'); } diff --git a/src/features/setup/server/verification.service.ts b/src/features/setup/server/verification.service.ts index ad7955a..7557c95 100644 --- a/src/features/setup/server/verification.service.ts +++ b/src/features/setup/server/verification.service.ts @@ -14,6 +14,7 @@ import { organizations, users } from '@/db/schema'; +import { getSetupPluginRegistry } from '@/features/setup/plugins/plugin-registry'; import { db } from '@/lib/db'; import type { SetupCheckResult, SetupVerificationReport } from '../api/types'; import { @@ -27,8 +28,28 @@ import { type VerificationInput = { organizationId?: string | null; administratorEmail?: string | null; + profileId?: string | null; }; +function buildPluginVerificationChecks(profileId?: string | null): SetupCheckResult[] { + return getSetupPluginRegistry() + .resolveVerificationChecks(profileId ?? 'crm_demo') + .map((check) => + buildSetupCheck( + { + id: check.id, + area: check.area, + name: check.name, + requiredFor: check.requiredFor, + purpose: check.purpose, + suggestedFix: check.suggestedFix + }, + 'PASS', + `${check.name} is registered by the setup plugin registry.` + ) + ); +} + async function resolveOrganizationId(input: VerificationInput): Promise { if (input.organizationId) { const [organization] = await db @@ -214,7 +235,10 @@ export async function getSetupVerificationReport( ); if (!organizationId) { - const report = buildSetupReport(checks, 'Setup verification checks completed.'); + const report = buildSetupReport( + [...checks, ...buildPluginVerificationChecks(input.profileId)], + 'Setup verification checks completed.' + ); return { ...report, organizationId }; } @@ -430,6 +454,9 @@ export async function getSetupVerificationReport( ) ); - const report = buildSetupReport(checks, 'Setup verification checks completed.'); + const report = buildSetupReport( + [...checks, ...buildPluginVerificationChecks(input.profileId)], + 'Setup verification checks completed.' + ); return { ...report, organizationId }; } diff --git a/src/tests/setup/setup-plugin-registry.test.ts b/src/tests/setup/setup-plugin-registry.test.ts new file mode 100644 index 0000000..3819c87 --- /dev/null +++ b/src/tests/setup/setup-plugin-registry.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { assertFileExists, readText } from './helpers.ts'; + +test('setup plugin registry files exist', () => { + for (const file of [ + 'src/features/setup/plugins/plugin-types.ts', + 'src/features/setup/plugins/plugin-registry.ts', + 'src/features/setup/plugins/installation-profile.ts', + 'src/features/setup/plugins/foundation/foundation-plugin.ts', + 'src/features/setup/plugins/crm/crm-setup-plugin.ts', + 'src/features/setup/plugins/crm/crm-installation-profile.ts' + ]) { + assertFileExists(file); + } +}); + +test('installation profile API and wizard profile loading exist', () => { + assertFileExists('src/app/api/setup/profiles/route.ts'); + + const wizard = readText('src/features/setup/components/setup-wizard.tsx'); + assert.match(wizard, /setupProfilesQueryOptions/); + assert.match(wizard, /Installation profile/); +}); + +test('template API exposes plugin-aware metadata without removing legacy fields', () => { + const route = readText('src/app/api/setup/templates/route.ts'); + + assert.match(route, /plugin/); + assert.match(route, /profiles/); + assert.match(route, /requiredColumns/); + assert.match(route, /importOrder/); +});