task-d.7.9
This commit is contained in:
@@ -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=<profileId>` now returns plugin-aware metadata while preserving legacy template fields.
|
||||||
|
- `GET /api/setup/readiness?profile=<profileId>` 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.
|
||||||
20
docs/setup/installation-profiles.md
Normal file
20
docs/setup/installation-profiles.md
Normal file
@@ -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=<profileId>`
|
||||||
|
- `GET /api/setup/readiness?profile=<profileId>`
|
||||||
|
- `POST /api/setup/verify` with optional `profileId`
|
||||||
|
|
||||||
|
The setup wizard uses the selected profile to render profile-aware plugin metadata and wizard steps.
|
||||||
37
docs/setup/setup-plugin-development.md
Normal file
37
docs/setup/setup-plugin-development.md
Normal file
@@ -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.
|
||||||
749
plans/task-d.7.9.md
Normal file
749
plans/task-d.7.9.md
Normal file
@@ -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<string,unknown>;
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
21
src/app/api/setup/profiles/route.ts
Normal file
21
src/app/api/setup/profiles/route.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getSetupReadinessReport } from '@/features/setup/server/readiness.service';
|
import { getSetupReadinessReport } from '@/features/setup/server/readiness.service';
|
||||||
import { AuthError, requireSystemRole } from '@/lib/auth/session';
|
import { AuthError, requireSystemRole } from '@/lib/auth/session';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await requireSystemRole('super_admin');
|
const session = await requireSystemRole('super_admin');
|
||||||
|
const profileId = request.nextUrl.searchParams.get('profile') ?? 'crm_demo';
|
||||||
const report = await getSetupReadinessReport({
|
const report = await getSetupReadinessReport({
|
||||||
organizationId: session.user.activeOrganizationId ?? null
|
organizationId: session.user.activeOrganizationId ?? null,
|
||||||
|
profileId
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(report);
|
return NextResponse.json(report);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AuthError) {
|
if (error instanceof AuthError) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
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';
|
import { AuthError, requireSystemRole } from '@/lib/auth/session';
|
||||||
|
|
||||||
type SetupTemplateMetadata = {
|
interface SetupTemplateMetadata {
|
||||||
version: string;
|
version: string;
|
||||||
generatedFrom: string;
|
generatedFrom: string;
|
||||||
templates: Array<{
|
templates: Array<{
|
||||||
@@ -14,20 +15,38 @@ type SetupTemplateMetadata = {
|
|||||||
requiredColumns: string[];
|
requiredColumns: string[];
|
||||||
description: string;
|
description: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
await requireSystemRole('super_admin');
|
await requireSystemRole('super_admin');
|
||||||
|
const profileId = request.nextUrl.searchParams.get('profile') ?? 'crm_demo';
|
||||||
const metadataPath = path.join(process.cwd(), 'setup', 'templates', 'templates.json');
|
const metadataPath = path.join(process.cwd(), 'setup', 'templates', 'templates.json');
|
||||||
const metadata = JSON.parse(await readFile(metadataPath, 'utf8')) as SetupTemplateMetadata;
|
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({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
time: new Date().toISOString(),
|
time: new Date().toISOString(),
|
||||||
version: metadata.version,
|
version: metadata.version,
|
||||||
generatedFrom: metadata.generatedFrom,
|
generatedFrom: metadata.generatedFrom,
|
||||||
templates: metadata.templates
|
profile: profileBundle.selectedProfile,
|
||||||
|
plugins: profileBundle.plugins,
|
||||||
|
templates
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AuthError) {
|
if (error instanceof AuthError) {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { AuthError, requireSystemRole } from '@/lib/auth/session';
|
|||||||
const verificationSchema = z
|
const verificationSchema = z
|
||||||
.object({
|
.object({
|
||||||
organizationId: z.string().min(1).optional(),
|
organizationId: z.string().min(1).optional(),
|
||||||
administratorEmail: z.string().email().optional()
|
administratorEmail: z.string().email().optional(),
|
||||||
|
profileId: z.string().min(1).optional()
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = verificationSchema.parse(await request.json().catch(() => undefined)) ?? {};
|
const body = verificationSchema.parse(await request.json().catch(() => undefined)) ?? {};
|
||||||
const report = await getSetupVerificationReport({
|
const report = await getSetupVerificationReport({
|
||||||
organizationId: body.organizationId ?? session.user.activeOrganizationId ?? null,
|
organizationId: body.organizationId ?? session.user.activeOrganizationId ?? null,
|
||||||
administratorEmail: body.administratorEmail
|
administratorEmail: body.administratorEmail,
|
||||||
|
profileId: body.profileId
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(report);
|
return NextResponse.json(report);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
getCurrentSetupRun,
|
getCurrentSetupRun,
|
||||||
|
getSetupProfiles,
|
||||||
getSetupReadiness,
|
getSetupReadiness,
|
||||||
getSetupRunReport,
|
getSetupRunReport,
|
||||||
getSetupTemplates,
|
getSetupTemplates,
|
||||||
@@ -10,24 +11,31 @@ import type { SetupVerificationPayload } from './types';
|
|||||||
|
|
||||||
export const setupKeys = {
|
export const setupKeys = {
|
||||||
all: ['setup'] as const,
|
all: ['setup'] as const,
|
||||||
readiness: () => [...setupKeys.all, 'readiness'] as const,
|
readiness: (profileId?: string) => [...setupKeys.all, 'readiness', profileId ?? 'crm_demo'] as const,
|
||||||
templates: () => [...setupKeys.all, 'templates'] 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,
|
run: () => [...setupKeys.all, 'run'] as const,
|
||||||
report: () => [...setupKeys.all, 'run', 'report'] as const,
|
report: () => [...setupKeys.all, 'run', 'report'] as const,
|
||||||
verification: (payload: SetupVerificationPayload = {}) =>
|
verification: (payload: SetupVerificationPayload = {}) =>
|
||||||
[...setupKeys.all, 'verification', payload] as const
|
[...setupKeys.all, 'verification', payload] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupReadinessQueryOptions = () =>
|
export const setupReadinessQueryOptions = (profileId?: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: setupKeys.readiness(),
|
queryKey: setupKeys.readiness(profileId),
|
||||||
queryFn: () => getSetupReadiness()
|
queryFn: () => getSetupReadiness(profileId)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupTemplatesQueryOptions = () =>
|
export const setupTemplatesQueryOptions = (profileId?: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: setupKeys.templates(),
|
queryKey: setupKeys.templates(profileId),
|
||||||
queryFn: () => getSetupTemplates()
|
queryFn: () => getSetupTemplates(profileId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupProfilesQueryOptions = (profileId?: string) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: setupKeys.profiles(profileId),
|
||||||
|
queryFn: () => getSetupProfiles(profileId)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupRunQueryOptions = () =>
|
export const setupRunQueryOptions = () =>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
CsvPreviewResult,
|
CsvPreviewResult,
|
||||||
ImportReport,
|
ImportReport,
|
||||||
SaveSetupStepPayload,
|
SaveSetupStepPayload,
|
||||||
|
SetupProfilesResponse,
|
||||||
SetupReadinessReport,
|
SetupReadinessReport,
|
||||||
SetupRunResponse,
|
SetupRunResponse,
|
||||||
SetupTemplatesResponse,
|
SetupTemplatesResponse,
|
||||||
@@ -15,12 +16,22 @@ interface ApiErrorPayload {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSetupReadiness(): Promise<SetupReadinessReport> {
|
function withProfile(endpoint: string, profileId?: string) {
|
||||||
return apiClient<SetupReadinessReport>('/setup/readiness');
|
if (!profileId) return endpoint;
|
||||||
|
const separator = endpoint.includes('?') ? '&' : '?';
|
||||||
|
return `${endpoint}${separator}profile=${encodeURIComponent(profileId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSetupTemplates(): Promise<SetupTemplatesResponse> {
|
export async function getSetupReadiness(profileId?: string): Promise<SetupReadinessReport> {
|
||||||
return apiClient<SetupTemplatesResponse>('/setup/templates');
|
return apiClient<SetupReadinessReport>(withProfile('/setup/readiness', profileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSetupTemplates(profileId?: string): Promise<SetupTemplatesResponse> {
|
||||||
|
return apiClient<SetupTemplatesResponse>(withProfile('/setup/templates', profileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSetupProfiles(profileId?: string): Promise<SetupProfilesResponse> {
|
||||||
|
return apiClient<SetupProfilesResponse>(withProfile('/setup/profiles', profileId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentSetupRun(): Promise<SetupRunResponse> {
|
export async function getCurrentSetupRun(): Promise<SetupRunResponse> {
|
||||||
|
|||||||
@@ -32,12 +32,51 @@ export interface SetupReadinessReport {
|
|||||||
export interface SetupVerificationPayload {
|
export interface SetupVerificationPayload {
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
administratorEmail?: string;
|
administratorEmail?: string;
|
||||||
|
profileId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetupVerificationReport extends SetupReadinessReport {
|
export interface SetupVerificationReport extends SetupReadinessReport {
|
||||||
organizationId: string | null;
|
organizationId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InstallationProfileDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
wizardSteps: string[];
|
||||||
|
requiredTemplates: string[];
|
||||||
|
verificationChecks: string[];
|
||||||
|
resetScopes: string[];
|
||||||
|
reseedTargets: string[];
|
||||||
|
defaultOptions: Record<string, unknown>;
|
||||||
|
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 {
|
export interface SetupTemplateMetadataItem {
|
||||||
file: string;
|
file: string;
|
||||||
seedType: string;
|
seedType: string;
|
||||||
@@ -45,6 +84,9 @@ export interface SetupTemplateMetadataItem {
|
|||||||
supported: boolean;
|
supported: boolean;
|
||||||
requiredColumns: string[];
|
requiredColumns: string[];
|
||||||
description: string;
|
description: string;
|
||||||
|
plugin?: string;
|
||||||
|
profiles?: string[];
|
||||||
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetupTemplatesResponse {
|
export interface SetupTemplatesResponse {
|
||||||
@@ -52,9 +94,26 @@ export interface SetupTemplatesResponse {
|
|||||||
time: string;
|
time: string;
|
||||||
version: string;
|
version: string;
|
||||||
generatedFrom: string;
|
generatedFrom: string;
|
||||||
|
profile?: InstallationProfileDto | null;
|
||||||
|
plugins?: SetupPluginSummaryDto[];
|
||||||
templates: SetupTemplateMetadataItem[];
|
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 =
|
export type SetupRunStatus =
|
||||||
| 'not_started'
|
| 'not_started'
|
||||||
| 'readiness_checked'
|
| 'readiness_checked'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useCsvCommit, useCsvPreview } from '../api/use-csv-commit';
|
|||||||
import { useCompleteSetupRun, useSaveSetupStep, useStartSetupRun } from '../api/use-setup-run';
|
import { useCompleteSetupRun, useSaveSetupStep, useStartSetupRun } from '../api/use-setup-run';
|
||||||
import { useSetupVerification } from '../api/use-setup-verification';
|
import { useSetupVerification } from '../api/use-setup-verification';
|
||||||
import {
|
import {
|
||||||
|
setupProfilesQueryOptions,
|
||||||
setupReadinessQueryOptions,
|
setupReadinessQueryOptions,
|
||||||
setupRunQueryOptions,
|
setupRunQueryOptions,
|
||||||
setupTemplatesQueryOptions
|
setupTemplatesQueryOptions
|
||||||
@@ -30,19 +31,13 @@ import type {
|
|||||||
SetupRunStepDto,
|
SetupRunStepDto,
|
||||||
SetupStepStatus,
|
SetupStepStatus,
|
||||||
SetupTemplateMetadataItem,
|
SetupTemplateMetadataItem,
|
||||||
|
SetupWizardStepDto,
|
||||||
SetupVerificationReport
|
SetupVerificationReport
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
|
||||||
type WizardStepKey =
|
type WizardStepKey = string;
|
||||||
| 'readiness'
|
|
||||||
| 'templates'
|
|
||||||
| 'upload'
|
|
||||||
| 'preview'
|
|
||||||
| 'commit'
|
|
||||||
| 'verification'
|
|
||||||
| 'finish';
|
|
||||||
|
|
||||||
const WIZARD_STEPS: Array<{ key: WizardStepKey; label: string }> = [
|
const WIZARD_STEPS: Array<{ key: WizardStepKey; label: string; description?: string }> = [
|
||||||
{ key: 'readiness', label: 'Readiness' },
|
{ key: 'readiness', label: 'Readiness' },
|
||||||
{ key: 'templates', label: 'Templates' },
|
{ key: 'templates', label: 'Templates' },
|
||||||
{ key: 'upload', label: 'CSV Upload' },
|
{ key: 'upload', label: 'CSV Upload' },
|
||||||
@@ -338,12 +333,18 @@ function VerificationPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stepper({ currentStep }: { currentStep: WizardStepKey }) {
|
function Stepper({
|
||||||
const currentIndex = WIZARD_STEPS.findIndex((step) => step.key === currentStep);
|
currentStep,
|
||||||
|
steps
|
||||||
|
}: {
|
||||||
|
currentStep: WizardStepKey;
|
||||||
|
steps: Array<{ key: WizardStepKey; label: string; description?: string }>;
|
||||||
|
}) {
|
||||||
|
const currentIndex = steps.findIndex((step) => step.key === currentStep);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid gap-2 md:grid-cols-7'>
|
<div className='grid gap-2 md:grid-cols-7'>
|
||||||
{WIZARD_STEPS.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<div
|
<div
|
||||||
key={step.key}
|
key={step.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -372,6 +373,7 @@ function PersistedStepBadge({ step }: { step?: SetupRunStepDto }) {
|
|||||||
|
|
||||||
export function SetupWizard() {
|
export function SetupWizard() {
|
||||||
const [currentStep, setCurrentStep] = useState<WizardStepKey>('readiness');
|
const [currentStep, setCurrentStep] = useState<WizardStepKey>('readiness');
|
||||||
|
const [selectedProfileId, setSelectedProfileId] = useState('crm_demo');
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [preview, setPreview] = useState<CsvPreviewResult | null>(null);
|
const [preview, setPreview] = useState<CsvPreviewResult | null>(null);
|
||||||
const [previewFingerprint, setPreviewFingerprint] = useState('');
|
const [previewFingerprint, setPreviewFingerprint] = useState('');
|
||||||
@@ -379,8 +381,9 @@ export function SetupWizard() {
|
|||||||
const [warningAcknowledged, setWarningAcknowledged] = useState(false);
|
const [warningAcknowledged, setWarningAcknowledged] = useState(false);
|
||||||
const [dryRun, setDryRun] = useState(false);
|
const [dryRun, setDryRun] = useState(false);
|
||||||
|
|
||||||
const readinessQuery = useQuery(setupReadinessQueryOptions());
|
const profilesQuery = useQuery(setupProfilesQueryOptions(selectedProfileId));
|
||||||
const templatesQuery = useQuery(setupTemplatesQueryOptions());
|
const readinessQuery = useQuery(setupReadinessQueryOptions(selectedProfileId));
|
||||||
|
const templatesQuery = useQuery(setupTemplatesQueryOptions(selectedProfileId));
|
||||||
const setupRunQuery = useQuery(setupRunQueryOptions());
|
const setupRunQuery = useQuery(setupRunQueryOptions());
|
||||||
const previewMutation = useCsvPreview();
|
const previewMutation = useCsvPreview();
|
||||||
const commitMutation = useCsvCommit();
|
const commitMutation = useCsvCommit();
|
||||||
@@ -393,6 +396,15 @@ export function SetupWizard() {
|
|||||||
const entries = setupRunQuery.data?.steps.map((step) => [step.stepKey, step] as const) ?? [];
|
const entries = setupRunQuery.data?.steps.map((step) => [step.stepKey, step] as const) ?? [];
|
||||||
return new Map(entries);
|
return new Map(entries);
|
||||||
}, [setupRunQuery.data?.steps]);
|
}, [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 fileFingerprint = useMemo(() => getFileFingerprint(files), [files]);
|
||||||
const filesChangedAfterPreview = Boolean(preview) && fileFingerprint !== previewFingerprint;
|
const filesChangedAfterPreview = Boolean(preview) && fileFingerprint !== previewFingerprint;
|
||||||
@@ -487,7 +499,7 @@ export function SetupWizard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runVerification() {
|
async function runVerification() {
|
||||||
const report = await verificationMutation.mutateAsync({});
|
const report = await verificationMutation.mutateAsync({ profileId: selectedProfileId });
|
||||||
setCurrentStep('verification');
|
setCurrentStep('verification');
|
||||||
await saveStep({
|
await saveStep({
|
||||||
stepKey: 'verification',
|
stepKey: 'verification',
|
||||||
@@ -512,7 +524,7 @@ export function SetupWizard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
<Stepper currentStep={currentStep} />
|
<Stepper currentStep={currentStep} steps={profileWizardSteps} />
|
||||||
|
|
||||||
<Alert>
|
<Alert>
|
||||||
<Icons.info className='h-4 w-4' />
|
<Icons.info className='h-4 w-4' />
|
||||||
@@ -523,6 +535,50 @@ export function SetupWizard() {
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Installation profile</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Profiles are resolved through the setup plugin registry. Future modules can add profiles
|
||||||
|
without editing the wizard.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='space-y-4'>
|
||||||
|
{profilesQuery.isError ? (
|
||||||
|
<ErrorAlert title='Profile registry unavailable' message={getErrorMessage(profilesQuery.error)} />
|
||||||
|
) : null}
|
||||||
|
<div className='grid gap-3 md:grid-cols-[260px_1fr]'>
|
||||||
|
<select
|
||||||
|
value={selectedProfileId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSelectedProfileId(event.target.value);
|
||||||
|
setCurrentStep('readiness');
|
||||||
|
}}
|
||||||
|
className='h-10 rounded-md border bg-background px-3 text-sm'
|
||||||
|
>
|
||||||
|
{(profilesQuery.data?.profiles ?? []).map((profile) => (
|
||||||
|
<option key={profile.id} value={profile.id}>
|
||||||
|
{profile.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className='text-sm text-muted-foreground'>
|
||||||
|
{profilesQuery.data?.selectedProfile?.description ??
|
||||||
|
'Loading installation profile definitions...'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-2 md:grid-cols-3'>
|
||||||
|
{(profilesQuery.data?.plugins ?? []).map((plugin) => (
|
||||||
|
<div key={plugin.id} className='rounded-md border p-3'>
|
||||||
|
<div className='font-medium'>{plugin.displayName}</div>
|
||||||
|
<div className='text-xs text-muted-foreground'>v{plugin.version}</div>
|
||||||
|
<div className='mt-1 text-sm text-muted-foreground'>{plugin.description}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Setup run</CardTitle>
|
<CardTitle>Setup run</CardTitle>
|
||||||
|
|||||||
51
src/features/setup/plugins/crm/crm-installation-profile.ts
Normal file
51
src/features/setup/plugins/crm/crm-installation-profile.ts
Normal file
@@ -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']
|
||||||
|
}
|
||||||
|
];
|
||||||
144
src/features/setup/plugins/crm/crm-setup-plugin.ts
Normal file
144
src/features/setup/plugins/crm/crm-setup-plugin.ts
Normal file
@@ -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 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
175
src/features/setup/plugins/foundation/foundation-plugin.ts
Normal file
175
src/features/setup/plugins/foundation/foundation-plugin.ts
Normal file
@@ -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 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/features/setup/plugins/installation-profile.ts
Normal file
37
src/features/setup/plugins/installation-profile.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
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'];
|
||||||
138
src/features/setup/plugins/plugin-registry.ts
Normal file
138
src/features/setup/plugins/plugin-registry.ts
Normal file
@@ -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<string, SetupPlugin>();
|
||||||
|
|
||||||
|
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<string, InstallationProfile>();
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/features/setup/plugins/plugin-types.ts
Normal file
81
src/features/setup/plugins/plugin-types.ts
Normal file
@@ -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[];
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@/db/schema';
|
} from '@/db/schema';
|
||||||
import { EmailNotificationProvider } from '@/features/foundation/notifications/server/email-provider';
|
import { EmailNotificationProvider } from '@/features/foundation/notifications/server/email-provider';
|
||||||
import { getStorageProvider } from '@/features/foundation/storage/service';
|
import { getStorageProvider } from '@/features/foundation/storage/service';
|
||||||
|
import { getSetupPluginRegistry } from '@/features/setup/plugins/plugin-registry';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import type {
|
import type {
|
||||||
SetupCheckResult,
|
SetupCheckResult,
|
||||||
@@ -391,7 +392,24 @@ export async function checkEmailConfig(): Promise<SetupCheckResult> {
|
|||||||
|
|
||||||
export async function getSetupReadinessReport(input: {
|
export async function getSetupReadinessReport(input: {
|
||||||
organizationId?: string | null;
|
organizationId?: string | null;
|
||||||
|
profileId?: string | null;
|
||||||
} = {}): Promise<SetupReadinessReport> {
|
} = {}): Promise<SetupReadinessReport> {
|
||||||
|
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([
|
const checks = await Promise.all([
|
||||||
Promise.resolve(checkAuthSecret()),
|
Promise.resolve(checkAuthSecret()),
|
||||||
Promise.resolve(checkDatabaseUrl()),
|
Promise.resolve(checkDatabaseUrl()),
|
||||||
@@ -404,5 +422,5 @@ export async function getSetupReadinessReport(input: {
|
|||||||
checkEmailConfig()
|
checkEmailConfig()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return buildSetupReport(checks, 'Setup readiness checks completed.');
|
return buildSetupReport([...checks, ...pluginChecks], 'Setup readiness checks completed.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
organizations,
|
organizations,
|
||||||
users
|
users
|
||||||
} from '@/db/schema';
|
} from '@/db/schema';
|
||||||
|
import { getSetupPluginRegistry } from '@/features/setup/plugins/plugin-registry';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import type { SetupCheckResult, SetupVerificationReport } from '../api/types';
|
import type { SetupCheckResult, SetupVerificationReport } from '../api/types';
|
||||||
import {
|
import {
|
||||||
@@ -27,8 +28,28 @@ import {
|
|||||||
type VerificationInput = {
|
type VerificationInput = {
|
||||||
organizationId?: string | null;
|
organizationId?: string | null;
|
||||||
administratorEmail?: 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<string | null> {
|
async function resolveOrganizationId(input: VerificationInput): Promise<string | null> {
|
||||||
if (input.organizationId) {
|
if (input.organizationId) {
|
||||||
const [organization] = await db
|
const [organization] = await db
|
||||||
@@ -214,7 +235,10 @@ export async function getSetupVerificationReport(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
const report = buildSetupReport(checks, 'Setup verification checks completed.');
|
const report = buildSetupReport(
|
||||||
|
[...checks, ...buildPluginVerificationChecks(input.profileId)],
|
||||||
|
'Setup verification checks completed.'
|
||||||
|
);
|
||||||
return { ...report, organizationId };
|
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 };
|
return { ...report, organizationId };
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/tests/setup/setup-plugin-registry.test.ts
Normal file
33
src/tests/setup/setup-plugin-registry.test.ts
Normal file
@@ -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/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user