Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
Engineering reference: For service contracts, EventBus events, and data-layer details see src/features/organizations/organizations.md.
Organizations Feature
Overview
The organizations feature manages tenant data and organizational classification in GrantMaster. It bridges legacy organization profiles with a modern structured classification system, enabling understanding of organizational identity across six dimensions: role, sector, geography, scale, legal/fiscal form, and operational model.
Core Capabilities:
- Organization profile creation, reading, and updates (name, legal entity, contact, subscription)
- Tenant classification system with six structured dimensions
- Lazy migration of legacy loose fields into the classification schema
- Completeness scoring and audit trails with up to 50-item change history
- Multi-step wizards for onboarding and ongoing updates
Data Model
Firestore Collections
| Collection | Document Type | Description |
|---|
organizations | Organization | Tenant metadata (name, legal entity, contact, logoUrl, subscription). Nested classification field contains TenantClassification |
Key TypeScript Types
Organization (legacy profile):
name, slug, logoUrl, website — Branding
legalEntity — Legal structure (visitingAddress, postalAddress, statutoryName, kvkNumber, rsinNumber, vatNumber, legalRepresentative)
primaryContact — Primary person (name, email, phone, avatarUrl)
subscription — Subscription tier and billing data
annualBudgetRange — Budget classification (deprecated; migrates to classification.budgetTier)
- Deprecated loose fields:
targetSectors[], operationalScope, foundationYear, sdgs[], missionStatement, targetBeneficiaries
TenantClassification (nested on organizations document):
- Dimension 1 (Role):
organizationalRole: ORG_ROLE | null, missionStatement: string (≤500 chars), targetBeneficiaries: string (≤300 chars)
- Dimension 2 (Sector):
primarySector: SectorCode | null, secondarySectors: SectorCode[] (max 3)
- Dimension 3 (Geography):
geographicScope: GeographicScope | null, operatingCountries: string[] (ISO 3166-1 alpha-3), primaryCountry: string | null
- Dimension 4 (Scale):
budgetTier: BudgetTier | null
- Dimension 5 (Legal/Fiscal):
legalFiscal: { legalForm: LegalFormCode | null, fiscalStatus: FiscalStatusCode | null }
- Dimension 6 (Operational):
operationalModels: OperationalModelCode[] (max 3)
- Goals & Foundation:
foundationYear: number | null, sdgs: number[] (1-17, max 5)
- Audit Trail:
completenessScore: number (0-100), classificationHistory: ChangeRecord[] (max 50), lastUpdatedAt: ISO string, lastUpdatedBy: string
Completeness is 100% only when all six dimensions are filled (non-null and non-empty for arrays). Score = (filledDimensions / 6) * 100.
Key Behaviors
Lazy Migration
When getClassification() is called on an organization with no classification field but legacy loose fields present:
- Automatically creates classification from loose fields using dimension mappings
- Persists migration (fire-and-forget) to avoid repeated work
- Emits
TENANT_CLASSIFICATION_UPDATED event with isMigration: true
Legacy Mappings:
targetSectors[] → primarySector (first) + secondarySectors (rest, up to 3)
operationalScope → geographicScope
annualBudgetRange → budgetTier
sdgs[] → sdgs[] (string-to-number conversion)
missionStatement → missionStatement (truncated to 500 chars)
targetBeneficiaries → targetBeneficiaries
Partial Updates
Classification supports partial updates:
- Only set fields you change; unset fields preserve existing values
- Automatically diffs old vs. new to track changed fields
- Maintains 50-item circular audit trail (
classificationHistory)
- Recomputes completeness score after each update
- All changes validated via Zod schema before persistence
Completeness Scoring
Binary scoring across six dimensions:
- Filled: When required field is non-null (or non-empty array for operational models)
- Score:
(filledDimensions / 6) * 100
- UI Feedback:
ClassificationCompletenessCard displays progress and lists missing dimensions
Organization Switching (Multi-Tenancy)
Via TenantContext:
useCurrentTenant() provides active organization context
switchOrganization(orgId) switches tenant; emits EXTENSION_TENANT_SWITCHED event
- Cached in localStorage as
selectedOrganizationId
- Super Admin role sees all organizations; regular users see only their own
Service Contract
TenantClassificationService
Extends BaseService<TenantClassification>. Access via getTenantClassificationService() singleton.
| Service | Owns | Key Methods |
|---|
| TenantClassificationService | Classification read/update on organizations doc | getClassification(orgId), updateClassification(orgId, partial, userId, source) |
getClassification(organizationId: string) → Promise<ServiceResult<TenantClassification>>
- Fetches classification from organizations document
- Performs lazy migration if missing but loose fields exist
- Emits migration event (fire-and-forget; doesn’t block)
- Returns
{ success: true, data: TenantClassification } or { success: false, error: string }
updateClassification(organizationId, partial, userId, source) → Promise<ServiceResult<TenantClassification>>
- Loads current classification (triggers lazy migration if needed)
- Merges partial updates and diffs for audit trail
- Validates via Zod
classificationPartialSchema
- Persists to Firestore and emits
TENANT_CLASSIFICATION_UPDATED event
- Maintains up to 50 change records in history
- Returns
{ success: true, data: updated } or { success: false, error }
Error Handling: All operations wrapped in withErrorBoundary(). Non-critical operations (event emission) use executeNonCritical() to avoid blocking on failure.
Events
Emitted
| Event | Trigger | Severity | Persisted |
|---|
| TENANT_CLASSIFICATION_UPDATED | After successful classification update or lazy migration | INFO | Yes (persisted via EventBus) |
Payload (TenantClassificationUpdatedPayload):
{
fieldsChanged: string[] // Names of fields that changed
completenessScore: number // New score (0-100)
previousScore: number // Previous score
source: 'ONBOARDING' | 'SETTINGS' | 'MIGRATION' | 'AI_SUGGESTION'
isMigration: boolean // true if triggered by lazy migration
}
Correlation: organizationId in event metadata for tracing.
Consumed
This feature does not currently consume system events. Future integration points (not yet implemented):
- Organization deletion → Clean up classification history
- Subscription changes → Enforce tier-based feature limits
Dependencies
Internal
@/contexts/AuthContext — Current user for audit trail (currentUser.uid)
@/contexts/TenantContext — Organization context via useCurrentTenant()
@/core/BaseService — Base service class with error handling and EventBus integration
@/core/firebase — Firestore db reference
@/core/eventBus — System event emission
@/hooks/useZodForm — Form state, validation, toast notifications
@/features/organizations/classification.contracts — Classification types and createEmptyClassification()
@/schemas/classification.schema — Zod validation schemas
External
firebase/firestore — Document operations (getDoc, updateDoc, doc)
react, react-hook-form — React context and form state
zod — Schema validation
lucide-react — Icons in components
react-i18next — Internationalization in UI components
File Structure
src/features/organizations/
├── components/
│ ├── classification/
│ │ ├── ClassificationWizard.tsx # Main 4-step wizard (role → sector → geography → scale/legal/ops)
│ │ ├── ClassificationStep.tsx # Onboarding integration wrapper
│ │ ├── EditClassificationModal.tsx # Modal UI for editing
│ │ ├── QuickClassificationModal.tsx # Condensed modal variant
│ │ ├── ClassificationCompletenessCard.tsx # Progress badge + missing dimensions
│ │ ├── RoleSelector.tsx # Dimension 1: Role picker
│ │ ├── SectorSelector.tsx # Dimension 2: Primary + secondary sectors
│ │ ├── GeographySelector.tsx # Dimension 3: Scope + countries
│ │ ├── ScaleSelector.tsx # Dimension 4: Budget tier
│ │ ├── LegalFiscalSelector.tsx # Dimension 5: Legal form + fiscal status
│ │ ├── OperationalModelSelector.tsx # Dimension 6: Operational models
│ │ ├── SDGSelector.tsx # SDG goals picker (1-17, max 5)
│ │ ├── MissionInput.tsx # Mission statement text area
│ │ └── index.ts
│ ├── CreateOrganizationWizard/
│ │ ├── index.tsx # 4-step org creation flow
│ │ └── steps/
│ │ ├── BasicInfoStep.tsx
│ │ ├── LegalDetailsStep.tsx
│ │ ├── AddressStep.tsx
│ │ ├── SubscriptionStep.tsx
│ │ └── index.ts
│ ├── OrganizationEditModal/
│ │ ├── index.tsx # 6-tab org profile editor
│ │ ├── constants.ts
│ │ └── tabs/
│ │ ├── GeneralTab.tsx
│ │ ├── MissionTab.tsx
│ │ ├── LegalTab.tsx
│ │ ├── FinancialTab.tsx
│ │ ├── ContactTab.tsx
│ │ ├── SubscriptionTab.tsx
│ │ └── index.ts
│ ├── OrganizationCard.tsx # Org display card (legacy profile)
│ ├── OrganizationSkeleton.tsx # Loading state placeholder
│ └── index.ts
├── hooks/
│ ├── useOrganizationForm.ts # Form state for org profile edit (logo, sectors, SDGs)
│ ├── useOrganizationForm.test.ts
│ ├── useTenantClassificationForm.ts # Classification form state with partial save support
│ └── useTenantClassificationForm.test.ts
├── services/
│ ├── TenantClassificationService.ts # Main read/update service + lazy migration
│ └── TenantClassificationService.test.ts
├── utils/
│ ├── classificationUtils.ts # Pure functions: completeness, migration, diffing
│ └── classificationUtils.test.ts
├── types.ts # Local type definitions (form data, props)
├── index.ts # Public API exports
├── public.ts # External v1.0.0 API surface
├── organizations.md # This document
└── README.md # Extended reference (legacy)
Integration Points
Onboarding: ClassificationStep integrates into TenantOnboardingWizard (source: ONBOARDING)
Organization Settings: OrganizationEditModal with classification tabs; EditClassificationModal for standalone editing
Dashboard: ClassificationCompletenessCard displays progress at-a-glance with missing dimensions
Multi-Tenancy: TenantContext manages organization switching and organization list for Super Admin and regular users